Skip to content

Commit f7cb392

Browse files
committed
Add grid-aware paste in selection mode with single-step undo/redo
1 parent b61b3ff commit f7cb392

File tree

5 files changed

+418
-0
lines changed

5 files changed

+418
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Working with CSV files shouldn’t be a chore. With CSV, you get:
1919
- **Smart Column Sizing & Dynamic Color Coding:** Columns automatically adjust to fit content while being visually distinguished by data type. Whether it’s boolean, date, integer, float, or text, each column gets its own adaptive color that adjusts for light and dark themes.
2020
- **Sticky Headers & Fluid Navigation:** Keep your header row always visible as you scroll. Effortlessly move through cells using intuitive keyboard shortcuts like `Tab`, `Shift + Tab`, and arrow keys—just like a full-featured spreadsheet.
2121
- **Efficient Multi-Cell Selection & Clipboard Integration:** Select a range of cells with click-and-drag and copy them as well-formatted CSV data using `Ctrl/Cmd + C`.
22+
- **Grid Paste Support:** Paste copied ranges directly into the selected cell/range with `Ctrl/Cmd + V` in selection mode.
2223
- **Robust Data Handling:** Leveraging the power of [Papa Parse](https://www.papaparse.com/), the extension handles complex CSV structures, special characters, and various data types gracefully.
2324
- **Theme-Optimized Interface:** Whether you prefer light or dark mode, CSV automatically adapts its styles for an optimal viewing experience.
2425

@@ -130,6 +131,7 @@ Per-file (stored by the extension; set via commands):
130131
- Exit/save: click outside the cell or move focus to commit changes.
131132
- Global:
132133
- Copy selection: `Ctrl/Cmd + C`
134+
- Paste selection: `Ctrl/Cmd + V` (selection mode). Pasting a single value into a selected rectangle fills that rectangle.
133135
- Find: `Ctrl/Cmd + F`
134136
- Replace: `Ctrl/Cmd + H`
135137
- Next/Previous match: `F3` / `Shift + F3` (also `Enter` / `Shift + Enter` in the Find box)

media/main.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,41 @@ const selectFullRowRange = (row1, row2) => {
849849
});
850850
};
851851

852+
const getDataCellCoords = cell => {
853+
if (!cell || typeof cell.getAttribute !== 'function') return null;
854+
const coords = getCellCoords(cell);
855+
if (!coords || !Number.isInteger(coords.row) || !Number.isInteger(coords.col)) return null;
856+
if (coords.row < 0 || coords.col < 0) return null;
857+
return coords;
858+
};
859+
860+
const getDataSelectionBounds = () => {
861+
const coords = currentSelection
862+
.map(cell => getDataCellCoords(cell))
863+
.filter(Boolean);
864+
if (!coords.length) return null;
865+
const keys = new Set(coords.map(c => `${c.row}:${c.col}`));
866+
const rows = coords.map(c => c.row);
867+
const cols = coords.map(c => c.col);
868+
const minRow = Math.min(...rows);
869+
const maxRow = Math.max(...rows);
870+
const minCol = Math.min(...cols);
871+
const maxCol = Math.max(...cols);
872+
const expectedCount = (maxRow - minRow + 1) * (maxCol - minCol + 1);
873+
const rectangular = keys.size === expectedCount;
874+
return { minRow, maxRow, minCol, maxCol, rectangular };
875+
};
876+
877+
const getPasteAnchorCoords = () => {
878+
const anchor = getDataCellCoords(anchorCell);
879+
if (anchor) return anchor;
880+
const fromActive = getDataCellCoords(getCellTarget(document.activeElement));
881+
if (fromActive) return fromActive;
882+
const bounds = getDataSelectionBounds();
883+
if (bounds) return { row: bounds.minRow, col: bounds.minCol };
884+
return null;
885+
};
886+
852887
const findReplaceWidget = document.getElementById('findReplaceWidget');
853888
const replaceToggleGutter = document.getElementById('replaceToggleGutter');
854889
const replaceToggle = document.getElementById('replaceToggle');
@@ -1659,6 +1694,36 @@ document.addEventListener('keydown', e => {
16591694
}
16601695
});
16611696

1697+
document.addEventListener('paste', e => {
1698+
if (isFindWidgetTarget(e.target)) {
1699+
return;
1700+
}
1701+
if (editingCell) {
1702+
return;
1703+
}
1704+
const clipboard = e.clipboardData;
1705+
if (!clipboard) {
1706+
return;
1707+
}
1708+
const text = clipboard.getData('text/plain');
1709+
if (typeof text !== 'string' || text.length === 0) {
1710+
return;
1711+
}
1712+
const anchor = getPasteAnchorCoords();
1713+
if (!anchor) {
1714+
return;
1715+
}
1716+
e.preventDefault();
1717+
const selection = getDataSelectionBounds();
1718+
vscode.postMessage({
1719+
type: 'pasteCells',
1720+
text,
1721+
anchorRow: anchor.row,
1722+
anchorCol: anchor.col,
1723+
selection: selection || undefined
1724+
});
1725+
});
1726+
16621727
const selectAllCells = () => { clearSelection(); document.querySelectorAll('td, th').forEach(cell => { cell.classList.add('selected'); currentSelection.push(cell); }); };
16631728

16641729
const setCursorToEnd = cell => { setTimeout(() => {
@@ -1866,6 +1931,29 @@ window.addEventListener('message', event => {
18661931
if (findReplaceState.open && findInput.value) {
18671932
scheduleFind(true);
18681933
}
1934+
} else if (message.type === 'pasteApplied') {
1935+
const startRow = Number(message.startRow);
1936+
const startCol = Number(message.startCol);
1937+
const endRow = Number(message.endRow);
1938+
const endCol = Number(message.endCol);
1939+
if (
1940+
!Number.isInteger(startRow) || !Number.isInteger(startCol) ||
1941+
!Number.isInteger(endRow) || !Number.isInteger(endCol) ||
1942+
startRow < 0 || startCol < 0 || endRow < startRow || endCol < startCol
1943+
) {
1944+
return;
1945+
}
1946+
const startCell = ensureRenderedCellByCoords(startRow, startCol);
1947+
const endCell = ensureRenderedCellByCoords(endRow, endCol);
1948+
if (!startCell || !endCell) {
1949+
return;
1950+
}
1951+
anchorCell = startCell;
1952+
rangeEndCell = endCell;
1953+
selectRange({ row: startRow, col: startCol }, { row: endRow, col: endCol });
1954+
persistState();
1955+
try { startCell.focus({ preventScroll: true }); } catch { try { startCell.focus(); } catch {} }
1956+
endCell.scrollIntoView({ block:'nearest', inline:'nearest', behavior:'smooth' });
18691957
} else if (message.type === 'findMatchesResult') {
18701958
if (!findReplaceState.open) {
18711959
return;

0 commit comments

Comments
 (0)