Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ Working with CSV files shouldn’t be a chore. With CSV, you get:
- **Interactive Editing:** Two modes — type to start a quick edit, or press Enter/double‑click for detail edit. Changes commit to the document when focus leaves the cell or CSV view; unsaved changes remain visible when you switch tabs.
- **Smart Resizing:** Automatic calculation of column widths for improved readability.
- **Dynamic Color Coding:** Visual cues based on data type help you quickly identify numbers, dates, booleans, and more (with opt‑in cool palette and theme-foreground mode settings).
- **Sticky Headers:** Keep column titles in view as you scroll through large datasets.
- **Serial Index Column:** Right-click a serial index cell or its top-left corner and choose **Hide serial index column** for that file. Restore it with **CSV: Toggle Serial Index Column** in the Command Palette.
- **Sticky Headers:** Keep column titles in view as you scroll through large datasets. Right-click a cell in the first visible row and choose **Use first row as header** or **Treat first row as data** to toggle the header for that file. With leading metadata rows hidden, this applies to the first visible row; the CSV contents stay unchanged.
- **Enhanced Keyboard Navigation:** Navigate cells with arrows and Tab/Shift+Tab; quick edits can commit with arrow keys; `Ctrl/Cmd + A` selects all; `Ctrl/Cmd + C` copies selection.
- **Advanced Multi-Cell Selection:** Easily select and copy blocks of data, then paste them elsewhere as properly formatted CSV.
- **Add/Delete Columns:** Right-click any cell to add a column left or right, or remove the selected column.
- **Add/Delete Rows:** Insert above/below or remove the selected row via context menu.
- **Add/Delete Rows:** Insert above/below or remove the selected row via context menu. Menus open toward available space and remain scrollable in small editor panes.
- **Edit Empty CSVs:** Create or open an empty CSV file and start typing immediately.
- **Column Sorting:** Right-click a header and choose A–Z or Z–A.
- **Custom Font Controls:** Choose a font family and optional font-size override, or inherit VS Code defaults.
Expand Down
Binary file added images/context-menu-edge.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/header-menu-later.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/header-menu-off.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/header-menu-on.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/serial-index-menu.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 54 additions & 9 deletions media/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ const setZoomScale = (nextScale, persist = true) => {
return false;
}
zoomScale = normalized;
const menu = document.getElementById('contextMenu');
if (menu) { menu.style.display = 'none'; }
document.body.style.fontSize = `${Math.max(1, BASE_FONT_SIZE_PX * zoomScale)}px`;
applySizeStateToRenderedCells();
if (persist) {
Expand Down Expand Up @@ -178,14 +180,14 @@ const restoreState = () => {
}
}
if (typeof st.anchorRow === 'number' && typeof st.anchorCol === 'number') {
const tag = (hasHeader && st.anchorRow === 0 ? 'th' : 'td');
let sel = table.querySelector(`${tag}[data-row="${st.anchorRow}"][data-col="${st.anchorCol}"]`);
const selector = `td[data-row="${st.anchorRow}"][data-col="${st.anchorCol}"], th[data-row="${st.anchorRow}"][data-col="${st.anchorCol}"]`;
let sel = table.querySelector(selector);
// If not present yet (due to chunking), load chunks until available or exhausted
if (!sel && typeof window.__csvLoadNextChunk === 'function') {
let guard = 100; // prevent infinite loops
while (!sel && typeof window.__csvLoadNextChunk === 'function' && guard-- > 0) {
if (!window.__csvLoadNextChunk()) break;
sel = table.querySelector(`${tag}[data-row="${st.anchorRow}"][data-col="${st.anchorCol}"]`);
sel = table.querySelector(selector);
}
}
applySizeStateToRenderedCells();
Expand Down Expand Up @@ -367,6 +369,7 @@ const handleZoomWheel = e => {
window.addEventListener('wheel', handleZoomWheel, { passive: false });

const hasHeader = document.querySelector('thead') !== null;
const headerEnabled = root?.dataset?.headerenabled === '1';
const getCellCoords = cell => ({ row: parseInt(cell.getAttribute('data-row')), col: parseInt(cell.getAttribute('data-col')) });
const clearSelection = () => { currentSelection.forEach(c => c.classList.remove('selected')); currentSelection = []; };
const contextMenu = document.getElementById('contextMenu');
Expand All @@ -386,6 +389,32 @@ const showContextMenu = (x, y, row, col) => {
d.style.margin = '1px 0';
contextMenu.appendChild(d);
};
if (col === -1) {
item('Hide serial index column', () => {
const anchor = anchorCell ? getCellCoords(anchorCell) : null;
if (!anchor || !Number.isFinite(anchor.col) || anchor.col < 0) {
const row = anchor && Number.isFinite(anchor.row) ? anchor.row : Number(root?.dataset?.firstrow || '0');
const surviving = table.querySelector(`td[data-row="${row}"][data-col="0"], th[data-row="${row}"][data-col="0"]`);
if (surviving) {
clearSelection();
surviving.classList.add('selected');
currentSelection.push(surviving);
anchorCell = surviving;
rangeEndCell = surviving;
}
}
persistState();
vscode.postMessage({ type: 'setSerialIndex', enabled: false });
});
divider();
}
if (row === Number(root?.dataset?.firstrow)) {
item(headerEnabled ? 'Treat first row as data' : 'Use first row as header', () => {
persistState();
vscode.postMessage({ type: 'setHeader', enabled: !headerEnabled });
});
divider();
}
item('Cut', () => copySelectionToClipboard(true));
item('Copy', () => copySelectionToClipboard());
item('Paste', () => pasteFromClipboard());
Expand Down Expand Up @@ -460,10 +489,25 @@ const showContextMenu = (x, y, row, col) => {
});
}

