From 30b054919636fcb6efff3b7cc703ed0fa6ffee91 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 4 Nov 2026 21:57:59 +0800 Subject: [PATCH] fix(editor): don't autosave while focus is in a popup over the editor Opening a popup (intentions, completion) moved focus out of the editor and autosaved it. The save cleanup could change the text, such as trailing spaces on another line, so the quick fix the popup listed no longer matched and the first attempt to apply it failed. While focus is in a popup the editor is not saved; it is saved when focus moves on from the popup to anything but the editor. --- src/editor/core/controller.test.ts | 45 ++++++++++++++++++++++++++++++ src/editor/core/controller.ts | 20 ++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/editor/core/controller.test.ts b/src/editor/core/controller.test.ts index 085a9e29..09cb1b05 100644 --- a/src/editor/core/controller.test.ts +++ b/src/editor/core/controller.test.ts @@ -21,6 +21,51 @@ afterEach(() => { }); describe("EditorController", () => { + it("autosaves on blur, but not while focus is in a popup over the editor", async () => { + const backend = createMemoryBackend({ "/p/a.c": "int main() {\n \n}\n" }); + const save = vi.spyOn(backend, "save"); + ctrl = new EditorController(backend); + ctrl.start(); + await ctrl.openFile("/p/a.c", { focus: false }); + const slot = ctrl.activeSlot!; + const host = document.createElement("div"); + const popup = document.createElement("div"); + popup.setAttribute("data-popup", ""); + const popupList = document.createElement("button"); + popup.appendChild(popupList); + const elsewhere = document.createElement("button"); + document.body.append(host, popup, elsewhere); + try { + const view = slot.mount(host); + ctrl.onMounted(slot); + const type = () => view.dispatch({ changes: { from: 0, insert: "x" }, userEvent: "input.type" }); + // Into a popup (⌥↵ intentions): nothing is saved while it is open. + view.focus(); + type(); + popupList.focus(); + await new Promise((r) => setTimeout(r, 20)); + expect(save).not.toHaveBeenCalled(); + // Back to the editor (popup closed): still nothing. + view.focus(); + await new Promise((r) => setTimeout(r, 20)); + expect(save).not.toHaveBeenCalled(); + // From the popup elsewhere: saved then. + popupList.focus(); + await new Promise((r) => setTimeout(r, 20)); + elsewhere.focus(); + await vi.waitFor(() => expect(save).toHaveBeenCalledTimes(1)); + // Straight elsewhere: saved. + view.focus(); + type(); + elsewhere.focus(); + await vi.waitFor(() => expect(save).toHaveBeenCalledTimes(2)); + } finally { + host.remove(); + popup.remove(); + elsewhere.remove(); + } + }); + it("leaves modified Enter to a non-text editor after switching from a focused file", async () => { ctrl = new EditorController(createMemoryBackend({ "/p/a.ts": "one\ntwo\n" })); ctrl.start(); diff --git a/src/editor/core/controller.ts b/src/editor/core/controller.ts index 05f12416..20874e90 100644 --- a/src/editor/core/controller.ts +++ b/src/editor/core/controller.ts @@ -588,7 +588,21 @@ export class EditorController implements SlotHost { onBlur(slot: EditorSlot) { setContext("editorTextFocus", false); - if (this.autoSave()) void slot.model.save("auto").catch(() => {}); + if (!this.autoSave()) return; + const save = () => void slot.model.save("auto").catch(() => {}); + if (typeof document === "undefined") return save(); + // A popup (intentions, completion, …) belongs to the editor: saving under it would change + // the text (save cleanup) its items were computed for. Save once focus leaves it elsewhere. + setTimeout(() => { + if (slot.view?.hasFocus) return; + if (!inPopup(document.activeElement)) return save(); + const onFocusIn = (e: FocusEvent) => { + if (inPopup(e.target)) return; + document.removeEventListener("focusin", onFocusIn); + if (!slot.view?.hasFocus) save(); + }; + document.addEventListener("focusin", onFocusIn); + }); } onSelection(slot: EditorSlot, navigatedFrom?: { line: number; col: number }) { @@ -1892,3 +1906,7 @@ function revealLabel(): string { const os = usePlatform.getState().os; return os === "macos" ? "Finder" : os === "windows" ? "Explorer" : "Files"; } + +function inPopup(target: EventTarget | null): boolean { + return target instanceof Element && !!target.closest("[data-popup]"); +}