Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/tools/renameTool.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 { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';
11
import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js';
12
import { ThemeIcon } from '../../../../../base/common/themables.js';
13
import { Position } from '../../../../../editor/common/core/position.js';
14
import { TextEdit } from '../../../../../editor/common/languages.js';
15
import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js';
16
import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';
17
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
18
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
19
import { rename } from '../../../../../editor/contrib/rename/browser/rename.js';
20
import { localize } from '../../../../../nls.js';
21
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
22
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
23
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
24
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
25
import { IWorkbenchContribution } from '../../../../common/contributions.js';
26
import { IChatService } from '../../common/chatService/chatService.js';
27
import { ChatConfiguration } from '../../common/constants.js';
28
import { ChatModel } from '../../common/model/chatModel.js';
29
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolProgress } from '../../common/tools/languageModelToolsService.js';
30
import { createToolSimpleTextResult } from '../../common/tools/builtinTools/toolHelpers.js';
31
import { errorResult, findLineNumber, findSymbolColumn, ISymbolToolInput, resolveToolUri } from './toolHelpers.js';
32
33
export const RenameToolId = 'vscode_renameSymbol';
34
35
interface IRenameToolInput extends ISymbolToolInput {
36
newName: string;
37
}
38
39
const BaseModelDescription = `Rename a code symbol across the workspace using the language server's rename functionality. This performs a precise, semantics-aware rename that updates all references.
40
41
Input:
42
- "symbol": The exact current name of the symbol to rename.
43
- "newName": The new name for the symbol.
44
- "uri": A full URI (e.g. "file:///path/to/file.ts") of a file where the symbol appears. Provide either "uri" or "filePath".
45
- "filePath": A workspace-relative file path (e.g. "src/utils/helpers.ts") of a file where the symbol appears. Provide either "uri" or "filePath".
46
- "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.
47
48
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.
49
50
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.`;
51
52
/**
53
* Static description used when the {@link ChatConfiguration.SymbolToolsCacheStable}
54
* experiment is enabled. Identical to {@link BaseModelDescription} plus a single
55
* sentence describing the unsupported-language behavior. Crucially, this string
56
* does NOT depend on the set of registered rename providers, so it stays
57
* byte-stable across requests as language extensions activate during a turn.
58
*/
59
const StaticModelDescription = BaseModelDescription + `
60
61
If the file's language has no rename provider registered, the tool returns an error.`;
62
63
export class RenameTool extends Disposable implements IToolImpl {
64
65
private readonly _onDidUpdateToolData = this._store.add(new Emitter<void>());
66
readonly onDidUpdateToolData = this._onDidUpdateToolData.event;
67
68
constructor(
69
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
70
@ILanguageService private readonly _languageService: ILanguageService,
71
@ITextModelService private readonly _textModelService: ITextModelService,
72
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
73
@IChatService private readonly _chatService: IChatService,
74
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
75
@IConfigurationService private readonly _configurationService: IConfigurationService,
76
) {
77
super();
78
79
// In cache-stable mode the tool's wire bytes don't depend on the set
80
// of registered rename providers, so we don't need to re-fire the
81
// update event on provider changes. Skipping this subscription
82
// avoids unnecessary tool re-registration churn as well.
83
if (!this._isCacheStable()) {
84
this._store.add(Event.debounce(
85
this._languageFeaturesService.renameProvider.onDidChange,
86
() => { },
87
2000
88
)((() => this._onDidUpdateToolData.fire())));
89
}
90
}
91
92
private _isCacheStable(): boolean {
93
return this._configurationService.getValue<boolean>(ChatConfiguration.SymbolToolsCacheStable) === true;
94
}
95
96
getToolData(): IToolData | undefined {
97
if (this._isCacheStable()) {
98
return this._getStaticToolData();
99
}
100
101
const languageIds = this._languageFeaturesService.renameProvider.registeredLanguageIds;
102
103
if (languageIds.size === 0) {
104
return undefined;
105
}
106
107
let modelDescription = BaseModelDescription;
108
let userDescription: string;
109
if (languageIds.has('*')) {
110
modelDescription += '\n\nSupported for all languages.';
111
userDescription = localize('tool.rename.userDescription', 'Rename a symbol across the workspace');
112
} else {
113
const sorted = [...languageIds].sort();
114
modelDescription += `\n\nCurrently supported for: ${sorted.join(', ')}.`;
115
const niceNames = sorted.map(id => this._languageService.getLanguageName(id) ?? id);
116
userDescription = localize('tool.rename.userDescriptionWithLanguages', 'Rename a symbol across the workspace ({0})', niceNames.join(', '));
117
}
118
return this._buildToolData(modelDescription, userDescription);
119
}
120
121
private _getStaticToolData(): IToolData {
122
return this._buildToolData(
123
StaticModelDescription,
124
localize('tool.rename.userDescription', 'Rename a symbol across the workspace'),
125
);
126
}
127
128
private _buildToolData(modelDescription: string, userDescription: string): IToolData {
129
return {
130
id: RenameToolId,
131
toolReferenceName: 'rename',
132
canBeReferencedInPrompt: false,
133
icon: ThemeIcon.fromId(Codicon.rename.id),
134
displayName: localize('tool.rename.displayName', 'Rename Symbol'),
135
userDescription,
136
modelDescription,
137
source: ToolDataSource.Internal,
138
when: ContextKeyExpr.has('config.chat.tools.renameTool.enabled'),
139
inputSchema: {
140
type: 'object',
141
properties: {
142
symbol: {
143
type: 'string',
144
description: 'The exact current name of the symbol to rename.'
145
},
146
newName: {
147
type: 'string',
148
description: 'The new name for the symbol.'
149
},
150
uri: {
151
type: 'string',
152
description: 'A full URI of a file where the symbol appears (e.g. "file:///path/to/file.ts"). Provide either "uri" or "filePath".'
153
},
154
filePath: {
155
type: 'string',
156
description: 'A workspace-relative file path where the symbol appears (e.g. "src/utils/helpers.ts"). Provide either "uri" or "filePath".'
157
},
158
lineContent: {
159
type: 'string',
160
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.'
161
}
162
},
163
required: ['symbol', 'newName', 'lineContent']
164
}
165
};
166
}
167
168
async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
169
const input = context.parameters as IRenameToolInput;
170
return {
171
invocationMessage: localize('tool.rename.invocationMessage', 'Renaming `{0}` to `{1}`', input.symbol, input.newName),
172
};
173
}
174
175
async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise<IToolResult> {
176
const input = invocation.parameters as IRenameToolInput;
177
178
// --- resolve URI ---
179
const uri = resolveToolUri(input, this._workspaceContextService);
180
if (!uri) {
181
return errorResult('Provide either "uri" (a full URI) or "filePath" (a workspace-relative path) to identify the file.');
182
}
183
184
// --- open text model ---
185
const ref = await this._textModelService.createModelReference(uri);
186
try {
187
const model = ref.object.textEditorModel;
188
189
if (!this._languageFeaturesService.renameProvider.has(model)) {
190
return errorResult(`No rename provider available for this file's language. The rename tool may not support this language.`);
191
}
192
193
// --- find line containing lineContent ---
194
const lineNumber = findLineNumber(model, input.lineContent);
195
if (lineNumber === undefined) {
196
return errorResult(`Could not find line content "${input.lineContent}" in ${uri.toString()}. Provide the exact text from the line where the symbol appears.`);
197
}
198
199
// --- find symbol in that line ---
200
const lineText = model.getLineContent(lineNumber);
201
const column = findSymbolColumn(lineText, input.symbol);
202
if (column === undefined) {
203
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.`);
204
}
205
206
const position = new Position(lineNumber, column);
207
208
// --- perform rename ---
209
const renameResult = await rename(this._languageFeaturesService.renameProvider, model, position, input.newName);
210
211
if (renameResult.rejectReason) {
212
return errorResult(`Rename rejected: ${renameResult.rejectReason}`);
213
}
214
215
if (renameResult.edits.length === 0) {
216
return errorResult(`Rename produced no edits.`);
217
}
218
219
// --- apply edits via chat response stream ---
220
if (invocation.context) {
221
const chatModel = this._chatService.getSession(invocation.context.sessionResource) as ChatModel | undefined;
222
const request = chatModel?.getRequests().at(-1);
223
224
if (chatModel && request) {
225
// Group text edits by URI
226
const editsByUri = new ResourceMap<TextEdit[]>();
227
for (const edit of renameResult.edits) {
228
if (ResourceTextEdit.is(edit)) {
229
let edits = editsByUri.get(edit.resource);
230
if (!edits) {
231
edits = [];
232
editsByUri.set(edit.resource, edits);
233
}
234
edits.push(edit.textEdit);
235
}
236
}
237
238
// Push edits through the chat response stream
239
for (const [editUri, edits] of editsByUri) {
240
chatModel.acceptResponseProgress(request, {
241
kind: 'textEdit',
242
uri: editUri,
243
edits: [],
244
});
245
chatModel.acceptResponseProgress(request, {
246
kind: 'textEdit',
247
uri: editUri,
248
edits,
249
});
250
chatModel.acceptResponseProgress(request, {
251
kind: 'textEdit',
252
uri: editUri,
253
edits: [],
254
done: true,
255
});
256
}
257
258
return this._successResult(input, editsByUri.size, renameResult.edits.length);
259
}
260
}
261
262
// Fallback: apply via bulk edit service when no chat context is available
263
await this._bulkEditService.apply(renameResult);
264
const fileCount = new ResourceSet(renameResult.edits.filter(ResourceTextEdit.is).map(e => e.resource)).size;
265
return this._successResult(input, fileCount, renameResult.edits.length);
266
267
} finally {
268
ref.dispose();
269
}
270
}
271
272
private _successResult(input: IRenameToolInput, fileCount: number, editCount: number): IToolResult {
273
const text = editCount === 1
274
? localize('tool.rename.oneEdit', "Renamed `{0}` to `{1}` - 1 edit in {2} file.", input.symbol, input.newName, fileCount)
275
: localize('tool.rename.edits', "Renamed `{0}` to `{1}` - {2} edits across {3} files.", input.symbol, input.newName, editCount, fileCount);
276
const result = createToolSimpleTextResult(text);
277
result.toolResultMessage = new MarkdownString(text);
278
return result;
279
}
280
281
}
282
283
284
285
export class RenameToolContribution extends Disposable implements IWorkbenchContribution {
286
287
static readonly ID = 'chat.renameTool';
288
289
constructor(
290
@ILanguageModelToolsService toolsService: ILanguageModelToolsService,
291
@IInstantiationService instantiationService: IInstantiationService,
292
) {
293
super();
294
295
const renameTool = this._store.add(instantiationService.createInstance(RenameTool));
296
297
let registration: IDisposable | undefined;
298
const registerRenameTool = () => {
299
registration?.dispose();
300
registration = undefined;
301
toolsService.flushToolUpdates();
302
const toolData = renameTool.getToolData();
303
if (toolData) {
304
registration = toolsService.registerTool(toolData, renameTool);
305
}
306
};
307
registerRenameTool();
308
this._store.add(renameTool.onDidUpdateToolData(registerRenameTool));
309
this._store.add({
310
dispose: () => {
311
registration?.dispose();
312
}
313
});
314
}
315
}
316
317