Repository navigation
fix: keep block gutters only for the blocks in view - #677
Merged
Merged
Conversation
# Conflicts: # CHANGELOG.md
This was referenced Oct 10, 2026
Azganoth
added a commit
that referenced
this pull request
Oct 10, 2026
## Summary On every document change, `prosemirror-highlight` 0.15.3 (under `@milkdown/plugin-highlight`) gathered every code block's tokens and built one new `DecorationSet` from them, which costs top-level blocks times tokens. Typing anywhere in a document with much code slowed with the square of its code. - A Leafdown plugin replaces the package's plugin and keeps the same Shiki parser, so the decorations it produces are unchanged. It highlights every code block when the document opens; after that, each transaction maps the existing set and highlights again only the code blocks in its changed ranges. An edit outside code does no highlighting work. - Two cases need more than the changed ranges. A node whose own opening or closing token changed, such as a new quote wrapped around blocks, has every code block inside it highlighted again, because mapping drops their block decorations once they fail validation under the new parent. An attribute step maps no positions, so the node it changes is added as a range. - `@milkdown/plugin-highlight` stays as the source of the Shiki `createParser`. - `docs/architecture.md` describes the incremental highlighting, and `CHANGELOG.md` records the faster typing under Fixed. ## Related Issue Closes #673 ## Verification - Focused tests compare the plugin's decorations with a full rebuild after edits inside, between, and across code blocks, splits and joins, language changes through `setNodeMarkup` and `setNodeAttribute`, conversions between code and paragraph, wrapping in and lifting out of a quote, multi-step transactions, and whole-document replacement, and they assert which blocks the parser runs for. Disabling the wrapped-node rule, the removal of stale decorations, or the attribute-step range each makes them fail. - `pnpm check:frontend` passed. Manually verified in release builds on Windows 11, under a separate application identity in a 1280×900 window, driven through the WebView2 DevTools protocol: 1. The fixture was 1 MB of fenced code: 930 30-line excerpts of the repository's TypeScript, Rust, and CSS, each after a one-sentence paragraph. Each run opened it in a fresh process and typed `x` five times at the end of the middle top-level paragraph, then five times at the end of the middle code block, waiting for 1.5 s without long frames after each keystroke. Two runs of `main` at 7cea7b9 were interleaved with two of this branch; figures are each run's median keystroke. | Keystroke | `main` | This branch | | --- | --- | --- | | Paragraph, key press to paint | 2.26–3.39 s | 1.64–1.78 s | | Paragraph, long frames without forced layout | 0.76–1.08 s | 0.33–0.37 s | | Code block, key press to paint | 2.10–2.52 s | 1.77–1.78 s | | Code block, long frames without forced layout | 0.79–0.96 s | 0.40–0.42 s | 2. Characters typed into the code block were highlighted with Shiki's light and dark colours in the release build. Not verified: opening times were not compared beyond noting they stayed within the same noisy 12.6–19.7 s range on both builds. ## Notes Both builds predate #677, so their paint times include more than 1 s of forced layout per keystroke from the block gutters it since removed, and the machine slowed by up to twice between runs. The frame time without forced layout is the stable comparison. `codeLineNumbers.ts` also rebuilds its whole decoration set on every edit while code line numbers are shown. It is left unchanged here.
This was referenced Oct 10, 2026
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.
Summary
Every edit rebuilt a gutter for every selectable block and positioned each one by reading the block's layout right after writing the previous gutter's styles, so a keystroke forced one document layout per block. A node decoration on every block also rewrote a position attribute on each block after the edit and built its decoration set at a cost of blocks times decorations. Typing therefore slowed with the square of the document's size: 3.4 s per character at 500 KB.
data-leafdown-block-pos; hover and drop find their block through a map of the elements in view, and the gutter and handle keep the position. Only the blocks in view are watched for resizing, and the selectable-block traversal is computed once per document state.The E2E specs used the removed block attribute to pair a handle with its block. They now read the position from ProseMirror's
pmViewDescon the rendered element, an internal that is stable but undocumented. The block-selection fixture is taller than the E2E window, so its last three list items have no gutter at the default size:getGeometryForTextscrolls a block into view before reading its handle, list-item counts and order come from the document, and the list-item spacing check measures the items directly.Related Issue
Closes #670
Verification
pnpm exec vitest run src/features/editor/plugins/blockSelectionInteraction.test.ts: a gutter appears as its block scrolls into view and is dropped once it leaves; a container reaching into view from above keeps a gutter fitted to its visible part while a child above the view has none; an edit to a 400-block document with a folded section reads a bounded number of block rects, rebuilds only the gutters in view, and leaves no decoration on any block. These tests fail against the previous plugin. The clipping test now expects no gutter, rather than a hidden one, for blocks wholly outside the view.pnpm check:frontendpassed.Manually verified in an isolated Windows 11 release build driven over the WebView2 DevTools protocol, with documents built by concatenating
README.md,CONTRIBUTING.md,docs/*.md, anddocs/help/*.mdto each size. Keystroke time is from key down to the next paint after typing at the end of a paragraph near the middle, the median of five keystrokes on this branch and three onmain; layouts are counted in a trace of one keystroke; open time is from choosing the recent file to the end of the last long frame, one open each. Same session, same machine:main(7cea7b9)Not verified: the desktop E2E specs were adapted without running them locally; CI runs them.
Notes
About 200 ms after each keystroke, a 270–580 ms frame remains at 500 KB. It is the debounced whole-document serialization, with no forced layout, which #672 covers. Opening 1 MB still exceeds the 3 s budget #638 proposes, and the gutters no longer account for that time.