/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { hasDriveLetter, toSlashes } from './extpath.js'; import { posix, sep, win32 } from './path.js'; import { isMacintosh, isWindows, OS } from './platform.js'; import { extUri, extUriIgnorePathCase } from './resources.js'; import { rtrim, startsWithIgnoreCase } from './strings.js'; export function getPathLabel(resource, formatting) { const { os, tildify: tildifier, relative: relatifier } = formatting; // return early with a relative path if we can resolve one if (relatifier) { const relativePath = getRelativePathLabel(resource, relatifier, os); if (typeof relativePath === 'string') { return relativePath; } } // otherwise try to resolve a absolute path label and // apply target OS standard path separators if target // OS differs from actual OS we are running in let absolutePath = resource.fsPath; if (os === 1 /* OperatingSystem.Windows */ && !isWindows) { absolutePath = absolutePath.replace(/\//g, '\\'); } else if (os !== 1 /* OperatingSystem.Windows */ && isWindows) { absolutePath = absolutePath.replace(/\\/g, '/'); } // macOS/Linux: tildify with provided user home directory if (os !== 1 /* OperatingSystem.Windows */ && tildifier?.userHome) { const userHome = tildifier.userHome.fsPath; // This is a bit of a hack, but in order to figure out if the // resource is in the user home, we need to make sure to convert it // to a user home resource. We cannot assume that the resource is // already a user home resource. let userHomeCandidate; if (resource.scheme !== tildifier.userHome.scheme && resource.path[0] === posix.sep && resource.path[1] !== posix.sep) { userHomeCandidate = tildifier.userHome.with({ path: resource.path }).fsPath; } else { userHomeCandidate = absolutePath; } absolutePath = tildify(userHomeCandidate, userHome, os); } // normalize const pathLib = os === 1 /* OperatingSystem.Windows */ ? win32 : posix; return pathLib.normalize(normalizeDriveLetter(absolutePath, os === 1 /* OperatingSystem.Windows */)); } function getRelativePathLabel(resource, relativePathProvider, os) { const pathLib = os === 1 /* OperatingSystem.Windows */ ? win32 : posix; const extUriLib = os === 3 /* OperatingSystem.Linux */ ? extUri : extUriIgnorePathCase; const workspace = relativePathProvider.getWorkspace(); const firstFolder = workspace.folders.at(0); if (!firstFolder) { return undefined; } // This is a bit of a hack, but in order to figure out the folder // the resource belongs to, we need to make sure to convert it // to a workspace resource. We cannot assume that the resource is // already matching the workspace. if (resource.scheme !== firstFolder.uri.scheme && resource.path[0] === posix.sep && resource.path[1] !== posix.sep) { resource = firstFolder.uri.with({ path: resource.path }); } const folder = relativePathProvider.getWorkspaceFolder(resource); if (!folder) { return undefined; } let relativePathLabel = undefined; if (extUriLib.isEqual(folder.uri, resource)) { relativePathLabel = ''; // no label if paths are identical } else { relativePathLabel = extUriLib.relativePath(folder.uri, resource) ?? ''; } // normalize if (relativePathLabel) { relativePathLabel = pathLib.normalize(relativePathLabel); } // always show root basename if there are multiple folders if (workspace.folders.length > 1 && !relativePathProvider.noPrefix) { const rootName = folder.name ? folder.name : extUriLib.basenameOrAuthority(folder.uri); relativePathLabel = relativePathLabel ? `${rootName} • ${relativePathLabel}` : rootName; } return relativePathLabel; } export function normalizeDriveLetter(path, isWindowsOS = isWindows) { if (hasDriveLetter(path, isWindowsOS)) { return path.charAt(0).toUpperCase() + path.slice(1); } return path; } let normalizedUserHomeCached = Object.create(null); export function tildify(path, userHome, os = OS) { if (os === 1 /* OperatingSystem.Windows */ || !path || !userHome) { return path; // unsupported on Windows } let normalizedUserHome = normalizedUserHomeCached.original === userHome ? normalizedUserHomeCached.normalized : undefined; if (!normalizedUserHome) { normalizedUserHome = userHome; if (isWindows) { normalizedUserHome = toSlashes(normalizedUserHome); // make sure that the path is POSIX normalized on Windows } normalizedUserHome = `${rtrim(normalizedUserHome, posix.sep)}${posix.sep}`; normalizedUserHomeCached = { original: userHome, normalized: normalizedUserHome }; } let normalizedPath = path; if (isWindows) { normalizedPath = toSlashes(normalizedPath); // make sure that the path is POSIX normalized on Windows } // Linux: case sensitive, macOS: case insensitive if (os === 3 /* OperatingSystem.Linux */ ? normalizedPath.startsWith(normalizedUserHome) : startsWithIgnoreCase(normalizedPath, normalizedUserHome)) { return `~/${normalizedPath.substr(normalizedUserHome.length)}`; } return path; } export function untildify(path, userHome) { return path.replace(/^~($|\/|\\)/, `${userHome}$1`); } /** * Shortens the paths but keeps them easy to distinguish. * Replaces not important parts with ellipsis. * Every shorten path matches only one original path and vice versa. * * Algorithm for shortening paths is as follows: * 1. For every path in list, find unique substring of that path. * 2. Unique substring along with ellipsis is shortened path of that path. * 3. To find unique substring of path, consider every segment of length from 1 to path.length of path from end of string * and if present segment is not substring to any other paths then present segment is unique path, * else check if it is not present as suffix of any other path and present segment is suffix of path itself, * if it is true take present segment as unique path. * 4. Apply ellipsis to unique segment according to whether segment is present at start/in-between/end of path. * * Example 1 * 1. consider 2 paths i.e. ['a\\b\\c\\d', 'a\\f\\b\\c\\d'] * 2. find unique path of first path, * a. 'd' is present in path2 and is suffix of path2, hence not unique of present path. * b. 'c' is present in path2 and 'c' is not suffix of present path, similarly for 'b' and 'a' also. * c. 'd\\c' is suffix of path2. * d. 'b\\c' is not suffix of present path. * e. 'a\\b' is not present in path2, hence unique path is 'a\\b...'. * 3. for path2, 'f' is not present in path1 hence unique is '...\\f\\...'. * * Example 2 * 1. consider 2 paths i.e. ['a\\b', 'a\\b\\c']. * 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'. * 2. for path2, 'c' is not present in path1 hence unique path is '..\\c'. */ const ellipsis = '\u2026'; const unc = '\\\\'; const home = '~'; export function shorten(paths, pathSeparator = sep) { const shortenedPaths = new Array(paths.length); // for every path let match = false; for (let pathIndex = 0; pathIndex < paths.length; pathIndex++) { const originalPath = paths[pathIndex]; if (originalPath === '') { shortenedPaths[pathIndex] = `.${pathSeparator}`; continue; } if (!originalPath) { shortenedPaths[pathIndex] = originalPath; continue; } match = true; // trim for now and concatenate unc path (e.g. \\network) or root path (/etc, ~/etc) later let prefix = ''; let trimmedPath = originalPath; if (trimmedPath.indexOf(unc) === 0) { prefix = trimmedPath.substr(0, trimmedPath.indexOf(unc) + unc.length); trimmedPath = trimmedPath.substr(trimmedPath.indexOf(unc) + unc.length); } else if (trimmedPath.indexOf(pathSeparator) === 0) { prefix = trimmedPath.substr(0, trimmedPath.indexOf(pathSeparator) + pathSeparator.length); trimmedPath = trimmedPath.substr(trimmedPath.indexOf(pathSeparator) + pathSeparator.length); } else if (trimmedPath.indexOf(home) === 0) { prefix = trimmedPath.substr(0, trimmedPath.indexOf(home) + home.length); trimmedPath = trimmedPath.substr(trimmedPath.indexOf(home) + home.length); } // pick the first shortest subpath found const segments = trimmedPath.split(pathSeparator); for (let subpathLength = 1; match && subpathLength <= segments.length; subpathLength++) { for (let start = segments.length - subpathLength; match && start >= 0; start--) { match = false; let subpath = segments.slice(start, start + subpathLength).join(pathSeparator); // that is unique to any other path for (let otherPathIndex = 0; !match && otherPathIndex < paths.length; otherPathIndex++) { // suffix subpath treated specially as we consider no match 'x' and 'x/...' if (otherPathIndex !== pathIndex && paths[otherPathIndex] && paths[otherPathIndex].indexOf(subpath) > -1) { const isSubpathEnding = (start + subpathLength === segments.length); // Adding separator as prefix for subpath, such that 'endsWith(src, trgt)' considers subpath as directory name instead of plain string. // prefix is not added when either subpath is root directory or path[otherPathIndex] does not have multiple directories. const subpathWithSep = (start > 0 && paths[otherPathIndex].indexOf(pathSeparator) > -1) ? pathSeparator + subpath : subpath; const isOtherPathEnding = paths[otherPathIndex].endsWith(subpathWithSep); match = !isSubpathEnding || isOtherPathEnding; } } // found unique subpath if (!match) { let result = ''; // preserve disk drive or root prefix if (segments[0].endsWith(':') || prefix !== '') { if (start === 1) { // extend subpath to include disk drive prefix start = 0; subpathLength++; subpath = segments[0] + pathSeparator + subpath; } if (start > 0) { result = segments[0] + pathSeparator; } result = prefix + result; } // add ellipsis at the beginning if needed if (start > 0) { result = result + ellipsis + pathSeparator; } result = result + subpath; // add ellipsis at the end if needed if (start + subpathLength < segments.length) { result = result + pathSeparator + ellipsis; } shortenedPaths[pathIndex] = result; } } } if (match) { shortenedPaths[pathIndex] = originalPath; // use original path if no unique subpaths found } } return shortenedPaths; } var Type; (function (Type) { Type[Type["TEXT"] = 0] = "TEXT"; Type[Type["VARIABLE"] = 1] = "VARIABLE"; Type[Type["SEPARATOR"] = 2] = "SEPARATOR"; })(Type || (Type = {})); /** * Helper to insert values for specific template variables into the string. E.g. "this $(is) a $(template)" can be * passed to this function together with an object that maps "is" and "template" to strings to have them replaced. * @param value string to which template is applied * @param values the values of the templates to use */ export function template(template, values = Object.create(null)) { const segments = []; let inVariable = false; let curVal = ''; for (const char of template) { // Beginning of variable if (char === '$' || (inVariable && char === '{')) { if (curVal) { segments.push({ value: curVal, type: Type.TEXT }); } curVal = ''; inVariable = true; } // End of variable else if (char === '}' && inVariable) { const resolved = values[curVal]; // Variable if (typeof resolved === 'string') { if (resolved.length) { segments.push({ value: resolved, type: Type.VARIABLE }); } } // Separator else if (resolved) { const prevSegment = segments[segments.length - 1]; if (!prevSegment || prevSegment.type !== Type.SEPARATOR) { segments.push({ value: resolved.label, type: Type.SEPARATOR }); // prevent duplicate separators } } curVal = ''; inVariable = false; } // Text or Variable Name else { curVal += char; } } // Tail if (curVal && !inVariable) { segments.push({ value: curVal, type: Type.TEXT }); } return segments.filter((segment, index) => { // Only keep separator if we have values to the left and right if (segment.type === Type.SEPARATOR) { const left = segments[index - 1]; const right = segments[index + 1]; return [left, right].every(segment => segment && (segment.type === Type.VARIABLE || segment.type === Type.TEXT) && segment.value.length > 0); } // accept any TEXT and VARIABLE return true; }).map(segment => segment.value).join(''); } /** * Handles mnemonics for menu items. Depending on OS: * - Windows: Supported via & character (replace && with &) * - Linux: Supported via & character (replace && with &) * - macOS: Unsupported (replace && with empty string) */ export function mnemonicMenuLabel(label, forceDisableMnemonics) { if (isMacintosh || forceDisableMnemonics) { return label.replace(/\(&&\w\)|&&/g, '').replace(/&/g, isMacintosh ? '&' : '&&'); } return label.replace(/&&|&/g, m => m === '&' ? '&&' : '&'); } /** * Handles mnemonics for buttons. Depending on OS: * - Windows: Supported via & character (replace && with & and & with && for escaping) * - Linux: Supported via _ character (replace && with _) * - macOS: Unsupported (replace && with empty string) */ export function mnemonicButtonLabel(label, forceDisableMnemonics) { if (isMacintosh || forceDisableMnemonics) { return label.replace(/\(&&\w\)|&&/g, ''); } if (isWindows) { return label.replace(/&&|&/g, m => m === '&' ? '&&' : '&'); } return label.replace(/&&/g, '_'); } export function unmnemonicLabel(label) { return label.replace(/&/g, '&&'); } /** * Splits a recent label in name and parent path, supporting both '/' and '\' and workspace suffixes. * If the location is remote, the remote name is included in the name part. */ export function splitRecentLabel(recentLabel) { if (recentLabel.endsWith(']')) { // label with workspace suffix const lastIndexOfSquareBracket = recentLabel.lastIndexOf(' [', recentLabel.length - 2); if (lastIndexOfSquareBracket !== -1) { const split = splitName(recentLabel.substring(0, lastIndexOfSquareBracket)); const remoteNameWithSpace = recentLabel.substring(lastIndexOfSquareBracket); return { name: split.name + remoteNameWithSpace, parentPath: split.parentPath }; } } return splitName(recentLabel); } function splitName(fullPath) { const p = fullPath.indexOf('/') !== -1 ? posix : win32; const name = p.basename(fullPath); const parentPath = p.dirname(fullPath); if (name.length) { return { name, parentPath }; } // only the root segment return { name: parentPath, parentPath: '' }; } //# sourceMappingURL=labels.js.map