src/vs/base/common/labels.ts

484 LOC · 432 covered · 52 uncovered · 34 ranges · 2127 concepts · 7 introducers · 1212 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- labels.ts ×12
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 { hasDriveLetter, toSlashes } from './extpath.js';
7 > import { posix, sep, win32 } from './path.js';
8 > import { isMacintosh, isWindows, OperatingSystem, OS } from './platform.js';
9 > import { extUri, extUriIgnorePathCase } from './resources.js';
10 > import { rtrim, startsWithIgnoreCase } from './strings.js';
11 > import { URI } from './uri.js';
12 >
13 > export interface IPathLabelFormatting {
14 >
15 > /**
16 > * The OS the path label is from to produce a label
17 > * that matches OS expectations.
18 > */
19 > readonly os: OperatingSystem;
20 >
21 > /**
22 > * Whether to add a `~` when the path is in the
23 > * user home directory.
24 > *
25 > * Note: this only applies to Linux, macOS but not
26 > * Windows.
27 > */
28 > readonly tildify?: IUserHomeProvider;
29 >
30 > /**
31 > * Whether to convert to a relative path if the path
32 > * is within any of the opened workspace folders.
33 > */
34 > readonly relative?: IRelativePathProvider;
35 > }
36 >
37 > export interface IRelativePathProvider {
38 >
39 > /**
40 > * Whether to not add a prefix when in multi-root workspace.
41 > */
42 > readonly noPrefix?: boolean;
43 >
44 > getWorkspace(): { folders: { uri: URI; name?: string }[] };
45 > getWorkspaceFolder(resource: URI): { uri: URI; name?: string } | null;
46 > }
47 >
48 > export interface IUserHomeProvider {
49 > userHome: URI;
50 > }
51 >
52 > export function getPathLabel(resource: URI, formatting: IPathLabelFormatting): string {
53 > const { os, tildify: tildifier, relative: relatifier } = formatting; labels.ts ×12
54 >
55 > // return early with a relative path if we can resolve one
56 > if (relatifier) {
57 > const relativePath = getRelativePathLabel(resource, relatifier, os);
58 > if (typeof relativePath === 'string') {
59 > return relativePath;
60 > }
61 > }
62 >
63 > // otherwise try to resolve a absolute path label and
64 > // apply target OS standard path separators if target
65 > // OS differs from actual OS we are running in
66 > let absolutePath = resource.fsPath;
67 > if (os === OperatingSystem.Windows && !isWindows) {
68 > absolutePath = absolutePath.replace(/\//g, '\\');
69 > } else if (os !== OperatingSystem.Windows && isWindows) {
70 absolutePath = absolutePath.replace(/\\/g, '/');
71 }
73 > // macOS/Linux: tildify with provided user home directory
74 > if (os !== OperatingSystem.Windows && tildifier?.userHome) {
75 > const userHome = tildifier.userHome.fsPath;
76 >
77 > // This is a bit of a hack, but in order to figure out if the
78 > // resource is in the user home, we need to make sure to convert it
79 > // to a user home resource. We cannot assume that the resource is
80 > // already a user home resource.
81 > let userHomeCandidate: string;
82 > if (resource.scheme !== tildifier.userHome.scheme && resource.path[0] === posix.sep && resource.path[1] !== posix.sep) {
83 > userHomeCandidate = tildifier.userHome.with({ path: resource.path }).fsPath;
84 > } else {
85 > userHomeCandidate = absolutePath;
86 > }
87 >
88 > absolutePath = tildify(userHomeCandidate, userHome, os);
89 > }
90 >
91 > // normalize
92 > const pathLib = os === OperatingSystem.Windows ? win32 : posix;
93 > return pathLib.normalize(normalizeDriveLetter(absolutePath, os === OperatingSystem.Windows));
94 > }
96 > function getRelativePathLabel(resource: URI, relativePathProvider: IRelativePathProvider, os: OperatingSystem): string | undefined { labels.ts ×12
97 > const pathLib = os === OperatingSystem.Windows ? win32 : posix;
98 > const extUriLib = os === OperatingSystem.Linux ? extUri : extUriIgnorePathCase;
99 >
100 > const workspace = relativePathProvider.getWorkspace();
101 > const firstFolder = workspace.folders.at(0);
102 > if (!firstFolder) {
103 return undefined;
104 }
106 > // This is a bit of a hack, but in order to figure out the folder
107 > // the resource belongs to, we need to make sure to convert it
108 > // to a workspace resource. We cannot assume that the resource is
109 > // already matching the workspace.
110 > if (resource.scheme !== firstFolder.uri.scheme && resource.path[0] === posix.sep && resource.path[1] !== posix.sep) {
111 > resource = firstFolder.uri.with({ path: resource.path });
112 > }
113 >
114 > const folder = relativePathProvider.getWorkspaceFolder(resource);
115 > if (!folder) {
116 return undefined;
117 }
119 > let relativePathLabel: string | undefined = undefined;
120 > if (extUriLib.isEqual(folder.uri, resource)) {
121 relativePathLabel = ''; // no label if paths are identical
122 > } else { labels.ts ×12
123 > relativePathLabel = extUriLib.relativePath(folder.uri, resource) ?? '';
124 > }
125 >
126 > // normalize
127 > if (relativePathLabel) {
128 > relativePathLabel = pathLib.normalize(relativePathLabel);
129 > }
130 >
131 > // always show root basename if there are multiple folders
132 > if (workspace.folders.length > 1 && !relativePathProvider.noPrefix) {
133 const rootName = folder.name ? folder.name : extUriLib.basenameOrAuthority(folder.uri);
134 relativePathLabel = relativePathLabel ? `${rootName}${relativePathLabel}` : rootName;
135 }
137 > return relativePathLabel;
138 > }
140 > export function normalizeDriveLetter(path: string, isWindowsOS: boolean = isWindows): string {
141 > if (hasDriveLetter(path, isWindowsOS)) { labels.ts ×2
142 > return path.charAt(0).toUpperCase() + path.slice(1); labels.ts ×12
143 > }
145 > return path;
146 > }
148 > let normalizedUserHomeCached: { original: string; normalized: string } = Object.create(null);
149 > export function tildify(path: string, userHome: string, os = OS): string {
150 > if (os === OperatingSystem.Windows || !path || !userHome) { labels.ts ×12
151 return path; // unsupported on Windows
152 }
154 > let normalizedUserHome = normalizedUserHomeCached.original === userHome ? normalizedUserHomeCached.normalized : undefined;
155 > if (!normalizedUserHome) {
156 > normalizedUserHome = userHome;
157 > if (isWindows) {
158 normalizedUserHome = toSlashes(normalizedUserHome); // make sure that the path is POSIX normalized on Windows
159 }
160 > normalizedUserHome = `${rtrim(normalizedUserHome, posix.sep)}${posix.sep}`; labels.ts ×12
161 > normalizedUserHomeCached = { original: userHome, normalized: normalizedUserHome };
162 > }
163 >
164 > let normalizedPath = path;
165 > if (isWindows) {
166 normalizedPath = toSlashes(normalizedPath); // make sure that the path is POSIX normalized on Windows
167 }
169 > // Linux: case sensitive, macOS: case insensitive
170 > if (os === OperatingSystem.Linux ? normalizedPath.startsWith(normalizedUserHome) : startsWithIgnoreCase(normalizedPath, normalizedUserHome)) {
171 > return `~/${normalizedPath.substr(normalizedUserHome.length)}`;
172 > }
173 >
174 > return path;
175 > }
177 > export function untildify(path: string, userHome: string): string {
178 > return path.replace(/^~($|\/|\\)/, `${userHome}$1`); hookSchema.ts ×2
179 > }
181 > /**
182 > * Shortens the paths but keeps them easy to distinguish.
183 > * Replaces not important parts with ellipsis.
184 > * Every shorten path matches only one original path and vice versa.
185 > *
186 > * Algorithm for shortening paths is as follows:
187 > * 1. For every path in list, find unique substring of that path.
188 > * 2. Unique substring along with ellipsis is shortened path of that path.
189 > * 3. To find unique substring of path, consider every segment of length from 1 to path.length of path from end of string
190 > * and if present segment is not substring to any other paths then present segment is unique path,
191 > * else check if it is not present as suffix of any other path and present segment is suffix of path itself,
192 > * if it is true take present segment as unique path.
193 > * 4. Apply ellipsis to unique segment according to whether segment is present at start/in-between/end of path.
194 > *
195 > * Example 1
196 > * 1. consider 2 paths i.e. ['a\\b\\c\\d', 'a\\f\\b\\c\\d']
197 > * 2. find unique path of first path,
198 > * a. 'd' is present in path2 and is suffix of path2, hence not unique of present path.
199 > * b. 'c' is present in path2 and 'c' is not suffix of present path, similarly for 'b' and 'a' also.
200 > * c. 'd\\c' is suffix of path2.
201 > * d. 'b\\c' is not suffix of present path.
202 > * e. 'a\\b' is not present in path2, hence unique path is 'a\\b...'.
203 > * 3. for path2, 'f' is not present in path1 hence unique is '...\\f\\...'.
204 > *
205 > * Example 2
206 > * 1. consider 2 paths i.e. ['a\\b', 'a\\b\\c'].
207 > * a. Even if 'b' is present in path2, as 'b' is suffix of path1 and is not suffix of path2, unique path will be '...\\b'.
208 > * 2. for path2, 'c' is not present in path1 hence unique path is '..\\c'.
209 > */
210 > const ellipsis = '\u2026';
211 > const unc = '\\\\';
212 > const urlSchemaRegexp = /^[^:/\\?#]+?:\/\//;
213 > const home = '~';
214 > export function shorten(paths: string[], defaultPathSeparator: string = sep): string[] {
215 > const shortenedPaths: string[] = new Array(paths.length); labels.ts ×2
216 >
217 > // for every path
218 > let match = false;
219 > for (let pathIndex = 0; pathIndex < paths.length; pathIndex++) {
220 > let pathSeparator = defaultPathSeparator;
221 > const originalPath = paths[pathIndex];
222 >
223 > if (originalPath === '') {
224 > shortenedPaths[pathIndex] = `.${pathSeparator}`;
225 > continue;
226 > }
227 >
228 > if (!originalPath) {
229 > shortenedPaths[pathIndex] = originalPath;
230 > continue;
231 > }
232 >
233 > match = true;
234 >
235 > // trim for now and concatenate unc path (e.g. \\network) or root path (/etc, ~/etc) later
236 > let prefix = '';
237 > let trimmedPath = originalPath;
238 > if (urlSchemaRegexp.test(trimmedPath)) {
239 > prefix = trimmedPath.substr(0, trimmedPath.indexOf('//') + 2);
240 > trimmedPath = trimmedPath.substr(trimmedPath.indexOf('//') + 2);
241 > pathSeparator = '/';
242 > } else if (trimmedPath.indexOf(unc) === 0) {
243 prefix = trimmedPath.substr(0, trimmedPath.indexOf(unc) + unc.length);
244 trimmedPath = trimmedPath.substr(trimmedPath.indexOf(unc) + unc.length);
245 > } else if (trimmedPath.indexOf(pathSeparator) === 0) { labels.ts ×2
246 > prefix = trimmedPath.substr(0, trimmedPath.indexOf(pathSeparator) + pathSeparator.length);
247 > trimmedPath = trimmedPath.substr(trimmedPath.indexOf(pathSeparator) + pathSeparator.length);
248 > } else if (trimmedPath.indexOf(home) === 0) {
249 > prefix = trimmedPath.substr(0, trimmedPath.indexOf(home) + home.length);
250 > trimmedPath = trimmedPath.substr(trimmedPath.indexOf(home) + home.length);
251 > }
252 >
253 > // pick the first shortest subpath found
254 > const segments: string[] = trimmedPath.split(pathSeparator);
255 > for (let subpathLength = 1; match && subpathLength <= segments.length; subpathLength++) {
256 > for (let start = segments.length - subpathLength; match && start >= 0; start--) {
257 > match = false;
258 > let subpath = segments.slice(start, start + subpathLength).join(pathSeparator);
259 >
260 > // that is unique to any other path
261 > for (let otherPathIndex = 0; !match && otherPathIndex < paths.length; otherPathIndex++) {
262 >
263 > // suffix subpath treated specially as we consider no match 'x' and 'x/...'
264 > if (otherPathIndex !== pathIndex && paths[otherPathIndex] && paths[otherPathIndex].indexOf(subpath) > -1) {
265 > const isSubpathEnding: boolean = (start + subpathLength === segments.length);
266 >
267 > // Adding separator as prefix for subpath, such that 'endsWith(src, trgt)' considers subpath as directory name instead of plain string.
268 > // prefix is not added when either subpath is root directory or path[otherPathIndex] does not have multiple directories.
269 > const subpathWithSep: string = (start > 0 && paths[otherPathIndex].indexOf(pathSeparator) > -1) ? pathSeparator + subpath : subpath;
270 > const isOtherPathEnding: boolean = paths[otherPathIndex].endsWith(subpathWithSep);
271 >
272 > match = !isSubpathEnding || isOtherPathEnding;
273 > }
274 > }
275 >
276 > // found unique subpath
277 > if (!match) {
278 > let result = '';
279 >
280 > // preserve disk drive or root prefix
281 > if (segments[0].endsWith(':') || prefix !== '') {
282 > if (start === 1) {
283 > // extend subpath to include disk drive prefix
284 > start = 0;
285 > subpathLength++;
286 > subpath = segments[0] + pathSeparator + subpath;
287 > }
288 >
289 > if (start > 0) {
290 > result = segments[0] + pathSeparator;
291 > }
292 >
293 > result = prefix + result;
294 > }
295 >
296 > // add ellipsis at the beginning if needed
297 > if (start > 0) {
298 > result = result + ellipsis + pathSeparator;
299 > }
300 >
301 > result = result + subpath;
302 >
303 > // add ellipsis at the end if needed
304 > if (start + subpathLength < segments.length) {
305 > // If the last segment is empty, preserve the trailing slash.
306 > if (start + subpathLength === segments.length - 1 && segments[segments.length - 1] === '') {
307 > result = result + pathSeparator;
308 > } else {
309 > result = result + pathSeparator + ellipsis;
310 > }
311 > }
312 >
313 > shortenedPaths[pathIndex] = result;
314 > }
315 > }
316 > }
317 >
318 > if (match) {
319 > shortenedPaths[pathIndex] = originalPath; // use original path if no unique subpaths found
320 > }
321 > }
322 >
323 > return shortenedPaths;
324 > }
326 > export interface ISeparator {
327 > label: string;
328 > }
329 >
330 > enum Type {
331 > TEXT,
332 > VARIABLE,
333 > SEPARATOR
334 > }
335 >
336 > interface ISegment {
337 > value: string;
338 > type: Type;
339 > }
340 >
341 > /**
342 > * Helper to insert values for specific template variables into the string. E.g. "this $(is) a $(template)" can be
343 > * passed to this function together with an object that maps "is" and "template" to strings to have them replaced.
344 > * @param value string to which template is applied
345 > * @param values the values of the templates to use
346 > */
347 > export function template(template: string, values: { [key: string]: string | ISeparator | undefined | null } = Object.create(null)): string {
348 > const segments: ISegment[] = []; labels.ts ×1
349 >
350 > let inVariable = false;
351 > let curVal = '';
352 > for (const char of template) {
353 > // Beginning of variable
354 > if (char === '$' || (inVariable && char === '{')) {
355 > if (curVal) {
356 > segments.push({ value: curVal, type: Type.TEXT });
357 > }
358 >
359 > curVal = '';
360 > inVariable = true;
361 > }
362 >
363 > // End of variable
364 > else if (char === '}' && inVariable) {
365 > const resolved = values[curVal];
366 >
367 > // Variable
368 > if (typeof resolved === 'string') {
369 > if (resolved.length) {
370 > segments.push({ value: resolved, type: Type.VARIABLE });
371 > }
372 > }
373 >
374 > // Separator
375 > else if (resolved) {
376 > const prevSegment = segments[segments.length - 1];
377 > if (!prevSegment || prevSegment.type !== Type.SEPARATOR) {
378 > segments.push({ value: resolved.label, type: Type.SEPARATOR }); // prevent duplicate separators
379 > }
380 > }
381 >
382 > curVal = '';
383 > inVariable = false;
384 > }
385 >
386 > // Text or Variable Name
387 > else {
388 > curVal += char;
389 > }
390 > }
391 >
392 > // Tail
393 > if (curVal && !inVariable) {
394 > segments.push({ value: curVal, type: Type.TEXT });
395 > }
396 >
397 > return segments.filter((segment, index) => {
398 >
399 > // Only keep separator if we have values to the left and right
400 > if (segment.type === Type.SEPARATOR) {
401 > const left = segments[index - 1];
402 > const right = segments[index + 1];
403 >
404 > return [left, right].every(segment => segment && (segment.type === Type.VARIABLE || segment.type === Type.TEXT) && segment.value.length > 0);
405 > }
406 >
407 > // accept any TEXT and VARIABLE
408 > return true;
409 > }).map(segment => segment.value).join('');
410 > }
412 > /**
413 > * Handles mnemonics for menu items. Depending on OS:
414 > * - Windows: Supported via & character (replace && with &)
415 > * - Linux: Supported via & character (replace && with &)
416 > * - macOS: Unsupported (replace && with empty string)
417 > */
418 > export function mnemonicMenuLabel(label: string, forceDisableMnemonics?: boolean): string {
419 if (isMacintosh || forceDisableMnemonics) {
420 return label.replace(/\(&&\w\)|&&/g, '').replace(/&/g, isMacintosh ? '&' : '&&');
421 }
422
423 return label.replace(/&&|&/g, m => m === '&' ? '&&' : '&');
424 }
426 > /**
427 > * Handles mnemonics for buttons. Depending on OS:
428 > * - Windows: Supported via & character (replace && with & and & with && for escaping)
429 > * - Linux: Supported via _ character (replace && with _)
430 > * - macOS: Unsupported (replace && with empty string)
431 > * When forceDisableMnemonics is set, returns just the label without mnemonics.
432 > */
433 > export function mnemonicButtonLabel(label: string, forceDisableMnemonics: true): string;
434 > export function mnemonicButtonLabel(label: string, forceDisableMnemonics?: false): { readonly withMnemonic: string; readonly withoutMnemonic: string };
435 > export function mnemonicButtonLabel(label: string, forceDisableMnemonics?: boolean): { readonly withMnemonic: string; readonly withoutMnemonic: string } | string {
436 > const withoutMnemonic = label.replace(/\(&&\w\)|&&/g, ''); labels.ts ×4
437 >
438 > if (forceDisableMnemonics) {
439 return withoutMnemonic;
440 }
441 > if (isMacintosh) { labels.ts ×4
442 return { withMnemonic: withoutMnemonic, withoutMnemonic };
443 }
445 > let withMnemonic: string;
446 > if (isWindows) {
447 withMnemonic = label.replace(/&&|&/g, m => m === '&' ? '&&' : '&');
448 > } else { labels.ts ×4
449 > withMnemonic = label.replace(/&&/g, '_');
450 > }
451 > return { withMnemonic, withoutMnemonic };
452 > }
454 > export function unmnemonicLabel(label: string): string {
455 return label.replace(/&/g, '&&');
456 }
458 > /**
459 > * Splits a recent label in name and parent path, supporting both '/' and '\' and workspace suffixes.
460 > * If the location is remote, the remote name is included in the name part.
461 > */
462 > export function splitRecentLabel(recentLabel: string): { name: string; parentPath: string } {
463 if (recentLabel.endsWith(']')) {
464 // label with workspace suffix
465 const lastIndexOfSquareBracket = recentLabel.lastIndexOf(' [', recentLabel.length - 2);
466 if (lastIndexOfSquareBracket !== -1) {
467 const split = splitName(recentLabel.substring(0, lastIndexOfSquareBracket));
468 const remoteNameWithSpace = recentLabel.substring(lastIndexOfSquareBracket);
469 return { name: split.name + remoteNameWithSpace, parentPath: split.parentPath };
470 }
471 }
472 return splitName(recentLabel);
473 }
475 function splitName(fullPath: string): { name: string; parentPath: string } {
476 const p = fullPath.indexOf('/') !== -1 ? posix : win32;
477 const name = p.basename(fullPath);
478 const parentPath = p.dirname(fullPath);
479 if (name.length) {
480 return { name, parentPath };
481 }
482 // only the root segment
483 return { name: parentPath, parentPath: '' };
484 }