Found while reviewing CON-20, measured rather than reasoned about, and deliberately not fixed there — CON-20 did not introduce it and both available fixes are trades that need deciding on their own.
What was measured
CodeMirror's syntaxTree(state) returns whatever the parser has finished so far. On a large document the parse is bounded by a work budget, so the tree simply stops before the end. On a document of roughly 131 KB the tree ended 20 characters short of the caret: resolveInner(pos, -1) then answered Document instead of the node actually under the cursor.
For CON-20's emoji replacement that means the "am I inside a code block?" guard silently returns "no" and :smile: is replaced inside a fenced code block — precisely the case the ticket exists to prevent.
ensureSyntaxTree(state, pos, timeout) is the API that parses up to a position before answering.
Why it is not a CON-20 bug
Bare syntaxTree is the pattern everywhere in this editor, not a slip in one new file:
src/ui/MarkdownEditor.tsx:638 — syntaxTree(view.state).resolveInner(…)
src/ui/markdown-live.ts:268,527,542,557
src/ui/editor-table.ts:975,1006,1028
src/ui/editor-emoji-replace.ts:47 (CON-20's, following the same pattern)
ensureSyntaxTree appears nowhere in the repo. So every feature built on the tree — live formatting, table editing, the slash menu's context checks, emoji replacement — inherits the same blind spot past the parse frontier, each with its own consequence.
Why it needs a decision rather than a patch
Both fixes cost something real:
ensureSyntaxTree with a timeout on every keystroke buys correctness with parse latency in the one place latency is most visible — typing. The budget has to be picked, and picked per call site, since a formatting decoration and a destructive text replacement do not deserve the same wait.
- Refusing to act past the frontier is honest and cheap, but it means a feature silently stops working on a long page, which is its own bad surprise — and "long" is not visible to the writer.
A third option worth weighing: only the destructive call sites (anything that rewrites the document, like the emoji replacement) need certainty. A decoration that guesses wrong repaints a moment later and costs nothing. That would narrow the change to a couple of places and leave the rest alone.
Task
- Decide the rule, per call-site class, and write it into
docs/13-editing.md next to the editing rules it governs.
- Apply it to the call sites above.
- Pin it with a test that builds a document long enough for the frontier to fall short of the caret — the measurement above is the recipe. A test on a short document cannot fail and would prove nothing.
Deliberately out of scope
Reworking the parser configuration or the work budget itself. This is about what the app does with an incomplete answer, not about making the answer always complete.
Acceptance
- Each affected call site either waits for a parse up to its position or declines to act, by an explicit rule.
- A test with a document past the parse frontier covers at least the destructive case (emoji replacement inside a fenced block).
docs/13-editing.md states the rule.
npm run typecheck && npm run lint && npm test && npm run build pass.
Affected: src/ui/editor-emoji-replace.ts, src/ui/markdown-live.ts, src/ui/editor-table.ts, src/ui/MarkdownEditor.tsx, docs/13-editing.md
Source: CON-20 code review, measured and reported 2026-09-14.
Task: e6rwkqalgp0apz9nnbvdiclj
Found while reviewing CON-20, measured rather than reasoned about, and deliberately not fixed there — CON-20 did not introduce it and both available fixes are trades that need deciding on their own.
What was measured
CodeMirror's
syntaxTree(state)returns whatever the parser has finished so far. On a large document the parse is bounded by a work budget, so the tree simply stops before the end. On a document of roughly 131 KB the tree ended 20 characters short of the caret:resolveInner(pos, -1)then answeredDocumentinstead of the node actually under the cursor.For CON-20's emoji replacement that means the "am I inside a code block?" guard silently returns "no" and
:smile:is replaced inside a fenced code block — precisely the case the ticket exists to prevent.ensureSyntaxTree(state, pos, timeout)is the API that parses up to a position before answering.Why it is not a CON-20 bug
Bare
syntaxTreeis the pattern everywhere in this editor, not a slip in one new file:src/ui/MarkdownEditor.tsx:638—syntaxTree(view.state).resolveInner(…)src/ui/markdown-live.ts:268,527,542,557src/ui/editor-table.ts:975,1006,1028src/ui/editor-emoji-replace.ts:47(CON-20's, following the same pattern)ensureSyntaxTreeappears nowhere in the repo. So every feature built on the tree — live formatting, table editing, the slash menu's context checks, emoji replacement — inherits the same blind spot past the parse frontier, each with its own consequence.Why it needs a decision rather than a patch
Both fixes cost something real:
ensureSyntaxTreewith a timeout on every keystroke buys correctness with parse latency in the one place latency is most visible — typing. The budget has to be picked, and picked per call site, since a formatting decoration and a destructive text replacement do not deserve the same wait.A third option worth weighing: only the destructive call sites (anything that rewrites the document, like the emoji replacement) need certainty. A decoration that guesses wrong repaints a moment later and costs nothing. That would narrow the change to a couple of places and leave the rest alone.
Task
docs/13-editing.mdnext to the editing rules it governs.Deliberately out of scope
Reworking the parser configuration or the work budget itself. This is about what the app does with an incomplete answer, not about making the answer always complete.
Acceptance
docs/13-editing.mdstates the rule.npm run typecheck && npm run lint && npm test && npm run buildpass.Affected:
src/ui/editor-emoji-replace.ts,src/ui/markdown-live.ts,src/ui/editor-table.ts,src/ui/MarkdownEditor.tsx,docs/13-editing.mdSource: CON-20 code review, measured and reported 2026-09-14.
Task: e6rwkqalgp0apz9nnbvdiclj