Skip to content

Commit

Permalink
Merge pull request #66352 from Microsoft/isidorn/contributes-labels
Browse files Browse the repository at this point in the history
labelService: allow extensions to contribute label formatting rules
  • Loading branch information
isidorn authored Jan 18, 2019
2 parents 13affa0 + 39bb8bb commit 2494a5f
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 77 deletions.
8 changes: 4 additions & 4 deletions src/vs/editor/standalone/browser/simpleServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -611,8 +611,8 @@ export class SimpleBulkEditService implements IBulkEditService {
export class SimpleUriLabelService implements ILabelService {
_serviceBrand: any;

private readonly _onDidRegisterFormatter = new Emitter<RegisterFormatterData>();
public readonly onDidRegisterFormatter: Event<RegisterFormatterData> = this._onDidRegisterFormatter.event;
private readonly _onDidRegisterFormatter = new Emitter<void>();
public readonly onDidRegisterFormatter: Event<void> = this._onDidRegisterFormatter.event;

public getUriLabel(resource: URI, options?: { relative?: boolean, forceNoTildify?: boolean }): string {
if (resource.scheme === 'file') {
Expand All @@ -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');
}

Expand Down
33 changes: 15 additions & 18 deletions src/vs/platform/label/common/label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand All @@ -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<RegisterFormatterData>;
registerFormatter(formatter: ResourceLabelFormatter): IDisposable;
onDidRegisterFormatter: Event<void>;
}

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';
Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/api/node/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/api/node/extHostFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: ''
}
});
}
Expand Down
139 changes: 102 additions & 37 deletions src/vs/workbench/services/label/common/labelService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResourceLabelFormatter[]>({
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;
Expand All @@ -28,40 +77,56 @@ 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<IWorkbenchContributionsRegistry>(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<RegisterFormatterData>();
private formatters: ResourceLabelFormatter[] = [];
private readonly _onDidRegisterFormatter = new Emitter<void>();

constructor(
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IWindowService private readonly windowService: IWindowService
) { }

get onDidRegisterFormatter(): Event<RegisterFormatterData> {
get onDidRegisterFormatter(): Event<void> {
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);
}

Expand All @@ -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;
Expand All @@ -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 {
Expand All @@ -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;
}

Expand All @@ -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;
Expand All @@ -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);
}
}
Loading

0 comments on commit 2494a5f

Please sign in to comment.