From 62c903cc3945904d0f9c513a82ebc85f850329d8 Mon Sep 17 00:00:00 2001 From: Oluwatosin Alli Date: Thu, 8 Oct 2026 09:11:23 +0100 Subject: [PATCH] fix(code-index): refresh status routing on workspace-folder changes Status routing only refreshed on active-editor changes, so a workspace-folder change that switched the selected workspace left the UI subscribed to the previous manager until the editor changed. Closes #1857 --- .../code-index-status-manager.spec.ts | 97 ++++++++++++++++++- .../code-index/code-index-status-manager.ts | 7 ++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/services/code-index/__tests__/code-index-status-manager.spec.ts b/src/services/code-index/__tests__/code-index-status-manager.spec.ts index c73fb4f03b..f640836b3a 100644 --- a/src/services/code-index/__tests__/code-index-status-manager.spec.ts +++ b/src/services/code-index/__tests__/code-index-status-manager.spec.ts @@ -20,7 +20,12 @@ vi.mock("../state-manager") vi.mock("vscode", () => ({ window: { onDidChangeActiveTextEditor: vi.fn(), activeTextEditor: undefined }, - workspace: { workspaceFolders: undefined, getWorkspaceFolder: vi.fn(), getConfiguration: vi.fn() }, + workspace: { + workspaceFolders: undefined, + getWorkspaceFolder: vi.fn(), + getConfiguration: vi.fn(), + onDidChangeWorkspaceFolders: vi.fn(), + }, })) function makeSource(workspacePath: string) { @@ -59,6 +64,8 @@ function makeSource(workspacePath: string) { describe("CodeIndexStatusManager", () => { let editorChanged: () => void let disposeEditor: ReturnType void>> + let workspaceFoldersChanged: () => void + let disposeWorkspaceFolders: ReturnType void>> beforeEach(() => { vi.clearAllMocks() @@ -74,6 +81,11 @@ describe("CodeIndexStatusManager", () => { editorChanged = () => listener(undefined) return { dispose: disposeEditor } }) + disposeWorkspaceFolders = vi.fn() + vi.mocked(vscode.workspace.onDidChangeWorkspaceFolders).mockImplementation((listener) => { + workspaceFoldersChanged = () => listener({ added: [], removed: [] }) + return { dispose: disposeWorkspaceFolders } + }) }) it("resolves the workspace before requesting its manager", () => { @@ -308,4 +320,87 @@ describe("CodeIndexStatusManager", () => { expect(source.subscription.dispose).not.toHaveBeenCalled() manager.dispose() }) + + it("switches to the newly selected workspace on a workspace-folder change alone", () => { + const first = makeSource("/first") + const second = makeSource("/second") + const resolve = vi.fn((path: string) => (path === "/first" ? first : second)) + const publish = vi.fn() + const manager = new CodeIndexStatusManager(resolve) + manager["publishStatus"] = publish + try { + manager.init() + expect(publish).toHaveBeenCalledExactlyOnceWith(first.status) + Object.defineProperty(vscode.workspace, "workspaceFolders", { + configurable: true, + value: [{ uri: makeUri("/second"), name: "second", index: 0 }], + }) + workspaceFoldersChanged() + expect(resolve).toHaveBeenLastCalledWith("/second") + expect(first.subscription.dispose).toHaveBeenCalledTimes(1) + expect(second.onProgressUpdate).toHaveBeenCalledTimes(1) + expect(publish).toHaveBeenCalledTimes(2) + expect(publish).toHaveBeenLastCalledWith(second.status) + first.emit() + expect(publish).toHaveBeenCalledTimes(2) + second.status.message = "Updated" + second.emit() + expect(publish).toHaveBeenCalledTimes(3) + expect(publish).toHaveBeenLastCalledWith(second.status) + } finally { + manager.dispose() + } + }) + + it("keeps the subscription when a workspace-folder change leaves the selection unchanged", () => { + const source = makeSource("/first") + const publish = vi.fn() + const manager = new CodeIndexStatusManager(() => source) + manager["publishStatus"] = publish + manager.init() + workspaceFoldersChanged() + expect(source.onProgressUpdate).toHaveBeenCalledTimes(1) + expect(source.subscription.dispose).not.toHaveBeenCalled() + expect(publish).toHaveBeenCalledExactlyOnceWith(source.status) + manager.dispose() + }) + + it("releases the subscription when a workspace-folder change leaves no workspace", () => { + const source = makeSource("/first") + const publish = vi.fn() + const manager = new CodeIndexStatusManager(() => source) + manager["publishStatus"] = publish + manager.init() + Object.defineProperty(vscode.workspace, "workspaceFolders", { configurable: true, value: [] }) + workspaceFoldersChanged() + expect(source.subscription.dispose).toHaveBeenCalledTimes(1) + source.emit() + expect(publish).toHaveBeenCalledExactlyOnceWith(source.status) + manager.dispose() + }) + + it("releases the workspace-folder listener on disposal even when its cleanup fails", () => { + const source = makeSource("/first") + const manager = new CodeIndexStatusManager(() => source) + const folderError = new Error("workspace folders cleanup failed") + const log = vi.spyOn(console, "error").mockImplementation(() => {}) + try { + manager.init() + disposeWorkspaceFolders.mockImplementationOnce(() => { + throw folderError + }) + expect(() => manager.dispose()).not.toThrow() + expect(log).toHaveBeenCalledExactlyOnceWith( + "[CodeIndexStatusManager] Failed to dispose workspace folders subscription:", + folderError, + ) + expect(disposeEditor).toHaveBeenCalledTimes(1) + expect(source.subscription.dispose).toHaveBeenCalledTimes(1) + manager.dispose() + expect(disposeWorkspaceFolders).toHaveBeenCalledTimes(1) + expect(log).toHaveBeenCalledTimes(1) + } finally { + log.mockRestore() + } + }) }) diff --git a/src/services/code-index/code-index-status-manager.ts b/src/services/code-index/code-index-status-manager.ts index 2461630eaa..bffd9a5cdb 100644 --- a/src/services/code-index/code-index-status-manager.ts +++ b/src/services/code-index/code-index-status-manager.ts @@ -10,6 +10,7 @@ export class CodeIndexStatusManager implements vscode.Disposable { private currentManager?: CodeIndexManager private progressSubscription?: vscode.Disposable private editorSubscription?: vscode.Disposable + private workspaceFoldersSubscription?: vscode.Disposable public constructor( private readonly getManagerForWorkspace: (workspacePath: string) => CodeIndexManager | undefined, @@ -17,6 +18,9 @@ export class CodeIndexStatusManager implements vscode.Disposable { public init(): void { this.editorSubscription = vscode.window.onDidChangeActiveTextEditor(() => this.updateSubscription()) + this.workspaceFoldersSubscription = vscode.workspace.onDidChangeWorkspaceFolders(() => + this.updateSubscription(), + ) this.updateSubscription() } @@ -69,11 +73,14 @@ export class CodeIndexStatusManager implements vscode.Disposable { public dispose(): void { const editorSubscription = this.editorSubscription + const workspaceFoldersSubscription = this.workspaceFoldersSubscription const progressSubscription = this.progressSubscription this.currentManager = undefined this.editorSubscription = undefined + this.workspaceFoldersSubscription = undefined this.progressSubscription = undefined this.disposeSubscription(editorSubscription, "active editor") + this.disposeSubscription(workspaceFoldersSubscription, "workspace folders") this.disposeSubscription(progressSubscription, "progress") } }