Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/honest-kings-follow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": patch
---

Preserve focus onto currently edited file.
73 changes: 35 additions & 38 deletions src/integrations/editor/DiffViewProvider.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -196,17 +196,13 @@ 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()

if (updatedDocument.isDirty) {
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.
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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))

Expand All @@ -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
Expand Down
19 changes: 7 additions & 12 deletions src/integrations/editor/__tests__/DiffViewProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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() }
})
Expand All @@ -231,27 +232,21 @@ 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(
"vscode.diff",
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() })
Expand Down