Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/label/common/labelService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { localize } from '../../../../nls.js';
7
import { URI } from '../../../../base/common/uri.js';
8
import { IDisposable, Disposable, dispose } from '../../../../base/common/lifecycle.js';
9
import { posix, sep, win32 } from '../../../../base/common/path.js';
10
import { Emitter } from '../../../../base/common/event.js';
11
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from '../../../common/contributions.js';
12
import { Registry } from '../../../../platform/registry/common/platform.js';
13
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
14
import { IWorkspaceContextService, IWorkspace, isWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier, toWorkspaceIdentifier, WORKSPACE_EXTENSION, isUntitledWorkspace, isTemporaryWorkspace } from '../../../../platform/workspace/common/workspace.js';
15
import { basenameOrAuthority, basename, joinPath, dirname } from '../../../../base/common/resources.js';
16
import { tildify, getPathLabel } from '../../../../base/common/labels.js';
17
import { ILabelService, ResourceLabelFormatter, ResourceLabelFormatting, IFormatterChangeEvent, Verbosity } from '../../../../platform/label/common/label.js';
18
import { ExtensionsRegistry } from '../../extensions/common/extensionsRegistry.js';
19
import { match } from '../../../../base/common/glob.js';
20
import { ILifecycleService, LifecyclePhase } from '../../lifecycle/common/lifecycle.js';
21
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
22
import { IPathService } from '../../path/common/pathService.js';
23
import { isProposedApiEnabled } from '../../extensions/common/extensions.js';
24
import { OperatingSystem, OS } from '../../../../base/common/platform.js';
25
import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js';
26
import { Schemas } from '../../../../base/common/network.js';
27
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
28
import { Memento } from '../../../common/memento.js';
29
30
const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoint<ResourceLabelFormatter[]>({
31
extensionPoint: 'resourceLabelFormatters',
32
jsonSchema: {
33
description: localize('vscode.extension.contributes.resourceLabelFormatters', 'Contributes resource label formatting rules.'),
34
type: 'array',
35
items: {
36
type: 'object',
37
required: ['scheme', 'formatting'],
38
properties: {
39
scheme: {
40
type: 'string',
41
description: localize('vscode.extension.contributes.resourceLabelFormatters.scheme', 'URI scheme on which to match the formatter on. For example "file". Simple glob patterns are supported.'),
42
},
43
authority: {
44
type: 'string',
45
description: localize('vscode.extension.contributes.resourceLabelFormatters.authority', 'URI authority on which to match the formatter on. Simple glob patterns are supported.'),
46
},
47
formatting: {
48
description: localize('vscode.extension.contributes.resourceLabelFormatters.formatting', "Rules for formatting uri resource labels."),
49
type: 'object',
50
properties: {
51
label: {
52
type: 'string',
53
description: localize('vscode.extension.contributes.resourceLabelFormatters.label', "Label rules to display. For example: myLabel:/${path}. ${path}, ${scheme}, ${authority} and ${authoritySuffix} are supported as variables.")
54
},
55
separator: {
56
type: 'string',
57
description: localize('vscode.extension.contributes.resourceLabelFormatters.separator', "Separator to be used in the uri label display. '/' or '\' as an example.")
58
},
59
stripPathStartingSeparator: {
60
type: 'boolean',
61
description: localize('vscode.extension.contributes.resourceLabelFormatters.stripPathStartingSeparator', "Controls whether `${path}` substitutions should have starting separator characters stripped.")
62
},
63
tildify: {
64
type: 'boolean',
65
description: localize('vscode.extension.contributes.resourceLabelFormatters.tildify', "Controls if the start of the uri label should be tildified when possible.")
66
},
67
workspaceSuffix: {
68
type: 'string',
69
description: localize('vscode.extension.contributes.resourceLabelFormatters.formatting.workspaceSuffix', "Suffix appended to the workspace label.")
70
}
71
}
72
}
73
}
74
}
75
}
76
});
77
78
const sepRegexp = /\//g;
79
const labelMatchingRegexp = /\$\{(scheme|authoritySuffix|authority|path|(query)\.(.+?))\}/g;
80
81
function hasDriveLetterIgnorePlatform(path: string): boolean {
82
return !!(path && path[2] === ':');
83
}
84
85
class ResourceLabelFormattersHandler implements IWorkbenchContribution {
86
87
private readonly formattersDisposables = new Map<ResourceLabelFormatter, IDisposable>();
88
89
constructor(@ILabelService labelService: ILabelService) {
90
resourceLabelFormattersExtPoint.setHandler((extensions, delta) => {
91
for (const added of delta.added) {
92
for (const untrustedFormatter of added.value) {
93
94
// We cannot trust that the formatter as it comes from an extension
95
// adheres to our interface, so for the required properties we fill
96
// in some defaults if missing.
97
98
const formatter = { ...untrustedFormatter };
99
if (typeof formatter.formatting.label !== 'string') {
100
formatter.formatting.label = '${authority}${path}';
101
}
102
if (typeof formatter.formatting.separator !== `string`) {
103
formatter.formatting.separator = sep;
104
}
105
106
if (!isProposedApiEnabled(added.description, 'contribLabelFormatterWorkspaceTooltip') && formatter.formatting.workspaceTooltip) {
107
formatter.formatting.workspaceTooltip = undefined; // workspaceTooltip is only proposed
108
}
109
110
this.formattersDisposables.set(formatter, labelService.registerFormatter(formatter));
111
}
112
}
113
114
for (const removed of delta.removed) {
115
for (const formatter of removed.value) {
116
dispose(this.formattersDisposables.get(formatter));
117
}
118
}
119
});
120
}
121
}
122
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ResourceLabelFormattersHandler, LifecyclePhase.Restored);
123
124
const FORMATTER_CACHE_SIZE = 50;
125
126
interface IStoredFormatters {
127
formatters?: ResourceLabelFormatter[];
128
i?: number;
129
}
130
131
export class LabelService extends Disposable implements ILabelService {
132
133
declare readonly _serviceBrand: undefined;
134
135
private formatters: ResourceLabelFormatter[];
136
137
private readonly _onDidChangeFormatters = this._register(new Emitter<IFormatterChangeEvent>({ leakWarningThreshold: 400 }));
138
readonly onDidChangeFormatters = this._onDidChangeFormatters.event;
139
140
private readonly storedFormattersMemento: Memento;
141
private readonly storedFormatters: IStoredFormatters;
142
private os: OperatingSystem;
143
private userHome: URI | undefined;
144
145
constructor(
146
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
147
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
148
@IPathService private readonly pathService: IPathService,
149
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
150
@IStorageService storageService: IStorageService,
151
@ILifecycleService lifecycleService: ILifecycleService,
152
) {
153
super();
154
155
// Find some meaningful defaults until the remote environment
156
// is resolved, by taking the current OS we are running in
157
// and by taking the local `userHome` if we run on a local
158
// file scheme.
159
this.os = OS;
160
this.userHome = pathService.defaultUriScheme === Schemas.file ? this.pathService.userHome({ preferLocal: true }) : undefined;
161
162
const memento = this.storedFormattersMemento = new Memento('cachedResourceLabelFormatters2', storageService);
163
this.storedFormatters = memento.getMemento(StorageScope.PROFILE, StorageTarget.MACHINE);
164
this.formatters = this.storedFormatters?.formatters?.slice() || [];
165
166
// Remote environment is potentially long running
167
this.resolveRemoteEnvironment();
168
}
169
170
private async resolveRemoteEnvironment(): Promise<void> {
171
172
// OS
173
const env = await this.remoteAgentService.getEnvironment();
174
this.os = env?.os ?? OS;
175
176
// User home
177
this.userHome = await this.pathService.userHome();
178
}
179
180
findFormatting(resource: URI): ResourceLabelFormatting | undefined {
181
let bestResult: ResourceLabelFormatter | undefined;
182
183
for (const formatter of this.formatters) {
184
if (formatter.scheme === resource.scheme) {
185
if (!formatter.authority && (!bestResult || formatter.priority)) {
186
bestResult = formatter;
187
continue;
188
}
189
190
if (!formatter.authority) {
191
continue;
192
}
193
194
if (
195
match(formatter.authority.toLowerCase(), resource.authority.toLowerCase()) &&
196
(
197
!bestResult ||
198
!bestResult.authority ||
199
formatter.authority.length > bestResult.authority.length ||
200
((formatter.authority.length === bestResult.authority.length) && formatter.priority)
201
)
202
) {
203
bestResult = formatter;
204
}
205
}
206
}
207
208
return bestResult ? bestResult.formatting : undefined;
209
}
210
211
getUriLabel(resource: URI, options: { relative?: boolean; noPrefix?: boolean; separator?: '/' | '\\'; appendWorkspaceSuffix?: boolean } = {}): string {
212
let formatting = this.findFormatting(resource);
213
if (formatting && options.separator) {
214
// mixin separator if defined from the outside
215
formatting = { ...formatting, separator: options.separator };
216
}
217
218
let label = this.doGetUriLabel(resource, formatting, options);
219
220
// Without formatting we still need to support the separator
221
// as provided in options (https://github.com/microsoft/vscode/issues/130019)
222
if (!formatting && options.separator) {
223
label = label.replace(sepRegexp, options.separator);
224
}
225
226
if (options.appendWorkspaceSuffix && formatting?.workspaceSuffix) {
227
label = this.appendWorkspaceSuffix(label, resource);
228
}
229
230
return label;
231
}
232
233
private doGetUriLabel(resource: URI, formatting?: ResourceLabelFormatting, options: { relative?: boolean; noPrefix?: boolean } = {}): string {
234
if (!formatting) {
235
return getPathLabel(resource, {
236
os: this.os,
237
tildify: this.userHome ? { userHome: this.userHome } : undefined,
238
relative: options.relative ? {
239
noPrefix: options.noPrefix,
240
getWorkspace: () => this.contextService.getWorkspace(),
241
getWorkspaceFolder: resource => this.contextService.getWorkspaceFolder(resource)
242
} : undefined
243
});
244
}
245
246
// Relative label
247
if (options.relative && this.contextService) {
248
let folder = this.contextService.getWorkspaceFolder(resource);
249
if (!folder) {
250
251
// It is possible that the resource we want to resolve the
252
// workspace folder for is not using the same scheme as
253
// the folders in the workspace, so we help by trying again
254
// to resolve a workspace folder by trying again with a
255
// scheme that is workspace contained.
256
257
const workspace = this.contextService.getWorkspace();
258
const firstFolder = workspace.folders.at(0);
259
if (firstFolder && resource.scheme !== firstFolder.uri.scheme && resource.path.startsWith(posix.sep)) {
260
folder = this.contextService.getWorkspaceFolder(firstFolder.uri.with({ path: resource.path }));
261
}
262
}
263
264
if (folder) {
265
const folderLabel = this.formatUri(folder.uri, formatting, options.noPrefix);
266
267
let relativeLabel = this.formatUri(resource, formatting, options.noPrefix);
268
let overlap = 0;
269
while (relativeLabel[overlap] && relativeLabel[overlap] === folderLabel[overlap]) {
270
overlap++;
271
}
272
273
if (!relativeLabel[overlap] || relativeLabel[overlap] === formatting.separator) {
274
relativeLabel = relativeLabel.substring(1 + overlap);
275
} else if (overlap === folderLabel.length && folder.uri.path === posix.sep) {
276
relativeLabel = relativeLabel.substring(overlap);
277
}
278
279
// always show root basename if there are multiple folders
280
const hasMultipleRoots = this.contextService.getWorkspace().folders.length > 1;
281
if (hasMultipleRoots && !options.noPrefix) {
282
const rootName = folder?.name ?? basenameOrAuthority(folder.uri);
283
relativeLabel = relativeLabel ? `${rootName} • ${relativeLabel}` : rootName;
284
}
285
286
return relativeLabel;
287
}
288
}
289
290
// Absolute label
291
return this.formatUri(resource, formatting, options.noPrefix);
292
}
293
294
getUriBasenameLabel(resource: URI): string {
295
const formatting = this.findFormatting(resource);
296
const label = this.doGetUriLabel(resource, formatting);
297
298
let pathLib: typeof win32 | typeof posix;
299
if (formatting?.separator === win32.sep) {
300
pathLib = win32;
301
} else if (formatting?.separator === posix.sep) {
302
pathLib = posix;
303
} else {
304
pathLib = (this.os === OperatingSystem.Windows) ? win32 : posix;
305
}
306
307
return pathLib.basename(label);
308
}
309
310
getWorkspaceLabel(workspace: IWorkspace | IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI, options?: { verbose: Verbosity }): string {
311
if (isWorkspace(workspace)) {
312
const identifier = toWorkspaceIdentifier(workspace);
313
if (isSingleFolderWorkspaceIdentifier(identifier) || isWorkspaceIdentifier(identifier)) {
314
return this.getWorkspaceLabel(identifier, options);
315
}
316
317
return '';
318
}
319
320
// Workspace: Single Folder (as URI)
321
if (URI.isUri(workspace)) {
322
return this.doGetSingleFolderWorkspaceLabel(workspace, options);
323
}
324
325
// Workspace: Single Folder (as workspace identifier)
326
if (isSingleFolderWorkspaceIdentifier(workspace)) {
327
return this.doGetSingleFolderWorkspaceLabel(workspace.uri, options);
328
}
329
330
// Workspace: Multi Root
331
if (isWorkspaceIdentifier(workspace)) {
332
return this.doGetWorkspaceLabel(workspace.configPath, options);
333
}
334
335
return '';
336
}
337
338
private doGetWorkspaceLabel(workspaceUri: URI, options?: { verbose: Verbosity }): string {
339
340
// Workspace: Untitled
341
if (isUntitledWorkspace(workspaceUri, this.environmentService)) {
342
return localize('untitledWorkspace', "Untitled (Workspace)");
343
}
344
345
// Workspace: Temporary
346
if (isTemporaryWorkspace(workspaceUri)) {
347
return localize('temporaryWorkspace', "Workspace");
348
}
349
350
// Workspace: Saved
351
let filename = basename(workspaceUri);
352
if (filename.endsWith(WORKSPACE_EXTENSION)) {
353
filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1);
354
}
355
356
let label: string;
357
switch (options?.verbose) {
358
case Verbosity.SHORT:
359
label = filename; // skip suffix for short label
360
break;
361
case Verbosity.LONG:
362
label = localize('workspaceNameVerbose', "{0} (Workspace)", this.getUriLabel(joinPath(dirname(workspaceUri), filename)));
363
break;
364
case Verbosity.MEDIUM:
365
default:
366
label = localize('workspaceName', "{0} (Workspace)", filename);
367
break;
368
}
369
370
if (options?.verbose === Verbosity.SHORT) {
371
return label; // skip suffix for short label
372
}
373
374
return this.appendWorkspaceSuffix(label, workspaceUri);
375
}
376
377
private doGetSingleFolderWorkspaceLabel(folderUri: URI, options?: { verbose: Verbosity }): string {
378
let label: string;
379
switch (options?.verbose) {
380
case Verbosity.LONG:
381
label = this.getUriLabel(folderUri);
382
break;
383
case Verbosity.SHORT:
384
case Verbosity.MEDIUM:
385
default:
386
label = basename(folderUri) || posix.sep;
387
break;
388
}
389
390
if (options?.verbose === Verbosity.SHORT) {
391
return label; // skip suffix for short label
392
}
393
394
return this.appendWorkspaceSuffix(label, folderUri);
395
}
396
397
getSeparator(scheme: string, authority?: string): '/' | '\\' {
398
const formatter = this.findFormatting(URI.from({ scheme, authority }));
399
400
return formatter?.separator || posix.sep;
401
}
402
403
getHostLabel(scheme: string, authority?: string): string {
404
const formatter = this.findFormatting(URI.from({ scheme, authority }));
405
406
return formatter?.workspaceSuffix || authority || '';
407
}
408
409
getHostTooltip(scheme: string, authority?: string): string | undefined {
410
const formatter = this.findFormatting(URI.from({ scheme, authority }));
411
412
return formatter?.workspaceTooltip;
413
}
414
415
registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable {
416
const list = this.storedFormatters.formatters ??= [];
417
418
let replace = list.findIndex(f => f.scheme === formatter.scheme && f.authority === formatter.authority);
419
if (replace === -1 && list.length >= FORMATTER_CACHE_SIZE) {
420
replace = FORMATTER_CACHE_SIZE - 1; // at max capacity, replace the last element
421
}
422
423
if (replace === -1) {
424
list.unshift(formatter);
425
} else {
426
for (let i = replace; i > 0; i--) {
427
list[i] = list[i - 1];
428
}
429
list[0] = formatter;
430
}
431
432
this.storedFormattersMemento.saveMemento();
433
434
return this.registerFormatter(formatter);
435
}
436
437
registerFormatter(formatter: ResourceLabelFormatter): IDisposable {
438
this.formatters.push(formatter);
439
this._onDidChangeFormatters.fire({ scheme: formatter.scheme });
440
441
return {
442
dispose: () => {
443
this.formatters = this.formatters.filter(f => f !== formatter);
444
this._onDidChangeFormatters.fire({ scheme: formatter.scheme });
445
}
446
};
447
}
448
449
private formatUri(resource: URI, formatting: ResourceLabelFormatting, forceNoTildify?: boolean): string {
450
let label = formatting.label.replace(labelMatchingRegexp, (match, token, qsToken, qsValue) => {
451
switch (token) {
452
case 'scheme': return resource.scheme;
453
case 'authority': return resource.authority;
454
case 'authoritySuffix': {
455
const i = resource.authority.indexOf('+');
456
return i === -1 ? resource.authority : resource.authority.slice(i + 1);
457
}
458
case 'path':
459
return formatting.stripPathStartingSeparator
460
? resource.path.slice(resource.path[0] === formatting.separator ? 1 : 0)
461
: resource.path;
462
default: {
463
if (qsToken === 'query') {
464
const { query } = resource;
465
if (query && query[0] === '{' && query[query.length - 1] === '}') {
466
try {
467
return JSON.parse(query)[qsValue] || '';
468
} catch { }
469
}
470
}
471
472
return '';
473
}
474
}
475
});
476
477
// convert \c:\something => C:\something
478
if (formatting.normalizeDriveLetter && hasDriveLetterIgnorePlatform(label)) {
479
label = label.charAt(1).toUpperCase() + label.substr(2);
480
}
481
482
if (formatting.tildify && !forceNoTildify) {
483
if (this.userHome) {
484
label = tildify(label, this.userHome.fsPath, this.os);
485
}
486
}
487
488
if (formatting.authorityPrefix && resource.authority) {
489
label = formatting.authorityPrefix + label;
490
}
491
492
return label.replace(sepRegexp, formatting.separator);
493
}
494
495
private appendWorkspaceSuffix(label: string, uri: URI): string {
496
const formatting = this.findFormatting(uri);
497
const suffix = formatting && (typeof formatting.workspaceSuffix === 'string') ? formatting.workspaceSuffix : undefined;
498
499
return suffix ? `${label} [${suffix}]` : label;
500
}
501
}
502
503
registerSingleton(ILabelService, LabelService, InstantiationType.Delayed);
504
505