Skip to content

feat: adopt CodeMirror 6 behind the EditorPort seam (#21) - #145

Merged
BorisTyshkevich merged 8 commits into
mainfrom
feat/codemirror-editor-21
Jul 3, 2026
Merged

feat: adopt CodeMirror 6 behind the EditorPort seam (#21)#145
BorisTyshkevich merged 8 commits into
mainfrom
feat/codemirror-editor-21

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

What & why

Closes #21 — the SQL editor is now CodeMirror 6, behind the #143 EditorPort seam (a one-factory swap in main.js). This is the deliberate 4th bundled runtime dependency and the foundation Phase 4 builds on (#84 schema-aware autocomplete becomes a completion-source data swap, #60 a hover/docs extension).

What users get: per-tab undo history, real IME/touch editing, CM6's find/replace panel (⌘F), measured-text rendering. What gets deleted: the whole hand-rolled stack — execCommand undo (with its Firefox workaround), fixed-glyph-width geometry, four-way scroll sync, the editor's html{zoom} popover bridging — src/editor/{textarea-adapter,editor-complete,editor-intel,editor-search}.js, src/core/editor-{marks,geometry,brackets,search}.js, maskLiterals/signatureContext, ~2,600 LOC incl. tests. Net diff: −1,100 lines.

Parity notes

  • Highlighting still tracks server system.keywords/functions — they feed a ClickHouse SQLDialect swapped via a Compartment on connect (parked tab states re-applied on restore).
  • Completion keeps the pure core/completions.js candidates + ranking (filter: false; per-option apply for the () caret). Function docs show as CM6's info tooltip (side panel, not the old footer) and on hover.
  • Global chords stay on the document handler: CM6's Mod-Enter and Escape defaultKeymap bindings are stripped so ⌘↵ is never swallowed by an open completion and Esc still cancels a running query; shortcuts.js now ignores defaultPrevented keys.
  • A Prec.high input guard restores the old literal semantics CM6 lacks: brackets/quotes never pair inside strings/comments/backtick idents, type-over works on pre-existing closers/quotes, quote-over-selection inside a string replaces instead of wrapping. {} still deliberately unpaired (Support variables in SELECT queries #134 variables).
  • Dropped in this parity v0 (documented): in-call signature help (Version-exact ClickHouse reference documentation in Workbench #60 rebuilds docs properly); drops now land at the pointer (the dropCursor target) rather than the caret.

Step-0 bundle gate (measured, approved at the plan gate)

raw gzip
before 484,674 B 155,810 B
after 890,482 B 289,466 B
delta +405,808 B (+84%) +133,656 B (+86%)

Over the issue's ~100–150 KB raw estimate (gzip in range); accepted as the price of the Phase-4 editor foundation. THIRD-PARTY-NOTICES covers all 14 bundled packages.

Review & verification

  • Plan pre-reviewed by an independent Plan agent (re-mount path, stale-Compartment hole, @marijn/find-cluster-break import-map entry, filter:false all caught pre-code).
  • 8-angle /code-review + a 21-agent adversarial ultrareview pass; 20 confirmed findings fixed (empty-dialect-at-mount, non-lowercased builtin words, quote pairing in comments, Esc swallowed by simplifySelection, IME-composition guard, prototype-pollution in hover lookup, ⌘Z dead after Format, …) with mutation-proven test pins.
  • Live verification against play.clickhouse.com (CH 26.7.1) and github.demo.altinity.cloud (CH 26.3.17) through the bundled artifact: sign-in → type → highlight → complete → run → format → undo → search → zoom 1.25 tooltip placement.
  • Unit: adapter tested against real CM6 under happy-dom at the per-file gate (49 specs). e2e: 54/54 on chromium + firefox + webkit, with the 13-entry import map the unbundled harness needs.
  • ⚠️ Remaining human step: a manual real-Safari pass (Playwright WebKit ≠ Safari for zoom/rect behavior — the De-risk the html{zoom} layout dependency (Safari verification + fallback/guard) #70 lesson).

Checklist

  • npm test passes (the per-file coverage gate is non-negotiable)
  • Tests added/updated in the same change as the code
  • npm run build succeeds (single-file dist/sql.html)
  • Layers kept honest: pure logic in src/core/, network in src/net/ (injected fetch), DOM in src/ui/
  • No new runtime dependency (or it's a deliberate, justified addition — see CONTRIBUTING): CM6 is the pre-approved 4th dep, bundle delta measured up front with an abort gate
  • README / CHANGELOG.md ([Unreleased]) updated if behavior or the deployed surface changed
  • Reconciled affected tracked work (roadmap Roadmap to 1.0.0 #68, the issue body, ADR/CHANGELOG) if this change reshaped it: CLAUDE.md rules 4/5, ADR-0001 addendum, THIRD-PARTY-NOTICES, Roadmap to 1.0.0 #68 Phase 4

🤖 Generated with Claude Code

https://claude.ai/code/session_014X92mUpk2ts56USdMGKaNa

BorisTyshkevich and others added 7 commits July 3, 2026 17:31
Step-0 delta on dist/sql.html (esbuild, minified, real build):
raw 484,674 -> 896,794 B (+412,120, +85%); gzip 155,810 -> 291,723 B
(+135,913, +87%). Over the issue's ~100-150 KB raw estimate; approved to
proceed at the plan gate (gzip transfer cost is in range, artifact stays
single-file/zero-request).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014X92mUpk2ts56USdMGKaNa
src/editor/codemirror-adapter.js implements the #143 port against real CM6:
per-tab EditorState (per-tab undo), annotation-guarded state sync (tab
switches never emit doc changes), ClickHouse dialect from app.refData with
Compartment reconfigure (parked tabs re-applied on restore), completion
parity v0 over core/completions.js ranking (filter:false, per-option apply
for the () caret), hover docs, IDENT/SUBQUERY drop handling, and defaultKeymap
with Mod-Enter stripped so global chords bubble to the document handler.
Token colors map onto the existing .sql-* classes via HighlightStyle; editor
chrome (gutter/selection/search panel/tooltips) styled in styles.css.

Unit-tested against real CM6 under happy-dom at the per-file gate (35 specs,
incl. the ported #143 port behavior suite + re-mount reparenting).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014X92mUpk2ts56USdMGKaNa
Deletes the superseded modules the CM6 adapter replaces wholesale:
src/editor/{textarea-adapter,editor-complete,editor-intel,editor-search}.js,
src/core/editor-{marks,geometry,brackets,search}.js, their unit suites, the
textarea-specific styles (.sql-textarea/.sql-pre/.sql-gutter/mark overlay/
.ac-*/.srch-*/.sig-*), and the now-consumerless maskFromTokens/maskLiterals
(sql-highlight) + signatureContext (completions) — the #141 mis-masking
consumers the issue slated to die in this cutover. tokenize and the .sql-*
token color classes stay (completions.js + the adapter's HighlightStyle).

app.test.js editor-backed flows now inject the CM6 adapter and drive the
port/view. e2e: editor.html mounts the CM6 adapter with the full 13-entry
import map (incl. @marijn/find-cluster-break); editor-insert.spec.js ported;
editor-alignment.spec.js deleted (textarea/pre scroll-sync bug class is
structurally gone); new editor-cm6.spec.js covers highlight-on-type, the
never-swallowed run chord, Enter-accept, per-tab undo, and the search panel
— 30/30 green on chromium+firefox+webkit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014X92mUpk2ts56USdMGKaNa
Caught in the live pass: CM6's addInfoPane appendChild()s an info
FUNCTION's result, so returning the doc text raw threw
"TypeError: parameter 1 is not of type 'Node'" the first time a row with
a description was dwelled on. Wrap keyword/function docs in a div; unit
specs now assert nodeType, not just text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014X92mUpk2ts56USdMGKaNa
CHANGELOG [Unreleased] records the swap + measured bundle delta
(+402,911 B raw / +132,903 B gzip). README: dep list is four, SQL-editor
section rewritten for CM6 (per-tab undo, CM6 search panel, info tooltip;
signature help deferred to #60), layout map gains src/editor/. CLAUDE.md
rules 4/5: CM6 is the landed 4th dep (port-shaped injected seam), no longer
"pre-approved pending #84". THIRD-PARTY-NOTICES: 14 MIT packages (7
@codemirror, 3 @lezer, style-mod, w3c-keyname, crelt,
@marijn/find-cluster-break). ADR-0001 addendum: the editor island has its
intended imperative-adapter implementation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014X92mUpk2ts56USdMGKaNa
Correctness (all confirmed by the 8-angle review pass):
- lowercase dialect `builtin` words: lang-sql looks words up lowercased, so
  camelCase function names (toDateTime, arrayJoin, …) never highlighted
- resolve langExt lazily at first mount: the factory runs before createApp
  assembles the fallback refData, so the editor mounted with an EMPTY
  dialect (and stayed monochrome all session if the reference load failed)
- re-register app.dom.editorView on every mount (renderApp resets app.dom;
  the ref was lost after sign-out → sign-in)
- restore quote auto-pairing (' " `) — the deleted editor-brackets.js paired
  quotes (wrap/type-over/auto-close); only {} is deliberately unpaired
- literal-aware brackets: a Prec.high inputHandler types a bare ( / [ inside
  strings/comments/quoted idents (CM6's closeBrackets only tree-checks
  quotes) — the issue's acceptance criterion the first cut missed
- hover: case-insensitive function lookup (SUM → sum) and no phantom doc
  cards inside strings/comments; scan the line, not the whole doc
- IDENT drops land at the pointer like SUBQUERY drops — dropCursor already
  promises exactly that target (the caret-insert was textarea-era semantics)
- collapse a restored tab's parked selection to its head: an invisible
  selection silently retargeted ⌘↵/Export
- emit unless EVERY transaction in an update is sync-annotated (a coalesced
  user edit + reconcile update must still reach tab.sql); sync reconciles
  are also addToHistory:false now (⌘Z can't resurrect a replaced doc)
- shortcuts.js: ignore defaultPrevented keys — Esc that closed the CM6
  completion/search no longer also cancels the running query

Cleanup from the same pass: clamp() reused from core/format.js, identity
CM_TYPE map dropped (kind passes through), dead .hover-card overlay block
collapsed into one .cm-tooltip rule, shared fullReplace spec for the three
full-doc replaces, completion source slices to the caret instead of
serializing the rope, same-tab sync short-circuits on length, adapter tests
ride tests/helpers/fake-app.js. README bullets updated (quotes pair; drops
land at the pointer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014X92mUpk2ts56USdMGKaNa
Product fixes (all adversarially confirmed by the multi-agent pass):
- inputGuards (né bracketInLiteral) now mirrors closeBrackets' bail-outs:
  no dispatch mid-IME-composition, no action when the reported range isn't
  the selection (browser-generated corrections must not re-anchor), and it
  additionally owns: type-over of PRE-EXISTING closers/quotes (CM6's
  closedBracketAt only tracks pairs it inserted this session — a loaded/
  formatted doc's quotes never stepped over), quote quiet-zones in comments
  and backtick identifiers (CM6 is only tree-aware for same-char quotes in
  Strings), and quote-over-selection inside a string replacing instead of
  wrapping (editor-brackets.js parity)
- Escape filtered out of defaultKeymap alongside Mod-Enter: CM6's
  simplifySelection consumed Esc whenever a selection existed, and with the
  defaultPrevented guard the first Esc silently collapsed the selection
  instead of cancelling the running query
- replaceDocument refocuses the editor (microtask-deferred) — a toolbar
  Format left focus on the button and the advertised ⌘Z dead until a click
- hover lookupFn guards own properties: a column named `constructor` hovered
  a phantom card off Object.prototype (hole inherited from editor-intel)
- completion source slices to the caret's line END, not the caret: cutting
  at the caret misread an open backtick-identifier with an escaped backtick
  as closed and popped a spurious completion on Ctrl+Space

Test pins the review demanded (each proven load-bearing by mutation):
undo-after-reconcile (addToHistory:false), multi-line hover offsets, the
behavioral auto-pairing e2e (pairs in code / steps over / quiet in strings —
pins the inputHandler wiring + Prec.high), and the per-tab-undo e2e now
undoes AFTER the tab round-trip so a fresh-state impostor fails it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014X92mUpk2ts56USdMGKaNa
@BorisTyshkevich BorisTyshkevich mentioned this pull request Jul 3, 2026
93 tasks
The app's styled ::-webkit-scrollbar paints a classic always-visible bar
whenever an element is scrollable, and under real-Safari zoom rounding the
editor scroller can read as scrollable by a few phantom px — painting both
bars over a six-line query that visibly fits (reported on the PR). The old
textarea editor always hid its native bars (scrollbar-width: none) for
exactly this class of noise; restore that for .cm-scroller. Wheel/trackpad/
keyboard scrolling is unaffected (verified live: scrollTop/scrollLeft take
on a 40-line doc with no bars painted).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014X92mUpk2ts56USdMGKaNa
@BorisTyshkevich
BorisTyshkevich merged commit 0ebaddc into main Jul 3, 2026
6 checks passed
BorisTyshkevich added a commit that referenced this pull request Jul 4, 2026
… bars (#147)

The console rendered at 1.2x via `html{zoom}` since the first commit. zoom:1.2
(=6/5) lands element box sizes on fractional device pixels, and the leftover
sub-pixel made scroll containers read as scrollable-by-~1px over content that
visibly fit — a phantom scrollbar. #145 dodged it on the editor by hiding its
bars entirely (both axes); the rest of the UI still showed spurious bars at
some window sizes. (The same rounding drove the Safari viewport divergence #70.)

Set --zoom:1 (native size; users enlarge with browser zoom). This puts every
engine into the state the `@supports not (zoom:1)` fallback already defined and
tested, so the zoom-bridging machinery (--vp-zoom measurement, Chart/menu/
splitter correction) is now dormant at divisor 1 rather than removed — full
teardown tracked in roadmap #68. With no rounding the editor uses the app's
standard themed scrollbars like every pane: a vertical bar for a long query, a
horizontal bar for a long line, and nothing when it fits.

Verified live on otel through the bundled artifact: short query → no bars,
long line → horizontal bar, many lines → vertical bar, back-to-short clears;
fullscreen graph panel measures --vp-zoom=1 and fits one screen. 1337 tests pass.


Claude-Session: https://claude.ai/code/session_01FXHG1CxKa5vm4SzNUQ8iWW

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BorisTyshkevich added a commit that referenced this pull request Jul 4, 2026
Versioned screenshot capture of the shipped app (antalya, v0.2.0 + CM6 #145/#84,
ClickHouse 26.3.10) for design review: 22 desktop + 5 mobile shots, dark canonical
with two light-theme parity shots, plus meta.json and notes.md.

Also adds the reusable capture spec + MCP runbook (CAPTURE-SPEC.md) and the version
index (README.md). 21-header-longversion skipped (antalya header doesn't crowd).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyDQ7yG4gaxWiGqB3Bbtgu
BorisTyshkevich added a commit that referenced this pull request Jul 4, 2026
Cuts the accumulated Unreleased work since 0.2.0: dashboard phases
1-3 (#149 D1-D3 — favorited-query dashboard, Arrange/Report layout,
global filter bar), FROM-aware autocompletion (#84), the CodeMirror 6
editor cutover (#21/#145) and EditorPort seam (#143), query variables
(#134), best-effort mobile mode (#126), schema-graph click-to-draw
(#124), CSS minification, and assorted fixes.
@BorisTyshkevich
BorisTyshkevich deleted the feat/codemirror-editor-21 branch August 6, 2026 15:28
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.

Adopt CodeMirror 6 behind the EditorPort seam (replace the hand-rolled textarea editor)

1 participant