Skip to content
Open
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
41 changes: 40 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,30 @@
"command": "git-history.history.filter",
"title": "Git History",
"icon": "$(filter)"
},
{
"category": "Git History",
"command": "git-history.changes.view.toggle",
"title": "Toggle Changes View",
"icon": "$(list-tree)"
},
{
"category": "Git History",
"command": "git-history.changes.view.tree",
"title": "Changes Tree View",
"icon": "$(list-tree)"
},
{
"category": "Git History",
"command": "git-history.changes.view.flat",
"title": "Changes Flat List View",
"icon": "$(list-flat)"
},
{
"category": "Git History",
"command": "git-history.changes.filter",
"title": "Filter Changes",
"icon": "$(filter)"
}
],
"menus": {
Expand All @@ -117,6 +141,21 @@
"command": "git-history.history.switch.branch",
"when": "view =~ /git-history.history/",
"group": "navigation@4"
},
{
"command": "git-history.changes.filter",
"when": "view == git-history.changes",
"group": "navigation@1"
},
{
"command": "git-history.changes.view.tree",
"when": "view =~ /git-history.changes/ && !gitHistory:changesViewIsTree",
"group": "navigation@2"
},
{
"command": "git-history.changes.view.flat",
"when": "view =~ /git-history.changes/ && gitHistory:changesViewIsTree",
"group": "navigation@2"
}
]
}
Expand Down Expand Up @@ -293,4 +332,4 @@
"stylelint-config-prettier"
]
}
}
}
117 changes: 105 additions & 12 deletions src/views/changes/ChangeTreeDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,32 +28,117 @@ import {
export class ChangeTreeDataProvider implements TreeDataProvider<TreeItem> {
private _onDidChangeTreeData = new EventEmitter<void>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
private _isTreeView: boolean = true; // true: tree, false: flat
private _filterText: string = '';

constructor(
@inject(TYPES.ExtensionContext) private context: ExtensionContext
) {}
) {
this._isTreeView = this.context.globalState.get<boolean>("changesTreeView", true);
}

get isTreeView(): boolean {
return this._isTreeView;
}

get filterText(): string {
return this._filterText;
}

get isFiltered(): boolean {
return this._filterText !== '';
}

setViewMode(mode: string): void {
this._isTreeView = mode === 'toggle' ? !this._isTreeView : (mode === "tree");
this.context.globalState.update("changesTreeView", this._isTreeView);
this.refresh();
}

setFilter(filterText?: string): void {
this._filterText = (filterText?.trim() || '').toLowerCase();
this.refresh();
}

getTreeItem(element: Path) {
return element;
}

getChildren(element?: Path) {
// TODO: order by type and name

if (!this.isTreeView) {
const fileTree = rebuildUri(
this.context.globalState.get<PathCollection>("changedFileTree")
)!;
return Promise.resolve(this.getFlatFileList(fileTree));
}

const sourceCollection = element
? (element.children as PathCollection)!
: rebuildUri(
this.context.globalState.get<PathCollection>(
"changedFileTree"
)
)!;

let childrenEntries = Object.entries(sourceCollection)
.sort(compareFileTreeNode);

if (this._filterText) {
childrenEntries = childrenEntries.filter(([name, props]) =>
this._nodeMatchesOrHasMatchingDescendant(props, name, this._filterText)
);
}

return Promise.resolve(
Object.entries(
element
? (element.children as PathCollection)!
: rebuildUri(
this.context.globalState.get<PathCollection>(
"changedFileTree"
)
)!
)
.sort(compareFileTreeNode)
.map(([name, props]) => new Path(name, props))
childrenEntries.map(([name, props]) => new Path(name, props))
);
}

private _nodeMatchesOrHasMatchingDescendant(props: FolderNode | FileNode, name: string, filterText: string): boolean {
if (name.toLowerCase().includes(filterText)) {
return true;
}

if (props.type === PathType.FOLDER) {
const folderNode = props as FolderNode;
if (folderNode.children) {
for (const [childName, childProps] of Object.entries(folderNode.children)) {
if (this._nodeMatchesOrHasMatchingDescendant(childProps, childName, filterText)) {
return true;
}
}
}
}
return false;
}

private getFlatFileList(tree: PathCollection): Path[] {
const result: Path[] = [];

const traverseTree = (node: PathCollection, parentPath: string = '') => {
Object.entries(node).forEach(([name, props]) => {
const fullPath = parentPath ? `${parentPath}/${name}` : name;

if (props.type === PathType.FILE) {
const filePath = new Path(fullPath, props);
result.push(filePath);
} else if (props.type === PathType.FOLDER) {
traverseTree((props as FolderNode).children, fullPath);
}
});
};

traverseTree(tree);

const filteredResult = this._filterText
? result.filter(pathItem => pathItem.label.toLowerCase().includes(this._filterText))
: result;

return filteredResult.sort((a, b) => a.label.localeCompare(b.label));
}

refresh() {
this._onDidChangeTreeData.fire();
}
Expand All @@ -65,9 +150,17 @@ class Path extends TreeItem {
resourceUri = this.getResourceUri();
collapsibleState = this.getCollapsibleState();
readonly command?: Command = this.getCommand();
originalPath: string;

constructor(public label: string, public props: FolderNode | FileNode) {
super(label);

this.originalPath = label;

// For flat list view, we want to show the full path in the tooltip
if (this.props.type === PathType.FILE) {
this.tooltip = this.originalPath;
}
}

private getResourceUri() {
Expand Down
59 changes: 58 additions & 1 deletion src/views/changes/ChangeTreeView.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExtensionContext, TreeView, window } from "vscode";
import { ExtensionContext, TreeView, window, commands } from "vscode";
import { inject, injectable } from "inversify";

import { TYPES } from "../../container/types";
Expand All @@ -21,5 +21,62 @@ export class ChangeTreeView {
treeDataProvider: this.changeTreeDataProvider,
}
);

commands.registerCommand(
`${EXTENSION_SCHEME}.changes.view.toggle`,
this.viewToggle.bind(this)
);

commands.registerCommand(
`${EXTENSION_SCHEME}.changes.view.tree`,
this.viewTree.bind(this)
);

commands.registerCommand(
`${EXTENSION_SCHEME}.changes.view.flat`,
this.viewFlat.bind(this)
);

commands.registerCommand(
`${EXTENSION_SCHEME}.changes.filter`,
this.setFilter.bind(this)
);

this.updateView();
}

private viewToggle(): void {
this.changeTreeDataProvider.setViewMode('toggle');
this.updateView();
}

private viewTree(): void {
this.changeTreeDataProvider.setViewMode('tree');
this.updateView();
}

private viewFlat(): void {
this.changeTreeDataProvider.setViewMode('flat');
this.updateView();
}

private async setFilter(): Promise<void> {
const filterText = await window.showInputBox({
placeHolder: "Input filenames to filter changes",
value: this.changeTreeDataProvider.filterText
});

this.changeTreeDataProvider.setFilter(filterText);
this.updateView();
}

private updateView(): void {
const isTreeView = this.changeTreeDataProvider.isTreeView;
const isFiltered = this.changeTreeDataProvider.isFiltered;
this.changesViewer.description = isTreeView ? "Tree View" : "Flat List View";
if (isFiltered) {
this.changesViewer.description += ' (filtered)';
}
commands.executeCommand('setContext', 'gitHistory:changesViewIsTree', isTreeView);
}
}