labels.ts 11.4 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

7
import URI from 'vs/base/common/uri';
E
Erich Gamma 已提交
8
import platform = require('vs/base/common/platform');
B
Benjamin Pasero 已提交
9
import { nativeSep, normalize, isEqualOrParent, isEqual, basename as pathsBasename, join } from 'vs/base/common/paths';
B
Benjamin Pasero 已提交
10
import { endsWith, ltrim } from 'vs/base/common/strings';
E
Erich Gamma 已提交
11

S
Sandeep Somavarapu 已提交
12
export interface IWorkspaceFolderProvider {
13
	getWorkspaceFolder(resource: URI): { uri: URI };
B
Benjamin Pasero 已提交
14
	getWorkspace(): {
15
		folders: { uri: URI }[];
B
Benjamin Pasero 已提交
16
	};
E
Erich Gamma 已提交
17 18
}

19 20 21 22
export interface IUserHomeProvider {
	userHome: string;
}

S
Sandeep Somavarapu 已提交
23
export function getPathLabel(resource: URI | string, rootProvider?: IWorkspaceFolderProvider, userHomeProvider?: IUserHomeProvider): string {
B
Benjamin Pasero 已提交
24
	if (!resource) {
25 26 27
		return null;
	}

B
Benjamin Pasero 已提交
28 29 30
	if (typeof resource === 'string') {
		resource = URI.file(resource);
	}
31

I
isidor 已提交
32 33 34
	if (resource.scheme !== 'file' && resource.scheme !== 'untitled') {
		return resource.authority + resource.path;
	}
B
Benjamin Pasero 已提交
35 36

	// return early if we can resolve a relative path label from the root
S
Sandeep Somavarapu 已提交
37
	const baseResource = rootProvider ? rootProvider.getWorkspaceFolder(resource) : null;
B
Benjamin Pasero 已提交
38
	if (baseResource) {
S
Sandeep Somavarapu 已提交
39
		const hasMultipleRoots = rootProvider.getWorkspace().folders.length > 1;
B
Benjamin Pasero 已提交
40 41

		let pathLabel: string;
42
		if (isEqual(baseResource.uri.fsPath, resource.fsPath, !platform.isLinux /* ignorecase */)) {
B
Benjamin Pasero 已提交
43 44
			pathLabel = ''; // no label if pathes are identical
		} else {
45
			pathLabel = normalize(ltrim(resource.fsPath.substr(baseResource.uri.fsPath.length), nativeSep), true);
B
Benjamin Pasero 已提交
46
		}
E
Erich Gamma 已提交
47

B
Benjamin Pasero 已提交
48
		if (hasMultipleRoots) {
B
Benjamin Pasero 已提交
49
			const rootName = pathsBasename(baseResource.uri.fsPath);
B
Benjamin Pasero 已提交
50
			pathLabel = pathLabel ? join(rootName, pathLabel) : rootName; // always show root basename if there are multiple
51 52
		}

B
Benjamin Pasero 已提交
53
		return pathLabel;
E
Erich Gamma 已提交
54 55
	}

B
Benjamin Pasero 已提交
56
	// convert c:\something => C:\something
57 58
	if (hasDriveLetter(resource.fsPath)) {
		return normalize(normalizeDriveLetter(resource.fsPath), true);
E
Erich Gamma 已提交
59 60
	}

B
Benjamin Pasero 已提交
61
	// normalize and tildify (macOS, Linux only)
62
	let res = normalize(resource.fsPath, true);
63 64 65 66 67
	if (!platform.isWindows && userHomeProvider) {
		res = tildify(res, userHomeProvider.userHome);
	}

	return res;
E
Erich Gamma 已提交
68 69
}

