Skip to content

feat(editor): editor-enhancement track #23–#26 + design import#28

Merged
BorisTyshkevich merged 11 commits into
mainfrom
feat/editor-enhancements
Jun 24, 2026
Merged

feat(editor): editor-enhancement track #23–#26 + design import#28
BorisTyshkevich merged 11 commits into
mainfrom
feat/editor-enhancements

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

Summary

Implements the editor-enhancement track from #22 on a single branch
(per request), plus imports the updated design handoff bundle. Everything is
built on the existing hand-rolled <textarea>-over-<pre> editor — no editor
library, no new bundled runtime dependency.

Closes #23, #24, #25, #26. Part of #22. (#27 — signature help + hover docs —
is intentionally deferred as the optional Phase-2c item.)

What's here

  • Editor intelligence: reference data loader and tokenizer dynamic-keyword API (Phase 2a) #25 — dynamic reference data + tokenizer API. tokenize(sql, {keywords, funcs}) (optional, backward-compatible) so the server's keyword/function set
    drives version-correct highlighting. core/completions.js (assemble / build /
    context / rank) and a best-effort one-shot loader from system.keywords +
    system.functions, cached in memory per connection with a built-in fallback.
  • Editor: in-editor search/replace on the textarea (Phase 1a) #23 — find/replace. Cmd/Ctrl+F (bound on the textarea so it beats the
    browser's native find): live match count, prev/next, case / whole-word / regex
    toggles, replace + replace-all. Matches highlight via a new transparent
    mark-overlay <pre> layered below the token <pre>, so the token render
    path is untouched.
  • Editor: bracket matching and auto-close on the textarea (Phase 1b) #24 — bracket matching + auto-close. matchBracketAt + a pure
    bracketEdit decision (auto-close, wrap-selection, quote/closer type-over,
    empty-pair Backspace). Caret-adjacent pair highlights through the same
    overlay. {/} auto-close excluded per the resolved decision.
  • Editor intelligence: autocomplete dropdown (Phase 2b) #26 — schema-aware autocomplete. Ranked dropdown of keywords / functions /
    databases / tables / already-loaded columns; ↑↓/Enter/Tab/Esc + click;
    table. lists that table's columns; functions insert name(; footer shows
    signature → return + description.
  • Design import. design/ is a verbatim snapshot of the "Altinity Play"
    Claude Design project's handoff bundle — the UI spec for this track.
    Reference only: not imported by src/main.js, so esbuild never bundles it and
    coverage never sees it.

The keystroke rule holds throughout: no SQL runs while typing. Reference
data is fetched once per connection and everything else is client-side.

Architecture

Pure logic → src/core/ (completions, editor-search, editor-brackets,
editor-marks, editor-geometry), all at 100/100/100/100. DOM wiring →
src/ui/editor.js (thin integrator: overlay, scroll-sync, mark aggregation) +
focused render modules editor-search.js / editor-complete.js, mirroring the
design's file split. Reference-data load → src/net/ch-client.js (injected
fetch). No new deploy/http_handlers.xml rule — reference data uses the
existing query path.

Verification

  • npm test626 tests pass; per-file coverage gate green (new core/
    modules 100/100/100/100; editor.js 100/100/100/100; the two UI render
    modules at 100 stmts-lines / 100 funcs / ≥95 branches).
  • npm run builddist/sql.html builds as one self-contained file (~332 KB;
    Chart.js still the only bundled dep, zero third-party requests).
  • Real-browser boot smoke (agent Chrome): clean boot, no JS errors from the new
    code.

Live verification against a cluster (reference data from system.*, completion
over real columns) to be done at deploy time.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VxARMjGin32SxX95TCuzQw

