mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-24 02:28:34 +01:00
186 lines
6.7 KiB
TypeScript
186 lines
6.7 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { IMirrorModel, IWorkerContext } from 'vs/editor/common/services/editorSimpleWorker';
|
|
import { ILink } from 'vs/editor/common/modes';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import * as extpath from 'vs/base/common/extpath';
|
|
import * as resources from 'vs/base/common/resources';
|
|
import * as strings from 'vs/base/common/strings';
|
|
import { Range } from 'vs/editor/common/core/range';
|
|
import { isWindows } from 'vs/base/common/platform';
|
|
import { Schemas } from 'vs/base/common/network';
|
|
import { find } from 'vs/base/common/arrays';
|
|
|
|
export interface ICreateData {
|
|
workspaceFolders: string[];
|
|
}
|
|
|
|
export interface IResourceCreator {
|
|
toResource: (folderRelativePath: string) => URI | null;
|
|
}
|
|
|
|
export class OutputLinkComputer {
|
|
private ctx: IWorkerContext;
|
|
private patterns: Map<URI /* folder uri */, RegExp[]>;
|
|
|
|
constructor(ctx: IWorkerContext, createData: ICreateData) {
|
|
this.ctx = ctx;
|
|
this.patterns = new Map<URI, RegExp[]>();
|
|
|
|
this.computePatterns(createData);
|
|
}
|
|
|
|
private computePatterns(createData: ICreateData): void {
|
|
|
|
// Produce patterns for each workspace root we are configured with
|
|
// This means that we will be able to detect links for paths that
|
|
// contain any of the workspace roots as segments.
|
|
const workspaceFolders = createData.workspaceFolders.map(r => URI.parse(r));
|
|
workspaceFolders.forEach(workspaceFolder => {
|
|
const patterns = OutputLinkComputer.createPatterns(workspaceFolder);
|
|
this.patterns.set(workspaceFolder, patterns);
|
|
});
|
|
}
|
|
|
|
private getModel(uri: string): IMirrorModel | undefined {
|
|
const models = this.ctx.getMirrorModels();
|
|
return find(models, model => model.uri.toString() === uri);
|
|
}
|
|
|
|
public computeLinks(uri: string): Promise<ILink[]> {
|
|
const model = this.getModel(uri);
|
|
if (!model) {
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
const links: ILink[] = [];
|
|
const lines = model.getValue().split(/\r\n|\r|\n/);
|
|
|
|
// For each workspace root patterns
|
|
this.patterns.forEach((folderPatterns, folderUri) => {
|
|
const resourceCreator: IResourceCreator = {
|
|
toResource: (folderRelativePath: string): URI | null => {
|
|
if (typeof folderRelativePath === 'string') {
|
|
return resources.joinPath(folderUri, folderRelativePath);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
};
|
|
|
|
for (let i = 0, len = lines.length; i < len; i++) {
|
|
links.push(...OutputLinkComputer.detectLinks(lines[i], i + 1, folderPatterns, resourceCreator));
|
|
}
|
|
});
|
|
|
|
return Promise.resolve(links);
|
|
}
|
|
|
|
public static createPatterns(workspaceFolder: URI): RegExp[] {
|
|
const patterns: RegExp[] = [];
|
|
|
|
const workspaceFolderPath = workspaceFolder.scheme === Schemas.file ? workspaceFolder.fsPath : workspaceFolder.path;
|
|
const workspaceFolderVariants = [workspaceFolderPath];
|
|
if (isWindows && workspaceFolder.scheme === Schemas.file) {
|
|
workspaceFolderVariants.push(extpath.toSlashes(workspaceFolderPath));
|
|
}
|
|
|
|
workspaceFolderVariants.forEach(workspaceFolderVariant => {
|
|
const validPathCharacterPattern = '[^\\s\\(\\):<>"]';
|
|
const validPathCharacterOrSpacePattern = `(?:${validPathCharacterPattern}| ${validPathCharacterPattern})`;
|
|
const pathPattern = `${validPathCharacterOrSpacePattern}+\\.${validPathCharacterPattern}+`;
|
|
const strictPathPattern = `${validPathCharacterPattern}+`;
|
|
|
|
// Example: /workspaces/express/server.js on line 8, column 13
|
|
patterns.push(new RegExp(strings.escapeRegExpCharacters(workspaceFolderVariant) + `(${pathPattern}) on line ((\\d+)(, column (\\d+))?)`, 'gi'));
|
|
|
|
// Example: /workspaces/express/server.js:line 8, column 13
|
|
patterns.push(new RegExp(strings.escapeRegExpCharacters(workspaceFolderVariant) + `(${pathPattern}):line ((\\d+)(, column (\\d+))?)`, 'gi'));
|
|
|
|
// Example: /workspaces/mankala/Features.ts(45): error
|
|
// Example: /workspaces/mankala/Features.ts (45): error
|
|
// Example: /workspaces/mankala/Features.ts(45,18): error
|
|
// Example: /workspaces/mankala/Features.ts (45,18): error
|
|
// Example: /workspaces/mankala/Features Special.ts (45,18): error
|
|
patterns.push(new RegExp(strings.escapeRegExpCharacters(workspaceFolderVariant) + `(${pathPattern})(\\s?\\((\\d+)(,(\\d+))?)\\)`, 'gi'));
|
|
|
|
// Example: at /workspaces/mankala/Game.ts
|
|
// Example: at /workspaces/mankala/Game.ts:336
|
|
// Example: at /workspaces/mankala/Game.ts:336:9
|
|
patterns.push(new RegExp(strings.escapeRegExpCharacters(workspaceFolderVariant) + `(${strictPathPattern})(:(\\d+))?(:(\\d+))?`, 'gi'));
|
|
});
|
|
|
|
return patterns;
|
|
}
|
|
|
|
/**
|
|
* Detect links. Made public static to allow for tests.
|
|
*/
|
|
public static detectLinks(line: string, lineIndex: number, patterns: RegExp[], resourceCreator: IResourceCreator): ILink[] {
|
|
const links: ILink[] = [];
|
|
|
|
patterns.forEach(pattern => {
|
|
pattern.lastIndex = 0; // the holy grail of software development
|
|
|
|
let match: RegExpExecArray | null;
|
|
let offset = 0;
|
|
while ((match = pattern.exec(line)) !== null) {
|
|
|
|
// Convert the relative path information to a resource that we can use in links
|
|
const folderRelativePath = strings.rtrim(match[1], '.').replace(/\\/g, '/'); // remove trailing "." that likely indicate end of sentence
|
|
let resourceString: string | undefined;
|
|
try {
|
|
const resource = resourceCreator.toResource(folderRelativePath);
|
|
if (resource) {
|
|
resourceString = resource.toString();
|
|
}
|
|
} catch (error) {
|
|
continue; // we might find an invalid URI and then we dont want to loose all other links
|
|
}
|
|
|
|
// Append line/col information to URI if matching
|
|
if (match[3]) {
|
|
const lineNumber = match[3];
|
|
|
|
if (match[5]) {
|
|
const columnNumber = match[5];
|
|
resourceString = strings.format('{0}#{1},{2}', resourceString, lineNumber, columnNumber);
|
|
} else {
|
|
resourceString = strings.format('{0}#{1}', resourceString, lineNumber);
|
|
}
|
|
}
|
|
|
|
const fullMatch = strings.rtrim(match[0], '.'); // remove trailing "." that likely indicate end of sentence
|
|
|
|
const index = line.indexOf(fullMatch, offset);
|
|
offset += index + fullMatch.length;
|
|
|
|
const linkRange = {
|
|
startColumn: index + 1,
|
|
startLineNumber: lineIndex,
|
|
endColumn: index + 1 + fullMatch.length,
|
|
endLineNumber: lineIndex
|
|
};
|
|
|
|
if (links.some(link => Range.areIntersectingOrTouching(link.range, linkRange))) {
|
|
return; // Do not detect duplicate links
|
|
}
|
|
|
|
links.push({
|
|
range: linkRange,
|
|
url: resourceString
|
|
});
|
|
}
|
|
});
|
|
|
|
return links;
|
|
}
|
|
}
|
|
|
|
export function create(ctx: IWorkerContext, createData: ICreateData): OutputLinkComputer {
|
|
return new OutputLinkComputer(ctx, createData);
|
|
}
|