Browse Source

Closes #258 - Adds tree-like layout to branches

Add folder icon
Implement branch folder
Implement branches.layout config option
Fix branch node ordering in Tree layout
Sort branch folder containing current branch correctly
- Add current getter method to BranchNode
- Implment a recursive findCurrent method on BranchFolderNode
Do not parse invalid branch name as branch folder
Expand current branch folder
main
Yukai Huang 7 years ago
committed by Eric Amodio
parent
commit
4103381af1
10 changed files with 175 additions and 5 deletions
  1. +1
    -0
      README.md
  2. +2
    -0
      images/dark/folder.svg
  3. +2
    -0
      images/light/folder.svg
  4. +11
    -0
      package.json
  5. +6
    -0
      src/git/models/branch.ts
  6. +17
    -0
      src/ui/config.ts
  7. +78
    -0
      src/views/branchFolderNode.ts
  8. +15
    -1
      src/views/branchNode.ts
  9. +20
    -2
      src/views/branchesNode.ts
  10. +23
    -2
      src/views/remoteNode.ts

+ 1
- 0
README.md View File

@ -491,6 +491,7 @@ See also [Explorer Settings](#explorer-settings "Jump to the Explorer settings")
|Name | Description
|-----|------------
|`gitlens.gitExplorer.autoRefresh`|Specifies whether or not to automatically refresh the **GitLens** view when the repository or the file system changes
|`gitlens.gitExplorer.branches.layout`| Specifies how the **Branches** view will display branches<br />`list` - display all branches<br />`tree` - organize branch as folder if branch name contains slashes "/"<br />`mix-tree` - display branch folders along with normal branch alphabetically
|`gitlens.gitExplorer.enabled`|Specifies whether or not to show the **GitLens** view"
|`gitlens.gitExplorer.files.compact`|Specifies whether or not to compact (flatten) unnecessary file nesting in the **GitLens** view<br />Only applies when displaying files as a `tree` or `auto`
|`gitlens.gitExplorer.files.layout`|Specifies how the **GitLens** 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

+ 2
- 0
images/dark/folder.svg View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{opacity:0;fill:#F6F6F6;} .icon-vs-fg{opacity:0;fill:#F0EFF1;} .icon-folder{fill:#C5C5C5;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 2.5v10c0 .827-.673 1.5-1.5 1.5h-11.996c-.827 0-1.5-.673-1.5-1.5v-8c0-.827.673-1.5 1.5-1.5h2.886l1-2h8.11c.827 0 1.5.673 1.5 1.5z" id="outline"/><path class="icon-folder" d="M14.5 2h-7.492l-1 2h-3.504c-.277 0-.5.224-.5.5v8c0 .276.223.5.5.5h11.996c.275 0 .5-.224.5-.5v-10c0-.276-.225-.5-.5-.5zm-.496 2h-6.496l.5-1h5.996v1z" id="iconBg"/><path class="icon-vs-fg" d="M14 3v1h-6.5l.5-1h6z" id="iconFg"/></svg>

+ 2
- 0
images/light/folder.svg View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{opacity:0;fill:#F6F6F6;} .icon-vs-fg{fill:#F0EFF1;} .icon-folder{fill:#656565;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 2.5v10c0 .827-.673 1.5-1.5 1.5h-11.996c-.827 0-1.5-.673-1.5-1.5v-8c0-.827.673-1.5 1.5-1.5h2.886l1-2h8.11c.827 0 1.5.673 1.5 1.5z" id="outline"/><path class="icon-folder" d="M14.5 2h-7.492l-1 2h-3.504c-.277 0-.5.224-.5.5v8c0 .276.223.5.5.5h11.996c.275 0 .5-.224.5-.5v-10c0-.276-.225-.5-.5-.5zm-.496 2h-6.496l.5-1h5.996v1z" id="iconBg"/><path class="icon-vs-fg" d="M14 3v1h-6.5l.5-1h6z" id="iconFg"/></svg>

+ 11
- 0
package.json View File

@ -397,6 +397,17 @@
"description": "Specifies whether or not to automatically refresh the `GitLens` explorer when the repository or the file system changes",
"scope": "window"
},
"gitlens.gitExplorer.branches.layout": {
"type": "string",
"default": "list",
"enum": [
"list",
"tree",
"mix-tree"
],
"description": "Specifies how the `Branches` view will display branches\n `list` - display all branches \n`tree` - organize branch as folder if branch name contains slashes \"\/\"\n `mix-tree` - display branch folders along with normal branch alphabetically",
"scope": "window"
},
"gitlens.gitExplorer.enabled": {
"type": "boolean",
"default": true,

+ 6
- 0
src/git/models/branch.ts View File

@ -10,6 +10,7 @@ export class GitBranch {
ahead: number;
behind: number;
};
basename: string;
constructor(
public readonly repoPath: string,
@ -29,6 +30,7 @@ export class GitBranch {
this.current = current;
this.name = branch;
this.basename = this.name.split('/').pop() || this.name;
this.tracking = tracking === '' || tracking == null ? undefined : tracking;
this.state = {
ahead: ahead,
@ -47,6 +49,10 @@ export class GitBranch {
return this._name;
}
getBasename(): string {
return this.basename;
}
getRemote(): string | undefined {
if (this.remote) return GitBranch.getRemote(this.name);
if (this.tracking !== undefined) return GitBranch.getRemote(this.tracking);

+ 17
- 0
src/ui/config.ts View File

@ -40,6 +40,12 @@ export enum ExplorerFilesLayout {
Tree = 'tree'
}
export enum ExplorerBranchesLayout {
List = 'list',
Tree = 'tree',
MixTree = 'mix-tree'
}
export enum FileAnnotationType {
Blame = 'blame',
Heatmap = 'heatmap',
@ -174,6 +180,17 @@ export interface ICodeLensConfig {
export interface IExplorersConfig {
avatars: boolean;
branches: {
layout: ExplorerBranchesLayout;
};
files: {
layout: ExplorerFilesLayout;
compact: boolean;
threshold: number;
};
commitFileFormat: string;
commitFormat: string;
// dateFormat: string | null;

+ 78
- 0
src/views/branchFolderNode.ts View File

@ -0,0 +1,78 @@
'use strict';
import { Arrays, Objects } from '../system';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { Container } from '../container';
import { Explorer, ExplorerNode, ResourceType } from './explorerNode';
import { BranchNode } from './branchNode';
import { ExplorerBranchesLayout } from '../configuration';
import { GitUri } from '../gitService';
export class BranchFolderNode extends ExplorerNode {
constructor(
public readonly repoPath: string,
public readonly branchFolderName: string,
public readonly relativePath: string | undefined,
public readonly root: Arrays.IHierarchicalItem<BranchNode>,
private readonly explorer: Explorer
) {
super(GitUri.fromRepoPath(repoPath));
}
async getChildren(): Promise<ExplorerNode[]> {
if (this.root.descendants === undefined || this.root.children === undefined) return [];
const children: (BranchFolderNode | BranchNode)[] = [];
for (const folder of Objects.values(this.root.children)) {
if (folder.value === undefined) {
children.push(new BranchFolderNode(this.repoPath, folder.name, folder.relativePath, folder, this.explorer));
continue;
}
children.push(folder.value);
}
if (this.explorer.config.branches.layout === ExplorerBranchesLayout.Tree) {
// sort strategy: current branch / current branch folder - normal branches - other folder branches (alphabetical order)
children.sort((a, b) => {
return (a.current ? -1 : 1) - (b.current ? -1 : 1) ||
((a instanceof BranchNode) ? -1 : 1) - ((b instanceof BranchNode) ? -1 : 1) ||
((a instanceof BranchNode && a.branch.current) ? -1 : 1) - ((b instanceof BranchNode && b.branch.current) ? -1 : 1) ||
a.label!.localeCompare(b.label!);
});
}
return children;
}
async getTreeItem(): Promise<TreeItem> {
const item = new TreeItem(this.label, this.current ? TreeItemCollapsibleState.Expanded : TreeItemCollapsibleState.Collapsed);
item.contextValue = ResourceType.Folder;
item.iconPath = {
dark: Container.context.asAbsolutePath('images/dark/folder.svg'),
light: Container.context.asAbsolutePath('images/light/folder.svg')
};
return item;
}
get label(): string {
return this.branchFolderName;
}
get current(): boolean {
return this.findCurrent(this.root);
}
// collect whether branch folder containes the current branch recursively
findCurrent(tree: Arrays.IHierarchicalItem<BranchNode>): boolean {
if (tree.value !== undefined) { // BranchNode
return tree.value.branch.current;
} else if (tree.children !== undefined) { // BranchFolderNode
return Object.keys(tree.children).reduce((bool, key) => {
return bool || this.findCurrent(tree.children![key]);
}, false);
} else {
return false;
}
}
}

+ 15
- 1
src/views/branchNode.ts View File

@ -7,6 +7,7 @@ import { Container } from '../container';
import { ExplorerNode, ExplorerRefNode, MessageNode, ResourceType, ShowAllNode } from './explorerNode';
import { GitExplorer } from './gitExplorer';
import { GitBranch, GitUri } from '../gitService';
import { ExplorerBranchesLayout } from '../configuration';
export class BranchNode extends ExplorerRefNode {
@ -24,6 +25,19 @@ export class BranchNode extends ExplorerRefNode {
return this.branch.name;
}
get label(): string {
const branchName = this.branch.getName();
if (this.explorer.config.branches.layout === ExplorerBranchesLayout.List) {
return branchName;
} else {
return !!branchName.match(/\s/) ? branchName : this.branch.getBasename();
}
}
get current(): boolean {
return this.branch.current;
}
async getChildren(): Promise<ExplorerNode[]> {
const log = await Container.git.getLog(this.uri.repoPath!, { maxCount: this.maxCount, ref: this.branch.name });
if (log === undefined) return [new MessageNode('No commits yet')];
@ -36,7 +50,7 @@ export class BranchNode extends ExplorerRefNode {
}
async getTreeItem(): Promise<TreeItem> {
let name = this.branch.getName();
let name = this.label;
if (!this.branch.remote && this.branch.tracking !== undefined && this.explorer.config.showTrackingBranch) {
name += ` ${GlyphChars.Space}${GlyphChars.ArrowLeftRight}${GlyphChars.Space} ${this.branch.tracking}`;
}

+ 20
- 2
src/views/branchesNode.ts View File

@ -1,11 +1,13 @@
'use strict';
import { Iterables } from '../system';
import { Arrays, Iterables } from '../system';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchNode } from './branchNode';
import { Container } from '../container';
import { ExplorerNode, ResourceType } from './explorerNode';
import { GitExplorer } from './gitExplorer';
import { GitUri, Repository } from '../gitService';
import { BranchFolderNode } from './branchFolderNode';
import { ExplorerBranchesLayout } from '../configuration';
export class BranchesNode extends ExplorerNode {
@ -23,7 +25,23 @@ export class BranchesNode extends ExplorerNode {
if (branches === undefined) return [];
branches.sort((a, b) => (a.current ? -1 : 1) - (b.current ? -1 : 1) || a.name.localeCompare(b.name));
return [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchNode(b, this.uri, this.explorer))];
let children = [];
// filter local branches
const branchNodes = [...Iterables.filterMap(branches, b => b.remote ? undefined : new BranchNode(b, this.uri, this.explorer))];
if (this.explorer.config.branches.layout === ExplorerBranchesLayout.List) {
return branchNodes;
}
const hierarchy = Arrays.makeHierarchical(branchNodes,
n => !!n.branch.name.match(/\s/) ? [n.branch.name] : n.branch.name.split('/'),
(...paths: string[]) => paths.join('/'), this.explorer.config.files.compact);
const root = new BranchFolderNode(this.repo.path, '', undefined, hierarchy, this.explorer);
children = await root.getChildren() as (BranchFolderNode | BranchNode)[];
return children;
}
async getTreeItem(): Promise<TreeItem> {

+ 23
- 2
src/views/remoteNode.ts View File

@ -1,11 +1,13 @@
'use strict';
import { Iterables } from '../system';
import { Arrays, Iterables } from '../system';
import { TreeItem, TreeItemCollapsibleState } from 'vscode';
import { BranchNode } from './branchNode';
import { GlyphChars } from '../constants';
import { ExplorerNode, ResourceType } from './explorerNode';
import { GitExplorer } from './gitExplorer';
import { GitRemote, GitRemoteType, GitUri, Repository } from '../gitService';
import { BranchFolderNode } from './branchFolderNode';
import { ExplorerBranchesLayout } from '../configuration';
export class RemoteNode extends ExplorerNode {
@ -23,7 +25,26 @@ export class RemoteNode extends ExplorerNode {
if (branches === undefined) return [];
branches.sort((a, b) => a.name.localeCompare(b.name));
return [...Iterables.filterMap(branches, b => !b.remote || !b.name.startsWith(this.remote.name) ? undefined : new BranchNode(b, this.uri, this.explorer))];
let children = [];
// filter remote branches
const branchNodes = [...Iterables.filterMap(branches, b => !b.remote || !b.name.startsWith(this.remote.name) ? undefined : new BranchNode(b, this.uri, this.explorer))];
if (this.explorer.config.branches.layout === ExplorerBranchesLayout.List) {
return branchNodes;
}
const hierarchy = Arrays.makeHierarchical(
branchNodes,
n => !!n.branch.name.match(/\s/) ? [n.branch.name] : n.branch.name.split('/').slice(1), // remove remote name
(...paths: string[]) => paths.join('/'),
this.explorer.config.files.compact
);
const root = new BranchFolderNode(this.repo.path, '', undefined, hierarchy, this.explorer);
children = await root.getChildren() as (BranchFolderNode | BranchNode)[];
return children;
}
getTreeItem(): TreeItem {

Loading…
Cancel
Save