terminalLinkManager.ts 25.0 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

D
Daniel Imms 已提交
6
import * as nls from 'vs/nls';
7
import { URI } from 'vs/base/common/uri';
D
Daniel Imms 已提交
8
import { DisposableStore, IDisposable, dispose } from 'vs/base/common/lifecycle';
9
import { IOpenerService } from 'vs/platform/opener/common/opener';
10
import { TerminalWidgetManager, WidgetVerticalAlignment } from 'vs/workbench/contrib/terminal/browser/terminalWidgetManager';
11
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
12
import { ITerminalProcessManager, ITerminalConfigHelper } from 'vs/workbench/contrib/terminal/common/terminal';
B
Benjamin Pasero 已提交
13
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
14
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
15
import { IFileService } from 'vs/platform/files/common/files';
16
import { Terminal, ILinkMatcherOptions, IViewportRange, ITerminalAddon } from 'xterm';
17 18
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { posix, win32 } from 'vs/base/common/path';
19
import { ITerminalInstanceService, ITerminalBeforeHandleLinkEvent, LINK_INTERCEPT_THRESHOLD } from 'vs/workbench/contrib/terminal/browser/terminal';
20
import { OperatingSystem, isMacintosh, OS } from 'vs/base/common/platform';
21
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
22 23
import { Emitter, Event } from 'vs/base/common/event';
import { ILogService } from 'vs/platform/log/common/log';
24
import { TerminalWebLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalWebLinkProvider';
D
Daniel Imms 已提交
25
import { TerminalValidatedLocalLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider';
D
Daniel Imms 已提交
26 27 28
import { TerminalWordLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
29 30 31 32
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { isEqualOrParent } from 'vs/base/common/resources';
33 34
import { ISearchService } from 'vs/workbench/services/search/common/search';
import { QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
35 36

const pathPrefix = '(\\.\\.?|\\~)';
D
Daniel Imms 已提交
37
const pathSeparatorClause = '\\/';
38 39 40
// '":; are allowed in paths but they are often separators so ignore them
// Also disallow \\ to prevent a catastropic backtracking case #24798
const excludedPathCharactersClause = '[^\\0\\s!$`&*()\\[\\]+\'":;\\\\]';
D
Daniel Imms 已提交
41
/** A regex that matches paths in the form /foo, ~/foo, ./foo, ../foo, foo/bar */
42
const unixLocalLinkClause = '((' + pathPrefix + '|(' + excludedPathCharactersClause + ')+)?(' + pathSeparatorClause + '(' + excludedPathCharactersClause + ')+)+)';
D
Daniel Imms 已提交
43

M
Mark Pearce 已提交
44 45
const winDrivePrefix = '[a-zA-Z]:';
const winPathPrefix = '(' + winDrivePrefix + '|\\.\\.?|\\~)';
D
Daniel Imms 已提交
46
const winPathSeparatorClause = '(\\\\|\\/)';
47
const winExcludedPathCharactersClause = '[^\\0<>\\?\\|\\/\\s!$`&*()\\[\\]+\'":;]';
D
Daniel Imms 已提交
48
/** A regex that matches paths in the form c:\foo, ~\foo, .\foo, ..\foo, foo\bar */
49
const winLocalLinkClause = '((' + winPathPrefix + '|(' + winExcludedPathCharactersClause + ')+)?(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)+)';
50 51 52

/** As xterm reads from DOM, space in that case is nonbreaking char ASCII code - 160,
replacing space with nonBreakningSpace or space ASCII code - 32. */
53
const lineAndColumnClause = [
54
	'((\\S*)", line ((\\d+)( column (\\d+))?))', // "(file path)", line 45 [see #40468]
55
	'((\\S*)",((\\d+)(:(\\d+))?))', // "(file path)",45 [see #78205]
56 57
	'((\\S*) on line ((\\d+)(, column (\\d+))?))', // (file path) on line 8, column 13
	'((\\S*):line ((\\d+)(, column (\\d+))?))', // (file path):line 8, column 13
58
	'(([^\\s\\(\\)]*)(\\s?[\\(\\[](\\d+)(,\\s?(\\d+))?)[\\)\\]])', // (file path)(45), (file path) (45), (file path)(45,18), (file path) (45,18), (file path)(45, 18), (file path) (45, 18), also with []
59
	'(([^:\\s\\(\\)<>\'\"\\[\\]]*)(:(\\d+))?(:(\\d+))?)' // (file path):336, (file path):336:9
60
].join('|').replace(/ /g, `[${'\u00A0'} ]`);
61 62 63

// Changing any regex may effect this value, hence changes this as well if required.
const winLineAndColumnMatchIndex = 12;
64
const unixLineAndColumnMatchIndex = 11;
65 66 67 68

// Each line and column clause have 6 groups (ie no. of expressions in round brackets)
const lineAndColumnClauseGroupCount = 6;

69 70 71 72 73
/** Higher than local link, lower than hypertext */
const CUSTOM_LINK_PRIORITY = -1;
/** Lowest */
const LOCAL_LINK_PRIORITY = -2;

D
Daniel Imms 已提交
74
export type XtermLinkMatcherHandler = (event: MouseEvent, link: string) => Promise<void>;
75
export type XtermLinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
D
Daniel Imms 已提交
76

77 78 79 80 81
interface IPath {
	join(...paths: string[]): string;
	normalize(path: string): string;
}

82 83 84 85
/**
 * An object responsible for managing registration of link matchers and link providers.
 */
export class TerminalLinkManager extends DisposableStore {
86 87
	private _widgetManager: TerminalWidgetManager | undefined;
	private _processCwd: string | undefined;
H
Hao Hu 已提交
88 89
	private _gitDiffPreImagePattern: RegExp;
	private _gitDiffPostImagePattern: RegExp;
90
	private readonly _tooltipCallback: (event: MouseEvent, uri: string, location: IViewportRange, linkHandler: (url: string) => void) => boolean | void;
91
	private readonly _leaveCallback: () => void;
J
Jon Bockhorst 已提交
92 93
	private _linkMatchers: number[] = [];
	private _webLinksAddon: ITerminalAddon | undefined;
D
Daniel Imms 已提交
94
	private _linkProviders: IDisposable[] = [];
95
	private _hasBeforeHandleLinkListeners = false;
96
	private readonly _fileQueryBuilder = this._instantiationService.createInstance(QueryBuilder);
97

D
Daniel Imms 已提交
98
	protected static _LINK_INTERCEPT_THRESHOLD = LINK_INTERCEPT_THRESHOLD;
99
	public static readonly LINK_INTERCEPT_THRESHOLD = TerminalLinkManager._LINK_INTERCEPT_THRESHOLD;
D
Daniel Imms 已提交
100

D
Daniel Imms 已提交
101
	private readonly _onBeforeHandleLink = this.add(new Emitter<ITerminalBeforeHandleLinkEvent>({
102 103
		onFirstListenerAdd: () => this._hasBeforeHandleLinkListeners = true,
		onLastListenerRemove: () => this._hasBeforeHandleLinkListeners = false
D
Daniel Imms 已提交
104
	}));
105 106 107 108 109 110
	/**
	 * Allows intercepting links and handling them outside of the default link handler. When fired
	 * the listener has a set amount of time to handle the link or the default handler will fire.
	 * This was designed to only be handled by a single listener.
	 */
	public get onBeforeHandleLink(): Event<ITerminalBeforeHandleLinkEvent> { return this._onBeforeHandleLink.event; }
111

112
	constructor(
113
		private _xterm: Terminal,
114
		private readonly _processManager: ITerminalProcessManager,
115
		private readonly _configHelper: ITerminalConfigHelper,
116
		@IOpenerService private readonly _openerService: IOpenerService,
117
		@IEditorService private readonly _editorService: IEditorService,
118
		@IConfigurationService private readonly _configurationService: IConfigurationService,
D
Daniel Imms 已提交
119
		@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
120
		@IFileService private readonly _fileService: IFileService,
D
Daniel Imms 已提交
121 122
		@ILogService private readonly _logService: ILogService,
		@IInstantiationService private readonly _instantiationService: IInstantiationService,
123 124 125
		@IQuickInputService private readonly _quickInputService: IQuickInputService,
		@ICommandService private readonly _commandService: ICommandService,
		@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
126 127
		@IHostService private readonly _hostService: IHostService,
		@ISearchService private readonly _searchService: ISearchService
128
	) {
D
Daniel Imms 已提交
129 130
		super();

H
Hao Hu 已提交
131 132 133 134 135
		// Matches '--- a/src/file1', capturing 'src/file1' in group 1
		this._gitDiffPreImagePattern = /^--- a\/(\S*)/;
		// Matches '+++ b/src/file1', capturing 'src/file1' in group 1
		this._gitDiffPostImagePattern = /^\+\+\+ b\/(\S*)/;

136
		this._tooltipCallback = (e: MouseEvent, uri: string, location: IViewportRange, linkHandler: (url: string) => void) => {
D
Daniel Imms 已提交
137 138 139
			if (!this._widgetManager) {
				return;
			}
140 141

			// Get the row bottom up
D
Daniel Imms 已提交
142
			let offsetRow = this._xterm.rows - location.start.y;
143
			let verticalAlignment = WidgetVerticalAlignment.Bottom;
144

145
			// Show the tooltip on the top of the next row to avoid obscuring the first row
146 147
			if (location.start.y <= 0) {
				offsetRow = this._xterm.rows - 1;
148
				verticalAlignment = WidgetVerticalAlignment.Top;
149 150 151 152
				// The start of the wrapped line is above the viewport, move to start of the line
				if (location.start.y < 0) {
					location.start.x = 0;
				}
153 154
			}

155
			if (this._configHelper.config.rendererType === 'dom') {
156 157 158 159
				const font = this._configHelper.getFont();
				const charWidth = font.charWidth;
				const charHeight = font.charHeight;

D
Daniel Imms 已提交
160
				const leftPosition = location.start.x * (charWidth! + (font.letterSpacing / window.devicePixelRatio));
161
				const bottomPosition = offsetRow * (Math.ceil(charHeight! * window.devicePixelRatio) * font.lineHeight) / window.devicePixelRatio;
162

163
				this._widgetManager.showMessage(leftPosition, bottomPosition, this._getLinkHoverString(uri), verticalAlignment, linkHandler);
H
Hao Hu 已提交
164
			} else {
165 166 167 168
				const target = (e.target as HTMLElement);
				const colWidth = target.offsetWidth / this._xterm.cols;
				const rowHeight = target.offsetHeight / this._xterm.rows;

D
Daniel Imms 已提交
169
				const leftPosition = location.start.x * colWidth;
170
				const bottomPosition = offsetRow * rowHeight;
171
				this._widgetManager.showMessage(leftPosition, bottomPosition, this._getLinkHoverString(uri), verticalAlignment, linkHandler);
H
Hao Hu 已提交
172 173
			}
		};
174 175 176 177 178
		this._leaveCallback = () => {
			if (this._widgetManager) {
				this._widgetManager.closeMessage();
			}
		};
H
Hao Hu 已提交
179

J
Jon Bockhorst 已提交
180 181 182 183 184 185
		if (this._configHelper.config.experimentalLinkProvider) {
			this.registerLinkProvider();
		} else {
			this._registerLinkMatchers();
		}

J
Jon Bockhorst 已提交
186
		this._configurationService?.onDidChangeConfiguration(e => {
J
Jon Bockhorst 已提交
187 188 189 190 191
			if (e.affectsConfiguration('terminal.integrated.experimentalLinkProvider')) {
				if (this._configHelper.config.experimentalLinkProvider) {
					this._deregisterLinkMatchers();
					this.registerLinkProvider();
				} else {
D
Daniel Imms 已提交
192 193
					dispose(this._linkProviders);
					this._linkProviders.length = 0;
J
Jon Bockhorst 已提交
194 195 196 197 198 199 200
					this._registerLinkMatchers();
				}
			}
		});
	}

	private _registerLinkMatchers() {
201
		this.registerWebLinkHandler();
202
		if (this._processManager) {
203 204 205
			if (this._configHelper.config.enableFileLinks) {
				this.registerLocalLinkHandler();
			}
206 207
			this.registerGitDiffLinkHandlers();
		}
208 209
	}

J
Jon Bockhorst 已提交
210 211 212 213 214 215 216 217
	private _deregisterLinkMatchers() {
		this._webLinksAddon?.dispose();

		this._linkMatchers.forEach(matcherId => {
			this._xterm.deregisterLinkMatcher(matcherId);
		});
	}

D
Daniel Imms 已提交
218
	public setWidgetManager(widgetManager: TerminalWidgetManager): void {
219 220 221
		this._widgetManager = widgetManager;
	}

222 223
	public set processCwd(processCwd: string) {
		this._processCwd = processCwd;
D
Daniel Imms 已提交
224 225
	}

226
	public registerCustomLinkHandler(regex: RegExp, handler: (uri: string) => void, matchIndex?: number, validationCallback?: XtermLinkMatcherValidationCallback): number {
227 228 229
		const tooltipCallback = (event: MouseEvent, uri: string, location: IViewportRange) => {
			this._tooltipCallback(event, uri, location, handler);
		};
230
		const options: ILinkMatcherOptions = {
231
			matchIndex,
232
			tooltipCallback,
233
			leaveCallback: this._leaveCallback,
234
			willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
235
			priority: CUSTOM_LINK_PRIORITY
236 237 238 239 240
		};
		if (validationCallback) {
			options.validationCallback = (uri: string, callback: (isValid: boolean) => void) => validationCallback(uri, callback);
		}
		return this._xterm.registerLinkMatcher(regex, this._wrapLinkHandler(handler), options);
241 242
	}

243
	public registerWebLinkHandler(): void {
D
Daniel Imms 已提交
244
		this._terminalInstanceService.getXtermWebLinksConstructor().then((WebLinksAddon) => {
245 246 247
			if (!this._xterm) {
				return;
			}
248 249
			const wrappedHandler = this._wrapLinkHandler(link => {
				this._handleHypertextLink(link);
D
Daniel Imms 已提交
250
			});
251 252 253
			const tooltipCallback = (event: MouseEvent, uri: string, location: IViewportRange) => {
				this._tooltipCallback(event, uri, location, this._handleHypertextLink.bind(this));
			};
J
Jon Bockhorst 已提交
254
			this._webLinksAddon = new WebLinksAddon(wrappedHandler, {
255
				validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateWebLink(callback),
256
				tooltipCallback,
D
Daniel Imms 已提交
257 258
				leaveCallback: this._leaveCallback,
				willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e)
J
Jon Bockhorst 已提交
259 260
			});
			this._xterm.loadAddon(this._webLinksAddon);
261 262 263 264
		});
	}

	public registerLocalLinkHandler(): void {
265 266 267
		const wrappedHandler = this._wrapLinkHandler(url => {
			this._handleLocalLink(url);
		});
268 269 270
		const tooltipCallback = (event: MouseEvent, uri: string, location: IViewportRange) => {
			this._tooltipCallback(event, uri, location, this._handleLocalLink.bind(this));
		};
J
Jon Bockhorst 已提交
271
		this._linkMatchers.push(this._xterm.registerLinkMatcher(this._localLinkRegex, wrappedHandler, {
272
			validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateLocalLink(uri, callback),
273
			tooltipCallback,
274
			leaveCallback: this._leaveCallback,
275
			willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
276
			priority: LOCAL_LINK_PRIORITY
J
Jon Bockhorst 已提交
277
		}));
D
Daniel Imms 已提交
278 279
	}

H
Hao Hu 已提交
280 281 282 283
	public registerGitDiffLinkHandlers(): void {
		const wrappedHandler = this._wrapLinkHandler(url => {
			this._handleLocalLink(url);
		});
284 285 286
		const tooltipCallback = (event: MouseEvent, uri: string, location: IViewportRange) => {
			this._tooltipCallback(event, uri, location, this._handleLocalLink.bind(this));
		};
H
Hao Hu 已提交
287 288 289
		const options = {
			matchIndex: 1,
			validationCallback: (uri: string, callback: (isValid: boolean) => void) => this._validateLocalLink(uri, callback),
290
			tooltipCallback,
291
			leaveCallback: this._leaveCallback,
H
Hao Hu 已提交
292 293 294
			willLinkActivate: (e: MouseEvent) => this._isLinkActivationModifierDown(e),
			priority: LOCAL_LINK_PRIORITY
		};
J
Jon Bockhorst 已提交
295 296 297 298 299
		this._linkMatchers.push(this._xterm.registerLinkMatcher(this._gitDiffPreImagePattern, wrappedHandler, options));
		this._linkMatchers.push(this._xterm.registerLinkMatcher(this._gitDiffPostImagePattern, wrappedHandler, options));
	}

	public registerLinkProvider(): void {
300
		// Web links
D
Daniel Imms 已提交
301
		const tooltipWebCallback = (event: MouseEvent, link: string, location: IViewportRange) => {
D
Daniel Imms 已提交
302
			this._tooltipCallback(event, link, location, this._handleHypertextLink.bind(this, link));
303
		};
D
Daniel Imms 已提交
304
		const wrappedActivateCallback = this._wrapLinkHandler(this._handleHypertextLink.bind(this));
D
Daniel Imms 已提交
305 306 307
		this._linkProviders.push(this._xterm.registerLinkProvider(
			new TerminalWebLinkProvider(this._xterm, wrappedActivateCallback, tooltipWebCallback, this._leaveCallback)
		));
308

D
Daniel Imms 已提交
309
		// Validated local links
D
Daniel Imms 已提交
310
		const tooltipValidatedLocalCallback = (event: MouseEvent, link: string, location: IViewportRange) => {
311 312
			this._tooltipCallback(event, link, location, this._handleLocalLink.bind(this, link));
		};
313 314
		const wrappedTextLinkActivateCallback = this._wrapLinkHandler(this._handleLocalLink.bind(this));
		const wrappedDirectoryLinkActivateCallback = this._wrapLinkHandler2(this._handleLocalFolderLink.bind(this));
D
Daniel Imms 已提交
315
		this._linkProviders.push(this._xterm.registerLinkProvider(
316 317 318 319 320 321 322
			new TerminalValidatedLocalLinkProvider(
				this._xterm, this._processManager.os || OS,
				wrappedTextLinkActivateCallback,
				wrappedDirectoryLinkActivateCallback,
				tooltipValidatedLocalCallback,
				this._leaveCallback,
				async (link, cb) => cb(await this._resolvePath(link)))
D
Daniel Imms 已提交
323
		));
D
Daniel Imms 已提交
324

D
Daniel Imms 已提交
325
		// Word links
D
Daniel Imms 已提交
326 327 328
		const tooltipWordCallback = (event: MouseEvent, link: string, location: IViewportRange) => {
			this._tooltipCallback(event, link, location, link => this._quickInputService.quickAccess.show(link));
		};
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
		const wrappedWordActivateCallback = this._wrapLinkHandler(async link => {
			const results = await this._searchService.fileSearch(
				this._fileQueryBuilder.file(this._workspaceContextService.getWorkspace().folders, {
					filePattern: link,
					maxResults: 2
				})
			);

			// If there was exactly one match, open it
			if (results.results.length === 1) {
				const match = results.results[0];
				await this._editorService.openEditor({ resource: match.resource, options: { pinned: true } });
				return;
			}

			// Fallback to searching quick access
			this._quickInputService.quickAccess.show(link);
		});
D
Daniel Imms 已提交
347 348 349
		this._linkProviders.push(this._xterm.registerLinkProvider(
			this._instantiationService.createInstance(TerminalWordLinkProvider, this._xterm, wrappedWordActivateCallback, tooltipWordCallback, this._leaveCallback)
		));
H
Hao Hu 已提交
350 351
	}

D
Daniel Imms 已提交
352 353
	protected _wrapLinkHandler(handler: (link: string) => void): XtermLinkMatcherHandler {
		return async (event: MouseEvent, link: string) => {
C
cleidigh 已提交
354 355
			// Prevent default electron link handling so Alt+Click mode works normally
			event.preventDefault();
356

357 358
			// Require correct modifier on click
			if (!this._isLinkActivationModifierDown(event)) {
359
				return;
360
			}
361 362 363

			// Allow the link to be intercepted if there are listeners
			if (this._hasBeforeHandleLinkListeners) {
364
				const wasHandled = await this._triggerBeforeHandleLinkListeners(link);
D
Daniel Imms 已提交
365 366 367
				if (!wasHandled) {
					handler(link);
				}
368
				return;
369
			}
370 371 372

			// Just call the handler if there is no before listener
			handler(link);
D
Daniel Imms 已提交
373 374 375
		};
	}

376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
	protected _wrapLinkHandler2(handler: (uri: URI) => void): (event: MouseEvent, link: string, uri: URI) => Promise<void> {
		return async (event: MouseEvent, link: string, uri: URI) => {
			// Prevent default electron link handling so Alt+Click mode works normally
			event.preventDefault();

			// Require correct modifier on click
			if (!this._isLinkActivationModifierDown(event)) {
				return;
			}

			// Allow the link to be intercepted if there are listeners
			if (this._hasBeforeHandleLinkListeners) {
				const wasHandled = await this._triggerBeforeHandleLinkListeners(link);
				if (!wasHandled) {
					handler(uri);
				}
				return;
			}

			// Just call the handler if there is no before listener
			handler(uri);
		};
	}

	private async _triggerBeforeHandleLinkListeners(link: string): Promise<boolean> {
		return new Promise<boolean>(r => {
			const timeoutId = setTimeout(() => {
				canceled = true;
				this._logService.error(`An extension intecepted a terminal link but it timed out after ${TerminalLinkManager.LINK_INTERCEPT_THRESHOLD / 1000} seconds`);
				r(false);
			}, TerminalLinkManager.LINK_INTERCEPT_THRESHOLD);
			let canceled = false;
			const resolve = (handled: boolean) => {
				if (!canceled) {
					clearTimeout(timeoutId);
					r(handled);
				}
			};
			this._onBeforeHandleLink.fire({ link, resolve });
		});
	}

D
Daniel Imms 已提交
418
	protected get _localLinkRegex(): RegExp {
419 420 421
		if (!this._processManager) {
			throw new Error('Process manager is required');
		}
D
Daniel Imms 已提交
422
		const baseLocalLinkClause = this._processManager.os === OperatingSystem.Windows ? winLocalLinkClause : unixLocalLinkClause;
423 424
		// Append line and column number regex
		return new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`);
425 426
	}

H
Hao Hu 已提交
427 428 429 430 431 432 433 434
	protected get _gitDiffPreImageRegex(): RegExp {
		return this._gitDiffPreImagePattern;
	}

	protected get _gitDiffPostImageRegex(): RegExp {
		return this._gitDiffPostImagePattern;
	}

435
	private async _handleLocalLink(link: string): Promise<void> {
436
		// TODO: This gets resolved again but doesn't need to as it's already validated
437 438 439 440 441 442 443 444 445
		const resolvedLink = await this._resolvePath(link);
		if (!resolvedLink) {
			return;
		}
		const lineColumnInfo: LineColumnInfo = this.extractLineColumnInfo(link);
		const selection: ITextEditorSelection = {
			startLineNumber: lineColumnInfo.lineNumber,
			startColumn: lineColumnInfo.columnNumber
		};
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
		await this._editorService.openEditor({ resource: resolvedLink.uri, options: { pinned: true, selection } });
	}

	private async _handleLocalFolderLink(uri: URI): Promise<void> {
		// If the folder is within one of the window's workspaces, focus it in the explorer
		const folders = this._workspaceContextService.getWorkspace().folders;
		for (let i = 0; i < folders.length; i++) {
			if (isEqualOrParent(uri, folders[0].uri)) {
				await this._commandService.executeCommand('revealInExplorer', uri);
				return;
			}
		}

		// Open a new window for the folder
		this._hostService.openWindow([{ folderUri: uri }], { forceNewWindow: true });
461 462
	}

D
Daniel Imms 已提交
463 464
	private _validateLocalLink(link: string, callback: (isValid: boolean) => void): void {
		this._resolvePath(link).then(resolvedLink => callback(!!resolvedLink));
D
Daniel Imms 已提交
465
	}
466

467
	private _validateWebLink(callback: (isValid: boolean) => void): void {
D
Daniel Imms 已提交
468 469 470
		callback(true);
	}

471
	private _handleHypertextLink(url: string): void {
472
		this._openerService.open(url, { allowTunneling: !!(this._processManager && this._processManager.remoteAuthority) });
C
cleidigh 已提交
473 474
	}

D
Daniel Imms 已提交
475
	protected _isLinkActivationModifierDown(event: MouseEvent): boolean {
476
		const editorConf = this._configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor');
477 478 479
		if (editorConf.multiCursorModifier === 'ctrlCmd') {
			return !!event.altKey;
		}
D
Daniel Imms 已提交
480
		return isMacintosh ? event.metaKey : event.ctrlKey;
481 482
	}

483
	private _getLinkHoverString(uri: string): IMarkdownString {
484
		const editorConf = this._configurationService.getValue<{ multiCursorModifier: 'ctrlCmd' | 'alt' }>('editor');
485 486

		let label = '';
487
		if (editorConf.multiCursorModifier === 'ctrlCmd') {
D
Daniel Imms 已提交
488
			if (isMacintosh) {
489
				label = nls.localize('terminalLinkHandler.followLinkAlt.mac', "Option + click");
490
			} else {
491
				label = nls.localize('terminalLinkHandler.followLinkAlt', "Alt + click");
492
			}
D
Daniel Imms 已提交
493 494 495 496 497 498
		} else {
			if (isMacintosh) {
				label = nls.localize('terminalLinkHandler.followLinkCmd', "Cmd + click");
			} else {
				label = nls.localize('terminalLinkHandler.followLinkCtrl', "Ctrl + click");
			}
499
		}
500

501
		return new MarkdownString(`[Follow Link](${uri}) (${label})`, true);
502 503
	}

504
	private get osPath(): IPath {
505 506 507
		if (!this._processManager) {
			throw new Error('Process manager is required');
		}
D
Daniel Imms 已提交
508
		if (this._processManager.os === OperatingSystem.Windows) {
509 510 511 512 513
			return win32;
		}
		return posix;
	}

514
	protected _preprocessPath(link: string): string | null {
515 516 517
		if (!this._processManager) {
			throw new Error('Process manager is required');
		}
518 519
		if (link.charAt(0) === '~') {
			// Resolve ~ -> userHome
D
Daniel Imms 已提交
520 521 522
			if (!this._processManager.userHome) {
				return null;
			}
523 524 525
			link = this.osPath.join(this._processManager.userHome, link.substring(1));
		} else if (link.charAt(0) !== '/' && link.charAt(0) !== '~') {
			// Resolve workspace path . | .. | <relative_path> -> <path>/. | <path>/.. | <path>/<relative_path>
D
Daniel Imms 已提交
526
			if (this._processManager.os === OperatingSystem.Windows) {
527 528 529 530 531 532
				if (!link.match('^' + winDrivePrefix)) {
					if (!this._processCwd) {
						// Abort if no workspace is open
						return null;
					}
					link = this.osPath.join(this._processCwd, link);
D
Daniel Imms 已提交
533
				}
534
			} else {
535
				if (!this._processCwd) {
D
Daniel Imms 已提交
536
					// Abort if no workspace is open
M
Mark Pearce 已提交
537
					return null;
D
Daniel Imms 已提交
538
				}
539
				link = this.osPath.join(this._processCwd, link);
D
Daniel Imms 已提交
540 541
			}
		}
542 543
		link = this.osPath.normalize(link);

M
Mark Pearce 已提交
544 545 546
		return link;
	}

547
	private async _resolvePath(link: string): Promise<{ uri: URI, isDirectory: boolean } | undefined> {
548 549 550 551
		if (!this._processManager) {
			throw new Error('Process manager is required');
		}

552 553
		const preprocessedLink = this._preprocessPath(link);
		if (!preprocessedLink) {
554
			return undefined;
M
Mark Pearce 已提交
555
		}
556

557
		const linkUrl = this.extractLinkUrl(preprocessedLink);
558
		if (!linkUrl) {
559
			return undefined;
560 561
		}

562 563 564 565 566 567 568 569 570 571
		try {
			let uri: URI;
			if (this._processManager.remoteAuthority) {
				uri = URI.from({
					scheme: REMOTE_HOST_SCHEME,
					authority: this._processManager.remoteAuthority,
					path: linkUrl
				});
			} else {
				uri = URI.file(linkUrl);
572
			}
573

574 575
			try {
				const stat = await this._fileService.resolve(uri);
576
				return { uri, isDirectory: stat.isDirectory };
577 578
			}
			catch (e) {
579
				// Does not exist
580 581
				return undefined;
			}
582 583
		} catch {
			// Errors in parsing the path
584
			return undefined;
585
		}
586
	}
587 588 589 590 591 592

	/**
	 * Returns line and column number of URl if that is present.
	 *
	 * @param link Url link which may contain line and column number.
	 */
593
	public extractLineColumnInfo(link: string): LineColumnInfo {
594

595
		const matches: string[] | null = this._localLinkRegex.exec(link);
596 597 598 599
		const lineColumnInfo: LineColumnInfo = {
			lineNumber: 1,
			columnNumber: 1
		};
600

601
		if (!matches || !this._processManager) {
602 603 604
			return lineColumnInfo;
		}

D
Daniel Imms 已提交
605
		const lineAndColumnMatchIndex = this._processManager.os === OperatingSystem.Windows ? winLineAndColumnMatchIndex : unixLineAndColumnMatchIndex;
606 607
		for (let i = 0; i < lineAndColumnClause.length; i++) {
			const lineMatchIndex = lineAndColumnMatchIndex + (lineAndColumnClauseGroupCount * i);
608 609
			const rowNumber = matches[lineMatchIndex];
			if (rowNumber) {
610
				lineColumnInfo['lineNumber'] = parseInt(rowNumber, 10);
611
				// Check if column number exists
612 613
				const columnNumber = matches[lineMatchIndex + 2];
				if (columnNumber) {
614
					lineColumnInfo['columnNumber'] = parseInt(columnNumber, 10);
615 616 617 618 619 620 621 622 623 624 625 626 627
				}
				break;
			}
		}

		return lineColumnInfo;
	}

	/**
	 * Returns url from link as link may contain line and column information.
	 *
	 * @param link url link which may contain line and column number.
	 */
628 629
	public extractLinkUrl(link: string): string | null {
		const matches: string[] | null = this._localLinkRegex.exec(link);
630 631 632
		if (!matches) {
			return null;
		}
633 634
		return matches[1];
	}
635
}
636 637

export interface LineColumnInfo {
638 639
	lineNumber: number;
	columnNumber: number;
640
}