Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/output/common/outputLinkComputer.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { ILink } from '../../../../editor/common/languages.js';
7
import { URI } from '../../../../base/common/uri.js';
8
import * as extpath from '../../../../base/common/extpath.js';
9
import * as resources from '../../../../base/common/resources.js';
10
import * as strings from '../../../../base/common/strings.js';
11
import { Range } from '../../../../editor/common/core/range.js';
12
import { isWindows } from '../../../../base/common/platform.js';
13
import { Schemas } from '../../../../base/common/network.js';
14
import { IWebWorkerServerRequestHandler, IWebWorkerServer } from '../../../../base/common/worker/webWorker.js';
15
import { WorkerTextModelSyncServer, ICommonModel } from '../../../../editor/common/services/textModelSync/textModelSync.impl.js';
16
17
export interface IResourceCreator {
18
toResource: (folderRelativePath: string) => URI | null;
19
}
20
21
export class OutputLinkComputer implements IWebWorkerServerRequestHandler {
22
_requestHandlerBrand: void = undefined;
23
24
private readonly workerTextModelSyncServer = new WorkerTextModelSyncServer();
25
private patterns = new Map<URI /* folder uri */, RegExp[]>();
26
27
constructor(workerServer: IWebWorkerServer) {
28
this.workerTextModelSyncServer.bindToServer(workerServer);
29
}
30
31
$setWorkspaceFolders(workspaceFolders: string[]) {
32
this.computePatterns(workspaceFolders);
33
}
34
35
private computePatterns(_workspaceFolders: string[]): void {
36
37
// Produce patterns for each workspace root we are configured with
38
// This means that we will be able to detect links for paths that
39
// contain any of the workspace roots as segments.
40
const workspaceFolders = _workspaceFolders
41
.sort((resourceStrA, resourceStrB) => resourceStrB.length - resourceStrA.length) // longest paths first (for https://github.com/microsoft/vscode/issues/88121)
42
.map(resourceStr => URI.parse(resourceStr));
43
44
for (const workspaceFolder of workspaceFolders) {
45
const patterns = OutputLinkComputer.createPatterns(workspaceFolder);
46
this.patterns.set(workspaceFolder, patterns);
47
}
48
}
49
50
private getModel(uri: string): ICommonModel | undefined {
51
return this.workerTextModelSyncServer.getModel(uri);
52
}
53
54
$computeLinks(uri: string): ILink[] {
55
const model = this.getModel(uri);
56
if (!model) {
57
return [];
58
}
59
60
const links: ILink[] = [];
61
const lines = strings.splitLines(model.getValue());
62
63
// For each workspace root patterns
64
for (const [folderUri, folderPatterns] of this.patterns) {
65
const resourceCreator: IResourceCreator = {
66
toResource: (folderRelativePath: string): URI | null => {
67
if (typeof folderRelativePath === 'string') {
68
return resources.joinPath(folderUri, folderRelativePath);
69
}
70
71
return null;
72
}
73
};
74
75
for (let i = 0, len = lines.length; i < len; i++) {
76
links.push(...OutputLinkComputer.detectLinks(lines[i], i + 1, folderPatterns, resourceCreator));
77
}
78
}
79
80
return links;
81
}
82
83
static createPatterns(workspaceFolder: URI): RegExp[] {
84
const patterns: RegExp[] = [];
85
86
const workspaceFolderPath = workspaceFolder.scheme === Schemas.file ? workspaceFolder.fsPath : workspaceFolder.path;
87
const workspaceFolderVariants = [workspaceFolderPath];
88
if (isWindows && workspaceFolder.scheme === Schemas.file) {
89
workspaceFolderVariants.push(extpath.toSlashes(workspaceFolderPath));
90
}
91
92
for (const workspaceFolderVariant of workspaceFolderVariants) {
93
const validPathCharacterPattern = '[^\\s\\(\\):<>\'"]';
94
const validPathCharacterOrSpacePattern = `(?:${validPathCharacterPattern}| ${validPathCharacterPattern})`;
95
const pathPattern = `${validPathCharacterOrSpacePattern}+\\.${validPathCharacterPattern}+`;
96
const strictPathPattern = `${validPathCharacterPattern}+`;
97
98
// Example: /workspaces/express/server.js on line 8, column 13
99
patterns.push(new RegExp(strings.escapeRegExpCharacters(workspaceFolderVariant) + `(${pathPattern}) on line ((\\d+)(, column (\\d+))?)`, 'gi'));
100
101
// Example: /workspaces/express/server.js:line 8, column 13
102
patterns.push(new RegExp(strings.escapeRegExpCharacters(workspaceFolderVariant) + `(${pathPattern}):line ((\\d+)(, column (\\d+))?)`, 'gi'));
103
104
// Example: /workspaces/mankala/Features.ts(45): error
105
// Example: /workspaces/mankala/Features.ts (45): error
106
// Example: /workspaces/mankala/Features.ts(45,18): error
107
// Example: /workspaces/mankala/Features.ts (45,18): error
108
// Example: /workspaces/mankala/Features Special.ts (45,18): error
109
patterns.push(new RegExp(strings.escapeRegExpCharacters(workspaceFolderVariant) + `(${pathPattern})(\\s?\\((\\d+)(,(\\d+))?)\\)`, 'gi'));
110
111
// Example: at /workspaces/mankala/Game.ts
112
// Example: at /workspaces/mankala/Game.ts:336
113
// Example: at /workspaces/mankala/Game.ts:336:9
114
patterns.push(new RegExp(strings.escapeRegExpCharacters(workspaceFolderVariant) + `(${strictPathPattern})(:(\\d+))?(:(\\d+))?`, 'gi'));
115
}
116
117
return patterns;
118
}
119
120
/**
121
* Detect links. Made static to allow for tests.
122
*/
123
static detectLinks(line: string, lineIndex: number, patterns: RegExp[], resourceCreator: IResourceCreator): ILink[] {
124
const links: ILink[] = [];
125
126
patterns.forEach(pattern => {
127
pattern.lastIndex = 0; // the holy grail of software development
128
129
let match: RegExpExecArray | null;
130
let offset = 0;
131
while ((match = pattern.exec(line)) !== null) {
132
133
// Convert the relative path information to a resource that we can use in links
134
const folderRelativePath = strings.rtrim(match[1], '.').replace(/\\/g, '/'); // remove trailing "." that likely indicate end of sentence
135
let resourceString: string | undefined;
136
try {
137
const resource = resourceCreator.toResource(folderRelativePath);
138
if (resource) {
139
resourceString = resource.toString();
140
}
141
} catch (error) {
142
continue; // we might find an invalid URI and then we dont want to loose all other links
143
}
144
145
// Append line/col information to URI if matching
146
if (match[3]) {
147
const lineNumber = match[3];
148
149
if (match[5]) {
150
const columnNumber = match[5];
151
resourceString = strings.format('{0}#{1},{2}', resourceString, lineNumber, columnNumber);
152
} else {
153
resourceString = strings.format('{0}#{1}', resourceString, lineNumber);
154
}
155
}
156
157
const fullMatch = strings.rtrim(match[0], '.'); // remove trailing "." that likely indicate end of sentence
158
159
const index = line.indexOf(fullMatch, offset);
160
offset = index + fullMatch.length;
161
162
const linkRange = {
163
startColumn: index + 1,
164
startLineNumber: lineIndex,
165
endColumn: index + 1 + fullMatch.length,
166
endLineNumber: lineIndex
167
};
168
169
if (links.some(link => Range.areIntersectingOrTouching(link.range, linkRange))) {
170
return; // Do not detect duplicate links
171
}
172
173
links.push({
174
range: linkRange,
175
url: resourceString
176
});
177
}
178
});
179
180
return links;
181
}
182
}
183
184
export function create(workerServer: IWebWorkerServer): OutputLinkComputer {
185
return new OutputLinkComputer(workerServer);
186
}
187
188