From 85c03d33aff63b524c4092e14f71a5d97634680a Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:51:27 +0800 Subject: [PATCH] fix(editor): release a decision to leave whose project change never came (#1178) When the project change after an authorized leave never came, the editors' close decision stayed authorized: files failed with "The project is closing" and later leaves with "An editor close decision is already pending" until restart. An authorized decision with no project opening is now released when the window opens a file or is left again. Fixes #1139 --- src/editor/core/controller.ts | 11 +++++++++++ src/editor/core/customDocuments.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/editor/core/controller.ts b/src/editor/core/controller.ts index 723a27d5a..18ffcb05d 100644 --- a/src/editor/core/controller.ts +++ b/src/editor/core/controller.ts @@ -883,8 +883,18 @@ export class EditorController implements SlotHost { return generation; } + /** + * A decision to leave that was authorized, but whose project change or window close never + * came (the window stayed with its project): released, so the window can open files and + * be left again instead of refusing both until it is restarted. + */ + private releaseStaleClose() { + if (this.closeSession && this.closeCommitted && !useProject.getState().opening) this.closeSession.cancel(); + } + private checkOpening(generation: { project: number; admission: number }) { if (generation.project !== this.projectGeneration) throw new Error("The project changed while opening the editor"); + this.releaseStaleClose(); if (generation.admission !== this.admissionGeneration || this.closeSession || this.closeRequests) throw new Error("The project is closing"); } @@ -1612,6 +1622,7 @@ export class EditorController implements SlotHost { ++this.admissionGeneration; ++this.closeRequests; return this.enqueueRemoval(async () => { + this.releaseStaleClose(); if (this.closeSession) throw new Error("An editor close decision is already pending"); const generation = this.projectGeneration; const documents = [...customDocuments(useTabs.getState(), useContributions.getState().editorKinds).values()]; diff --git a/src/editor/core/customDocuments.test.ts b/src/editor/core/customDocuments.test.ts index b9f8a6993..cb56d13b7 100644 --- a/src/editor/core/customDocuments.test.ts +++ b/src/editor/core/customDocuments.test.ts @@ -631,6 +631,30 @@ describe("custom editor document lifetime", () => { expect(b.saved).toBe("edited"); }); + it("releases an authorized decision to leave when the project never changed", async () => { + const doc = await open(); + const session = await ctrl.prepareClose(); + session.commit(); + expect(doc.frozen).toBe(true); + // The window stayed with its project: files open again, and it can be left again. + await open("b"); + expect(findTab(useTabs.getState(), input("b").key)).not.toBeNull(); + expect(doc.frozen).toBe(false); + const again = await ctrl.prepareClose(); + expect(() => session.commit()).toThrow(); + again.cancel(); + }); + + it("keeps an authorized decision while the project it leaves for is opening", async () => { + await open(); + const session = await ctrl.prepareClose(); + session.commit(); + useProject.setState({ opening: true }); + await expect(open("b")).rejects.toThrow("The project is closing"); + await expect(ctrl.prepareClose()).rejects.toThrow("already pending"); + useProject.setState({ opening: false }); + }); + it("rejects authorization from a cancelled project close session", async () => { const doc = await open(); const session = await ctrl.prepareClose();