mirror of
https://github.com/ckaczor/vscode-gitlens.git
synced 2026-01-13 17:23:11 -05:00
Adds new file layouts to the custom view
This commit is contained in:
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
|
- Adds new file layouts to the `GitLens` custom view
|
||||||
|
- `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
|
||||||
|
- `list` - displays files as a list
|
||||||
|
- `tree` - displays files as a tree
|
||||||
|
- Adds `gitlens.gitExplorer.files.layout` setting to specify how the `GitLens` custom view will display files
|
||||||
|
- Adds `gitlens.gitExplorer.files.compact` setting to specify whether or not to compact (flatten) unnecessary file nesting in the `GitLens` custom view
|
||||||
|
- 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
|
||||||
- Adds `${directory}` token to the file formatting settings
|
- Adds `${directory}` token to the file formatting settings
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -356,6 +356,9 @@ GitLens is highly customizable and provides many configuration settings to allow
|
|||||||
|-----|------------
|
|-----|------------
|
||||||
|`gitlens.gitExplorer.enabled`|Specifies whether or not to show the `GitLens` custom view"
|
|`gitlens.gitExplorer.enabled`|Specifies whether or not to show the `GitLens` custom view"
|
||||||
|`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"
|
|`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"
|
||||||
|
|`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
|
||||||
|
|`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`
|
||||||
|
|`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`
|
||||||
|`gitlens.gitExplorer.includeWorkingTree`|Specifies whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view
|
|`gitlens.gitExplorer.includeWorkingTree`|Specifies whether or not to include working tree files inside the `Repository Status` node of the `GitLens` custom view
|
||||||
|`gitlens.gitExplorer.showTrackingBranch`|Specifies whether or not to show the tracking branch when displaying local branches in the `GitLens` custom view"
|
|`gitlens.gitExplorer.showTrackingBranch`|Specifies whether or not to show the tracking branch when displaying local branches in the `GitLens` custom view"
|
||||||
|`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
|
|`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
|
||||||
|
|||||||
20
package.json
20
package.json
@@ -428,6 +428,26 @@
|
|||||||
"default": true,
|
"default": true,
|
||||||
"description": "Specifies whether or not to show the `GitLens` custom view"
|
"description": "Specifies whether or not to show the `GitLens` custom view"
|
||||||
},
|
},
|
||||||
|
"gitlens.gitExplorer.files.layout": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "auto",
|
||||||
|
"enum": [
|
||||||
|
"auto",
|
||||||
|
"list",
|
||||||
|
"tree"
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"gitlens.gitExplorer.files.compact": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"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`"
|
||||||
|
},
|
||||||
|
"gitlens.gitExplorer.files.threshold": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 5,
|
||||||
|
"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`"
|
||||||
|
},
|
||||||
"gitlens.gitExplorer.includeWorkingTree": {
|
"gitlens.gitExplorer.includeWorkingTree": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"default": true,
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ export const CustomRemoteType = {
|
|||||||
GitLab: 'GitLab' as CustomRemoteType
|
GitLab: 'GitLab' as CustomRemoteType
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GitExplorerFilesLayout =
|
||||||
|
'auto' |
|
||||||
|
'list' |
|
||||||
|
'tree';
|
||||||
|
export const GitExplorerFilesLayout = {
|
||||||
|
Auto: 'auto' as GitExplorerFilesLayout,
|
||||||
|
List: 'list' as GitExplorerFilesLayout,
|
||||||
|
Tree: 'tree' as GitExplorerFilesLayout
|
||||||
|
};
|
||||||
|
|
||||||
export type StatusBarCommand =
|
export type StatusBarCommand =
|
||||||
'gitlens.toggleFileBlame' |
|
'gitlens.toggleFileBlame' |
|
||||||
'gitlens.showBlameHistory' |
|
'gitlens.showBlameHistory' |
|
||||||
@@ -132,6 +142,24 @@ export interface ICodeLensLanguageLocation {
|
|||||||
customSymbols?: string[];
|
customSymbols?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IGitExplorerConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
view: GitExplorerView;
|
||||||
|
files: {
|
||||||
|
layout: GitExplorerFilesLayout;
|
||||||
|
compact: boolean;
|
||||||
|
threshold: number;
|
||||||
|
};
|
||||||
|
includeWorkingTree: boolean;
|
||||||
|
showTrackingBranch: boolean;
|
||||||
|
commitFormat: string;
|
||||||
|
commitFileFormat: string;
|
||||||
|
stashFormat: string;
|
||||||
|
stashFileFormat: string;
|
||||||
|
statusFileFormat: string;
|
||||||
|
// dateFormat: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IRemotesConfig {
|
export interface IRemotesConfig {
|
||||||
type: CustomRemoteType;
|
type: CustomRemoteType;
|
||||||
domain: string;
|
domain: string;
|
||||||
@@ -316,18 +344,7 @@ export interface IConfig {
|
|||||||
|
|
||||||
defaultDateFormat: string | null;
|
defaultDateFormat: string | null;
|
||||||
|
|
||||||
gitExplorer: {
|
gitExplorer: IGitExplorerConfig;
|
||||||
enabled: boolean;
|
|
||||||
view: GitExplorerView;
|
|
||||||
includeWorkingTree: boolean;
|
|
||||||
showTrackingBranch: boolean;
|
|
||||||
commitFormat: string;
|
|
||||||
commitFileFormat: string;
|
|
||||||
stashFormat: string;
|
|
||||||
stashFileFormat: string;
|
|
||||||
statusFileFormat: string;
|
|
||||||
// dateFormat: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
remotes: IRemotesConfig[];
|
remotes: IRemotesConfig[];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
import { Objects } from './object';
|
||||||
|
|
||||||
export namespace Arrays {
|
export namespace Arrays {
|
||||||
|
export function countUniques<T>(array: T[], accessor: (item: T) => string): { [key: string]: number } {
|
||||||
|
const uniqueCounts = Object.create(null);
|
||||||
|
for (const item of array) {
|
||||||
|
const value = accessor(item);
|
||||||
|
uniqueCounts[value] = (uniqueCounts[value] || 0) + 1;
|
||||||
|
}
|
||||||
|
return uniqueCounts;
|
||||||
|
}
|
||||||
|
|
||||||
export function groupBy<T>(array: T[], accessor: (item: T) => string): { [key: string]: T[] } {
|
export function groupBy<T>(array: T[], accessor: (item: T) => string): { [key: string]: T[] } {
|
||||||
return array.reduce((previous, current) => {
|
return array.reduce((previous, current) => {
|
||||||
const value = accessor(current);
|
const value = accessor(current);
|
||||||
@@ -10,6 +20,96 @@ export namespace Arrays {
|
|||||||
}, Object.create(null));
|
}, Object.create(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IHierarchicalItem<T> {
|
||||||
|
name: string;
|
||||||
|
relativePath: string;
|
||||||
|
value?: T;
|
||||||
|
|
||||||
|
// parent?: IHierarchicalItem<T>;
|
||||||
|
children: { [key: string]: IHierarchicalItem<T> } | undefined;
|
||||||
|
descendants: T[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeHierarchical<T>(values: T[], splitPath: (i: T) => string[], joinPath: (...paths: string[]) => string, compact: boolean = false): IHierarchicalItem<T> {
|
||||||
|
const seed = {
|
||||||
|
name: '',
|
||||||
|
relativePath: '',
|
||||||
|
children: Object.create(null),
|
||||||
|
descendants: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const hierarchy = values.reduce((root: IHierarchicalItem<T>, value) => {
|
||||||
|
let folder = root;
|
||||||
|
|
||||||
|
let relativePath = '';
|
||||||
|
for (const folderName of splitPath(value)) {
|
||||||
|
relativePath = joinPath(relativePath, folderName);
|
||||||
|
|
||||||
|
if (folder.children === undefined) {
|
||||||
|
folder.children = Object.create(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
let f = folder.children![folderName];
|
||||||
|
if (f === undefined) {
|
||||||
|
folder.children![folderName] = f = {
|
||||||
|
name: folderName,
|
||||||
|
relativePath: relativePath,
|
||||||
|
// parent: folder,
|
||||||
|
children: undefined,
|
||||||
|
descendants: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (folder.descendants === undefined) {
|
||||||
|
folder.descendants = [];
|
||||||
|
}
|
||||||
|
folder.descendants.push(value);
|
||||||
|
folder = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
folder.value = value;
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}, seed);
|
||||||
|
|
||||||
|
if (compact) return compactHierarchy(hierarchy, joinPath, true);
|
||||||
|
return hierarchy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compactHierarchy<T>(root: IHierarchicalItem<T>, joinPath: (...paths: string[]) => string, isRoot: boolean = true): IHierarchicalItem<T> {
|
||||||
|
if (root.children === undefined) return root;
|
||||||
|
|
||||||
|
const children = [...Objects.values(root.children)];
|
||||||
|
|
||||||
|
// // Attempts less nesting but duplicate roots
|
||||||
|
// if (!isRoot && children.every(c => c.value === undefined)) {
|
||||||
|
// const parentSiblings = root.parent!.children!;
|
||||||
|
// if (parentSiblings[root.name] !== undefined) {
|
||||||
|
// delete parentSiblings[root.name];
|
||||||
|
|
||||||
|
// for (const child of children) {
|
||||||
|
// child.name = joinPath(root.name, child.name);
|
||||||
|
// parentSiblings[child.name] = child;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
for (const child of children) {
|
||||||
|
compactHierarchy(child, joinPath, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRoot && children.length === 1) {
|
||||||
|
const child = children[0];
|
||||||
|
if (child.value === undefined) {
|
||||||
|
root.name = joinPath(root.name, child.name);
|
||||||
|
root.relativePath = child.relativePath;
|
||||||
|
root.children = child.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
export function uniqueBy<T>(array: T[], accessor: (item: T) => any, predicate?: (item: T) => boolean): T[] {
|
export function uniqueBy<T>(array: T[], accessor: (item: T) => any, predicate?: (item: T) => boolean): T[] {
|
||||||
const uniqueValues = Object.create(null);
|
const uniqueValues = Object.create(null);
|
||||||
return array.filter(_ => {
|
return array.filter(_ => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
|
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
|
||||||
import { Commands, DiffWithPreviousCommandArgs } from '../commands';
|
import { Commands, DiffWithPreviousCommandArgs } from '../commands';
|
||||||
import { ExplorerNode, ResourceType } from './explorerNode';
|
import { ExplorerNode, ResourceType } from './explorerNode';
|
||||||
import { CommitFormatter, getGitStatusIcon, GitBranch, GitCommit, GitService, GitUri, ICommitFormatOptions, IGitStatusFile, StatusFileFormatter } from '../gitService';
|
import { CommitFormatter, getGitStatusIcon, GitBranch, GitCommit, GitService, GitUri, ICommitFormatOptions, IGitStatusFile, IStatusFormatOptions, StatusFileFormatter } from '../gitService';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
export enum CommitFileNodeDisplayAs {
|
export enum CommitFileNodeDisplayAs {
|
||||||
@@ -17,6 +17,8 @@ export enum CommitFileNodeDisplayAs {
|
|||||||
|
|
||||||
export class CommitFileNode extends ExplorerNode {
|
export class CommitFileNode extends ExplorerNode {
|
||||||
|
|
||||||
|
readonly priority: boolean = false;
|
||||||
|
readonly repoPath: string;
|
||||||
readonly resourceType: ResourceType = 'gitlens:commit-file';
|
readonly resourceType: ResourceType = 'gitlens:commit-file';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -28,6 +30,7 @@ export class CommitFileNode extends ExplorerNode {
|
|||||||
public readonly branch?: GitBranch
|
public readonly branch?: GitBranch
|
||||||
) {
|
) {
|
||||||
super(new GitUri(Uri.file(path.resolve(commit.repoPath, status.fileName)), { repoPath: commit.repoPath, fileName: status.fileName, sha: commit.sha }));
|
super(new GitUri(Uri.file(path.resolve(commit.repoPath, status.fileName)), { repoPath: commit.repoPath, fileName: status.fileName, sha: commit.sha }));
|
||||||
|
this.repoPath = commit.repoPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChildren(): Promise<ExplorerNode[]> {
|
async getChildren(): Promise<ExplorerNode[]> {
|
||||||
@@ -36,7 +39,7 @@ export class CommitFileNode extends ExplorerNode {
|
|||||||
|
|
||||||
async getTreeItem(): Promise<TreeItem> {
|
async getTreeItem(): Promise<TreeItem> {
|
||||||
if (this.commit.type !== 'file') {
|
if (this.commit.type !== 'file') {
|
||||||
const log = await this.git.getLogForFile(this.commit.repoPath, this.status.fileName, this.commit.sha, { maxCount: 2 });
|
const log = await this.git.getLogForFile(this.repoPath, this.status.fileName, this.commit.sha, { maxCount: 2 });
|
||||||
if (log !== undefined) {
|
if (log !== undefined) {
|
||||||
this.commit = log.commits.get(this.commit.sha) || this.commit;
|
this.commit = log.commits.get(this.commit.sha) || this.commit;
|
||||||
}
|
}
|
||||||
@@ -62,6 +65,14 @@ export class CommitFileNode extends ExplorerNode {
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _folderName: string | undefined;
|
||||||
|
get folderName() {
|
||||||
|
if (this._folderName === undefined) {
|
||||||
|
this._folderName = path.dirname(this.uri.getRelativePath());
|
||||||
|
}
|
||||||
|
return this._folderName;
|
||||||
|
}
|
||||||
|
|
||||||
private _label: string | undefined;
|
private _label: string | undefined;
|
||||||
get label() {
|
get label() {
|
||||||
if (this._label === undefined) {
|
if (this._label === undefined) {
|
||||||
@@ -70,11 +81,22 @@ export class CommitFileNode extends ExplorerNode {
|
|||||||
truncateMessageAtNewLine: true,
|
truncateMessageAtNewLine: true,
|
||||||
dataFormat: this.git.config.defaultDateFormat
|
dataFormat: this.git.config.defaultDateFormat
|
||||||
} as ICommitFormatOptions)
|
} as ICommitFormatOptions)
|
||||||
: StatusFileFormatter.fromTemplate(this.getCommitFileTemplate(), this.status);
|
: StatusFileFormatter.fromTemplate(this.getCommitFileTemplate(),
|
||||||
|
this.status,
|
||||||
|
{ relativePath: this.relativePath } as IStatusFormatOptions);
|
||||||
}
|
}
|
||||||
return this._label;
|
return this._label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _relativePath: string | undefined;
|
||||||
|
get relativePath(): string | undefined {
|
||||||
|
return this._relativePath;
|
||||||
|
}
|
||||||
|
set relativePath(value: string | undefined) {
|
||||||
|
this._relativePath = value;
|
||||||
|
this._label = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
protected getCommitTemplate() {
|
protected getCommitTemplate() {
|
||||||
return this.git.config.gitExplorer.commitFormat;
|
return this.git.config.gitExplorer.commitFormat;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import { Iterables } from '../system';
|
import { Arrays, Iterables } from '../system';
|
||||||
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
|
import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode';
|
||||||
import { Commands, DiffWithPreviousCommandArgs } from '../commands';
|
import { Commands, DiffWithPreviousCommandArgs } from '../commands';
|
||||||
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
|
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
|
||||||
|
import { GitExplorerFilesLayout } from '../configuration';
|
||||||
|
import { FolderNode, IFileExplorerNode } from './folderNode';
|
||||||
import { ExplorerNode, ResourceType } from './explorerNode';
|
import { ExplorerNode, ResourceType } from './explorerNode';
|
||||||
import { CommitFormatter, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService';
|
import { CommitFormatter, GitBranch, GitLogCommit, GitService, GitUri, ICommitFormatOptions } from '../gitService';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
export class CommitNode extends ExplorerNode {
|
export class CommitNode extends ExplorerNode {
|
||||||
|
|
||||||
|
readonly repoPath: string;
|
||||||
readonly resourceType: ResourceType = 'gitlens:commit';
|
readonly resourceType: ResourceType = 'gitlens:commit';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -17,17 +21,32 @@ export class CommitNode extends ExplorerNode {
|
|||||||
public readonly branch?: GitBranch
|
public readonly branch?: GitBranch
|
||||||
) {
|
) {
|
||||||
super(new GitUri(commit.uri, commit));
|
super(new GitUri(commit.uri, commit));
|
||||||
|
this.repoPath = commit.repoPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChildren(): Promise<ExplorerNode[]> {
|
async getChildren(): Promise<ExplorerNode[]> {
|
||||||
const log = await this.git.getLogForRepo(this.commit.repoPath, this.commit.sha, 1);
|
const repoPath = this.repoPath;
|
||||||
|
|
||||||
|
const log = await this.git.getLogForRepo(repoPath, this.commit.sha, 1);
|
||||||
if (log === undefined) return [];
|
if (log === undefined) return [];
|
||||||
|
|
||||||
const commit = Iterables.first(log.commits.values());
|
const commit = Iterables.first(log.commits.values());
|
||||||
if (commit === undefined) return [];
|
if (commit === undefined) return [];
|
||||||
|
|
||||||
const children = [...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, CommitFileNodeDisplayAs.File, this.branch))];
|
let children: IFileExplorerNode[] = [
|
||||||
children.sort((a, b) => a.label!.localeCompare(b.label!));
|
...Iterables.map(commit.fileStatuses, s => new CommitFileNode(s, commit, this.context, this.git, CommitFileNodeDisplayAs.File, this.branch))
|
||||||
|
];
|
||||||
|
|
||||||
|
if (this.git.config.gitExplorer.files.layout !== GitExplorerFilesLayout.List) {
|
||||||
|
const hierarchy = Arrays.makeHierarchical(children, n => n.uri.getRelativePath().split('/'),
|
||||||
|
(...paths: string[]) => GitService.normalizePath(path.join(...paths)), this.git.config.gitExplorer.files.compact);
|
||||||
|
|
||||||
|
const root = new FolderNode(repoPath, '', undefined, hierarchy, this.git.config.gitExplorer);
|
||||||
|
children = await root.getChildren() as IFileExplorerNode[];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
children.sort((a, b) => a.label!.localeCompare(b.label!));
|
||||||
|
}
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export declare type ResourceType =
|
|||||||
'gitlens:commit' |
|
'gitlens:commit' |
|
||||||
'gitlens:commit-file' |
|
'gitlens:commit-file' |
|
||||||
'gitlens:file-history' |
|
'gitlens:file-history' |
|
||||||
|
'gitlens:folder' |
|
||||||
'gitlens:history' |
|
'gitlens:history' |
|
||||||
'gitlens:message' |
|
'gitlens:message' |
|
||||||
'gitlens:pager' |
|
'gitlens:pager' |
|
||||||
|
|||||||
85
src/views/folderNode.ts
Normal file
85
src/views/folderNode.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
'use strict';
|
||||||
|
import { Arrays, Objects } from '../system';
|
||||||
|
import { TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
|
||||||
|
import { GitExplorerFilesLayout, IGitExplorerConfig } from '../configuration';
|
||||||
|
import { ExplorerNode, ResourceType } from './explorerNode';
|
||||||
|
import { GitUri } from '../gitService';
|
||||||
|
|
||||||
|
export interface IFileExplorerNode extends ExplorerNode {
|
||||||
|
folderName: string;
|
||||||
|
label?: string;
|
||||||
|
priority: boolean;
|
||||||
|
relativePath?: string;
|
||||||
|
root?: Arrays.IHierarchicalItem<IFileExplorerNode>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FolderNode extends ExplorerNode {
|
||||||
|
|
||||||
|
readonly priority: boolean = true;
|
||||||
|
readonly resourceType: ResourceType = 'gitlens:folder';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly repoPath: string,
|
||||||
|
public folderName: string,
|
||||||
|
public relativePath: string | undefined,
|
||||||
|
public readonly root: Arrays.IHierarchicalItem<IFileExplorerNode>,
|
||||||
|
private readonly config: IGitExplorerConfig
|
||||||
|
) {
|
||||||
|
super(new GitUri(Uri.file(repoPath), { repoPath: repoPath, fileName: repoPath }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChildren(): Promise<(FolderNode | IFileExplorerNode)[]> {
|
||||||
|
if (this.root.descendants === undefined || this.root.children === undefined) return [];
|
||||||
|
|
||||||
|
let children: (FolderNode | IFileExplorerNode)[];
|
||||||
|
|
||||||
|
const nesting = FolderNode.getFileNesting(this.config, this.root.descendants, this.relativePath === undefined);
|
||||||
|
if (nesting !== GitExplorerFilesLayout.List) {
|
||||||
|
children = [];
|
||||||
|
for (const folder of Objects.values(this.root.children)) {
|
||||||
|
if (folder.value === undefined) {
|
||||||
|
children.push(new FolderNode(this.repoPath, folder.name, folder.relativePath, folder, this.config));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
folder.value.relativePath = this.root.relativePath;
|
||||||
|
children.push(folder.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.root.descendants.forEach(n => n.relativePath = this.root.relativePath);
|
||||||
|
children = this.root.descendants;
|
||||||
|
}
|
||||||
|
|
||||||
|
children.sort((a, b) => {
|
||||||
|
return ((a instanceof FolderNode) ? -1 : 1) - ((b instanceof FolderNode) ? -1 : 1) ||
|
||||||
|
(a.priority ? -1 : 1) - (b.priority ? -1 : 1) ||
|
||||||
|
a.label!.localeCompare(b.label!);
|
||||||
|
});
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTreeItem(): Promise<TreeItem> {
|
||||||
|
// TODO: Change this to expanded once https://github.com/Microsoft/vscode/issues/30918 is fixed
|
||||||
|
const item = new TreeItem(this.label, TreeItemCollapsibleState.Collapsed);
|
||||||
|
item.contextValue = this.resourceType;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
get label(): string {
|
||||||
|
return this.folderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getFileNesting<T extends IFileExplorerNode>(config: IGitExplorerConfig, children: T[], isRoot: boolean): GitExplorerFilesLayout {
|
||||||
|
const nesting = config.files.layout || GitExplorerFilesLayout.Auto;
|
||||||
|
if (nesting === GitExplorerFilesLayout.Auto) {
|
||||||
|
if (isRoot || config.files.compact) {
|
||||||
|
const nestingThreshold = config.files.threshold || 5;
|
||||||
|
if (children.length <= nestingThreshold) return GitExplorerFilesLayout.List;
|
||||||
|
}
|
||||||
|
return GitExplorerFilesLayout.Tree;
|
||||||
|
}
|
||||||
|
return nesting;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { Command, ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } fr
|
|||||||
import { Commands, DiffWithPreviousCommandArgs } from '../commands';
|
import { Commands, DiffWithPreviousCommandArgs } from '../commands';
|
||||||
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
|
import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode';
|
||||||
import { ExplorerNode, ResourceType } from './explorerNode';
|
import { ExplorerNode, ResourceType } from './explorerNode';
|
||||||
import { getGitStatusIcon, GitBranch, GitLogCommit, GitService, GitUri, IGitStatusFile, IGitStatusFileWithCommit, StatusFileFormatter } from '../gitService';
|
import { getGitStatusIcon, GitBranch, GitLogCommit, GitService, GitUri, IGitStatusFile, IGitStatusFileWithCommit, IStatusFormatOptions, StatusFileFormatter } from '../gitService';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
export class StatusFileCommitsNode extends ExplorerNode {
|
export class StatusFileCommitsNode extends ExplorerNode {
|
||||||
@@ -11,7 +11,7 @@ export class StatusFileCommitsNode extends ExplorerNode {
|
|||||||
readonly resourceType: ResourceType = 'gitlens:status-file-commits';
|
readonly resourceType: ResourceType = 'gitlens:status-file-commits';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
repoPath: string,
|
public readonly repoPath: string,
|
||||||
public readonly status: IGitStatusFile,
|
public readonly status: IGitStatusFile,
|
||||||
public commits: GitLogCommit[],
|
public commits: GitLogCommit[],
|
||||||
protected readonly context: ExtensionContext,
|
protected readonly context: ExtensionContext,
|
||||||
@@ -47,10 +47,20 @@ export class StatusFileCommitsNode extends ExplorerNode {
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _folderName: string | undefined;
|
||||||
|
get folderName() {
|
||||||
|
if (this._folderName === undefined) {
|
||||||
|
this._folderName = path.dirname(this.uri.getRelativePath());
|
||||||
|
}
|
||||||
|
return this._folderName;
|
||||||
|
}
|
||||||
|
|
||||||
private _label: string | undefined;
|
private _label: string | undefined;
|
||||||
get label() {
|
get label() {
|
||||||
if (this._label === undefined) {
|
if (this._label === undefined) {
|
||||||
this._label = StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.statusFileFormat, { ...this.status, commit: this.commit } as IGitStatusFileWithCommit);
|
this._label = StatusFileFormatter.fromTemplate(this.git.config.gitExplorer.statusFileFormat,
|
||||||
|
{ ...this.status, commit: this.commit } as IGitStatusFileWithCommit,
|
||||||
|
{ relativePath: this.relativePath } as IStatusFormatOptions);
|
||||||
}
|
}
|
||||||
return this._label;
|
return this._label;
|
||||||
}
|
}
|
||||||
@@ -59,12 +69,25 @@ export class StatusFileCommitsNode extends ExplorerNode {
|
|||||||
return this.commits[0];
|
return this.commits[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get priority(): boolean {
|
||||||
|
return this.commit.isUncommitted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _relativePath: string | undefined;
|
||||||
|
get relativePath(): string | undefined {
|
||||||
|
return this._relativePath;
|
||||||
|
}
|
||||||
|
set relativePath(value: string | undefined) {
|
||||||
|
this._relativePath = value;
|
||||||
|
this._label = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
getCommand(): Command | undefined {
|
getCommand(): Command | undefined {
|
||||||
return {
|
return {
|
||||||
title: 'Compare File with Previous Revision',
|
title: 'Compare File with Previous Revision',
|
||||||
command: Commands.DiffWithPrevious,
|
command: Commands.DiffWithPrevious,
|
||||||
arguments: [
|
arguments: [
|
||||||
GitUri.fromFileStatus(this.status, this.uri.repoPath!),
|
GitUri.fromFileStatus(this.status, this.repoPath),
|
||||||
{
|
{
|
||||||
commit: this.commit,
|
commit: this.commit,
|
||||||
line: 0,
|
line: 0,
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import { Arrays, Iterables, Objects } from '../system';
|
import { Arrays, Iterables, Objects } from '../system';
|
||||||
import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
|
import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode';
|
||||||
|
import { GitExplorerFilesLayout } from '../configuration';
|
||||||
import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode';
|
import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode';
|
||||||
|
import { FolderNode, IFileExplorerNode } from './folderNode';
|
||||||
import { GitBranch, GitLog, GitLogCommit, GitService, GitStatus, GitUri, IGitStatusFileWithCommit } from '../gitService';
|
import { GitBranch, GitLog, GitLogCommit, GitService, GitStatus, GitUri, IGitStatusFileWithCommit } from '../gitService';
|
||||||
import { StatusFileCommitsNode } from './statusFileCommitsNode';
|
import { StatusFileCommitsNode } from './statusFileCommitsNode';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
export class StatusFilesNode extends ExplorerNode {
|
export class StatusFilesNode extends ExplorerNode {
|
||||||
|
|
||||||
|
readonly repoPath: string;
|
||||||
readonly resourceType: ResourceType = 'gitlens:status-files';
|
readonly resourceType: ResourceType = 'gitlens:status-files';
|
||||||
|
|
||||||
maxCount: number | undefined = undefined;
|
maxCount: number | undefined = undefined;
|
||||||
@@ -19,14 +23,17 @@ export class StatusFilesNode extends ExplorerNode {
|
|||||||
public readonly branch?: GitBranch
|
public readonly branch?: GitBranch
|
||||||
) {
|
) {
|
||||||
super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath }));
|
super(new GitUri(Uri.file(status.repoPath), { repoPath: status.repoPath, fileName: status.repoPath }));
|
||||||
|
this.repoPath = status.repoPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChildren(): Promise<ExplorerNode[]> {
|
async getChildren(): Promise<ExplorerNode[]> {
|
||||||
let statuses: IGitStatusFileWithCommit[] = [];
|
let statuses: IGitStatusFileWithCommit[] = [];
|
||||||
|
|
||||||
|
const repoPath = this.repoPath;
|
||||||
|
|
||||||
let log: GitLog | undefined;
|
let log: GitLog | undefined;
|
||||||
if (this.range !== undefined) {
|
if (this.range !== undefined) {
|
||||||
log = await this.git.getLogForRepo(this.status.repoPath, this.range, this.maxCount);
|
log = await this.git.getLogForRepo(repoPath, this.range, this.maxCount);
|
||||||
if (log !== undefined) {
|
if (log !== undefined) {
|
||||||
statuses = Array.from(Iterables.flatMap(log.commits.values(), c => {
|
statuses = Array.from(Iterables.flatMap(log.commits.values(), c => {
|
||||||
return c.fileStatuses.map(s => {
|
return c.fileStatuses.map(s => {
|
||||||
@@ -38,22 +45,33 @@ export class StatusFilesNode extends ExplorerNode {
|
|||||||
|
|
||||||
if (this.status.files.length !== 0 && this.includeWorkingTree) {
|
if (this.status.files.length !== 0 && this.includeWorkingTree) {
|
||||||
statuses.splice(0, 0, ...this.status.files.map(s => {
|
statuses.splice(0, 0, ...this.status.files.map(s => {
|
||||||
return { ...s, commit: new GitLogCommit('file', this.status.repoPath, GitService.uncommittedSha, s.fileName, 'You', new Date(), '', s.status, [s], s.originalFileName, 'HEAD', s.fileName) } as IGitStatusFileWithCommit;
|
return {
|
||||||
|
...s,
|
||||||
|
commit: new GitLogCommit('file', repoPath, GitService.uncommittedSha, s.fileName, 'You', new Date(), '', s.status, [s], s.originalFileName, 'HEAD', s.fileName)
|
||||||
|
} as IGitStatusFileWithCommit;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
statuses.sort((a, b) => b.commit.date.getTime() - a.commit.date.getTime());
|
statuses.sort((a, b) => b.commit.date.getTime() - a.commit.date.getTime());
|
||||||
|
|
||||||
const groups = Arrays.groupBy(statuses, s => s.fileName);
|
const groups = Arrays.groupBy(statuses, s => s.fileName);
|
||||||
|
|
||||||
const children: (StatusFileCommitsNode | ShowAllNode)[] = [
|
let children: IFileExplorerNode[] = [
|
||||||
...Iterables.map(Objects.values<IGitStatusFileWithCommit[]>(groups),
|
...Iterables.map(Objects.values(groups), statuses => new StatusFileCommitsNode(repoPath, statuses[statuses.length - 1], statuses.map(s => s.commit), this.context, this.git, this.branch))
|
||||||
statuses => new StatusFileCommitsNode(this.uri.repoPath!, statuses[statuses.length - 1], statuses.map(s => s.commit), this.context, this.git, this.branch))
|
|
||||||
];
|
];
|
||||||
|
|
||||||
children.sort((a: StatusFileCommitsNode, b: StatusFileCommitsNode) => (a.commit.isUncommitted ? -1 : 1) - (b.commit.isUncommitted ? -1 : 1) || a.label!.localeCompare(b.label!));
|
if (this.git.config.gitExplorer.files.layout !== GitExplorerFilesLayout.List) {
|
||||||
|
const hierarchy = Arrays.makeHierarchical(children, n => n.uri.getRelativePath().split('/'),
|
||||||
|
(...paths: string[]) => GitService.normalizePath(path.join(...paths)), this.git.config.gitExplorer.files.compact);
|
||||||
|
|
||||||
|
const root = new FolderNode(repoPath, '', undefined, hierarchy, this.git.config.gitExplorer);
|
||||||
|
children = await root.getChildren() as IFileExplorerNode[];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
children.sort((a, b) => (a.priority ? -1 : 1) - (b.priority ? -1 : 1) || a.label!.localeCompare(b.label!));
|
||||||
|
}
|
||||||
|
|
||||||
if (log !== undefined && log.truncated) {
|
if (log !== undefined && log.truncated) {
|
||||||
children.push(new ShowAllNode('Show All Changes', this, this.context));
|
(children as (IFileExplorerNode | ShowAllNode)[]).push(new ShowAllNode('Show All Changes', this, this.context));
|
||||||
}
|
}
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
@@ -62,7 +80,7 @@ export class StatusFilesNode extends ExplorerNode {
|
|||||||
let files = (this.status.files !== undefined && this.includeWorkingTree) ? this.status.files.length : 0;
|
let files = (this.status.files !== undefined && this.includeWorkingTree) ? this.status.files.length : 0;
|
||||||
|
|
||||||
if (this.status.upstream !== undefined) {
|
if (this.status.upstream !== undefined) {
|
||||||
const stats = await this.git.getChangedFilesCount(this.status.repoPath, `${this.status.upstream}...`);
|
const stats = await this.git.getChangedFilesCount(this.repoPath, `${this.status.upstream}...`);
|
||||||
if (stats !== undefined) {
|
if (stats !== undefined) {
|
||||||
files += stats.files;
|
files += stats.files;
|
||||||
}
|
}
|
||||||
@@ -82,5 +100,4 @@ export class StatusFilesNode extends ExplorerNode {
|
|||||||
private get includeWorkingTree(): boolean {
|
private get includeWorkingTree(): boolean {
|
||||||
return this.git.config.gitExplorer.includeWorkingTree;
|
return this.git.config.gitExplorer.includeWorkingTree;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user