Skip to content

Commit ce93942

Browse files
committed
Adds new file layouts to the custom view
1 parent 4d18bf7 commit ce93942

File tree

11 files changed

+346
-32
lines changed

11 files changed

+346
-32
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
66

77
## [Unreleased]
88
### Added
9+
- Adds new file layouts to the `GitLens` custom view
10+
- `auto` - automatically switches between displaying files as a `tree` or `list` based on the `gitlens.gitExplorer.files.threshold` setting and the number of files at each nesting level
11+
- `list` - displays files as a list
12+
- `tree` - displays files as a tree
13+
- Adds `gitlens.gitExplorer.files.layout` setting to specify how the `GitLens` custom view will display files
14+
- Adds `gitlens.gitExplorer.files.compact` setting to specify whether or not to compact (flatten) unnecessary file nesting in the `GitLens` custom view
15+
- Adds `gitlens.gitExplorer.files.threshold` setting to specify when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the `GitLens` custom view
916
- Adds `${directory}` token to the file formatting settings
1017

1118
### Changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,9 @@ GitLens is highly customizable and provides many configuration settings to allow
356356
|-----|------------
357357
|`gitlens.gitExplorer.enabled`|Specifies whether or not to show the `GitLens` custom view"
358358
|`gitlens.gitExplorer.view`|Specifies the starting view (mode) of the `GitLens` custom view<br /> `auto` - shows the last selected view, defaults to `repository`<br />`history` - shows the commit history of the active file<br />`repository` - shows a repository explorer"
359+
|`gitlens.gitExplorer.files.layout`|Specifies how the `GitLens` custom view will display files<br /> `auto` - automatically switches between displaying files as a `tree` or `list` based on the `gitlens.gitExplorer.files.threshold` setting and the number of files at each nesting level<br /> `list` - displays files as a list<br /> `tree` - displays files as a tree
360+
|`gitlens.gitExplorer.files.compact`|Specifies whether or not to compact (flatten) unnecessary file nesting in the `GitLens` custom view<br />Only applies when displaying files as a `tree` or `auto`
361+
|`gitlens.gitExplorer.files.threshold`|Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the `GitLens` custom view<br />Only applies when displaying files as `auto`
359362
|`gitlens.gitExplorer.includeWorkingTree`|Specifies whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view
360363
|`gitlens.gitExplorer.showTrackingBranch`|Specifies whether or not to show the tracking branch when displaying local branches in the `GitLens` custom view"
361364
|`gitlens.gitExplorer.commitFormat`|Specifies the format of committed changes in the `GitLens` custom view<br />Available tokens<br /> ${id} - commit id<br /> ${author} - commit author<br /> ${message} - commit message<br /> ${ago} - relative commit date (e.g. 1 day ago)<br /> ${date} - formatted commit date (format specified by `gitlens.statusBar.dateFormat`)<br /> ${authorAgo} - commit author, relative commit date<br />See https://github.com/eamodio/vscode-gitlens/wiki/Advanced-Formatting for advanced formatting

