Path: blob/main/src/vs/workbench/contrib/chat/browser/tools/usagesTool.ts
13406 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*--------------------------------------------------------------------------------------------*/45import { CancellationToken } from '../../../../../base/common/cancellation.js';6import { Codicon } from '../../../../../base/common/codicons.js';7import { Emitter, Event } from '../../../../../base/common/event.js';8import { MarkdownString } from '../../../../../base/common/htmlContent.js';9import { escapeRegExpCharacters } from '../../../../../base/common/strings.js';10import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';11import { ResourceSet } from '../../../../../base/common/map.js';12import { ThemeIcon } from '../../../../../base/common/themables.js';13import { isEqual, relativePath } from '../../../../../base/common/resources.js';14import { Position } from '../../../../../editor/common/core/position.js';15import { Range } from '../../../../../editor/common/core/range.js';16import { Location, LocationLink } from '../../../../../editor/common/languages.js';17import { IModelService } from '../../../../../editor/common/services/model.js';18import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';19import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';20import { ILanguageService } from '../../../../../editor/common/languages/language.js';21import { getDefinitionsAtPosition, getImplementationsAtPosition, getReferencesAtPosition } from '../../../../../editor/contrib/gotoSymbol/browser/goToSymbol.js';22import { localize } from '../../../../../nls.js';23import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';24import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';25import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';26import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';27import { IWorkbenchContribution } from '../../../../common/contributions.js';28import { ISearchService, QueryType, resultIsMatch } from '../../../../services/search/common/search.js';29import { ChatConfiguration } from '../../common/constants.js';30import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolProgress, } from '../../common/tools/languageModelToolsService.js';31import { createToolSimpleTextResult } from '../../common/tools/builtinTools/toolHelpers.js';32import { errorResult, findLineNumber, findSymbolColumn, ISymbolToolInput, resolveToolUri } from './toolHelpers.js';3334export const UsagesToolId = 'vscode_listCodeUsages';3536const 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.3738Input:39- "symbol": The exact name of the symbol to search for (function, class, method, variable, type, etc.).40- "uri": A full URI (e.g. "file:///path/to/file.ts") of a file where the symbol appears. Provide either "uri" or "filePath".41- "filePath": A workspace-relative file path (e.g. "src/utils/helpers.ts") of a file where the symbol appears. Provide either "uri" or "filePath".42- "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.4344IMPORTANT: 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.4546If 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.`;4748/**49* Static description used when the {@link ChatConfiguration.SymbolToolsCacheStable}50* experiment is enabled. Identical to {@link BaseModelDescription} plus a single51* sentence describing the unsupported-language behavior. Crucially, this string52* does NOT depend on the set of registered reference providers, so it stays53* byte-stable across requests as language extensions activate during a turn.54*/55const StaticModelDescription = BaseModelDescription + `5657If the file's language has no reference provider registered, the tool returns an error.`;5859export class UsagesTool extends Disposable implements IToolImpl {6061private readonly _onDidUpdateToolData = this._store.add(new Emitter<void>());62readonly onDidUpdateToolData = this._onDidUpdateToolData.event;6364constructor(65@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,66@ILanguageService private readonly _languageService: ILanguageService,67@IModelService private readonly _modelService: IModelService,68@ISearchService private readonly _searchService: ISearchService,69@ITextModelService private readonly _textModelService: ITextModelService,70@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,71@IConfigurationService private readonly _configurationService: IConfigurationService,72) {73super();7475// In cache-stable mode the tool's wire bytes don't depend on the set76// of registered reference providers, so we don't re-fire the update77// event on provider changes. Skipping this subscription also avoids78// unnecessary tool re-registration churn.79if (!this._isCacheStable()) {80this._store.add(Event.debounce(81this._languageFeaturesService.referenceProvider.onDidChange,82() => { },83200084)((() => this._onDidUpdateToolData.fire())));85}86}8788private _isCacheStable(): boolean {89return this._configurationService.getValue<boolean>(ChatConfiguration.SymbolToolsCacheStable) === true;90}9192getToolData(): IToolData | undefined {93if (this._isCacheStable()) {94return this._getStaticToolData();95}9697const languageIds = this._languageFeaturesService.referenceProvider.registeredLanguageIds;9899if (languageIds.size === 0) {100return undefined;101}102103let modelDescription = BaseModelDescription;104let userDescription: string;105if (languageIds.has('*')) {106modelDescription += '\n\nSupported for all languages.';107userDescription = localize('tool.usages.userDescription', 'Find references, definitions, and implementations of a symbol');108} else {109const sorted = [...languageIds].sort();110modelDescription += `\n\nCurrently supported for: ${sorted.join(', ')}.`;111const niceNames = sorted.map(id => this._languageService.getLanguageName(id) ?? id);112userDescription = localize('tool.usages.userDescriptionWithLanguages', 'Find references, definitions, and implementations of a symbol ({0})', niceNames.join(', '));113}114115return this._buildToolData(modelDescription, userDescription);116}117118private _getStaticToolData(): IToolData {119return this._buildToolData(120StaticModelDescription,121localize('tool.usages.userDescription', 'Find references, definitions, and implementations of a symbol'),122);123}124125private _buildToolData(modelDescription: string, userDescription: string): IToolData {126return {127id: UsagesToolId,128toolReferenceName: 'usages',129canBeReferencedInPrompt: false,130icon: ThemeIcon.fromId(Codicon.references.id),131displayName: localize('tool.usages.displayName', 'List Code Usages'),132userDescription,133modelDescription,134source: ToolDataSource.Internal,135when: ContextKeyExpr.has('config.chat.tools.usagesTool.enabled'),136inputSchema: {137type: 'object',138properties: {139symbol: {140type: 'string',141description: 'The exact name of the symbol (function, class, method, variable, type, etc.) to find usages of.'142},143uri: {144type: 'string',145description: 'A full URI of a file where the symbol appears (e.g. "file:///path/to/file.ts"). Provide either "uri" or "filePath".'146},147filePath: {148type: 'string',149description: 'A workspace-relative file path where the symbol appears (e.g. "src/utils/helpers.ts"). Provide either "uri" or "filePath".'150},151lineContent: {152type: 'string',153description: 'A substring of the line of code where the symbol appears. Used to locate the exact position. Must be actual text from the file.'154}155},156required: ['symbol', 'lineContent']157}158};159}160161async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {162const input = context.parameters as ISymbolToolInput;163return {164invocationMessage: localize('tool.usages.invocationMessage', 'Analyzing usages of `{0}`', input.symbol),165};166}167168async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise<IToolResult> {169const input = invocation.parameters as ISymbolToolInput;170171// --- resolve URI ---172const uri = resolveToolUri(input, this._workspaceContextService);173if (!uri) {174return errorResult('Provide either "uri" (a full URI) or "filePath" (a workspace-relative path) to identify the file.');175}176177// --- open text model ---178const ref = await this._textModelService.createModelReference(uri);179try {180const model = ref.object.textEditorModel;181182if (!this._languageFeaturesService.referenceProvider.has(model)) {183return errorResult(`No reference provider available for this file's language. The usages tool may not support this language.`);184}185186// --- find line containing lineContent ---187const lineNumber = findLineNumber(model, input.lineContent);188if (lineNumber === undefined) {189return errorResult(`Could not find line content "${input.lineContent}" in ${uri.toString()}. Provide the exact text from the line where the symbol appears.`);190}191192// --- find symbol in that line ---193const lineText = model.getLineContent(lineNumber);194const column = findSymbolColumn(lineText, input.symbol);195if (column === undefined) {196return errorResult(`Could not find symbol "${input.symbol}" in the matched line. Ensure the symbol name is correct and appears in the provided line content.`);197}198199const position = new Position(lineNumber, column);200201// --- query references, definitions, implementations in parallel ---202const [definitions, references, implementations] = await Promise.all([203getDefinitionsAtPosition(this._languageFeaturesService.definitionProvider, model, position, false, token),204getReferencesAtPosition(this._languageFeaturesService.referenceProvider, model, position, false, false, token),205getImplementationsAtPosition(this._languageFeaturesService.implementationProvider, model, position, false, token),206]);207208if (references.length === 0) {209const result = createToolSimpleTextResult(`No usages found for \`${input.symbol}\`.`);210result.toolResultMessage = new MarkdownString(localize('tool.usages.noResults', 'Analyzed usages of `{0}`, no results', input.symbol));211return result;212}213214// --- classify and format results with previews ---215const previews = await this._getLinePreviews(input.symbol, references, token);216217const lines: string[] = [];218lines.push(`${references.length} usages of \`${input.symbol}\`:\n`);219220for (let i = 0; i < references.length; i++) {221const ref = references[i];222const kind = this._classifyReference(ref, definitions, implementations);223const startLine = Range.lift(ref.range).startLineNumber;224const preview = previews[i];225if (preview) {226lines.push(`<usage type="${kind}" uri="${ref.uri.toString()}" line="${startLine}">`);227lines.push(`\t${preview}`);228lines.push(`</usage>`);229} else {230lines.push(`<usage type="${kind}" uri="${ref.uri.toString()}" line="${startLine}" />`);231}232}233234const text = lines.join('\n');235const result = createToolSimpleTextResult(text);236237result.toolResultMessage = references.length === 1238? new MarkdownString(localize('tool.usages.oneResult', 'Analyzed usages of `{0}`, 1 result', input.symbol))239: new MarkdownString(localize('tool.usages.results', 'Analyzed usages of `{0}`, {1} results', input.symbol, references.length));240241result.toolResultDetails = references.map((r): Location => ({ uri: r.uri, range: r.range }));242243return result;244} finally {245ref.dispose();246}247}248249private async _getLinePreviews(symbol: string, references: LocationLink[], token: CancellationToken): Promise<(string | undefined)[]> {250const previews: (string | undefined)[] = new Array(references.length);251252// Build a lookup: (uriString, lineNumber) → index in references array253const lookup = new Map<string, number>();254const needSearch = new ResourceSet();255256for (let i = 0; i < references.length; i++) {257const ref = references[i];258const lineNumber = Range.lift(ref.range).startLineNumber;259260// Try already-open models first261const existingModel = this._modelService.getModel(ref.uri);262if (existingModel) {263previews[i] = existingModel.getLineContent(lineNumber).trim();264} else {265lookup.set(`${ref.uri.toString()}:${lineNumber}`, i);266needSearch.add(ref.uri);267}268}269270if (needSearch.size === 0 || token.isCancellationRequested) {271return previews;272}273274// Use ISearchService to search for the symbol name, restricted to the275// referenced files. This is backed by ripgrep for file:// URIs.276try {277// Build includePattern from workspace-relative paths278const folders = this._workspaceContextService.getWorkspace().folders;279const relativePaths: string[] = [];280for (const uri of needSearch) {281const folder = this._workspaceContextService.getWorkspaceFolder(uri);282if (folder) {283const rel = relativePath(folder.uri, uri);284if (rel) {285relativePaths.push(rel);286}287}288}289290if (relativePaths.length > 0) {291const includePattern: Record<string, true> = {};292if (relativePaths.length === 1) {293includePattern[relativePaths[0]] = true;294} else {295includePattern[`{${relativePaths.join(',')}}`] = true;296}297298const searchResult = await this._searchService.textSearch(299{300type: QueryType.Text,301contentPattern: { pattern: escapeRegExpCharacters(symbol), isRegExp: true, isWordMatch: true },302folderQueries: folders.map(f => ({ folder: f.uri })),303includePattern,304},305token,306);307308for (const fileMatch of searchResult.results) {309if (!fileMatch.results) {310continue;311}312for (const textMatch of fileMatch.results) {313if (!resultIsMatch(textMatch)) {314continue;315}316for (const range of textMatch.rangeLocations) {317const lineNumber = range.source.startLineNumber + 1; // 0-based → 1-based318const key = `${fileMatch.resource.toString()}:${lineNumber}`;319const idx = lookup.get(key);320if (idx !== undefined) {321previews[idx] = textMatch.previewText.trim();322lookup.delete(key);323}324}325}326}327}328} catch {329// search might fail, leave remaining previews as undefined330}331332return previews;333}334335private _classifyReference(ref: LocationLink, definitions: LocationLink[], implementations: LocationLink[]): string {336if (definitions.some(d => this._overlaps(ref, d))) {337return 'definition';338}339if (implementations.some(d => this._overlaps(ref, d))) {340return 'implementation';341}342return 'reference';343}344345private _overlaps(a: LocationLink, b: LocationLink): boolean {346if (!isEqual(a.uri, b.uri)) {347return false;348}349return Range.areIntersectingOrTouching(a.range, b.range);350}351352}353354export class UsagesToolContribution extends Disposable implements IWorkbenchContribution {355356static readonly ID = 'chat.usagesTool';357358constructor(359@ILanguageModelToolsService toolsService: ILanguageModelToolsService,360@IInstantiationService instantiationService: IInstantiationService,361) {362super();363364const usagesTool = this._store.add(instantiationService.createInstance(UsagesTool));365366let registration: IDisposable | undefined;367const registerUsagesTool = () => {368registration?.dispose();369registration = undefined;370toolsService.flushToolUpdates();371const toolData = usagesTool.getToolData();372if (toolData) {373registration = toolsService.registerTool(toolData, usagesTool);374}375};376registerUsagesTool();377this._store.add(usagesTool.onDidUpdateToolData(registerUsagesTool));378this._store.add({ dispose: () => registration?.dispose() });379}380}381382383