diff --git a/package.json b/package.json index be8256a..3f6636e 100644 --- a/package.json +++ b/package.json @@ -2807,6 +2807,69 @@ "light": "#b15e35", "highContrast": "#ff874c" } + }, + { + "id": "gitlens.decorations.copiedForegroundColor", + "description": "Specifies the decoration foreground color of copied files", + "defaults": { + "light": "#007100", + "dark": "#73C991", + "highContrast": "#73C991" + } + }, + { + "id": "gitlens.decorations.renamedForegroundColor", + "description": "Specifies the decoration foreground color of renamed files", + "defaults": { + "light": "#007100", + "dark": "#73C991", + "highContrast": "#73C991" + } + }, + { + "id": "gitlens.decorations.branchAheadForegroundColor", + "description": "Specifies the decoration foreground color of branches that are ahead of their upstream", + "defaults": { + "dark": "#35b15e", + "light": "#35b15e", + "highContrast": "#4dff88" + } + }, + { + "id": "gitlens.decorations.branchBehindForegroundColor", + "description": "Specifies the decoration foreground color of branches that are behind their upstream", + "defaults": { + "dark": "#b15e35", + "light": "#b15e35", + "highContrast": "#ff874c" + } + }, + { + "id": "gitlens.decorations.branchDivergedForegroundColor", + "description": "Specifies the decoration foreground color of branches that are both ahead and behind their upstream", + "defaults": { + "dark": "#D8AF1B", + "light": "#D8AF1B", + "highContrast": "#D8AF1B" + } + }, + { + "id": "gitlens.decorations.branchUpToDateForegroundColor", + "description": "Specifies the decoration foreground color of branches that are up to date with their upstream", + "defaults": { + "dark": "sideBar.foreground", + "light": "sideBar.foreground", + "highContrast": "sideBar.foreground" + } + }, + { + "id": "gitlens.decorations.branchUnpublishedForegroundColor", + "description": "Specifies the decoration foreground color of branches that are not yet published to an upstream", + "defaults": { + "dark": "#35b15e", + "light": "#35b15e", + "highContrast": "#4dff88" + } } ], "commands": [ diff --git a/src/constants.ts b/src/constants.ts index a90f432..468bf2e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -119,11 +119,12 @@ export function hasVisibleTextEditor(): boolean { return window.visibleTextEditors.some(e => isTextEditor(e)); } -export enum GlyphChars { +export const enum GlyphChars { AngleBracketLeftHeavy = '\u2770', AngleBracketRightHeavy = '\u2771', ArrowBack = '\u21a9', ArrowDown = '\u2193', + ArrowDownUp = '\u21F5', ArrowDropRight = '\u2937', ArrowHeadRight = '\u27A4', ArrowLeft = '\u2190', @@ -136,13 +137,14 @@ export enum GlyphChars { ArrowRightDouble = '\u21d2', ArrowRightHollow = '\u21e8', ArrowUp = '\u2191', + ArrowUpDown = '\u21C5', ArrowUpRight = '\u2197', ArrowsHalfLeftRight = '\u21cb', ArrowsHalfRightLeft = '\u21cc', ArrowsLeftRight = '\u21c6', ArrowsRightLeft = '\u21c4', Asterisk = '\u2217', - Check = '\u2713', + Check = '✔', Dash = '\u2014', Dot = '\u2022', Ellipsis = '\u2026', diff --git a/src/container.ts b/src/container.ts index 19b7aff..15484b1 100644 --- a/src/container.ts +++ b/src/container.ts @@ -35,6 +35,7 @@ import { SearchAndCompareView } from './views/searchAndCompareView'; import { StashesView } from './views/stashesView'; import { TagsView } from './views/tagsView'; import { ViewCommands } from './views/viewCommands'; +import { ViewFileDecorationProvider } from './views/viewDecorationProvider'; import { VslsController } from './vsls/vsls'; import { RebaseEditorProvider } from './webviews/rebaseEditor'; import { SettingsWebview } from './webviews/settingsWebview'; @@ -60,6 +61,8 @@ export class Container { context.subscriptions.push((this._git = new GitService())); + context.subscriptions.push(new ViewFileDecorationProvider()); + // Since there is a bit of a chicken & egg problem with the DocumentTracker and the GitService, initialize the tracker once the GitService is loaded this._tracker.initialize(); diff --git a/src/git/models/branch.ts b/src/git/models/branch.ts index 31cc86a..9db03ab 100644 --- a/src/git/models/branch.ts +++ b/src/git/models/branch.ts @@ -25,6 +25,15 @@ export interface GitTrackingState { behind: number; } +export enum GitBranchStatus { + Ahead = 'ahead', + Behind = 'behind', + Diverged = 'diverged', + UpToDate = 'upToDate', + Unpublished = 'unpublished', + LocalOnly = 'localOnly', +} + export class GitBranch implements GitBranchReference { static is(branch: any): branch is GitBranch { return branch instanceof GitBranch; @@ -189,7 +198,23 @@ export class GitBranch implements GitBranchReference { return undefined; } + @memoize() + async getStatus(): Promise { + if (this.tracking) { + if (this.state.ahead && this.state.behind) return GitBranchStatus.Diverged; + if (this.state.ahead) return GitBranchStatus.Ahead; + if (this.state.behind) return GitBranchStatus.Behind; + return GitBranchStatus.UpToDate; + } + + const remotes = await Container.git.getRemotes(this.repoPath); + if (remotes.length > 0) return GitBranchStatus.Unpublished; + + return GitBranchStatus.LocalOnly; + } + getTrackingStatus(options?: { + count?: boolean; empty?: string; expand?: boolean; icons?: boolean; diff --git a/src/git/models/status.ts b/src/git/models/status.ts index 630de92..7cec6a3 100644 --- a/src/git/models/status.ts +++ b/src/git/models/status.ts @@ -256,6 +256,7 @@ export class GitStatus { upstream: string | undefined, state: { ahead: number; behind: number }, options: { + count?: boolean; empty?: string; expand?: boolean; icons?: boolean; @@ -264,7 +265,7 @@ export class GitStatus { suffix?: string; } = {}, ): string { - const { expand = false, icons = false, prefix = '', separator = ' ', suffix = '' } = options; + const { count = true, expand = false, icons = false, prefix = '', separator = ' ', suffix = '' } = options; if (upstream == null || (state.behind === 0 && state.ahead === 0)) return options.empty ?? ''; if (expand) { @@ -285,7 +286,9 @@ export class GitStatus { return `${prefix}${status}${suffix}`; } - return `${prefix}${state.behind}${GlyphChars.ArrowDown}${separator}${state.ahead}${GlyphChars.ArrowUp}${suffix}`; + return `${prefix}${count ? state.behind : ''}${ + count || state.behind !== 0 ? GlyphChars.ArrowDown : '' + }${separator}${count ? state.ahead : ''}${count || state.ahead !== 0 ? GlyphChars.ArrowUp : ''}${suffix}`; } } diff --git a/src/views/nodes/branchNode.ts b/src/views/nodes/branchNode.ts index 15d5510..efec140 100644 --- a/src/views/nodes/branchNode.ts +++ b/src/views/nodes/branchNode.ts @@ -1,5 +1,5 @@ 'use strict'; -import { MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; +import { MarkdownString, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri, window } from 'vscode'; import { BranchesView } from '../branchesView'; import { BranchTrackingStatusNode } from './branchTrackingStatusNode'; import { CommitNode } from './commitNode'; @@ -404,6 +404,11 @@ export class BranchNode item.description = description; item.id = this.id; item.tooltip = tooltip; + item.resourceUri = Uri.parse( + `gitlens-view://branch/status/${await this.branch.getStatus()}${ + this.options.showCurrent && this.current ? '/current' : '' + }`, + ); return item; } diff --git a/src/views/nodes/commitFileNode.ts b/src/views/nodes/commitFileNode.ts index f6a0604..196b3dd 100644 --- a/src/views/nodes/commitFileNode.ts +++ b/src/views/nodes/commitFileNode.ts @@ -1,6 +1,6 @@ 'use strict'; import * as paths from 'path'; -import { Command, Selection, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { Command, Selection, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { Commands, DiffWithPreviousCommandArgs } from '../../commands'; import { Container } from '../../container'; import { GitBranch, GitFile, GitLogCommit, GitRevisionReference, StatusFileFormatter } from '../../git/git'; @@ -63,6 +63,7 @@ export class CommitFileNode extends ViewR const item = new TreeItem(this.label, TreeItemCollapsibleState.None); item.contextValue = this.contextValue; item.description = this.description; + item.resourceUri = Uri.parse(`gitlens-view://commit-file/status/${this.file.status}`); item.tooltip = this.tooltip; const icon = GitFile.getStatusIcon(this.file.status); diff --git a/src/views/nodes/fileRevisionAsCommitNode.ts b/src/views/nodes/fileRevisionAsCommitNode.ts index 1fef585..4e9e001 100644 --- a/src/views/nodes/fileRevisionAsCommitNode.ts +++ b/src/views/nodes/fileRevisionAsCommitNode.ts @@ -104,6 +104,8 @@ export class FileRevisionAsCommitNode extends ViewRefFileNode(); + get onDidChange(): Event { + return this._onDidChange.event; + } + + private readonly disposable: Disposable; + constructor() { + this.disposable = Disposable.from( + // Register the current branch decorator separately (since we can only have 2 char's per decoration) + window.registerFileDecorationProvider({ + provideFileDecoration: (uri, token) => { + if (uri.scheme !== 'gitlens-view' || uri.authority !== 'branch') return undefined; + + return this.provideBranchCurrentDecoration(uri, token); + }, + }), + window.registerFileDecorationProvider(this), + ); + } + + dispose(): void { + this.disposable.dispose(); + } + + provideFileDecoration(uri: Uri, token: CancellationToken): FileDecoration | undefined { + if (uri.scheme !== 'gitlens-view') return undefined; + + switch (uri.authority) { + case 'branch': + return this.provideBranchStatusDecoration(uri, token); + case 'commit-file': + return this.provideCommitFileStatusDecoration(uri, token); + } + + return undefined; + } + + provideCommitFileStatusDecoration(uri: Uri, _token: CancellationToken): FileDecoration | undefined { + const [, , status] = uri.path.split('/'); + + switch (status) { + case '!': + return { + badge: 'I', + color: new ThemeColor('gitDecoration.ignoredResourceForeground'), + tooltip: 'Ignored', + }; + case '?': + return { + badge: 'U', + color: new ThemeColor('gitDecoration.untrackedResourceForeground'), + tooltip: 'Untracked', + }; + case 'A': + return { + badge: 'A', + color: new ThemeColor('gitDecoration.addedResourceForeground'), + tooltip: 'Added', + }; + case 'C': + return { + badge: 'C', + color: new ThemeColor('gitlens.decorations.copiedForegroundColor'), + tooltip: 'Copied', + }; + case 'D': + return { + badge: 'D', + color: new ThemeColor('gitDecoration.deletedResourceForeground'), + tooltip: 'Deleted', + }; + case 'M': + return { + badge: 'M', + // color: new ThemeColor('gitDecoration.modifiedResourceForeground'), + tooltip: 'Modified', + }; + case 'R': + return { + badge: 'R', + color: new ThemeColor('gitlens.decorations.renamedForegroundColor'), + tooltip: 'Renamed', + }; + default: + return undefined; + } + } + + provideBranchStatusDecoration(uri: Uri, _token: CancellationToken): FileDecoration | undefined { + const [, , status] = uri.path.split('/'); + + switch (status as GitBranchStatus) { + case GitBranchStatus.Ahead: + return { + badge: '⮝', + color: new ThemeColor('gitlens.decorations.branchAheadForegroundColor'), + tooltip: 'Ahead', + }; + case GitBranchStatus.Behind: + return { + badge: '⮟', + color: new ThemeColor('gitlens.decorations.branchBehindForegroundColor'), + tooltip: 'Behind', + }; + case GitBranchStatus.Diverged: + return { + badge: '⮟⮝', + color: new ThemeColor('gitlens.decorations.branchDivergedForegroundColor'), + tooltip: 'Diverged', + }; + case GitBranchStatus.UpToDate: + return { + badge: '', + color: new ThemeColor('gitlens.decorations.branchUpToDateForegroundColor'), + tooltip: 'Up to Date', + }; + case GitBranchStatus.Unpublished: + return { + badge: '⮙+', + color: new ThemeColor('gitlens.decorations.branchUnpublishedForegroundColor'), + tooltip: 'Unpublished', + }; + default: + return undefined; + } + } + + provideBranchCurrentDecoration(uri: Uri, _token: CancellationToken): FileDecoration | undefined { + const [, , status, current] = uri.path.split('/'); + + if (!current) return undefined; + + let color; + switch (status as GitBranchStatus) { + case GitBranchStatus.Ahead: + color = new ThemeColor('gitlens.decorations.branchAheadForegroundColor'); + break; + case GitBranchStatus.Behind: + color = new ThemeColor('gitlens.decorations.branchBehindForegroundColor'); + break; + case GitBranchStatus.Diverged: + color = new ThemeColor('gitlens.decorations.branchDivergedForegroundColor'); + break; + case GitBranchStatus.UpToDate: + color = new ThemeColor('gitlens.decorations.branchUpToDateForegroundColor'); + break; + case GitBranchStatus.Unpublished: + color = new ThemeColor('gitlens.decorations.branchUnpublishedForegroundColor'); + break; + } + + return { + badge: GlyphChars.Check, + color: color, + tooltip: 'Current Branch', + }; + } +}