Repository navigation
Phase 23: pinned reparse windows; serialize without a whole-output parse (v0.1.20) - #23
Merged
Merged
Conversation
…arse reparseBlocks stitched the old blocks after its window without checking that the edit had not run into them, so an unclosed fence, display-math block or HTML comment was read as one short block followed by the old suffix, where a whole parse has it run to the end of the text. A window now opens at the start of the untouched block before it and must close with the untouched block after it, exactly as that block was; otherwise the reparse reads on to the end. Frontmatter, decided by unbounded lookahead, is refused by a window that opens the note and stops short of its close. serializeDocument uses the same pinned windows to build its next document from the blocks it moved plus small reparses around what changed, instead of parsing its whole output again. On an 8 MB note a one-block save goes from about 130-200 ms to 40-80 ms. Line-ending detection uses two native scans instead of a character loop. Property tests hold both to a whole parse on generated edits and saves (LF, CRLF, BOM, no final newline, no frontmatter). Phase 23, v0.1.20. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011Zsubk1jzejiCfkbLeWVQd
lr00rl
pushed a commit
to roobli/Noto
that referenced
this pull request
Sep 30, 2026
@roobli/md 0.1.20 (roobli/md#23, merge commit 2e723e8) pins every reparse window at both edges. That fixes a wrong answer on the path Noto uses to replace the editor's content with new text (leaving Source Mode, taking a change made on disk, plugin transforms): an unclosed fence, math block or comment was read as one short block with the old blocks after it, where the file has them inside it. The same windows let the engine's serializer build its next document without parsing its whole output: on the 8 MB benchmark document the engine's part of a save fell from 130-203 ms to 41-77 ms. Pinned by commit until the v0.1.20 tag is pushed. The license inventory is regenerated; prior-split-cache gets a test that fails on 0.1.19. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011Zsubk1jzejiCfkbLeWVQd
4 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
reparseBlocks/reparseFromTextreparsed a window around an edit and then appended the old blocks after it, shifted, without checking that the edit had not run into them. An edit that opens a code fence, a display-math block or an HTML comment and does not close it was read as one short block followed by the old suffix. A whole parse runs it to the end of the text.Noto calls this from
replaceMarkdown: leaving Source Mode, taking an external change on reload, and plugin transforms.The fix: pin both edges of the window (
src/window.ts)The scanner decides where a top-level block starts from that block's first line and everything above it. So a window can stand in for the whole parse when:
Frontmatter is the one block decided by unbounded lookahead:
---on line 1 is frontmatter if a close comes anywhere later, and a thematic break otherwise. A window that opens the note with one and stops short of its close is refused.neighborSlacknow only trades window size against how often the fallback is needed. It no longer decides whether the result is correct.Serialize without a whole-output parse
serializeDocumentused to parse its entire output again to build the next document. It now records where each unit landed and builds the document from the blocks it moved. It reparses, with the same pinned windows, only around changed units, around seams where blocks were deleted (two lists can merge), and at the ends of the note when those change. If any window does not hold, it falls back to the whole parse, as before. Line-ending detection now uses two native scans instead of a character loop.On an 8 MB note (43,970 blocks), one edited paragraph:
serializeDocumentTests
reparse-window.test.tscovers:<script>at slack 0/1/2;parseBlocks.Five of the seven tests fail on
main.serialize-incremental.test.tsruns 500 generated saves each over LF, CRLF, BOM, no final newline and no frontmatter. Every document is compared withparseDocument(outputBytes)(envelope, text, leading, gaps, trailing, every block). It also covers the unclosed fence, list merge, first and last block deleted, final newline changed, and frontmatter inserted.A local stress run (6,000 × 5 saves, 8,000 × 2 edits) passes.
pnpm verifypasses: 132 tests.Noto's unit suite passes against this build (2,116 tests), including its own equivalence test for the save path.
Version 0.1.20; CHANGELOG, README, contract and roadmap are updated.
🤖 Generated with Claude Code
https://claude.ai/code/session_011Zsubk1jzejiCfkbLeWVQd
Generated by Claude Code