Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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').",
Expand Down
87 changes: 72 additions & 15 deletions src/treeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -114,6 +114,12 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
private _onDidChangeTreeData = new EventEmitter<Element | void>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

private fireTreeDataChange() {
this.parentMap.clear();
this.elementMap.clear();
this._onDidChangeTreeData.fire();
}

// Configuration options
private treeRootIsRepo: boolean;
private includeFilesOutsideWorkspaceFolderRoot: boolean;
Expand All @@ -132,6 +138,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
private omitUntrackedFiles: boolean;
private omitUnstagedChanges: boolean;
private sortOrder: SortOrder;
private autoReveal: boolean;

// Dynamic options
private repository: Repository | undefined;
Expand Down Expand Up @@ -163,6 +170,8 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
private treeView: TreeView<Element>;
private isPaused: boolean;
private checkboxStates: Map<string, CheckboxStateInfo> = new Map<string, CheckboxStateInfo>();
private parentMap: Map<string, Element> = new Map();
private elementMap: Map<string, FileElement> = new Map();

// Other
private readonly disposables: Disposable[] = [];
Expand Down Expand Up @@ -215,6 +224,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, 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) {
Expand Down Expand Up @@ -262,7 +272,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos

async unsetRepository() {
this.repository = undefined;
this._onDidChangeTreeData.fire();
this.fireTreeDataChange();
this.log('No repository selected');

this.updateTreeTitle();
Expand All @@ -282,7 +292,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
this.checkboxStates.clear();
this.searchFilter = undefined;
this.updateFilterContext();
this._onDidChangeTreeData.fire();
this.fireTreeDataChange();
}

async promptChangeRepository() {
Expand Down Expand Up @@ -358,6 +368,22 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, 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);
Expand Down Expand Up @@ -394,6 +420,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
this.omitUntrackedFiles = config.get<boolean>('omitUntrackedFiles', false);
this.omitUnstagedChanges = config.get<boolean>('omitUnstagedChanges', false);
this.sortOrder = config.get<SortOrder>('sortOrder', 'path');
this.autoReveal = config.get<boolean>('autoReveal', true);
}

private async getStoredBaseRef(): Promise<string | undefined> {
Expand Down Expand Up @@ -442,6 +469,11 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, 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);
Expand Down Expand Up @@ -490,20 +522,35 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, 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<void>
{
this.log('Updating refs');
Expand Down Expand Up @@ -617,7 +664,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
this.filesInsideTreeRoot = new Map();
this.filesOutsideTreeRoot = new Map();
if (fireChangeEvents) {
this._onDidChangeTreeData.fire();
this.fireTreeDataChange();
}
return;
}
Expand Down Expand Up @@ -686,7 +733,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos

// Fire tree refresh to update checkbox UI
if (actualPathsToReset.length > 0) {
this._onDidChangeTreeData.fire();
this.fireTreeDataChange();
}
}

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

if (fireChangeEvents && (treeHasChanged || needsRefreshForSorting)) {
this.log('Refreshing tree')
this._onDidChangeTreeData.fire();
this.fireTreeDataChange();
}
}

Expand Down Expand Up @@ -882,7 +929,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
this.filesOutsideTreeRoot = new Map();
}
}
this._onDidChangeTreeData.fire();
this.fireTreeDataChange();
}
}

Expand Down Expand Up @@ -1346,7 +1393,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
this.filesOutsideTreeRoot = new Map();
}
this.log('Refreshing tree');
this._onDidChangeTreeData.fire();
this.fireTreeDataChange();
});
}

Expand Down Expand Up @@ -1521,7 +1568,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, 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';
Expand Down Expand Up @@ -1575,7 +1622,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
this.viewAsList = viewAsList;
commands.executeCommand('setContext', NAMESPACE + '.viewAsList', viewAsList);
this.log('Refreshing tree');
this._onDidChangeTreeData.fire();
this.fireTreeDataChange();
}

async sortByName() {
Expand Down Expand Up @@ -1623,7 +1670,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
this.updateTreeTitle();
this.updateFilterContext();
this.log(this.searchFilter ? `Filtering files by: ${this.searchFilter}` : 'Cleared file filter');
this._onDidChangeTreeData.fire();
this.fireTreeDataChange();
}

clearFilter() {
Expand All @@ -1634,7 +1681,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
this.updateTreeTitle();
this.updateFilterContext();
this.log('Cleared file filter');
this._onDidChangeTreeData.fire();
this.fireTreeDataChange();
}

private updateFilterContext() {
Expand Down Expand Up @@ -1715,6 +1762,16 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, 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,
Expand Down
Loading