diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index d5d279e00ce7d..9555c23c01da1 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -964,7 +964,7 @@ export interface SourceControlGroupFeatures { export interface ScmRawResource { handle: number, sourceUri: UriComponents, - icons: UriComponents[], + icons: (IconUrl | ThemeIcon | undefined)[], /* icons: light, dark */ tooltip: string, strikeThrough: boolean, faded: boolean, diff --git a/packages/plugin-ext/src/main/browser/scm-main.ts b/packages/plugin-ext/src/main/browser/scm-main.ts index 665a3e95bd551..dec95c621c922 100644 --- a/packages/plugin-ext/src/main/browser/scm-main.ts +++ b/packages/plugin-ext/src/main/browser/scm-main.ts @@ -40,6 +40,9 @@ import { URI as vscodeURI } from '@theia/core/shared/vscode-uri'; import { Splice } from '../../common/arrays'; import { UriComponents } from '../../common/uri-components'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; +import { PluginSharedStyle } from './plugin-shared-style'; +import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; +import { IconUrl } from '../../common'; export class PluginScmResourceGroup implements ScmResourceGroup { @@ -147,10 +150,12 @@ export class PluginScmProvider implements ScmProvider { constructor( private readonly proxy: ScmExt, private readonly colors: ColorRegistry, + private readonly sharedStyle: PluginSharedStyle, private readonly _handle: number, private readonly _contextValue: string, private readonly _label: string, - private readonly _rootUri: vscodeURI | undefined + private readonly _rootUri: vscodeURI | undefined, + private disposables: DisposableCollection ) { } updateSourceControl(features: SourceControlProviderFeatures): void { @@ -222,13 +227,13 @@ export class PluginScmProvider implements ScmProvider { const { start, deleteCount, rawResources } = groupSlice; const resources = rawResources.map(rawResource => { const { handle, sourceUri, icons, tooltip, strikeThrough, faded, contextValue, command } = rawResource; - const icon = icons[0]; - const iconDark = icons[1] || icon; + const icon = this.toIconClass(icons[0]); + const iconDark = this.toIconClass(icons[1]) || icon; // eslint-disable-next-line @typescript-eslint/no-explicit-any const colorVariable = (rawResource as any).colorId && this.colors.toCssVariableName((rawResource as any).colorId); const decorations = { - icon: icon ? vscodeURI.revive(icon) : undefined, - iconDark: iconDark ? vscodeURI.revive(iconDark) : undefined, + icon, + iconDark, tooltip, strikeThrough, // TODO remove the letter and colorId fields when the FileDecorationProvider is applied, see https://github.com/eclipse-theia/theia/pull/8911 @@ -258,6 +263,18 @@ export class PluginScmProvider implements ScmProvider { this.onDidChangeResourcesEmitter.fire(); } + private toIconClass(icon: IconUrl | ThemeIcon | undefined): string | undefined { + if (!icon) { + return undefined; + } + if (ThemeIcon.isThemeIcon(icon)) { + return ThemeIcon.asClassName(icon); + } + const reference = this.sharedStyle.toIconClass(icon); + this.disposables.push(reference); + return reference.object.iconClass; + } + unregisterGroup(handle: number): void { const group = this.groupsByHandle[handle]; @@ -280,11 +297,13 @@ export class ScmMainImpl implements ScmMain { private repositoryDisposables = new Map(); private readonly disposables = new DisposableCollection(); private readonly colors: ColorRegistry; + private readonly sharedStyle: PluginSharedStyle; constructor(rpc: RPCProtocol, container: interfaces.Container) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.SCM_EXT); this.scmService = container.get(ScmService); this.colors = container.get(ColorRegistry); + this.sharedStyle = container.get(PluginSharedStyle); } dispose(): void { @@ -298,7 +317,7 @@ export class ScmMainImpl implements ScmMain { } async $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined): Promise { - const provider = new PluginScmProvider(this.proxy, this.colors, handle, id, label, rootUri ? vscodeURI.revive(rootUri) : undefined); + const provider = new PluginScmProvider(this.proxy, this.colors, this.sharedStyle, handle, id, label, rootUri ? vscodeURI.revive(rootUri) : undefined, this.disposables); const repository = this.scmService.registerScmProvider(provider, { input: { validator: async value => { diff --git a/packages/plugin-ext/src/plugin/scm.ts b/packages/plugin-ext/src/plugin/scm.ts index d13c6ef4e8c21..2586c2638ecb2 100644 --- a/packages/plugin-ext/src/plugin/scm.ts +++ b/packages/plugin-ext/src/plugin/scm.ts @@ -35,20 +35,26 @@ import { Splice } from '../common/arrays'; import { UriComponents } from '../common/uri-components'; import { Command } from '../common/plugin-api-rpc-model'; import { RPCProtocol } from '../common/rpc-protocol'; -import { URI } from './types-impl'; +import { URI, ThemeIcon } from './types-impl'; import { ScmCommandArg } from '../common/plugin-api-rpc'; import { sep } from '@theia/core/lib/common/paths'; +import { PluginIconPath } from './plugin-icon-path'; type ProviderHandle = number; type GroupHandle = number; type ResourceStateHandle = number; -function getIconResource(decorations?: theia.SourceControlResourceThemableDecorations): theia.Uri | undefined { - if (!decorations) { +function getIconResource(decorations?: theia.SourceControlResourceThemableDecorations): UriComponents | ThemeIcon | undefined { + if (!decorations || !decorations.iconPath) { return undefined; } else if (typeof decorations.iconPath === 'string') { return URI.file(decorations.iconPath); - } else { + } else if (URI.isUri(decorations.iconPath)) { + return decorations.iconPath; + } else if (ThemeIcon.is(decorations.iconPath)) { return decorations.iconPath; + } else { + console.warn(`Unexpected Value ${decorations.iconPath} in Source Control Resource Themable Decoration. URI, ThemeIcon or string expected.`); + return undefined; } } @@ -111,8 +117,8 @@ function compareResourceThemableDecorations(a: theia.SourceControlResourceThemab return 1; } - const aPath = typeof a.iconPath === 'string' ? a.iconPath : a.iconPath.fsPath; - const bPath = typeof b.iconPath === 'string' ? b.iconPath : b.iconPath.fsPath; + const aPath = typeof a.iconPath === 'string' ? a.iconPath : URI.isUri(a.iconPath) ? a.iconPath.fsPath : (a.iconPath as ThemeIcon).id; + const bPath = typeof b.iconPath === 'string' ? b.iconPath : URI.isUri(b.iconPath) ? b.iconPath.fsPath : (b.iconPath as ThemeIcon).id; return comparePaths(aPath, bPath); } @@ -361,7 +367,7 @@ export class ScmInputBoxImpl implements theia.SourceControlInputBox { } } -class SsmResourceGroupImpl implements theia.SourceControlResourceGroup { +class ScmResourceGroupImpl implements theia.SourceControlResourceGroup { private static handlePool: number = 0; private resourceHandlePool: number = 0; @@ -409,12 +415,13 @@ class SsmResourceGroupImpl implements theia.SourceControlResourceGroup { this.onDidUpdateResourceStatesEmitter.fire(); } - readonly handle = SsmResourceGroupImpl.handlePool++; + readonly handle = ScmResourceGroupImpl.handlePool++; constructor( private proxy: ScmMain, private commands: CommandRegistryImpl, private sourceControlHandle: number, + private plugin: Plugin, private _id: string, private _label: string, ) { } @@ -443,10 +450,11 @@ class SsmResourceGroupImpl implements theia.SourceControlResourceGroup { this.resourceStatesMap.set(handle, r); const sourceUri = r.resourceUri; - const iconUri = getIconResource(r.decorations); - const lightIconUri = r.decorations && getIconResource(r.decorations.light) || iconUri; - const darkIconUri = r.decorations && getIconResource(r.decorations.dark) || iconUri; - const icons: UriComponents[] = []; + + const icon = getIconResource(r.decorations); + const lightIcon = r.decorations && getIconResource(r.decorations.light) || icon; + const darkIcon = r.decorations && getIconResource(r.decorations.dark) || icon; + const icons = [this.getThemableIcon(lightIcon), this.getThemableIcon(darkIcon)]; let command: Command | undefined; if (r.command) { @@ -459,14 +467,6 @@ class SsmResourceGroupImpl implements theia.SourceControlResourceGroup { } } - if (lightIconUri) { - icons.push(lightIconUri); - } - - if (darkIconUri && (darkIconUri.toString() !== lightIconUri?.toString())) { - icons.push(darkIconUri); - } - const tooltip = (r.decorations && r.decorations.tooltip) || ''; const strikeThrough = r.decorations && !!r.decorations.strikeThrough; const faded = r.decorations && !!r.decorations.faded; @@ -511,6 +511,15 @@ class SsmResourceGroupImpl implements theia.SourceControlResourceGroup { return rawResourceSplices; } + private getThemableIcon(icon: UriComponents | ThemeIcon | undefined): string | ThemeIcon | undefined { + if (!icon) { + return undefined; + } else if (ThemeIcon.is(icon)) { + return icon; + } + return PluginIconPath.asString(URI.revive(icon), this.plugin); + } + dispose(): void { this._disposed = true; this.onDidDisposeEmitter.fire(); @@ -520,7 +529,7 @@ class SsmResourceGroupImpl implements theia.SourceControlResourceGroup { class SourceControlImpl implements theia.SourceControl { private static handlePool: number = 0; - private groups: Map = new Map(); + private groups: Map = new Map(); get id(): string { return this._id; @@ -626,7 +635,7 @@ class SourceControlImpl implements theia.SourceControl { private handle: number = SourceControlImpl.handlePool++; constructor( - plugin: Plugin, + private plugin: Plugin, private proxy: ScmMain, private commands: CommandRegistryImpl, private _id: string, @@ -637,11 +646,11 @@ class SourceControlImpl implements theia.SourceControl { this.proxy.$registerSourceControl(this.handle, _id, _label, _rootUri); } - private createdResourceGroups = new Map(); - private updatedResourceGroups = new Set(); + private createdResourceGroups = new Map(); + private updatedResourceGroups = new Set(); - createResourceGroup(id: string, label: string): SsmResourceGroupImpl { - const group = new SsmResourceGroupImpl(this.proxy, this.commands, this.handle, id, label); + createResourceGroup(id: string, label: string): ScmResourceGroupImpl { + const group = new ScmResourceGroupImpl(this.proxy, this.commands, this.handle, this.plugin, id, label); const disposable = group.onDidDispose(() => this.createdResourceGroups.delete(group)); this.createdResourceGroups.set(group, disposable); this.eventuallyAddResourceGroups(); @@ -703,7 +712,7 @@ class SourceControlImpl implements theia.SourceControl { this.updatedResourceGroups.clear(); } - getResourceGroup(handle: GroupHandle): SsmResourceGroupImpl | undefined { + getResourceGroup(handle: GroupHandle): ScmResourceGroupImpl | undefined { return this.groups.get(handle); } diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 5f46a5b41ca61..2777614393d58 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -11379,7 +11379,7 @@ export module '@theia/plugin' { * The icon path for a specific * {@link SourceControlResourceState source control resource state}. */ - readonly iconPath?: string | Uri; + readonly iconPath?: string | Uri | ThemeIcon; } /** diff --git a/packages/scm/src/browser/scm-provider.ts b/packages/scm/src/browser/scm-provider.ts index df4dc3542c3c9..7e0b87999ddd6 100644 --- a/packages/scm/src/browser/scm-provider.ts +++ b/packages/scm/src/browser/scm-provider.ts @@ -58,6 +58,8 @@ export interface ScmResource { } export interface ScmResourceDecorations { + icon?: string; + iconDark?: string; tooltip?: string; source?: string; letter?: string; diff --git a/packages/scm/src/browser/scm-tree-widget.tsx b/packages/scm/src/browser/scm-tree-widget.tsx index 210aef8505de1..9a868729b50c3 100644 --- a/packages/scm/src/browser/scm-tree-widget.tsx +++ b/packages/scm/src/browser/scm-tree-widget.tsx @@ -17,7 +17,7 @@ /* eslint-disable no-null/no-null, @typescript-eslint/no-explicit-any */ import * as React from '@theia/core/shared/react'; -import { injectable, inject } from '@theia/core/shared/inversify'; +import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import { isOSX } from '@theia/core/lib/common/os'; import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; @@ -32,6 +32,7 @@ import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import { Decoration, DecorationsService } from '@theia/core/lib/browser/decorations-service'; import { FileStat } from '@theia/filesystem/lib/common/files'; +import { ThemeService } from '@theia/core/lib/browser/theming'; @injectable() export class ScmTreeWidget extends TreeWidget { @@ -55,6 +56,7 @@ export class ScmTreeWidget extends TreeWidget { @inject(IconThemeService) protected readonly iconThemeService: IconThemeService; @inject(DecorationsService) protected readonly decorationsService: DecorationsService; @inject(ColorRegistry) protected readonly colors: ColorRegistry; + @inject(ThemeService) protected readonly themeService: ThemeService; // TODO: Make TreeWidget generic to better type those fields. override readonly model: ScmTreeModel; @@ -69,6 +71,12 @@ export class ScmTreeWidget extends TreeWidget { this.addClass('groups-outer-container'); } + @postConstruct() + protected override init(): void { + super.init(); + this.toDispose.push(this.themeService.onDidColorThemeChange(() => this.update())); + } + set viewMode(id: 'tree' | 'list') { // Close the search box because the structure of the tree will change dramatically // and the search results will be out of date. @@ -153,6 +161,7 @@ export class ScmTreeWidget extends TreeWidget { sourceUri: node.sourceUri, decoration: this.decorationsService.getDecoration(new URI(node.sourceUri), true)[0], colors: this.colors, + isLightTheme: this.isCurrentThemeLight(), renderExpansionToggle: () => this.renderExpansionToggle(node, props), }} />; @@ -436,6 +445,11 @@ export class ScmTreeWidget extends TreeWidget { return super.getPaddingLeft(node, props); } + protected isCurrentThemeLight(): boolean { + const type = this.themeService.getCurrentTheme().type; + return type.toLocaleLowerCase().includes('light'); + } + protected override needsExpansionTogglePadding(node: TreeNode): boolean { const theme = this.iconThemeService.getDefinition(this.iconThemeService.current); if (theme && (theme.hidesExplorerArrows || (theme.hasFileIcons && !theme.hasFolderIcons))) { @@ -528,12 +542,16 @@ export class ScmResourceComponent extends ScmElement override render(): JSX.Element | undefined { const { hover } = this.state; - const { model, treeNode, colors, parentPath, sourceUri, decoration, labelProvider, commandExecutor, menus, contextKeys, caption } = this.props; + const { model, treeNode, colors, parentPath, sourceUri, decoration, labelProvider, commandExecutor, menus, contextKeys, caption, isLightTheme } = this.props; const resourceUri = new URI(sourceUri); + const decorationIcon = treeNode.decorations; + const themedIcon = isLightTheme ? decorationIcon?.icon : decorationIcon?.iconDark; + const classNames: string[] = themedIcon ? ['decoration-icon', themedIcon] : ['decoration-icon', 'status']; + const icon = labelProvider.getIcon(resourceUri); - const color = decoration && decoration.colorId ? `var(${colors.toCssVariableName(decoration.colorId)})` : ''; - const letter = decoration && decoration.letter || ''; + const color = decoration && decoration.colorId && !themedIcon ? `var(${colors.toCssVariableName(decoration.colorId)})` : ''; + const letter = decoration && decoration.letter && !themedIcon ? decoration.letter : ''; const tooltip = decoration && decoration.tooltip || ''; const textDecoration = treeNode.decorations?.strikeThrough === true ? 'line-through' : 'normal'; const relativePath = parentPath.relative(resourceUri.parent); @@ -567,7 +585,7 @@ export class ScmResourceComponent extends ScmElement model, treeNode }}> -
+
{letter}
@@ -630,6 +648,7 @@ export class ScmResourceComponent extends ScmElement } }; } + export namespace ScmResourceComponent { export interface Props extends ScmElement.Props { treeNode: ScmFileChangeNode; @@ -637,6 +656,7 @@ export namespace ScmResourceComponent { sourceUri: string; decoration: Decoration | undefined; colors: ColorRegistry; + isLightTheme: boolean } } diff --git a/packages/scm/src/browser/style/index.css b/packages/scm/src/browser/style/index.css index dfe925b95783f..77bf11bd9923e 100644 --- a/packages/scm/src/browser/style/index.css +++ b/packages/scm/src/browser/style/index.css @@ -219,6 +219,10 @@ font-size: var(--theia-ui-font-size0); } +.theia-scm .decoration-icon { + margin: 2px 0px; +} + .scm-change-count { float: right; }