diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 255348a5218..963815fa275 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -188,7 +188,7 @@ export class ResourceLabel extends IconLabel { this.options.fileKind !== FileKind.FILE ); if (deco && this.options.fileDecorations.colors) { - iconLabelOptions.extraClasses.push(deco.labelClasses); + iconLabelOptions.extraClasses.push(deco.labelClassName); } if (deco && deco.letter && this.options.fileDecorations.badges) { iconLabelOptions.badge = { diff --git a/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts b/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts index 9559c096924..d2e31659374 100644 --- a/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts +++ b/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts @@ -7,7 +7,7 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { IMarkerService } from 'vs/platform/markers/common/markers'; -import { IResourceDecorationsService, IDecorationsProvider, IResourceDecoration } from 'vs/workbench/services/decorations/browser/decorations'; +import { IResourceDecorationsService, IDecorationsProvider, IResourceDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import URI from 'vs/base/common/uri'; import Event from 'vs/base/common/event'; @@ -30,7 +30,7 @@ class MarkersDecorationsProvider implements IDecorationsProvider { this.onDidChange = _markerService.onMarkerChanged; } - provideDecorations(resource: URI): IResourceDecoration { + provideDecorations(resource: URI): IResourceDecorationData { const markers = this._markerService.read({ resource }) .sort((a, b) => Severity.compare(a.severity, b.severity)); diff --git a/src/vs/workbench/parts/scm/electron-browser/scmFileDecorations.ts b/src/vs/workbench/parts/scm/electron-browser/scmFileDecorations.ts index cfef09270ad..dec7a16bb77 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmFileDecorations.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmFileDecorations.ts @@ -6,7 +6,7 @@ 'use strict'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IResourceDecorationsService, IDecorationsProvider, IResourceDecoration } from 'vs/workbench/services/decorations/browser/decorations'; +import { IResourceDecorationsService, IDecorationsProvider, IResourceDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource } from 'vs/workbench/services/scm/common/scm'; import URI from 'vs/base/common/uri'; @@ -61,17 +61,16 @@ class SCMDecorationsProvider implements IDecorationsProvider { this._onDidChange.fire(uris); } - provideDecorations(uri: URI): IResourceDecoration { + provideDecorations(uri: URI): IResourceDecorationData { const resource = this._data.get(uri.toString()); if (!resource) { return undefined; } return { severity: Severity.Info, - tooltip: localize('tooltip', "{0} - {1}", resource.decorations.tooltip, this._provider.label), + tooltip: localize('tooltip', "{0}, {1}", resource.decorations.tooltip, this._provider.label), color: resource.decorations.color, - letter: resource.decorations.tooltip.charAt(0), - icon: { light: resource.decorations.icon, dark: resource.decorations.iconDark }, + letter: resource.decorations.tooltip.charAt(0) }; } } diff --git a/src/vs/workbench/services/decorations/browser/decorations.ts b/src/vs/workbench/services/decorations/browser/decorations.ts index 0e229c103d6..72f06102f77 100644 --- a/src/vs/workbench/services/decorations/browser/decorations.ts +++ b/src/vs/workbench/services/decorations/browser/decorations.ts @@ -13,22 +13,26 @@ import { IDisposable } from 'vs/base/common/lifecycle'; export const IResourceDecorationsService = createDecorator('IFileDecorationsService'); -export interface IResourceDecoration { +export interface IResourceDecorationData { readonly severity: Severity; readonly color?: ColorIdentifier; readonly letter?: string; readonly tooltip?: string; - readonly icon?: { light: URI, dark: URI }; - readonly leafOnly?: boolean; +} - labelClasses?: string; - badgeClassName?: string; +export interface IResourceDecoration { + readonly _decoBrand: undefined; + readonly severity: Severity; + readonly letter?: string; + readonly tooltip?: string; + readonly labelClassName?: string; + readonly badgeClassName?: string; } export interface IDecorationsProvider { readonly label: string; readonly onDidChange: Event; - provideDecorations(uri: URI): IResourceDecoration | Thenable; + provideDecorations(uri: URI): IResourceDecorationData | Thenable; } export interface IResourceDecorationChangeEvent { diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index f489c5004d4..501c6ae6735 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -7,7 +7,7 @@ import URI from 'vs/base/common/uri'; import Severity from 'vs/base/common/severity'; import Event, { Emitter, debounceEvent, any } from 'vs/base/common/event'; -import { IResourceDecorationsService, IResourceDecoration, IResourceDecorationChangeEvent, IDecorationsProvider } from './decorations'; +import { IResourceDecorationsService, IResourceDecoration, IResourceDecorationChangeEvent, IDecorationsProvider, IResourceDecorationData } from './decorations'; import { TernarySearchTree } from 'vs/base/common/map'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { isThenable } from 'vs/base/common/async'; @@ -17,6 +17,79 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IdGenerator } from 'vs/base/common/idGenerator'; import { listActiveSelectionForeground, ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; + +class DecorationColors { + + private readonly _disposables: IDisposable[]; + private readonly _styleElement = createStyleSheet(); + private readonly _classNames = new IdGenerator('monaco-decoration-styles-'); + private readonly _classNames2ColorIds = new Map(); + + constructor( + private _themeService: IThemeService, + ) { + this._disposables = [ + this._themeService.onThemeChange(this._onThemeChange, this), + ]; + } + + dispose(): void { + dispose(this._disposables); + this._styleElement.innerHTML = ''; + } + + makeResourceDecoration(decoration: IResourceDecorationData): IResourceDecoration { + if (!decoration) { + return undefined; + } + + let { severity, letter, tooltip } = decoration; + let labelClassName, badgeClassName; + + let tuple = this._classNames2ColorIds.get(decoration.color); + + if (tuple) { + // from cache + labelClassName = tuple[0]; + badgeClassName = tuple[1]; + } else { + // new css rules + labelClassName = this._classNames.nextId(); + badgeClassName = this._classNames.nextId(); + this._classNames2ColorIds.set(decoration.color, [labelClassName, badgeClassName]); + this._createCssRules(labelClassName, badgeClassName, decoration.color); + } + + return { + _decoBrand: undefined, + severity, + letter, + tooltip, + labelClassName, + badgeClassName + }; + } + + private _onThemeChange(): void { + this._classNames2ColorIds.forEach((tuple, color) => { + const [labelClassName, badgeClassName] = tuple; + removeCSSRulesContainingSelector(labelClassName, this._styleElement); + removeCSSRulesContainingSelector(badgeClassName, this._styleElement); + this._createCssRules(labelClassName, badgeClassName, color); + }); + } + + private _createCssRules(labelClassName: string, badgeClassName: string, color: ColorIdentifier): void { + const theme = this._themeService.getTheme(); + // label + createCSSRule(`.${labelClassName}`, `color: ${theme.getColor(color)}`, this._styleElement); + createCSSRule(`.selected .${labelClassName}`, `color: ${theme.getColor(listActiveSelectionForeground)}`, this._styleElement); + + // badge + createCSSRule(`.${badgeClassName}`, `background-color: ${theme.getColor(color)}; color: ${theme.getColor(listActiveSelectionForeground)};`, this._styleElement); + } +} + class FileDecorationChangeEvent implements IResourceDecorationChangeEvent { private readonly _data = TernarySearchTree.forPaths(); @@ -49,6 +122,7 @@ class DecorationProviderWrapper { private readonly _dispoable: IDisposable; constructor( + private readonly _decorationStyles: DecorationColors, private readonly _provider: IDecorationsProvider, private readonly _emitter: Emitter ) { @@ -92,7 +166,7 @@ class DecorationProviderWrapper { const childTree = this._data.findSuperstr(key); if (childTree) { childTree.forEach(([, value]) => { - if (value && !isThenable(value) && !value.leafOnly) { + if (value && !isThenable(value)) { callback(value, true); } }); @@ -102,88 +176,27 @@ class DecorationProviderWrapper { private _fetchData(uri: URI): IResourceDecoration { - const decoOrThenable = this._provider.provideDecorations(uri); - if (!isThenable(decoOrThenable)) { + const dataOrThenable = this._provider.provideDecorations(uri); + if (!isThenable(dataOrThenable)) { // sync -> we have a result now - this._data.set(uri.toString(), decoOrThenable || null); - this._emitter.fire(uri); - return decoOrThenable; + return this._keepItem(uri, dataOrThenable); } else { // async -> we have a result soon - const request = Promise.resolve(decoOrThenable) - .then(data => { - this._data.set(uri.toString(), data || null); - this._emitter.fire(uri); - }) + const request = Promise.resolve(dataOrThenable) + .then(data => this._keepItem(uri, data)) .catch(_ => this._data.delete(uri.toString())); this._data.set(uri.toString(), request); return undefined; } } -} -class DecorationColors { - - private readonly _disposables: IDisposable[]; - private readonly _styleElement = createStyleSheet(); - private readonly _classNames = new IdGenerator('monaco-decoration-styles-'); - private readonly _classNames2ColorIds = new Map(); - - constructor( - private _themeService: IThemeService, - ) { - this._disposables = [ - this._themeService.onThemeChange(this._onThemeChange, this), - ]; - } - - dispose(): void { - dispose(this._disposables); - this._styleElement.innerHTML = ''; - } - - ensureCssStyles(decoration: IResourceDecoration): void { - if (!decoration || !decoration.color) { - return; - } - - const tuple = this._classNames2ColorIds.get(decoration.color); - if (tuple) { - // from cache - decoration.labelClasses = tuple[0]; - decoration.badgeClassName = tuple[1]; - return; - } - - let labelClassName = this._classNames.nextId(); - let badgeClassName = this._classNames.nextId(); - - this._classNames2ColorIds.set(decoration.color, [labelClassName, badgeClassName]); - decoration.labelClasses = labelClassName; - decoration.badgeClassName = badgeClassName; - - this._createCssRules(labelClassName, badgeClassName, decoration.color); - } - - private _onThemeChange(): void { - this._classNames2ColorIds.forEach((tuple, color) => { - const [labelClassName, badgeClassName] = tuple; - removeCSSRulesContainingSelector(labelClassName, this._styleElement); - removeCSSRulesContainingSelector(badgeClassName, this._styleElement); - this._createCssRules(labelClassName, badgeClassName, color); - }); - } - - private _createCssRules(labelClassName: string, badgeClassName: string, color: ColorIdentifier): void { - const theme = this._themeService.getTheme(); - // label - createCSSRule(`.${labelClassName}`, `color: ${theme.getColor(color)}`, this._styleElement); - createCSSRule(`.selected .${labelClassName}`, `color: ${theme.getColor(listActiveSelectionForeground)}`, this._styleElement); - - // badge - createCSSRule(`.${badgeClassName}`, `background-color: ${theme.getColor(color)}; color: ${theme.getColor(listActiveSelectionForeground)};`, this._styleElement); + private _keepItem(uri: URI, data: IResourceDecorationData): IResourceDecoration { + let deco = data ? this._decorationStyles.makeResourceDecoration(data) : null; + this._data.set(uri.toString(), deco); + this._emitter.fire(uri); + return deco; } } @@ -216,7 +229,11 @@ export class FileDecorationsService implements IResourceDecorationsService { registerDecortionsProvider(provider: IDecorationsProvider): IDisposable { - const wrapper = new DecorationProviderWrapper(provider, this._onDidChangeDecorationsDelayed); + const wrapper = new DecorationProviderWrapper( + this._decorationStyles, + provider, + this._onDidChangeDecorationsDelayed + ); const remove = this._data.push(wrapper); return { dispose: () => { @@ -237,13 +254,13 @@ export class FileDecorationsService implements IResourceDecorationsService { if (isChild && top === candidate) { // only bubble up color top = { + _decoBrand: undefined, severity: top.severity, - color: top.color + labelClassName: top.labelClassName }; } }); } - this._decorationStyles.ensureCssStyles(top); return top; } diff --git a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts index 3f5e430ac97..f6a004f6fb0 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { FileDecorationsService } from 'vs/workbench/services/decorations/browser/decorationsService'; -import { IDecorationsProvider, IResourceDecoration } from 'vs/workbench/services/decorations/browser/decorations'; +import { IDecorationsProvider, IResourceDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; import URI from 'vs/base/common/uri'; import Event, { toPromise } from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; @@ -34,10 +34,11 @@ suite('DecorationsService', function () { readonly onDidChange: Event = Event.None; provideDecorations(uri: URI) { callCounter += 1; - return new Promise(resolve => { + return new Promise(resolve => { setTimeout(() => resolve({ severity: Severity.Info, - color: 'someBlue' + color: 'someBlue', + letter: 'T' })); }); } @@ -52,7 +53,7 @@ suite('DecorationsService', function () { assert.equal(e.affectsResource(uri), true); // sync result - assert.deepEqual(service.getTopDecoration(uri, false), { severity: Severity.Info, color: 'someBlue' }); + assert.deepEqual(service.getTopDecoration(uri, false).letter, 'T'); assert.equal(callCounter, 1); }); }); @@ -67,12 +68,12 @@ suite('DecorationsService', function () { readonly onDidChange: Event = Event.None; provideDecorations(uri: URI) { callCounter += 1; - return { severity: Severity.Info, color: 'someBlue' }; + return { severity: Severity.Info, color: 'someBlue', letter: 'Z' }; } }); // trigger -> sync - assert.deepEqual(service.getTopDecoration(uri, false), { severity: Severity.Info, color: 'someBlue' }); + assert.deepEqual(service.getTopDecoration(uri, false).letter, 'Z'); assert.equal(callCounter, 1); }); @@ -85,12 +86,12 @@ suite('DecorationsService', function () { readonly onDidChange: Event = Event.None; provideDecorations(uri: URI) { callCounter += 1; - return { severity: Severity.Info, color: 'someBlue' }; + return { severity: Severity.Info, color: 'someBlue', letter: 'J' }; } }); // trigger -> sync - assert.deepEqual(service.getTopDecoration(uri, false), { severity: Severity.Info, color: 'someBlue' }); + assert.deepEqual(service.getTopDecoration(uri, false).letter, 'J'); assert.equal(callCounter, 1); // un-register -> ensure good event