fix(editor): stop keystrokes being overwritten on large schemas - #809
Conversation
The query editor rendered <Editor value={...}> as a controlled component and
fed that prop from onContentChange on every keystroke. @monaco-editor/react's
controlled-value effect runs executeEdits over the full model range whenever
the prop changes and differs from the buffer; when a keystroke lands between
the state update and the re-render, the prop is one keystroke stale, so the
full-range edit rewrites the buffer and snaps the caret to line 1. A large
schemaContext widens that window (extra parse/render work), which is why it
showed up on large-schema connections and not on the small samples.
Make the editor uncontrolled: pass defaultValue instead of value, so the
library's value effect early-returns, and route external changes (tab switch,
Format, Clear, setValue) through the existing useEffect([value]) that guards
self-echoes via lastSyncedValueRef.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Passing `defaultValue` stops the library rewriting the buffer from a stale prop, which is what scrambled typing. It leaves the other half of the race open: the sync effect still has to decide whether an incoming `value` is this editor's own text arriving late or somebody else's write, and it decided by asking whether the buffer had moved since the last sync. An external write that arrives while the user is typing answers that question exactly like a late echo does, so it was swallowed. The new-tab shortcut is registered on `document` so it fires while Monaco holds focus (libredb#745): typing and then pressing Cmd/Ctrl+Shift+X opened the new tab holding the previous tab's query, 8 runs out of 8 under CPU throttling, on this branch and on main alike. Two changes settle it. The editor now records every text it hands up through `onContentChange`, so a value that is still outstanding is its own by construction rather than by inference. And it takes the tab id as `documentId`, which makes a switch an event: a prop that never changed cannot announce one, and at the moment a new empty tab arrives `value` can still be the empty string the editor mounted with. `lastSyncedValueRef` and `isInternalChangeRef` go with the old reasoning; both were write-only. (cherry picked from commit 6f19bf48a8b7201f4e8c268eef847baccab9c943)
# Conflicts: # src/components/Studio.tsx # src/workspace/StudioWorkspace.tsx
|
Thanks @nycjay, and your diagnosis is exact. I reproduced it on a generated 1000-table SQLite schema against production builds: on main, 79 typed characters land as One correction to the framing. The large schema is no longer the amplifier: #811 landed a virtualised object tree, which takes the per-keystroke render cost with 1000 tables from 51 ms to 5 ms, and 1000 tables expanded now render 29 rows. The race needs no schema at all, a 10-table one scrambles once the CPU is throttled 4x. I have pushed a commit on top of yours, because the same race stays open on the other side. A divergence test cannot tell a late echo from an external write that arrives mid-typing, and the new-tab shortcut is registered on It keeps your |
The query editor rendered
<Editor value={...}>as a controlled component and fed that prop from onContentChange on every keystroke. @monaco-editor/react's controlled-value effect runs executeEdits over the full model range whenever the prop changes and differs from the buffer; when a keystroke lands between the state update and the re-render, the prop is one keystroke stale, so the full-range edit rewrites the buffer and snaps the caret to line 1. A large schemaContext widens that window (extra parse/render work), which is why it showed up on large-schema connections and not on the small samples.Make the editor uncontrolled: pass defaultValue instead of value, so the library's value effect early-returns, and route external changes (tab switch, Format, Clear, setValue) through the existing useEffect([value]) that guards self-echoes via lastSyncedValueRef.
Description
Fixes a bug where typing in the query editor on a large-schema connection dropped and reordered characters and reset the cursor to line 1. The editor is now uncontrolled on the keystroke path, so a keystroke no longer races a stale
valueprop.Type of Change
Related Issue
Closes #808
Changes Made
src/components/QueryEditor.tsx: passdefaultValue={value}instead ofvalue={value}so@monaco-editor/react's controlled-value effect stays at its early return and no longer runs a full-rangeexecuteEditson a stale prop.useEffect([value]), which guards self-echoes vialastSyncedValueRefso it does not clobber the buffer during active typing.tests/components/QueryEditor.test.tsx: added regression tests for the uncontrolled-editor contract and the stale-echo guard.Testing
Reproduced and confirmed the fix in a real browser against a 400-table SQLite schema. Before: typing
SELECT id FROM entity_0001 WHERE owner_id = 42 ORDER BY created_at DESCproducedSEETi RMett_01WEEonri 2ODRB rae_tDSC. After: the text lands verbatim, the caret only moves forward, and instrumenting the live editor showed zerosetValue/executeEditscalls during typing (one applyEdits per character, which is the normal insertion path).Test Environment
Screenshots (if applicable)
N/A
Reproducing manually
The bug only shows up on a large schema, so you need one to see it. This script builds a 400-table SQLite database you can connect to. Save it as
gen-large-sqlite.mjsand runbun gen-large-sqlite.mjs(it writes todata/large-schema-test.dbby default; pass a count and path to override, e.g.bun gen-large-sqlite.mjs 800 /tmp/big.db).Then add a SQLite connection in the UI pointing at the generated file, open a query tab, and type a sentence at a normal pace. On
mainthe characters scramble and the cursor jumps to line 1, with this fix typing is much more smooth. I didn't include this reproduction script in the codebase itself as a test fixture, but can if maintainers would prefer.Checklist
bun run test:coverageandbun run coverage:check)src/lib/db/providers/, I updated the matchingdocs/providers/documentation andtests/integration/db/tests in the same PR (provider triad)Additional Notes
Documentation checkbox is left unchecked because this is an editor-behavior fix with no user-facing docs to update. The provider-triad checkbox does not apply: this PR does not touch
src/lib/db/providers/.On the coverage gate: I could not run the full merged gate locally (
tests/run-core.shusesmapfile, which needs bash 4+, and this machine is on bash 3.2; Helm is not installed so the chart checks cannot run either). I ran the component suite directly (108 pass) and measured the merged component lcov for the changed file at 100% (QueryEditor.tsx427/427 lines). The CI job is the authoritative gate for the full merged report.