Skip to content

Commit 0c8355b

Browse files
letmaikCopilot
andcommitted
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 <copilot@github.com>
1 parent 3424f94 commit 0c8355b

3 files changed

Lines changed: 78 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## 1.20.0
22

3+
* Add `autoReveal` option to automatically reveal and select the tree item corresponding to the active editor (enabled by default)
34
* Add file filter functionality to tree view
45
* Automatically offer to fetch more history when merge base cannot be found in shallow clones
56
* Limit displayed diff entries to 10,000 to avoid performance issues

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,11 @@
588588
"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.",
589589
"default": "path"
590590
},
591+
"gitTreeCompare.autoReveal": {
592+
"type": "boolean",
593+
"description": "Whether to automatically reveal and select the tree item corresponding to the currently active editor.",
594+
"default": true
595+
},
591596
"gitTreeCompare.openChangesWithDifftool": {
592597
"type": "boolean",
593598
"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').",

src/treeProvider.ts

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as fs from 'fs'
55
import { TreeDataProvider, TreeItem, TreeItemCollapsibleState,
66
Uri, Disposable, EventEmitter, TextDocumentShowOptions,
77
QuickPickItem, ProgressLocation, Memento, OutputChannel,
8-
workspace, commands, window, env, WorkspaceFoldersChangeEvent, TreeView, ThemeIcon, TreeItemCheckboxState, TreeCheckboxChangeEvent, authentication } from 'vscode'
8+
workspace, commands, window, env, WorkspaceFoldersChangeEvent, TreeView, ThemeIcon, TreeItemCheckboxState, TreeCheckboxChangeEvent, authentication, TextEditor } from 'vscode'
99
import { NAMESPACE } from './constants'
1010
import { Repository, Git } from './git/git'
1111
import { Ref, RefType } from './git/api/git'
@@ -114,6 +114,12 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
114114
private _onDidChangeTreeData = new EventEmitter<Element | void>();
115115
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
116116

117+
private fireTreeDataChange() {
118+
this.parentMap.clear();
119+
this.elementMap.clear();
120+
this._onDidChangeTreeData.fire();
121+
}
122+
117123
// Configuration options
118124
private treeRootIsRepo: boolean;
119125
private includeFilesOutsideWorkspaceFolderRoot: boolean;
@@ -132,6 +138,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
132138
private omitUntrackedFiles: boolean;
133139
private omitUnstagedChanges: boolean;
134140
private sortOrder: SortOrder;
141+
private autoReveal: boolean;
135142

136143
// Dynamic options
137144
private repository: Repository | undefined;
@@ -163,6 +170,8 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
163170
private treeView: TreeView<Element>;
164171
private isPaused: boolean;
165172
private checkboxStates: Map<string, CheckboxStateInfo> = new Map<string, CheckboxStateInfo>();
173+
private parentMap: Map<string, Element> = new Map();
174+
private elementMap: Map<string, FileElement> = new Map();
166175

167176
// Other
168177
private readonly disposables: Disposable[] = [];
@@ -215,6 +224,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
215224
this.disposables.push(onRelevantWorkspaceChange(this.handleWorkspaceChange, this));
216225

217226
this.disposables.push(treeView.onDidChangeCheckboxState(this.handleChangeCheckboxState, this));
227+
this.disposables.push(window.onDidChangeActiveTextEditor(this.handleActiveEditorChange, this));
218228
}
219229

220230
async setRepository(repositoryRoot: string) {
@@ -262,7 +272,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
262272

263273
async unsetRepository() {
264274
this.repository = undefined;
265-
this._onDidChangeTreeData.fire();
275+
this.fireTreeDataChange();
266276
this.log('No repository selected');
267277

268278
this.updateTreeTitle();
@@ -282,7 +292,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
282292
this.checkboxStates.clear();
283293
this.searchFilter = undefined;
284294
this.updateFilterContext();
285-
this._onDidChangeTreeData.fire();
295+
this.fireTreeDataChange();
286296
}
287297

288298
async promptChangeRepository() {
@@ -358,6 +368,22 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
358368
}
359369
}
360370

