Skip to content

Test(editor): a parse that ran out of time reads the rest of the note again - #940

Merged
adibhanna merged 1 commit into
v2.70.0from
fix/note-parse-reuse
Oct 14, 2026
Merged

adibhanna merged 1 commit into
v2.70.0from
fix/note-parse-reuse

Conversation

@adibhanna

Copy link
Copy Markdown
Contributor

After 2.69.1, the note grammar's habit of reading text again looked like waste worth removing. It is not, so this PR changes no code: it records why, and pins it with a test.

What was tried

noteMarkdown's outer tree always spans the whole note. When a state update's 20 ms parse slice runs out, CodeMirror drops the older tree past the point the parse reached, and the parse that resumes reads that text again instead of reusing its blocks. When the body start moves (the closing frontmatter fence typed or broken), parseMixed loses the old mount and the body is parsed in full.

A custom mount that reused the older trees in both cases brought block reuse after a cut from 0 of 390 to 389 of 390, as plain markdown does. A lockstep fuzz then checked every incremental tree against a fresh parse (2,000 seeds of 25 random edits each, two generators, mounts and code fence overlays compared):

Parser Stale trees in 50,026 checks
noteMarkdown as it is 3
noteMarkdown with that reuse 29
plain markdown() 205

The causes are in Lezer's markdown reuse. A fragment that starts at the start of the parse kept an indented code block whose indent had been deleted along with the frontmatter. A paragraph directly above a table stays split from the table's lines when the table's delimiter row breaks; that one also happens on an ordinary edit, today and with plain markdown, and heals on the next edit nearby. Reading again after a cut and after a moved body start is what keeps the grammar out of the extra cases.

The time the reuse would save is small. A typed character parses in about 4 ms in an 800 KB note, well inside its slice, so a cut needs a slow machine or a pause first; reading the rest after one costs what opening the note costs (6, 21 and 84 ms at 50, 200 and 800 KB), in background slices.

What changes

  • The header of cm-markdown-language.ts says why both full reads stay, and that syntaxTree(state).length is always the whole note, so code that needs to know how far the parse got asks ensureSyntaxTree or syntaxTreeAvailable (as headingAbove in table-to-database.ts does since 2.69.1).
  • A new test, "reads the text past a parse that ran out of time again", pins the read after a cut, next to the existing test that pins the full parse when the closing fence is typed. Both fail on the reusing version.

No behavior change, and not a release: this can ride with the next desktop release.

How to test locally

  1. cd packages/app-core && npx vitest run src/lib/cm-markdown-language.test.ts: 18 tests pass.
  2. npx tsc --noEmit -p tsconfig.json in packages/app-core: clean.

… again

After 2.69.1, the note grammar's habit of reading text again looked like
waste worth removing. noteMarkdown's outer tree always spans the whole note,
so when a state update's 20 ms parse slice runs out, CodeMirror drops the
older tree past the point the parse reached, and the parse that resumes
reads that text again instead of reusing its blocks.

Reusing those blocks was tried, and it is not waste. Lezer's markdown reuse
can keep a block whose meaning changed with text outside it: an indented
code block whose indent was deleted along with the frontmatter, a paragraph
whose end moved when a table row under it broke. The grammar happens to read
again exactly where that bites. Checked against a fresh parse 50,000 times
over 100,000 random edits, the grammar as it is was stale 3 times, with that
reuse 29 times, and plain markdown() 205 times. The time it would save is
small: a typed character parses in about 4 ms in an 800 KB note, inside its
slice, so a cut needs a slow machine or a pause first, and reading the rest
after one costs what opening the note costs, in background slices.

So the parser does not change. The header of cm-markdown-language.ts says
why both full reads (after a cut, and when the body start moves) stay, and
that syntaxTree(state).length is always the whole note, so code that needs
to know how far the parse got asks ensureSyntaxTree or syntaxTreeAvailable.
A new test pins the read after a cut, next to the one that pins the full
parse when the closing fence is typed; both fail on the reusing version.
@adibhanna
adibhanna changed the base branch from main to v2.70.0 October 14, 2026 16:54
@adibhanna
adibhanna merged commit 9d18a10 into v2.70.0 Oct 14, 2026
10 checks passed
@adibhanna
adibhanna deleted the fix/note-parse-reuse branch October 14, 2026 16:55
@adibhanna adibhanna mentioned this pull request Oct 14, 2026
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.

1 participant