Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,19 @@
"description": "Whether to use a compact icon alignment (like Seti file icon theme) where only files have icons.",
"default": false
},
"gitTreeCompare.iconStyle": {
"type": "string",
"enum": [
"status",
"fileTheme"
],
"enumDescriptions": [
"Use the extension's git status icons for files.",
"Use VS Code file theme icons and color file labels by git status."
],
"description": "Which icon style to use for files in the compare tree.",
"default": "status"
},
"gitTreeCompare.collapsed": {
"type": "boolean",
"description": "Whether to show folders collapsed instead of expanded. NOTE: A restart is required when changing this setting.",
Expand Down
74 changes: 63 additions & 11 deletions src/treeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import * as fs from 'fs'
import { TreeDataProvider, TreeItem, TreeItemCollapsibleState,
Uri, Disposable, EventEmitter, TextDocumentShowOptions,
QuickPickItem, ProgressLocation, Memento, OutputChannel,
workspace, commands, window, env, WorkspaceFoldersChangeEvent, TreeView, ThemeIcon, TreeItemCheckboxState, TreeCheckboxChangeEvent, authentication, TextEditor } from 'vscode'
workspace, commands, window, env, WorkspaceFoldersChangeEvent, TreeView, ThemeIcon, TreeItemCheckboxState, TreeCheckboxChangeEvent, authentication, TextEditor,
FileDecorationProvider, FileDecoration, ProviderResult, ThemeColor } from 'vscode'
import { NAMESPACE } from './constants'
import { Repository, Git } from './git/git'
import { Ref, RefType } from './git/api/git'
Expand All @@ -21,8 +22,10 @@ import { Octokit } from '@octokit/rest';


type SortOrder = 'name' | 'path' | 'status' | 'recentlyModified';
type IconStyle = 'status' | 'fileTheme';

const MAX_DIFF_ENTRIES = 10000;
const TREE_RESOURCE_SCHEME = 'git-tree-compare';

const STATUS_SORT_ORDER: { [key: string]: number } = {
'M': 0, // Modified
Expand Down Expand Up @@ -129,6 +132,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
private autoRefresh: boolean;
private refreshIndex: boolean;
private iconsMinimal: boolean;
private iconStyle: IconStyle;
private fullDiff: boolean;
private findRenames: boolean;
private renameThreshold: number;
Expand Down Expand Up @@ -195,6 +199,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos

this.disposables.push(workspace.onDidChangeConfiguration(this.handleConfigChange, this));
this.disposables.push(workspace.onDidChangeWorkspaceFolders(this.handleWorkspaceFoldersChanged, this));
this.disposables.push(window.registerFileDecorationProvider(new GitTreeCompareFileDecorationProvider()));
this.disposables.push(this.gitApi.onDidOpenRepository(this.handleRepositoryOpened, this));
for (const repository of this.gitApi.repositories) {
this.disposables.push(repository.ui.onDidChange(() => this.handleRepositoryUiChange(repository)));
Expand Down Expand Up @@ -416,6 +421,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
this.autoRefresh = config.get<boolean>('autoRefresh', true);
this.refreshIndex = config.get<boolean>('refreshIndex', true);
this.iconsMinimal = config.get<boolean>('iconsMinimal', false);
this.iconStyle = config.get<IconStyle>('iconStyle', 'status');
this.fullDiff = config.get<string>('diffMode') === 'full';
this.findRenames = config.get<boolean>('findRenames', true);
this.renameThreshold = config.get<number>('renameThreshold', 50);
Expand Down Expand Up @@ -473,7 +479,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
checkboxState = this.computeFolderCheckboxState(element);
}
}
return toTreeItem(element, this.openChangesOnSelect, this.iconsMinimal, this.showCollapsed, this.viewAsList, this.showDiffStats, checkboxState, this.asAbsolutePath);
return toTreeItem(element, this.openChangesOnSelect, this.iconsMinimal, this.iconStyle, this.showCollapsed, this.viewAsList, this.showDiffStats, checkboxState, this.asAbsolutePath);
}

