feat(editor): editor-enhancement track #23–#26 + design import#28
Merged
Conversation
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
This was referenced Jun 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 editorlibrary, 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
tokenize(sql, {keywords, funcs})(optional, backward-compatible) so the server's keyword/function setdrives 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.Cmd/Ctrl+F(bound on the textarea so it beats thebrowser'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 renderpath is untouched.
matchBracketAt+ a purebracketEditdecision (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.databases / tables / already-loaded columns; ↑↓/Enter/Tab/Esc + click;
table.lists that table's columns; functions insertname(; footer showssignature → return + description.
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 andcoverage 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 thedesign's file split. Reference-data load →
src/net/ch-client.js(injectedfetch). No new
deploy/http_handlers.xmlrule — reference data uses theexisting query path.
Verification
npm test— 626 tests pass; per-file coverage gate green (newcore/modules 100/100/100/100;
editor.js100/100/100/100; the two UI rendermodules at 100 stmts-lines / 100 funcs / ≥95 branches).
npm run build—dist/sql.htmlbuilds as one self-contained file (~332 KB;Chart.js still the only bundled dep, zero third-party requests).
code.
Live verification against a cluster (reference data from
system.*, completionover real columns) to be done at deploy time.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VxARMjGin32SxX95TCuzQw