addons/shared/minimalDisambiguousPaths.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318type PathObject = {
b69ab319 depth: number | null;
b69ab3110 parts: string[];
b69ab3111 rootPrefix: string;
b69ab3112 separator: string;
b69ab3113 hadLeadingSeparator: boolean;
b69ab3114};
b69ab3115
b69ab3116type Options = {
b69ab3117 alwaysShowLeadingSeparator?: boolean;
b69ab3118 maxDepth?: number;
b69ab3119};
b69ab3120
b69ab3121/**
b69ab3122 * Computes the minimally differentiable display path for each file.
b69ab3123 *
b69ab3124 * The algorithm is O(n*m^2) where n = paths.length and m = maximum number parts in a given path and the
b69ab3125 * implementation is semi-optimized for performance.
b69ab3126 *
b69ab3127 * Note that this function only applies to absolute paths and does NOT dedupe paths, see the last example below.
b69ab3128 *
b69ab3129 * ['/a/b/c.js', '/a/d/c.js'] would return ['b/c.js', 'd/c.js']
b69ab3130 * ['/a/b/c.js', '/a/b/d.js'] would return ['c.js', 'd.js']
b69ab3131 * ['/a/b.js', '/c/a/b.js'] would return ['/a/b.js', 'c/a/b.js']
b69ab3132 * ['/a/b.js', '/a/b.js'] would return ['/a/b.js', '/a/b.js'] since this function does not dedupe.
b69ab3133 *
b69ab3134 * @param paths a list of paths to compute the minimal disambiguation
b69ab3135 * @param options.alwaysShowLeadingSeparator If true, all file paths will start with a leading
b69ab3136 * separator (`/` on Unix, `\` on Windows). If false, only full file paths will start with a leading
b69ab3137 * separator. Leading separators are never shown for paths that consist of only a filename, e.g.
b69ab3138 * "foo.txt". Defaults to true.
b69ab3139 * @param options.maxDepth maximum depth to truncate paths to, even if it doesn't fully disambiguate.
b69ab3140
b69ab3141 */
b69ab3142export function minimalDisambiguousPaths(paths: string[], options: Options = {}): string[] {
b69ab3143 const pathObjects: PathObject[] = paths.map(path => {
b69ab3144 const separator = guessSeparator(path);
b69ab3145 const rootPrefixResult = /^(\w:).*/.exec(path);
b69ab3146 const rootPrefix = rootPrefixResult != null ? rootPrefixResult[1] : '';
b69ab3147
b69ab3148 return {
b69ab3149 depth: null,
b69ab3150 parts: path
b69ab3151 .split(separator)
b69ab3152 // Path parts are reversed for easier processing below.
b69ab3153 .reverse()
b69ab3154 .filter(part => part !== '' && part !== rootPrefix),
b69ab3155 rootPrefix,
b69ab3156 separator,
b69ab3157 // paths which start with `/` or `\` or `C:\` should keep that prefix when they remain at the root level.
b69ab3158 hadLeadingSeparator:
b69ab3159 path.startsWith(separator) || (rootPrefix.length > 0 && path.startsWith(rootPrefix)),
b69ab3160 };
b69ab3161 });
b69ab3162
b69ab3163 const maxDepth =
b69ab3164 options.maxDepth == null
b69ab3165 ? Math.max(...pathObjects.map(pathObject => pathObject.parts.length))
b69ab3166 : options.maxDepth;
b69ab3167
b69ab3168 const pathObjectsToProcess = new Set(pathObjects);
b69ab3169 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Please fix as you edit this code
b69ab3170 const groupedPathObjects: Map<string, Set<PathObject>> = new Map();
b69ab3171
b69ab3172 for (let currentDepth = 1; currentDepth <= maxDepth; currentDepth++) {
b69ab3173 // Group path objects by their path up to the current depth.
b69ab3174 groupedPathObjects.clear();
b69ab3175 for (const pathObject of pathObjectsToProcess) {
b69ab3176 const path = pathObject.parts.slice(0, currentDepth).join(pathObject.separator);
b69ab3177 if (!groupedPathObjects.has(path)) {
b69ab3178 groupedPathObjects.set(path, new Set());
b69ab3179 }
b69ab3180 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Please fix as you edit this code
b69ab3181 groupedPathObjects.get(path)!.add(pathObject);
b69ab3182 }
b69ab3183
b69ab3184 // Mark the depth for unique path objects and remove them from the set of objects to process.
b69ab3185 for (const pathObjectGroup of groupedPathObjects.values()) {
b69ab3186 if (pathObjectGroup.size === 1) {
b69ab3187 const pathObject = Array.from(pathObjectGroup)[0];
b69ab3188 pathObject.depth = currentDepth;
b69ab3189 pathObjectsToProcess.delete(pathObject);
b69ab3190 }
b69ab3191 }
b69ab3192 }
b69ab3193
b69ab3194 return pathObjects.map(({depth, parts, rootPrefix, separator, hadLeadingSeparator}) => {
b69ab3195 let resultPathParts = parts.slice(0, depth == null ? maxDepth : depth).reverse();
b69ab3196
b69ab3197 // Empty path ('/' or 'c:\') should return a separator to indicate root.
b69ab3198 if (resultPathParts.length === 0) {
b69ab3199 return `${rootPrefix}${separator}`;
b69ab31100 }
b69ab31101
b69ab31102 // - Complex paths should always start with a separator, if the original paths started with a separator
b69ab31103 // - Simple paths (e.g. single directory or file) should not start with a separator
b69ab31104 // - If we show the full path (no truncation), include any prefix like 'C:' as well.
b69ab31105 if (
b69ab31106 (resultPathParts.length === 1 && resultPathParts[0] === '') ||
b69ab31107 (resultPathParts.length > 1 && resultPathParts[0] !== '')
b69ab31108 ) {
b69ab31109 resultPathParts =
b69ab31110 resultPathParts.length === parts.length
b69ab31111 ? hadLeadingSeparator
b69ab31112 ? [rootPrefix, ...resultPathParts]
b69ab31113 : resultPathParts
b69ab31114 : (options.alwaysShowLeadingSeparator ?? hadLeadingSeparator)
b69ab31115 ? ['', ...resultPathParts]
b69ab31116 : resultPathParts;
b69ab31117 }
b69ab31118
b69ab31119 return resultPathParts.join(separator);
b69ab31120 });
b69ab31121}
b69ab31122
b69ab31123function guessSeparator(path: string): '/' | '\\' {
b69ab31124 const windowsCount = path.replace(/[^\\]/g, '').length;
b69ab31125 const unixCount = path.replace(/[^/]/g, '').length;
b69ab31126
b69ab31127 return windowsCount > unixCount ? '\\' : '/';
b69ab31128}