diff --git a/src/codemirror/Cm6_ViewPlugin.ts b/src/codemirror/Cm6_ViewPlugin.ts index 783d0a7..1441df3 100644 --- a/src/codemirror/Cm6_ViewPlugin.ts +++ b/src/codemirror/Cm6_ViewPlugin.ts @@ -55,7 +55,14 @@ export function createCm6Plugin(plugin: ShikiPlugin) { * @param update */ update(update: ViewUpdate): void { - this.decorations = this.decorations.map(update.changes); + try { + this.decorations = this.decorations.map(update.changes); + } catch (e) { + // Decorations may have stale positions if the document changed while an async + // updateWidgets call was in flight. Reset them so the next update can rebuild. + this.decorations = Decoration.none; + console.warn('Resetting decorations due to error:', e); + } // we handle doc changes and selection changes here if (update.docChanged || update.selectionSet) { @@ -79,6 +86,9 @@ export function createCm6Plugin(plugin: ShikiPlugin) { let lang = ''; let state: SyntaxNode[] = []; const decorationUpdates: DecorationUpdate[] = []; + // Capture the state at the time of the syntax tree traversal so we can + // detect if the document changed while async decoration building was in flight. + const capturedState = view.state; // const t1 = performance.now(); @@ -172,6 +182,11 @@ export function createCm6Plugin(plugin: ShikiPlugin) { this.removeDecoration(node.from, node.to); } else if (node.type === DecorationUpdateType.Insert) { const decorations = await this.buildDecorations(node.hideTo ?? node.from, node.to, node.lang, node.content); + // If the document changed while we were awaiting, the positions we captured + // from the syntax tree are stale. Abort to avoid applying out-of-range decorations. + if (this.view.state !== capturedState) { + return; + } this.removeDecoration(node.from, node.to); if (node.hideLang) { // add the decoration that hides the language tag