Skip to content

Commit 6b28509

Browse files
author
Alexey Perov
committed
Add unified search query model with regex/whole-word support for find/replace.
M8 find/replace polish: a tested SearchQuery contract (searchQuery.ts) drives both in-file editor search and project-wide search through @codemirror/search's RegExpCursor. Editor ops, highlight, domain APIs, host factory, and types all consume SearchQuery objects. Project search extends matches with to/length, validates before traversal, and routes replace through the existing dirty-doc safety gate. FindReplacePanel gains match-case/whole-word/regex toggles, inline regex validation, selection seeding, and responsive width. ProjectSearchPanel gains the same toggles, confirmation dialog before replace-all, and cancellable search on workspace switch/close. 282 test files, 2960 tests pass. npm run check: 0 errors.
1 parent fde0ec9 commit 6b28509

28 files changed

Lines changed: 1421 additions & 314 deletions

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ and **OpenCode**-powered workspace sessions. Built with
88
99
## What works today
1010

11-
- **Editor** — syntax highlighting for Markdown and common code languages; optional **minimap**
11+
- **Editor** — syntax highlighting for Markdown and common code languages; optional **minimap**;
12+
multi-cursor, code folding, Markdown outline; find/replace with regex, whole-word,
13+
and case matching in-file and across the project
1214
- **Markdown** preview and edit
1315
- **Folders as workspaces** — multi-root activity rail
1416
- **Project panel** — file tree, drag-and-drop move, context menu (new/rename/delete), live refresh, tabs, show/hide hidden files

app/src/lib/components/AppShell.svelte

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@
227227
query: string;
228228
replaceValue: string;
229229
caseSensitive: boolean;
230+
wholeWord: boolean;
231+
regex: boolean;
232+
/** Inline regex validation error (empty string when the query is valid). */
233+
queryError: string;
230234
results: import("../services/projectSearch").ProjectSearchResult[];
231235
running: boolean;
232236
status: string;
@@ -236,6 +240,8 @@
236240
onQueryChange: (value: string) => void;
237241
onReplaceValueChange: (value: string) => void;
238242
onCaseSensitiveChange: (value: boolean) => void;
243+
onWholeWordChange: (value: boolean) => void;
244+
onRegexChange: (value: boolean) => void;
239245
onRunSearch: () => void;
240246
onReplaceAll: () => void;
241247
onOpenResult: (path: string, line: number) => void;
@@ -596,6 +602,9 @@
596602
query={projectSearch.query}
597603
replaceValue={projectSearch.replaceValue}
598604
caseSensitive={projectSearch.caseSensitive}
605+
wholeWord={projectSearch.wholeWord}
606+
regex={projectSearch.regex}
607+
queryError={projectSearch.queryError}
599608
results={projectSearch.results}
600609
running={projectSearch.running}
601610
status={projectSearch.status}
@@ -604,6 +613,8 @@
604613
onQueryChange={projectSearch.onQueryChange}
605614
onReplaceValueChange={projectSearch.onReplaceValueChange}
606615
onCaseSensitiveChange={projectSearch.onCaseSensitiveChange}
616+
onWholeWordChange={projectSearch.onWholeWordChange}
617+
onRegexChange={projectSearch.onRegexChange}
607618
onRunSearch={projectSearch.onRunSearch}
608619
onReplaceAll={projectSearch.onReplaceAll}
609620
onOpenResult={projectSearch.onOpenResult}

app/src/lib/components/EditorPaneContent.svelte

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,23 @@
158158
let findQuery = $state("");
159159
let replaceValue = $state("");
160160
let findCaseSensitive = $state(false);
161+
let findWholeWord = $state(false);
162+
let findRegexp = $state(false);
161163
let goToLineValue = $state("");
164+
/**
165+
* Text captured from the editor's main selection at the moment Find opens.
166+
* Used to seed the query when the selection is non-empty and single-ranged;
167+
* empty when there is nothing useful to seed.
168+
*/
169+
let findReplaceSeedSelection = $state("");
170+
let prevFindReplaceOpen = false;
162171
163172
$effect(() => {
164173
findQuery = toolSnapshot.find.query;
165174
replaceValue = toolSnapshot.find.replace;
166175
findCaseSensitive = toolSnapshot.find.caseSensitive;
176+
findWholeWord = toolSnapshot.find.wholeWord;
177+
findRegexp = toolSnapshot.find.regexp;
167178
goToLineValue = toolSnapshot.goToLineValue;
168179
});
169180
@@ -176,10 +187,37 @@
176187
$effect(() => {
177188
editorTools.setFindCaseSensitive(findCaseSensitive);
178189
});
190+
$effect(() => {
191+
editorTools.setFindWholeWord(findWholeWord);
192+
});
193+
$effect(() => {
194+
editorTools.setFindRegexp(findRegexp);
195+
});
179196
$effect(() => {
180197
editorTools.setGoToLineValue(goToLineValue);
181198
});
182199
200+
// Seed the find query from a non-empty single selection when Find opens.
201+
$effect(() => {
202+
const isOpen = findReplaceOpen;
203+
if (isOpen && !prevFindReplaceOpen) {
204+
const host = getActiveEditorHost();
205+
const selResult = host?.queries.selection.getSelection();
206+
if (host && selResult?.ok && !selResult.value.empty) {
207+
const docResult = host.queries.document.getDocumentContent();
208+
if (docResult.ok) {
209+
findReplaceSeedSelection = docResult.value.slice(
210+
selResult.value.from,
211+
selResult.value.to,
212+
);
213+
}
214+
} else {
215+
findReplaceSeedSelection = "";
216+
}
217+
}
218+
prevFindReplaceOpen = isOpen;
219+
});
220+
183221
let paneSectionEl = $state<HTMLElement | null>(null);
184222
185223
const layout = $derived(session.editorLayout);
@@ -387,6 +425,9 @@
387425
bind:findQuery
388426
bind:replaceValue
389427
bind:findCaseSensitive
428+
bind:findWholeWord
429+
bind:findRegexp
430+
seedSelection={findReplaceSeedSelection}
390431
getEditorRunner={getActiveEditorRunner}
391432
{notify}
392433
documentId={paneDocument?.id ?? null}

0 commit comments

Comments
 (0)