diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 7babe1e48638a..c8247a8387193 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -36,7 +36,7 @@ import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingReso import { IKeybindingItem, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; -import { ILabelService, LabelRules, RegisterFormatterData } from 'vs/platform/label/common/label'; +import { ILabelService, ResourceLabelFormatter } from 'vs/platform/label/common/label'; import { INotification, INotificationHandle, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification } from 'vs/platform/notification/common/notification'; import { IProgressRunner, IProgressService } from 'vs/platform/progress/common/progress'; import { ITelemetryInfo, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -611,8 +611,8 @@ export class SimpleBulkEditService implements IBulkEditService { export class SimpleUriLabelService implements ILabelService { _serviceBrand: any; - private readonly _onDidRegisterFormatter = new Emitter(); - public readonly onDidRegisterFormatter: Event = this._onDidRegisterFormatter.event; + private readonly _onDidRegisterFormatter = new Emitter(); + public readonly onDidRegisterFormatter: Event = this._onDidRegisterFormatter.event; public getUriLabel(resource: URI, options?: { relative?: boolean, forceNoTildify?: boolean }): string { if (resource.scheme === 'file') { @@ -625,7 +625,7 @@ export class SimpleUriLabelService implements ILabelService { return ''; } - public registerFormatter(selector: string, formatter: LabelRules): IDisposable { + public registerFormatter(formatter: ResourceLabelFormatter): IDisposable { throw new Error('Not implemented'); } diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index a003d9d1dd129..5846fd3796e62 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -15,11 +15,6 @@ import { localize } from 'vs/nls'; import { isParent } from 'vs/platform/files/common/files'; import { basename } from 'vs/base/common/paths'; -export interface RegisterFormatterData { - selector: string; - formatter: LabelRules; -} - export interface ILabelService { _serviceBrand: any; /** @@ -30,21 +25,23 @@ export interface ILabelService { getUriLabel(resource: URI, options?: { relative?: boolean, noPrefix?: boolean }): string; getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWorkspace), options?: { verbose: boolean }): string; getHostLabel(): string; - registerFormatter(selector: string, formatter: LabelRules): IDisposable; - onDidRegisterFormatter: Event; + registerFormatter(formatter: ResourceLabelFormatter): IDisposable; + onDidRegisterFormatter: Event; +} + +export interface ResourceLabelFormatter { + scheme: string; + authority?: string; + formatting: ResourceLabelFormatting; } -export interface LabelRules { - uri: { - label: string; // myLabel:/${path} - separator: '/' | '\\' | ''; - tildify?: boolean; - normalizeDriveLetter?: boolean; - authorityPrefix?: string; - }; - workspace?: { - suffix: string; - }; +export interface ResourceLabelFormatting { + label: string; // myLabel:/${path} + separator: '/' | '\\' | ''; + tildify?: boolean; + normalizeDriveLetter?: boolean; + workspaceSuffix?: string; + authorityPrefix?: string; } const LABEL_SERVICE_ID = 'label'; diff --git a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts index 277b711619d4c..3ad9bff063c0c 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { FileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IFileSystemProvider, IStat, IWatchOptions, FileType, FileOverwriteOptions, FileDeleteOptions } from 'vs/platform/files/common/files'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { ExtHostContext, ExtHostFileSystemShape, IExtHostContext, IFileChangeDto, MainContext, MainThreadFileSystemShape } from '../node/extHost.protocol'; -import { LabelRules, ILabelService } from 'vs/platform/label/common/label'; +import { ResourceLabelFormatter, ILabelService } from 'vs/platform/label/common/label'; @extHostNamedCustomer(MainContext.MainThreadFileSystem) export class MainThreadFileSystem implements MainThreadFileSystemShape { @@ -39,8 +39,8 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { this._fileProvider.delete(handle); } - $setUriFormatter(selector: string, formatter: LabelRules): void { - this._labelService.registerFormatter(selector, formatter); + $setUriFormatter(formatter: ResourceLabelFormatter): void { + this._labelService.registerFormatter(formatter); } $onFileSystemChange(handle: number, changes: IFileChangeDto[]): void { diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 7ecd69fcb54cd..fdaa10614adc4 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -21,7 +21,7 @@ import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands import { ConfigurationTarget, IConfigurationData, IConfigurationModel } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { FileChangeType, FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileType, FileWriteOptions, IStat, IWatchOptions } from 'vs/platform/files/common/files'; -import { LabelRules } from 'vs/platform/label/common/label'; +import { ResourceLabelFormatter } from 'vs/platform/label/common/label'; import { LogLevel } from 'vs/platform/log/common/log'; import { IMarkerData } from 'vs/platform/markers/common/markers'; import { IPickOptions, IQuickInputButton, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; @@ -512,7 +512,7 @@ export interface IFileChangeDto { export interface MainThreadFileSystemShape extends IDisposable { $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities): void; $unregisterProvider(handle: number): void; - $setUriFormatter(scheme: string, formatter: LabelRules): void; + $setUriFormatter(formatter: ResourceLabelFormatter): void; $onFileSystemChange(handle: number, resource: IFileChangeDto[]): void; } diff --git a/src/vs/workbench/api/node/extHostFileSystem.ts b/src/vs/workbench/api/node/extHostFileSystem.ts index 6a605ba9158af..2fd8bd78205f6 100644 --- a/src/vs/workbench/api/node/extHostFileSystem.ts +++ b/src/vs/workbench/api/node/extHostFileSystem.ts @@ -12,7 +12,7 @@ import { FileChangeType, DocumentLink } from 'vs/workbench/api/node/extHostTypes import * as typeConverter from 'vs/workbench/api/node/extHostTypeConverters'; import { ExtHostLanguageFeatures } from 'vs/workbench/api/node/extHostLanguageFeatures'; import { Schemas } from 'vs/base/common/network'; -import { LabelRules } from 'vs/platform/label/common/label'; +import { ResourceLabelFormatter } from 'vs/platform/label/common/label'; import { State, StateMachine, LinkComputer } from 'vs/editor/common/modes/linkComputer'; import { commonPrefixLength } from 'vs/base/common/strings'; import { CharCode } from 'vs/base/common/charCode'; @@ -205,8 +205,8 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { }); } - setUriFormatter(scheme: string, formatter: LabelRules): void { - this._proxy.$setUriFormatter(scheme, formatter); + setUriFormatter(formatter: ResourceLabelFormatter): void { + this._proxy.$setUriFormatter(formatter); } private static _asIStat(stat: vscode.FileStat): files.IStat { diff --git a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts index b325009c33e89..a0bfe75ae605f 100644 --- a/src/vs/workbench/parts/files/electron-browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/files.contribution.ts @@ -57,16 +57,15 @@ export class OpenExplorerViewletAction extends ShowViewletAction { class FileUriLabelContribution implements IWorkbenchContribution { constructor(@ILabelService labelService: ILabelService) { - labelService.registerFormatter('file://', { - uri: { + labelService.registerFormatter({ + scheme: 'file', + formatting: { label: '${authority}${path}', separator: nativeSep, tildify: !platform.isWindows, normalizeDriveLetter: platform.isWindows, - authorityPrefix: nativeSep + nativeSep - }, - workspace: { - suffix: '' + authorityPrefix: nativeSep + nativeSep, + workspaceSuffix: '' } }); } diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index 5c550a95d1f6d..7b2180920c1eb 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -7,19 +7,68 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceContextService, IWorkspace } from 'vs/platform/workspace/common/workspace'; import { isEqual, basenameOrAuthority, basename as resourceBasename } from 'vs/base/common/resources'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { tildify, getPathLabel } from 'vs/base/common/labels'; -import { ltrim, startsWith } from 'vs/base/common/strings'; +import { ltrim } from 'vs/base/common/strings'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, WORKSPACE_EXTENSION, toWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { isParent } from 'vs/platform/files/common/files'; import { basename, dirname, join } from 'vs/base/common/paths'; import { Schemas } from 'vs/base/common/network'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; -import { ILabelService, LabelRules, RegisterFormatterData } from 'vs/platform/label/common/label'; +import { ILabelService, ResourceLabelFormatter, ResourceLabelFormatting } from 'vs/platform/label/common/label'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { match } from 'vs/base/common/glob'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; + +const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'resourceLabelFormatters', + jsonSchema: { + description: localize('vscode.extension.contributes.resourceLabelFormatters', 'Contributes resource label formatting rules.'), + type: 'array', + items: { + type: 'object', + required: ['scheme', 'formatting'], + properties: { + scheme: { + type: 'string', + description: localize('vscode.extension.contributes.resourceLabelFormatters.scheme', 'URI scheme on which to match the formatter on. For example "file". Simple glob patterns are supported.'), + }, + authority: { + type: 'string', + description: localize('vscode.extension.contributes.resourceLabelFormatters.authority', 'URI authority on which to match the formatter on. Simple glob patterns are supported.'), + }, + formatting: { + description: localize('vscode.extension.contributes.resourceLabelFormatters.formatting', "Rules for formatting uri resource labels."), + type: 'object', + properties: { + label: { + type: 'string', + description: localize('vscode.extension.contributes.resourceLabelFormatters.label', "Label rules to display. For example: myLabel:/${path}. ${path}, ${scheme} and ${authority} are supported as variables.") + }, + separator: { + type: 'string', + description: localize('vscode.extension.contributes.resourceLabelFormatters.separator', "Separator to be used in the uri label display. '/' or '\' as an example.") + }, + tildify: { + type: 'boolean', + description: localize('vscode.extension.contributes.resourceLabelFormatters.tildify', "Controls if the start of the uri label should be tildified when possible.") + }, + workspaceSuffix: { + type: 'string', + description: localize('vscode.extension.contributes.resourceLabelFormatters.formatting.workspaceSuffix', "Suffix appended to the workspace label.") + } + } + } + } + } + } +}); const sepRegexp = /\//g; const labelMatchingRegexp = /\$\{scheme\}|\$\{authority\}|\$\{path\}/g; @@ -28,12 +77,20 @@ function hasDriveLetter(path: string): boolean { return !!(isWindows && path && path[2] === ':'); } +class ResourceLabelFormattersHandler implements IWorkbenchContribution { + constructor(@ILabelService labelService: ILabelService) { + resourceLabelFormattersExtPoint.setHandler(extensions => { + extensions.forEach(extension => extension.value.forEach(formatter => labelService.registerFormatter(formatter))); + }); + } +} +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ResourceLabelFormattersHandler, LifecyclePhase.Restored); export class LabelService implements ILabelService { _serviceBrand: any; - private readonly formatters: { [prefix: string]: LabelRules } = Object.create(null); - private readonly _onDidRegisterFormatter = new Emitter(); + private formatters: ResourceLabelFormatter[] = []; + private readonly _onDidRegisterFormatter = new Emitter(); constructor( @IEnvironmentService private readonly environmentService: IEnvironmentService, @@ -41,27 +98,35 @@ export class LabelService implements ILabelService { @IWindowService private readonly windowService: IWindowService ) { } - get onDidRegisterFormatter(): Event { + get onDidRegisterFormatter(): Event { return this._onDidRegisterFormatter.event; } - findFormatter(resource: URI): LabelRules | undefined { - const path = `${resource.scheme}://${resource.authority}`; - let bestPrefix = ''; - for (let prefix in this.formatters) { - if (startsWith(path, prefix) && prefix.length > bestPrefix.length) { - bestPrefix = prefix; + findFormatting(resource: URI): ResourceLabelFormatting | undefined { + let bestResult: ResourceLabelFormatter | undefined; + + this.formatters.forEach(formatter => { + if (formatter.scheme === resource.scheme) { + if (!bestResult) { + bestResult = formatter; + return; + } + if (!formatter.authority) { + return; + } + + if (match(formatter.authority, resource.authority) && (!bestResult.authority || formatter.authority.length > bestResult.authority.length)) { + bestResult = formatter; + } } - } - if (bestPrefix.length) { - return this.formatters[bestPrefix]; - } - return undefined; + }); + + return bestResult ? bestResult.formatting : undefined; } getUriLabel(resource: URI, options: { relative?: boolean, noPrefix?: boolean } = {}): string { - const formatter = this.findFormatter(resource); - if (!formatter) { + const formatting = this.findFormatting(resource); + if (!formatting) { return getPathLabel(resource.path, this.environmentService, options.relative ? this.contextService : undefined); } @@ -72,8 +137,8 @@ export class LabelService implements ILabelService { if (isEqual(baseResource.uri, resource, !isLinux)) { relativeLabel = ''; // no label if resources are identical } else { - const baseResourceLabel = this.formatUri(baseResource.uri, formatter, options.noPrefix); - relativeLabel = ltrim(this.formatUri(resource, formatter, options.noPrefix).substring(baseResourceLabel.length), formatter.uri.separator); + const baseResourceLabel = this.formatUri(baseResource.uri, formatting, options.noPrefix); + relativeLabel = ltrim(this.formatUri(resource, formatting, options.noPrefix).substring(baseResourceLabel.length), formatting.separator); } const hasMultipleRoots = this.contextService.getWorkspace().folders.length > 1; @@ -86,7 +151,7 @@ export class LabelService implements ILabelService { } } - return this.formatUri(resource, formatter, options.noPrefix); + return this.formatUri(resource, formatting, options.noPrefix); } getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWorkspace), options?: { verbose: boolean }): string { @@ -107,8 +172,8 @@ export class LabelService implements ILabelService { return label; } - const formatter = this.findFormatter(workspace); - const suffix = formatter && formatter.workspace && (typeof formatter.workspace.suffix === 'string') ? formatter.workspace.suffix : workspace.scheme; + const formatting = this.findFormatting(workspace); + const suffix = formatting && (typeof formatting.workspaceSuffix === 'string') ? formatting.workspaceSuffix : workspace.scheme; return suffix ? `${label} (${suffix})` : label; } @@ -131,26 +196,26 @@ export class LabelService implements ILabelService { if (this.windowService) { const authority = this.windowService.getConfiguration().remoteAuthority; if (authority) { - const formatter = this.findFormatter(URI.from({ scheme: REMOTE_HOST_SCHEME, authority })); - if (formatter && formatter.workspace) { - return formatter.workspace.suffix; + const formatter = this.findFormatting(URI.from({ scheme: REMOTE_HOST_SCHEME, authority })); + if (formatter && formatter.workspaceSuffix) { + return formatter.workspaceSuffix; } } } return ''; } - registerFormatter(selector: string, formatter: LabelRules): IDisposable { - this.formatters[selector] = formatter; - this._onDidRegisterFormatter.fire({ selector, formatter }); + registerFormatter(formatter: ResourceLabelFormatter): IDisposable { + this.formatters.push(formatter); + this._onDidRegisterFormatter.fire(); return { - dispose: () => delete this.formatters[selector] + dispose: () => this.formatters = this.formatters.filter(f => f !== formatter) }; } - private formatUri(resource: URI, formatter: LabelRules, forceNoTildify?: boolean): string { - let label = formatter.uri.label.replace(labelMatchingRegexp, match => { + private formatUri(resource: URI, formatting: ResourceLabelFormatting, forceNoTildify?: boolean): string { + let label = formatting.label.replace(labelMatchingRegexp, match => { switch (match) { case '${scheme}': return resource.scheme; case '${authority}': return resource.authority; @@ -160,17 +225,17 @@ export class LabelService implements ILabelService { }); // convert \c:\something => C:\something - if (formatter.uri.normalizeDriveLetter && hasDriveLetter(label)) { + if (formatting.normalizeDriveLetter && hasDriveLetter(label)) { label = label.charAt(1).toUpperCase() + label.substr(2); } - if (formatter.uri.tildify && !forceNoTildify) { + if (formatting.tildify && !forceNoTildify) { label = tildify(label, this.environmentService.userHome); } - if (formatter.uri.authorityPrefix && resource.authority) { - label = formatter.uri.authorityPrefix + label; + if (formatting.authorityPrefix && resource.authority) { + label = formatting.authorityPrefix + label; } - return label.replace(sepRegexp, formatter.uri.separator); + return label.replace(sepRegexp, formatting.separator); } } diff --git a/src/vs/workbench/services/label/test/label.test.ts b/src/vs/workbench/services/label/test/label.test.ts index f983ba30f34ea..914b791249bcf 100644 --- a/src/vs/workbench/services/label/test/label.test.ts +++ b/src/vs/workbench/services/label/test/label.test.ts @@ -20,8 +20,9 @@ suite('URI Label', () => { }); test('file scheme', function () { - labelService.registerFormatter('file://', { - uri: { + labelService.registerFormatter({ + scheme: 'file', + formatting: { label: '${path}', separator: nativeSep, tildify: !isWindows, @@ -38,8 +39,9 @@ suite('URI Label', () => { }); test('custom scheme', function () { - labelService.registerFormatter('vscode://', { - uri: { + labelService.registerFormatter({ + scheme: 'vscode', + formatting: { label: 'LABEL/${path}/${authority}/END', separator: '/', tildify: true, @@ -50,4 +52,49 @@ suite('URI Label', () => { const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5'); assert.equal(labelService.getUriLabel(uri1, { relative: false }), 'LABEL//1/2/3/4/5/microsoft.com/END'); }); + + test('custom authority', function () { + labelService.registerFormatter({ + scheme: 'vscode', + authority: 'micro*', + formatting: { + label: 'LABEL/${path}/${authority}/END', + separator: '/' + } + }); + + const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5'); + assert.equal(labelService.getUriLabel(uri1, { relative: false }), 'LABEL//1/2/3/4/5/microsoft.com/END'); + }); + + test('mulitple authority', function () { + labelService.registerFormatter({ + scheme: 'vscode', + authority: 'micro*', + formatting: { + label: 'first', + separator: '/' + } + }); + labelService.registerFormatter({ + scheme: 'vscode', + authority: 'microsof*', + formatting: { + label: 'second', + separator: '/' + } + }); + labelService.registerFormatter({ + scheme: 'vscode', + authority: 'mi*', + formatting: { + label: 'third', + separator: '/' + } + }); + + // Make sure the most specific authority is picked + const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5'); + assert.equal(labelService.getUriLabel(uri1, { relative: false }), 'second'); + }); });