Skip to content

Commit 1426735

Browse files
committed
feat(diffView): enhance auto-focus behavior and manage user interactions
1 parent c0a5673 commit 1426735

File tree

1 file changed

+144
-33
lines changed

1 file changed

+144
-33
lines changed

src/integrations/editor/DiffViewProvider.ts

Lines changed: 144 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as vscode from "vscode"
2-
import { TextDocumentShowOptions, ViewColumn } from "vscode"
2+
import { TextDocument, TextDocumentShowOptions, ViewColumn } from "vscode"
33
import * as path from "path"
44
import * as fs from "fs/promises"
55
import { createDirectoriesForFile } from "../../utils/fs"
@@ -27,39 +27,152 @@ export class DiffViewProvider {
2727
private streamedLines: string[] = []
2828
private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
2929
private rooOpenedTabs: Set<string> = new Set()
30-
private preserveFocus: boolean = false
31-
private autoFocus: boolean = true
30+
private preserveFocus: boolean | undefined = undefined
31+
private autoApproval: boolean | undefined = undefined
32+
private autoFocus: boolean | undefined = undefined
3233
private autoCloseTabs: boolean = false
3334
// have to set the default view column to -1 since we need to set it in the initialize method and during initialization the enum ViewColumn is undefined
3435
private viewColumn: ViewColumn = -1 // ViewColumn.Active
36+
private userInteractionListeners: vscode.Disposable[] = []
37+
private suppressInteractionFlag: boolean = false
3538

3639
constructor(private cwd: string) {}
3740

38-
async initialize(viewColumn: ViewColumn) {
41+
private async initialize(viewColumn: ViewColumn) {
3942
const provider = ClineProvider.getVisibleInstance()
40-
const autoFocus = vscode.workspace.getConfiguration("roo-cline").get<boolean>("diffViewAutoFocus", true)
41-
42-
const autoApproval =
43-
(provider?.getValue("autoApprovalEnabled") && provider?.getValue("alwaysAllowWrite")) ?? false
4443
// If autoApproval is enabled, we want to preserve focus if autoFocus is disabled
4544
// AutoApproval is enabled when the user has set "alwaysAllowWrite" and "autoApprovalEnabled" to true
4645
// AutoFocus is enabled when the user has set "diffView.autoFocus" to true, this is the default.
4746
// If autoFocus is disabled, we want to preserve focus on the diff editor we are working on.
48-
this.preserveFocus = autoApproval && !autoFocus
49-
this.autoFocus = autoFocus
47+
// we have to check for null values for the first initialization
48+
if (this.autoApproval === undefined) {
49+
this.autoApproval =
50+
(provider?.getValue("autoApprovalEnabled") && provider?.getValue("alwaysAllowWrite")) ?? false
51+
}
52+
if (this.autoFocus === undefined) {
53+
this.autoFocus = vscode.workspace.getConfiguration("roo-cline").get<boolean>("diffViewAutoFocus", true)
54+
}
55+
this.preserveFocus = this.autoApproval && !this.autoFocus
5056
this.autoCloseTabs = vscode.workspace.getConfiguration("roo-cline").get<boolean>("autoCloseRooTabs", false)
5157
this.viewColumn = viewColumn
5258
// Track currently visible editors and active editor for focus restoration and tab cleanup
5359
this.rooOpenedTabs.clear()
5460
}
5561

62+
private async showTextDocumentSafe({
63+
uri,
64+
textDocument,
65+
options,
66+
}: {
67+
uri?: vscode.Uri
68+
textDocument?: TextDocument
69+
options?: TextDocumentShowOptions
70+
}) {
71+
this.suppressInteractionFlag = true
72+
// If the uri is already open, we want to focus it
73+
if (uri) {
74+
const editor = await vscode.window.showTextDocument(uri, options)
75+
this.suppressInteractionFlag = false
76+
return editor
77+
}
78+
// If the textDocument is already open, we want to focus it
79+
if (textDocument) {
80+
const editor = await vscode.window.showTextDocument(textDocument, options)
81+
this.suppressInteractionFlag = false
82+
return editor
83+
}
84+
// If the textDocument is not open and not able to be opened, we just reset the suppressInteractionFlag
85+
this.suppressInteractionFlag = false
86+
return null
87+
}
88+
89+
/**
90+
* Resets the auto-focus listeners to prevent memory leaks.
91+
* This is called when the diff editor is closed or when the user interacts with other editors.
92+
*/
93+
private resetAutoFocusListeners() {
94+
this.userInteractionListeners.forEach((listener) => listener.dispose())
95+
this.userInteractionListeners = []
96+
}
97+
98+
/**
99+
* Disables auto-focus on the diff editor after user interaction.
100+
* This is to prevent the diff editor from stealing focus when the user interacts with other editors or tabs.
101+
*/
102+
private disableAutoFocusAfterUserInteraction() {
103+
this.resetAutoFocusListeners()
104+
// first reset listeners if they exist
105+
this.userInteractionListeners.forEach((listener) => listener.dispose())
106+
this.userInteractionListeners = []
107+
// if auto approval is disabled or auto focus is disabled, we don't need to add listeners
108+
if (!this.autoApproval || !this.autoFocus) {
109+
return
110+
}
111+
// then add new listeners
112+
const changeTextEditorSelectionListener = vscode.window.onDidChangeTextEditorSelection((_e) => {
113+
// If the change was done programmatically, or if there is actually no editor or the user did not allow auto approval, we don't want to suppress focus
114+
if (this.suppressInteractionFlag) {
115+
// If the user is interacting with the diff editor, we don't want to suppress focus
116+
// If the user is interacting with another editor, we want to suppress focus
117+
return
118+
}
119+
// Consider this a "user interaction"
120+
this.preserveFocus = true
121+
this.autoFocus = false
122+
// remove the listeners since we don't need them anymore
123+
this.resetAutoFocusListeners()
124+
}, this)
125+
const changeActiveTextEditorListener = vscode.window.onDidChangeActiveTextEditor((editor) => {
126+
// If the change was done programmatically, or if there is actually no editor or the user did not allow auto approval, we don't want to suppress focus
127+
if (this.suppressInteractionFlag || !editor) {
128+
// If the user is interacting with the diff editor, we don't want to suppress focus
129+
// If the user is interacting with another editor, we want to suppress focus
130+
return
131+
}
132+
// Consider this a "user interaction"
133+
this.preserveFocus = true
134+
this.autoFocus = false
135+
// remove the listeners since we don't need them anymore
136+
this.resetAutoFocusListeners()
137+
}, this)
138+
const changeTabListener = vscode.window.tabGroups.onDidChangeTabs((_e) => {
139+
// Some tab was added/removed/changed
140+
// If the change was done programmatically, or the user did not allow auto approval, we don't want to suppress focus
141+
if (this.suppressInteractionFlag) {
142+
return
143+
}
144+
this.preserveFocus = true
145+
this.autoFocus = false
146+
// remove the listeners since we don't need them anymore
147+
this.resetAutoFocusListeners()
148+
}, this)
149+
const changeTabGroupListener = vscode.window.tabGroups.onDidChangeTabGroups((_e) => {
150+
// Tab group layout changed (e.g., split view)
151+
// If the change was done programmatically, or the user did not allow auto approval, we don't want to suppress focus
152+
if (this.suppressInteractionFlag) {
153+
return
154+
}
155+
this.preserveFocus = true
156+
this.autoFocus = false
157+
// remove the listeners since we don't need them anymore
158+
this.resetAutoFocusListeners()
159+
}, this)
160+
this.userInteractionListeners.push(
161+
changeTextEditorSelectionListener,
162+
changeActiveTextEditorListener,
163+
changeTabListener,
164+
changeTabGroupListener,
165+
)
166+
}
167+
56168
/**
57169
* Opens a diff editor for the given relative path, optionally in a specific viewColumn.
58170
* @param relPath The relative file path to open.
59171
* @param viewColumn (Optional) The VSCode editor group to open the diff in.
60172
*/
61173
async open(relPath: string, viewColumn: ViewColumn): Promise<void> {
62174
await this.initialize(viewColumn)
175+
this.disableAutoFocusAfterUserInteraction()
63176
// Set the edit type based on the file existence
64177
this.relPath = relPath
65178
const fileExists = this.editType === "modify"
@@ -117,7 +230,7 @@ export class DiffViewProvider {
117230
* Opens a file editor and tracks it as opened by Roo if not already open.
118231
*/
119232
private async showAndTrackEditor(uri: vscode.Uri, options: vscode.TextDocumentShowOptions = {}) {
120-
const editor = await vscode.window.showTextDocument(uri, options)
233+
const editor = await this.showTextDocumentSafe({ uri, options })
121234
if (this.autoCloseTabs && !this.documentWasOpen) {
122235
this.rooOpenedTabs.add(uri.toString())
123236
}
@@ -193,26 +306,11 @@ export class DiffViewProvider {
193306
if (!this.relPath || !this.newContent || !this.activeDiffEditor) {
194307
return { newProblemsMessage: undefined, userEdits: undefined, finalContent: undefined }
195308
}
196-
const absolutePath = path.resolve(this.cwd, this.relPath)
197309
const updatedDocument = this.activeDiffEditor.document
198310
const editedContent = updatedDocument.getText()
199311
if (updatedDocument.isDirty) {
200312
await updatedDocument.save()
201313
}
202-
const previousEditor = vscode.window.activeTextEditor
203-
const textDocumentShowOptions: TextDocumentShowOptions = {
204-
preview: false,
205-
preserveFocus: this.preserveFocus,
206-
viewColumn: this.viewColumn,
207-
}
208-
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), textDocumentShowOptions)
209-
if (!this.autoFocus && previousEditor) {
210-
await vscode.window.showTextDocument(previousEditor.document, {
211-
preview: false,
212-
preserveFocus: false,
213-
selection: previousEditor.selection,
214-
})
215-
}
216314
await this.closeAllRooOpenedViews()
217315
/*
218316
Getting diagnostics before and after the file edit is a better approach than
@@ -293,8 +391,11 @@ export class DiffViewProvider {
293391
await updatedDocument.save()
294392
console.log(`File ${absolutePath} has been reverted to its original content.`)
295393
if (this.documentWasOpen) {
296-
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), {
297-
preview: false,
394+
await this.showTextDocumentSafe({
395+
uri: vscode.Uri.file(absolutePath),
396+
options: {
397+
preview: false,
398+
},
298399
})
299400
}
300401
await this.closeAllRooOpenedViews()
@@ -386,9 +487,13 @@ export class DiffViewProvider {
386487
preserveFocus: this.preserveFocus,
387488
viewColumn: this.viewColumn,
388489
}
490+
// set interaction flag to true to prevent autoFocus from being triggered
491+
this.suppressInteractionFlag = true
389492
vscode.commands
390493
.executeCommand("vscode.diff", leftUri, rightUri, title, textDocumentShowOptions)
391494
.then(() => {
495+
// set interaction flag to false to allow autoFocus to be triggered
496+
this.suppressInteractionFlag = false
392497
if (this.autoCloseTabs && !this.documentWasOpen) {
393498
// If the diff tab is not already open, add it to the set
394499
this.rooOpenedTabs.add(rightUri.toString())
@@ -402,11 +507,14 @@ export class DiffViewProvider {
402507
return
403508
}
404509
// if there is, we need to focus it
405-
vscode.window.showTextDocument(previousEditor.document, {
406-
preview: false,
407-
// we need to force focus here now, because we want to restore the previous selection
408-
preserveFocus: false,
409-
selection: previousEditor.selection,
510+
this.showTextDocumentSafe({
511+
textDocument: previousEditor.document,
512+
options: {
513+
preview: false,
514+
preserveFocus: false,
515+
selection: previousEditor.selection,
516+
viewColumn: previousEditor.viewColumn,
517+
},
410518
})
411519
})
412520
.then(() => {
@@ -479,5 +587,8 @@ export class DiffViewProvider {
479587
this.activeLineController = undefined
480588
this.streamedLines = []
481589
this.preDiagnostics = []
590+
this.rooOpenedTabs.clear()
591+
this.autoCloseTabs = false
592+
this.resetAutoFocusListeners()
482593
}
483594
}

0 commit comments

Comments
 (0)