Path: blob/main/extensions/copilot/src/extension/linkify/vscode-node/symbolLinkifier.ts
13399 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 * as vscode from 'vscode';6import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';7import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';8import { collapseRangeToStart } from '../../../util/common/range';9import { CancellationToken } from '../../../util/vs/base/common/cancellation';10import { SymbolInformation, Uri } from '../../../vscodeTypes';11import { LinkifiedPart, LinkifiedText, LinkifySymbolAnchor } from '../common/linkifiedText';12import { IContributedLinkifier, LinkifierContext } from '../common/linkifyService';13import { findBestSymbolByPath } from './findSymbol';1415/**16* Linkifies symbol paths in responses. For example:17*18* ```19* [`symbol`](file.md)20* ```21*/22export class SymbolLinkifier implements IContributedLinkifier {2324constructor(25@IFileSystemService private readonly fileSystem: IFileSystemService,26@IWorkspaceService private readonly workspaceService: IWorkspaceService,27) { }2829async linkify(30text: string,31context: LinkifierContext,32token: CancellationToken,33): Promise<LinkifiedText | undefined> {34const workspaceFolders = this.workspaceService.getWorkspaceFolders();35if (!workspaceFolders.length) {36return;37}3839const out: LinkifiedPart[] = [];4041let endLastMatch = 0;42for (const match of text.matchAll(/\[`([^`\[\]]+?)`]\((\S+?\.\w+)\)/g)) {43const prefix = text.slice(endLastMatch, match.index);44if (prefix) {45out.push(prefix);46}4748const symbolText = match[1];49let symbolPath = match[2];50try {51symbolPath = decodeURIComponent(symbolPath);52} catch {53// noop54}5556const resolvedUri = await this.resolveInWorkspace(symbolPath, workspaceFolders);5758if (resolvedUri) {59const info: SymbolInformation = {60name: symbolText,61containerName: '',62kind: vscode.SymbolKind.Variable,63location: new vscode.Location(resolvedUri, new vscode.Position(0, 0))64};6566out.push(new LinkifySymbolAnchor(info, async (token) => {67let symbols: Array<vscode.SymbolInformation | vscode.DocumentSymbol> | undefined;68try {69symbols = await vscode.commands.executeCommand<Array<vscode.SymbolInformation | vscode.DocumentSymbol> | undefined>('vscode.executeDocumentSymbolProvider', resolvedUri);70} catch (e) {71// Noop72}7374if (symbols?.length) {75const matchingSymbol = findBestSymbolByPath(symbols, symbolText);76if (matchingSymbol) {77info.kind = matchingSymbol.kind;7879// Not a real instance of 'vscode.DocumentSymbol' so use cast to check80if ((matchingSymbol as vscode.DocumentSymbol).children) {81const symbol = matchingSymbol as vscode.DocumentSymbol;82info.location = new vscode.Location(resolvedUri, collapseRangeToStart(symbol.selectionRange));83} else {84const symbol = matchingSymbol as vscode.SymbolInformation;85info.location = new vscode.Location(symbol.location.uri, collapseRangeToStart(symbol.location.range));86}87}88}89return info;90}));91} else {92out.push('`' + symbolText + '`');93}9495endLastMatch = match.index + match[0].length;96}9798const suffix = text.slice(endLastMatch);99if (suffix) {100out.push(suffix);101}102103return { parts: out };104}105106private async resolveInWorkspace(symbolPath: string, workspaceFolders: readonly Uri[]): Promise<Uri | undefined> {107const candidates = workspaceFolders.map(folder => Uri.joinPath(folder, symbolPath));108const results = await Promise.all(candidates.map(uri => this.exists(uri).then(exists => exists ? uri : undefined)));109return results.find((uri): uri is Uri => uri !== undefined);110}111112private async exists(uri: Uri) {113try {114await this.fileSystem.stat(uri);115return true;116} catch {117return false;118}119}120}121122123