You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Split from #22. Phase 1 of the textarea editor enhancement track.
Context
Same zero-dependency editor surface as the search ticket (#23). The pure tokenizer (src/core/sql-highlight.js) is the context oracle: running it over the SQL and mapping token ranges to offsets tells us whether the caret is inside a string or comment, so structural bracket operations can be suppressed there.
Tokenizer note:[ and ] are currently classified as other (the op regex /[=<>!+\-*/%(),.;]/ excludes them). They have no CSS class in the token render. The bracket module must apply its own overlay class (e.g. sql-bracket-hl) for pair highlights; it cannot rely on existing token styling.
Overlay strategy
Same second transparent <pre> inside .sql-area as the search overlay (see #22 design reference). If #23 has already landed, reuse app.dom.searchOverlayPre; if this lands first, introduce it here. Bracket-pair highlights and search match highlights use distinct CSS classes and coexist in the same overlay.
Pure module src/core/editor-brackets.js
tokenRanges(sql) → [{type, start, end}] — tokenizer output annotated with character offsets.
isInStringOrComment(ranges, offset) → boolean.
findMatchingBracket(sql, caretPos) → {open: number, close: number} | null. Matches () and []; skips string/comment tokens; returns null for unmatched or caret not adjacent to a bracket.
When selectedText is non-empty and typed is (, [, ', or ": return openChar + selectedText + closeChar.
Otherwise: null.
The function takes only the selected text, not the full SQL. The UI is responsible for extracting the selection and passing the result to applyEdit.
Pairs in scope:( ), [ ], ' ', " ". { } is explicitly excluded (defer).
UI wiring in src/ui/editor.js
Add to the textarea keydown listener, before the existing Tab check:
// 1. Wrap selectionconstsel=ta.value.slice(ta.selectionStart,ta.selectionEnd);constwrapped=wrapWithPair(sel,e.key);if(wrapped!==null){e.preventDefault();applyEdit(ta,wrapped);// replaces the current selection with open+sel+closereturn;}// 2. Auto-close / skip-over// Capture start BEFORE applyEdit — applyEdit mutates ta.selectionStart.conststart=ta.selectionStart;constnextChar=ta.value[start]??'';constac=autoCloseResult(ta.value,start,e.key,nextChar);if(ac!==null){e.preventDefault();if(ac.insert)applyEdit(ta,ac.insert);ta.setSelectionRange(start+ac.cursorOffset,start+ac.cursorOffset);return;}// 3. Fall through to Tab check and default behavior
After each input event: call findMatchingBracket at the current caret and repaint the overlay with the pair highlight (or clear it if null).
Acceptance criteria
(, [, ', " auto-close when caret is not inside a string or comment.
Typing a closing bracket when the next character is the same skips over it.
Typing ( with text selected produces (selected text) — selection replaced, not duplicated.
Matching bracket pair is highlighted in the overlay when caret is adjacent.
Split from #22. Phase 1 of the textarea editor enhancement track.
Context
Same zero-dependency editor surface as the search ticket (#23). The pure tokenizer (
src/core/sql-highlight.js) is the context oracle: running it over the SQL and mapping token ranges to offsets tells us whether the caret is inside a string or comment, so structural bracket operations can be suppressed there.Tokenizer note:
[and]are currently classified asother(theopregex/[=<>!+\-*/%(),.;]/excludes them). They have no CSS class in the token render. The bracket module must apply its own overlay class (e.g.sql-bracket-hl) for pair highlights; it cannot rely on existing token styling.Overlay strategy
Same second transparent
<pre>inside.sql-areaas the search overlay (see #22 design reference). If #23 has already landed, reuseapp.dom.searchOverlayPre; if this lands first, introduce it here. Bracket-pair highlights and search match highlights use distinct CSS classes and coexist in the same overlay.Pure module
src/core/editor-brackets.jstokenRanges(sql)→[{type, start, end}]— tokenizer output annotated with character offsets.isInStringOrComment(ranges, offset)→boolean.findMatchingBracket(sql, caretPos)→{open: number, close: number} | null. Matches()and[]; skips string/comment tokens; returns null for unmatched or caret not adjacent to a bracket.autoCloseResult(sql, caretPos, typed, nextChar)→{insert: string, cursorOffset: number} | null.(→(), cursor between. Suppress if caret is in string/comment.[→[], cursor between. Suppress if caret is in string/comment.'→'', cursor between. Suppress if caret is inside a string."→"", cursor between. Suppress if caret is inside a string.),],',"whennextCharis the same: skip over — return{insert: '', cursorOffset: 1}.null(normal insert).wrapWithPair(selectedText, typed)→string | null.selectedTextis non-empty andtypedis(,[,', or": returnopenChar + selectedText + closeChar.null.applyEdit.Pairs in scope:
( ),[ ],' '," ".{ }is explicitly excluded (defer).UI wiring in
src/ui/editor.jsAdd to the textarea
keydownlistener, before the existing Tab check:After each
inputevent: callfindMatchingBracketat the current caret and repaint the overlay with the pair highlight (or clear it if null).Acceptance criteria
(,[,',"auto-close when caret is not inside a string or comment.(with text selected produces(selected text)— selection replaced, not duplicated.--comments,/* */comments, and string literals.src/core/editor-brackets.jsat 100% coverage; all branches exercised: matching pair, unmatched, nested, comment-skipped, string-skipped, auto-close, skip-over, wrap selection (non-emptyselectedText), wrap no-op (emptyselectedText).src/ui/editor.jschanges maintain existing coverage threshold.Non-goals
{ }pair (defer).