Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

labelService: allow extensions to contribute label formatting rules #66352

Merged
merged 7 commits into from
Jan 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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