diff --git a/.changeset/honest-kings-follow.md b/.changeset/honest-kings-follow.md new file mode 100644 index 0000000000..796feef4fe --- /dev/null +++ b/.changeset/honest-kings-follow.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Preserve focus onto currently edited file. diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index 64820beffb..c065ff07ff 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode" +import { TextDocumentShowOptions, ViewColumn } from "vscode" import * as path from "path" import * as fs from "fs/promises" import * as diff from "diff" @@ -63,10 +64,6 @@ export class DiffViewProvider { } } - // Get diagnostics before editing the file, we'll compare to diagnostics - // after editing to see if cline needs to fix anything. - this.preDiagnostics = vscode.languages.getDiagnostics() - if (fileExists) { this.originalContent = await fs.readFile(absolutePath, "utf-8") } else { @@ -102,6 +99,11 @@ export class DiffViewProvider { } this.activeDiffEditor = await this.openDiffEditor() + // Get diagnostics before editing the file, we'll compare to diagnostics + // after editing to see if cline needs to fix anything. + // Open the document to ensure diagnostics are up-to-date. + // This must happen AFTER opening the diff editor, since the diff editor triggers diagnostics of vscode. + this.preDiagnostics = vscode.languages.getDiagnostics() this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor) this.activeLineController = new DecorationController("activeLine", this.activeDiffEditor) // Apply faded overlay to all lines initially. @@ -129,11 +131,6 @@ export class DiffViewProvider { throw new Error("User closed text editor, unable to edit file...") } - // Place cursor at the beginning of the diff editor to keep it out of - // the way of the stream animation, but do this without stealing focus - const beginningOfDocument = new vscode.Position(0, 0) - diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument) - const endLine = accumulatedLines.length // Replace all content up to the current line with accumulated lines. const edit = new vscode.WorkspaceEdit() @@ -187,7 +184,10 @@ export class DiffViewProvider { } } - async saveChanges(diagnosticsEnabled: boolean = true, writeDelayMs: number = DEFAULT_WRITE_DELAY_MS): Promise<{ + async saveChanges( + diagnosticsEnabled: boolean = true, + writeDelayMs: number = DEFAULT_WRITE_DELAY_MS, + ): Promise<{ newProblemsMessage: string | undefined userEdits: string | undefined finalContent: string | undefined @@ -196,7 +196,6 @@ export class DiffViewProvider { return { newProblemsMessage: undefined, userEdits: undefined, finalContent: undefined } } - const absolutePath = path.resolve(this.cwd, this.relPath) const updatedDocument = this.activeDiffEditor.document const editedContent = updatedDocument.getText() @@ -204,9 +203,6 @@ export class DiffViewProvider { await updatedDocument.save() } - await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false, preserveFocus: true }) - await this.closeAllDiffViews() - // Getting diagnostics before and after the file edit is a better approach than // automatically tracking problems in real-time. This method ensures we only // report new problems that are a direct result of this specific edit. @@ -222,22 +218,22 @@ export class DiffViewProvider { // and can address them accordingly. If problems don't change immediately after // applying a fix, won't be notified, which is generally fine since the // initial fix is usually correct and it may just take time for linters to catch up. - + let newProblemsMessage = "" - + if (diagnosticsEnabled) { // Add configurable delay to allow linters time to process and clean up issues // like unused imports (especially important for Go and other languages) // Ensure delay is non-negative const safeDelayMs = Math.max(0, writeDelayMs) - + try { await delay(safeDelayMs) } catch (error) { // Log error but continue - delay failure shouldn't break the save operation console.warn(`Failed to apply write delay: ${error}`) } - + const postDiagnostics = vscode.languages.getDiagnostics() // Get diagnostic settings from state @@ -259,16 +255,18 @@ export class DiffViewProvider { newProblemsMessage = newProblems.length > 0 ? `\n\nNew problems detected after saving the file:\n${newProblems}` : "" } + // Close diff views AFTER getting diagnostics, so we can get the diagnostics for the edited file. + await this.closeAllDiffViews() // If the edited content has different EOL characters, we don't want to // show a diff with all the EOL differences. const newContentEOL = this.newContent.includes("\r\n") ? "\r\n" : "\n" // Normalize EOL characters without trimming content - const normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL) + const normalizedEditedContent = editedContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() // Just in case the new content has a mix of varying EOL characters. - const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL) + const normalizedNewContent = this.newContent.replace(/\r\n|\n/g, newContentEOL).trimEnd() if (normalizedEditedContent !== normalizedNewContent) { // User made changes before approving edit. @@ -506,7 +504,7 @@ export class DiffViewProvider { // Listen for document open events - more efficient than scanning all tabs disposables.push( vscode.workspace.onDidOpenTextDocument(async (document) => { - if (arePathsEqual(document.uri.fsPath, uri.fsPath)) { + if (document.uri.scheme == DIFF_VIEW_URI_SCHEME && uri.fsPath.endsWith(document.fileName)) { // Wait a tick for the editor to be available await new Promise((r) => setTimeout(r, 0)) @@ -533,23 +531,22 @@ export class DiffViewProvider { } }), ) - - // Pre-open the file as a text document to ensure it doesn't open in preview mode - // This fixes issues with files that have custom editor associations (like markdown preview) - vscode.window - .showTextDocument(uri, { preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true }) - .then(() => { - // Execute the diff command after ensuring the file is open as text - return vscode.commands.executeCommand( - "vscode.diff", - vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({ - query: Buffer.from(this.originalContent ?? "").toString("base64"), - }), - uri, - `${fileName}: ${fileExists ? `${DIFF_VIEW_LABEL_CHANGES}` : "New File"} (Editable)`, - { preserveFocus: true }, - ) - }) + // Now execute the diff command + const textShowOptions: TextDocumentShowOptions = { + preview: false, + viewColumn: ViewColumn.Beside, + preserveFocus: true, + } + vscode.commands + .executeCommand( + "vscode.diff", + vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${fileName}`).with({ + query: Buffer.from(this.originalContent ?? "").toString("base64"), + }), + uri, + `${fileName}: ${fileExists ? `${DIFF_VIEW_LABEL_CHANGES}` : "New File"} (Editable)`, + textShowOptions, + ) .then( () => { // Command executed successfully, now wait for the editor to appear diff --git a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts index 7159aca57a..74a2e09267 100644 --- a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts +++ b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts @@ -128,7 +128,7 @@ describe("DiffViewProvider", () => { ;(diffViewProvider as any).relPath = "test.txt" ;(diffViewProvider as any).activeDiffEditor = { document: { - uri: { fsPath: `${mockCwd}/test.txt` }, + uri: { fsPath: `${mockCwd}/test.txt`, scheme: DIFF_VIEW_URI_SCHEME }, getText: vi.fn(), lineCount: 10, }, @@ -183,7 +183,8 @@ describe("DiffViewProvider", () => { // Setup const mockEditor = { document: { - uri: { fsPath: `${mockCwd}/test.md` }, + uri: { fsPath: `${mockCwd}/test.md`, scheme: DIFF_VIEW_URI_SCHEME }, + fileName: `test.md`, getText: vi.fn().mockReturnValue(""), lineCount: 0, }, @@ -216,7 +217,7 @@ describe("DiffViewProvider", () => { vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => { // Trigger the callback immediately with the document setTimeout(() => { - callback({ uri: { fsPath: `${mockCwd}/test.md` } } as any) + callback(mockEditor.document as any) }, 0) return { dispose: vi.fn() } }) @@ -231,13 +232,7 @@ describe("DiffViewProvider", () => { await diffViewProvider.open("test.md") // Verify that showTextDocument was called before executeCommand - expect(callOrder).toEqual(["showTextDocument", "executeCommand"]) - - // Verify that showTextDocument was called with preview: false and preserveFocus: true - expect(vscode.window.showTextDocument).toHaveBeenCalledWith( - expect.objectContaining({ fsPath: `${mockCwd}/test.md` }), - { preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true }, - ) + expect(callOrder).toEqual(["executeCommand"]) // Verify that the diff command was executed expect(vscode.commands.executeCommand).toHaveBeenCalledWith( @@ -245,13 +240,13 @@ describe("DiffViewProvider", () => { expect.any(Object), expect.any(Object), `test.md: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`, - { preserveFocus: true }, + { preview: false, preserveFocus: true, viewColumn: vscode.ViewColumn.Beside }, ) }) it("should handle showTextDocument failure", async () => { // Mock showTextDocument to fail - vi.mocked(vscode.window.showTextDocument).mockRejectedValue(new Error("Cannot open file")) + vi.mocked(vscode.commands.executeCommand).mockRejectedValue(new Error("Cannot open file")) // Mock workspace.onDidOpenTextDocument vi.mocked(vscode.workspace.onDidOpenTextDocument).mockReturnValue({ dispose: vi.fn() })