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