/
terminalValidatedLocalLinkProvider.ts
187 lines (165 loc) · 8.77 KB
/
terminalValidatedLocalLinkProvider.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Terminal, IViewportRange, IBufferLine } from 'xterm';
import { getXtermLineContent, convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
import { OperatingSystem } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { TerminalLink, OPEN_FILE_LABEL, FOLDER_IN_WORKSPACE_LABEL, FOLDER_NOT_IN_WORKSPACE_LABEL } from 'vs/workbench/contrib/terminal/browser/links/terminalLink';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { isEqualOrParent } from 'vs/base/common/resources';
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 { XtermLinkMatcherHandler } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
import { TerminalBaseLinkProvider } from 'vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider';
const pathPrefix = '(\\.\\.?|\\~)';
const pathSeparatorClause = '\\/';
// '":; 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!$`&*()\\[\\]+\'":;\\\\]';
/** A regex that matches paths in the form /foo, ~/foo, ./foo, ../foo, foo/bar */
export const unixLocalLinkClause = '((' + pathPrefix + '|(' + excludedPathCharactersClause + ')+)?(' + pathSeparatorClause + '(' + excludedPathCharactersClause + ')+)+)';
export const winDrivePrefix = '(?:\\\\\\\\\\?\\\\)?[a-zA-Z]:';
const winPathPrefix = '(' + winDrivePrefix + '|\\.\\.?|\\~)';
const winPathSeparatorClause = '(\\\\|\\/)';
const winExcludedPathCharactersClause = '[^\\0<>\\?\\|\\/\\s!$`&*()\\[\\]+\'":;]';
/** A regex that matches paths in the form \\?\c:\foo c:\foo, ~\foo, .\foo, ..\foo, foo\bar */
export const winLocalLinkClause = '((' + winPathPrefix + '|(' + winExcludedPathCharactersClause + ')+)?(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)+)';
/** As xterm reads from DOM, space in that case is nonbreaking char ASCII code - 160,
replacing space with nonBreakningSpace or space ASCII code - 32. */
export const lineAndColumnClause = [
'((\\S*)", line ((\\d+)( column (\\d+))?))', // "(file path)", line 45 [see #40468]
'((\\S*)",((\\d+)(:(\\d+))?))', // "(file path)",45 [see #78205]
'((\\S*) on line ((\\d+)(, column (\\d+))?))', // (file path) on line 8, column 13
'((\\S*):line ((\\d+)(, column (\\d+))?))', // (file path):line 8, column 13
'(([^\\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 []
'(([^:\\s\\(\\)<>\'\"\\[\\]]*)(:(\\d+))?(:(\\d+))?)' // (file path):336, (file path):336:9
].join('|').replace(/ /g, `[${'\u00A0'} ]`);
// Changing any regex may effect this value, hence changes this as well if required.
export const winLineAndColumnMatchIndex = 12;
export const unixLineAndColumnMatchIndex = 11;
// Each line and column clause have 6 groups (ie no. of expressions in round brackets)
export const lineAndColumnClauseGroupCount = 6;
export class TerminalValidatedLocalLinkProvider extends TerminalBaseLinkProvider {
constructor(
private readonly _xterm: Terminal,
private readonly _processOperatingSystem: OperatingSystem,
private readonly _activateFileCallback: (event: MouseEvent | undefined, link: string) => void,
private readonly _wrapLinkHandler: (handler: (event: MouseEvent | undefined, link: string) => void) => XtermLinkMatcherHandler,
private readonly _tooltipCallback: (link: TerminalLink, viewportRange: IViewportRange, modifierDownCallback?: () => void, modifierUpCallback?: () => void) => void,
private readonly _validationCallback: (link: string, callback: (result: { uri: URI, isDirectory: boolean } | undefined) => void) => void,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ICommandService private readonly _commandService: ICommandService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IHostService private readonly _hostService: IHostService
) {
super();
}
protected async _provideLinks(y: number): Promise<TerminalLink[]> {
const result: TerminalLink[] = [];
let startLine = y - 1;
let endLine = startLine;
const lines: IBufferLine[] = [
this._xterm.buffer.active.getLine(startLine)!
];
while (this._xterm.buffer.active.getLine(startLine)?.isWrapped) {
lines.unshift(this._xterm.buffer.active.getLine(startLine - 1)!);
startLine--;
}
while (this._xterm.buffer.active.getLine(endLine + 1)?.isWrapped) {
lines.push(this._xterm.buffer.active.getLine(endLine + 1)!);
endLine++;
}
const text = getXtermLineContent(this._xterm.buffer.active, startLine, endLine, this._xterm.cols);
// clone regex to do a global search on text
const rex = new RegExp(this._localLinkRegex, 'g');
let match;
let stringIndex = -1;
while ((match = rex.exec(text)) !== null) {
// const link = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
let link = match[0];
if (!link) {
// something matched but does not comply with the given matchIndex
// since this is most likely a bug the regex itself we simply do nothing here
// this._logService.debug('match found without corresponding matchIndex', match, matcher);
break;
}
// Get index, match.index is for the outer match which includes negated chars
// therefore we cannot use match.index directly, instead we search the position
// of the match group in text again
// also correct regex and string search offsets for the next loop run
stringIndex = text.indexOf(link, stringIndex + 1);
rex.lastIndex = stringIndex + link.length;
if (stringIndex < 0) {
// invalid stringIndex (should not have happened)
break;
}
// Adjust the link range to exclude a/ and b/ if it looks like a git diff
if (
// --- a/foo/bar
// +++ b/foo/bar
((text.startsWith('--- a/') || text.startsWith('+++ b/')) && stringIndex === 4) ||
// diff --git a/foo/bar b/foo/bar
(text.startsWith('diff --git') && (link.startsWith('a/') || link.startsWith('b/')))
) {
link = link.substring(2);
stringIndex += 2;
}
// Convert the link text's string index into a wrapped buffer range
const bufferRange = convertLinkRangeToBuffer(lines, this._xterm.cols, {
startColumn: stringIndex + 1,
startLineNumber: 1,
endColumn: stringIndex + link.length + 1,
endLineNumber: 1
}, startLine);
const validatedLink = await new Promise<TerminalLink | undefined>(r => {
this._validationCallback(link, (result) => {
if (result) {
const label = result.isDirectory
? (this._isDirectoryInsideWorkspace(result.uri) ? FOLDER_IN_WORKSPACE_LABEL : FOLDER_NOT_IN_WORKSPACE_LABEL)
: OPEN_FILE_LABEL;
const activateCallback = this._wrapLinkHandler((event: MouseEvent | undefined, text: string) => {
if (result.isDirectory) {
this._handleLocalFolderLink(result.uri);
} else {
this._activateFileCallback(event, text);
}
});
r(this._instantiationService.createInstance(TerminalLink, bufferRange, link, this._xterm.buffer.active.viewportY, activateCallback, this._tooltipCallback, true, label));
} else {
r(undefined);
}
});
});
if (validatedLink) {
result.push(validatedLink);
}
}
return result;
}
protected get _localLinkRegex(): RegExp {
const baseLocalLinkClause = this._processOperatingSystem === OperatingSystem.Windows ? winLocalLinkClause : unixLocalLinkClause;
// Append line and column number regex
return new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`);
}
private async _handleLocalFolderLink(uri: URI): Promise<void> {
// If the folder is within one of the window's workspaces, focus it in the explorer
if (this._isDirectoryInsideWorkspace(uri)) {
await this._commandService.executeCommand('revealInExplorer', uri);
return;
}
// Open a new window for the folder
this._hostService.openWindow([{ folderUri: uri }], { forceNewWindow: true });
}
private _isDirectoryInsideWorkspace(uri: URI) {
const folders = this._workspaceContextService.getWorkspace().folders;
for (let i = 0; i < folders.length; i++) {
if (isEqualOrParent(uri, folders[i].uri)) {
return true;
}
}
return false;
}
}