Skip to content

Commit 95ff78e

Browse files
committed
Add file filter functionality to tree view
- Add Filter Files command with input box - Add Clear Filter command (visible only when filtered) - Filter matches against filename and relative path (case-insensitive) - Hide empty folders when filtering in tree view mode - Update tree title to show (filtered) state - Maintain existing Search Changes functionality
1 parent 14fe364 commit 95ff78e

4 files changed

Lines changed: 130 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.20.0
2+
3+
* Add file filter functionality to tree view
4+
15
## 1.19.0
26

37
* Add GitHub PR comparison command [#130](https://github.com/letmaik/vscode-git-tree-compare/pull/130)

package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,18 @@
154154
"icon": "$(search)",
155155
"category": "Git Tree Compare"
156156
},
157+
{
158+
"command": "gitTreeCompare.filterFiles",
159+
"title": "Filter Files",
160+
"icon": "$(filter)",
161+
"category": "Git Tree Compare"
162+
},
163+
{
164+
"command": "gitTreeCompare.clearFilter",
165+
"title": "Clear Filter",
166+
"icon": "$(clear-all)",
167+
"category": "Git Tree Compare"
168+
},
157169
{
158170
"command": "gitTreeCompare.copyPath",
159171
"title": "Copy Path",
@@ -318,6 +330,16 @@
318330
"when": "view == gitTreeCompare",
319331
"group": "2_files"
320332
},
333+
{
334+
"command": "gitTreeCompare.filterFiles",
335+
"when": "view == gitTreeCompare",
336+
"group": "navigation@4"
337+
},
338+
{
339+
"command": "gitTreeCompare.clearFilter",
340+
"when": "view == gitTreeCompare && gitTreeCompare.isFiltered",
341+
"group": "navigation@5"
342+
},
321343
{
322344
"submenu": "gitTreeCompare.viewAndSort",
323345
"when": "view == gitTreeCompare",

src/extension.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ export function activate(context: ExtensionContext) {
9797
commands.registerCommand(NAMESPACE + '.searchChanges', () => {
9898
runAfterInit(() => provider!.searchChanges());
9999
});
100+
commands.registerCommand(NAMESPACE + '.filterFiles', () => {
101+
runAfterInit(() => provider!.filterFiles());
102+
});
103+
commands.registerCommand(NAMESPACE + '.clearFilter', () => {
104+
runAfterInit(() => provider!.clearFilter());
105+
});
100106
commands.registerCommand(NAMESPACE + '.copyPath', node => {
101107
runAfterInit(() => provider!.copyPath(node));
102108
});
@@ -127,6 +133,7 @@ export function activate(context: ExtensionContext) {
127133

128134
// Set initial context for menu enablement (starts in tree view mode)
129135
commands.executeCommand('setContext', NAMESPACE + '.viewAsList', false);
136+
commands.executeCommand('setContext', NAMESPACE + '.isFiltered', false);
130137

131138
provider = new GitTreeCompareProvider(git, gitApi, outputChannel, context.globalState, context.asAbsolutePath);
132139

src/treeProvider.ts

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
134134
private repository: Repository | undefined;
135135
private baseRef: string;
136136
private viewAsList = false;
137+
private searchFilter: string | undefined;
137138

138139
// Static state of repository
139140
private workspaceFolder: string;
@@ -240,16 +241,28 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
240241
this.updateTreeRootFolder();
241242
this.log('Using repository: ' + this.repoRoot);
242243

243-
const repoName = path.basename(repoRoot);
244-
this.treeView.title = repoName;
244+
this.updateTreeTitle();
245+
}
246+
247+
private updateTreeTitle() {
248+
if (!this.repository) {
249+
this.treeView.title = 'none';
250+
return;
251+
}
252+
const repoName = path.basename(this.repoRoot);
253+
if (this.searchFilter) {
254+
this.treeView.title = `${repoName} (filtered)`;
255+
} else {
256+
this.treeView.title = repoName;
257+
}
245258
}
246259

247260
async unsetRepository() {
248261
this.repository = undefined;
249262
this._onDidChangeTreeData.fire();
250263
this.log('No repository selected');
251264

252-
this.treeView.title = 'none';
265+
this.updateTreeTitle();
253266
}
254267

255268
async changeRepository(repositoryRoot: string) {
@@ -264,6 +277,8 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
264277
return;
265278
}
266279
this.checkboxStates.clear();
280+
this.searchFilter = undefined;
281+
this.updateFilterContext();
267282
this._onDidChangeTreeData.fire();
268283
}
269284

@@ -831,6 +846,36 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
831846
}
832847
}
833848

