in patched-vscode/src/vs/workbench/services/label/common/labelService.ts [70:462]
description: localize('vscode.extension.contributes.resourceLabelFormatters.formatting.workspaceSuffix', "Suffix appended to the workspace label.")
}
}
}
}
}
}
});
const sepRegexp = /\//g;
const labelMatchingRegexp = /\$\{(scheme|authoritySuffix|authority|path|(query)\.(.+?))\}/g;
function hasDriveLetterIgnorePlatform(path: string): boolean {
return !!(path && path[2] === ':');
}
class ResourceLabelFormattersHandler implements IWorkbenchContribution {
private readonly formattersDisposables = new Map<ResourceLabelFormatter, IDisposable>();
constructor(@ILabelService labelService: ILabelService) {
resourceLabelFormattersExtPoint.setHandler((extensions, delta) => {
for (const added of delta.added) {
for (const untrustedFormatter of added.value) {
// We cannot trust that the formatter as it comes from an extension
// adheres to our interface, so for the required properties we fill
// in some defaults if missing.
const formatter = { ...untrustedFormatter };
if (typeof formatter.formatting.label !== 'string') {
formatter.formatting.label = '${authority}${path}';
}
if (typeof formatter.formatting.separator !== `string`) {
formatter.formatting.separator = sep;
}
if (!isProposedApiEnabled(added.description, 'contribLabelFormatterWorkspaceTooltip') && formatter.formatting.workspaceTooltip) {
formatter.formatting.workspaceTooltip = undefined; // workspaceTooltip is only proposed
}
this.formattersDisposables.set(formatter, labelService.registerFormatter(formatter));
}
}
for (const removed of delta.removed) {
for (const formatter of removed.value) {
dispose(this.formattersDisposables.get(formatter));
}
}
});
}
}
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ResourceLabelFormattersHandler, LifecyclePhase.Restored);
const FORMATTER_CACHE_SIZE = 50;
interface IStoredFormatters {
formatters?: ResourceLabelFormatter[];
i?: number;
}
export class LabelService extends Disposable implements ILabelService {
declare readonly _serviceBrand: undefined;
private formatters: ResourceLabelFormatter[];
private readonly _onDidChangeFormatters = this._register(new Emitter<IFormatterChangeEvent>({ leakWarningThreshold: 400 }));
readonly onDidChangeFormatters = this._onDidChangeFormatters.event;
private readonly storedFormattersMemento: Memento;
private readonly storedFormatters: IStoredFormatters;
private os: OperatingSystem;
private userHome: URI | undefined;
constructor(
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IPathService private readonly pathService: IPathService,
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
@IStorageService storageService: IStorageService,
@ILifecycleService lifecycleService: ILifecycleService,
) {
super();
// Find some meaningful defaults until the remote environment
// is resolved, by taking the current OS we are running in
// and by taking the local `userHome` if we run on a local
// file scheme.
this.os = OS;
this.userHome = pathService.defaultUriScheme === Schemas.file ? this.pathService.userHome({ preferLocal: true }) : undefined;
const memento = this.storedFormattersMemento = new Memento('cachedResourceLabelFormatters2', storageService);
this.storedFormatters = memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE);
this.formatters = this.storedFormatters?.formatters?.slice() || [];
// Remote environment is potentially long running
this.resolveRemoteEnvironment();
}
private async resolveRemoteEnvironment(): Promise<void> {
// OS
const env = await this.remoteAgentService.getEnvironment();
this.os = env?.os ?? OS;
// User home
this.userHome = await this.pathService.userHome();
}
findFormatting(resource: URI): ResourceLabelFormatting | undefined {
let bestResult: ResourceLabelFormatter | undefined;
for (const formatter of this.formatters) {
if (formatter.scheme === resource.scheme) {
if (!formatter.authority && (!bestResult || formatter.priority)) {
bestResult = formatter;
continue;
}
if (!formatter.authority) {
continue;
}
if (
match(formatter.authority.toLowerCase(), resource.authority.toLowerCase()) &&
(
!bestResult ||
!bestResult.authority ||
formatter.authority.length > bestResult.authority.length ||
((formatter.authority.length === bestResult.authority.length) && formatter.priority)
)
) {
bestResult = formatter;
}
}
}
return bestResult ? bestResult.formatting : undefined;
}
getUriLabel(resource: URI, options: { relative?: boolean; noPrefix?: boolean; separator?: '/' | '\\' } = {}): string {
let formatting = this.findFormatting(resource);
if (formatting && options.separator) {
// mixin separator if defined from the outside
formatting = { ...formatting, separator: options.separator };
}
const label = this.doGetUriLabel(resource, formatting, options);
// Without formatting we still need to support the separator
// as provided in options (https://github.com/microsoft/vscode/issues/130019)
if (!formatting && options.separator) {
return label.replace(sepRegexp, options.separator);
}
return label;
}
private doGetUriLabel(resource: URI, formatting?: ResourceLabelFormatting, options: { relative?: boolean; noPrefix?: boolean } = {}): string {
if (!formatting) {
return getPathLabel(resource, {
os: this.os,
tildify: this.userHome ? { userHome: this.userHome } : undefined,
relative: options.relative ? {
noPrefix: options.noPrefix,
getWorkspace: () => this.contextService.getWorkspace(),
getWorkspaceFolder: resource => this.contextService.getWorkspaceFolder(resource)
} : undefined
});
}
// Relative label
if (options.relative && this.contextService) {
let folder = this.contextService.getWorkspaceFolder(resource);
if (!folder) {
// It is possible that the resource we want to resolve the
// workspace folder for is not using the same scheme as
// the folders in the workspace, so we help by trying again
// to resolve a workspace folder by trying again with a
// scheme that is workspace contained.
const workspace = this.contextService.getWorkspace();
const firstFolder = firstOrDefault(workspace.folders);
if (firstFolder && resource.scheme !== firstFolder.uri.scheme && resource.path.startsWith(posix.sep)) {
folder = this.contextService.getWorkspaceFolder(firstFolder.uri.with({ path: resource.path }));
}
}
if (folder) {
const folderLabel = this.formatUri(folder.uri, formatting, options.noPrefix);
let relativeLabel = this.formatUri(resource, formatting, options.noPrefix);
let overlap = 0;
while (relativeLabel[overlap] && relativeLabel[overlap] === folderLabel[overlap]) {
overlap++;
}
if (!relativeLabel[overlap] || relativeLabel[overlap] === formatting.separator) {
relativeLabel = relativeLabel.substring(1 + overlap);
} else if (overlap === folderLabel.length && folder.uri.path === posix.sep) {
relativeLabel = relativeLabel.substring(overlap);
}
// always show root basename if there are multiple folders
const hasMultipleRoots = this.contextService.getWorkspace().folders.length > 1;
if (hasMultipleRoots && !options.noPrefix) {
const rootName = folder?.name ?? basenameOrAuthority(folder.uri);
relativeLabel = relativeLabel ? `${rootName} • ${relativeLabel}` : rootName;
}
return relativeLabel;
}
}
// Absolute label
return this.formatUri(resource, formatting, options.noPrefix);
}
getUriBasenameLabel(resource: URI): string {
const formatting = this.findFormatting(resource);
const label = this.doGetUriLabel(resource, formatting);
let pathLib: typeof win32 | typeof posix;
if (formatting?.separator === win32.sep) {
pathLib = win32;
} else if (formatting?.separator === posix.sep) {
pathLib = posix;
} else {
pathLib = (this.os === OperatingSystem.Windows) ? win32 : posix;
}
return pathLib.basename(label);
}
getWorkspaceLabel(workspace: IWorkspace | IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI, options?: { verbose: Verbosity }): string {
if (isWorkspace(workspace)) {
const identifier = toWorkspaceIdentifier(workspace);
if (isSingleFolderWorkspaceIdentifier(identifier) || isWorkspaceIdentifier(identifier)) {
return this.getWorkspaceLabel(identifier, options);
}
return '';
}
// Workspace: Single Folder (as URI)
if (URI.isUri(workspace)) {
return this.doGetSingleFolderWorkspaceLabel(workspace, options);
}
// Workspace: Single Folder (as workspace identifier)
if (isSingleFolderWorkspaceIdentifier(workspace)) {
return this.doGetSingleFolderWorkspaceLabel(workspace.uri, options);
}
// Workspace: Multi Root
if (isWorkspaceIdentifier(workspace)) {
return this.doGetWorkspaceLabel(workspace.configPath, options);
}
return '';
}
private doGetWorkspaceLabel(workspaceUri: URI, options?: { verbose: Verbosity }): string {
// Workspace: Untitled
if (isUntitledWorkspace(workspaceUri, this.environmentService)) {
return localize('untitledWorkspace', "Untitled (Workspace)");
}
// Workspace: Temporary
if (isTemporaryWorkspace(workspaceUri)) {
return localize('temporaryWorkspace', "Workspace");
}
// Workspace: Saved
let filename = basename(workspaceUri);
if (filename.endsWith(WORKSPACE_EXTENSION)) {
filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1);
}
let label: string;
switch (options?.verbose) {
case Verbosity.SHORT:
label = filename; // skip suffix for short label
break;
case Verbosity.LONG:
label = localize('workspaceNameVerbose', "{0} (Workspace)", this.getUriLabel(joinPath(dirname(workspaceUri), filename)));
break;
case Verbosity.MEDIUM:
default:
label = localize('workspaceName', "{0} (Workspace)", filename);
break;
}
if (options?.verbose === Verbosity.SHORT) {
return label; // skip suffix for short label
}
return this.appendWorkspaceSuffix(label, workspaceUri);
}
private doGetSingleFolderWorkspaceLabel(folderUri: URI, options?: { verbose: Verbosity }): string {
let label: string;
switch (options?.verbose) {
case Verbosity.LONG:
label = this.getUriLabel(folderUri);
break;
case Verbosity.SHORT:
case Verbosity.MEDIUM:
default:
label = basename(folderUri) || posix.sep;
break;
}
if (options?.verbose === Verbosity.SHORT) {
return label; // skip suffix for short label
}
return this.appendWorkspaceSuffix(label, folderUri);
}
getSeparator(scheme: string, authority?: string): '/' | '\\' {
const formatter = this.findFormatting(URI.from({ scheme, authority }));
return formatter?.separator || posix.sep;
}
getHostLabel(scheme: string, authority?: string): string {
const formatter = this.findFormatting(URI.from({ scheme, authority }));
return formatter?.workspaceSuffix || authority || '';
}
getHostTooltip(scheme: string, authority?: string): string | undefined {
const formatter = this.findFormatting(URI.from({ scheme, authority }));
return formatter?.workspaceTooltip;
}
registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable {
const list = this.storedFormatters.formatters ??= [];
let replace = list.findIndex(f => f.scheme === formatter.scheme && f.authority === formatter.authority);
if (replace === -1 && list.length >= FORMATTER_CACHE_SIZE) {
replace = FORMATTER_CACHE_SIZE - 1; // at max capacity, replace the last element
}
if (replace === -1) {
list.unshift(formatter);
} else {
for (let i = replace; i > 0; i--) {
list[i] = list[i - 1];
}
list[0] = formatter;
}
this.storedFormattersMemento.saveMemento();
return this.registerFormatter(formatter);
}
registerFormatter(formatter: ResourceLabelFormatter): IDisposable {
this.formatters.push(formatter);
this._onDidChangeFormatters.fire({ scheme: formatter.scheme });
return {
dispose: () => {
this.formatters = this.formatters.filter(f => f !== formatter);
this._onDidChangeFormatters.fire({ scheme: formatter.scheme });
}
};
}
private formatUri(resource: URI, formatting: ResourceLabelFormatting, forceNoTildify?: boolean): string {
let label = formatting.label.replace(labelMatchingRegexp, (match, token, qsToken, qsValue) => {
switch (token) {
case 'scheme': return resource.scheme;
case 'authority': return resource.authority;
case 'authoritySuffix': {
const i = resource.authority.indexOf('+');
return i === -1 ? resource.authority : resource.authority.slice(i + 1);
}
case 'path':
return formatting.stripPathStartingSeparator
? resource.path.slice(resource.path[0] === formatting.separator ? 1 : 0)
: resource.path;
default: {
if (qsToken === 'query') {
const { query } = resource;
if (query && query[0] === '{' && query[query.length - 1] === '}') {