package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,26 @@
428428
"default": true,
429429
"description": "Specifies whether or not to show the `GitLens` custom view"
430430
},
431+
"gitlens.gitExplorer.files.layout": {
432+
"type": "string",
433+
"default": "auto",
434+
"enum": [
435+
"auto",
436+
"list",
437+
"tree"
438+
],
439+
"description": "Specifies how the `GitLens` custom view will display files\n `auto` - automatically switches between displaying files as a `tree` or `list` based on the `gitlens.gitExplorer.files.threshold` setting and the number of files at each nesting level\n `list` - displays files as a list\n `tree` - displays files as a tree"
440+
},
441+
"gitlens.gitExplorer.files.compact": {
442+
"type": "boolean",
443+
"default": true,
444+
"description": "Specifies whether or not to compact (flatten) unnecessary file nesting in the `GitLens` custom view\nOnly applies when displaying files as a `tree` or `auto`"
445+
},
446+
"gitlens.gitExplorer.files.threshold": {
447+
"type": "number",
448+
"default": 5,
449+
"description": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the `GitLens` custom view\nOnly applies when displaying files as `auto`"
450+
},
431451
"gitlens.gitExplorer.includeWorkingTree": {
432452
"type": "boolean",
433453
"default": true,

src/configuration.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ export const CustomRemoteType = {
5353
GitLab: 'GitLab' as CustomRemoteType
5454
};
5555

56+
export type GitExplorerFilesLayout =
57+
'auto' |
58+
'list' |
59+
'tree';
60+
export const GitExplorerFilesLayout = {
61+
Auto: 'auto' as GitExplorerFilesLayout,
62+
List: 'list' as GitExplorerFilesLayout,
63+
Tree: 'tree' as GitExplorerFilesLayout
64+
};
65+
5666
export type StatusBarCommand =
5767
'gitlens.toggleFileBlame' |
5868
'gitlens.showBlameHistory' |
@@ -132,6 +142,24 @@ export interface ICodeLensLanguageLocation {
132142
customSymbols?: string[];
133143
}
134144

145+
export interface IGitExplorerConfig {
146+
enabled: boolean;
147+
view: GitExplorerView;
148+
files: {
149+
layout: GitExplorerFilesLayout;
150+
compact: boolean;
151+
threshold: number;
152+
};
153+
includeWorkingTree: boolean;
154+
showTrackingBranch: boolean;
155+
commitFormat: string;
156+
commitFileFormat: string;
157+
stashFormat: string;
158+
stashFileFormat: string;
159+
statusFileFormat: string;
160+
// dateFormat: string | null;
161+
}
162+
135163
export interface IRemotesConfig {
136164
type: CustomRemoteType;
137165
domain: string;
@@ -316,18 +344,7 @@ export interface IConfig {
316344

317345
defaultDateFormat: string | null;
318346

319-
gitExplorer: {
320-
enabled: boolean;
321-
view: GitExplorerView;
322-
includeWorkingTree: boolean;
323-
showTrackingBranch: boolean;
324-
commitFormat: string;
325-
commitFileFormat: string;
326-
stashFormat: string;
327-
stashFileFormat: string;
328-
statusFileFormat: string;
329-
// dateFormat: string | null;
330-
};
347+
gitExplorer: IGitExplorerConfig;
331348

332349
remotes: IRemotesConfig[];
333350

src/system/array.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
'use strict';
2+
import { Objects } from './object';
23

34
export namespace Arrays {
5+
export function countUniques<T>(array: T[], accessor: (item: T) => string): { [key: string]: number } {
6+
const uniqueCounts = Object.create(null);
7+
for (const item of array) {
8+
const value = accessor(item);
9+
uniqueCounts[value] = (uniqueCounts[value] || 0) + 1;
10+
}
11+
return uniqueCounts;
12+
}
13+
414
export function groupBy<T>(array: T[], accessor: (item: T) => string): { [key: string]: T[] } {
515
return array.reduce((previous, current) => {
616
const value = accessor(current);
@@ -10,6 +20,96 @@ export namespace Arrays {
1020
}, Object.create(null));
1121
}
1222

23+
export interface IHierarchicalItem<T> {
24+
name: string;
25+
relativePath: string;
26+
value?: T;
27+
28+
// parent?: IHierarchicalItem<T>;
29+
children: { [key: string]: IHierarchicalItem<T> } | undefined;
30+
descendants: T[] | undefined;
31+
}
32+
33+
export function makeHierarchical<T>(values: T[], splitPath: (i: T) => string[], joinPath: (...paths: string[]) => string, compact: boolean = false): IHierarchicalItem<T> {
34+
const seed = {
35+
name: '',
36+
relativePath: '',
37+
children: Object.create(null),
38+
descendants: []
39+
};
40+
41+
const hierarchy = values.reduce((root: IHierarchicalItem<T>, value) => {
42+
let folder = root;
43+
44+
let relativePath = '';
45+
for (const folderName of splitPath(value)) {
46+
relativePath = joinPath(relativePath, folderName);
47+
48+
if (folder.children === undefined) {
49+
folder.children = Object.create(null);
50+
}
51+
52+
let f = folder.children![folderName];
53+
if (f === undefined) {
54+
folder.children![folderName] = f = {
55+
name: folderName,
56+
relativePath: relativePath,
57+
// parent: folder,
58+
children: undefined,
59+
descendants: undefined
60+
};
61+
}
62+
63+
if (folder.descendants === undefined) {
64+
folder.descendants = [];
65+
}
66+
folder.descendants.push(value);
67+
folder = f;
68+
}
69+
70+
folder.value = value;
71+
72+
return root;
73+
}, seed);
74+
75+
if (compact) return compactHierarchy(hierarchy, joinPath, true);
76+
return hierarchy;
77+
}
78+
79+
export function compactHierarchy<T>(root: IHierarchicalItem<T>, joinPath: (...paths: string[]) => string, isRoot: boolean = true): IHierarchicalItem<T> {
80+
if (root.children === undefined) return root;
81+
82+
const children = [...Objects.values(root.children)];
83+
84+
// // Attempts less nesting but duplicate roots
85+
// if (!isRoot && children.every(c => c.value === undefined)) {
86+
// const parentSiblings = root.parent!.children!;
87+
// if (parentSiblings[root.name] !== undefined) {
88+
// delete parentSiblings[root.name];
89+
90+
// for (const child of children) {
91+
// child.name = joinPath(root.name, child.name);
92+
// parentSiblings[child.name] = child;
93+
// }
94+
// }
95+
// }
96+
97+
for (const child of children) {
98+
compactHierarchy(child, joinPath, false);
99+
}
100+
101+
if (!isRoot && children.length === 1) {
102+
const child = children[0];
103+
if (child.value === undefined) {
104+
root.name = joinPath(root.name, child.name);
105+
root.relativePath = child.relativePath;
106+
root.children = child.children;
107+
}
108+
}
109+
110+
return root;
111+
}
112+
13113
export function uniqueBy<T>(array: T[], accessor: (item: T) => any, predicate?: (item: T) => boolean): T[] {
14114
const uniqueValues = Object.create(null);
15115
return array.filter(_ => {

src/views/commitFileNode.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
33
import { Commands, DiffWithPreviousCommandArgs } from '../commands';
44
import { ExplorerNode, ResourceType } from './explorerNode';
5-
import { CommitFormatter, getGitStatusIcon, GitBranch, GitCommit, GitService, GitUri, ICommitFormatOptions, IGitStatusFile, StatusFileFormatter } from '../gitService';
5+
import { CommitFormatter, getGitStatusIcon, GitBranch, GitCommit, GitService, GitUri, ICommitFormatOptions, IGitStatusFile, IStatusFormatOptions, StatusFileFormatter } from '../gitService';
66
import * as path from 'path';
77

88
export enum CommitFileNodeDisplayAs {
@@ -17,6 +17,8 @@ export enum CommitFileNodeDisplayAs {
1717

1818
export class CommitFileNode extends ExplorerNode {
1919

20+
readonly priority: boolean = false;
21+
readonly repoPath: string;
2022
readonly resourceType: ResourceType = 'gitlens:commit-file';
2123

2224
constructor(
@@ -28,6 +30,7 @@ export class CommitFileNode extends ExplorerNode {
2830
public readonly branch?: GitBranch
2931
) {
3032
super(new GitUri(Uri.file(path.resolve(commit.repoPath, status.fileName)), { repoPath: commit.repoPath, fileName: status.fileName, sha: commit.sha }));
33+
this.repoPath = commit.repoPath;
3134
}
3235

3336
async getChildren(): Promise<ExplorerNode[]> {
@@ -36,7 +39,7 @@ export class CommitFileNode extends ExplorerNode {
3639

3740
async getTreeItem(): Promise<TreeItem> {
3841
if (this.commit.type !== 'file') {
39-
const log = await this.git.getLogForFile(this.commit.repoPath, this.status.fileName, this.commit.sha, { maxCount: 2 });
42+
const log = await this.git.getLogForFile(this.repoPath, this.status.fileName, this.commit.sha, { maxCount: 2 });
4043
if (log !== undefined) {
4144
this.commit = log.commits.get(this.commit.sha) || this.commit;
4245
}
@@ -62,6 +65,14 @@ export class CommitFileNode extends ExplorerNode {
6265
return item;
6366
}
6467

68+
private _folderName: string | undefined;
69+
get folderName() {
70+
if (this._folderName === undefined) {
71+
this._folderName = path.dirname(this.uri.getRelativePath());
72+
}
73+
return this._folderName;
74+
}
75+
6576
private _label: string | undefined;
6677
get label() {
6778
if (this._label === undefined) {
@@ -70,11 +81,22 @@ export class CommitFileNode extends ExplorerNode {
7081
truncateMessageAtNewLine: true,
7182
dataFormat: this.git.config.defaultDateFormat
7283
} as ICommitFormatOptions)
73-
: StatusFileFormatter.fromTemplate(this.getCommitFileTemplate(), this.status);
84+
: StatusFileFormatter.fromTemplate(this.getCommitFileTemplate(),
85+
this.status,
86+
{ relativePath: this.relativePath } as IStatusFormatOptions);
7487
}
7588
return this._label;
7689
}
7790

91+
private _relativePath: string | undefined;
92+
get relativePath(): string | undefined {
93+
return this._relativePath;
94+
}
95+
set relativePath(value: string | undefined) {
96+
this._relativePath = value;
97+
this._label = undefined;
98+
}
99+
78100
protected getCommitTemplate() {
79101
return this.git.config.gitExplorer.commitFormat;
80102
}

src/views/commitNode.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
'use strict';
2-
import { Iterables } from '../system';
2+
import { Arrays, Iterables } from '../system';
33
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
44
import { Commands, DiffWithPreviousCommandArgs } from '../commands';
55
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
6+
import { GitExplorerFilesLayout } from '../configuration';
7+
import { FolderNode, IFileExplorerNode } from './folderNode';
68
import { ExplorerNode, ResourceType } from './explorerNode';
79
import { CommitFormatter, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService';
10+
import * as path from 'path';
811

912
export class CommitNode extends ExplorerNode {
1013

14+
readonly repoPath: string;
1115
readonly resourceType: ResourceType = 'gitlens:commit';
1216

1317
constructor(
@@ -17,17 +21,32 @@ export class CommitNode extends ExplorerNode {
1721
public readonly branch?: GitBranch
1822
) {
1923
super(new GitUri(commit.uri, commit));
24+
this.repoPath = commit.repoPath;
2025
}
2126

2227
async getChildren(): Promise<ExplorerNode[]> {
23-
const log = await this.git.getLogForRepo(this.commit.repoPath, this.commit.sha, 1);
28+
const repoPath = this.repoPath;
29+
30+
const log = await this.git.getLogForRepo(repoPath, this.commit.sha, 1);
2431
if (log === undefined) return [];
2532

2633
const commit = Iterables.first(log.commits.values());
2734
if (commit === undefined) return [];
2835

29-
const children = [...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, CommitFileNodeDisplayAs.File, this.branch))];
30-
children.sort((a, b) => a.label!.localeCompare(b.label!));
36+
let children: IFileExplorerNode[] = [
37+
...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, CommitFileNodeDisplayAs.File, this.branch))
38+
];
39+
40+
if (this.git.config.gitExplorer.files.layout !== GitExplorerFilesLayout.List) {
41+
const hierarchy = Arrays.makeHierarchical(children, n => n.uri.getRelativePath().split('/'),
42+
(...paths: string[]) => GitService.normalizePath(path.join(...paths)), this.git.config.gitExplorer.files.compact);
43+
44+
const root = new FolderNode(repoPath, '', undefined, hierarchy, this.git.config.gitExplorer);
45+
children = await root.getChildren() as IFileExplorerNode[];
46+
}
47+
else {
48+
children.sort((a, b) => a.label!.localeCompare(b.label!));
49+
}
3150
return children;
3251
}
3352

src/views/explorerNode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export declare type ResourceType =
1010
'gitlens:commit' |
1111
'gitlens:commit-file' |
1212
'gitlens:file-history' |
13+
'gitlens:folder' |
1314
'gitlens:history' |
1415
'gitlens:message' |
1516
'gitlens:pager' |

0 commit comments

Comments
 (0)