Skip to content

Editor features read a lazily-parsed syntax tree, so on a long page they act on the wrong answer #82

Description

@usekaneo

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions