Skip to content

Editor: bracket matching and auto-close on the textarea (Phase 1b) #24

Description

@BorisTyshkevich

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.
  • 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.
    • Closing ), ], ', " when nextChar is the same: skip over — return {insert: '', cursorOffset: 1}.
    • Otherwise: null (normal insert).
  • wrapWithPair(selectedText, typed)string | null.
    • 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 selection
const sel = ta.value.slice(ta.selectionStart, ta.selectionEnd);
const wrapped = wrapWithPair(sel, e.key);
if (wrapped !== null) {
  e.preventDefault();
  applyEdit(ta, wrapped);   // replaces the current selection with open+sel+close
  return;
}
// 2. Auto-close / skip-over
// Capture start BEFORE applyEdit — applyEdit mutates ta.selectionStart.
const start = ta.selectionStart;
const nextChar = ta.value[start] ?? '';
const ac = 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.
  • Bracket matching skips structural brackets inside -- comments, /* */ comments, and string literals.
  • Works across multi-line SQL.
  • Auto-close and skip-over are undoable with ⌘Z.
  • Existing Tab insertion and schema drag-drop still work unchanged.
  • src/core/editor-brackets.js at 100% coverage; all branches exercised: matching pair, unmatched, nested, comment-skipped, string-skipped, auto-close, skip-over, wrap selection (non-empty selectedText), wrap no-op (empty selectedText).
  • src/ui/editor.js changes maintain existing coverage threshold.

Non-goals

  • { } pair (defer).
  • Code folding, multi-cursor.
  • Server-side bracket validation.
  • Autocomplete.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions