Skip to content

Commit bdcee80

Browse files
committed
feat: add experimental setting to prevent editor focus disruption
- Add experimentalPreventFocusDisruption setting to package.json - Update DiffViewProvider to respect the new setting when opening diff views - Add localization entry for the new setting - Add comprehensive tests for the new functionality Fixes #4784
1 parent 2411c8f commit bdcee80

File tree

4 files changed

+236
-15
lines changed

4 files changed

+236
-15
lines changed

src/integrations/editor/DiffViewProvider.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
1313
import { ClineSayTool } from "../../shared/ExtensionMessage"
1414
import { Task } from "../../core/task/Task"
1515
import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
16+
import { Package } from "../../shared/package"
1617

1718
import { DecorationController } from "./DecorationController"
1819

@@ -181,7 +182,10 @@ export class DiffViewProvider {
181182
}
182183
}
183184

184-
async saveChanges(diagnosticsEnabled: boolean = true, writeDelayMs: number = DEFAULT_WRITE_DELAY_MS): Promise<{
185+
async saveChanges(
186+
diagnosticsEnabled: boolean = true,
187+
writeDelayMs: number = DEFAULT_WRITE_DELAY_MS,
188+
): Promise<{
185189
newProblemsMessage: string | undefined
186190
userEdits: string | undefined
187191
finalContent: string | undefined
@@ -198,7 +202,15 @@ export class DiffViewProvider {
198202
await updatedDocument.save()
199203
}
200204

201-
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false, preserveFocus: true })
205+
// Check if the experimental setting is enabled
206+
const preventFocusDisruption = vscode.workspace
207+
.getConfiguration(Package.name)
208+
.get<boolean>("experimentalPreventFocusDisruption", false)
209+
210+
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), {
211+
preview: false,
212+
preserveFocus: preventFocusDisruption,
213+
})
202214
await this.closeAllDiffViews()
203215

204216
// Getting diagnostics before and after the file edit is a better approach than
@@ -216,22 +228,22 @@ export class DiffViewProvider {
216228
// and can address them accordingly. If problems don't change immediately after
217229
// applying a fix, won't be notified, which is generally fine since the
218230
// initial fix is usually correct and it may just take time for linters to catch up.
219-
231+
220232
let newProblemsMessage = ""
221-
233+
222234
if (diagnosticsEnabled) {
223235
// Add configurable delay to allow linters time to process and clean up issues
224236
// like unused imports (especially important for Go and other languages)
225237
// Ensure delay is non-negative
226238
const safeDelayMs = Math.max(0, writeDelayMs)
227-
239+
228240
try {
229241
await delay(safeDelayMs)
230242
} catch (error) {
231243
// Log error but continue - delay failure shouldn't break the save operation
232244
console.warn(`Failed to apply write delay: ${error}`)
233245
}
234-
246+
235247
const postDiagnostics = vscode.languages.getDiagnostics()
236248

237249
const newProblems = await diagnosticsToProblemsString(
@@ -388,9 +400,14 @@ export class DiffViewProvider {
388400
await updatedDocument.save()
389401

390402
if (this.documentWasOpen) {
403+
// Check if the experimental setting is enabled
404+
const preventFocusDisruption = vscode.workspace
405+
.getConfiguration(Package.name)
406+
.get<boolean>("experimentalPreventFocusDisruption", false)
407+
391408
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), {
392409
preview: false,
393-
preserveFocus: true,
410+
preserveFocus: preventFocusDisruption,
394411
})
395412
}
396413

@@ -444,6 +461,11 @@ export class DiffViewProvider {
444461

445462
const uri = vscode.Uri.file(path.resolve(this.cwd, this.relPath))
446463

464+
// Check if the experimental setting is enabled
465+
const preventFocusDisruption = vscode.workspace
466+
.getConfiguration(Package.name)
467+
.get<boolean>("experimentalPreventFocusDisruption", false)
468+
447469
// If this diff editor is already open (ie if a previous write file was
448470
// interrupted) then we should activate that instead of opening a new
449471
// diff.
@@ -457,7 +479,9 @@ export class DiffViewProvider {
457479
)
458480

459481
if (diffTab && diffTab.input instanceof vscode.TabInputTextDiff) {
460-
const editor = await vscode.window.showTextDocument(diffTab.input.modified, { preserveFocus: true })
482+
const editor = await vscode.window.showTextDocument(diffTab.input.modified, {
483+
preserveFocus: preventFocusDisruption,
484+
})
461485
return editor
462486
}
463487

@@ -523,7 +547,11 @@ export class DiffViewProvider {
523547
// Pre-open the file as a text document to ensure it doesn't open in preview mode
524548
// This fixes issues with files that have custom editor associations (like markdown preview)
525549
vscode.window
526-
.showTextDocument(uri, { preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true })
550+
.showTextDocument(uri, {
551+
preview: false,
552+
viewColumn: vscode.ViewColumn.Active,
553+
preserveFocus: preventFocusDisruption,
554+
})
527555
.then(() => {
528556
// Execute the diff command after ensuring the file is open as text
529557
return vscode.commands.executeCommand(
@@ -533,7 +561,7 @@ export class DiffViewProvider {
533561
}),
534562
uri,
535563
`${fileName}: ${fileExists ? `${DIFF_VIEW_LABEL_CHANGES}` : "New File"} (Editable)`,
536-
{ preserveFocus: true },
564+
{ preserveFocus: preventFocusDisruption },
537565
)
538566
})
539567
.then(

src/integrations/editor/__tests__/DiffViewProvider.spec.ts

Lines changed: 191 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ vi.mock("vscode", () => ({
3434
fs: {
3535
stat: vi.fn(),
3636
},
37+
getConfiguration: vi.fn(() => ({
38+
get: vi.fn().mockReturnValue(false), // Default value for experimentalPreventFocusDisruption
39+
})),
3740
},
3841
window: {
3942
createTextEditorDecorationType: vi.fn(),
@@ -81,6 +84,7 @@ vi.mock("vscode", () => ({
8184
InCenter: 2,
8285
},
8386
TabInputTextDiff: class TabInputTextDiff {},
87+
TabInputText: class TabInputText {},
8488
Uri: {
8589
file: vi.fn((path) => ({ fsPath: path })),
8690
parse: vi.fn((uri) => ({ with: vi.fn(() => ({})) })),
@@ -188,7 +192,7 @@ describe("DiffViewProvider", () => {
188192
// Mock showTextDocument to track when it's called
189193
vi.mocked(vscode.window.showTextDocument).mockImplementation(async (uri, options) => {
190194
callOrder.push("showTextDocument")
191-
expect(options).toEqual({ preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true })
195+
expect(options).toEqual({ preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: false })
192196
return mockEditor as any
193197
})
194198

@@ -220,10 +224,10 @@ describe("DiffViewProvider", () => {
220224
// Verify that showTextDocument was called before executeCommand
221225
expect(callOrder).toEqual(["showTextDocument", "executeCommand"])
222226

223-
// Verify that showTextDocument was called with preview: false and preserveFocus: true
227+
// Verify that showTextDocument was called with preview: false and preserveFocus: false (default)
224228
expect(vscode.window.showTextDocument).toHaveBeenCalledWith(
225229
expect.objectContaining({ fsPath: `${mockCwd}/test.md` }),
226-
{ preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true },
230+
{ preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: false },
227231
)
228232

229233
// Verify that the diff command was executed
@@ -232,7 +236,7 @@ describe("DiffViewProvider", () => {
232236
expect.any(Object),
233237
expect.any(Object),
234238
`test.md: ${DIFF_VIEW_LABEL_CHANGES} (Editable)`,
235-
{ preserveFocus: true },
239+
{ preserveFocus: false },
236240
)
237241
})
238242

@@ -418,4 +422,187 @@ describe("DiffViewProvider", () => {
418422
expect(vscode.languages.getDiagnostics).toHaveBeenCalled()
419423
})
420424
})
425+
426+
describe("experimentalPreventFocusDisruption setting", () => {
427+
it("should preserve focus when experimentalPreventFocusDisruption is enabled", async () => {
428+
// Mock the configuration to return true for experimentalPreventFocusDisruption
429+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
430+
get: vi.fn().mockReturnValue(true),
431+
} as any)
432+
433+
// Setup mock editor
434+
const mockEditor = {
435+
document: {
436+
uri: { fsPath: `${mockCwd}/test.ts` },
437+
getText: vi.fn().mockReturnValue(""),
438+
lineCount: 0,
439+
},
440+
selection: {
441+
active: { line: 0, character: 0 },
442+
anchor: { line: 0, character: 0 },
443+
},
444+
edit: vi.fn().mockResolvedValue(true),
445+
revealRange: vi.fn(),
446+
}
447+
448+
// Mock showTextDocument
449+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor as any)
450+
451+
// Mock workspace.onDidOpenTextDocument
452+
vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => {
453+
setTimeout(() => {
454+
callback({ uri: { fsPath: `${mockCwd}/test.ts` } } as any)
455+
}, 0)
456+
return { dispose: vi.fn() }
457+
})
458+
459+
// Mock window.visibleTextEditors
460+
vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any]
461+
462+
// Set up for file
463+
;(diffViewProvider as any).editType = "modify"
464+
465+
// Execute open
466+
await diffViewProvider.open("test.ts")
467+
468+
// Verify that showTextDocument was called with preserveFocus: true
469+
expect(vscode.window.showTextDocument).toHaveBeenCalledWith(
470+
expect.objectContaining({ fsPath: `${mockCwd}/test.ts` }),
471+
{ preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true },
472+
)
473+
474+
// Verify that the diff command was executed with preserveFocus: true
475+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
476+
"vscode.diff",
477+
expect.any(Object),
478+
expect.any(Object),
479+
expect.any(String),
480+
{ preserveFocus: true },
481+
)
482+
})
483+
484+
it("should not preserve focus when experimentalPreventFocusDisruption is disabled", async () => {
485+
// Mock the configuration to return false for experimentalPreventFocusDisruption
486+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
487+
get: vi.fn().mockReturnValue(false),
488+
} as any)
489+
490+
// Setup mock editor
491+
const mockEditor = {
492+
document: {
493+
uri: { fsPath: `${mockCwd}/test.ts` },
494+
getText: vi.fn().mockReturnValue(""),
495+
lineCount: 0,
496+
},
497+
selection: {
498+
active: { line: 0, character: 0 },
499+
anchor: { line: 0, character: 0 },
500+
},
501+
edit: vi.fn().mockResolvedValue(true),
502+
revealRange: vi.fn(),
503+
}
504+
505+
// Mock showTextDocument
506+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue(mockEditor as any)
507+
508+
// Mock workspace.onDidOpenTextDocument
509+
vi.mocked(vscode.workspace.onDidOpenTextDocument).mockImplementation((callback) => {
510+
setTimeout(() => {
511+
callback({ uri: { fsPath: `${mockCwd}/test.ts` } } as any)
512+
}, 0)
513+
return { dispose: vi.fn() }
514+
})
515+
516+
// Mock window.visibleTextEditors
517+
vi.mocked(vscode.window).visibleTextEditors = [mockEditor as any]
518+
519+
// Set up for file
520+
;(diffViewProvider as any).editType = "modify"
521+
522+
// Execute open
523+
await diffViewProvider.open("test.ts")
524+
525+
// Verify that showTextDocument was called with preserveFocus: false
526+
expect(vscode.window.showTextDocument).toHaveBeenCalledWith(
527+
expect.objectContaining({ fsPath: `${mockCwd}/test.ts` }),
528+
{ preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: false },
529+
)
530+
531+
// Verify that the diff command was executed with preserveFocus: false
532+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
533+
"vscode.diff",
534+
expect.any(Object),
535+
expect.any(Object),
536+
expect.any(String),
537+
{ preserveFocus: false },
538+
)
539+
})
540+
541+
it("should preserve focus in saveChanges when experimentalPreventFocusDisruption is enabled", async () => {
542+
// Mock the configuration to return true
543+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
544+
get: vi.fn().mockReturnValue(true),
545+
} as any)
546+
547+
// Setup for saveChanges
548+
;(diffViewProvider as any).relPath = "test.ts"
549+
;(diffViewProvider as any).newContent = "new content"
550+
;(diffViewProvider as any).activeDiffEditor = {
551+
document: {
552+
getText: vi.fn().mockReturnValue("new content"),
553+
isDirty: false,
554+
save: vi.fn().mockResolvedValue(undefined),
555+
},
556+
}
557+
;(diffViewProvider as any).preDiagnostics = []
558+
;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined)
559+
560+
// Mock vscode functions
561+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any)
562+
vi.mocked(vscode.languages.getDiagnostics).mockReturnValue([])
563+
564+
await diffViewProvider.saveChanges(false) // Disable diagnostics for simplicity
565+
566+
// Verify showTextDocument was called with preserveFocus: true
567+
expect(vscode.window.showTextDocument).toHaveBeenCalledWith(
568+
expect.objectContaining({ fsPath: `${mockCwd}/test.ts` }),
569+
{ preview: false, preserveFocus: true },
570+
)
571+
})
572+
573+
it("should preserve focus in revertChanges when experimentalPreventFocusDisruption is enabled", async () => {
574+
// Mock the configuration to return true
575+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
576+
get: vi.fn().mockReturnValue(true),
577+
} as any)
578+
579+
// Setup for revertChanges
580+
;(diffViewProvider as any).relPath = "test.ts"
581+
;(diffViewProvider as any).editType = "modify"
582+
;(diffViewProvider as any).documentWasOpen = true
583+
;(diffViewProvider as any).originalContent = "original content"
584+
;(diffViewProvider as any).activeDiffEditor = {
585+
document: {
586+
uri: { fsPath: `${mockCwd}/test.ts` },
587+
getText: vi.fn().mockReturnValue("modified content"),
588+
isDirty: false,
589+
save: vi.fn().mockResolvedValue(undefined),
590+
positionAt: vi.fn().mockReturnValue({ line: 0, character: 0 }),
591+
},
592+
}
593+
;(diffViewProvider as any).closeAllDiffViews = vi.fn().mockResolvedValue(undefined)
594+
;(diffViewProvider as any).reset = vi.fn().mockResolvedValue(undefined)
595+
596+
// Mock vscode functions
597+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({} as any)
598+
599+
await diffViewProvider.revertChanges()
600+
601+
// Verify showTextDocument was called with preserveFocus: true
602+
expect(vscode.window.showTextDocument).toHaveBeenCalledWith(
603+
expect.objectContaining({ fsPath: `${mockCwd}/test.ts` }),
604+
{ preview: false, preserveFocus: true },
605+
)
606+
})
607+
})
421608
})

