diff --git a/src/editor/core/model/document.test.ts b/src/editor/core/model/document.test.ts index 30b21b5b6..ff414f244 100644 --- a/src/editor/core/model/document.test.ts +++ b/src/editor/core/model/document.test.ts @@ -347,6 +347,28 @@ describe("DocumentManager + slots", () => { for (const slot of slots) slot.dispose(); manager.stop(); }); + + it("restores a destroyed kept view at its exact scroll offset, not its top line", async () => { + const { manager } = setup({ "/p/a.txt": "line\n".repeat(500) }); + const model = await manager.acquire("/p/a.txt"); + const slot = new EditorSlot(model, "t0", host); + const view = slot.mount(document.body); + Object.defineProperty(view.scrollDOM, "scrollTop", { value: 137, configurable: true }); + view.scrollDOM.dispatchEvent(new Event("scroll")); + slot.unmount(); + const expected = view.scrollSnapshot().value as { range: unknown; yMargin: number; isSnapshot: boolean }; + const others = Array.from({ length: 8 }, (_, i) => new EditorSlot(model, `t${i + 1}`, host)); + for (const other of others) { + other.mount(document.body); + other.unmount(); + } + const restored = (slot as unknown as { scroll: { value: typeof expected } }).scroll.value; + expect(restored.isSnapshot).toBe(true); + expect(restored.range).toEqual(expected.range); + expect(restored.yMargin).toBe(expected.yMargin); + for (const s of [slot, ...others]) s.dispose(); + manager.stop(); + }); }); describe("save cleanup", () => { diff --git a/src/editor/core/model/slot.ts b/src/editor/core/model/slot.ts index 77b180ae7..5e2673179 100644 --- a/src/editor/core/model/slot.ts +++ b/src/editor/core/model/slot.ts @@ -242,9 +242,8 @@ export class EditorSlot { this.kept = null; const i = keptSlots.indexOf(this); if (i >= 0) keptSlots.splice(i, 1); - const top = topLineAt(view, this.scrollPos.top); - this.savedTopLine = top; - this.scroll = EditorView.scrollIntoView(view.state.doc.line(top + 1).from, { y: "start" }); + this.savedTopLine = topLineAt(view, this.scrollPos.top); + this.scroll = scrollSnapshotAt(view, this.scrollPos); this.stored = view.state; view.destroy(); } @@ -399,6 +398,13 @@ function topLineOf(view: EditorView): number { return topLineAt(view, view.scrollDOM.scrollTop); } +/** `view.scrollSnapshot()` at a tracked offset: a detached view's scrollDOM reports 0. */ +function scrollSnapshotAt(view: EditorView, pos: { top: number; left: number }): StateEffect { + const atPos = Object.create(view) as EditorView; + Object.defineProperty(atPos, "scrollDOM", { value: { scrollTop: pos.top, scrollLeft: pos.left } }); + return atPos.scrollSnapshot(); +} + function topLineAt(view: EditorView, scrollTop: number): number { const block = view.lineBlockAtHeight(Math.max(0, scrollTop)); return view.state.doc.lineAt(block.from).number - 1;