From 731cdd243f121e5bd4c3e1a1ae2ffae1675b4d6a Mon Sep 17 00:00:00 2001 From: unknown <1986038808@qq.com> Date: Sun, 20 Sep 2026 11:19:05 +0800 Subject: [PATCH] fix(core): flush debounced change before silent mutations When parseDelayMs is enabled, a pending onChange timer could fire after a silent setDocument or replaceRange and emit stale Markdown against the new document. Flush the scheduled change first so hosts see the last user edit once and AST stays aligned with the loaded buffer. Co-authored-by: Cursor --- packages/core/src/editor.ts | 4 +++ packages/core/test/editor.test.ts | 54 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/packages/core/src/editor.ts b/packages/core/src/editor.ts index 4d356ea2..b9bd3762 100644 --- a/packages/core/src/editor.ts +++ b/packages/core/src/editor.ts @@ -436,6 +436,9 @@ export function createEditor(config: EditorConfig): EditorAPI { if (tableEditFlushed) flushScheduledChangeNow(); const beforeSelection = view.state.selection.main; const silent = opts?.silent === true; + // Host-driven silent loads must not leave a debounced user edit pending — the + // timer would emit stale Markdown against the newly loaded document. + if (silent) flushScheduledChangeNow(); const selection = resolveDocumentSelection( { anchor: beforeSelection.anchor, head: beforeSelection.head }, next.length, @@ -879,6 +882,7 @@ export function createEditor(config: EditorConfig): EditorAPI { replaceRange(from, to, insert, selection, opts) { if (destroyed) return; const silent = opts?.silent === true; + if (silent) flushScheduledChangeNow(); view.dispatch({ changes: { from, to, insert }, selection: selection diff --git a/packages/core/test/editor.test.ts b/packages/core/test/editor.test.ts index 85505eba..cffcc0a6 100644 --- a/packages/core/test/editor.test.ts +++ b/packages/core/test/editor.test.ts @@ -275,6 +275,60 @@ describe("createEditor", () => { editor.destroy(); }); + it("flushes a pending user change before a silent setDocument", () => { + vi.useFakeTimers(); + + const container = document.createElement("div"); + const docs: string[] = []; + const editor = createEditor({ + container, + initialValue: "", + parseDelayMs: 20, + onChange(doc) { + docs.push(doc); + } + }); + + editor.setDocument("before-load"); + expect(docs).toEqual([]); + + editor.setDocument("after-load", { silent: true }); + expect(docs).toEqual(["before-load"]); + expect(editor.getDocument()).toBe("after-load"); + + vi.advanceTimersByTime(20); + expect(docs).toEqual(["before-load"]); + + editor.destroy(); + }); + + it("flushes a pending user change before a silent replaceRange", () => { + vi.useFakeTimers(); + + const container = document.createElement("div"); + const docs: string[] = []; + const editor = createEditor({ + container, + initialValue: "abc", + parseDelayMs: 20, + onChange(doc) { + docs.push(doc); + } + }); + + editor.setDocument("abcdef"); + expect(docs).toEqual([]); + + editor.replaceRange(0, 6, "synced", undefined, { silent: true }); + expect(docs).toEqual(["abcdef"]); + expect(editor.getDocument()).toBe("synced"); + + vi.advanceTimersByTime(20); + expect(docs).toEqual(["abcdef"]); + + editor.destroy(); + }); + it("defers change emission until IME composition ends", async () => { const container = document.createElement("div"); const docs: string[] = [];