Skip to content

Commit 9ac9f70

Browse files
authored
Add column insertion and deletion (#31)
* feat: add column insertion and deletion * Fix context menu and allow column insertion from any cell * Add debug logging for context menu * Undo bad changes. * Add row insertion and deletion via context menu * Undo more bad changes * Working add/delete
1 parent b0e0228 commit 9ac9f70

File tree

2 files changed

+170
-1
lines changed

2 files changed

+170
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Working with CSV files shouldn’t be a chore. With CSV, you get:
2525
- **Sticky Headers:** Keep column titles in view as you scroll through large datasets.
2626
- **Enhanced Keyboard Navigation:** Navigate cells with Tab/Shift+Tab and use keyboard shortcuts for quick editing, saving, and selection.
2727
- **Advanced Multi-Cell Selection:** Easily select and copy blocks of data, then paste them elsewhere as properly formatted CSV.
28+
- **Add/Delete Columns:** Right-click any cell to add a column left or right, or remove the selected column.
2829
- **Find & Highlight:** Built-in find widget helps you search for text within your CSV with real-time highlighting and navigation through matches.
2930
- **Preserved CSV Integrity:** All modifications respect CSV formatting—no unwanted extra characters or formatting issues.
3031
- **Optimized for Performance:** Designed for medium-sized datasets, ensuring a smooth editing experience without compromising on functionality.
@@ -62,7 +63,7 @@ Working with CSV files shouldn’t be a chore. With CSV, you get:
6263

6364
## Planned Improvements
6465

65-
- **Row and Column Insertion/Deletion:** Quickly add or remove rows or columns without leaving the editor. Track progress on the [issue tracker](https://github.com/jonaraphael/csv/issues).
66+
- **Row Insertion/Deletion:** Quickly add or remove rows without leaving the editor.
6667

6768
---
6869

src/extension.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,19 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
103103
await vscode.env.clipboard.writeText(e.text);
104104
console.log('CSV: Copied to clipboard');
105105
break;
106+
case 'insertColumn':
107+
await this.insertColumn(e.index);
108+
break;
109+
case 'deleteColumn':
110+
await this.deleteColumn(e.index);
111+
break;
112+
/* ──────── NEW ──────── */
113+
case 'insertRow':
114+
await this.insertRow(e.index);
115+
break;
116+
case 'deleteRow':
117+
await this.deleteRow(e.index);
118+
break;
106119
}
107120
});
108121

@@ -199,6 +212,104 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
199212
}
200213
}
201214

215+
/**
216+
* Inserts a new empty column at the specified index for all rows.
217+
*/
218+
private async insertColumn(index: number) {
219+
this.isUpdatingDocument = true;
220+
const config = vscode.workspace.getConfiguration('csv');
221+
const separator = config.get<string>('separator', ',');
222+
const text = this.document.getText();
223+
const result = Papa.parse(text, { dynamicTyping: false, delimiter: separator });
224+
const data = result.data as string[][];
225+
for (const row of data) {
226+
if (index > row.length) {
227+
while (row.length < index) row.push('');
228+
}
229+
row.splice(index, 0, '');
230+
}
231+
const newText = Papa.unparse(data, { delimiter: separator });
232+
const fullRange = new vscode.Range(0, 0, this.document.lineCount, this.document.lineCount ? this.document.lineAt(this.document.lineCount - 1).text.length : 0);
233+
const edit = new vscode.WorkspaceEdit();
234+
edit.replace(this.document.uri, fullRange, newText);
235+
await vscode.workspace.applyEdit(edit);
236+
this.isUpdatingDocument = false;
237+
this.updateWebviewContent();
238+
}
239+
240+
/**
241+
* Deletes the column at the specified index from all rows.
242+
*/
243+
private async deleteColumn(index: number) {
244+
this.isUpdatingDocument = true;
245+
const config = vscode.workspace.getConfiguration('csv');
246+
const separator = config.get<string>('separator', ',');
247+
const text = this.document.getText();
248+
const result = Papa.parse(text, { dynamicTyping: false, delimiter: separator });
249+
const data = result.data as string[][];
250+
for (const row of data) {
251+
if (index < row.length) {
252+
row.splice(index, 1);
253+
}
254+
}
255+
const newText = Papa.unparse(data, { delimiter: separator });
256+
const fullRange = new vscode.Range(0, 0, this.document.lineCount, this.document.lineCount ? this.document.lineAt(this.document.lineCount - 1).text.length : 0);
257+
const edit = new vscode.WorkspaceEdit();
258+
edit.replace(this.document.uri, fullRange, newText);
259+
await vscode.workspace.applyEdit(edit);
260+
this.isUpdatingDocument = false;
261+
this.updateWebviewContent();
262+
}
263+
264+
/* ───────────── NEW ROW METHODS ───────────── */
265+
266+
/**
267+
* Inserts a new empty row at the specified index.
268+
*/
269+
private async insertRow(index: number) {
270+
this.isUpdatingDocument = true;
271+
const config = vscode.workspace.getConfiguration('csv');
272+
const separator = config.get<string>('separator', ',');
273+
const text = this.document.getText();
274+
const result = Papa.parse(text, { dynamicTyping: false, delimiter: separator });
275+
const data = result.data as string[][];
276+
const numColumns = Math.max(...data.map(r => r.length), 0);
277+
const newRow = Array(numColumns).fill('');
278+
if (index > data.length) {
279+
while (data.length < index) data.push(Array(numColumns).fill(''));
280+
}
281+
data.splice(index, 0, newRow);
282+
const newText = Papa.unparse(data, { delimiter: separator });
283+
const fullRange = new vscode.Range(0, 0, this.document.lineCount, this.document.lineCount ? this.document.lineAt(this.document.lineCount - 1).text.length : 0);
284+
const edit = new vscode.WorkspaceEdit();
285+
edit.replace(this.document.uri, fullRange, newText);
286+
await vscode.workspace.applyEdit(edit);
287+
this.isUpdatingDocument = false;
288+
this.updateWebviewContent();
289+
}
290+
291+
/**
292+
* Deletes the row at the specified index.
293+
*/
294+
private async deleteRow(index: number) {
295+
this.isUpdatingDocument = true;
296+
const config = vscode.workspace.getConfiguration('csv');
297+
const separator = config.get<string>('separator', ',');
298+
const text = this.document.getText();
299+
const result = Papa.parse(text, { dynamicTyping: false, delimiter: separator });
300+
const data = result.data as string[][];
301+
if (index < data.length) {
302+
data.splice(index, 1);
303+
}
304+
const newText = Papa.unparse(data, { delimiter: separator });
305+
const fullRange = new vscode.Range(0, 0, this.document.lineCount, this.document.lineCount ? this.document.lineAt(this.document.lineCount - 1).text.length : 0);
306+
const edit = new vscode.WorkspaceEdit();
307+
edit.replace(this.document.uri, fullRange, newText);
308+
await vscode.workspace.applyEdit(edit);
309+
this.isUpdatingDocument = false;
310+
this.updateWebviewContent();
311+
}
312+
202313
// ───────────── Webview Rendering Methods ─────────────
203314

