Skip to content

Commit 9537743

Browse files
committed
feat: rebuild find/replace overlay with full-file chunk-aware search, single-step replace-all undo, and dynamic row-index sizing
1 parent a1592a2 commit 9537743

File tree

6 files changed

+236
-63
lines changed

6 files changed

+236
-63
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Working with CSV files shouldn’t be a chore. With CSV, you get:
3737
- **Edit Empty CSVs:** Create or open an empty CSV file and start typing immediately.
3838
- **Column Sorting:** Right-click a header and choose A–Z or Z–A.
3939
- **Custom Font Selection:** Choose a font from a dropdown or inherit VS Code's default.
40-
- **Find & Replace Overlay:** Built-in find/replace bar with match options (case, whole-word, regex), keyboard navigation, and single/all replace actions.
40+
- **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).
4141
- **Clickable Links:** URLs in cells are automatically detected and displayed as clickable links. Ctrl/Cmd+click to open them in your browser.
4242
- **Preserved CSV Integrity:** All modifications respect CSV formatting—no unwanted extra characters or formatting issues.
4343
- **Optimized for Performance:** Designed for medium-sized datasets, ensuring a smooth editing experience without compromising on functionality.

media/main.js

Lines changed: 133 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ if (csvChunks.length) {
170170
const html = csvChunks.shift();
171171
tbody.insertAdjacentHTML('beforeend', html);
172172
applySizeStateToRenderedCells();
173+
window.dispatchEvent(new Event('csvChunkLoaded'));
173174
} finally {
174175
loading = false;
175176
}
@@ -880,8 +881,13 @@ let findMatches = [];
880881
let currentMatchIndex = -1;
881882
let findDebounce = null;
882883
let findFocusBeforeOpen = null;
884+
let findRequestSeq = 0;
885+
let latestFindRequestId = 0;
886+
const pendingFindRequests = new Map();
887+
let findMatchKeySet = new Set();
883888

