From e444c553bc39c12c6ce58f33727afe59f5050679 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 9 Feb 2026 07:21:04 +0000 Subject: [PATCH] fix: preserve pinned tab state when editing files When RooCode edits a file, it closes the existing tab and opens a diff editor. After accepting or rejecting changes, the file is reopened but the pinned state is lost. This fix captures the tab isPinned state before closing, then restores it after reopening via workbench.action.pinEditor command in both saveChanges and revertChanges flows. Closes #11326 --- src/integrations/editor/DiffViewProvider.ts | 17 ++ .../editor/__tests__/DiffViewProvider.spec.ts | 208 +++++++++++++++++- 2 files changed, 224 insertions(+), 1 deletion(-) diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index 94a483706e6..f16d4106c34 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -28,6 +28,7 @@ export class DiffViewProvider { originalContent: string | undefined private createdDirs: string[] = [] private documentWasOpen = false + private documentWasPinned = false private relPath?: string private newContent?: string private activeDiffEditor?: vscode.TextEditor @@ -84,6 +85,7 @@ export class DiffViewProvider { // If the file was already open, close it (must happen after showing the // diff view since if it's the only tab the column will close). this.documentWasOpen = false + this.documentWasPinned = false // Close the tab if it's open (it's already saved above). const tabs = vscode.window.tabGroups.all @@ -97,6 +99,9 @@ export class DiffViewProvider { ) for (const tab of tabs) { + if (tab.isPinned) { + this.documentWasPinned = true + } if (!tab.isDirty) { await vscode.window.tabGroups.close(tab) } @@ -210,6 +215,12 @@ export class DiffViewProvider { } await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false, preserveFocus: true }) + + // Restore pinned state if the tab was pinned before editing. + if (this.documentWasPinned) { + await vscode.commands.executeCommand("workbench.action.pinEditor") + } + await this.closeAllDiffViews() // Getting diagnostics before and after the file edit is a better approach than @@ -401,6 +412,11 @@ export class DiffViewProvider { preview: false, preserveFocus: true, }) + + // Restore pinned state if the tab was pinned before editing. + if (this.documentWasPinned) { + await vscode.commands.executeCommand("workbench.action.pinEditor") + } } await this.closeAllDiffViews() @@ -619,6 +635,7 @@ export class DiffViewProvider { this.originalContent = undefined this.createdDirs = [] this.documentWasOpen = false + this.documentWasPinned = false this.activeDiffEditor = undefined this.fadedOverlayController = undefined this.activeLineController = undefined diff --git a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts index e99f7bf9c86..8a7cab5f085 100644 --- a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts +++ b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts @@ -50,7 +50,7 @@ vi.mock("vscode", () => ({ visibleTextEditors: [], }, commands: { - executeCommand: vi.fn(), + executeCommand: vi.fn().mockResolvedValue(undefined), }, languages: { getDiagnostics: vi.fn(() => []), @@ -84,6 +84,7 @@ vi.mock("vscode", () => ({ TextEditorRevealType: { InCenter: 2, }, + TabInputText: class TabInputText {}, TabInputTextDiff: class TabInputTextDiff {}, Uri: { file: vi.fn((path) => ({ fsPath: path })), @@ -517,4 +518,209 @@ describe("DiffViewProvider", () => { expect(vscode.languages.getDiagnostics).toHaveBeenCalled() }) }) + + describe("pinned tab preservation", () => { + describe("open method - pin state capture", () => { + it("should capture pinned state when closing a pinned tab", async () => { + const absolutePath = `${mockCwd}/test.txt` + + // Setup a pinned tab + const pinnedTab = { + input: { uri: { scheme: "file", fsPath: absolutePath } }, + isDirty: false, + isPinned: true, + } + + // Make the tab look like a TabInputText instance + Object.setPrototypeOf(pinnedTab.input, vscode.TabInputText?.prototype ?? {}) + + Object.defineProperty(vscode.window.tabGroups, "all", { + get: () => [{ tabs: [pinnedTab] }], + configurable: true, + }) + + vi.mocked(vscode.window.tabGroups.close).mockResolvedValue(true as any) + + // Mock for openDiffEditor + const mockEditor = { + document: { + uri: { fsPath: absolutePath, scheme: "file" }, + getText: vi.fn().mockReturnValue(""), + lineCount: 0, + }, + selection: { active: { line: 0, character: 0 }, anchor: { line: 0, character: 0 } }, + edit: vi.fn().mockResolvedValue(true), + revealRange: vi.fn(), + } + + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor as any) + + vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { + setTimeout(() => { + callback({ uri: { fsPath: absolutePath, scheme: "file" } } as any) + }, 0) + return { dispose: vi.fn() } + }) + + vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any] + ;(diffViewProvider as any).editType = "modify" + + await diffViewProvider.open("test.txt") + + expect((diffViewProvider as any).documentWasPinned).toBe(true) + expect((diffViewProvider as any).documentWasOpen).toBe(true) + }) + + it("should not set pinned state when closing an unpinned tab", async () => { + const absolutePath = `${mockCwd}/test.txt` + + const unpinnedTab = { + input: { uri: { scheme: "file", fsPath: absolutePath } }, + isDirty: false, + isPinned: false, + } + + Object.setPrototypeOf(unpinnedTab.input, vscode.TabInputText?.prototype ?? {}) + + Object.defineProperty(vscode.window.tabGroups, "all", { + get: () => [{ tabs: [unpinnedTab] }], + configurable: true, + }) + + vi.mocked(vscode.window.tabGroups.close).mockResolvedValue(true as any) + + const mockEditor = { + document: { + uri: { fsPath: absolutePath, scheme: "file" }, + getText: vi.fn().mockReturnValue(""), + lineCount: 0, + }, + selection: { active: { line: 0, character: 0 }, anchor: { line: 0, character: 0 } }, + edit: vi.fn().mockResolvedValue(true), + revealRange: vi.fn(), + } + + vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor as any) + + vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { + setTimeout(() => { + callback({ uri: { fsPath: absolutePath, scheme: "file" } } as any) + }, 0) + return { dispose: vi.fn() } + }) + + vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any] + ;(diffViewProvider as any).editType = "modify" + + await diffViewProvider.open("test.txt") + + expect((diffViewProvider as any).documentWasPinned).toBe(false) + expect((diffViewProvider as any).documentWasOpen).toBe(true) + }) + }) + + describe("saveChanges - pin state restoration", () => { + beforeEach(() => { + ;(diffViewProvider as any).relPath = "test.ts" + ;(diffViewProvider as any).newContent = "new content" + ;(diffViewProvider as any).activeDiffEditor = { + document: { + getText: vi.fn().mockReturnValue("new content"), + isDirty: false, + save: vi.fn().mockResolvedValue(undefined), + }, + } + ;(diffViewProvider as any).preDiagnostics = [] + ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) + + vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + vi.mocked(vscode.languages.getDiagnostics).mockReturnValue([]) + vi.mocked(vscode.commands.executeCommand).mockResolvedValue(undefined) + }) + + it("should pin the editor after saving when documentWasPinned is true", async () => { + ;(diffViewProvider as any).documentWasPinned = true + + await diffViewProvider.saveChanges(false) + + expect(vscode.commands.executeCommand).toHaveBeenCalledWith("workbench.action.pinEditor") + }) + + it("should not pin the editor after saving when documentWasPinned is false", async () => { + ;(diffViewProvider as any).documentWasPinned = false + + await diffViewProvider.saveChanges(false) + + expect(vscode.commands.executeCommand).not.toHaveBeenCalledWith("workbench.action.pinEditor") + }) + }) + + describe("revertChanges - pin state restoration", () => { + beforeEach(() => { + ;(diffViewProvider as any).relPath = "test.ts" + ;(diffViewProvider as any).newContent = "new content" + ;(diffViewProvider as any).originalContent = "original content" + ;(diffViewProvider as any).editType = "modify" + ;(diffViewProvider as any).activeDiffEditor = { + document: { + uri: { fsPath: `${mockCwd}/test.ts` }, + getText: vi.fn().mockReturnValue("new content"), + positionAt: vi.fn().mockReturnValue({ line: 0, character: 0 }), + isDirty: false, + save: vi.fn().mockResolvedValue(undefined), + }, + } + + vi.mocked(vscode.workspace.applyEdit).mockResolvedValue(true) + vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any) + vi.mocked(vscode.commands.executeCommand).mockResolvedValue(undefined) + + // Mock closeAllDiffViews + ;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined) + }) + + it("should pin the editor after reverting when documentWasPinned is true", async () => { + ;(diffViewProvider as any).documentWasOpen = true + ;(diffViewProvider as any).documentWasPinned = true + + await diffViewProvider.revertChanges() + + expect(vscode.commands.executeCommand).toHaveBeenCalledWith("workbench.action.pinEditor") + }) + + it("should not pin the editor after reverting when documentWasPinned is false", async () => { + ;(diffViewProvider as any).documentWasOpen = true + ;(diffViewProvider as any).documentWasPinned = false + + await diffViewProvider.revertChanges() + + expect(vscode.commands.executeCommand).not.toHaveBeenCalledWith("workbench.action.pinEditor") + }) + + it("should not pin the editor when document was not open", async () => { + ;(diffViewProvider as any).documentWasOpen = false + ;(diffViewProvider as any).documentWasPinned = true + + await diffViewProvider.revertChanges() + + expect(vscode.commands.executeCommand).not.toHaveBeenCalledWith("workbench.action.pinEditor") + }) + }) + + describe("reset - pin state cleanup", () => { + it("should reset documentWasPinned to false", async () => { + ;(diffViewProvider as any).documentWasPinned = true + + // Mock tabGroups.all to return empty for closeAllDiffViews + Object.defineProperty(vscode.window.tabGroups, "all", { + get: () => [], + configurable: true, + }) + + await (diffViewProvider as any).reset() + + expect((diffViewProvider as any).documentWasPinned).toBe(false) + }) + }) + }) })