Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/linkify/vscode-node/findWord.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
6
import * as vscode from 'vscode';
7
import { TextDocument } from 'vscode-languageserver-textdocument';
8
import { TreeSitterExpressionInfo } from '../../../platform/parser/node/nodes';
9
import { IParserService } from '../../../platform/parser/node/parserService';
10
import { getWasmLanguage } from '../../../platform/parser/node/treeSitterLanguages';
11
import { getLanguageForResource } from '../../../util/common/languages';
12
import { Limiter } from '../../../util/vs/base/common/async';
13
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
14
import { escapeRegExpCharacters } from '../../../util/vs/base/common/strings';
15
import { isUriComponents, URI } from '../../../util/vs/base/common/uri';
16
import { IInstantiationService, ServicesAccessor } from '../../../util/vs/platform/instantiation/common/instantiation';
17
import { PromptReference } from '../../prompt/common/conversation';
18
19
/**
20
* How the word was resolved.
21
*/
22
enum ResolvedWordLocationType {
23
// Ordered by priority. Higher properties are preferred.
24
25
/** Resolve using string matching */
26
TextualMatch = 1,
27
28
/** Resolve by matching a symbol name in code */
29
SymbolMatch = 2,
30
31
/** Resolve by matching a definition in code */
32
// TODO: not implemented yet
33
Definition = 3,
34
}
35
36
interface ResolvedWordLocation {
37
readonly type: ResolvedWordLocationType;
38
readonly location: vscode.Location;
39
}
40
41
interface FindWordOptions {
42
readonly symbolMatchesOnly?: boolean;
43
readonly maxResultCount?: number;
44
}
45
46
export async function findWordInReferences(
47
accessor: ServicesAccessor,
48
references: readonly PromptReference[],
49
word: string,
50
options: FindWordOptions,
51
token: CancellationToken,
52
documentCache?: Map<string, Promise<SimpleTextDocument | undefined>>,
53
): Promise<vscode.Location[]> {
54
const parserService = accessor.get(IParserService);
55
56
const out: ResolvedWordLocation[] = [];
57
const maxResultCount = options.maxResultCount ?? Infinity;
58
const limiter = new Limiter<void>(10);
59
try {
60
await Promise.all(references.map(ref =>
61
limiter.queue(async () => {
62
if (out.length >= maxResultCount || token.isCancellationRequested) {
63
return;
64
}
65
66
let loc: ResolvedWordLocation | undefined;
67
if (isUriComponents(ref.anchor)) {
68
loc = await findWordInDoc(parserService, word, ref.anchor, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), options, token, documentCache);
69
} else if ('range' in ref.anchor) {
70
loc = await findWordInDoc(parserService, word, ref.anchor.uri, ref.anchor.range, options, token, documentCache);
71
} else if ('value' in ref.anchor && URI.isUri(ref.anchor.value)) {
72
loc = await findWordInDoc(parserService, word, ref.anchor.value, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), options, token, documentCache);
73
}
74
75
if (loc) {
76
out.push(loc);
77
}
78
})));
79
} finally {
80
limiter.dispose();
81
}
82
83
return out
84
.sort((a, b) => b.type - a.type)
85
.map(x => x.location)
86
.slice(0, options.maxResultCount);
87
}
88
89
async function findWordInDoc(parserService: IParserService, word: string, uri: vscode.Uri, range: vscode.Range, options: FindWordOptions, token: vscode.CancellationToken, documentCache?: Map<string, Promise<SimpleTextDocument | undefined>>): Promise<ResolvedWordLocation | undefined> {
90
if (options.symbolMatchesOnly) {
91
const languageId = getLanguageForResource(uri).languageId;
92
if (!getWasmLanguage(languageId)) {
93
return;
94
}
95
}
96
97
const doc = await openDocument(uri, documentCache);
98
if (!doc || token.isCancellationRequested) {
99
return;
100
}
101
102
const symbols = await getSymbolsInRange(parserService, doc, range, token);
103
if (token.isCancellationRequested) {
104
return;
105
}
106
107
for (const symbol of symbols) {
108
if (symbol.identifier === word) {
109
const pos = doc.positionAt(symbol.startIndex);
110
return { type: ResolvedWordLocationType.SymbolMatch, location: new vscode.Location(uri, pos) };
111
}
112
}
113
114
if (options.symbolMatchesOnly) {
115
return;
116
}
117
118
// Fall back to word based
119
const text = doc.getText(range);
120
const startOffset = doc.offsetAt(range.start);
121
for (const match of text.matchAll(new RegExp(escapeRegExpCharacters(word), 'g'))) {
122
if (match.index) {
123
const wordPos = doc.positionAt(startOffset + match.index);
124
if ('getWordRangeAtPosition' in doc) {
125
const wordInDoc = doc.getText((doc as vscode.TextDocument).getWordRangeAtPosition(wordPos));
126
if (word === wordInDoc) {
127
return { type: ResolvedWordLocationType.TextualMatch, location: new vscode.Location(uri, wordPos) };
128
}
129
} else {
130
const wordInDoc = doc.getText(new vscode.Range(wordPos, doc.positionAt(doc.offsetAt(wordPos) + word.length)));
131
if (word === wordInDoc) {
132
return { type: ResolvedWordLocationType.TextualMatch, location: new vscode.Location(uri, wordPos) };
133
}
134
}
135
}
136
}
137
138
return undefined;
139
}
140
141
142
interface SimpleTextDocument {
143
readonly languageId: string;
144
145
getText(range?: vscode.Range): string;
146
147
offsetAt(position: vscode.Position): number;
148
149
positionAt(offset: number): vscode.Position;
150
}
151
152
153
async function openDocument(uri: vscode.Uri, documentCache?: Map<string, Promise<SimpleTextDocument | undefined>>): Promise<SimpleTextDocument | undefined> {
154
const vsCodeDoc = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString());
155
if (vsCodeDoc) {
156
return vsCodeDoc;
157
}
158
159
if (documentCache) {
160
const key = uri.toString();
161
const existing = documentCache.get(key);
162
if (existing) {
163
return existing;
164
}
165
166
const pending = doOpenDocument(uri);
167
documentCache.set(key, pending);
168
return pending;
169
}
170
171
return doOpenDocument(uri);
172
}
173
174
async function doOpenDocument(uri: vscode.Uri): Promise<SimpleTextDocument | undefined> {
175
try {
176
const contents = await vscode.workspace.fs.readFile(uri);
177
const languageId = getLanguageForResource(uri).languageId;
178
const doc = TextDocument.create(uri.toString(), languageId, 0, new TextDecoder().decode(contents));
179
return new class implements SimpleTextDocument {
180
readonly languageId = languageId;
181
getText(range?: vscode.Range): string {
182
return doc.getText(range);
183
}
184
offsetAt(position: vscode.Position): number {
185
return doc.offsetAt(position);
186
}
187
positionAt(offset: number): vscode.Position {
188
const pos = doc.positionAt(offset);
189
return new vscode.Position(pos.line, pos.character);
190
}
191
};
192
} catch {
193
return undefined;
194
}
195
}
196
197
async function getSymbolsInRange(parserService: IParserService, doc: SimpleTextDocument, range: vscode.Range, token: vscode.CancellationToken): Promise<TreeSitterExpressionInfo[]> {
198
const wasmLanguage = getWasmLanguage(doc.languageId);
199
if (!wasmLanguage) {
200
return [];
201
}
202
203
const ast = parserService.getTreeSitterASTForWASMLanguage(wasmLanguage, doc.getText());
204
if (!ast) {
205
return [];
206
}
207
208
return ast.getSymbols({
209
startIndex: doc.offsetAt(range.start),
210
endIndex: doc.offsetAt(range.end),
211
});
212
}
213
214
export class ReferencesSymbolResolver {
215
/** Symbols which we have already tried to resolve */
216
private readonly cache = new Map<string, Promise<vscode.Location[] | undefined>>();
217
private readonly documentCache = new Map<string, Promise<SimpleTextDocument | undefined>>();
218
219
constructor(
220
private readonly findWordOptions: FindWordOptions,
221
@IInstantiationService private readonly instantiationService: IInstantiationService
222
) { }
223
224
async resolve(codeText: string, references: readonly PromptReference[], token: CancellationToken): Promise<vscode.Location[] | undefined> {
225
if (!references.length) {
226
return;
227
}
228
229
const existing = this.cache.get(codeText);
230
if (existing) {
231
return existing;
232
} else {
233
const p = this.doResolve(codeText, references, token);
234
this.cache.set(codeText, p);
235
return p;
236
}
237
}
238
239
private async doResolve(codeText: string, references: readonly PromptReference[], token: CancellationToken): Promise<vscode.Location[] | undefined> {
240
// Prefer exact match
241
let wordMatches = await this.instantiationService.invokeFunction(accessor => findWordInReferences(accessor, references, codeText, this.findWordOptions, token, this.documentCache));
242
if (token.isCancellationRequested) {
243
return;
244
}
245
246
// But then try breaking up inline code into symbol parts
247
if (!wordMatches.length) {
248
// Extract all symbol parts from the code text
249
// For example: `TextModel.undo()` -> ['TextModel', 'undo']
250
const symbolParts = Array.from(codeText.matchAll(/[#\w$][\w\d$]*/g), x => x[0]);
251
252
if (symbolParts.length >= 2) {
253
// For qualified names like `Class.method()`, search for both parts together
254
// This helps disambiguate when there are multiple methods with the same name
255
const firstPart = symbolParts[0];
256
const lastPart = symbolParts[symbolParts.length - 1];
257
258
// First, try to find the class
259
const classMatches = await this.instantiationService.invokeFunction(accessor => findWordInReferences(accessor, references, firstPart, {
260
symbolMatchesOnly: true,
261
maxResultCount: this.findWordOptions.maxResultCount,
262
}, token, this.documentCache));
263
264
// If we found the class, we'll rely on the click-time resolution to find the method
265
if (classMatches.length) {
266
wordMatches = classMatches;
267
} else {
268
// If no class found, try just the method name as fallback
269
wordMatches = await this.instantiationService.invokeFunction(accessor => findWordInReferences(accessor, references, lastPart, {
270
symbolMatchesOnly: true,
271
maxResultCount: this.findWordOptions.maxResultCount,
272
}, token));
273
}
274
} else if (symbolParts.length > 0) {
275
// For single names like `undo`, try to find the method directly
276
const lastPart = symbolParts[symbolParts.length - 1];
277
278
if (lastPart && lastPart !== codeText) {
279
wordMatches = await this.instantiationService.invokeFunction(accessor => findWordInReferences(accessor, references, lastPart, {
280
symbolMatchesOnly: true,
281
maxResultCount: this.findWordOptions.maxResultCount,
282
}, token, this.documentCache));
283
}
284
}
285
}
286
287
return wordMatches.slice(0, this.findWordOptions.maxResultCount);
288
}
289
}
290
291