Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/tools/usagesTool.ts
13406 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 { CancellationToken } from '../../../../../base/common/cancellation.js';
7
import { Codicon } from '../../../../../base/common/codicons.js';
8
import { Emitter, Event } from '../../../../../base/common/event.js';
9
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
10
import { escapeRegExpCharacters } from '../../../../../base/common/strings.js';
11
import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';
12
import { ResourceSet } from '../../../../../base/common/map.js';
13
import { ThemeIcon } from '../../../../../base/common/themables.js';
14
import { isEqual, relativePath } from '../../../../../base/common/resources.js';
15
import { Position } from '../../../../../editor/common/core/position.js';
16
import { Range } from '../../../../../editor/common/core/range.js';
17
import { Location, LocationLink } from '../../../../../editor/common/languages.js';
18
import { IModelService } from '../../../../../editor/common/services/model.js';
19
import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';
20
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
21
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
22
import { getDefinitionsAtPosition, getImplementationsAtPosition, getReferencesAtPosition } from '../../../../../editor/contrib/gotoSymbol/browser/goToSymbol.js';
23
import { localize } from '../../../../../nls.js';
24
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
25
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
26
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
27
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
28
import { IWorkbenchContribution } from '../../../../common/contributions.js';
29
import { ISearchService, QueryType, resultIsMatch } from '../../../../services/search/common/search.js';
30
import { ChatConfiguration } from '../../common/constants.js';
31
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolProgress, } from '../../common/tools/languageModelToolsService.js';
32
import { createToolSimpleTextResult } from '../../common/tools/builtinTools/toolHelpers.js';
33
import { errorResult, findLineNumber, findSymbolColumn, ISymbolToolInput, resolveToolUri } from './toolHelpers.js';
34
35
export const UsagesToolId = 'vscode_listCodeUsages';
36
37
const BaseModelDescription = `Find all usages (references, definitions, and implementations) of a code symbol across the workspace. This tool locates where a symbol is referenced, defined, or implemented.
38
39
Input:
40
- "symbol": The exact name of the symbol to search for (function, class, method, variable, type, etc.).
41
- "uri": A full URI (e.g. "file:///path/to/file.ts") of a file where the symbol appears. Provide either "uri" or "filePath".
42
- "filePath": A workspace-relative file path (e.g. "src/utils/helpers.ts") of a file where the symbol appears. Provide either "uri" or "filePath".
43
- "lineContent": A substring of the line of code where the symbol appears. This is used to locate the exact position in the file. Must be the actual text from the file - do NOT fabricate it.
44
45
IMPORTANT: The file and line do NOT need to be the definition of the symbol. Any occurrence works - a usage, an import, a call site, etc. You can pick whichever occurrence is most convenient.
46
47
If the tool returns an error, retry with corrected input - ensure the file path is correct, the line content matches the actual file content, and the symbol name appears in that line.`;
48
49
/**
50
* Static description used when the {@link ChatConfiguration.SymbolToolsCacheStable}
51
* experiment is enabled. Identical to {@link BaseModelDescription} plus a single
52
* sentence describing the unsupported-language behavior. Crucially, this string
53
* does NOT depend on the set of registered reference providers, so it stays
54
* byte-stable across requests as language extensions activate during a turn.
55
*/
56
const StaticModelDescription = BaseModelDescription + `
57
58
If the file's language has no reference provider registered, the tool returns an error.`;
59
60
export class UsagesTool extends Disposable implements IToolImpl {
61
62
private readonly _onDidUpdateToolData = this._store.add(new Emitter<void>());
63
readonly onDidUpdateToolData = this._onDidUpdateToolData.event;
64
65
constructor(
66
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
67
@ILanguageService private readonly _languageService: ILanguageService,
68
@IModelService private readonly _modelService: IModelService,
69
@ISearchService private readonly _searchService: ISearchService,
70
@ITextModelService private readonly _textModelService: ITextModelService,
71
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
72
@IConfigurationService private readonly _configurationService: IConfigurationService,
73
) {
74
super();
75
76
// In cache-stable mode the tool's wire bytes don't depend on the set
77
// of registered reference providers, so we don't re-fire the update
78
// event on provider changes. Skipping this subscription also avoids
79
// unnecessary tool re-registration churn.
80
if (!this._isCacheStable()) {
81
this._store.add(Event.debounce(
82
this._languageFeaturesService.referenceProvider.onDidChange,
83
() => { },
84
2000
85
)((() => this._onDidUpdateToolData.fire())));
86
}
87
}
88
89
private _isCacheStable(): boolean {
90
return this._configurationService.getValue<boolean>(ChatConfiguration.SymbolToolsCacheStable) === true;
91
}
92
93
getToolData(): IToolData | undefined {
94
if (this._isCacheStable()) {
95
return this._getStaticToolData();
96
}
97
98
const languageIds = this._languageFeaturesService.referenceProvider.registeredLanguageIds;
99
100
if (languageIds.size === 0) {
101
return undefined;
102
}
103
104
let modelDescription = BaseModelDescription;
105
let userDescription: string;
106
if (languageIds.has('*')) {
107
modelDescription += '\n\nSupported for all languages.';
108
userDescription = localize('tool.usages.userDescription', 'Find references, definitions, and implementations of a symbol');
109
} else {
110
const sorted = [...languageIds].sort();
111
modelDescription += `\n\nCurrently supported for: ${sorted.join(', ')}.`;
112
const niceNames = sorted.map(id => this._languageService.getLanguageName(id) ?? id);
113
userDescription = localize('tool.usages.userDescriptionWithLanguages', 'Find references, definitions, and implementations of a symbol ({0})', niceNames.join(', '));
114
}
115
116
return this._buildToolData(modelDescription, userDescription);
117
}
118
119
private _getStaticToolData(): IToolData {
120
return this._buildToolData(
121
StaticModelDescription,
122
localize('tool.usages.userDescription', 'Find references, definitions, and implementations of a symbol'),
123
);
124
}
125
126
private _buildToolData(modelDescription: string, userDescription: string): IToolData {
127
return {
128
id: UsagesToolId,
129
toolReferenceName: 'usages',
130
canBeReferencedInPrompt: false,
131
icon: ThemeIcon.fromId(Codicon.references.id),
132
displayName: localize('tool.usages.displayName', 'List Code Usages'),
133
userDescription,
134
modelDescription,
135
source: ToolDataSource.Internal,
136
when: ContextKeyExpr.has('config.chat.tools.usagesTool.enabled'),
137
inputSchema: {
138
type: 'object',
139
properties: {
140
symbol: {
141
type: 'string',
142
description: 'The exact name of the symbol (function, class, method, variable, type, etc.) to find usages of.'
143
},
144
uri: {
145
type: 'string',
146
description: 'A full URI of a file where the symbol appears (e.g. "file:///path/to/file.ts"). Provide either "uri" or "filePath".'
147
},
148
filePath: {
149
type: 'string',
150
description: 'A workspace-relative file path where the symbol appears (e.g. "src/utils/helpers.ts"). Provide either "uri" or "filePath".'
151
},
152
lineContent: {
153
type: 'string',
154
description: 'A substring of the line of code where the symbol appears. Used to locate the exact position. Must be actual text from the file.'
155
}
156
},
157
required: ['symbol', 'lineContent']
158
}
159
};
160
}
161
162
async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
163
const input = context.parameters as ISymbolToolInput;
164
return {
165
invocationMessage: localize('tool.usages.invocationMessage', 'Analyzing usages of `{0}`', input.symbol),
166
};
167
}
168
169
async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise<IToolResult> {
170
const input = invocation.parameters as ISymbolToolInput;
171
172
// --- resolve URI ---
173
const uri = resolveToolUri(input, this._workspaceContextService);
174
if (!uri) {
175
return errorResult('Provide either "uri" (a full URI) or "filePath" (a workspace-relative path) to identify the file.');
176
}
177
178
// --- open text model ---
179
const ref = await this._textModelService.createModelReference(uri);
180
try {
181
const model = ref.object.textEditorModel;
182
183
if (!this._languageFeaturesService.referenceProvider.has(model)) {
184
return errorResult(`No reference provider available for this file's language. The usages tool may not support this language.`);
185
}
186
187
// --- find line containing lineContent ---
188
const lineNumber = findLineNumber(model, input.lineContent);
189
if (lineNumber === undefined) {
190
return errorResult(`Could not find line content "${input.lineContent}" in ${uri.toString()}. Provide the exact text from the line where the symbol appears.`);
191
}
192
193
// --- find symbol in that line ---
194
const lineText = model.getLineContent(lineNumber);
195
const column = findSymbolColumn(lineText, input.symbol);
196
if (column === undefined) {
197
return errorResult(`Could not find symbol "${input.symbol}" in the matched line. Ensure the symbol name is correct and appears in the provided line content.`);
198
}
199
200
const position = new Position(lineNumber, column);
201
202
// --- query references, definitions, implementations in parallel ---
203
const [definitions, references, implementations] = await Promise.all([
204
getDefinitionsAtPosition(this._languageFeaturesService.definitionProvider, model, position, false, token),
205
getReferencesAtPosition(this._languageFeaturesService.referenceProvider, model, position, false, false, token),
206
getImplementationsAtPosition(this._languageFeaturesService.implementationProvider, model, position, false, token),
207
]);
208
209
if (references.length === 0) {
210
const result = createToolSimpleTextResult(`No usages found for \`${input.symbol}\`.`);
211
result.toolResultMessage = new MarkdownString(localize('tool.usages.noResults', 'Analyzed usages of `{0}`, no results', input.symbol));
212
return result;
213
}
214
215
// --- classify and format results with previews ---
216
const previews = await this._getLinePreviews(input.symbol, references, token);
217
218
const lines: string[] = [];
219
lines.push(`${references.length} usages of \`${input.symbol}\`:\n`);
220
221
for (let i = 0; i < references.length; i++) {
222
const ref = references[i];
223
const kind = this._classifyReference(ref, definitions, implementations);
224
const startLine = Range.lift(ref.range).startLineNumber;
225
const preview = previews[i];
226
if (preview) {
227
lines.push(`<usage type="${kind}" uri="${ref.uri.toString()}" line="${startLine}">`);
228
lines.push(`\t${preview}`);
229
lines.push(`</usage>`);
230
} else {
231
lines.push(`<usage type="${kind}" uri="${ref.uri.toString()}" line="${startLine}" />`);
232
}
233
}
234
235
const text = lines.join('\n');
236
const result = createToolSimpleTextResult(text);
237
238
result.toolResultMessage = references.length === 1
239
? new MarkdownString(localize('tool.usages.oneResult', 'Analyzed usages of `{0}`, 1 result', input.symbol))
240
: new MarkdownString(localize('tool.usages.results', 'Analyzed usages of `{0}`, {1} results', input.symbol, references.length));
241
242
result.toolResultDetails = references.map((r): Location => ({ uri: r.uri, range: r.range }));
243
244
return result;
245
} finally {
246
ref.dispose();
247
}
248
}
249
250
private async _getLinePreviews(symbol: string, references: LocationLink[], token: CancellationToken): Promise<(string | undefined)[]> {
251
const previews: (string | undefined)[] = new Array(references.length);
252
253
// Build a lookup: (uriString, lineNumber) → index in references array
254
const lookup = new Map<string, number>();
255
const needSearch = new ResourceSet();
256
257
for (let i = 0; i < references.length; i++) {
258
const ref = references[i];
259
const lineNumber = Range.lift(ref.range).startLineNumber;
260
261
// Try already-open models first
262
const existingModel = this._modelService.getModel(ref.uri);
263
if (existingModel) {
264
previews[i] = existingModel.getLineContent(lineNumber).trim();
265
} else {
266
lookup.set(`${ref.uri.toString()}:${lineNumber}`, i);
267
needSearch.add(ref.uri);
268
}
269
}
270
271
if (needSearch.size === 0 || token.isCancellationRequested) {
272
return previews;
273
}
274
275
// Use ISearchService to search for the symbol name, restricted to the
276
// referenced files. This is backed by ripgrep for file:// URIs.
277
try {
278
// Build includePattern from workspace-relative paths
279
const folders = this._workspaceContextService.getWorkspace().folders;
280
const relativePaths: string[] = [];
281
for (const uri of needSearch) {
282
const folder = this._workspaceContextService.getWorkspaceFolder(uri);
283
if (folder) {
284
const rel = relativePath(folder.uri, uri);
285
if (rel) {
286
relativePaths.push(rel);
287
}
288
}
289
}
290
291
if (relativePaths.length > 0) {
292
const includePattern: Record<string, true> = {};
293
if (relativePaths.length === 1) {
294
includePattern[relativePaths[0]] = true;
295
} else {
296
includePattern[`{${relativePaths.join(',')}}`] = true;
297
}
298
299
const searchResult = await this._searchService.textSearch(
300
{
301
type: QueryType.Text,
302
contentPattern: { pattern: escapeRegExpCharacters(symbol), isRegExp: true, isWordMatch: true },
303
folderQueries: folders.map(f => ({ folder: f.uri })),
304
includePattern,
305
},
306
token,
307
);
308
309
for (const fileMatch of searchResult.results) {
310
if (!fileMatch.results) {
311
continue;
312
}
313
for (const textMatch of fileMatch.results) {
314
if (!resultIsMatch(textMatch)) {
315
continue;
316
}
317
for (const range of textMatch.rangeLocations) {
318
const lineNumber = range.source.startLineNumber + 1; // 0-based → 1-based
319
const key = `${fileMatch.resource.toString()}:${lineNumber}`;
320
const idx = lookup.get(key);
321
if (idx !== undefined) {
322
previews[idx] = textMatch.previewText.trim();
323
lookup.delete(key);
324
}
325
}
326
}
327
}
328
}
329
} catch {
330
// search might fail, leave remaining previews as undefined
331
}
332
333
return previews;
334
}
335
336
private _classifyReference(ref: LocationLink, definitions: LocationLink[], implementations: LocationLink[]): string {
337
if (definitions.some(d => this._overlaps(ref, d))) {
338
return 'definition';
339
}
340
if (implementations.some(d => this._overlaps(ref, d))) {
341
return 'implementation';
342
}
343
return 'reference';
344
}
345
346
private _overlaps(a: LocationLink, b: LocationLink): boolean {
347
if (!isEqual(a.uri, b.uri)) {
348
return false;
349
}
350
return Range.areIntersectingOrTouching(a.range, b.range);
351
}
352
353
}
354
355
export class UsagesToolContribution extends Disposable implements IWorkbenchContribution {
356
357
static readonly ID = 'chat.usagesTool';
358
359
constructor(
360
@ILanguageModelToolsService toolsService: ILanguageModelToolsService,
361
@IInstantiationService instantiationService: IInstantiationService,
362
) {
363
super();
364
365
const usagesTool = this._store.add(instantiationService.createInstance(UsagesTool));
366
367
let registration: IDisposable | undefined;
368
const registerUsagesTool = () => {
369
registration?.dispose();
370
registration = undefined;
371
toolsService.flushToolUpdates();
372
const toolData = usagesTool.getToolData();
373
if (toolData) {
374
registration = toolsService.registerTool(toolData, usagesTool);
375
}
376
};
377
registerUsagesTool();
378
this._store.add(usagesTool.onDidUpdateToolData(registerUsagesTool));
379
this._store.add({ dispose: () => registration?.dispose() });
380
}
381
}
382
383