371+
private handleActiveEditorChange(editor: TextEditor | undefined) {
372+
if (!this.autoReveal || !editor || !this.treeView.visible) {
373+
return;
374+
}
375+
const uri = editor.document.uri;
376+
if (uri.scheme !== 'file') {
377+
return;
378+
}
379+
const fileElement = this.elementMap.get(uri.fsPath);
380+
if (fileElement) {
381+
this.treeView.reveal(fileElement, { select: true, focus: false }).then(undefined, () => {
382+
// Element may not be in the tree (e.g. not yet expanded), ignore
383+
});
384+
}
385+
}
386+
361387
private log(msg: string, error: Error | undefined=undefined) {
362388
if (error) {
363389
console.warn(msg, error);
@@ -394,6 +420,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
394420
this.omitUntrackedFiles = config.get<boolean>('omitUntrackedFiles', false);
395421
this.omitUnstagedChanges = config.get<boolean>('omitUnstagedChanges', false);
396422
this.sortOrder = config.get<SortOrder>('sortOrder', 'path');
423+
this.autoReveal = config.get<boolean>('autoReveal', true);
397424
}
398425

399426
private async getStoredBaseRef(): Promise<string | undefined> {
@@ -442,6 +469,11 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
442469
return toTreeItem(element, this.openChangesOnSelect, this.iconsMinimal, this.showCollapsed, this.viewAsList, checkboxState, this.asAbsolutePath);
443470
}
444471

472+
getParent(element: Element): Element | undefined {
473+
const id = getElementId(element);
474+
return this.parentMap.get(id);
475+
}
476+
445477
private computeFolderCheckboxState(folder: FolderElement): TreeItemCheckboxState {
446478
// Check if user explicitly set state on this folder
447479
const explicitState = this.checkboxStates.get(folder.dstAbsPath);
@@ -490,20 +522,35 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
490522
this.filesInsideTreeRoot.size > 0 ||
491523
(this.includeFilesOutsideWorkspaceFolderRoot && this.filesOutsideTreeRoot.size > 0);
492524

493-
return [new RefElement(this.repoRoot, this.baseRef, hasFiles)];
525+
const children = [new RefElement(this.repoRoot, this.baseRef, hasFiles)];
526+
// RefElement is the root, no parent to record
527+
return children;
494528
} else if (element instanceof RefElement) {
495529
const entries: Element[] = [];
496530
if (this.includeFilesOutsideWorkspaceFolderRoot && this.filesOutsideTreeRoot.size > 0) {
497531
entries.push(new RepoRootElement(this.repoRoot));
498532
}
499-
return entries.concat(this.getFileSystemEntries(this.treeRoot, false));
533+
const children = entries.concat(this.getFileSystemEntries(this.treeRoot, false));
534+
this.recordParents(element, children);
535+
return children;
500536
} else if (element instanceof FolderElement) {
501-
return this.getFileSystemEntries(element.dstAbsPath, element.useFilesOutsideTreeRoot);
537+
const children = this.getFileSystemEntries(element.dstAbsPath, element.useFilesOutsideTreeRoot);
538+
this.recordParents(element, children);
539+
return children;
502540
}
503541
assert.fail("unsupported element type");
504542
return [];
505543
}
506544

545+
private recordParents(parent: Element, children: Element[]) {
546+
for (const child of children) {
547+
this.parentMap.set(getElementId(child), parent);
548+
if (child instanceof FileElement) {
549+
this.elementMap.set(child.dstAbsPath, child);
550+
}
551+
}
552+
}
553+
507554
private async updateRefs(baseRef?: string): Promise<void>
508555
{
509556
this.log('Updating refs');
@@ -617,7 +664,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
617664
this.filesInsideTreeRoot = new Map();
618665
this.filesOutsideTreeRoot = new Map();
619666
if (fireChangeEvents) {
620-
this._onDidChangeTreeData.fire();
667+
this.fireTreeDataChange();
621668
}
622669
return;
623670
}
@@ -686,7 +733,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
686733

687734
// Fire tree refresh to update checkbox UI
688735
if (actualPathsToReset.length > 0) {
689-
this._onDidChangeTreeData.fire();
736+
this.fireTreeDataChange();
690737
}
691738
}
692739

@@ -752,7 +799,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
752799

753800
if (fireChangeEvents && (treeHasChanged || needsRefreshForSorting)) {
754801
this.log('Refreshing tree')
755-
this._onDidChangeTreeData.fire();
802+
this.fireTreeDataChange();
756803
}
757804
}
758805

@@ -882,7 +929,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
882929
this.filesOutsideTreeRoot = new Map();
883930
}
884931
}
885-
this._onDidChangeTreeData.fire();
932+
this.fireTreeDataChange();
886933
}
887934
}
888935

@@ -1346,7 +1393,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
13461393
this.filesOutsideTreeRoot = new Map();
13471394
}
13481395
this.log('Refreshing tree');
1349-
this._onDidChangeTreeData.fire();
1396+
this.fireTreeDataChange();
13501397
});
13511398
}
13521399

@@ -1521,7 +1568,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
15211568
await this.updateRefs(originBaseRef);
15221569
await this.updateDiff(false);
15231570
this.log('Refreshing tree');
1524-
this._onDidChangeTreeData.fire();
1571+
this.fireTreeDataChange();
15251572
window.showInformationMessage(`Now comparing PR #${prNumber}: ${pr.title}`);
15261573
} catch (e: any) {
15271574
let msg = 'Failed to update comparison base';
@@ -1575,7 +1622,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
15751622
this.viewAsList = viewAsList;
15761623
commands.executeCommand('setContext', NAMESPACE + '.viewAsList', viewAsList);
15771624
this.log('Refreshing tree');
1578-
this._onDidChangeTreeData.fire();
1625+
this.fireTreeDataChange();
15791626
}
15801627

15811628
async sortByName() {
@@ -1623,7 +1670,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
16231670
this.updateTreeTitle();
16241671
this.updateFilterContext();
16251672
this.log(this.searchFilter ? `Filtering files by: ${this.searchFilter}` : 'Cleared file filter');
1626-
this._onDidChangeTreeData.fire();
1673+
this.fireTreeDataChange();
16271674
}
16281675

16291676
clearFilter() {
@@ -1634,7 +1681,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
16341681
this.updateTreeTitle();
16351682
this.updateFilterContext();
16361683
this.log('Cleared file filter');
1637-
this._onDidChangeTreeData.fire();
1684+
this.fireTreeDataChange();
16381685
}
16391686

16401687
private updateFilterContext() {
@@ -1715,6 +1762,16 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
17151762
}
17161763
}
17171764

1765+
function getElementId(element: Element): string {
1766+
if (element instanceof RefElement) {
1767+
return 'ref';
1768+
} else if (element instanceof RepoRootElement) {
1769+
return 'root';
1770+
} else {
1771+
return element.dstAbsPath;
1772+
}
1773+
}
1774+
17181775
function toTreeItem(element: Element, openChangesOnSelect: boolean, iconsMinimal: boolean,
17191776
showCollapsed: boolean, viewAsList: boolean,
17201777
checkboxState: TreeItemCheckboxState | undefined,

0 commit comments

Comments
 (0)