204315
/**
@@ -352,6 +463,9 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
352463
cursor: pointer;
353464
}
354465
#findWidget button:hover { background: #005f9e; }
466+
#contextMenu { position: absolute; display: none; background: ${isDark ? '#2d2d2d' : '#ffffff'}; border: 1px solid ${isDark ? '#555' : '#ccc'}; z-index: 10000; font-family: ${fontFamily}; }
467+
#contextMenu div { padding: 4px 12px; cursor: pointer; }
468+
#contextMenu div:hover { background: ${isDark ? '#3d3d3d' : '#eeeeee'}; }
355469
</style>
356470
</head>
357471
<body>
@@ -361,6 +475,7 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
361475
<span id="findStatus"></span>
362476
<button id="findClose">✕</button>
363477
</div>
478+
<div id="contextMenu"></div>
364479
<script nonce="${nonce}">
365480
document.body.setAttribute('tabindex', '0'); document.body.focus();
366481
const vscode = acquireVsCodeApi();
@@ -371,6 +486,59 @@ class CsvEditorProvider implements vscode.CustomTextEditorProvider {
371486
const hasHeader = document.querySelector('thead') !== null;
372487
const getCellCoords = cell => ({ row: parseInt(cell.getAttribute('data-row')), col: parseInt(cell.getAttribute('data-col')) });
373488
const clearSelection = () => { currentSelection.forEach(c => c.classList.remove('selected')); currentSelection = []; };
489+
const contextMenu = document.getElementById('contextMenu');
490+
491+
/* ──────── UPDATED showContextMenu ──────── */
492+
const showContextMenu = (x, y, row, col) => {
493+
contextMenu.innerHTML = '';
494+
const item = (label, cb) => {
495+
const d = document.createElement('div');
496+
d.textContent = label;
497+
d.addEventListener('click', () => { cb(); contextMenu.style.display = 'none'; });
498+
contextMenu.appendChild(d);
499+
};
500+
const divider = () => {
501+
const d = document.createElement('div');
502+
d.style.borderTop = '1px solid #888';
503+
d.style.margin = '1px 0';
504+
contextMenu.appendChild(d);
505+
};
506+
let addedRowItems = false;
507+
/* Row section */
508+
if (!isNaN(row) && row >= 0) {
509+
item('Add ROW: above', () => vscode.postMessage({ type: 'insertRow', index: row }));
510+
item('Add ROW: below', () => vscode.postMessage({ type: 'insertRow', index: row + 1 }));
511+
item('Delete ROW', () => vscode.postMessage({ type: 'deleteRow', index: row }));
512+
addedRowItems = true;
513+
}
514+
515+
/* Column section, preceded by divider if row items exist */
516+
if (!isNaN(col) && col >= 0) {
517+
if (addedRowItems) divider();
518+
item('Add COLUMN: left', () => vscode.postMessage({ type: 'insertColumn', index: col }));
519+
item('Add COLUMN: right', () => vscode.postMessage({ type: 'insertColumn', index: col + 1 }));
520+
item('Delete COLUMN', () => vscode.postMessage({ type: 'deleteColumn', index: col }));
521+
}
522+
contextMenu.style.left = x + 'px';
523+
contextMenu.style.top = y + 'px';
524+
contextMenu.style.display = 'block';
525+
};
526+
527+
document.addEventListener('click', () => { contextMenu.style.display = 'none'; });
528+
529+
/* ──────── UPDATED contextmenu listener ──────── */
530+
table.addEventListener('contextmenu', e => {
531+
const target = e.target;
532+
if(target.tagName !== 'TH' && target.tagName !== 'TD') return;
533+
const colAttr = target.getAttribute('data-col');
534+
const rowAttr = target.getAttribute('data-row');
535+
const col = parseInt(colAttr);
536+
const row = parseInt(rowAttr);
537+
if ((isNaN(col) || col === -1) && (isNaN(row) || row === -1)) return;
538+
e.preventDefault();
539+
showContextMenu(e.pageX, e.pageY, row, col);
540+
});
541+
374542
table.addEventListener('mousedown', e => {
375543
if(e.target.tagName !== 'TD' && e.target.tagName !== 'TH') return;
376544
if(editingCell){ if(e.target !== editingCell) editingCell.blur(); else return; } else clearSelection();

0 commit comments

Comments
 (0)