From c9ece69241712756d10e3cb643cbf78b3e6dbc7c Mon Sep 17 00:00:00 2001 From: trin Date: Thu, 18 Jun 2026 17:04:37 +0000 Subject: [PATCH] Add configurable file theme icons --- package.json | 13 ++++++++ src/treeProvider.ts | 74 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 685e03e..06a66a8 100644 --- a/package.json +++ b/package.json @@ -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.", diff --git a/src/treeProvider.ts b/src/treeProvider.ts index 53fbc38..81cf989 100644 --- a/src/treeProvider.ts +++ b/src/treeProvider.ts @@ -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' @@ -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 @@ -129,6 +132,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos private autoRefresh: boolean; private refreshIndex: boolean; private iconsMinimal: boolean; + private iconStyle: IconStyle; private fullDiff: boolean; private findRenames: boolean; private renameThreshold: number; @@ -195,6 +199,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, 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))); @@ -416,6 +421,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos this.autoRefresh = config.get('autoRefresh', true); this.refreshIndex = config.get('refreshIndex', true); this.iconsMinimal = config.get('iconsMinimal', false); + this.iconStyle = config.get('iconStyle', 'status'); this.fullDiff = config.get('diffMode') === 'full'; this.findRenames = config.get('findRenames', true); this.renameThreshold = config.get('renameThreshold', 50); @@ -473,7 +479,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, 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 { @@ -883,6 +889,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, 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; @@ -901,6 +908,7 @@ export class GitTreeCompareProvider implements TreeDataProvider, Dispos oldInclude != this.includeFilesOutsideWorkspaceFolderRoot || oldOpenChangesOnSelect != this.openChangesOnSelect || oldIconsMinimal != this.iconsMinimal || + oldIconStyle != this.iconStyle || (!oldAutoRefresh && this.autoRefresh) || (!oldRefreshIndex && this.refreshIndex) || oldFullDiff != this.fullDiff || @@ -1826,8 +1834,17 @@ function formatDiffStats(stats: IDiffStats): string { return parts.join(' '); } +class GitTreeCompareFileDecorationProvider implements FileDecorationProvider { + provideFileDecoration(uri: Uri): ProviderResult { + 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'); @@ -1835,7 +1852,7 @@ function toTreeItem(element: Element, openChangesOnSelect: boolean, iconsMinimal 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}`; @@ -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; } @@ -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) { @@ -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) { @@ -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'; @@ -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'; @@ -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 (a: T[], b: T[]): boolean { if (a.length != b.length) { return false;