diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index d51303ab2b6..ca88f03c537 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -177,8 +177,10 @@ export class Resource implements SourceControlResourceState { const tooltip = this.tooltip; const strikeThrough = this.strikeThrough; const faded = this.faded; + const letter = this.letter; + const color = this.color; - return { strikeThrough, faded, tooltip, light, dark }; + return { strikeThrough, faded, tooltip, light, dark, letter, color, source: 'git.resource' /*todo@joh*/ }; } get letter(): string | undefined { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 6aa10608b03..028db0ba669 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -181,6 +181,12 @@ declare module 'vscode' { source?: string; } + export interface SourceControlResourceDecorations { + source?: string; + letter?: string; + color?: ThemeColor; + } + export interface DecorationProvider { onDidChangeDecorations: Event; provideDecoration(uri: Uri, token: CancellationToken): ProviderResult; diff --git a/src/vs/workbench/api/electron-browser/mainThreadSCM.ts b/src/vs/workbench/api/electron-browser/mainThreadSCM.ts index 64253cfa2d4..126693bc736 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSCM.ts @@ -182,7 +182,7 @@ class MainThreadSCMProvider implements ISCMProvider { for (const [start, deleteCount, rawResources] of groupSlices) { const resources = rawResources.map(rawResource => { - const [handle, sourceUri, icons, tooltip, strikeThrough, faded] = rawResource; + const [handle, sourceUri, icons, tooltip, strikeThrough, faded, source, letter, color] = rawResource; const icon = icons[0]; const iconDark = icons[1] || icon; const decorations = { @@ -190,7 +190,11 @@ class MainThreadSCMProvider implements ISCMProvider { iconDark: iconDark && URI.parse(iconDark), tooltip, strikeThrough, - faded + faded, + + source, + letter, + color: color.id }; return new MainThreadSCMResource( diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index e7f484ddd5f..1d9c80b498f 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -378,8 +378,8 @@ export function createApiFactory( sampleFunction: proposedApiFunction(extension, () => { return extHostMessageService.showMessage(extension, Severity.Info, 'Hello Proposed Api!', {}, []); }), - registerDecorationProvider: proposedApiFunction(extension, (provider: vscode.DecorationProvider, source) => { - return extHostDecorations.registerDecorationProvider(provider, source); + registerDecorationProvider: proposedApiFunction(extension, (provider: vscode.DecorationProvider) => { + return extHostDecorations.registerDecorationProvider(provider, extension.id); }) }; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index dd812a65ad4..62c7bbc1f4e 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -368,7 +368,11 @@ export type SCMRawResource = [ string[] /*icons: light, dark*/, string /*tooltip*/, boolean /*strike through*/, - boolean /*faded*/ + boolean /*faded*/, + + string /*source*/, + string /*letter*/, + ThemeColor /*color*/ ]; export type SCMRawResourceSplice = [ diff --git a/src/vs/workbench/api/node/extHostSCM.ts b/src/vs/workbench/api/node/extHostSCM.ts index 310ea752f91..5eab8af95e1 100644 --- a/src/vs/workbench/api/node/extHostSCM.ts +++ b/src/vs/workbench/api/node/extHostSCM.ts @@ -244,7 +244,11 @@ class ExtHostSourceControlResourceGroup implements vscode.SourceControlResourceG const strikeThrough = r.decorations && !!r.decorations.strikeThrough; const faded = r.decorations && !!r.decorations.faded; - return [handle, sourceUri, icons, tooltip, strikeThrough, faded] as SCMRawResource; + const source = r.decorations && r.decorations.source; + const letter = r.decorations && r.decorations.letter; + const color = r.decorations && r.decorations.color; + + return [handle, sourceUri, icons, tooltip, strikeThrough, faded, source, letter, color] as SCMRawResource; }); handlesToDelete.push(...this._handlesSnapshot.splice(start, deleteCount, ...handles)); diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 87ecb23e4c6..b508b556af9 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -20,7 +20,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; -import { IDecorationsService, IResourceDecorationChangeEvent } from 'vs/workbench/services/decorations/browser/decorations'; +import { IDecorationsService, IResourceDecorationChangeEvent, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations'; import { Schemas } from 'vs/base/common/network'; import { FileKind, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files'; import { IModel } from 'vs/editor/common/editorCommon'; @@ -34,7 +34,7 @@ export interface IResourceLabel { export interface IResourceLabelOptions extends IIconLabelOptions { fileKind?: FileKind; - fileDecorations?: { colors: boolean, badges: boolean }; + fileDecorations?: { colors: boolean, badges: boolean, data?: IDecorationData }; } export class ResourceLabel extends IconLabel { @@ -199,12 +199,13 @@ export class ResourceLabel extends IconLabel { if (this.options && this.options.fileDecorations && resource) { let deco = this.decorationsService.getDecoration( resource, - this.options.fileKind !== FileKind.FILE + this.options.fileKind !== FileKind.FILE, + this.options.fileDecorations.data ); if (deco) { - if (deco.title) { - iconLabelOptions.title = `${deco.title}, ${iconLabelOptions.title}`; + if (deco.tooltip) { + iconLabelOptions.title = `${deco.tooltip}, ${iconLabelOptions.title}`; } if (this.options.fileDecorations.colors) { iconLabelOptions.extraClasses.push(deco.labelClassName); diff --git a/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts b/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts index 5630a44f061..5f9edce74d8 100644 --- a/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts +++ b/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts @@ -45,7 +45,7 @@ class MarkersDecorationsProvider implements IDecorationsProvider { return { weight: 100 * first.severity, bubble: true, - title: markers.length === 1 ? localize('tooltip.1', "1 problem in this file") : localize('tooltip.N', "{0} problems in this file", markers.length), + tooltip: markers.length === 1 ? localize('tooltip.1', "1 problem in this file") : localize('tooltip.N', "{0} problems in this file", markers.length), letter: markers.length < 10 ? markers.length.toString() : '+9', color: first.severity === Severity.Error ? editorErrorForeground : editorWarningForeground, }; diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 6b272185b6c..df7160bac14 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -450,7 +450,7 @@ class ResourceRenderer implements IRenderer { const theme = this.themeService.getTheme(); const icon = theme.type === LIGHT ? resource.decorations.icon : resource.decorations.iconDark; - template.fileLabel.setFile(resource.sourceUri, { fileDecorations: { colors: false, badges: !icon } }); + template.fileLabel.setFile(resource.sourceUri, { fileDecorations: { colors: false, badges: !icon, data: resource.decorations } }); template.actionBar.clear(); template.actionBar.context = resource; template.actionBar.push(this.scmMenus.getResourceActions(resource), { icon: true, label: false }); diff --git a/src/vs/workbench/services/decorations/browser/decorations.ts b/src/vs/workbench/services/decorations/browser/decorations.ts index 8334be55869..1ce66c0408d 100644 --- a/src/vs/workbench/services/decorations/browser/decorations.ts +++ b/src/vs/workbench/services/decorations/browser/decorations.ts @@ -16,17 +16,16 @@ export interface IDecorationData { readonly weight?: number; readonly color?: ColorIdentifier; readonly letter?: string; - readonly title?: string; + readonly tooltip?: string; readonly bubble?: boolean; readonly source?: string; } export interface IDecoration { - readonly title: string; + readonly tooltip: string; readonly labelClassName: string; readonly badgeClassName: string; - readonly data: IDecorationData[]; - update(replace: { source?: string, data?: IDecorationData }): IDecoration; + update(source?: string, data?: IDecorationData): IDecoration; } export interface IDecorationsProvider { @@ -47,5 +46,5 @@ export interface IDecorationsService { registerDecorationsProvider(provider: IDecorationsProvider): IDisposable; - getDecoration(uri: URI, includeChildren: boolean): IDecoration; + getDecoration(uri: URI, includeChildren: boolean, overwrite?: IDecorationData): IDecoration; } diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index dcb84c316ad..13215c9aa95 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -110,27 +110,26 @@ class DecorationStyles { } return { - data, labelClassName: rule.labelClassName, badgeClassName: !onlyChildren ? rule.badgeClassName : '', - title: !onlyChildren ? data.filter(d => Boolean(d.title)).map(d => d.title).join(', ') : '', - update: replace => { + tooltip: !onlyChildren ? data.filter(d => Boolean(d.tooltip)).map(d => d.tooltip).join(', ') : '', + update: (source, insert) => { let newData = data.slice(); - if (!replace.source) { + if (!source) { // add -> just append - newData.push(replace.data); + newData.push(insert); } else { // remove/replace -> require a walk for (let i = 0; i < newData.length; i++) { - if (newData[i].source === replace.source) { - if (!replace.data) { + if (newData[i].source === source) { + if (!insert) { // remove newData.splice(i, 1); i--; } else { // replace - newData[i] = replace.data; + newData[i] = insert; } } } @@ -361,22 +360,33 @@ export class FileDecorationsService implements IDecorationsService { }; } - getDecoration(uri: URI, includeChildren: boolean): IDecoration { + getDecoration(uri: URI, includeChildren: boolean, overwrite?: IDecorationData): IDecoration { let data: IDecorationData[] = []; - let onlyChildren = true; + let onlyChildren: boolean; for (let iter = this._data.iterator(), next = iter.next(); !next.done; next = iter.next()) { next.value.getOrRetrieve(uri, includeChildren, (deco, isChild) => { if (!isChild || deco.bubble) { data.push(deco); - onlyChildren = onlyChildren && isChild; + onlyChildren = onlyChildren === undefined ? isChild : onlyChildren && isChild; } }); } if (data.length === 0) { - return undefined; + // nothing, maybe overwrite data + if (overwrite) { + return this._decorationStyles.asDecoration([overwrite], onlyChildren); + } else { + return undefined; + } + } else { + // result, maybe overwrite + let result = this._decorationStyles.asDecoration(data, onlyChildren); + if (overwrite) { + return result.update(overwrite.source, overwrite); + } else { + return result; + } } - - return this._decorationStyles.asDecoration(data, onlyChildren); } } 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 ccbdc666109..fbbf81417eb 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -36,7 +36,7 @@ suite('DecorationsService', function () { return new Promise(resolve => { setTimeout(() => resolve({ color: 'someBlue', - title: 'T' + tooltip: 'T' })); }); } @@ -51,7 +51,7 @@ suite('DecorationsService', function () { assert.equal(e.affectsResource(uri), true); // sync result - assert.deepEqual(service.getDecoration(uri, false).title, 'T'); + assert.deepEqual(service.getDecoration(uri, false).tooltip, 'T'); assert.equal(callCounter, 1); }); }); @@ -66,12 +66,12 @@ suite('DecorationsService', function () { readonly onDidChange: Event = Event.None; provideDecorations(uri: URI) { callCounter += 1; - return { color: 'someBlue', title: 'Z' }; + return { color: 'someBlue', tooltip: 'Z' }; } }); // trigger -> sync - assert.deepEqual(service.getDecoration(uri, false).title, 'Z'); + assert.deepEqual(service.getDecoration(uri, false).tooltip, 'Z'); assert.equal(callCounter, 1); }); @@ -84,12 +84,12 @@ suite('DecorationsService', function () { readonly onDidChange: Event = Event.None; provideDecorations(uri: URI) { callCounter += 1; - return { color: 'someBlue', title: 'J' }; + return { color: 'someBlue', tooltip: 'J' }; } }); // trigger -> sync - assert.deepEqual(service.getDecoration(uri, false).title, 'J'); + assert.deepEqual(service.getDecoration(uri, false).tooltip, 'J'); assert.equal(callCounter, 1); // un-register -> ensure good event @@ -111,7 +111,7 @@ suite('DecorationsService', function () { onDidChange: Event.None, provideDecorations(uri: URI) { return uri.path.match(/\.txt/) - ? { title: '.txt', weight: 17 } + ? { tooltip: '.txt', weight: 17 } : undefined; } }); @@ -119,7 +119,7 @@ suite('DecorationsService', function () { let childUri = URI.parse('file:///some/path/some/file.txt'); let deco = service.getDecoration(childUri, false); - assert.equal(deco.title, '.txt'); + assert.equal(deco.tooltip, '.txt'); deco = service.getDecoration(childUri.with({ path: 'some/path/' }), true); assert.equal(deco, undefined); @@ -131,15 +131,41 @@ suite('DecorationsService', function () { onDidChange: Event.None, provideDecorations(uri: URI) { return uri.path.match(/\.txt/) - ? { title: '.txt.bubble', weight: 71, bubble: true } + ? { tooltip: '.txt.bubble', weight: 71, bubble: true } : undefined; } }); deco = service.getDecoration(childUri, false); - assert.equal(deco.title, '.txt.bubble'); + assert.equal(deco.tooltip, '.txt.bubble'); deco = service.getDecoration(childUri.with({ path: 'some/path/' }), true); - assert.equal(deco.title, ''); + assert.equal(deco.tooltip, ''); + }); + + test('Overwrite data', function () { + + let someUri = URI.parse('file:///some/path/some/file.txt'); + let deco = service.getDecoration(someUri, false); + assert.equal(deco, undefined); + + deco = service.getDecoration(someUri, false, { tooltip: 'Overwrite' }); + assert.equal(deco.tooltip, 'Overwrite'); + + let reg = service.registerDecorationsProvider({ + label: 'Test', + onDidChange: Event.None, + provideDecorations(uri: URI) { + return { tooltip: 'FromMe', source: 'foo' }; + } + }); + + deco = service.getDecoration(someUri, false); + assert.equal(deco.tooltip, 'FromMe'); + + deco = service.getDecoration(someUri, false, { source: 'foo', tooltip: 'O' }); + assert.equal(deco.tooltip, 'O'); + + reg.dispose(); }); }); diff --git a/src/vs/workbench/services/scm/common/scm.ts b/src/vs/workbench/services/scm/common/scm.ts index b659bfeedc4..a907a64cf48 100644 --- a/src/vs/workbench/services/scm/common/scm.ts +++ b/src/vs/workbench/services/scm/common/scm.ts @@ -11,6 +11,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import Event from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Command } from 'vs/editor/common/modes'; +import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry'; export interface IBaselineResourceProvider { getBaselineResource(resource: URI): TPromise; @@ -24,6 +25,10 @@ export interface ISCMResourceDecorations { tooltip?: string; strikeThrough?: boolean; faded?: boolean; + + source?: string; + letter?: string; + color?: ColorIdentifier; } export interface ISCMResourceSplice {