From 0c8355b18ac2c5d576e794ad0467e8e6b15bade1 Mon Sep 17 00:00:00 2001 From: Maik Riechert Date: Sun, 19 Apr 2026 21:22:08 +0100 Subject: [PATCH] Add autoReveal option to focus tree item of active editor When enabled (default), the tree view automatically reveals and selects the item corresponding to the currently active editor. Internally, parent-child relationships and file elements are now cached in maps populated during getChildren(), making both getParent() and the active editor lookup simple map lookups. Maps are cleared on every tree data change via a new fireTreeDataChange() helper. Co-authored-by: Copilot --- CHANGELOG.md | 1 + package.json | 5 +++ src/treeProvider.ts | 87 +++++++++++++++++++++++++++++++++++++-------- 3 files changed, 78 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ddc45..2aeee02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 1.20.0 +* Add `autoReveal` option to automatically reveal and select the tree item corresponding to the active editor (enabled by default) * Add file filter functionality to tree view * Automatically offer to fetch more history when merge base cannot be found in shallow clones * Limit displayed diff entries to 10,000 to avoid performance issues diff --git a/package.json b/package.json index bc2cd19..10d839e 100644 --- a/package.json +++ b/package.json @@ -588,6 +588,11 @@ "description": "How to sort files when viewing as list. Only applies in list view mode. 'name' sorts by file name, 'path' sorts by full path, 'status' sorts by git status, 'recentlyModified' sorts by modification date with most recent first.", "default": "path" }, + "gitTreeCompare.autoReveal": { + "type": "boolean", + "description": "Whether to automatically reveal and select the tree item corresponding to the currently active editor.", + "default": true + }, "gitTreeCompare.openChangesWithDifftool": { "type": "boolean", "description": "Whether to show the 'Open Changes with Difftool' command in the context menu for files. This command opens the changes in the external diff tool configured in Git (e.g., via 'git config diff.tool').", diff --git a/src/treeProvider.ts b/src/treeProvider.ts index 8664a61..fe8272c 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -5,7 +5,7 @@ import * as fs from 'fs' import { TreeDataProvider, TreeItem, TreeItemCollapsibleState, Uri, Disposable, EventEmitter, TextDocumentShowOptions, QuickPickItem, ProgressLocation, Memento, OutputChannel, - workspace, commands, window, env, WorkspaceFoldersChangeEvent, TreeView, ThemeIcon, TreeItemCheckboxState, TreeCheckboxChangeEvent, authentication } from 'vscode' + workspace, commands, window, env, WorkspaceFoldersChangeEvent, TreeView, ThemeIcon, TreeItemCheckboxState, TreeCheckboxChangeEvent, authentication, TextEditor } from 'vscode' import { NAMESPACE } from './constants' import { Repository, Git } from './git/git' import { Ref, RefType } from './git/api/git' @@ -114,6 +114,12 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos private _onDidChangeTreeData = new EventEmitter(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + private fireTreeDataChange() { + this.parentMap.clear(); + this.elementMap.clear(); + this._onDidChangeTreeData.fire(); + } + // Configuration options private treeRootIsRepo: boolean; private includeFilesOutsideWorkspaceFolderRoot: boolean; @@ -132,6 +138,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos private omitUntrackedFiles: boolean; private omitUnstagedChanges: boolean; private sortOrder: SortOrder; + private autoReveal: boolean; // Dynamic options private repository: Repository | undefined; @@ -163,6 +170,8 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos private treeView: TreeView; private isPaused: boolean; private checkboxStates: Map = new Map(); + private parentMap: Map = new Map(); + private elementMap: Map = new Map(); // Other private readonly disposables: Disposable[] = []; @@ -215,6 +224,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos this.disposables.push(onRelevantWorkspaceChange(this.handleWorkspaceChange, this)); this.disposables.push(treeView.onDidChangeCheckboxState(this.handleChangeCheckboxState, this)); + this.disposables.push(window.onDidChangeActiveTextEditor(this.handleActiveEditorChange, this)); } async setRepository(repositoryRoot: string) { @@ -262,7 +272,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos async unsetRepository() { this.repository = undefined; - this._onDidChangeTreeData.fire(); + this.fireTreeDataChange(); this.log('No repository selected'); this.updateTreeTitle(); @@ -282,7 +292,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos this.checkboxStates.clear(); this.searchFilter = undefined; this.updateFilterContext(); - this._onDidChangeTreeData.fire(); + this.fireTreeDataChange(); } async promptChangeRepository() { @@ -358,6 +368,22 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos } } + private handleActiveEditorChange(editor: TextEditor | undefined) { + if (!this.autoReveal || !editor || !this.treeView.visible) { + return; + } + const uri = editor.document.uri; + if (uri.scheme !== 'file') { + return; + } + const fileElement = this.elementMap.get(uri.fsPath); + if (fileElement) { + this.treeView.reveal(fileElement, { select: true, focus: false }).then(undefined, () => { + // Element may not be in the tree (e.g. not yet expanded), ignore + }); + } + } + private log(msg: string, error: Error | undefined=undefined) { if (error) { console.warn(msg, error); @@ -394,6 +420,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos this.omitUntrackedFiles = config.get('omitUntrackedFiles', false); this.omitUnstagedChanges = config.get('omitUnstagedChanges', false); this.sortOrder = config.get('sortOrder', 'path'); + this.autoReveal = config.get('autoReveal', true); } private async getStoredBaseRef(): Promise { @@ -442,6 +469,11 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos return toTreeItem(element, this.openChangesOnSelect, this.iconsMinimal, this.showCollapsed, this.viewAsList, checkboxState, this.asAbsolutePath); } + getParent(element: Element): Element | undefined { + const id = getElementId(element); + return this.parentMap.get(id); + } + private computeFolderCheckboxState(folder: FolderElement): TreeItemCheckboxState { // Check if user explicitly set state on this folder const explicitState = this.checkboxStates.get(folder.dstAbsPath); @@ -490,20 +522,35 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos this.filesInsideTreeRoot.size > 0 || (this.includeFilesOutsideWorkspaceFolderRoot && this.filesOutsideTreeRoot.size > 0); - return [new RefElement(this.repoRoot, this.baseRef, hasFiles)]; + const children = [new RefElement(this.repoRoot, this.baseRef, hasFiles)]; + // RefElement is the root, no parent to record + return children; } else if (element instanceof RefElement) { const entries: Element[] = []; if (this.includeFilesOutsideWorkspaceFolderRoot && this.filesOutsideTreeRoot.size > 0) { entries.push(new RepoRootElement(this.repoRoot)); } - return entries.concat(this.getFileSystemEntries(this.treeRoot, false)); + const children = entries.concat(this.getFileSystemEntries(this.treeRoot, false)); + this.recordParents(element, children); + return children; } else if (element instanceof FolderElement) { - return this.getFileSystemEntries(element.dstAbsPath, element.useFilesOutsideTreeRoot); + const children = this.getFileSystemEntries(element.dstAbsPath, element.useFilesOutsideTreeRoot); + this.recordParents(element, children); + return children; } assert.fail("unsupported element type"); return []; } + private recordParents(parent: Element, children: Element[]) { + for (const child of children) { + this.parentMap.set(getElementId(child), parent); + if (child instanceof FileElement) { + this.elementMap.set(child.dstAbsPath, child); + } + } + } + private async updateRefs(baseRef?: string): Promise { this.log('Updating refs'); @@ -617,7 +664,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos this.filesInsideTreeRoot = new Map(); this.filesOutsideTreeRoot = new Map(); if (fireChangeEvents) { - this._onDidChangeTreeData.fire(); + this.fireTreeDataChange(); } return; } @@ -686,7 +733,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos // Fire tree refresh to update checkbox UI if (actualPathsToReset.length > 0) { - this._onDidChangeTreeData.fire(); + this.fireTreeDataChange(); } } @@ -752,7 +799,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos if (fireChangeEvents && (treeHasChanged || needsRefreshForSorting)) { this.log('Refreshing tree') - this._onDidChangeTreeData.fire(); + this.fireTreeDataChange(); } } @@ -882,7 +929,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos this.filesOutsideTreeRoot = new Map(); } } - this._onDidChangeTreeData.fire(); + this.fireTreeDataChange(); } } @@ -1346,7 +1393,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos this.filesOutsideTreeRoot = new Map(); } this.log('Refreshing tree'); - this._onDidChangeTreeData.fire(); + this.fireTreeDataChange(); }); } @@ -1521,7 +1568,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos await this.updateRefs(originBaseRef); await this.updateDiff(false); this.log('Refreshing tree'); - this._onDidChangeTreeData.fire(); + this.fireTreeDataChange(); window.showInformationMessage(`Now comparing PR #${prNumber}: ${pr.title}`); } catch (e: any) { let msg = 'Failed to update comparison base'; @@ -1575,7 +1622,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos this.viewAsList = viewAsList; commands.executeCommand('setContext', NAMESPACE + '.viewAsList', viewAsList); this.log('Refreshing tree'); - this._onDidChangeTreeData.fire(); + this.fireTreeDataChange(); } async sortByName() { @@ -1623,7 +1670,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos this.updateTreeTitle(); this.updateFilterContext(); this.log(this.searchFilter ? `Filtering files by: ${this.searchFilter}` : 'Cleared file filter'); - this._onDidChangeTreeData.fire(); + this.fireTreeDataChange(); } clearFilter() { @@ -1634,7 +1681,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos this.updateTreeTitle(); this.updateFilterContext(); this.log('Cleared file filter'); - this._onDidChangeTreeData.fire(); + this.fireTreeDataChange(); } private updateFilterContext() { @@ -1715,6 +1762,16 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos } } +function getElementId(element: Element): string { + if (element instanceof RefElement) { + return 'ref'; + } else if (element instanceof RepoRootElement) { + return 'root'; + } else { + return element.dstAbsPath; + } +} + function toTreeItem(element: Element, openChangesOnSelect: boolean, iconsMinimal: boolean, showCollapsed: boolean, viewAsList: boolean, checkboxState: TreeItemCheckboxState | undefined,