Path: blob/main/src/vs/workbench/contrib/output/common/outputLinkComputer.ts
4780 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { ILink } from '../../../../editor/common/languages.js';6import { URI } from '../../../../base/common/uri.js';7import * as extpath from '../../../../base/common/extpath.js';8import * as resources from '../../../../base/common/resources.js';9import * as strings from '../../../../base/common/strings.js';10import { Range } from '../../../../editor/common/core/range.js';11import { isWindows } from '../../../../base/common/platform.js';12import { Schemas } from '../../../../base/common/network.js';13import { IWebWorkerServerRequestHandler, IWebWorkerServer } from '../../../../base/common/worker/webWorker.js';14import { WorkerTextModelSyncServer, ICommonModel } from '../../../../editor/common/services/textModelSync/textModelSync.impl.js';1516export interface IResourceCreator {17toResource: (folderRelativePath: string) => URI | null;18}1920export class OutputLinkComputer implements IWebWorkerServerRequestHandler {21_requestHandlerBrand: void = undefined;2223private readonly workerTextModelSyncServer = new WorkerTextModelSyncServer();24private patterns = new Map<URI /* folder uri */, RegExp[]>();2526constructor(workerServer: IWebWorkerServer) {27this.workerTextModelSyncServer.bindToServer(workerServer);28}2930$setWorkspaceFolders(workspaceFolders: string[]) {31this.computePatterns(workspaceFolders);32}3334private computePatterns(_workspaceFolders: string[]): void {3536// Produce patterns for each workspace root we are configured with37// This means that we will be able to detect links for paths that38// contain any of the workspace roots as segments.39const workspaceFolders = _workspaceFolders40.sort((resourceStrA, resourceStrB) => resourceStrB.length - resourceStrA.length) // longest paths first (for https://github.com/microsoft/vscode/issues/88121)41.map(resourceStr => URI.parse(resourceStr));4243for (const workspaceFolder of workspaceFolders) {44const patterns = OutputLinkComputer.createPatterns(workspaceFolder);45this.patterns.set(workspaceFolder, patterns);46}47}4849private getModel(uri: string): ICommonModel | undefined {50return this.workerTextModelSyncServer.getModel(uri);51}5253$computeLinks(uri: string): ILink[] {54const model = this.getModel(uri);55if (!model) {56return [];57}5859const links: ILink[] = [];60const lines = strings.splitLines(model.getValue());6162// For each workspace root patterns63for (const [folderUri, folderPatterns] of this.patterns) {64const resourceCreator: IResourceCreator = {65toResource: (folderRelativePath: string): URI | null => {66if (typeof folderRelativePath === 'string') {67return resources.joinPath(folderUri, folderRelativePath);68}6970return null;71}72};7374for (let i = 0, len = lines.length; i < len; i++) {75links.push(...OutputLinkComputer.detectLinks(lines[i], i + 1, folderPatterns, resourceCreator));76}77}7879return links;80}8182static createPatterns(workspaceFolder: URI): RegExp[] {83const patterns: RegExp[] = [];8485const workspaceFolderPath = workspaceFolder.scheme === Schemas.file ? workspaceFolder.fsPath : workspaceFolder.path;86const workspaceFolderVariants = [workspaceFolderPath];87if (isWindows && workspaceFolder.scheme === Schemas.file) {88workspaceFolderVariants.push(extpath.toSlashes(workspaceFolderPath));89}9091for (const workspaceFolderVariant of workspaceFolderVariants) {92const validPathCharacterPattern = '[^\\s\\(\\):<>\'"]';93const validPathCharacterOrSpacePattern = `(?:${validPathCharacterPattern}| ${validPathCharacterPattern})`;94const pathPattern = `${validPathCharacterOrSpacePattern}+\\.${validPathCharacterPattern}+`;95const strictPathPattern = `${validPathCharacterPattern}+`;9697// Example: /workspaces/express/server.js on line 8, column 1398patterns.push(new RegExp(strings.escapeRegExpCharacters(workspaceFolderVariant) + `(${pathPattern}) on line ((\\d+)(, column (\\d+))?)`, 'gi'));99100// Example: /workspaces/express/server.js:line 8, column 13101patterns.push(new RegExp(strings.escapeRegExpCharacters(workspaceFolderVariant) + `(${pathPattern}):line ((\\d+)(, column (\\d+))?)`, 'gi'));102103// Example: /workspaces/mankala/Features.ts(45): error104// Example: /workspaces/mankala/Features.ts (45): error105// Example: /workspaces/mankala/Features.ts(45,18): error106// Example: /workspaces/mankala/Features.ts (45,18): error107// Example: /workspaces/mankala/Features Special.ts (45,18): error108patterns.push(new RegExp(strings.escapeRegExpCharacters(workspaceFolderVariant) + `(${pathPattern})(\\s?\\((\\d+)(,(\\d+))?)\\)`, 'gi'));109110// Example: at /workspaces/mankala/Game.ts111// Example: at /workspaces/mankala/Game.ts:336112// Example: at /workspaces/mankala/Game.ts:336:9113patterns.push(new RegExp(strings.escapeRegExpCharacters(workspaceFolderVariant) + `(${strictPathPattern})(:(\\d+))?(:(\\d+))?`, 'gi'));114}115116return patterns;117}118119/**120* Detect links. Made static to allow for tests.121*/122static detectLinks(line: string, lineIndex: number, patterns: RegExp[], resourceCreator: IResourceCreator): ILink[] {123const links: ILink[] = [];124125patterns.forEach(pattern => {126pattern.lastIndex = 0; // the holy grail of software development127128let match: RegExpExecArray | null;129let offset = 0;130while ((match = pattern.exec(line)) !== null) {131132// Convert the relative path information to a resource that we can use in links133const folderRelativePath = strings.rtrim(match[1], '.').replace(/\\/g, '/'); // remove trailing "." that likely indicate end of sentence134let resourceString: string | undefined;135try {136const resource = resourceCreator.toResource(folderRelativePath);137if (resource) {138resourceString = resource.toString();139}140} catch (error) {141continue; // we might find an invalid URI and then we dont want to loose all other links142}143144// Append line/col information to URI if matching145if (match[3]) {146const lineNumber = match[3];147148if (match[5]) {149const columnNumber = match[5];150resourceString = strings.format('{0}#{1},{2}', resourceString, lineNumber, columnNumber);151} else {152resourceString = strings.format('{0}#{1}', resourceString, lineNumber);153}154}155156const fullMatch = strings.rtrim(match[0], '.'); // remove trailing "." that likely indicate end of sentence157158const index = line.indexOf(fullMatch, offset);159offset = index + fullMatch.length;160161const linkRange = {162startColumn: index + 1,163startLineNumber: lineIndex,164endColumn: index + 1 + fullMatch.length,165endLineNumber: lineIndex166};167168if (links.some(link => Range.areIntersectingOrTouching(link.range, linkRange))) {169return; // Do not detect duplicate links170}171172links.push({173range: linkRange,174url: resourceString175});176}177});178179return links;180}181}182183export function create(workerServer: IWebWorkerServer): OutputLinkComputer {184return new OutputLinkComputer(workerServer);185}186187188