From a6cf4442d846cef7b3b3c94b85d32fa8a877dedd Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Fri, 6 Nov 2026 06:33:12 +0800 Subject: [PATCH] fix(editor): auto-save when the app is deactivated while focus is in a frame (#1636) With focus in a plugin's web view, the window's blur fires when the frame takes focus, before any edit, and not again when the user switches applications, so edits made in the frame weren't auto-saved. The editor now also saves on the application-deactivated event. --- src/editor/core/backend.ts | 11 +++++++++++ src/editor/core/controller.ts | 2 ++ src/editor/core/customDocuments.test.ts | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/src/editor/core/backend.ts b/src/editor/core/backend.ts index 8c1cfbef..e75ce8d8 100644 --- a/src/editor/core/backend.ts +++ b/src/editor/core/backend.ts @@ -21,6 +21,7 @@ import { type SyntaxHighlights, type TextChange, } from "@/platform/ipc"; +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { EditorState } from "@codemirror/state"; import { customRegions } from "./cm/regions"; @@ -60,6 +61,11 @@ export interface DocBackend { onDirty(handler: (e: { doc: DocId; dirty: boolean }) => void): () => void; /** Files changed on disk (used to refresh nothing but closed-file state). */ onFsChanged?(handler: (e: { removed: string[] }) => void): () => void; + /** + * Every OpenIDEA window lost focus to another application. Also seen while focus is in a frame + * (a plugin's web view), which hides the window's own `blur`. + */ + onApplicationDeactivated?(handler: () => void): () => void; /** * An open document's language changed (Settings › File Types, Override File * Type), or its own mode (`DocumentInfo.uiMode`). @@ -86,6 +92,11 @@ export const ipcBackend: DocBackend = { onPatched: (h) => onIdeEvent("docPatched", (e) => h(e)), onDirty: (h) => onIdeEvent("docDirty", (e) => h(e)), onFsChanged: (h) => onIdeEvent("fsChanged", (e) => h(e)), + onApplicationDeactivated: (h) => { + if (!isTauri()) return () => {}; + const listener = getCurrentWebviewWindow().listen("ide://application-deactivated", () => h()); + return () => void listener.then((unlisten) => unlisten()); + }, onLanguage: (h) => onIdeEvent("docLanguage", (e) => h(e)), }; diff --git a/src/editor/core/controller.ts b/src/editor/core/controller.ts index a883e509..3c31d476 100644 --- a/src/editor/core/controller.ts +++ b/src/editor/core/controller.ts @@ -353,6 +353,8 @@ export class EditorController implements SlotHost { void this.saveCustomDocuments(true).catch((e) => this.documentError(e)); } }; + if (this.manager.backend.onApplicationDeactivated) + this.disposers.push(this.manager.backend.onApplicationDeactivated(onWindowBlur)); const onPageHide = () => this.saveSession(); if (typeof window !== "undefined") { if (this.manager.backend.syncFromDisk) diff --git a/src/editor/core/customDocuments.test.ts b/src/editor/core/customDocuments.test.ts index cb56d13b..8eaaae8f 100644 --- a/src/editor/core/customDocuments.test.ts +++ b/src/editor/core/customDocuments.test.ts @@ -481,6 +481,24 @@ describe("custom editor document lifetime", () => { expect(findTab(useTabs.getState(), input().key)).toBeNull(); }); + it("autosaves when the application is deactivated while focus is in a frame", async () => { + ctrl.stop(); + let deactivated = () => {}; + ctrl = new EditorController({ + ...createMemoryBackend(), + onApplicationDeactivated: (handler) => { + deactivated = handler; + return () => {}; + }, + }); + ctrl.start(); + const doc = await open(); + useProject.setState({ settings: { files: { autoSave: true } } as never }); + // Focus inside a plugin's frame: the window's own blur has already fired, before the edit. + deactivated(); + await vi.waitFor(() => expect(doc.saved).toBe("edited")); + }); + it("does not prompt or blur-autosave a document until execution has drained", async () => { const doc = await open(); useProject.setState({ settings: { files: { autoSave: true } } as never });