849+
private matchesFilter(filePath: string, relPathBase: string): boolean {
850+
if (!this.searchFilter) {
851+
return true;
852+
}
853+
const fileName = path.basename(filePath);
854+
const relativePath = path.relative(relPathBase, filePath);
855+
const searchLower = this.searchFilter.toLowerCase();
856+
return fileName.toLowerCase().includes(searchLower) ||
857+
relativePath.toLowerCase().includes(searchLower);
858+
}
859+
860+
private folderHasMatchingFiles(folder: string, useFilesOutsideTreeRoot: boolean): boolean {
861+
if (!this.searchFilter) {
862+
return true;
863+
}
864+
const files = useFilesOutsideTreeRoot ? this.filesOutsideTreeRoot : this.filesInsideTreeRoot;
865+
const relPathBase = useFilesOutsideTreeRoot ? this.repoRoot : this.treeRoot;
866+
867+
for (const [folderPath, fileEntries] of files.entries()) {
868+
if (folderPath === folder || folderPath.startsWith(folder + path.sep)) {
869+
for (const file of fileEntries) {
870+
if (this.matchesFilter(file.dstAbsPath, relPathBase)) {
871+
return true;
872+
}
873+
}
874+
}
875+
}
876+
return false;
877+
}
878+
834879
private getFileSystemEntries(folder: string, useFilesOutsideTreeRoot: boolean): FileSystemElement[] {
835880
const entries: FileSystemElement[] = [];
836881
const files = useFilesOutsideTreeRoot ? this.filesOutsideTreeRoot : this.filesInsideTreeRoot;
@@ -849,14 +894,19 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
849894
for (const folder2 of folders) {
850895
const fileEntries = files.get(folder2)!;
851896
for (const file of fileEntries) {
852-
const dstRelPath = path.relative(relPathBase, file.dstAbsPath);
853-
entries.push(new FileElement(file.srcAbsPath, file.dstAbsPath, dstRelPath, file.status, file.isSubmodule));
897+
if (this.matchesFilter(file.dstAbsPath, relPathBase)) {
898+
const dstRelPath = path.relative(relPathBase, file.dstAbsPath);
899+
entries.push(new FileElement(file.srcAbsPath, file.dstAbsPath, dstRelPath, file.status, file.isSubmodule));
900+
}
854901
}
855902
}
856903
} else if (this.compactFolders) {
857904
// add direct subfolders and apply compaction
858905
for (const folder2 of files.keys()) {
859906
if (path.dirname(folder2) === folder) {
907+
if (!this.folderHasMatchingFiles(folder2, useFilesOutsideTreeRoot)) {
908+
continue;
909+
}
860910
let compactedPath = folder2;
861911
// not very efficient, needs a better data structure
862912
outer: while (true) {
@@ -891,9 +941,11 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
891941
// add direct subfolders
892942
for (const folder2 of files.keys()) {
893943
if (path.dirname(folder2) === folder) {
894-
const label = path.basename(folder2);
895-
entries.push(new FolderElement(
896-
label, folder2, useFilesOutsideTreeRoot));
944+
if (this.folderHasMatchingFiles(folder2, useFilesOutsideTreeRoot)) {
945+
const label = path.basename(folder2);
946+
entries.push(new FolderElement(
947+
label, folder2, useFilesOutsideTreeRoot));
948+
}
897949
}
898950
}
899951
entries.sort((a, b) => path.basename(a.dstAbsPath).localeCompare(path.basename(b.dstAbsPath)));
@@ -905,8 +957,10 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
905957
// there are no files within treeRoot, therefore, this is guarded
906958
if (fileEntries) {
907959
for (const file of fileEntries) {
908-
const dstRelPath = path.relative(relPathBase, file.dstAbsPath);
909-
entries.push(new FileElement(file.srcAbsPath, file.dstAbsPath, dstRelPath, file.status, file.isSubmodule));
960+
if (this.matchesFilter(file.dstAbsPath, relPathBase)) {
961+
const dstRelPath = path.relative(relPathBase, file.dstAbsPath);
962+
entries.push(new FileElement(file.srcAbsPath, file.dstAbsPath, dstRelPath, file.status, file.isSubmodule));
963+
}
910964
}
911965
}
912966

@@ -1514,6 +1568,39 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
15141568
});
15151569
}
15161570

1571+
async filterFiles() {
1572+
const searchTerm = await window.showInputBox({
1573+
prompt: 'Enter text to filter files (leave empty to show all)',
1574+
placeHolder: 'Filter by filename or path...',
1575+
value: this.searchFilter || ''
1576+
});
1577+
1578+
if (searchTerm === undefined) {
1579+
return;
1580+
}
1581+
1582+
this.searchFilter = searchTerm.trim() || undefined;
1583+
this.updateTreeTitle();
1584+
this.updateFilterContext();
1585+
this.log(this.searchFilter ? `Filtering files by: ${this.searchFilter}` : 'Cleared file filter');
1586+
this._onDidChangeTreeData.fire();
1587+
}
1588+
1589+
clearFilter() {
1590+
if (!this.searchFilter) {
1591+
return;
1592+
}
1593+
this.searchFilter = undefined;
1594+
this.updateTreeTitle();
1595+
this.updateFilterContext();
1596+
this.log('Cleared file filter');
1597+
this._onDidChangeTreeData.fire();
1598+
}
1599+
1600+
private updateFilterContext() {
1601+
commands.executeCommand('setContext', NAMESPACE + '.isFiltered', !!this.searchFilter);
1602+
}
1603+
15171604
async copyPath(fileEntry: FileElement) {
15181605
const diffStatus = this.getDiffStatus(fileEntry);
15191606
if (!diffStatus) {

0 commit comments

Comments
 (0)