Path: blob/main/extensions/copilot/src/extension/linkify/vscode-node/commands.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*--------------------------------------------------------------------------------------------*/4import { t } from '@vscode/l10n';5import * as vscode from 'vscode';6import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';7import { collapseRangeToStart } from '../../../util/common/range';8import { CancellationToken } from '../../../util/vs/base/common/cancellation';9import { combinedDisposable } from '../../../util/vs/base/common/lifecycle';10import { UriComponents } from '../../../util/vs/base/common/uri';11import { openFileLinkCommand, OpenFileLinkCommandArgs, openSymbolInFileCommand, OpenSymbolInFileCommandArgs } from '../common/commands';12import { findBestSymbolByPath } from './findSymbol';1314export const openSymbolFromReferencesCommand = '_github.copilot.openSymbolFromReferences';1516export type OpenSymbolFromReferencesCommandArgs = [_word_unused: string, locations: ReadonlyArray<{ uri: UriComponents; pos: vscode.Position }>, requestId: string | undefined];171819export function registerLinkCommands(20telemetryService: ITelemetryService,21) {22return combinedDisposable(23vscode.commands.registerCommand(openFileLinkCommand, async (...[path, requestId]: OpenFileLinkCommandArgs) => {24/* __GDPR__25"panel.action.filelink" : {26"owner": "digitarald",27"comment": "Clicks on file links in the panel response",28"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Id of the chat request." }29}30*/31telemetryService.sendMSFTTelemetryEvent('panel.action.filelink', {32requestId33});3435const workspaceRoot = vscode.workspace.workspaceFolders?.[0].uri;36if (!workspaceRoot) {37return;38}39const fileUri = typeof path === 'string' ? vscode.Uri.joinPath(workspaceRoot, path) : vscode.Uri.from(path);4041if (await isDirectory(fileUri)) {42await vscode.commands.executeCommand('revealInExplorer', fileUri);43} else {44return vscode.commands.executeCommand('vscode.open', fileUri);45}4647async function isDirectory(uri: vscode.Uri): Promise<boolean> {48if (uri.path.endsWith('/')) {49return true;50}5152try {53const stat = await vscode.workspace.fs.stat(uri);54return stat.type === vscode.FileType.Directory;55} catch {56return false;57}58}59}),6061// Command used when we have a symbol name and file path but not a line number62// This is currently used by the symbol for links such as: [`symbol`](file.ts)63vscode.commands.registerCommand(openSymbolInFileCommand, async (...[inFileUri, symbolText, requestId]: OpenSymbolInFileCommandArgs) => {64const fileUri = vscode.Uri.from(inFileUri);6566let symbols: Array<vscode.SymbolInformation | vscode.DocumentSymbol> | undefined;67try {68symbols = await vscode.commands.executeCommand<Array<vscode.SymbolInformation | vscode.DocumentSymbol> | undefined>('vscode.executeDocumentSymbolProvider', fileUri);69} catch (e) {70console.error(e);71}7273if (symbols?.length) {74const matchingSymbol = findBestSymbolByPath(symbols, symbolText);7576/* __GDPR__77"panel.action.symbollink" : {78"owner": "digitarald",79"comment": "Clicks on symbol links in the panel response",80"hadMatch": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the symbol was found." },81"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Id of the chat request." }82}83*/84telemetryService.sendMSFTTelemetryEvent('panel.action.symbollink', {85requestId,86}, {87hadMatch: matchingSymbol ? 1 : 088});89if (matchingSymbol) {90const range = matchingSymbol instanceof vscode.SymbolInformation ? matchingSymbol.location.range : matchingSymbol.selectionRange;91return vscode.commands.executeCommand('vscode.open', fileUri, {92selection: new vscode.Range(range.start, range.start), // Move cursor to the start of the symbol93} satisfies vscode.TextDocumentShowOptions);94}95}9697return vscode.commands.executeCommand('vscode.open', fileUri);98}),99100// Command used when we have already resolved the link to a location.101// This is currently used by the inline code linkifier for links such as `symbolName`102vscode.commands.registerCommand(openSymbolFromReferencesCommand, async (...[_word, locations, requestId]: OpenSymbolFromReferencesCommandArgs) => {103const dest = await resolveSymbolFromReferences(locations, undefined, CancellationToken.None);104105/* __GDPR__106"panel.action.openSymbolFromReferencesLink" : {107"owner": "mjbvz",108"comment": "Clicks on symbol links in the panel response",109"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Id of the chat request." },110"resolvedDestinationType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "How the link was actually resolved." }111}112*/113telemetryService.sendMSFTTelemetryEvent('panel.action.openSymbolFromReferencesLink', {114requestId,115resolvedDestinationType: dest?.type ?? 'unresolved',116});117118if (dest) {119const selectionRange = dest.loc.targetSelectionRange ?? dest.loc.targetRange;120return vscode.commands.executeCommand('vscode.open', dest.loc.targetUri, {121selection: collapseRangeToStart(selectionRange),122} satisfies vscode.TextDocumentShowOptions);123} else {124return vscode.window.showWarningMessage(t('Could not resolve this symbol in the current workspace.'));125}126})127);128}129130function toLocationLink(def: vscode.Location | vscode.LocationLink): vscode.LocationLink {131if ('uri' in def) {132return { targetUri: def.uri, targetRange: def.range };133} else {134return def;135}136}137138function findSymbolByName(symbols: Array<vscode.SymbolInformation | vscode.DocumentSymbol>, symbolName: string, maxDepth: number = 5): vscode.SymbolInformation | vscode.DocumentSymbol | undefined {139for (const symbol of symbols) {140if (symbol.name === symbolName) {141return symbol;142}143// Check children if it's a DocumentSymbol and we haven't exceeded max depth144if (maxDepth > 0 && 'children' in symbol && symbol.children) {145const found = findSymbolByName(symbol.children, symbolName, maxDepth - 1);146if (found) {147return found;148}149}150}151return undefined;152}153154export async function resolveSymbolFromReferences(locations: ReadonlyArray<{ uri: UriComponents; pos: vscode.Position }>, symbolText: string | undefined, token: CancellationToken) {155let dest: {156type: 'definition' | 'firstOccurrence' | 'unresolved';157loc: vscode.LocationLink;158} | undefined;159160// Extract the rightmost part from qualified symbol like "TextModel.undo()"161const symbolParts = symbolText ? Array.from(symbolText.matchAll(/[#\w$][\w\d$]*/g), x => x[0]) : [];162const targetSymbolName = symbolParts.length >= 2 ? symbolParts[symbolParts.length - 1] : undefined;163164// TODO: These locations may no longer be valid if the user has edited the file since the references were found.165for (const loc of locations) {166try {167const def = (await vscode.commands.executeCommand<vscode.Location[] | vscode.LocationLink[]>('vscode.executeDefinitionProvider', vscode.Uri.from(loc.uri), loc.pos)).at(0);168if (token.isCancellationRequested) {169return;170}171172if (def) {173const defLoc = toLocationLink(def);174175// If we have a qualified name like "TextModel.undo()", try to find the specific symbol in the file176if (targetSymbolName && symbolParts.length >= 2) {177try {178const symbols = await vscode.commands.executeCommand<Array<vscode.SymbolInformation | vscode.DocumentSymbol> | undefined>('vscode.executeDocumentSymbolProvider', defLoc.targetUri);179if (symbols) {180// Search for the target symbol in the document symbols181const targetSymbol = findSymbolByName(symbols, targetSymbolName);182if (targetSymbol) {183let targetRange: vscode.Range;184if ('selectionRange' in targetSymbol) {185targetRange = targetSymbol.selectionRange;186} else {187targetRange = targetSymbol.location.range;188}189dest = {190type: 'definition',191loc: { targetUri: defLoc.targetUri, targetRange: targetRange, targetSelectionRange: targetRange },192};193break;194}195}196} catch {197// Failed to find symbol, fall through to use the first definition198}199}200201dest = {202type: 'definition',203loc: defLoc,204};205break;206}207} catch (e) {208console.error(e);209}210}211212if (!dest) {213const firstLoc = locations.at(0);214if (firstLoc) {215dest = {216type: 'firstOccurrence',217loc: { targetUri: vscode.Uri.from(firstLoc.uri), targetRange: new vscode.Range(firstLoc.pos, firstLoc.pos) }218};219}220}221222return dest;223}224225226