diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 0c5dba2a3e5..e8485b673fc 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -168,4 +168,26 @@ declare module 'vscode' { */ export function registerDiffInformationCommand(command: string, callback: (diff: LineChange[], ...args: any[]) => any, thisArg?: any): Disposable; } + + //#region decorations + + //todo@joh -> make class + export interface DecorationData { + priority?: number; + title?: string; + abbreviation?: string; + color?: ThemeColor; + opacity?: number; + } + + export interface DecorationProvider { + onDidChangeDecorations: Event; + provideDecoration(uri: Uri, token: CancellationToken): ProviderResult; + } + + export namespace window { + export function registerDecorationProvider(provider: DecorationProvider, label: string): Disposable; + } + + //#endregion } diff --git a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts index be065d33308..2940cda2e51 100644 --- a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts @@ -19,6 +19,7 @@ import './mainThreadCommands'; import './mainThreadConfiguration'; import './mainThreadCredentials'; import './mainThreadDebugService'; +import './mainThreadDecorations'; import './mainThreadDiagnostics'; import './mainThreadDialogs'; import './mainThreadDocumentContentProviders'; diff --git a/src/vs/workbench/api/electron-browser/mainThreadDecorations.ts b/src/vs/workbench/api/electron-browser/mainThreadDecorations.ts new file mode 100644 index 00000000000..1dd8523ff44 --- /dev/null +++ b/src/vs/workbench/api/electron-browser/mainThreadDecorations.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import URI from 'vs/base/common/uri'; +import { Emitter } from 'vs/base/common/event'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { ExtHostContext, MainContext, IExtHostContext, MainThreadDecorationsShape, ExtHostDecorationsShape } from '../node/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; + +@extHostNamedCustomer(MainContext.MainThreadDecorations) +export class MainThreadDecorations implements MainThreadDecorationsShape { + + private readonly _provider = new Map, IDisposable]>(); + private readonly _proxy: ExtHostDecorationsShape; + + constructor( + context: IExtHostContext, + @IDecorationsService private readonly _decorationsService: IDecorationsService + ) { + this._proxy = context.get(ExtHostContext.ExtHostDecorations); + } + + dispose() { + this._provider.forEach(value => dispose(value)); + this._provider.clear(); + } + + $registerDecorationProvider(handle: number, label: string): void { + let emitter = new Emitter(); + let registration = this._decorationsService.registerDecorationsProvider({ + label, + onDidChange: emitter.event, + provideDecorations: (uri) => { + return this._proxy.$providerDecorations(handle, uri).then(data => { + const [weight, title, letter, opacity, themeColor] = data; + return { + weight: weight || 0, + title, + letter, + opacity, + color: themeColor && themeColor.id + }; + }); + } + }); + this._provider.set(handle, [emitter, registration]); + } + + $onDidChange(handle: number, resources: URI[]): void { + const [emitter] = this._provider.get(handle); + emitter.fire(resources); + } + + $unregisterDecorationProvider(handle: number): void { + if (this._provider.has(handle)) { + dispose(this._provider.get(handle)); + this._provider.delete(handle); + } + } +} diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 32af7291dfc..73c966c463e 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -55,6 +55,7 @@ import { ProxyIdentifier } from 'vs/workbench/services/thread/common/threadServi import { ExtHostDialogs } from 'vs/workbench/api/node/extHostDialogs'; import { ExtHostFileSystem } from 'vs/workbench/api/node/extHostFileSystem'; import { FileChangeType, FileType } from 'vs/platform/files/common/files'; +import { ExtHostDecorations } from 'vs/workbench/api/node/extHostDecorations'; export interface IExtensionApiFactory { (extension: IExtensionDescription): typeof vscode; @@ -83,6 +84,7 @@ export function createApiFactory( // Addressable instances const extHostHeapService = threadService.set(ExtHostContext.ExtHostHeapService, new ExtHostHeapService()); + const extHostDecorations = threadService.set(ExtHostContext.ExtHostDecorations, new ExtHostDecorations(threadService)); const extHostDocumentsAndEditors = threadService.set(ExtHostContext.ExtHostDocumentsAndEditors, new ExtHostDocumentsAndEditors(threadService)); const extHostDocuments = threadService.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(threadService, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = threadService.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(threadService, extHostDocumentsAndEditors)); @@ -376,6 +378,9 @@ export function createApiFactory( sampleFunction: proposedApiFunction(extension, () => { return extHostMessageService.showMessage(extension, Severity.Info, 'Hello Proposed Api!', {}, []); }), + registerDecorationProvider: proposedApiFunction(extension, (provider: vscode.DecorationProvider, label: string) => { + return extHostDecorations.registerDecorationProvider(provider, label); + }) }; // namespace: workspace diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index d07165fce5a..eee71c3f13d 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -135,6 +135,12 @@ export interface MainThreadDiaglogsShape extends IDisposable { $showSaveDialog(options: MainThreadDialogSaveOptions): TPromise; } +export interface MainThreadDecorationsShape extends IDisposable { + $registerDecorationProvider(handle: number, label: string): void; + $unregisterDecorationProvider(handle: number): void; + $onDidChange(handle: number, resources: URI[]): void; +} + export interface MainThreadDocumentContentProvidersShape extends IDisposable { $registerTextContentProvider(handle: number, scheme: string): void; $unregisterTextContentProvider(handle: number): void; @@ -596,6 +602,13 @@ export interface ExtHostDebugServiceShape { $acceptDebugSessionCustomEvent(id: DebugSessionUUID, type: string, name: string, event: any): void; } + +export type DecorationData = [number, string, string, number, ThemeColor]; + +export interface ExtHostDecorationsShape { + $providerDecorations(handle: number, uri: URI): TPromise; +} + export interface ExtHostCredentialsShape { } @@ -609,6 +622,7 @@ export const MainContext = { MainThreadCommands: createMainId('MainThreadCommands'), MainThreadConfiguration: createMainId('MainThreadConfiguration'), MainThreadDebugService: createMainId('MainThreadDebugService'), + MainThreadDecorations: createMainId('MainThreadDecorations'), MainThreadDiagnostics: createMainId('MainThreadDiagnostics'), MainThreadDialogs: createMainId('MainThreadDiaglogs'), MainThreadDocuments: createMainId('MainThreadDocuments'), @@ -640,6 +654,7 @@ export const ExtHostContext = { ExtHostConfiguration: createExtId('ExtHostConfiguration'), ExtHostDiagnostics: createExtId('ExtHostDiagnostics'), ExtHostDebugService: createExtId('ExtHostDebugService'), + ExtHostDecorations: createExtId('ExtHostDecorations'), ExtHostDocumentsAndEditors: createExtId('ExtHostDocumentsAndEditors'), ExtHostDocuments: createExtId('ExtHostDocuments'), ExtHostDocumentContentProviders: createExtId('ExtHostDocumentContentProviders'), diff --git a/src/vs/workbench/api/node/extHostDecorations.ts b/src/vs/workbench/api/node/extHostDecorations.ts new file mode 100644 index 00000000000..05b93b96f33 --- /dev/null +++ b/src/vs/workbench/api/node/extHostDecorations.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as vscode from 'vscode'; +import URI from 'vs/base/common/uri'; +import { MainContext, IMainContext, ExtHostDecorationsShape, MainThreadDecorationsShape, DecorationData } from 'vs/workbench/api/node/extHost.protocol'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { Disposable } from 'vs/workbench/api/node/extHostTypes'; +import { asWinJsPromise } from 'vs/base/common/async'; + +export class ExtHostDecorations implements ExtHostDecorationsShape { + + private static _handlePool = 0; + + private readonly _provider = new Map(); + private readonly _proxy: MainThreadDecorationsShape; + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.get(MainContext.MainThreadDecorations); + } + + registerDecorationProvider(provider: vscode.DecorationProvider, label: string): vscode.Disposable { + const handle = ExtHostDecorations._handlePool++; + this._provider.set(handle, provider); + this._proxy.$registerDecorationProvider(handle, label); + + const listener = provider.onDidChangeDecorations(e => { + this._proxy.$onDidChange(handle, Array.isArray(e) ? e : [e]); + }); + + return new Disposable(() => { + listener.dispose(); + this._proxy.$unregisterDecorationProvider(handle); + this._provider.delete(handle); + }); + } + + $providerDecorations(handle: number, uri: URI): TPromise { + const provider = this._provider.get(handle); + return asWinJsPromise(token => provider.provideDecoration(uri, token)).then(data => { + return [data.priority, data.title, data.abbreviation, data.opacity, data.color]; + }); + } +} diff --git a/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts b/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts index d9a6ce584da..9dd0ce444fb 100644 --- a/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts +++ b/src/vs/workbench/parts/markers/browser/markersFileDecorations.ts @@ -44,6 +44,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), letter: markers.length.toString(), color: first.severity === Severity.Error ? editorErrorForeground : editorWarningForeground, diff --git a/src/vs/workbench/parts/scm/electron-browser/scmFileDecorations.ts b/src/vs/workbench/parts/scm/electron-browser/scmFileDecorations.ts index e69496fafdb..23e0d15fdae 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmFileDecorations.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmFileDecorations.ts @@ -65,7 +65,8 @@ class SCMDecorationsProvider implements IDecorationsProvider { return undefined; } return { - weight: 100 - resource.decorations.tooltip.charAt(0).toLowerCase().charCodeAt(0), + bubble: true, + weight: 255 - resource.decorations.tooltip.charAt(0).toLowerCase().charCodeAt(0), title: localize('tooltip', "{0}, {1}", resource.decorations.tooltip, this._provider.label), color: resource.decorations.color, 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 25401fd795a..6a5b7993205 100644 --- a/src/vs/workbench/services/decorations/browser/decorations.ts +++ b/src/vs/workbench/services/decorations/browser/decorations.ts @@ -18,6 +18,7 @@ export interface IDecorationData { readonly opacity?: number; readonly letter?: string; readonly title?: string; + readonly bubble?: boolean; } export interface IDecoration { diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 9792da756f4..ddbf12c6133 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -347,6 +347,12 @@ export class FileDecorationsService implements IDecorationsService { this._onDidChangeDecorationsDelayed ); const remove = this._data.push(wrapper); + + this._onDidChangeDecorations.fire({ + // everything might have changed + affectsResource() { return true; } + }); + return { dispose: () => { // fire event that says 'yes' for any resource @@ -363,9 +369,10 @@ export class FileDecorationsService implements IDecorationsService { let onlyChildren = true; for (let iter = this._data.iterator(), next = iter.next(); !next.done; next = iter.next()) { next.value.getOrRetrieve(uri, includeChildren, (deco, isChild) => { - // top = FileDecorationsService._pickBest(top, candidate); - data.push(deco); - onlyChildren = onlyChildren && isChild; + if (!isChild || deco.bubble) { + data.push(deco); + onlyChildren = onlyChildren && isChild; + } }); } 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 21708a3b55d..721051fb31a 100644 --- a/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts +++ b/src/vs/workbench/services/decorations/test/browser/decorationsService.test.ts @@ -103,4 +103,43 @@ suite('DecorationsService', function () { reg.dispose(); assert.equal(didSeeEvent, true); }); + + test('No default bubbling', function () { + + let reg = service.registerDecorationsProvider({ + label: 'Test', + onDidChange: Event.None, + provideDecorations(uri: URI) { + return uri.path.match(/\.txt/) + ? { title: '.txt' } + : undefined; + } + }); + + let childUri = URI.parse('file:///some/path/some/file.txt'); + + let deco = service.getDecoration(childUri, false); + assert.equal(deco.title, '.txt'); + + deco = service.getDecoration(childUri.with({ path: 'some/path/' }), true); + assert.equal(deco, undefined); + reg.dispose(); + + // bubble + reg = service.registerDecorationsProvider({ + label: 'Test', + onDidChange: Event.None, + provideDecorations(uri: URI) { + return uri.path.match(/\.txt/) + ? { title: '.txt.bubble', bubble: true } + : undefined; + } + }); + + deco = service.getDecoration(childUri, false); + assert.equal(deco.title, '.txt.bubble'); + + deco = service.getDecoration(childUri.with({ path: 'some/path/' }), true); + assert.equal(deco.title, '.txt.bubble'); + }); });