diff --git a/packages/core/src/browser/decorations-service.ts b/packages/core/src/browser/decorations-service.ts new file mode 100644 index 0000000000000..2a8af7aded8c7 --- /dev/null +++ b/packages/core/src/browser/decorations-service.ts @@ -0,0 +1,214 @@ +/******************************************************************************** + * Copyright (C) 2020 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable } from 'inversify'; +import { CancellationToken, CancellationTokenSource, Disposable, Emitter, Event } from '../common'; +import { TernarySearchTree } from '../common/ternary-search-tree'; +import URI from '../common/uri'; + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// some code copied and modified from https://github.com/microsoft/vscode/blob/1.52.1/src/vs/workbench/services/decorations/browser/decorationsService.ts#L24-L23 + +export interface DecorationsProvider { + readonly onDidChange: Event; + provideDecorations(uri: URI, token: CancellationToken): Decoration | Promise | undefined; +} + +export interface Decoration { + readonly weight?: number; + readonly colorId?: string; + readonly letter?: string; + readonly tooltip?: string; + readonly bubble?: boolean; +} + +export interface ResourceDecorationChangeEvent { + affectsResource(uri: URI): boolean; +} +export const DecorationsService = Symbol('DecorationsService'); +export interface DecorationsService { + + readonly onDidChangeDecorations: Event>; + + registerDecorationsProvider(provider: DecorationsProvider): Disposable; + + getDecoration(uri: URI, includeChildren: boolean): Decoration []; +} + +class DecorationDataRequest { + constructor( + readonly source: CancellationTokenSource, + readonly thenable: Promise, + ) { } +} + +class DecorationProviderWrapper { + + readonly data: TernarySearchTree; + readonly decorations: Map = new Map(); + private readonly disposable: Disposable; + + constructor( + readonly provider: DecorationsProvider, + readonly onDidChangeDecorationsEmitter: Emitter> + ) { + + this.data = TernarySearchTree.forUris(true); + + this.disposable = this.provider.onDidChange(async uris => { + this.decorations.clear(); + if (!uris) { + this.data.clear(); + } else { + for (const uri of uris) { + this.fetchData(new URI(uri.toString())); + const decoration = await provider.provideDecorations(uri, CancellationToken.None); + if (decoration) { + this.decorations.set(uri.toString(), decoration); + } + } + } + this.onDidChangeDecorationsEmitter.fire(this.decorations); + }); + } + + dispose(): void { + this.disposable.dispose(); + this.data.clear(); + } + + knowsAbout(uri: URI): boolean { + return !!this.data.get(uri) || Boolean(this.data.findSuperstr(uri)); + } + + getOrRetrieve(uri: URI, includeChildren: boolean, callback: (data: Decoration, isChild: boolean) => void): void { + + let item = this.data.get(uri); + + if (item === undefined) { + // unknown -> trigger request + item = this.fetchData(uri); + } + + if (item && !(item instanceof DecorationDataRequest)) { + // found something (which isn't pending anymore) + callback(item, false); + } + + if (includeChildren) { + // (resolved) children + const iter = this.data.findSuperstr(uri); + if (iter) { + let next = iter.next(); + while (!next.done) { + const value = next.value; + if (value && !(value instanceof DecorationDataRequest)) { + callback(value, true); + } + next = iter.next(); + } + } + } + } + + private fetchData(uri: URI): Decoration | undefined { + + // check for pending request and cancel it + const pendingRequest = this.data.get(new URI(uri.toString())); + if (pendingRequest instanceof DecorationDataRequest) { + pendingRequest.source.cancel(); + this.data.delete(uri); + } + + const source = new CancellationTokenSource(); + const dataOrThenable = this.provider.provideDecorations(new URI(uri.toString()), source.token); + if (!isThenable | undefined>(dataOrThenable)) { + // sync -> we have a result now + return this.keepItem(uri, dataOrThenable); + + } else { + // async -> we have a result soon + const request = new DecorationDataRequest(source, Promise.resolve(dataOrThenable).then(data => { + if (this.data.get(uri) === request) { + this.keepItem(uri, data); + } + }).catch(err => { + if (!(err instanceof Error && err.name === 'Canceled' && err.message === 'Canceled') && this.data.get(uri) === request) { + this.data.delete(uri); + } + })); + + this.data.set(uri, request); + return undefined; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function isThenable(obj: any): obj is Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return obj && typeof (>obj).then === 'function'; + } + } + + private keepItem(uri: URI, data: Decoration | undefined): Decoration | undefined { + const deco = data ? data : undefined; + this.data.set(uri, deco); + return deco; + } +} + +@injectable() +export class DecorationsServiceImpl implements DecorationsService { + + private readonly data: DecorationProviderWrapper[] = []; + private readonly onDidChangeDecorationsEmitter = new Emitter>(); + + readonly onDidChangeDecorations = this.onDidChangeDecorationsEmitter.event; + + dispose(): void { + this.onDidChangeDecorationsEmitter.dispose(); + } + + registerDecorationsProvider(provider: DecorationsProvider): Disposable { + + const wrapper = new DecorationProviderWrapper(provider, this.onDidChangeDecorationsEmitter); + this.data.push(wrapper); + + return Disposable.create(() => { + // fire event that says 'yes' for any resource + // known to this provider. then dispose and remove it. + this.data.splice(this.data.indexOf(wrapper), 1); + this.onDidChangeDecorationsEmitter.fire(new Map()); + wrapper.dispose(); + }); + } + + getDecoration(uri: URI, includeChildren: boolean): Decoration [] { + const data: Decoration[] = []; + let containsChildren: boolean = false; + for (const wrapper of this.data) { + wrapper.getOrRetrieve(new URI(uri.toString()), includeChildren, (deco, isChild) => { + if (!isChild || deco.bubble) { + data.push(deco); + containsChildren = isChild || containsChildren; + } + }); + } + return data; + } +} diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index f9a049b5f2da2..9f041675a91b0 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -97,6 +97,7 @@ import { LanguageService } from './language-service'; import { EncodingRegistry } from './encoding-registry'; import { EncodingService } from '../common/encoding-service'; import { AuthenticationService, AuthenticationServiceImpl } from '../browser/authentication-service'; +import { DecorationsService, DecorationsServiceImpl } from './decorations-service'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -338,4 +339,5 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo bind(ContextMenuContext).toSelf().inSingletonScope(); bind(AuthenticationService).to(AuthenticationServiceImpl).inSingletonScope(); + bind(DecorationsService).to(DecorationsServiceImpl).inSingletonScope(); }); diff --git a/packages/git/src/browser/git-contribution.ts b/packages/git/src/browser/git-contribution.ts index e86b839b43fe6..8b97d0dcda6a1 100644 --- a/packages/git/src/browser/git-contribution.ts +++ b/packages/git/src/browser/git-contribution.ts @@ -47,6 +47,8 @@ import { GitPreferences } from './git-preferences'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import { ScmInputIssueType } from '@theia/scm/lib/browser/scm-input'; +import { DecorationsService } from '@theia/core/lib/browser/decorations-service'; +import { GitDecorationProvider } from './git-decoration-provider'; export namespace GIT_COMMANDS { export const CLONE = { @@ -257,11 +259,14 @@ export class GitContribution implements CommandContribution, MenuContribution, T @inject(CommandRegistry) protected readonly commands: CommandRegistry; @inject(ProgressService) protected readonly progressService: ProgressService; @inject(GitPreferences) protected readonly gitPreferences: GitPreferences; + @inject(DecorationsService) protected readonly decorationsService: DecorationsService; + @inject(GitDecorationProvider) protected readonly gitDecorationProvider: GitDecorationProvider; onStart(): void { this.updateStatusBar(); this.repositoryTracker.onGitEvent(() => this.updateStatusBar()); this.syncService.onDidChange(() => this.updateStatusBar()); + this.decorationsService.registerDecorationsProvider(this.gitDecorationProvider); } registerMenus(menus: MenuModelRegistry): void { diff --git a/packages/git/src/browser/git-decoration-provider.ts b/packages/git/src/browser/git-decoration-provider.ts new file mode 100644 index 0000000000000..616f580866709 --- /dev/null +++ b/packages/git/src/browser/git-decoration-provider.ts @@ -0,0 +1,66 @@ +/******************************************************************************** + * Copyright (C) 2020 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { GitFileChange, GitFileStatus, GitStatusChangeEvent } from '../common'; +import { CancellationToken, Emitter, Event } from '@theia/core/lib/common'; +import { Decoration, DecorationsProvider } from '@theia/core/lib/browser/decorations-service'; +import { GitRepositoryTracker } from './git-repository-tracker'; +import URI from '@theia/core/lib/common/uri'; + +@injectable() +export class GitDecorationProvider implements DecorationsProvider { + + private readonly onDidChangeDecorationsEmitter = new Emitter(); + readonly onDidChange: Event = this.onDidChangeDecorationsEmitter.event; + + private decorations = new Map(); + + constructor(@inject(GitRepositoryTracker) protected readonly gitRepositoryTracker: GitRepositoryTracker) { + this.gitRepositoryTracker.onGitEvent((event: GitStatusChangeEvent | undefined) => { + this.onGitEvent(event); + }); + } + + private async onGitEvent(event: GitStatusChangeEvent | undefined): Promise { + if (!event) { + return; + } + + const newDecorations = new Map(); + this.collectDecorationData(event.status.changes, newDecorations); + + const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()])); + this.decorations = newDecorations; + this.onDidChangeDecorationsEmitter.fire([...uris.values()].map(value => new URI(value))); + } + + private collectDecorationData(changes: GitFileChange[], bucket: Map): void { + changes.forEach(change => { + const color = GitFileStatus.getColor(change.status, change.staged); + bucket.set(change.uri, { + colorId: color.substring(12, color.length - 1).replace(/-/g, '.'), + tooltip: GitFileStatus.toString(change.status), + letter: GitFileStatus.toAbbreviation(change.status, change.staged) + }); + }); + } + + provideDecorations(uri: URI, token: CancellationToken): Decoration | Promise | undefined { + return this.decorations.get(uri.toString()); + } +} + diff --git a/packages/git/src/browser/git-decorator.ts b/packages/git/src/browser/git-decorator.ts deleted file mode 100644 index a22a7a7b6df2a..0000000000000 --- a/packages/git/src/browser/git-decorator.ts +++ /dev/null @@ -1,169 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 TypeFox and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; -import URI from '@theia/core/lib/common/uri'; -import { ILogger } from '@theia/core/lib/common/logger'; -import { Event, Emitter } from '@theia/core/lib/common/event'; -import { Tree } from '@theia/core/lib/browser/tree/tree'; -import { DepthFirstTreeIterator } from '@theia/core/lib/browser/tree/tree-iterator'; -import { PreferenceChangeEvent } from '@theia/core/lib/browser/preferences/preference-proxy'; -import { TreeDecorator, TreeDecoration } from '@theia/core/lib/browser/tree/tree-decorator'; -import { Git } from '../common/git'; -import { WorkingDirectoryStatus } from '../common/git-model'; -import { GitFileChange, GitFileStatus } from '../common/git-model'; -import { GitPreferences, GitConfiguration } from './git-preferences'; -import { GitRepositoryTracker } from './git-repository-tracker'; -import { FileStatNode } from '@theia/filesystem/lib/browser'; - -@injectable() -export class GitDecorator implements TreeDecorator { - - readonly id = 'theia-git-decorator'; - - @inject(Git) protected readonly git: Git; - @inject(GitRepositoryTracker) protected readonly repositories: GitRepositoryTracker; - @inject(GitPreferences) protected readonly preferences: GitPreferences; - @inject(ILogger) protected readonly logger: ILogger; - - protected readonly emitter = new Emitter<(tree: Tree) => Map>(); - - protected enabled: boolean; - protected showColors: boolean; - - @postConstruct() - protected init(): void { - this.repositories.onGitEvent(event => this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree, event && event.status))); - this.preferences.onPreferenceChanged(event => this.handlePreferenceChange(event)); - this.enabled = this.preferences['git.decorations.enabled']; - this.showColors = this.preferences['git.decorations.colors']; - } - - async decorations(tree: Tree): Promise> { - const status = this.repositories.selectedRepositoryStatus; - if (status) { - return this.collectDecorators(tree, status); - } - return new Map(); - } - - get onDidChangeDecorations(): Event<(tree: Tree) => Map> { - return this.emitter.event; - } - - protected fireDidChangeDecorations(event: (tree: Tree) => Map): void { - this.emitter.fire(event); - } - - protected collectDecorators(tree: Tree, status: WorkingDirectoryStatus | undefined): Map { - const result = new Map(); - if (tree.root === undefined || !this.enabled) { - return result; - } - const markers = this.appendContainerChanges(tree, status ? status.changes : []); - for (const treeNode of new DepthFirstTreeIterator(tree.root)) { - const uri = FileStatNode.getUri(treeNode); - if (uri) { - const marker = markers.get(uri); - if (marker) { - result.set(treeNode.id, marker); - } - } - } - return new Map(Array.from(result.entries()).map(m => [m[0], this.toDecorator(m[1])] as [string, TreeDecoration.Data])); - } - - protected appendContainerChanges(tree: Tree, changes: GitFileChange[]): Map { - const result: Map = new Map(); - // We traverse up and assign the highest Git file change status the container directory. - // Note, instead of stopping at the WS root, we traverse up the driver root. - // We will filter them later based on the expansion state of the tree. - for (const [uri, change] of new Map(changes.map(m => [new URI(m.uri), m] as [URI, GitFileChange])).entries()) { - const uriString = uri.toString(); - result.set(uriString, change); - let parentUri: URI | undefined = uri.parent; - while (parentUri && !parentUri.path.isRoot) { - const parentUriString = parentUri.toString(); - const existing = result.get(parentUriString); - if (existing === undefined || this.compare(existing, change) < 0) { - result.set(parentUriString, { - uri: parentUriString, - status: change.status, - staged: !!change.staged - }); - parentUri = parentUri.parent; - } else { - parentUri = undefined; - } - } - } - return result; - } - - protected toDecorator(change: GitFileChange): TreeDecoration.Data { - const data = GitFileStatus.toAbbreviation(change.status, change.staged); - const color = GitFileStatus.getColor(change.status, change.staged); - const tooltip = GitFileStatus.toString(change.status, change.staged); - let decorationData: TreeDecoration.Data = { - tailDecorations: [ - { - data, - fontData: { - color - }, - tooltip - } - ] - }; - if (this.showColors) { - decorationData = { - ...decorationData, - fontData: { - color - } - }; - } - return decorationData; - } - - protected compare(left: GitFileChange, right: GitFileChange): number { - return GitFileStatus.statusCompare(left.status, right.status); - } - - protected async handlePreferenceChange(event: PreferenceChangeEvent): Promise { - let refresh = false; - const { preferenceName, newValue } = event; - if (preferenceName === 'git.decorations.enabled') { - const enabled = !!newValue; - if (this.enabled !== enabled) { - this.enabled = enabled; - refresh = true; - } - } - if (preferenceName === 'git.decorations.colors') { - const showColors = !!newValue; - if (this.showColors !== showColors) { - this.showColors = showColors; - refresh = true; - } - } - const status = this.repositories.selectedRepositoryStatus; - if (refresh && status) { - this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree, status)); - } - } - -} diff --git a/packages/git/src/browser/git-frontend-module.ts b/packages/git/src/browser/git-frontend-module.ts index 9a6bdb1c36d43..c8e13eacab74c 100644 --- a/packages/git/src/browser/git-frontend-module.ts +++ b/packages/git/src/browser/git-frontend-module.ts @@ -20,11 +20,9 @@ import { ContainerModule, interfaces } from '@theia/core/shared/inversify'; import { CommandContribution, MenuContribution, ResourceResolver } from '@theia/core/lib/common'; import { WebSocketConnectionProvider, - LabelProviderContribution, FrontendApplicationContribution, } from '@theia/core/lib/browser'; import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; -import { NavigatorTreeDecorator } from '@theia/navigator/lib/browser'; import { Git, GitPath, GitWatcher, GitWatcherPath, GitWatcherServer, GitWatcherServerProxy, ReconnectingGitWatcherServer } from '../common'; import { GitContribution } from './git-contribution'; import { bindGitDiffModule } from './diff/git-diff-frontend-module'; @@ -32,8 +30,6 @@ import { bindGitHistoryModule } from './history/git-history-frontend-module'; import { GitResourceResolver } from './git-resource-resolver'; import { GitRepositoryProvider } from './git-repository-provider'; import { GitQuickOpenService } from './git-quick-open-service'; -import { GitUriLabelProviderContribution } from './git-uri-label-contribution'; -import { GitDecorator } from './git-decorator'; import { bindGitPreferences } from './git-preferences'; import { bindDirtyDiff } from './dirty-diff/dirty-diff-module'; import { bindBlame } from './blame/blame-module'; @@ -46,6 +42,7 @@ import { ColorContribution } from '@theia/core/lib/browser/color-application-con import { ScmHistorySupport } from '@theia/scm-extra/lib/browser/history/scm-history-widget'; import { ScmHistoryProvider } from '@theia/scm-extra/lib/browser/history'; import { GitHistorySupport } from './history/git-history-support'; +import { GitDecorationProvider } from './git-decoration-provider'; export default new ContainerModule(bind => { bindGitPreferences(bind); @@ -71,11 +68,9 @@ export default new ContainerModule(bind => { bind(GitScmProvider.Factory).toFactory(createGitScmProviderFactory); bind(GitRepositoryProvider).toSelf().inSingletonScope(); + bind(GitDecorationProvider).toSelf().inSingletonScope(); bind(GitQuickOpenService).toSelf().inSingletonScope(); - bind(LabelProviderContribution).to(GitUriLabelProviderContribution).inSingletonScope(); - bind(NavigatorTreeDecorator).to(GitDecorator).inSingletonScope(); - bind(GitCommitMessageValidator).toSelf().inSingletonScope(); bind(GitSyncService).toSelf().inSingletonScope(); diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 7cb367bc37329..1695d4a86c85d 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -765,6 +765,14 @@ export interface TimelineCommandArg { uri: string; } +export interface DecorationRequest { + readonly id: number; + readonly uri: UriComponents; +} + +export type DecorationData = [boolean, string, string, ThemeColor]; +export interface DecorationReply { [id: number]: DecorationData; } + export namespace CommentsCommandArg { export function is(arg: Object | undefined): arg is CommentsCommandArg { return !!arg && typeof arg === 'object' && 'commentControlHandle' in arg && 'commentThreadHandle' in arg && 'text' in arg && !('commentUniqueId' in arg); @@ -800,23 +808,14 @@ export interface CommentsEditCommandArg { } export interface DecorationsExt { - registerDecorationProvider(provider: theia.DecorationProvider): theia.Disposable - $provideDecoration(id: number, uri: string): Promise + registerFileDecorationProvider(provider: theia.FileDecorationProvider, pluginInfo: PluginInfo): theia.Disposable + $provideDecorations(handle: number, requests: DecorationRequest[], token: CancellationToken): Promise; } export interface DecorationsMain { - $registerDecorationProvider(id: number, provider: DecorationProvider): Promise; - $fireDidChangeDecorations(id: number, arg: undefined | string | string[]): Promise; - $dispose(id: number): Promise; -} - -export interface DecorationData { - letter?: string; - title?: string; - color?: ThemeColor; - priority?: number; - bubble?: boolean; - source?: string; + $registerDecorationProvider(handle: number): Promise; + $unregisterDecorationProvider(handle: number): void; + $onDidChange(handle: number, resources: UriComponents[] | null): void; } export interface ScmMain { @@ -927,10 +926,6 @@ export interface SourceControlResourceDecorations { readonly iconPath?: string; } -export interface DecorationProvider { - provideDecoration(uri: string): Promise; -} - export interface NotificationMain { $startProgress(options: NotificationMain.StartProgressOptions): Promise; $stopProgress(id: string): void; diff --git a/packages/plugin-ext/src/main/browser/decorations/decorations-main.ts b/packages/plugin-ext/src/main/browser/decorations/decorations-main.ts index 4bdb2ec3b8ec0..6d5c8666a6b09 100644 --- a/packages/plugin-ext/src/main/browser/decorations/decorations-main.ts +++ b/packages/plugin-ext/src/main/browser/decorations/decorations-main.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { DecorationData, - DecorationProvider, + DecorationRequest, DecorationsExt, DecorationsMain, MAIN_RPC_CONTEXT @@ -23,57 +23,124 @@ import { import { interfaces } from '@theia/core/shared/inversify'; import { Emitter } from '@theia/core/lib/common/event'; -import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; -import { Tree, TreeDecoration } from '@theia/core/lib/browser'; +import { Disposable } from '@theia/core/lib/common/disposable'; import { RPCProtocol } from '../../../common/rpc-protocol'; -import { ScmDecorationsService } from '@theia/scm/lib/browser/decorations/scm-decorations-service'; +import { UriComponents } from '../../../common/uri-components'; +import { URI as VSCodeURI } from '@theia/core/shared/vscode-uri'; +import { CancellationToken } from '@theia/core/lib/common/cancellation'; +import URI from '@theia/core/lib/common/uri'; +import { Decoration, DecorationsService } from '@theia/core/lib/browser/decorations-service'; -export class DecorationsMainImpl implements DecorationsMain, Disposable { +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// some code copied and modified from https://github.com/microsoft/vscode/blob/1.52.1/src/vs/workbench/api/browser/mainThreadDecorations.ts#L85 - private readonly proxy: DecorationsExt; - // TODO: why it is SCM specific? VS Code apis about any decorations for the explorer - private readonly scmDecorationsService: ScmDecorationsService; +class DecorationRequestsQueue { - protected readonly emitter = new Emitter<(tree: Tree) => Map>(); + private idPool = 0; + private requests = new Map(); + private resolver = new Map void>(); - protected readonly toDispose = new DisposableCollection(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private timer: any; - constructor(rpc: RPCProtocol, container: interfaces.Container) { - this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.DECORATIONS_EXT); - this.scmDecorationsService = container.get(ScmDecorationsService); + constructor( + private readonly proxy: DecorationsExt, + private readonly handle: number + ) { } - dispose(): void { - this.toDispose.dispose(); + enqueue(uri: URI, token: CancellationToken): Promise { + const id = ++this.idPool; + const result = new Promise(resolve => { + this.requests.set(id, { id, uri: VSCodeURI.parse(uri.toString()) }); + this.resolver.set(id, resolve); + this.processQueue(); + }); + token.onCancellationRequested(() => { + this.requests.delete(id); + this.resolver.delete(id); + }); + return result; } - // TODO: why it is never used? - protected readonly providers = new Map(); + private processQueue(): void { + if (typeof this.timer === 'number') { + // already queued + return; + } + this.timer = setTimeout(() => { + // make request + const requests = this.requests; + const resolver = this.resolver; + this.proxy.$provideDecorations(this.handle, [...requests.values()], CancellationToken.None).then(data => { + for (const [id, resolve] of resolver) { + resolve(data[id]); + } + }); + + // reset + this.requests = new Map(); + this.resolver = new Map(); + this.timer = undefined; + }, 0); + } +} + +export class DecorationsMainImpl implements DecorationsMain, Disposable { + + private readonly proxy: DecorationsExt; + private readonly providers = new Map, Disposable]>(); + private readonly decorationsService: DecorationsService; - async $dispose(id: number): Promise { - // TODO: What about removing decorations when a provider is gone? - this.providers.delete(id); + constructor(rpc: RPCProtocol, container: interfaces.Container) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.DECORATIONS_EXT); + this.decorationsService = container.get(DecorationsService); } - async $registerDecorationProvider(id: number, provider: DecorationProvider): Promise { - this.providers.set(id, provider); - this.toDispose.push(Disposable.create(() => this.$dispose(id))); - return id; + dispose(): void { + this.providers.forEach(value => value.forEach(v => v.dispose())); + this.providers.clear(); } - async $fireDidChangeDecorations(id: number, arg: string | string[] | undefined): Promise { - if (Array.isArray(arg)) { - const result: Map = new Map(); - for (const uri of arg) { - const data = await this.proxy.$provideDecoration(id, uri); - if (data) { - result.set(uri, data); + async $registerDecorationProvider(handle: number): Promise { + const emitter = new Emitter(); + const queue = new DecorationRequestsQueue(this.proxy, handle); + const registration = this.decorationsService.registerDecorationsProvider({ + onDidChange: emitter.event, + provideDecorations: async (uri, token) => { + const data = await queue.enqueue(uri, token); + if (!data) { + return undefined; } + const [bubble, tooltip, letter, themeColor] = data; + return { + weight: 10, + bubble: bubble ?? false, + colorId: themeColor?.id, + tooltip, + letter + }; } - this.scmDecorationsService.fireNavigatorDecorationsChanged(result); - } else if (arg) { - // TODO: why to make a remote call instead of sending decoration to `$fireDidChangeDecorations` in first place? - this.proxy.$provideDecoration(id, arg); + }); + this.providers.set(handle, [emitter, registration]); + } + + $onDidChange(handle: number, resources: UriComponents[]): void { + const providerSet = this.providers.get(handle); + if (providerSet) { + const [emitter] = providerSet; + emitter.fire(resources && resources.map(r => new URI(VSCodeURI.revive(r).toString()))); + } + } + + $unregisterDecorationProvider(handle: number): void { + const provider = this.providers.get(handle); + if (provider) { + provider.forEach(p => p.dispose()); + this.providers.delete(handle); } } } diff --git a/packages/plugin-ext/src/plugin/decorations.ts b/packages/plugin-ext/src/plugin/decorations.ts index c450cac4c3088..366fd160332fe 100644 --- a/packages/plugin-ext/src/plugin/decorations.ts +++ b/packages/plugin-ext/src/plugin/decorations.ts @@ -17,20 +17,34 @@ import * as theia from '@theia/plugin'; import { DecorationData, - DecorationProvider, + DecorationReply, + DecorationRequest, DecorationsExt, DecorationsMain, - PLUGIN_RPC_CONTEXT + PLUGIN_RPC_CONTEXT, PluginInfo } from '../common/plugin-api-rpc'; -import { Event } from '@theia/core/lib/common/event'; import { RPCProtocol } from '../common/rpc-protocol'; import { URI } from './types-impl'; -import { Disposable } from './types-impl'; +import { Disposable, FileDecoration } from './types-impl'; +import { CancellationToken } from '@theia/core/lib/common'; +import { dirname } from 'path'; + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// some code copied and modified from https://github.com/microsoft/vscode/blob/1.52.1/src/vs/workbench/api/common/extHostDecorations.ts#L39-L38 + +interface ProviderData { + provider: theia.FileDecorationProvider; + pluginInfo: PluginInfo; +} export class DecorationsExtImpl implements DecorationsExt { - static PROVIDER_ID: number = 0; + private static handle = 0; + private static maxEventSize = 250; - private readonly providersMap: Map; + private readonly providersMap: Map; private readonly proxy: DecorationsMain; constructor(readonly rpc: RPCProtocol) { @@ -38,54 +52,90 @@ export class DecorationsExtImpl implements DecorationsExt { this.providersMap = new Map(); } - registerDecorationProvider(provider: theia.DecorationProvider): Disposable { - const id = DecorationsExtImpl.PROVIDER_ID++; - provider.onDidChangeDecorations(arg => { - let argument; - if (Array.isArray(arg)) { - argument = arg.map(uri => uri.toString()); - } else if (arg) { - argument = arg.toString(); + registerFileDecorationProvider(provider: theia.FileDecorationProvider, pluginInfo: PluginInfo): theia.Disposable { + const handle = DecorationsExtImpl.handle++; + this.providersMap.set(handle, { provider, pluginInfo }); + this.proxy.$registerDecorationProvider(handle); + + const listener = provider.onDidChangeFileDecorations && provider.onDidChangeFileDecorations(e => { + if (!e) { + this.proxy.$onDidChange(handle, null); + return; } - this.proxy.$fireDidChangeDecorations(id, argument); - }); - const providerMain: DecorationProvider = { - async provideDecoration(uri: string): Promise { - const res = await provider.provideDecoration(URI.parse(uri), new CancellationTokenImpl()); - if (res) { - let color; - if (res.color) { - /* eslint-disable @typescript-eslint/no-explicit-any */ - const ob: any = res.color; - color = { id: ob.id }; + const array = Array.isArray(e) ? e : [e]; + if (array.length <= DecorationsExtImpl.maxEventSize) { + this.proxy.$onDidChange(handle, array); + return; + } + + // too many resources per event. pick one resource per folder, starting + // with parent folders + const mapped = array.map(uri => ({ uri, rank: (uri.path.match(/\//g) || []).length })); + const groups = groupBy(mapped, (a, b) => a.rank - b.rank); + const picked: URI[] = []; + outer: for (const uris of groups) { + let lastDirname: string | undefined; + for (const obj of uris) { + const myDirname = dirname(obj.uri.path); + if (lastDirname !== myDirname) { + lastDirname = myDirname; + if (picked.push(obj.uri) >= DecorationsExtImpl.maxEventSize) { + break outer; + } } - return { - letter: res.letter, - title: res.title, - color: color, - priority: res.priority, - bubble: res.bubble, - source: res.source - }; } } - }; - this.proxy.$registerDecorationProvider(id, providerMain); - this.providersMap.set(id, providerMain); + this.proxy.$onDidChange(handle, picked); + }); + return new Disposable(() => { - this.proxy.$dispose(id); + listener?.dispose(); + this.proxy.$unregisterDecorationProvider(handle); + this.providersMap.delete(handle); }); - } - async $provideDecoration(id: number, uri: string): Promise { - const provider = this.providersMap.get(id); - if (provider) { - return provider.provideDecoration(uri); + function groupBy(data: ReadonlyArray, compareFn: (a: T, b: T) => number): T[][] { + const result: T[][] = []; + let currentGroup: T[] | undefined = undefined; + for (const element of data.slice(0).sort(compareFn)) { + if (!currentGroup || compareFn(currentGroup[0], element) !== 0) { + currentGroup = [element]; + result.push(currentGroup); + } else { + currentGroup.push(element); + } + } + return result; } } -} -class CancellationTokenImpl implements theia.CancellationToken { - readonly isCancellationRequested: boolean; - readonly onCancellationRequested: Event; + async $provideDecorations(handle: number, requests: DecorationRequest[], token: CancellationToken): Promise { + if (!this.providersMap.has(handle)) { + // might have been unregistered in the meantime + return Object.create(null); + } + + const result: DecorationReply = Object.create(null); + const { provider, pluginInfo } = this.providersMap.get(handle)!; + + await Promise.all(requests.map(async request => { + try { + const { uri, id } = request; + const data = await Promise.resolve(provider.provideFileDecoration(URI.revive(uri), token)); + if (!data) { + return; + } + try { + FileDecoration.validate(data); + result[id] = [data.propagate, data.tooltip, data.badge, data.color]; + } catch (e) { + console.warn(`INVALID decoration from extension '${pluginInfo.name}': ${e}`); + } + } catch (err) { + console.error(err); + } + })); + + return result; + } } diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 2bff9cb5dde7f..8302cdc6a8186 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -133,7 +133,8 @@ import { SemanticTokensEdit, ColorThemeKind, SourceControlInputBoxValidationType, - URI + URI, + FileDecoration } from './types-impl'; import { AuthenticationExtImpl } from './authentication-ext'; import { SymbolKind } from '../common/plugin-api-rpc-model'; @@ -158,7 +159,7 @@ import { DebugExtImpl } from './node/debug/debug'; import { FileSystemExtImpl } from './file-system-ext-impl'; import { QuickPick, QuickPickItem, ResourceLabelFormatter } from '@theia/plugin'; import { ScmExtImpl } from './scm'; -import { DecorationProvider, LineChange } from '@theia/plugin'; +import { LineChange } from '@theia/plugin'; import { DecorationsExtImpl } from './decorations'; import { TextEditorExt } from './text-editor'; import { ClipboardExt } from './clipboard-ext'; @@ -432,8 +433,8 @@ export function createAPIFactory( ): PromiseLike { return notificationExt.withProgress(options, task); }, - registerDecorationProvider(provider: DecorationProvider): theia.Disposable { - return decorationsExt.registerDecorationProvider(provider); + registerFileDecorationProvider(provider: theia.FileDecorationProvider): theia.Disposable { + return decorationsExt.registerFileDecorationProvider(provider, pluginToPluginInfo(plugin)); }, registerUriHandler(handler: theia.UriHandler): theia.Disposable { // TODO ? @@ -949,7 +950,8 @@ export function createAPIFactory( SemanticTokensEdits, SemanticTokensEdit, ColorThemeKind, - SourceControlInputBoxValidationType + SourceControlInputBoxValidationType, + FileDecoration }; }; } diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 554505ecaade0..bf66f68c0a84c 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -1446,6 +1446,30 @@ export class QuickInputButtons { }; } +export class FileDecoration { + + static validate(d: FileDecoration): void { + if (d.badge && d.badge.length !== 1 && d.badge.length !== 2) { + throw new Error('The \'badge\'-property must be undefined or a short character'); + } + if (!d.color && !d.badge && !d.tooltip) { + throw new Error('The decoration is empty'); + } + } + + badge?: string; + tooltip?: string; + color?: theia.ThemeColor; + priority?: number; + propagate?: boolean; + + constructor(badge?: string, tooltip?: string, color?: ThemeColor) { + this.badge = badge; + this.tooltip = tooltip; + this.color = color; + } +} + export enum CommentMode { Editing = 0, Preview = 1 diff --git a/packages/plugin/src/theia-proposed.d.ts b/packages/plugin/src/theia-proposed.d.ts index 3f565a4524ba2..d09356b844d5a 100644 --- a/packages/plugin/src/theia-proposed.d.ts +++ b/packages/plugin/src/theia-proposed.d.ts @@ -298,11 +298,6 @@ declare module '@theia/plugin' { color?: ThemeColor; } - export interface DecorationProvider { - onDidChangeDecorations: Event; - provideDecoration(uri: Uri, token: CancellationToken): ProviderResult; - } - // #region LogLevel: https://github.com/microsoft/vscode/issues/85992 /** @@ -332,10 +327,6 @@ declare module '@theia/plugin' { // #endregion - export namespace window { - export function registerDecorationProvider(provider: DecorationProvider): Disposable; - } - // #region Tree View // copied from https://github.com/microsoft/vscode/blob/3ea5c9ddbebd8ec68e3b821f9c39c3ec785fde97/src/vs/vscode.proposed.d.ts#L1447-L1476 /** diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index de53e2abe237d..f7e04cf8c3b60 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -2970,6 +2970,71 @@ declare module '@theia/plugin' { tooltip?: string; } + /** + * A file decoration represents metadata that can be rendered with a file. + */ + export class FileDecoration { + + /** + * A very short string that represents this decoration. + */ + badge?: string; + + /** + * A human-readable tooltip for this decoration. + */ + tooltip?: string; + + /** + * The color of this decoration. + */ + color?: ThemeColor; + + /** + * A flag expressing that this decoration should be + * propagated to its parents. + */ + propagate?: boolean; + + /** + * Creates a new decoration. + * + * @param badge A letter that represents the decoration. + * @param tooltip The tooltip of the decoration. + * @param color The color of the decoration. + */ + constructor(badge?: string, tooltip?: string, color?: ThemeColor); + } + + /** + * The decoration provider interfaces defines the contract between extensions and + * file decorations. + */ + export interface FileDecorationProvider { + + /** + * An optional event to signal that decorations for one or many files have changed. + * + * *Note* that this event should be used to propagate information about children. + * + * @see [EventEmitter](#EventEmitter) + */ + onDidChangeFileDecorations?: Event; + + /** + * Provide decorations for a given uri. + * + * *Note* that this function is only called when a file gets rendered in the UI. + * This means a decoration from a descendent that propagates upwards must be signaled + * to the editor via the [onDidChangeFileDecorations](#FileDecorationProvider.onDidChangeFileDecorations)-event. + * + * @param uri The uri of the file to provide a decoration for. + * @param token A cancellation token. + * @returns A decoration or a thenable that resolves to such. + */ + provideFileDecoration(uri: Uri, token: CancellationToken): ProviderResult; + } + /** * A type of mutation that can be applied to an environment variable. */ @@ -4453,6 +4518,14 @@ declare module '@theia/plugin' { */ export function registerTerminalLinkProvider(provider: TerminalLinkProvider): void; + /** + * Register a file decoration provider. + * + * @param provider A [FileDecorationProvider](#FileDecorationProvider). + * @return A [disposable](#Disposable) that unregisters the provider. + */ + export function registerFileDecorationProvider(provider: FileDecorationProvider): Disposable; + /** * The currently active color theme as configured in the settings. The active * theme can be changed via the `workbench.colorTheme` setting. diff --git a/packages/scm/src/browser/decorations/scm-decorations-service.ts b/packages/scm/src/browser/decorations/scm-decorations-service.ts index bb79388f9b65f..5fec897763f6e 100644 --- a/packages/scm/src/browser/decorations/scm-decorations-service.ts +++ b/packages/scm/src/browser/decorations/scm-decorations-service.ts @@ -15,25 +15,15 @@ ********************************************************************************/ import { injectable, inject } from '@theia/core/shared/inversify'; -import { Emitter, Event, ResourceProvider } from '@theia/core'; +import { ResourceProvider } from '@theia/core'; import { DirtyDiffDecorator } from '../dirty-diff/dirty-diff-decorator'; import { DiffComputer } from '../dirty-diff/diff-computer'; import { ContentLines } from '../dirty-diff/content-lines'; import { EditorManager, TextEditor } from '@theia/editor/lib/browser'; import { ScmService } from '../scm-service'; -export interface DecorationData { - letter?: string; - title?: string; - color?: { id: string }; - priority?: number; - bubble?: boolean; - source?: string; -} - @injectable() export class ScmDecorationsService { - private readonly NavigatorDecorationsEmitter = new Emitter>(); private readonly diffComputer: DiffComputer; private dirtyState: boolean = true; @@ -85,12 +75,4 @@ export class ScmDecorationsService { } } } - - get onNavigatorDecorationsChanged(): Event> { - return this.NavigatorDecorationsEmitter.event; - } - - fireNavigatorDecorationsChanged(data: Map): void { - this.NavigatorDecorationsEmitter.fire(data); - } } diff --git a/packages/scm/src/browser/decorations/scm-navigator-decorator.ts b/packages/scm/src/browser/decorations/scm-navigator-decorator.ts index e75f130ded9c1..cbc68b88dcc05 100644 --- a/packages/scm/src/browser/decorations/scm-navigator-decorator.ts +++ b/packages/scm/src/browser/decorations/scm-navigator-decorator.ts @@ -21,23 +21,23 @@ import { Tree } from '@theia/core/lib/browser/tree/tree'; import { TreeDecorator, TreeDecoration } from '@theia/core/lib/browser/tree/tree-decorator'; import { DepthFirstTreeIterator } from '@theia/core/lib/browser'; import { FileStatNode } from '@theia/filesystem/lib/browser'; -import { DecorationData, ScmDecorationsService } from './scm-decorations-service'; import URI from '@theia/core/lib/common/uri'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; +import { Decoration, DecorationsService } from '@theia/core/lib/browser/decorations-service'; @injectable() export class ScmNavigatorDecorator implements TreeDecorator { readonly id = 'theia-scm-decorator'; - private decorationsMap: Map | undefined; + private decorationsMap: Map | undefined; @inject(ILogger) protected readonly logger: ILogger; @inject(ColorRegistry) protected readonly colors: ColorRegistry; - constructor(@inject(ScmDecorationsService) protected readonly decorationsService: ScmDecorationsService) { - this.decorationsService.onNavigatorDecorationsChanged(data => { + constructor(@inject(DecorationsService) protected readonly decorationsService: DecorationsService) { + this.decorationsService.onDidChangeDecorations(data => { this.decorationsMap = data; this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree)); }); @@ -61,8 +61,8 @@ export class ScmNavigatorDecorator implements TreeDecorator { return new Map(Array.from(result.entries()).map(m => [m[0], this.toDecorator(m[1])] as [string, TreeDecoration.Data])); } - protected toDecorator(change: DecorationData): TreeDecoration.Data { - const colorVariable = change.color && this.colors.toCssVariableName(change.color.id); + protected toDecorator(change: Decoration): TreeDecoration.Data { + const colorVariable = change.colorId && this.colors.toCssVariableName(change.colorId); return { tailDecorations: [ { @@ -70,7 +70,7 @@ export class ScmNavigatorDecorator implements TreeDecorator { fontData: { color: colorVariable && `var(${colorVariable})` }, - tooltip: change.title ? change.title : '' + tooltip: change.tooltip ? change.tooltip : '' } ] }; @@ -86,8 +86,8 @@ export class ScmNavigatorDecorator implements TreeDecorator { } } - protected appendContainerChanges(decorationsMap: Map): Map { - const result: Map = new Map(); + protected appendContainerChanges(decorationsMap: Map): Map { + const result: Map = new Map(); for (const [uri, data] of decorationsMap.entries()) { const uriString = uri.toString(); result.set(uriString, data); diff --git a/packages/scm/src/browser/scm-contribution.ts b/packages/scm/src/browser/scm-contribution.ts index add7d698316f3..789492f5b6645 100644 --- a/packages/scm/src/browser/scm-contribution.ts +++ b/packages/scm/src/browser/scm-contribution.ts @@ -37,6 +37,7 @@ import { ScmQuickOpenService } from './scm-quick-open-service'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorRegistry, Color } from '@theia/core/lib/browser/color-registry'; import { ScmCommand } from './scm-provider'; +import { ScmDecorationsService } from '../browser/decorations/scm-decorations-service'; export const SCM_WIDGET_FACTORY_ID = ScmWidget.ID; export const SCM_VIEW_CONTAINER_ID = 'scm-view-container'; @@ -93,6 +94,7 @@ export class ScmContribution extends AbstractViewContribution impleme @inject(CommandService) protected readonly commands: CommandService; @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; @inject(ContextKeyService) protected readonly contextKeys: ContextKeyService; + @inject(ScmDecorationsService) protected readonly scmDecorationsService: ScmDecorationsService; protected scmFocus: ContextKey; diff --git a/packages/scm/src/browser/scm-tree-widget.tsx b/packages/scm/src/browser/scm-tree-widget.tsx index 3defd8117f54e..dc016ec62b8a6 100644 --- a/packages/scm/src/browser/scm-tree-widget.tsx +++ b/packages/scm/src/browser/scm-tree-widget.tsx @@ -24,7 +24,7 @@ import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposa import { TreeWidget, TreeNode, SelectableTreeNode, TreeModel, TreeProps, NodeProps, TREE_NODE_SEGMENT_CLASS, TREE_NODE_SEGMENT_GROW_CLASS } from '@theia/core/lib/browser/tree'; import { ScmTreeModel } from './scm-tree-model'; import { MenuModelRegistry, ActionMenuNode, CompositeMenuNode, MenuPath } from '@theia/core/lib/common/menu'; -import { ScmResource, ScmResourceDecorations } from './scm-provider'; +import { ScmResource } from './scm-provider'; import { CommandRegistry } from '@theia/core/lib/common/command'; import { ContextMenuRenderer, LabelProvider, CorePreferences, DiffUris } from '@theia/core/lib/browser'; import { ScmContextKeyService } from './scm-context-key-service'; @@ -33,6 +33,8 @@ import { EditorManager, DiffNavigatorProvider } from '@theia/editor/lib/browser' import { FileStat } from '@theia/filesystem/lib/common'; import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service'; import { ScmFileChangeRootNode, ScmFileChangeGroupNode, ScmFileChangeFolderNode, ScmFileChangeNode } from './scm-tree-model'; +import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; +import { Decoration, DecorationsService } from '@theia/core/lib/browser/decorations-service'; @injectable() export class ScmTreeWidget extends TreeWidget { @@ -55,6 +57,8 @@ export class ScmTreeWidget extends TreeWidget { @inject(EditorManager) protected readonly editorManager: EditorManager; @inject(DiffNavigatorProvider) protected readonly diffNavigatorProvider: DiffNavigatorProvider; @inject(IconThemeService) protected readonly iconThemeService: IconThemeService; + @inject(DecorationsService) protected readonly decorationsService: DecorationsService; + @inject(ColorRegistry) protected readonly colors: ColorRegistry; model: ScmTreeModel; @@ -151,7 +155,8 @@ export class ScmTreeWidget extends TreeWidget { ...this.props, parentPath, sourceUri: node.sourceUri, - decorations: node.decorations, + decoration: this.decorationsService.getDecoration(new URI(node.sourceUri), true)[0], + colors: this.colors, renderExpansionToggle: () => this.renderExpansionToggle(node, props), }} />; @@ -515,13 +520,13 @@ export class ScmResourceComponent extends ScmElement render(): JSX.Element | undefined { const { hover } = this.state; - const { model, treeNode, parentPath, sourceUri, decorations, labelProvider, commands, menus, contextKeys, caption } = this.props; + const { model, treeNode, colors, parentPath, sourceUri, decoration, labelProvider, commands, menus, contextKeys, caption } = this.props; const resourceUri = new URI(sourceUri); const icon = labelProvider.getIcon(resourceUri); - const color = decorations && decorations.color || ''; - const letter = decorations && decorations.letter || ''; - const tooltip = decorations && decorations.tooltip || ''; + const color = decoration && decoration.colorId ? `var(${colors.toCssVariableName(decoration.colorId)})` : ''; + const letter = decoration && decoration.letter || ''; + const tooltip = decoration && decoration.tooltip || ''; const relativePath = parentPath.relative(resourceUri.parent); const path = relativePath ? relativePath.toString() : labelProvider.getLongName(resourceUri.parent); return