Skip to content

Commit 0e03f5f

Browse files
committed
feat(PostDiffViewBehaviorUtils): add utility for managing post-diff behavior and tab focus
1 parent 5f05cce commit 0e03f5f

File tree

4 files changed

+610
-245
lines changed

4 files changed

+610
-245
lines changed

src/integrations/editor/DiffViewProvider.ts

Lines changed: 36 additions & 217 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Task } from "../../core/task/Task"
1616
import { DecorationController } from "./DecorationController"
1717
import { ClineProvider } from "../../core/webview/ClineProvider"
1818
import { UserInteractionProvider } from "./UserInteractionProvider"
19+
import { PostDiffViewBehaviorUtils } from "./PostDiffViewBehaviorUtils"
1920

2021
export const DIFF_VIEW_URI_SCHEME = "cline-diff"
2122

@@ -43,27 +44,36 @@ export class DiffViewProvider {
4344
private streamedLines: string[] = []
4445
private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
4546
private rooOpenedTabs: Set<string> = new Set()
46-
private preserveFocus: boolean | undefined = undefined
4747
private autoApproval: boolean | undefined = undefined
4848
private autoFocus: boolean | undefined = undefined
49-
private autoCloseTabs: boolean = false
5049
private autoCloseAllRooTabs: boolean = false // Added new setting
5150
// 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
5251
private viewColumn: ViewColumn = -1 // ViewColumn.Active
5352
private userInteractionProvider: UserInteractionProvider
5453
private suppressInteractionFlag: boolean = false
5554
private preDiffActiveEditor?: vscode.TextEditor // Store active editor before diff operation
55+
private postDiffBehaviorUtils: PostDiffViewBehaviorUtils
5656

5757
constructor(private cwd: string) {
5858
this.userInteractionProvider = new UserInteractionProvider({
5959
onUserInteraction: () => {
60-
this.preserveFocus = true
6160
this.autoFocus = false
6261
},
6362
getSuppressFlag: () => this.suppressInteractionFlag,
6463
autoApproval: false,
6564
autoFocus: true,
6665
})
66+
67+
// Initialize PostDiffviewBehaviorUtils with initial context
68+
this.postDiffBehaviorUtils = new PostDiffViewBehaviorUtils({
69+
relPath: this.relPath,
70+
editType: this.editType,
71+
documentWasOpen: this.documentWasOpen,
72+
cwd: this.cwd,
73+
rooOpenedTabs: this.rooOpenedTabs,
74+
preDiffActiveEditor: this.preDiffActiveEditor,
75+
autoCloseAllRooTabs: this.autoCloseAllRooTabs,
76+
})
6777
}
6878

6979
public async initialize() {
@@ -79,11 +89,14 @@ export class DiffViewProvider {
7989
}
8090
const settings = await this._readDiffSettings()
8191
this.autoFocus = settings.autoFocus
82-
this.autoCloseTabs = settings.autoCloseRooTabs
8392
this.autoCloseAllRooTabs = settings.autoCloseAllRooTabs
84-
this.preserveFocus = this.autoApproval && !this.autoFocus
8593
// Track currently visible editors and active editor for focus restoration and tab cleanup
8694
this.rooOpenedTabs.clear()
95+
96+
// Update PostDiffviewBehaviorUtils context with latest values
97+
this.postDiffBehaviorUtils.updateContext({
98+
autoCloseAllRooTabs: this.autoCloseAllRooTabs,
99+
})
87100
}
88101

89102
private async _readDiffSettings(): Promise<DiffSettings> {
@@ -143,6 +156,14 @@ export class DiffViewProvider {
143156
this.preDiffActiveEditor = vscode.window.activeTextEditor
144157

145158
this.viewColumn = viewColumn
159+
160+
// Update PostDiffviewBehaviorUtils context with current state
161+
this.postDiffBehaviorUtils.updateContext({
162+
relPath: relPath,
163+
editType: this.editType,
164+
documentWasOpen: this.documentWasOpen,
165+
preDiffActiveEditor: this.preDiffActiveEditor,
166+
})
146167
// Update the user interaction provider with current settings
147168
this.userInteractionProvider.updateOptions({
148169
autoApproval: this.autoApproval ?? false,
@@ -203,6 +224,10 @@ export class DiffViewProvider {
203224
tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, absolutePath),
204225
).length > 0
205226

227+
this.postDiffBehaviorUtils.updateContext({
228+
documentWasOpen: this.documentWasOpen,
229+
})
230+
206231
this.activeDiffEditor = await this.openDiffEditor()
207232
this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
208233
this.activeLineController = new DecorationController("activeLine", this.activeDiffEditor)
@@ -417,10 +442,10 @@ export class DiffViewProvider {
417442
}
418443
}
419444

420-
await this.closeAllRooOpenedViews()
445+
await this.postDiffBehaviorUtils.closeAllRooOpenedViews(await this._readDiffSettings())
421446

422447
// Implement post-diff focus behavior
423-
await this.handlePostDiffFocus()
448+
await this.postDiffBehaviorUtils.handlePostDiffFocus()
424449

425450
// If no auto-close settings are enabled and the document was not open before,
426451
// open the file after the diff is complete.
@@ -583,7 +608,7 @@ export class DiffViewProvider {
583608
await updatedDocument.save()
584609
}
585610

586-
await this.closeAllRooOpenedViews()
611+
await this.postDiffBehaviorUtils.closeAllRooOpenedViews(await this._readDiffSettings())
587612
await fs.unlink(absolutePath)
588613

589614
// Remove only the directories we created, in reverse order.
@@ -615,222 +640,16 @@ export class DiffViewProvider {
615640
await this.showTextDocumentSafe({ uri: vscode.Uri.file(absolutePath), options: { preview: false } })
616641
}
617642

618-
await this.closeAllRooOpenedViews()
643+
await this.postDiffBehaviorUtils.closeAllRooOpenedViews(await this._readDiffSettings())
619644
}
620645

621646
// Implement post-diff focus behavior
622-
await this.handlePostDiffFocus()
647+
await this.postDiffBehaviorUtils.handlePostDiffFocus()
623648

624649
// Edit is done.
625650
this.resetWithListeners()
626651
}
627652

628-
/**
629-
* Handles post-diff focus behavior.
630-
* Currently defaults to focusing the edited file (Behavior A).
631-
* Future implementation will support configurable focus behavior.
632-
*/
633-
private async handlePostDiffFocus(): Promise<void> {
634-
if (!this.relPath) {
635-
return
636-
}
637-
638-
if (this.autoCloseAllRooTabs) {
639-
// Focus on the pre-diff active tab
640-
await this.focusOnPreDiffActiveTab()
641-
return
642-
}
643-
// Focus on the edited file (temporary default)
644-
await this.focusOnEditedFile()
645-
}
646-
647-
/**
648-
* Focuses on the tab of the file that was just edited.
649-
*/
650-
private async focusOnEditedFile(): Promise<void> {
651-
if (!this.relPath) {
652-
return
653-
}
654-
655-
try {
656-
const absolutePath = path.resolve(this.cwd, this.relPath)
657-
const fileUri = vscode.Uri.file(absolutePath)
658-
659-
// Check if the file still exists as a tab
660-
const editedFileTab = this.findTabForFile(absolutePath)
661-
if (editedFileTab) {
662-
// Find the tab group containing the edited file
663-
const tabGroup = vscode.window.tabGroups.all.find((group) =>
664-
group.tabs.some((tab) => tab === editedFileTab),
665-
)
666-
667-
if (tabGroup) {
668-
// Make the edited file's tab active
669-
await this.showTextDocumentSafe({
670-
uri: fileUri,
671-
options: {
672-
viewColumn: tabGroup.viewColumn,
673-
preserveFocus: false,
674-
preview: false,
675-
},
676-
})
677-
}
678-
}
679-
} catch (error) {
680-
console.error("Roo Debug: Error focusing on edited file:", error)
681-
}
682-
}
683-
684-
/**
685-
* Restores focus to the tab that was active before the diff operation.
686-
* This method is prepared for future use when configurable focus behavior is implemented.
687-
*/
688-
private async focusOnPreDiffActiveTab(): Promise<void> {
689-
if (!this.preDiffActiveEditor || !this.preDiffActiveEditor.document) {
690-
return
691-
}
692-
693-
try {
694-
// Check if the pre-diff active editor is still valid and its document is still open
695-
const isDocumentStillOpen = vscode.workspace.textDocuments.some(
696-
(doc) => doc === this.preDiffActiveEditor!.document,
697-
)
698-
699-
if (isDocumentStillOpen) {
700-
// Restore focus to the pre-diff active editor
701-
await vscode.window.showTextDocument(this.preDiffActiveEditor.document.uri, {
702-
viewColumn: this.preDiffActiveEditor.viewColumn,
703-
preserveFocus: false,
704-
preview: false,
705-
})
706-
}
707-
} catch (error) {
708-
console.error("Roo Debug: Error restoring focus to pre-diff active tab:", error)
709-
}
710-
}
711-
712-
private tabToCloseFilter(tab: vscode.Tab, settings: DiffSettings): boolean {
713-
// Always close DiffView tabs opened by Roo
714-
if (tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME) {
715-
return true
716-
}
717-
718-
let isRooOpenedTextTab = false
719-
if (tab.input instanceof vscode.TabInputText) {
720-
const currentTabUri = (tab.input as vscode.TabInputText).uri
721-
for (const openedUriString of this.rooOpenedTabs) {
722-
try {
723-
const previouslyOpenedUri = vscode.Uri.parse(openedUriString, true) // true for strict parsing
724-
if (currentTabUri.scheme === "file" && previouslyOpenedUri.scheme === "file") {
725-
if (arePathsEqual(currentTabUri.fsPath, previouslyOpenedUri.fsPath)) {
726-
isRooOpenedTextTab = true
727-
break
728-
}
729-
} else {
730-
if (currentTabUri.toString() === previouslyOpenedUri.toString()) {
731-
isRooOpenedTextTab = true
732-
break
733-
}
734-
}
735-
} catch (e) {
736-
// Log parsing error if necessary, or ignore if a URI in rooOpenedTabs is malformed
737-
console.error(`Roo Debug: Error parsing URI from rooOpenedTabs: ${openedUriString}`, e)
738-
}
739-
}
740-
}
741-
742-
if (!isRooOpenedTextTab) {
743-
return false // Not a text tab or not identified as opened by Roo
744-
}
745-
746-
// Haken 2 (settings.autoCloseAllRooTabs) - takes precedence
747-
if (settings.autoCloseAllRooTabs) {
748-
// This implies Haken 1 is also effectively on
749-
return true // Close all Roo-opened text tabs
750-
}
751-
752-
// Only Haken 1 (settings.autoCloseRooTabs) is on, Haken 2 is off
753-
if (settings.autoCloseRooTabs) {
754-
const tabUriFsPath = (tab.input as vscode.TabInputText).uri.fsPath
755-
const absolutePathDiffedFile = this.relPath ? path.resolve(this.cwd, this.relPath) : null
756-
757-
// Guard against null absolutePathDiffedFile if relPath is somehow not set
758-
if (!absolutePathDiffedFile) {
759-
// If we don't know the main diffed file, but Haken 1 is on,
760-
// it's safer to close any tab Roo opened to avoid leaving extras.
761-
return true
762-
}
763-
764-
const isMainDiffedFileTab = arePathsEqual(tabUriFsPath, absolutePathDiffedFile)
765-
766-
if (this.editType === "create" && isMainDiffedFileTab) {
767-
return true // Case: New file, Haken 1 is on -> Close its tab.
768-
}
769-
770-
if (this.editType === "modify" && isMainDiffedFileTab) {
771-
return !this.documentWasOpen
772-
}
773-
774-
// If the tab is for a file OTHER than the main diffedFile, but was opened by Roo
775-
if (!isMainDiffedFileTab) {
776-
// This covers scenarios where Roo might open auxiliary files (though less common for single diff).
777-
// If Haken 1 is on, these should also be closed.
778-
return true
779-
}
780-
}
781-
return false // Default: do not close if no above condition met
782-
}
783-
784-
private async closeTab(tab: vscode.Tab) {
785-
// If a tab has made it through the filter, it means one of the auto-close settings
786-
// (autoCloseTabs or autoCloseAllRooTabs) is active and the conditions for closing
787-
// this specific tab are met. Therefore, we should always bypass the dirty check.
788-
// const bypassDirtyCheck = true; // This is implicitly true now.
789-
790-
// Attempt to find the freshest reference to the tab before closing,
791-
// as the original 'tab' object from the initial flatMap might be stale.
792-
const tabInputToClose = tab.input
793-
const freshTabToClose = vscode.window.tabGroups.all
794-
.flatMap((group) => group.tabs)
795-
.find((t) => t.input === tabInputToClose)
796-
797-
if (freshTabToClose) {
798-
try {
799-
await vscode.window.tabGroups.close(freshTabToClose, true) // true to bypass dirty check implicitly
800-
} catch (closeError) {
801-
console.error(`Roo Debug CloseLoop: Error closing tab "${freshTabToClose.label}":`, closeError)
802-
}
803-
} else {
804-
// This case should ideally not happen if the tab was in the filtered list.
805-
// It might indicate the tab was closed by another means or its input changed.
806-
console.warn(
807-
`Roo Debug CloseLoop: Tab "${tab.label}" (input: ${JSON.stringify(tab.input)}) intended for closure was not found in the current tab list.`,
808-
)
809-
// Fallback: Try to close the original tab reference if the fresh one isn't found,
810-
// though this is less likely to succeed if it's genuinely stale.
811-
try {
812-
console.log(`Roo Debug CloseLoop: Attempting to close original (stale?) tab "${tab.label}"`)
813-
await vscode.window.tabGroups.close(tab, true)
814-
} catch (fallbackCloseError) {
815-
console.error(
816-
`Roo Debug CloseLoop: Error closing original tab reference for "${tab.label}":`,
817-
fallbackCloseError,
818-
)
819-
}
820-
}
821-
}
822-
823-
private async closeAllRooOpenedViews() {
824-
const settings = await this._readDiffSettings() // Dynamically read settings
825-
826-
const closeOps = vscode.window.tabGroups.all
827-
.flatMap((tg) => tg.tabs)
828-
.filter((tab) => this.tabToCloseFilter(tab, settings))
829-
.map(this.closeTab)
830-
831-
await Promise.all(closeOps)
832-
}
833-
834653
/**
835654
* Opens the diff editor, optionally in a specific viewColumn.
836655
*/
@@ -972,7 +791,7 @@ export class DiffViewProvider {
972791
// Ensure any diff views opened by this provider are closed to release
973792
// memory.
974793
try {
975-
await this.closeAllRooOpenedViews()
794+
await this.postDiffBehaviorUtils.closeAllRooOpenedViews(await this._readDiffSettings())
976795
} catch (error) {
977796
console.error("Error closing diff views", error)
978797
}

0 commit comments

Comments
 (0)