Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/linkify/vscode-node/commands.ts
13399 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
import { t } from '@vscode/l10n';
6
import * as vscode from 'vscode';
7
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
8
import { collapseRangeToStart } from '../../../util/common/range';
9
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
10
import { combinedDisposable } from '../../../util/vs/base/common/lifecycle';
11
import { UriComponents } from '../../../util/vs/base/common/uri';
12
import { openFileLinkCommand, OpenFileLinkCommandArgs, openSymbolInFileCommand, OpenSymbolInFileCommandArgs } from '../common/commands';
13
import { findBestSymbolByPath } from './findSymbol';
14
15
export const openSymbolFromReferencesCommand = '_github.copilot.openSymbolFromReferences';
16
17
export type OpenSymbolFromReferencesCommandArgs = [_word_unused: string, locations: ReadonlyArray<{ uri: UriComponents; pos: vscode.Position }>, requestId: string | undefined];
18
19
20
export function registerLinkCommands(
21
telemetryService: ITelemetryService,
22
) {
23
return combinedDisposable(
24
vscode.commands.registerCommand(openFileLinkCommand, async (...[path, requestId]: OpenFileLinkCommandArgs) => {
25
/* __GDPR__
26
"panel.action.filelink" : {
27
"owner": "digitarald",
28
"comment": "Clicks on file links in the panel response",
29
"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Id of the chat request." }
30
}
31
*/
32
telemetryService.sendMSFTTelemetryEvent('panel.action.filelink', {
33
requestId
34
});
35
36
const workspaceRoot = vscode.workspace.workspaceFolders?.[0].uri;
37
if (!workspaceRoot) {
38
return;
39
}
40
const fileUri = typeof path === 'string' ? vscode.Uri.joinPath(workspaceRoot, path) : vscode.Uri.from(path);
41
42
if (await isDirectory(fileUri)) {
43
await vscode.commands.executeCommand('revealInExplorer', fileUri);
44
} else {
45
return vscode.commands.executeCommand('vscode.open', fileUri);
46
}
47
48
async function isDirectory(uri: vscode.Uri): Promise<boolean> {
49
if (uri.path.endsWith('/')) {
50
return true;
51
}
52
53
try {
54
const stat = await vscode.workspace.fs.stat(uri);
55
return stat.type === vscode.FileType.Directory;
56
} catch {
57
return false;
58
}
59
}
60
}),
61
62
// Command used when we have a symbol name and file path but not a line number
63
// This is currently used by the symbol for links such as: [`symbol`](file.ts)
64
vscode.commands.registerCommand(openSymbolInFileCommand, async (...[inFileUri, symbolText, requestId]: OpenSymbolInFileCommandArgs) => {
65
const fileUri = vscode.Uri.from(inFileUri);
66
67
let symbols: Array<vscode.SymbolInformation | vscode.DocumentSymbol> | undefined;
68
try {
69
symbols = await vscode.commands.executeCommand<Array<vscode.SymbolInformation | vscode.DocumentSymbol> | undefined>('vscode.executeDocumentSymbolProvider', fileUri);
70
} catch (e) {
71
console.error(e);
72
}
73
74
if (symbols?.length) {
75
const matchingSymbol = findBestSymbolByPath(symbols, symbolText);
76
77
/* __GDPR__
78
"panel.action.symbollink" : {
79
"owner": "digitarald",
80
"comment": "Clicks on symbol links in the panel response",
81
"hadMatch": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the symbol was found." },
82
"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Id of the chat request." }
83
}
84
*/
85
telemetryService.sendMSFTTelemetryEvent('panel.action.symbollink', {
86
requestId,
87
}, {
88
hadMatch: matchingSymbol ? 1 : 0
89
});
90
if (matchingSymbol) {
91
const range = matchingSymbol instanceof vscode.SymbolInformation ? matchingSymbol.location.range : matchingSymbol.selectionRange;
92
return vscode.commands.executeCommand('vscode.open', fileUri, {
93
selection: new vscode.Range(range.start, range.start), // Move cursor to the start of the symbol
94
} satisfies vscode.TextDocumentShowOptions);
95
}
96
}
97
98
return vscode.commands.executeCommand('vscode.open', fileUri);
99
}),
100
101
// Command used when we have already resolved the link to a location.
102
// This is currently used by the inline code linkifier for links such as `symbolName`
103
vscode.commands.registerCommand(openSymbolFromReferencesCommand, async (...[_word, locations, requestId]: OpenSymbolFromReferencesCommandArgs) => {
104
const dest = await resolveSymbolFromReferences(locations, undefined, CancellationToken.None);
105
106
/* __GDPR__
107
"panel.action.openSymbolFromReferencesLink" : {
108
"owner": "mjbvz",
109
"comment": "Clicks on symbol links in the panel response",
110
"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Id of the chat request." },
111
"resolvedDestinationType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "How the link was actually resolved." }
112
}
113
*/
114
telemetryService.sendMSFTTelemetryEvent('panel.action.openSymbolFromReferencesLink', {
115
requestId,
116
resolvedDestinationType: dest?.type ?? 'unresolved',
117
});
118
119
if (dest) {
120
const selectionRange = dest.loc.targetSelectionRange ?? dest.loc.targetRange;
121
return vscode.commands.executeCommand('vscode.open', dest.loc.targetUri, {
122
selection: collapseRangeToStart(selectionRange),
123
} satisfies vscode.TextDocumentShowOptions);
124
} else {
125
return vscode.window.showWarningMessage(t('Could not resolve this symbol in the current workspace.'));
126
}
127
})
128
);
129
}
130
131
function toLocationLink(def: vscode.Location | vscode.LocationLink): vscode.LocationLink {
132
if ('uri' in def) {
133
return { targetUri: def.uri, targetRange: def.range };
134
} else {
135
return def;
136
}
137
}
138
139
function findSymbolByName(symbols: Array<vscode.SymbolInformation | vscode.DocumentSymbol>, symbolName: string, maxDepth: number = 5): vscode.SymbolInformation | vscode.DocumentSymbol | undefined {
140
for (const symbol of symbols) {
141
if (symbol.name === symbolName) {
142
return symbol;
143
}
144
// Check children if it's a DocumentSymbol and we haven't exceeded max depth
145
if (maxDepth > 0 && 'children' in symbol && symbol.children) {
146
const found = findSymbolByName(symbol.children, symbolName, maxDepth - 1);
147
if (found) {
148
return found;
149
}
150
}
151
}
152
return undefined;
153
}
154
155
export async function resolveSymbolFromReferences(locations: ReadonlyArray<{ uri: UriComponents; pos: vscode.Position }>, symbolText: string | undefined, token: CancellationToken) {
156
let dest: {
157
type: 'definition' | 'firstOccurrence' | 'unresolved';
158
loc: vscode.LocationLink;
159
} | undefined;
160
161
// Extract the rightmost part from qualified symbol like "TextModel.undo()"
162
const symbolParts = symbolText ? Array.from(symbolText.matchAll(/[#\w$][\w\d$]*/g), x => x[0]) : [];
163
const targetSymbolName = symbolParts.length >= 2 ? symbolParts[symbolParts.length - 1] : undefined;
164
165
// TODO: These locations may no longer be valid if the user has edited the file since the references were found.
166
for (const loc of locations) {
167
try {
168
const def = (await vscode.commands.executeCommand<vscode.Location[] | vscode.LocationLink[]>('vscode.executeDefinitionProvider', vscode.Uri.from(loc.uri), loc.pos)).at(0);
169
if (token.isCancellationRequested) {
170
return;
171
}
172
173
if (def) {
174
const defLoc = toLocationLink(def);
175
176
// If we have a qualified name like "TextModel.undo()", try to find the specific symbol in the file
177
if (targetSymbolName && symbolParts.length >= 2) {
178
try {
179
const symbols = await vscode.commands.executeCommand<Array<vscode.SymbolInformation | vscode.DocumentSymbol> | undefined>('vscode.executeDocumentSymbolProvider', defLoc.targetUri);
180
if (symbols) {
181
// Search for the target symbol in the document symbols
182
const targetSymbol = findSymbolByName(symbols, targetSymbolName);
183
if (targetSymbol) {
184
let targetRange: vscode.Range;
185
if ('selectionRange' in targetSymbol) {
186
targetRange = targetSymbol.selectionRange;
187
} else {
188
targetRange = targetSymbol.location.range;
189
}
190
dest = {
191
type: 'definition',
192
loc: { targetUri: defLoc.targetUri, targetRange: targetRange, targetSelectionRange: targetRange },
193
};
194
break;
195
}
196
}
197
} catch {
198
// Failed to find symbol, fall through to use the first definition
199
}
200
}
201
202
dest = {
203
type: 'definition',
204
loc: defLoc,
205
};
206
break;
207
}
208
} catch (e) {
209
console.error(e);
210
}
211
}
212
213
if (!dest) {
214
const firstLoc = locations.at(0);
215
if (firstLoc) {
216
dest = {
217
type: 'firstOccurrence',
218
loc: { targetUri: vscode.Uri.from(firstLoc.uri), targetRange: new vscode.Range(firstLoc.pos, firstLoc.pos) }
219
};
220
}
221
}
222
223
return dest;
224
}
225
226