src/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,11 @@
386386
"type": "string",
387387
"default": "",
388388
"description": "%settings.autoImportSettingsPath.description%"
389+
},
390+
"roo-cline.experimentalPreventFocusDisruption": {
391+
"type": "boolean",
392+
"default": false,
393+
"description": "%settings.experimentalPreventFocusDisruption.description%"
389394
}
390395
}
391396
}

src/package.nls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@
3636
"settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)",
3737
"settings.customStoragePath.description": "Custom storage path. Leave empty to use the default location. Supports absolute paths (e.g. 'D:\\RooCodeStorage')",
3838
"settings.enableCodeActions.description": "Enable Roo Code quick fixes",
39-
"settings.autoImportSettingsPath.description": "Path to a RooCode configuration file to automatically import on extension startup. Supports absolute paths and paths relative to the home directory (e.g. '~/Documents/roo-code-settings.json'). Leave empty to disable auto-import."
39+
"settings.autoImportSettingsPath.description": "Path to a RooCode configuration file to automatically import on extension startup. Supports absolute paths and paths relative to the home directory (e.g. '~/Documents/roo-code-settings.json'). Leave empty to disable auto-import.",
40+
"settings.experimentalPreventFocusDisruption.description": "(Experimental) Prevent file edits from stealing focus. When enabled, diff views and file edits will not disrupt your current work. Files will update in the background without forcing you to switch context."
4041
}

0 commit comments

Comments
 (0)