From 7b67bac3bb942e20c39a3709df34284e0d1a897f Mon Sep 17 00:00:00 2001 From: Moudy Date: Thu, 16 Jul 2026 18:42:47 -0700 Subject: [PATCH] Use the editor's syntax tree instead of re-parsing the full document --- CHANGELOG.md | 6 ++++++ src/text.ts | 45 ++++++++++++++++++--------------------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eda236..97caa00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +### Bug fixes + +Use the editor's incrementally-parsed syntax tree instead of synchronously re-parsing the entire document on every update. The previous full-document `parser.parse(doc.toString())` ran unbudgeted on the main thread and could block for seconds on large documents (or pathological ones, e.g. via quadratic parser edge cases), freezing the editor whenever the minimap was enabled. + ## 0.5.1 (2023-10-25) ### Bug fixes diff --git a/src/text.ts b/src/text.ts index 1e6bcb7..b2be95a 100644 --- a/src/text.ts +++ b/src/text.ts @@ -1,19 +1,17 @@ import { LineBasedState } from "./linebasedstate"; import { Highlighter, highlightTree } from "@lezer/highlight"; -import { ChangedRange, Tree, TreeFragment } from "@lezer/common"; -import { highlightingFor, language } from "@codemirror/language"; +import { highlightingFor, syntaxTree } from "@codemirror/language"; import { EditorView, ViewUpdate } from "@codemirror/view"; import { DrawContext } from "./types"; import { Config, Options, Scale } from "./Config"; import { LinesState, foldsChanged } from "./LinesState"; import crelt from "crelt"; -import { ChangeSet, EditorState } from "@codemirror/state"; +import { EditorState } from "@codemirror/state"; type TagSpan = { text: string; tags: string }; type FontInfo = { color: string; font: string; lineHeight: number }; export class TextState extends LineBasedState> { - private _previousTree: Tree | undefined; private _displayText: Required["displayText"] | undefined; private _fontInfoMap: Map = new Map(); private _themeClasses: Set | undefined; @@ -50,6 +48,11 @@ export class TextState extends LineBasedState> { return true; } + // If the editor's syntax tree advanced (e.g. async parsing progressed) + if (syntaxTree(update.state) !== syntaxTree(update.startState)) { + return true; + } + return false; } @@ -64,10 +67,10 @@ export class TextState extends LineBasedState> { : clearTimeout(this._highlightingCallbackId); } - this.updateImpl(update.state, update.changes); + this.updateImpl(update.state); } - private updateImpl(state: EditorState, changes?: ChangeSet) { + private updateImpl(state: EditorState) { this.map.clear(); /* Store display text setting for rendering */ @@ -78,27 +81,15 @@ export class TextState extends LineBasedState> { this._fontInfoMap.clear(); } - /* Incrementally parse the tree based on previous tree + changes */ - let treeFragments: ReadonlyArray | undefined = undefined; - if (this._previousTree && changes) { - const previousFragments = TreeFragment.addTree(this._previousTree); - - const changedRanges: Array = []; - changes.iterChangedRanges((fromA, toA, fromB, toB) => - changedRanges.push({ fromA, toA, fromB, toB }) - ); - - treeFragments = TreeFragment.applyChanges( - previousFragments, - changedRanges - ); - } - - /* Parse the document into a lezer tree */ - const docToString = state.doc.toString(); - const parser = state.facet(language)?.parser; - const tree = parser ? parser.parse(docToString, treeFragments) : undefined; - this._previousTree = tree; + /** + * Use the editor's incrementally-maintained, work-budgeted syntax tree + * rather than re-parsing the entire document here. A synchronous full + * parse can block the main thread for seconds on large or pathological + * documents. The tree may not cover the whole document yet; as the + * editor's background parse advances, `shouldUpdate` re-runs this and + * highlighting converges. + */ + const tree = syntaxTree(state); /* Highlight the document, and store the text and tags for each line */ const highlighter: Highlighter = {