884889
const escapeRegexLiteral = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
890+
const getFindMatchKey = (row, col) => `${row}:${col}`;
885891
const isFindWidgetTarget = target => {
886892
const el = getElementTarget(target);
887893
return !!(el && el.closest('#findReplaceWidget'));
@@ -924,15 +930,6 @@ const updateFindControls = () => {
924930
replaceOne.disabled = !hasQuery || !hasMatches;
925931
replaceAll.disabled = !hasQuery || !hasMatches;
926932
};
927-
const loadAllChunksForFind = () => {
928-
if (typeof window.__csvLoadNextChunk !== 'function' || !csvChunks || csvChunks.length === 0) {
929-
return;
930-
}
931-
let guard = 50000;
932-
while (csvChunks.length > 0 && guard-- > 0) {
933-
window.__csvLoadNextChunk();
934-
}
935-
};
936933
const getFindPattern = () => {
937934
const query = findInput.value;
938935
if (!query) return null;
@@ -952,10 +949,40 @@ const buildFindRegex = global => {
952949
return null;
953950
}
954951
};
955-
const getFindableCells = () => {
956-
return Array.from(table.querySelectorAll('td[data-col], th[data-col]')).filter(cell => {
952+
const getRenderedFindCells = () => {
953+
return Array.from(table.querySelectorAll('td[data-col], th[data-col]'));
954+
};
955+
const getRenderedCellByCoords = (row, col) => {
956+
return table.querySelector(`td[data-row="${row}"][data-col="${col}"], th[data-row="${row}"][data-col="${col}"]`);
957+
};
958+
const ensureRenderedCellByCoords = (row, col) => {
959+
let cell = getRenderedCellByCoords(row, col);
960+
if (cell) {
961+
return cell;
962+
}
963+
if (typeof window.__csvLoadNextChunk !== 'function') {
964+
return null;
965+
}
966+
let guard = 50000;
967+
while (!cell && csvChunks && csvChunks.length > 0 && guard-- > 0) {
968+
window.__csvLoadNextChunk();
969+
cell = getRenderedCellByCoords(row, col);
970+
}
971+
return cell;
972+
};
973+
const applyFindHighlightsToRendered = () => {
974+
if (!findMatchKeySet.size) {
975+
return;
976+
}
977+
getRenderedFindCells().forEach(cell => {
978+
const row = parseInt(cell.getAttribute('data-row') || 'NaN', 10);
957979
const col = parseInt(cell.getAttribute('data-col') || 'NaN', 10);
958-
return !Number.isNaN(col) && col >= 0;
980+
if (Number.isNaN(row) || Number.isNaN(col) || col < 0) {
981+
return;
982+
}
983+
if (findMatchKeySet.has(getFindMatchKey(row, col))) {
984+
cell.classList.add('highlight');
985+
}
959986
});
960987
};
961988
const setActiveFindMatch = (index, shouldScroll = true) => {
@@ -968,8 +995,10 @@ const setActiveFindMatch = (index, shouldScroll = true) => {
968995
}
969996
const normalized = ((index % findMatches.length) + findMatches.length) % findMatches.length;
970997
currentMatchIndex = normalized;
971-
const cell = findMatches[currentMatchIndex];
998+
const match = findMatches[currentMatchIndex];
999+
const cell = match ? ensureRenderedCellByCoords(match.row, match.col) : null;
9721000
if (cell) {
1001+
cell.classList.add('highlight');
9731002
cell.classList.add('active-match');
9741003
if (shouldScroll) {
9751004
cell.scrollIntoView({ block: 'center', inline: 'center', behavior: 'smooth' });
@@ -981,8 +1010,13 @@ const setActiveFindMatch = (index, shouldScroll = true) => {
9811010
const runFind = (preserveIndex = false) => {
9821011
const query = findInput.value;
9831012
const priorIndex = currentMatchIndex;
1013+
const requestId = ++findRequestSeq;
1014+
latestFindRequestId = requestId;
1015+
pendingFindRequests.set(requestId, { preserveIndex, priorIndex });
1016+
9841017
clearFindHighlights();
9851018
findMatches = [];
1019+
findMatchKeySet = new Set();
9861020
currentMatchIndex = -1;
9871021
findReplaceState.invalidRegex = false;
9881022
if (!query) {
@@ -991,32 +1025,18 @@ const runFind = (preserveIndex = false) => {
9911025
return;
9921026
}
9931027

994-
loadAllChunksForFind();
995-
const regex = buildFindRegex(false);
996-
if (!regex) {
997-
findReplaceState.invalidRegex = true;
998-
updateFindStatus();
999-
updateFindControls();
1000-
return;
1001-
}
1002-
1003-
getFindableCells().forEach(cell => {
1004-
regex.lastIndex = 0;
1005-
if (regex.test(cell.innerText || '')) {
1006-
findMatches.push(cell);
1007-
cell.classList.add('highlight');
1028+
vscode.postMessage({
1029+
type: 'findMatches',
1030+
requestId,
1031+
query,
1032+
options: {
1033+
matchCase: findReplaceState.matchCase,
1034+
wholeWord: findReplaceState.wholeWord,
1035+
regex: findReplaceState.regex
10081036
}
10091037
});
1010-
1011-
if (findMatches.length > 0) {
1012-
const nextIndex = preserveIndex && priorIndex >= 0
1013-
? Math.min(priorIndex, findMatches.length - 1)
1014-
: 0;
1015-
setActiveFindMatch(nextIndex);
1016-
} else {
1017-
updateFindStatus();
1018-
updateFindControls();
1019-
}
1038+
updateFindStatus();
1039+
updateFindControls();
10201040
};
10211041
const scheduleFind = (preserveIndex = false) => {
10221042
if (findDebounce) clearTimeout(findDebounce);
@@ -1049,41 +1069,44 @@ const replaceInText = (text, replaceAllMatches) => {
10491069
}
10501070
return text.replace(regex, matched => preserveReplacementCase(replacementText, matched));
10511071
};
1052-
const commitCellReplacement = (cell, value) => {
1053-
cell.textContent = value;
1054-
const { row, col } = getCellCoords(cell);
1055-
vscode.postMessage({ type: 'editCell', row, col, value });
1056-
};
10571072
const replaceCurrentMatch = () => {
10581073
if (!findMatches.length || findReplaceState.invalidRegex) return;
1059-
const cell = findMatches[currentMatchIndex];
1060-
if (!cell || !cell.isConnected) {
1074+
const match = findMatches[currentMatchIndex];
1075+
if (!match) {
10611076
runFind(true);
10621077
return;
10631078
}
1064-
const original = cell.innerText || '';
1079+
const original = String(match.value ?? '');
10651080
const next = replaceInText(original, false);
10661081
if (next === original) {
10671082
navigateFind(false);
10681083
return;
10691084
}
1070-
commitCellReplacement(cell, next);
1085+
const cell = ensureRenderedCellByCoords(match.row, match.col);
1086+
if (cell) {
1087+
cell.textContent = next;
1088+
}
1089+
vscode.postMessage({ type: 'editCell', row: match.row, col: match.col, value: next });
10711090
runFind(true);
10721091
};
10731092
const replaceAllMatches = () => {
1074-
if (findReplaceState.invalidRegex || !findInput.value) return;
1075-
runFind(false);
1093+
if (findReplaceState.invalidRegex || !findInput.value || !findMatches.length) return;
1094+
const seen = new Set();
10761095
if (!findMatches.length) return;
1077-
const unique = [...new Set(findMatches)];
10781096
const replacements = [];
1079-
unique.forEach(cell => {
1080-
if (!cell || !cell.isConnected) return;
1081-
const original = cell.innerText || '';
1097+
findMatches.forEach(match => {
1098+
if (!match) return;
1099+
const key = getFindMatchKey(match.row, match.col);
1100+
if (seen.has(key)) return;
1101+
seen.add(key);
1102+
const original = String(match.value ?? '');
10821103
const next = replaceInText(original, true);
10831104
if (next !== original) {
1084-
cell.textContent = next;
1085-
const { row, col } = getCellCoords(cell);
1086-
replacements.push({ row, col, value: next });
1105+
const cell = getRenderedCellByCoords(match.row, match.col);
1106+
if (cell) {
1107+
cell.textContent = next;
1108+
}
1109+
replacements.push({ row: match.row, col: match.col, value: next });
10871110
}
10881111
});
10891112
if (replacements.length > 0) {
@@ -1107,6 +1130,8 @@ const closeFindReplace = () => {
11071130
clearTimeout(findDebounce);
11081131
findDebounce = null;
11091132
}
1133+
pendingFindRequests.clear();
1134+
latestFindRequestId = 0;
11101135
findReplaceState.open = false;
11111136
findReplaceWidget.classList.remove('open');
11121137
hideFindOverflowMenu();
@@ -1201,6 +1226,19 @@ document.addEventListener('mousedown', e => {
12011226
syncFindToggleUi();
12021227
updateFindStatus();
12031228
updateFindControls();
1229+
window.addEventListener('csvChunkLoaded', () => {
1230+
if (!findReplaceState.open || findMatches.length === 0) {
1231+
return;
1232+
}
1233+
applyFindHighlightsToRendered();
1234+
if (currentMatchIndex >= 0 && currentMatchIndex < findMatches.length) {
1235+
const active = findMatches[currentMatchIndex];
1236+
const cell = getRenderedCellByCoords(active.row, active.col);
1237+
if (cell) {
1238+
cell.classList.add('active-match');
1239+
}
1240+
}
1241+
});
12041242

12051243
// Capture-phase handler to intercept Cmd/Ctrl + Arrow and move to extremes
12061244
document.addEventListener('keydown', e => {
@@ -1660,6 +1698,46 @@ window.addEventListener('message', event => {
16601698
if (findReplaceState.open && findInput.value) {
16611699
scheduleFind(true);
16621700
}
1701+
} else if (message.type === 'findMatchesResult') {
1702+
if (!findReplaceState.open) {
1703+
return;
1704+
}
1705+
const requestId = Number(message.requestId);
1706+
const requestState = pendingFindRequests.get(requestId);
1707+
pendingFindRequests.delete(requestId);
1708+
if (!Number.isInteger(requestId) || requestId !== latestFindRequestId) {
1709+
return;
1710+
}
1711+
1712+
findReplaceState.invalidRegex = !!message.invalidRegex;
1713+
findMatches = Array.isArray(message.matches)
1714+
? message.matches
1715+
.map(raw => {
1716+
const row = Number(raw?.row);
1717+
const col = Number(raw?.col);
1718+
if (!Number.isInteger(row) || row < 0 || !Number.isInteger(col) || col < 0) {
1719+
return null;
1720+
}
1721+
return { row, col, value: String(raw?.value ?? '') };
1722+
})
1723+
.filter(Boolean)
1724+
: [];
1725+
findMatchKeySet = new Set(findMatches.map(match => getFindMatchKey(match.row, match.col)));
1726+
1727+
clearFindHighlights();
1728+
applyFindHighlightsToRendered();
1729+
if (findMatches.length > 0) {
1730+
const preserveIndex = !!requestState?.preserveIndex;
1731+
const priorIndex = Number.isInteger(requestState?.priorIndex) ? requestState.priorIndex : -1;
1732+
const nextIndex = preserveIndex && priorIndex >= 0
1733+
? Math.min(priorIndex, findMatches.length - 1)
1734+
: 0;
1735+
setActiveFindMatch(nextIndex);
1736+
} else {
1737+
currentMatchIndex = -1;
1738+
updateFindStatus();
1739+
updateFindControls();
1740+
}
16631741
}
16641742
});
16651743

0 commit comments

Comments
 (0)