@@ -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