contextMenu.style.left = x + 'px';
contextMenu.style.top = y + 'px';
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const marginX = Math.min(8, viewportWidth / 4);
const marginY = Math.min(8, viewportHeight / 4);
contextMenu.style.maxWidth = `${Math.max(0, viewportWidth - 2 * marginX)}px`;
contextMenu.style.maxHeight = `${Math.max(0, viewportHeight - 2 * marginY)}px`;
// Measure all actions at their natural width, bounded by the viewport, before placing.
contextMenu.style.left = `${marginX}px`;
contextMenu.style.top = `${marginY}px`;
contextMenu.style.display = 'block';
contextMenu.scrollTop = 0;
contextMenu.scrollLeft = 0;
const { width, height } = contextMenu.getBoundingClientRect();
const left = x + width <= viewportWidth - marginX ? x : x - width;
const top = y + height <= viewportHeight - marginY ? y : y - height;
contextMenu.style.left = `${clamp(left, marginX, Math.max(marginX, viewportWidth - marginX - width))}px`;
contextMenu.style.top = `${clamp(top, marginY, Math.max(marginY, viewportHeight - marginY - height))}px`;
};
window.addEventListener('resize', () => { contextMenu.style.display = 'none'; });

const getElementTarget = target => {
if (target instanceof Element) return target;
Expand Down Expand Up @@ -753,12 +797,13 @@ table.addEventListener('contextmenu', e => {
if (!target || editingCell) return;
const colAttr = target.getAttribute('data-col');
const rowAttr = target.getAttribute('data-row');
const col = parseInt(colAttr);
const serialCorner = target.hasAttribute('data-serial-index');
const col = serialCorner ? -1 : parseInt(colAttr);
const row = parseInt(rowAttr);
if ((isNaN(col) || col === -1) && (isNaN(row) || row === -1)) return;
if (!serialCorner && (isNaN(col) || col === -1) && (isNaN(row) || row === -1)) return;
e.preventDefault();
lastContextIsHeader = target.tagName === 'TH';
showContextMenu(e.pageX, e.pageY, row, col);
lastContextIsHeader = target.tagName === 'TH' && !serialCorner;
showContextMenu(e.clientX, e.clientY, row, col);
});

table.addEventListener('mousedown', e => {
Expand Down
34 changes: 30 additions & 4 deletions src/CsvEditorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,28 @@
case 'save':
await this.handleSave();
break;
case 'setHeader': {
if (typeof e.enabled === 'boolean') {
const uri = this.document.uri;
await CsvEditorProvider.setHeaderForUri(this.context, uri, e.enabled);
CsvEditorProvider.editors
.filter(editor => editor.getDocumentUri().toString() === uri.toString())
.forEach(editor => editor.refresh());
}
break;
}
case 'setSerialIndex': {
if (typeof e.enabled === 'boolean') {
const uri = this.getDocumentUri();
if (CsvEditorProvider.getSerialIndexForUri(this.context, uri) !== e.enabled) {
await CsvEditorProvider.setSerialIndexForUri(this.context, uri, e.enabled);
CsvEditorProvider.editors
.filter(editor => editor.getDocumentUri().toString() === uri.toString())
.forEach(editor => editor.refresh());
}
}
break;
}
case 'copyCells':
await this.copyCells(e.matrix, e.startRow, e.startCol, e.cut === true);
break;
Expand Down Expand Up @@ -332,7 +354,7 @@
}

private forceReload() {
if (!this.currentWebviewPanel) return;

Check warning on line 357 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'if' condition
const panel = this.currentWebviewPanel;
// First, blank the DOM to ensure a full script/style reinit on next set
panel.webview.html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body></body></html>';
Expand Down Expand Up @@ -850,7 +872,7 @@
}

private buildFindRegex(query: string, options: { regex: boolean; wholeWord: boolean; matchCase: boolean }): RegExp | undefined {
if (!query) return undefined;

Check warning on line 875 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'if' condition
const useRegex = !!options.regex;
const wholeWord = !!options.wholeWord;
const matchCase = !!options.matchCase;
Expand Down Expand Up @@ -942,17 +964,17 @@
}
data[row][col] = '';
} else {
while (data.length <= row) data.push([]);

Check warning on line 967 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'while' condition
while (data[row].length <= col) data[row].push('');

Check warning on line 968 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'while' condition
data[row][col] = value;
}

let trimmed = false;
if (wasEditingLastRow) {
const isRowEmpty = (arr: string[] | undefined) => {
if (!arr || arr.length === 0) return true;

Check warning on line 975 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'if' condition
for (let i = 0; i < arr.length; i++) {
if ((arr[i] ?? '') !== '') return false;

Check warning on line 977 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'if' condition
}
return true;
};
Expand Down Expand Up @@ -990,7 +1012,7 @@
const data = result.data as string[][];
for (const row of data) {
if (index > row.length) {
while (row.length < index) row.push('');

Check warning on line 1015 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'while' condition
}
row.splice(index, 0, '');
}
Expand All @@ -1008,7 +1030,7 @@
}

private async insertColumns(index: number, count: number) {
if (count <= 0) return;

Check warning on line 1033 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'if' condition
this.isUpdatingDocument = true;
const separator = this.getSeparator();
const text = this.document.getText();
Expand All @@ -1017,7 +1039,7 @@
for (let k = 0; k < count; k++) {
for (const row of data) {
if (index > row.length) {
while (row.length < index) row.push('');

Check warning on line 1042 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'while' condition
}
row.splice(index, 0, '');
}
Expand Down Expand Up @@ -1060,7 +1082,7 @@
}

private async deleteColumns(indices: number[]) {
if (!indices || !indices.length) return;

Check warning on line 1085 in src/CsvEditorProvider.ts

View workflow job for this annotation

GitHub Actions / build

Expected { after 'if' condition
this.isUpdatingDocument = true;
const separator = this.getSeparator();
const text = this.document.getText();
Expand Down Expand Up @@ -1451,6 +1473,8 @@
extraColumnColorCss: colorCss,
nextChunkStart,
hasRemoteChunks,
headerEnabled: treatHeader,
firstVisibleRow: Math.min(Math.max(0, hiddenRows), data.length),
mouseWheelZoomEnabled,
mouseWheelZoomInvert,
realRowCount: data.length
Expand Down Expand Up @@ -1564,7 +1588,7 @@
if (headerFlag) {
tableHtml += `<thead><tr>${
addSerialIndex
? `<th tabindex="0" style="min-width: ${serialIndexWidthCh}ch; max-width: ${serialIndexWidthCh}ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; background-color: ${isDark ? '#1e1e1e' : '#ffffff'}; color: #888;"></th>`
? `<th tabindex="0" data-serial-index="1" style="min-width: ${serialIndexWidthCh}ch; max-width: ${serialIndexWidthCh}ch; border: 1px solid ${isDark ? '#555' : '#ccc'}; background-color: ${isDark ? '#1e1e1e' : '#ffffff'}; color: #888;"></th>`
: ''
}`;
for (let i = 0; i < numColumns; i++) {
Expand Down Expand Up @@ -1704,11 +1728,13 @@
extraColumnColorCss: string;
nextChunkStart: number;
hasRemoteChunks: boolean;
headerEnabled: boolean;
firstVisibleRow: number;
mouseWheelZoomEnabled: boolean;
mouseWheelZoomInvert: boolean;
realRowCount: number;
}): string {
const { webview, nonce, fontFamily, fontSize, cellPadding, separator, tableHtml, chunksJson, extraColumnColorCss, nextChunkStart, hasRemoteChunks, mouseWheelZoomEnabled, mouseWheelZoomInvert, realRowCount } = args;
const { webview, nonce, fontFamily, fontSize, cellPadding, separator, tableHtml, chunksJson, extraColumnColorCss, nextChunkStart, hasRemoteChunks, headerEnabled, firstVisibleRow, mouseWheelZoomEnabled, mouseWheelZoomInvert, realRowCount } = args;
const isDark = vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark;
// Build script URI using file path for compatibility (older APIs may lack Uri.joinPath)
const scriptUri = webview.asWebviewUri(
Expand Down Expand Up @@ -1911,7 +1937,7 @@
font-size: inherit;
}
#findReplaceWidget .fr-overflow-item:hover { background: rgba(255,255,255,0.05); }
#contextMenu { position: absolute; display: none; background: ${isDark ? '#2d2d2d' : '#ffffff'}; border: 1px solid ${isDark ? '#555' : '#ccc'}; z-index: 10000; font-family: ${this.escapeCss(fontFamily)}; font-size: inherit; }
#contextMenu { position: fixed; display: none; width: max-content; box-sizing: border-box; overflow: auto; overflow-wrap: anywhere; background: ${isDark ? '#2d2d2d' : '#ffffff'}; border: 1px solid ${isDark ? '#555' : '#ccc'}; z-index: 10000; font-family: ${this.escapeCss(fontFamily)}; font-size: inherit; }
#contextMenu div { padding: 4px 12px; cursor: pointer; }
#contextMenu div:hover { background: ${isDark ? '#3d3d3d' : '#eeeeee'}; }

Expand All @@ -1920,7 +1946,7 @@
</style>
</head>
<body>
<div id="csv-root" data-realrowcount="${realRowCount}" class="table-container" data-sepcode="${sepCode}" data-fontsize="${fontSize}" data-wheelzoomenabled="${mouseWheelZoomEnabled ? '1' : '0'}" data-wheelzoominvert="${mouseWheelZoomInvert ? '1' : '0'}" data-nextchunkstart="${nextChunkStart >= 0 ? nextChunkStart : ''}" data-hasmorechunks="${hasRemoteChunks ? '1' : '0'}">
<div id="csv-root" data-realrowcount="${realRowCount}" class="table-container" data-firstrow="${firstVisibleRow}" data-headerenabled="${headerEnabled ? '1' : '0'}" data-sepcode="${sepCode}" data-fontsize="${fontSize}" data-wheelzoomenabled="${mouseWheelZoomEnabled ? '1' : '0'}" data-wheelzoominvert="${mouseWheelZoomInvert ? '1' : '0'}" data-nextchunkstart="${nextChunkStart >= 0 ? nextChunkStart : ''}" data-hasmorechunks="${hasRemoteChunks ? '1' : '0'}">
${tableHtml}
</div>

Expand Down
Loading
Loading