Split from #22. Phase 1 of the textarea editor enhancement track.
Context
The editor (src/ui/editor.js) is a <textarea> overlaid on a <pre> for syntax highlighting. The existing undo-safe insertion path (applyEdit via execCommand('insertText') with fallback splice) is the correct seam for all text mutations. Match highlights must live in the overlay layer — not inside the textarea.
DOM structure
The relevant layout (from editor.js and styles.css):
.sql-editor (display: flex; position: relative)
.sql-gutter
.sql-area (position: relative; flex: 1; overflow: hidden)
.sql-pre (position: absolute; inset: 0; pointer-events: none)
.sql-textarea (position: absolute; inset: 0)
Both new elements — the search overlay <pre> and the find bar widget — must be mounted inside .sql-area, not .sql-editor. .sql-editor includes the gutter; .sql-area is the positioned text layer. An absolutely-positioned child of .sql-area sits over the text and textarea only, with correct coordinate origin.
Overlay strategy
Add a second, absolutely-positioned <pre> inside .sql-area, alongside the existing syntax <pre>. It has color: transparent and identical font/padding/line-height, and carries only <mark> spans at match offsets. Scroll-synced with the textarea the same way the syntax <pre> already is. This approach leaves renderHighlightInto untouched.
Scope
Pure module src/core/editor-search.js
findMatches(sql, pattern, { caseSensitive, regex }) → { matches: [{start, end}], error: string|null }. On invalid regex returns { matches: [], error: 'invalid regex' } — never throws.
applyReplace(sql, match, replacement) → new SQL string with that one match replaced.
applyReplaceAll(sql, matches, replacement) → new SQL string with all matches replaced (applied right-to-left so prior offsets stay valid).
UI additions in src/ui/editor.js
mountSearchBar(app) — appends the search overlay <pre> and the find bar <div class="search-bar"> (hidden by default) to .sql-area; registers app.dom.searchOverlayPre and app.dom.searchBar.
renderSearchMatches(app, matches, activeIdx) — rebuilds the search overlay <pre> with <mark> spans for all matches; adds class="active" to the current one.
Find bar contents:
- Search input + case-sensitive toggle + regex toggle.
- Replace input row (shown only in replace mode).
- Next / Prev / Replace / Replace All buttons.
Keyboard shortcuts — registered on the textarea keydown listener (not in shortcuts.js global handler; the browser fires native find before a global handler can intercept Cmd+F):
Cmd/Ctrl+F → open find bar, focus search input.
Cmd/Ctrl+Shift+F → open in replace mode.
Enter / Shift+Enter while find input is focused → next / previous match.
Esc → close bar, return focus to textarea.
Navigation: textarea.setSelectionRange(match.start, match.end) to select the active match; scroll the textarea so it is visible.
Replace flow:
setSelectionRange to the active match.
applyEdit(ta, replacement) — fires the existing input listener which syncs tab.sql and repaints.
No extra sync call needed.
Tab switch: close the find bar and clear match highlights when the active tab changes.
Acceptance criteria
Non-goals
- CodeMirror or any editor library.
- Regex replace with capture groups (plain string replacement only).
- Code folding, multi-cursor, autocomplete, live validation.
Split from #22. Phase 1 of the textarea editor enhancement track.
Context
The editor (
src/ui/editor.js) is a<textarea>overlaid on a<pre>for syntax highlighting. The existing undo-safe insertion path (applyEditviaexecCommand('insertText')with fallback splice) is the correct seam for all text mutations. Match highlights must live in the overlay layer — not inside the textarea.DOM structure
The relevant layout (from
editor.jsandstyles.css):Both new elements — the search overlay
<pre>and the find bar widget — must be mounted inside.sql-area, not.sql-editor..sql-editorincludes the gutter;.sql-areais the positioned text layer. An absolutely-positioned child of.sql-areasits over the text and textarea only, with correct coordinate origin.Overlay strategy
Add a second, absolutely-positioned
<pre>inside.sql-area, alongside the existing syntax<pre>. It hascolor: transparentand identical font/padding/line-height, and carries only<mark>spans at match offsets. Scroll-synced with the textarea the same way the syntax<pre>already is. This approach leavesrenderHighlightIntountouched.Scope
Pure module
src/core/editor-search.jsfindMatches(sql, pattern, { caseSensitive, regex })→{ matches: [{start, end}], error: string|null }. On invalid regex returns{ matches: [], error: 'invalid regex' }— never throws.applyReplace(sql, match, replacement)→ new SQL string with that one match replaced.applyReplaceAll(sql, matches, replacement)→ new SQL string with all matches replaced (applied right-to-left so prior offsets stay valid).UI additions in
src/ui/editor.jsmountSearchBar(app)— appends the search overlay<pre>and the find bar<div class="search-bar">(hidden by default) to.sql-area; registersapp.dom.searchOverlayPreandapp.dom.searchBar.renderSearchMatches(app, matches, activeIdx)— rebuilds the search overlay<pre>with<mark>spans for all matches; addsclass="active"to the current one.Find bar contents:
Keyboard shortcuts — registered on the textarea
keydownlistener (not inshortcuts.jsglobal handler; the browser fires native find before a global handler can interceptCmd+F):Cmd/Ctrl+F→ open find bar, focus search input.Cmd/Ctrl+Shift+F→ open in replace mode.Enter/Shift+Enterwhile find input is focused → next / previous match.Esc→ close bar, return focus to textarea.Navigation:
textarea.setSelectionRange(match.start, match.end)to select the active match; scroll the textarea so it is visible.Replace flow:
setSelectionRangeto the active match.applyEdit(ta, replacement)— fires the existinginputlistener which syncstab.sqland repaints.No extra sync call needed.
Tab switch: close the find bar and clear match highlights when the active tab changes.
Acceptance criteria
Cmd/Ctrl+Fopens the find bar without triggering browser native find.Cmd/Ctrl+Shift+Fopens in replace mode.Enter/Shift+Enteron the find input steps next/prev.Esccloses the bar and restores textarea focus.tab.sql, settab.dirty = true, and are undoable with ⌘Z / ⌘⇧Z.src/core/editor-search.jsis at 100% coverage.src/ui/editor.jschanges maintain its existing coverage threshold.Non-goals