Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/context/node/resolvers/selectionContextHelpers.ts
13405 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
6
import type * as vscode from 'vscode';
7
import { VsCodeTextDocument } from '../../../../platform/editing/common/abstractText';
8
import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';
9
import { ILanguageFeaturesService, isLocationLink } from '../../../../platform/languages/common/languageFeaturesService';
10
import { ILogService } from '../../../../platform/log/common/logService';
11
import { getStructureUsingIndentation } from '../../../../platform/parser/node/indentationStructure';
12
import { TreeSitterExpressionInfo } from '../../../../platform/parser/node/nodes';
13
import { IParserService, ParserWorkerTimeoutError, vscodeToTreeSitterOffsetRange } from '../../../../platform/parser/node/parserService';
14
import { TreeSitterUnknownLanguageError } from '../../../../platform/parser/node/treeSitterLanguages';
15
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
16
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
17
import { ILanguage } from '../../../../util/common/languages';
18
19
/**
20
* @param timeoutMs This function makes several async computations, and each gets up to `3 * timeoutMs`. No guarantee is made about the total time.
21
*/
22
export async function findAllReferencedFunctionImplementationsInSelection(
23
parserService: IParserService,
24
logService: ILogService,
25
telemetryService: ITelemetryService,
26
languageFeaturesService: ILanguageFeaturesService,
27
workspaceService: IWorkspaceService,
28
document: TextDocumentSnapshot,
29
selection: vscode.Range,
30
timeoutMs: number
31
) {
32
const currentDocAST = parserService.getTreeSitterAST(document);
33
if (!currentDocAST) {
34
return [];
35
}
36
37
// Parse all function calls in given selection
38
const treeSitterOffsetRange = vscodeToTreeSitterOffsetRange(selection, document);
39
const callExprs = await asyncComputeWithTimeBudget(logService, telemetryService, document, timeoutMs, () => currentDocAST.getCallExpressions(treeSitterOffsetRange), []);
40
41
// find implementation or, if not found, definition for a call expression
42
async function findImplementation(callExpr: TreeSitterExpressionInfo) {
43
const position = document.positionAt(callExpr.startIndex);
44
try {
45
const impls = await languageFeaturesService.getImplementations(document.uri, position);
46
if (impls.length) {
47
return impls;
48
}
49
return await languageFeaturesService.getDefinitions(document.uri, position);
50
} catch {
51
return [];
52
}
53
}
54
55
const implementations = await asyncComputeWithTimeBudget(
56
logService,
57
telemetryService,
58
document,
59
timeoutMs * 3, // apply a more generous timeout for language server results
60
() => Promise.all(callExprs.map(findImplementation)),
61
[]
62
);
63
64
// since language service gives us only links to identifiers, expand to whole implementation/definition using tree-sitter
65
const functionImplementations = [];
66
for (let i = 0; i < implementations.length; i++) {
67
const callExpr = callExprs[i];
68
const impl = implementations[i];
69
for (const link of impl) {
70
const { uri, range } = isLocationLink(link) ? { uri: link.targetUri, range: link.targetRange } : link;
71
const textDocument = await workspaceService.openTextDocumentAndSnapshot(uri);
72
const treeSitterAST = parserService.getTreeSitterAST(textDocument);
73
if (treeSitterAST) {
74
const functionDefinitions = await treeSitterAST.getFunctionDefinitions(); // TODO: we should do this once per document, not once per call expression
75
const functionDefinition = functionDefinitions.find((fn) => fn.identifier === callExpr.identifier); // FIXME: this's incorrect because it doesn't count for import aliases (e.g., `import { foo as bar } from 'baz'`)
76
if (functionDefinition) {
77
const treeSitterRange = vscodeToTreeSitterOffsetRange(range, textDocument);
78
functionImplementations.push({
79
uri,
80
range,
81
version: textDocument.version,
82
identifier: callExpr.identifier,
83
startIndex: treeSitterRange.startIndex,
84
endIndex: treeSitterRange.endIndex,
85
text: functionDefinition.text,
86
});
87
}
88
}
89
}
90
}
91
if (functionImplementations.length !== 0) {
92
return functionImplementations;
93
}
94
95
// For now, just search the current file for all functions
96
const allFunctions = await asyncComputeWithTimeBudget(logService, telemetryService, document, timeoutMs, () => currentDocAST.getFunctionDefinitions(), []);
97
98
// Collect all function implementations referenced in the current selection
99
const allFunctionImplementations: TreeSitterExpressionInfo[] = [];
100
for (const fn of allFunctions) {
101
for (const callExpr of callExprs) {
102
if (fn.identifier === callExpr.identifier) {
103
allFunctionImplementations.push(fn);
104
}
105
}
106
}
107
108
// Sort the function positions by start index
109
return allFunctionImplementations.sort((a, b) => a.startIndex - b.startIndex);
110
}
111
112
export async function findAllReferencedClassDeclarationsInSelection(
113
parserService: IParserService,
114
logService: ILogService,
115
telemetryService: ITelemetryService,
116
languageFeaturesService: ILanguageFeaturesService,
117
workspaceService: IWorkspaceService,
118
document: TextDocumentSnapshot,
119
selection: vscode.Range,
120
timeoutMs: number
121
) {
122
const currentDocAST = parserService.getTreeSitterAST(document);
123
if (!currentDocAST) {
124
return [];
125
}
126
127
// Parse all new expressions in active selection
128
const treeSitterOffsetRange = vscodeToTreeSitterOffsetRange(selection, document);
129
const matches = await asyncComputeWithTimeBudget(logService, telemetryService, document, timeoutMs, () => currentDocAST.getClassReferences(treeSitterOffsetRange), []);
130
131
const implementations = await asyncComputeWithTimeBudget(
132
logService,
133
telemetryService,
134
document,
135
timeoutMs * 3, // apply a more generous timeout for language server results
136
async () => await Promise.all(matches.map(async (match) => {
137
try {
138
const position = document.positionAt(match.startIndex);
139
const impls = await languageFeaturesService.getImplementations(document.uri, position);
140
if (impls.length) {
141
return impls;
142
}
143
return await languageFeaturesService.getDefinitions(document.uri, position);
144
} catch {
145
return [];
146
}
147
})),
148
[]
149
);
150
const classDeclarations = [];
151
for (let i = 0; i < implementations.length; i++) {
152
const match = matches[i];
153
const impl = implementations[i];
154
for (const link of impl) {
155
const { uri, range } = isLocationLink(link) ? { uri: link.targetUri, range: link.targetRange } : link;
156
const textDocument = await workspaceService.openTextDocumentAndSnapshot(uri);
157
const treeSitterAST = parserService.getTreeSitterAST(textDocument);
158
if (treeSitterAST) {
159
const classDeclaration = (await treeSitterAST.getClassDeclarations()).find((fn) => fn.identifier === match.identifier);
160
if (classDeclaration) {
161
const treeSitterRange = vscodeToTreeSitterOffsetRange(range, textDocument);
162
classDeclarations.push({
163
uri,
164
range,
165
version: textDocument.version,
166
identifier: match.identifier,
167
startIndex: treeSitterRange.startIndex,
168
endIndex: treeSitterRange.endIndex,
169
text: classDeclaration.text,
170
});
171
}
172
}
173
}
174
}
175
if (classDeclarations.length !== 0) {
176
return classDeclarations;
177
}
178
179
// For now, just search the current file for all class declarations
180
const allClasses = await asyncComputeWithTimeBudget(logService, telemetryService, document, timeoutMs, () => currentDocAST.getClassDeclarations(), []);
181
182
// Collect all class declarations referenced in the current selection
183
const allClassDeclarations: TreeSitterExpressionInfo[] = [];
184
for (const fn of allClasses) {
185
for (const match of matches) {
186
if (fn.identifier === match.identifier) {
187
allClassDeclarations.push(fn);
188
}
189
}
190
}
191
192
// Sort the class declaration positions by start index
193
return allClassDeclarations.sort((a, b) => a.startIndex - b.startIndex);
194
}
195
196
export async function findAllReferencedTypeDeclarationsInSelection(
197
parserService: IParserService,
198
logService: ILogService,
199
telemetryService: ITelemetryService,
200
_languageFeaturesService: ILanguageFeaturesService,
201
_workspaceService: IWorkspaceService,
202
document: TextDocumentSnapshot,
203
selection: vscode.Range,
204
timeoutMs: number
205
) {
206
const currentDocAST = parserService.getTreeSitterAST(document);
207
if (!currentDocAST) {
208
return [];
209
}
210
211
// Parse all type references in active selection
212
const treeSitterOffsetRange = vscodeToTreeSitterOffsetRange(selection, document);
213
const matches = await asyncComputeWithTimeBudget(logService, telemetryService, document, timeoutMs, () => currentDocAST.getTypeReferences(treeSitterOffsetRange), []);
214
215
// For now, just search the current file for all type declarations
216
const allFunctions = await asyncComputeWithTimeBudget(logService, telemetryService, document, timeoutMs, () => currentDocAST.getTypeDeclarations(), []);
217
218
// Collect all type declarations referenced in the current selection
219
const allTypeDeclarations: TreeSitterExpressionInfo[] = [];
220
for (const fn of allFunctions) {
221
for (const match of matches) {
222
if (fn.identifier === match.identifier) {
223
allTypeDeclarations.push(fn);
224
}
225
}
226
}
227
228
// Sort the type declaration positions by start index
229
return allTypeDeclarations.sort((a, b) => a.startIndex - b.startIndex);
230
}
231
232
/**
233
* Races the promise with a timeout.
234
*/
235
function raceWithTimeout<T>(
236
executor: Promise<T>,
237
timeoutMs: number
238
): Promise<{ type: 'success'; value: T } | { type: 'timeout' }> {
239
if (timeoutMs === 0) {
240
// no timeout
241
return executor.then(value => ({ type: 'success', value }));
242
}
243
244
return new Promise((resolve, reject) => {
245
const timeoutId = setTimeout(() => resolve({ type: 'timeout' }), timeoutMs);
246
executor
247
.then(value => {
248
clearTimeout(timeoutId);
249
resolve({ type: 'success', value });
250
})
251
.catch(err => {
252
clearTimeout(timeoutId);
253
reject(err);
254
});
255
});
256
}
257
258
/**
259
* @returns a promise that resolves to the result of `computation` if the document version is valid, otherwise to `defaultValue`
260
*/
261
export async function asyncComputeWithTimeBudget<T>(
262
logService: ILogService,
263
telemetryService: ITelemetryService,
264
document: TextDocumentSnapshot,
265
timeoutMs: number,
266
computation: () => Promise<T>,
267
defaultValue: T
268
): Promise<T> {
269
try {
270
const functionPositionsResult = await raceWithTimeout(
271
asyncComputeWithValidDocumentVersion(document, computation, defaultValue),
272
timeoutMs
273
);
274
275
if (functionPositionsResult.type === 'success') {
276
return functionPositionsResult.value;
277
} else {
278
logService.warn(`Computing async parser based result took longer than ${timeoutMs}ms`);
279
return defaultValue;
280
}
281
} catch (err) {
282
if (!(err instanceof TreeSitterUnknownLanguageError)) {
283
logService.error(err, `Failed to compute async parser based result`);
284
telemetryService.sendGHTelemetryException(err, 'Failed to compute async parser based result');
285
}
286
return defaultValue;
287
}
288
}
289
290
/**
291
* This function attempts to compute a value based on the provided document, ensuring that the document version remains consistent during the computation.
292
* If the document version changes during the computation, it will retry up to 3 times.
293
* If the document version continues to change after 3 attempts, it will return a default value.
294
*/
295
async function asyncComputeWithValidDocumentVersion<T>(
296
document: TextDocumentSnapshot,
297
computation: () => Promise<T>,
298
defaultValue: T,
299
attempt = 0
300
): Promise<T> {
301
const version = document.version;
302
const positions = await computation();
303
if (document.version !== version) {
304
// the document was changed in the meantime
305
if (attempt < 3) {
306
return asyncComputeWithValidDocumentVersion(document, computation, defaultValue, attempt + 1);
307
}
308
// we tried 3 times, but the document keeps changing
309
return defaultValue;
310
}
311
return positions;
312
}
313
314
/**
315
* Artificial marker used to identify code blocks inside prompts
316
*/
317
export class FilePathCodeMarker {
318
319
public static forDocument(language: ILanguage, document: TextDocumentSnapshot): string {
320
return this.forUri(language, document.uri);
321
}
322
323
public static forUri(language: ILanguage, uri: vscode.Uri): string {
324
return `${this.forLanguage(language)}: ${uri.path}`;
325
}
326
327
public static forLanguage(language: ILanguage): string {
328
return `${language.lineComment.start} FILEPATH`;
329
}
330
331
/**
332
* Checks if the given code starts with a file path marker
333
*/
334
public static testLine(language: ILanguage, code: string): boolean {
335
const filenameMarker = FilePathCodeMarker.forLanguage(language);
336
return code.trimStart().startsWith(filenameMarker);
337
}
338
339
}
340
341
export async function getStructure(parserService: IParserService, document: TextDocumentSnapshot, formattingOptions: vscode.FormattingOptions | undefined) {
342
const currentDocAST = parserService.getTreeSitterAST(document);
343
if (currentDocAST) {
344
try {
345
const result = await currentDocAST.getStructure();
346
if (result) {
347
return result;
348
}
349
} catch (e) {
350
if (!(e instanceof ParserWorkerTimeoutError)) {
351
throw e;
352
}
353
}
354
}
355
return getStructureUsingIndentation(new VsCodeTextDocument(document), document.languageId, formattingOptions);
356
}
357
358