BorisTyshkevich and others added 11 commits June 23, 2026 18:55
Verbatim snapshot of the "sql browser" Claude Design project's
design_handoff_altinity_play/ bundle — the UI source-of-truth for the
editor-enhancement track (#22#23#27). README is the full spec
(design tokens, region-by-region layout, per-issue editor reference);
the .jsx files are the React/Babel reference implementations to port.

Reference only — not built into dist/sql.html (esbuild bundles only
src/main.js) and outside the test coverage globs (src/**/*.js).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VxARMjGin32SxX95TCuzQw
Make highlighting and autocomplete version-correct off the connected
server, never running SQL on the keystroke path:

- tokenize(sql, {keywords, funcs}) — optional, backward-compatible second
  arg so server keyword/function sets can drive highlighting; existing
  callers are unaffected.
- src/core/completions.js (pure, 100%): assembleReferenceData (with
  built-in fallback), buildCompletions over the repo schema shape
  (loaded columns only), completionContext, rankCompletions.
- ch-client.loadReferenceData: best-effort one-shot load of
  system.keywords + system.functions; a missing/denied table degrades
  to the built-in sets.
- app: refData + completions cached in memory, loaded once per connection
  alongside loadSchema, rebuilt when lazy columns land; editor re-paints
  with the server keyword set. renderHighlightInto forwards the sets.

Prerequisite for the autocomplete dropdown (#26). Tests added per module
to 100%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VxARMjGin32SxX95TCuzQw
Cmd/Ctrl+F (bound on the textarea so it beats the browser's native find)
opens a floating find/replace panel: live match count, prev/next
(Enter / Shift+Enter / buttons), case / whole-word / regex toggles, and
a disclosure-gated replace row (Replace / Replace all).

- src/core/editor-search.js (pure, 100%): findMatches (escaped/word-bound/
  regex, zero-width + 10k-match guards) + validRegex.
- src/core/editor-marks.js (pure, 100%): buildMarkSegments — splits the
  text for the overlay; active > match > bracket priority on overlap.
- Match highlights paint into a new transparent <pre> mark overlay layered
  below the token <pre>, so the token render path is untouched (the
  resolved design). The overlay + scroll-sync live in editor.js; the panel
  is a focused render module (editor-search.js) wired via a small host
  seam, leaving a clean slot for #24's bracket marks.
- Replace goes through the editor's undoable applyEdit; editing the text
  re-runs the matcher and refreshes the count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VxARMjGin32SxX95TCuzQw
- src/core/editor-brackets.js (pure, 100%): matchBracketAt (depth scan in
  either direction, spans () [] {}) and bracketEdit — the decision for
  auto-close, wrap-selection, quote/closer type-over, and empty-pair
  Backspace delete. `{`/`}` auto-close is excluded per the Phase-1b
  decision (matching still spans it).
- editor.js wires bracketEdit ahead of the Tab handler (a non-bracket key
  falls through); structural edits apply via a direct splice + 'input'
  (caret-inside / pair-delete can't ride execCommand). The caret-adjacent
  bracket pair highlights through the same mark overlay as search, fed by
  caret-move (keyup/click/select) repaints and suppressed while find is open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VxARMjGin32SxX95TCuzQw
Typing a word (or after a `.`) opens a candidate popover ranked client-side
off the in-memory reference data + schema loaded once per connection (#25) —
no SQL on the keystroke path. ↑/↓ move, Enter/Tab accept, Esc dismisses,
mousedown on a row accepts; a qualified `table.` lists only that table's
columns; functions insert `name(`; a footer shows the active signature →
return type and description. Dismisses on blur/scroll and stays suppressed
while find/replace is open.

- src/core/editor-geometry.js (pure, 100%): caretLineCol + caretXY. Caret
  positioning uses a fixed monospace char-width constant (the editor font is
  fixed) — no canvas, so the math stays pure and testable.
- src/ui/editor-complete.js: the dropdown render + state + key handling,
  wired into editor.js (handleKeydown runs before bracket/Tab; onInput calls
  refresh; accept goes through the undoable replaceRange).

Completes the #22 editor-enhancement track (#23#26); #27 signature/hover
remains the optional Phase-2c follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VxARMjGin32SxX95TCuzQw
README: add a "SQL editor" section (find/replace, bracket matching +
auto-close, schema-aware autocomplete) and spell out the keystroke rule
(reference data loaded once per connection from system.keywords /
system.functions, cached in memory, with a built-in fallback). Refresh the
intro + layout map for the new core/ui modules and the imported design/ dir.

No deploy/http_handlers.xml change: reference data uses the existing query
path, not a new handler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VxARMjGin32SxX95TCuzQw
Correctness:
- ⌘↵ / ⌘⇧↵ while the autocomplete dropdown is open no longer splices a
  token and runs: handleKeydown only accepts on an unmodified Enter/Tab;
  a modified one dismisses and bubbles to the global run/format shortcut.
- loadColumns now rebuilds completions, so qualified `table.` and
  unqualified column suggestions actually populate once a table is
  expanded (the headline of #26 was returning nothing for the session).
- Caret-only moves (Arrow/Home/End/click) dismiss the dropdown, so a
  later accept can't overwrite text at a stale word range.
- bracketEdit no longer fires on command chords (⌘/Ctrl, minus AltGr,
  which types real brackets on EU layouts) or IME composition keydowns —
  it was swallowing shortcuts and corrupting composition.
- Autocomplete popover is anchored in CSS px, bridging the shipped
  html{zoom:1.2} via the same getBoundingClientRect/offsetWidth scale
  results.js uses for column resize.

UX:
- Find resets the active match to the first when the query/options change.
- Replace scrolls the now-active match into view; the visibility test uses
  consistent pixel coordinates (was mixing content coords with a padding
  threshold, causing a spurious re-center).

Efficiency (keep the keystroke path cheap):
- buildMarkSegments is a single linear pass (marks are sorted + disjoint by
  construction) instead of O(matches²); also drops the dead range filter.
- paintMarks early-returns when there are no marks instead of rebuilding a
  full-document overlay node every keystroke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VxARMjGin32SxX95TCuzQw
Completes the #22 editor-enhancement track. Both read the in-memory
reference data loaded once per connection (#25) — never a query on the
keystroke path.

- ch-client.loadReferenceData now pulls system.functions.{syntax,description}
  (one-liners) when the server exposes those doc columns, falling back to the
  minimal name+is_aggregate query on older ClickHouse.
- src/core/completions.js (pure): wordAt + signatureContext; assembleReferenceData
  exposes a small built-in keyword-doc map (no server source for keyword docs).
- src/core/editor-geometry.js (pure): offsetFromXY — map a mouse point to a
  text offset (inverse of caretXY, same html{zoom} scale handling in editor.js).
- src/ui/editor-intel.js: signature popover (follows the caret inside a call,
  bolds the active arg, persists across commas, dismisses on )/Esc/leaving the
  call, suppressed while find/autocomplete is open) + hover card (textarea
  mousemove dwell → word → function/keyword doc; .sql-pre is pointer-events:none
  so hover must come from the textarea, per the issue's constraint).

Resolved open questions: docs come from system.functions (no separate
system.documentation, no lazy per-entity fetch — keeps the keystroke rule);
event source is the textarea mousemove; signature help persists across commas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VxARMjGin32SxX95TCuzQw
…scroll & footer (#26, #27)

Found via manual testing on the otel demo cluster after the initial #27 landed.

Hover docs + signature help (#27):
- Descriptions are now fetched on demand per entity and cached (app.entityDoc /
  ch.loadEntityDoc) rather than bulk-loaded — they're large and mostly unread, so
  the bulk reference load keeps only name/kind/syntax. firstLine() returns the
  first NON-empty line (ClickHouse descriptions begin with a blank line, so the
  old "up to first \n" returned '' for every function).
- Hover card is anchored on the cursor under the shipped html{zoom:1.2} (was
  offset ~20% via raw client coords; now divides by the same scale offsetAt uses)
  and no longer renders a stray "null" when a function has no description.

Autocomplete dropdown (#26):
- The active row scrolls into view during arrow-nav. replaceChildren reset the
  list scrollTop on every render and nothing pulled the selection back, so long
  lists showed no scroll and no visible selection.
- The footer now shows the active entry's DESCRIPTION — lazy + cached for
  functions (debounced so navigating doesn't spam queries), the static map for
  keywords — instead of repeating the params already shown in the row.
- The detail column shows only the params `(s, offset[, …])`, not the redundant
  `substring(s, …)` (the label already shows the name).

Tests reflect the real-server shapes (leading-newline descriptions, lazy fetch,
cache, no-"null", footer description). The pure/net/state/DOM and render layers
stay at 100/100/100/100 (ui glue within its functions≥95 / branches≥90 floor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGBS74oUsXarGkCRQKEFLu
…27)

Issues the coverage gate can't see (native browser behavior, DOM geometry,
keystroke-path cost), found in a manual review. Verified live on otel.

Correctness:
- Bracket auto-close/wrap/type-over/pair-delete no longer wipes the native undo
  stack: applyBracketEdit applies the edit as a single execCommand over the
  changed range (diffed) instead of a `ta.value =` assignment, so ⌘Z still works.
- Bracket matching, signature help, and auto-close are string/comment-aware via a
  new maskLiterals() (tokenizer-backed): a `(` in 'O(' no longer auto-closes, the
  caret-pair highlight skips brackets inside strings, and a comma inside a string
  isn't counted as an argument separator.
- Signature help + hover docs resolve the function name case-insensitively
  (COUNT → count), matching autocomplete.
- Find's whole-word now fences in regex mode too (\b wraps the pattern).
- Find drops zero-width matches (a*, ^, $) — nothing to highlight or replace.
- Autocomplete/signature popovers are dismissed on tab switch (editorSync), so a
  later accept/replace can't act on the previous tab's offsets.
- Hover returns no card in the blank space right of a short line (offsetFromXY
  bounds the column to the line's glyphs instead of clamping to its end).

Quality:
- tab-size:2 set on .sql-pre and .sql-mark-overlay (was only on .sql-textarea) so
  highlights line up on pasted SQL containing tabs.
- keyup repaints only on caret-only keys; a printable key already repainted via
  'input' and selection changes fire 'select' — no double overlay rebuild + scan.
- The html{zoom} client→CSS-px bridge is one zoomScale(el) helper (dom.js), reused
  by the editor popovers and results.js column-resize (which had drifted without
  the divide-by-zero guard).

Tests cover each: maskLiterals, inLiteral bracket suppression, regex whole-word,
zero-width skip, case-insensitive lookup, string-aware signature, tab-switch
dismiss, and the hover bound. All gated layers stay 100/100/100/100 (ui glue
within functions≥95 / branches≥90).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGBS74oUsXarGkCRQKEFLu
…t, qualified fallback, doc-cache, single tokenize (#26, #27)

Five findings from a follow-up manual review; verified live on otel.

Correctness:
- Hover docs now resolve the hovered word from the string/comment-masked text
  (host.maskedValue()), so hovering a function/keyword inside a string or comment
  no longer pops a phantom doc card — consistent with signature help (#1).
- Signature help strips the optional-param brackets ClickHouse uses
  (`name(a, b[, c])`) before splitting on commas, so args render cleanly and the
  active-arg highlight aligns (was showing `offset[` / `length]` for substring) (#2).
- completionContext only flags `qualified` when a real identifier precedes the
  dot; a bare dot (`.col`, `count().c`) now falls back to normal completion
  instead of an empty dropdown (#4).
- loadEntityDoc returns null on a query FAILURE vs '' for genuinely-no-doc, and
  entityDoc caches the latter but drops the former — a transient error no longer
  permanently suppresses a function's hover/footer doc for the session (#8).

Performance:
- The keystroke path tokenizes the buffer ONCE and shares the token list between
  the syntax highlighter and the literal mask, instead of two full passes per
  keystroke. maskFromTokens()/renderTokensInto() consume a token list; the editor
  memoizes it by (text, refData) so it re-tokenizes when server keyword/func sets
  arrive after connect (#5).

Tests cover each: hover-in-literal, bracketed-param signature split, bare-dot
fallback, failed-doc retry, and re-highlight-on-refData-change (the shared-token
cache invalidation). All gated layers stay 100/100/100/100 (ui glue within its
functions>=95 / branches>=90 floor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGBS74oUsXarGkCRQKEFLu
@BorisTyshkevich BorisTyshkevich merged commit 76f1c74 into main Jun 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Editor: in-editor search/replace on the textarea (Phase 1a)

1 participant