Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ Working with CSV files shouldn’t be a chore. With CSV, you get:
- **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.
- **Drag to Reorder Columns:** Drag a column header to move it; an insertion line previews where the column will be placed.
- **Add/Delete Rows:** Insert above/below or remove the selected row via context menu.
- **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.
- **Chained Column Sorting:** Click headers to build an ordered multi-column sort, like pandas `sort_values(by=[...])`. Each header cycles through ascending, descending, and removal from the chain; its arrow and number show direction and priority. Empty values remain last, and clearing the chain restores the pre-sort row order. Double-click still edits the column name.
- **Custom Font Controls:** Choose a font family and optional font-size override, or inherit VS Code defaults.
- **In-View Zoom:** Use `Ctrl/Cmd + Mouse Wheel` or `Ctrl/Cmd + +/-/0` to zoom the CSV view without changing global editor zoom.
- **Find & Replace Overlay:** Built-in find/replace bar with match options (case, whole-word, regex), keyboard navigation, and single/all replace actions across the full file (including chunked rows).
Expand Down
132 changes: 125 additions & 7 deletions media/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ const DRAG_THRESHOLD_PX = 4;
const RESIZE_HANDLE_PX = 6;
let resizeState = null;
let reorderState = null;
let pendingHeaderSortTimer = null;
let pendingHeaderSortColumn = null;
let pendingHeaderSortInitial = null;

const table = document.querySelector('#csv-root table');
const scrollContainer = document.querySelector('.table-container');
Expand Down Expand Up @@ -371,6 +374,103 @@ const hasHeader = document.querySelector('thead') !== null;
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');
const getColumnSorts = () => {
const st = vscode.getState() || {};
const raw = Array.isArray(st.columnSorts)
? st.columnSorts
: (Number.isInteger(st.sortColumn) && typeof st.sortAscending === 'boolean'
? [{ index: st.sortColumn, ascending: st.sortAscending }]
: []);
const seen = new Set();
return raw.filter(sort => {
if (!sort || !Number.isInteger(sort.index) || sort.index < 0 || typeof sort.ascending !== 'boolean' || seen.has(sort.index)) {
return false;
}
seen.add(sort.index);
return true;
}).map(sort => ({ index: sort.index, ascending: sort.ascending }));
};
const setColumnSorts = sorts => {
const st = vscode.getState() || {};
vscode.setState({ ...st, columnSorts: sorts, sortColumn: undefined, sortAscending: undefined });
};
const renderSortIndicator = () => {
table.querySelectorAll('.sort-indicator').forEach(indicator => indicator.remove());
table.querySelectorAll('thead th[data-col]').forEach(header => {
header.removeAttribute('aria-sort');
header.removeAttribute('data-sort-priority');
});
getColumnSorts().forEach((sort, priority) => {
const header = table.querySelector(`thead th[data-col="${sort.index}"]`);
if (!header) return;
header.setAttribute('aria-sort', sort.ascending ? 'ascending' : 'descending');
header.setAttribute('data-sort-priority', String(priority + 1));
});
};
const applyColumnSorts = sorts => {
setColumnSorts(sorts);
renderSortIndicator();
vscode.postMessage({ type: 'sortColumns', sorts });
};
const requestColumnSort = (col, ascending) => {
cancelPendingHeaderSort();
const sorts = getColumnSorts();
const existing = sorts.find(sort => sort.index === col);
if (existing) {
existing.ascending = ascending;
} else {
sorts.push({ index: col, ascending });
}
applyColumnSorts(sorts);
};
const clearColumnSortIndicator = () => {
setColumnSorts([]);
renderSortIndicator();
};
const cancelPendingHeaderSort = (restoreInitial = false) => {
if (pendingHeaderSortTimer === null) return;
clearTimeout(pendingHeaderSortTimer);
if (restoreInitial && pendingHeaderSortInitial) {
setColumnSorts(pendingHeaderSortInitial);
renderSortIndicator();
}
pendingHeaderSortTimer = null;
pendingHeaderSortColumn = null;
pendingHeaderSortInitial = null;
};
const cycleColumnSortState = col => {
const sorts = getColumnSorts();
const existing = sorts.find(sort => sort.index === col);
if (!existing) {
sorts.push({ index: col, ascending: true });
} else if (existing.ascending) {
existing.ascending = false;
} else {
sorts.splice(sorts.indexOf(existing), 1);
}
setColumnSorts(sorts);
renderSortIndicator();
};
const scheduleHeaderSort = col => {
if (pendingHeaderSortTimer !== null && pendingHeaderSortColumn === col) {
clearTimeout(pendingHeaderSortTimer);
} else {
if (pendingHeaderSortTimer !== null) {
clearTimeout(pendingHeaderSortTimer);
}
pendingHeaderSortInitial = getColumnSorts();
pendingHeaderSortColumn = col;
cycleColumnSortState(col);
}
pendingHeaderSortTimer = setTimeout(() => {
const sorts = getColumnSorts();
pendingHeaderSortTimer = null;
pendingHeaderSortColumn = null;
pendingHeaderSortInitial = null;
vscode.postMessage({ type: 'sortColumns', sorts });
}, 250);
};
renderSortIndicator();

/* ──────── UPDATED showContextMenu ──────── */
const showContextMenu = (x, y, row, col) => {
Expand Down Expand Up @@ -401,9 +501,9 @@ const showContextMenu = (x, y, row, col) => {
/* Header-only: SORT functionality */
if (lastContextIsHeader) {
item('Sort: A-Z', () =>
vscode.postMessage({ type: 'sortColumn', index: col, ascending: true }));
requestColumnSort(col, true));
item('Sort: Z-A', () =>
vscode.postMessage({ type: 'sortColumn', index: col, ascending: false }));
requestColumnSort(col, false));
}

/* Row section */
Expand Down Expand Up @@ -629,17 +729,24 @@ const startResizeDrag = (target, e) => {
};
const startReorderDrag = (target, e) => {
if (e.button !== 0) return false;
if (isColumnHeaderCell(target) && target.classList.contains('selected')) {
if (isColumnHeaderCell(target)) {
const col = parseInt(target.getAttribute('data-col') || 'NaN', 10);
if (Number.isNaN(col)) return false;
const selected = getSelectedColumnIds();
const indices = selected.includes(col) ? selected : [col];
let indices = getSelectedColumnIds();
if (!target.classList.contains('selected')) {
selectFullColumnRange(col, col);
anchorCell = target;
rangeEndCell = target;
indices = [col];
}
if (!indices.includes(col)) indices = [col];
reorderState = {
axis: 'column',
indices,
startX: e.clientX,
startY: e.clientY,
active: false,
sourceIndex: col,
beforeIndex: null
};
return true;
Expand Down Expand Up @@ -684,6 +791,7 @@ const onGlobalDragMove = e => {
e.preventDefault();

if (reorderState.axis === 'column') {
table.style.cursor = 'grabbing';
const target = getColumnDropTarget(e.clientX);
if (!target) return;
reorderState.beforeIndex = target.beforeIndex;
Expand All @@ -703,13 +811,22 @@ const onGlobalDragEnd = () => {
}
if (!reorderState) return;

const { axis, indices, active, beforeIndex } = reorderState;
const { axis, indices, active, sourceIndex, beforeIndex } = reorderState;
reorderState = null;
hideDragIndicator();
table.style.cursor = '';

if (!active || !Number.isFinite(beforeIndex)) return;
if (!active) {
if (axis === 'column') {
scheduleHeaderSort(sourceIndex);
}
persistState();
return;
}
if (!Number.isFinite(beforeIndex)) return;
if (axis === 'column') {
cancelPendingHeaderSort();
clearColumnSortIndicator();
vscode.postMessage({ type: 'reorderColumns', indices, beforeIndex });
} else {
vscode.postMessage({ type: 'reorderRows', indices, beforeIndex });
Expand Down Expand Up @@ -1989,6 +2106,7 @@ const editCell = (cell, event, mode = 'detail') => {
};

table.addEventListener('dblclick', e => {
cancelPendingHeaderSort(true);
const edgeTarget = getCellTarget(e.target);
const edge = getResizeEdgeInfo(edgeTarget, e);
if (edge) {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/jonaraphael/csv.git"
},
"version": "1.3.0",
"version": "1.3.1",
"engines": {
"vscode": "^1.70.0",
"node": ">=14"
Expand Down
Loading