getParent(element: Element): Element | undefined {
Expand Down Expand Up @@ -883,6 +889,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
const oldAutoRefresh = this.autoRefresh;
const oldRefreshIndex = this.refreshIndex;
const oldIconsMinimal = this.iconsMinimal;
const oldIconStyle = this.iconStyle;
const oldFullDiff = this.fullDiff;
const oldFindRenames = this.findRenames;
const oldRenameThreshold = this.renameThreshold;
Expand All @@ -901,6 +908,7 @@ export class GitTreeCompareProvider implements TreeDataProvider<Element>, Dispos
oldInclude != this.includeFilesOutsideWorkspaceFolderRoot ||
oldOpenChangesOnSelect != this.openChangesOnSelect ||
oldIconsMinimal != this.iconsMinimal ||
oldIconStyle != this.iconStyle ||
(!oldAutoRefresh && this.autoRefresh) ||
(!oldRefreshIndex && this.refreshIndex) ||
oldFullDiff != this.fullDiff ||
Expand Down Expand Up @@ -1826,16 +1834,25 @@ function formatDiffStats(stats: IDiffStats): string {
return parts.join(' ');
}

class GitTreeCompareFileDecorationProvider implements FileDecorationProvider {
provideFileDecoration(uri: Uri): ProviderResult<FileDecoration> {
if (uri.scheme !== TREE_RESOURCE_SCHEME || !isStatusCode(uri.query)) {
return undefined;
}
return new FileDecoration(undefined, getStatusText(uri.query), getStatusColor(uri.query));
}
}

function toTreeItem(element: Element, openChangesOnSelect: boolean, iconsMinimal: boolean,
showCollapsed: boolean, viewAsList: boolean, showDiffStats: boolean,
iconStyle: IconStyle, showCollapsed: boolean, viewAsList: boolean, showDiffStats: boolean,
checkboxState: TreeItemCheckboxState | undefined,
asAbsolutePath: (relPath: string) => string): TreeItem {
const gitIconRoot = asAbsolutePath('resources/git-icons');
if (element instanceof FileElement) {
const statsText = showDiffStats && element.stats ? formatDiffStats(element.stats) : '';
const displayLabel = statsText ? `${element.label} ${statsText}` : element.label;
const item = new TreeItem(displayLabel);
const statusText = getStatusText(element);
const statusText = getStatusText(element.status);
item.tooltip = `${element.dstAbsPath} • ${statusText}`;
if (element.srcAbsPath !== element.dstAbsPath) {
item.tooltip = `${element.srcAbsPath} → ${item.tooltip}`;
Expand All @@ -1851,7 +1868,12 @@ function toTreeItem(element: Element, openChangesOnSelect: boolean, iconsMinimal
}
item.contextValue = element.isSubmodule ? 'submodule' : 'file';
item.id = element.dstAbsPath;
item.iconPath = path.join(gitIconRoot, toIconName(element) + '.svg');
if (iconStyle === 'fileTheme') {
item.resourceUri = toTreeResourceUri(element.dstAbsPath, element.status);
item.iconPath = ThemeIcon.File;
} else {
item.iconPath = path.join(gitIconRoot, toIconName(element.status) + '.svg');
}
if (checkboxState !== undefined) {
item.checkboxState = checkboxState;
}
Expand All @@ -1870,7 +1892,12 @@ function toTreeItem(element: Element, openChangesOnSelect: boolean, iconsMinimal
item.contextValue = 'root';
item.id = 'root'
if (!iconsMinimal) {
item.iconPath = new ThemeIcon('folder-opened');
if (iconStyle === 'fileTheme') {
item.resourceUri = toTreeResourceUri(element.dstAbsPath);
item.iconPath = ThemeIcon.Folder;
} else {
item.iconPath = new ThemeIcon('folder-opened');
}
}
return item;
} else if (element instanceof FolderElement) {
Expand All @@ -1882,7 +1909,12 @@ function toTreeItem(element: Element, openChangesOnSelect: boolean, iconsMinimal
item.checkboxState = checkboxState;
}
if (!iconsMinimal) {
item.iconPath = new ThemeIcon('folder-opened');
if (iconStyle === 'fileTheme') {
item.resourceUri = toTreeResourceUri(element.dstAbsPath);
item.iconPath = ThemeIcon.Folder;
} else {
item.iconPath = new ThemeIcon('folder-opened');
}
}
return item;
} else if (element instanceof RefElement) {
Expand All @@ -1900,8 +1932,12 @@ function toTreeItem(element: Element, openChangesOnSelect: boolean, iconsMinimal
throw new Error('unsupported element type');
}

function toIconName(element: FileElement) {
switch(element.status) {
function toTreeResourceUri(absPath: string, status?: StatusCode): Uri {
return Uri.file(absPath).with({ scheme: TREE_RESOURCE_SCHEME, query: status });
}

function toIconName(status: StatusCode) {
switch(status) {
case 'U': return 'status-untracked';
case 'A': return 'status-added';
case 'D': return 'status-deleted';
Expand All @@ -1912,8 +1948,12 @@ function toIconName(element: FileElement) {
}
}

function getStatusText(element: FileElement) {
switch(element.status) {
function isStatusCode(status: string): status is StatusCode {
return status === 'U' || status === 'A' || status === 'D' || status === 'M' || status === 'C' || status === 'T' || status === 'R';
}

function getStatusText(status: StatusCode) {
switch(status) {
case 'U': return 'Untracked';
case 'A': return 'Added';
case 'D': return 'Deleted';
Expand All @@ -1924,6 +1964,18 @@ function getStatusText(element: FileElement) {
}
}

function getStatusColor(status: StatusCode): ThemeColor {
switch(status) {
case 'U': return new ThemeColor('descriptionForeground');
case 'A': return new ThemeColor('gitDecoration.addedResourceForeground');
case 'D': return new ThemeColor('gitDecoration.deletedResourceForeground');
case 'M': return new ThemeColor('charts.blue');
case 'C': return new ThemeColor('gitDecoration.conflictingResourceForeground');
case 'T': return new ThemeColor('charts.blue');
case 'R': return new ThemeColor('gitDecoration.renamedResourceForeground');
}
}

function sortedArraysEqual<T> (a: T[], b: T[]): boolean {
if (a.length != b.length) {
return false;
Expand Down