Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

plugin: ThemeIcon Support in SCM #12187

Merged
merged 2 commits into from
Jun 1, 2023
Merged
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
2 changes: 1 addition & 1 deletion packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
31 changes: 25 additions & 6 deletions packages/plugin-ext/src/main/browser/scm-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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];

Expand All @@ -280,11 +297,13 @@ export class ScmMainImpl implements ScmMain {
private repositoryDisposables = new Map<number, DisposableCollection>();
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 {
Expand All @@ -298,7 +317,7 @@ export class ScmMainImpl implements ScmMain {
}

async $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined): Promise<void> {
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 => {
Expand Down
63 changes: 36 additions & 27 deletions packages/plugin-ext/src/plugin/scm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
tsmaeder marked this conversation as resolved.
Show resolved Hide resolved
console.warn(`Unexpected Value ${decorations.iconPath} in Source Control Resource Themable Decoration. URI, ThemeIcon or string expected.`);
return undefined;
}
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
) { }
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -520,7 +529,7 @@ class SsmResourceGroupImpl implements theia.SourceControlResourceGroup {
class SourceControlImpl implements theia.SourceControl {

private static handlePool: number = 0;
private groups: Map<GroupHandle, SsmResourceGroupImpl> = new Map<GroupHandle, SsmResourceGroupImpl>();
private groups: Map<GroupHandle, ScmResourceGroupImpl> = new Map<GroupHandle, ScmResourceGroupImpl>();

get id(): string {
return this._id;
Expand Down Expand Up @@ -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,
Expand All @@ -637,11 +646,11 @@ class SourceControlImpl implements theia.SourceControl {
this.proxy.$registerSourceControl(this.handle, _id, _label, _rootUri);
}

private createdResourceGroups = new Map<SsmResourceGroupImpl, Disposable>();
private updatedResourceGroups = new Set<SsmResourceGroupImpl>();
private createdResourceGroups = new Map<ScmResourceGroupImpl, Disposable>();
private updatedResourceGroups = new Set<ScmResourceGroupImpl>();

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();
Expand Down Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/plugin/src/theia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/scm/src/browser/scm-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export interface ScmResource {
}

export interface ScmResourceDecorations {
icon?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure whether it would be a better idea to keep the conditional structure (either a ThemeIcon or a { dark: ..., light...} structure until we render the whole thing in the UI? It kinda doesn't make sense to have a dark and light ThemeIcon, as mentioned elsewhere, IMO.

iconDark?: string;
tooltip?: string;
source?: string;
letter?: string;
Expand Down
30 changes: 25 additions & 5 deletions packages/scm/src/browser/scm-tree-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand All @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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),
}}
/>;
Expand Down Expand Up @@ -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))) {
Expand Down Expand Up @@ -528,12 +542,16 @@ export class ScmResourceComponent extends ScmElement<ScmResourceComponent.Props>

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);
Expand Down Expand Up @@ -567,7 +585,7 @@ export class ScmResourceComponent extends ScmElement<ScmResourceComponent.Props>
model,
treeNode
}}>
<div title={tooltip} className='status' style={{ color }}>
<div title={tooltip} className={classNames.join(' ')} style={{ color }}>
tsmaeder marked this conversation as resolved.
Show resolved Hide resolved
{letter}
</div>
</ScmInlineActions>
Expand Down Expand Up @@ -630,13 +648,15 @@ export class ScmResourceComponent extends ScmElement<ScmResourceComponent.Props>
}
};
}

export namespace ScmResourceComponent {
export interface Props extends ScmElement.Props {
treeNode: ScmFileChangeNode;
parentPath: URI;
sourceUri: string;
decoration: Decoration | undefined;
colors: ColorRegistry;
isLightTheme: boolean
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/scm/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@
font-size: var(--theia-ui-font-size0);
}

.theia-scm .decoration-icon {
margin: 2px 0px;
}

.scm-change-count {
float: right;
}
Expand Down