B
Benjamin Pasero 已提交
70 71 72 73 74 75 76 77 78
export function getBaseLabel(resource: URI | string): string {
	if (!resource) {
		return null;
	}

	if (typeof resource === 'string') {
		resource = URI.file(resource);
	}

B
Benjamin Pasero 已提交
79
	const base = pathsBasename(resource.fsPath) || resource.fsPath /* can be empty string if '/' is passed in */;
B
Benjamin Pasero 已提交
80 81 82

	// convert c: => C:
	if (hasDriveLetter(base)) {
B
Benjamin Pasero 已提交
83
		return normalizeDriveLetter(base);
B
Benjamin Pasero 已提交
84 85 86 87 88
	}

	return base;
}

89 90 91 92 93 94 95 96 97 98 99 100
function hasDriveLetter(path: string): boolean {
	return platform.isWindows && path && path[1] === ':';
}

export function normalizeDriveLetter(path: string): string {
	if (hasDriveLetter(path)) {
		return path.charAt(0).toUpperCase() + path.slice(1);
	}

	return path;
}

101
export function tildify(path: string, userHome: string): string {
102
	if (path && (platform.isMacintosh || platform.isLinux) && isEqualOrParent(path, userHome, !platform.isLinux /* ignorecase */)) {
103 104 105 106 107 108
		path = `~${path.substr(userHome.length)}`;
	}

	return path;
}

109 110 111 112 113 114 115 116
export function untildify(path: string, userHome: string): string {
	if (platform.isMacintosh || platform.isLinux) {
		path = path.replace(/^~($|\/|\\)/, `${userHome}$1`);
	}

	return path;
}

B
Benjamin Pasero 已提交
117 118 119 120
/**
 * 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.
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
 *
 * 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'.
B
Benjamin Pasero 已提交
145
 */
B
Benjamin Pasero 已提交
146 147
const ellipsis = '\u2026';
const unc = '\\\\';
148
const home = '~';
B
Benjamin Pasero 已提交
149
export function shorten(paths: string[]): string[] {
B
Benjamin Pasero 已提交
150
	const shortenedPaths: string[] = new Array(paths.length);
B
Benjamin Pasero 已提交
151 152

	// for every path
B
Benjamin Pasero 已提交
153 154
	let match = false;
	for (let pathIndex = 0; pathIndex < paths.length; pathIndex++) {
155 156 157
		let path = paths[pathIndex];

		if (path === '') {
A
Anton Vildyaev 已提交
158
			shortenedPaths[pathIndex] = `.${nativeSep}`;
159 160 161 162 163 164 165 166
			continue;
		}

		if (!path) {
			shortenedPaths[pathIndex] = path;
			continue;
		}

B
Benjamin Pasero 已提交
167 168
		match = true;

169
		// trim for now and concatenate unc path (e.g. \\network) or root path (/etc, ~/etc) later
B
Benjamin Pasero 已提交
170
		let prefix = '';
H
hun1ahpu 已提交
171
		if (path.indexOf(unc) === 0) {
B
Benjamin Pasero 已提交
172 173
			prefix = path.substr(0, path.indexOf(unc) + unc.length);
			path = path.substr(path.indexOf(unc) + unc.length);
H
hun1ahpu 已提交
174
		} else if (path.indexOf(nativeSep) === 0) {
B
Benjamin Pasero 已提交
175 176
			prefix = path.substr(0, path.indexOf(nativeSep) + nativeSep.length);
			path = path.substr(path.indexOf(nativeSep) + nativeSep.length);
177 178 179
		} else if (path.indexOf(home) === 0) {
			prefix = path.substr(0, path.indexOf(home) + home.length);
			path = path.substr(path.indexOf(home) + home.length);
180 181
		}

B
Benjamin Pasero 已提交
182
		// pick the first shortest subpath found
183 184 185 186 187 188 189 190 191 192 193 194
		const segments: string[] = path.split(nativeSep);
		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(nativeSep);

				// 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: boolean = (start + subpathLength === segments.length);
195 196 197 198 199

						// 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: string = (start > 0 && paths[otherPathIndex].indexOf(nativeSep) > -1) ? nativeSep + subpath : subpath;
						const isOtherPathEnding: boolean = endsWith(paths[otherPathIndex], subpathWithSep);
200 201

						match = !isSubpathEnding || isOtherPathEnding;
B
Benjamin Pasero 已提交
202
					}
203
				}
B
Benjamin Pasero 已提交
204

205 206 207 208 209 210 211 212 213 214 215
				// found unique subpath
				if (!match) {
					let result = '';

					// preserve disk drive or root prefix
					if (endsWith(segments[0], ':') || prefix !== '') {
						if (start === 1) {
							// extend subpath to include disk drive prefix
							start = 0;
							subpathLength++;
							subpath = segments[0] + nativeSep + subpath;
216
						}
B
Benjamin Pasero 已提交
217

218
						if (start > 0) {
219
							result = segments[0] + nativeSep;
220
						}
B
Benjamin Pasero 已提交
221

222 223 224 225 226 227
						result = prefix + result;
					}

					// add ellipsis at the beginning if neeeded
					if (start > 0) {
						result = result + ellipsis + nativeSep;
228
					}
229 230 231 232 233 234 235 236 237

					result = result + subpath;

					// add ellipsis at the end if needed
					if (start + subpathLength < segments.length) {
						result = result + nativeSep + ellipsis;
					}

					shortenedPaths[pathIndex] = result;
B
Benjamin Pasero 已提交
238 239 240 241 242
				}
			}
		}

		if (match) {
243
			shortenedPaths[pathIndex] = path; // use full path if no unique subpaths found
B
Benjamin Pasero 已提交
244 245 246 247
		}
	}

	return shortenedPaths;
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
}

export interface ISeparator {
	label: string;
}

enum Type {
	TEXT,
	VARIABLE,
	SEPARATOR
}

interface ISegment {
	value: string;
	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 templating is applied
 * @param values the values of the templates to use
 */
export function template(template: string, values: { [key: string]: string | ISeparator } = Object.create(null)): string {
	const segments: ISegment[] = [];

	let inVariable = false;
	let char: string;
	let curVal = '';
	for (let i = 0; i < template.length; i++) {
		char = template[i];

		// Beginning of variable
281
		if (char === '$' || (inVariable && char === '{')) {
282 283 284 285 286 287 288 289 290
			if (curVal) {
				segments.push({ value: curVal, type: Type.TEXT });
			}

			curVal = '';
			inVariable = true;
		}

		// End of variable
291
		else if (char === '}' && inVariable) {
292 293 294 295 296 297 298 299 300 301 302
			const resolved = values[curVal];

			// Variable
			if (typeof resolved === 'string') {
				if (resolved.length) {
					segments.push({ value: resolved, type: Type.VARIABLE });
				}
			}

			// Separator
			else if (resolved) {
303 304 305 306
				const prevSegment = segments[segments.length - 1];
				if (!prevSegment || prevSegment.type !== Type.SEPARATOR) {
					segments.push({ value: resolved.label, type: Type.SEPARATOR }); // prevent duplicate separators
				}
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
			}

			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.value.length > 0);
		}

		// accept any TEXT and VARIABLE
		return true;
	}).map(segment => segment.value).join('');
337 338
}

339 340 341 342 343 344 345 346 347
/**
 * 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: string, forceDisableMnemonics?: boolean): string {
	if (platform.isMacintosh || forceDisableMnemonics) {
		return label.replace(/\(&&\w\)|&&/g, '');
348 349 350
	}

	return label.replace(/&&/g, '&');
351 352
}

353 354 355 356 357 358 359 360 361 362 363 364 365 366
/**
 * Handles mnemonics for buttons. Depending on OS:
 * - Windows: Supported via & character (replace && with &)
 * -   Linux: Supported via _ character (replace && with _)
 * -   macOS: Unsupported (replace && with empty string)
 */
export function mnemonicButtonLabel(label: string): string {
	if (platform.isMacintosh) {
		return label.replace(/\(&&\w\)|&&/g, '');
	}

	return label.replace(/&&/g, platform.isWindows ? '&' : '_');
}

367 368
export function unmnemonicLabel(label: string): string {
	return label.replace(/&/g, '&&');
E
Erich Gamma 已提交
369
}