-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Expand file tree
/
Copy pathmultiReplaceStringTool.tsx
More file actions
206 lines (175 loc) · 8.38 KB
/
multiReplaceStringTool.tsx
File metadata and controls
206 lines (175 loc) · 8.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as l10n from '@vscode/l10n';
import type * as vscode from 'vscode';
import { ResourceMap, ResourceSet } from '../../../util/vs/base/common/map';
import { count } from '../../../util/vs/base/common/strings';
import { URI } from '../../../util/vs/base/common/uri';
import { MarkdownString } from '../../../vscodeTypes';
import { CellOrNotebookEdit } from '../../prompts/node/codeMapper/codeMapper';
import { ToolName } from '../common/toolNames';
import { ToolRegistry } from '../common/toolsRegistry';
import { formatUriForFileWidget } from '../common/toolUtils';
import { AbstractReplaceStringTool, IAbstractReplaceStringInput } from './abstractReplaceStringTool';
export interface IMultiReplaceStringToolParams {
explanation: string;
replacements: IAbstractReplaceStringInput[];
}
export const multiReplaceStringPrimaryDescription = 'This is the primary tool for making multiple edits to one or more files. Use this instead of calling replace_string_in_file repeatedly. It takes an array of replacement operations and applies them sequentially. Each replacement operation has the same parameters as replace_string_in_file: filePath, oldString, newString, and explanation. This tool is ideal when you need to make multiple edits across different files or multiple edits in the same file. The tool will provide a summary of successful and failed operations.';
export class MultiReplaceStringTool extends AbstractReplaceStringTool<IMultiReplaceStringToolParams> {
public static toolName = ToolName.MultiReplaceString;
public static readonly nonDeferred = true;
protected extractReplaceInputs(input: IMultiReplaceStringToolParams): IAbstractReplaceStringInput[] {
return input.replacements.map(r => ({
filePath: r.filePath,
oldString: r.oldString,
newString: r.newString,
}));
}
async handleToolStream(options: vscode.LanguageModelToolInvocationStreamOptions<IMultiReplaceStringToolParams>, _token: vscode.CancellationToken): Promise<vscode.LanguageModelToolStreamResult> {
const partialInput = options.rawInput as Partial<IMultiReplaceStringToolParams> | undefined;
let invocationMessage: MarkdownString;
if (partialInput && typeof partialInput === 'object' && Array.isArray(partialInput.replacements)) {
// Filter to valid replacements that have at least oldString
const validReplacements = partialInput.replacements.filter(
r => r && typeof r === 'object' && r.oldString !== undefined
);
if (validReplacements.length > 0) {
let totalOldLines = 0;
let totalNewLines = 0;
let hasNewString = false;
const fileNames = new ResourceSet();
for (const r of validReplacements) {
totalOldLines += count(r.oldString, '\n') + 1;
if (r.newString !== undefined) {
hasNewString = true;
totalNewLines += count(r.newString, '\n') + 1;
}
const uri = r.filePath && this.promptPathRepresentationService.resolveFilePath(r.filePath);
if (uri) {
fileNames.add(uri);
}
}
const fileList = fileNames.size > 0 ? Array.from(fileNames, n => formatUriForFileWidget(n)).join(', ') : undefined;
if (hasNewString && fileList) {
invocationMessage = new MarkdownString(l10n.t`Replacing ${totalOldLines} lines with ${totalNewLines} lines in ${fileList}`);
} else if (hasNewString) {
invocationMessage = new MarkdownString(l10n.t`Replacing ${totalOldLines} lines with ${totalNewLines} lines`);
} else if (fileList) {
invocationMessage = new MarkdownString(l10n.t`Replacing ${totalOldLines} lines in ${fileList}`);
} else {
invocationMessage = new MarkdownString(l10n.t`Replacing ${totalOldLines} lines`);
}
} else {
invocationMessage = new MarkdownString(l10n.t`Editing files`);
}
} else {
invocationMessage = new MarkdownString(l10n.t`Editing files`);
}
return { invocationMessage };
}
async invoke(options: vscode.LanguageModelToolInvocationOptions<IMultiReplaceStringToolParams>, token: vscode.CancellationToken) {
if (!options.input.replacements || !Array.isArray(options.input.replacements)) {
throw new Error('Invalid input, no replacements array');
}
const prepared = await this.prepareEdits(options, token);
let successes = 0;
let failures = 0;
let individualEdits = 0;
const uniqueUris = new ResourceSet();
for (const edit of prepared) {
uniqueUris.add(edit.uri);
if (edit.generatedEdit.success) {
successes++;
individualEdits += edit.generatedEdit.textEdits.length;
} else {
failures++;
}
}
/* __GDPR__
"multiStringReplaceCall" : {
"owner": "connor4312",
"comment": "Tracks how much percent of the AI edits survived after 5 minutes of accepting",
"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." },
"model": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model used for the request." },
"successes": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The number of successful edits.", "isMeasurement": true },
"failures": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The number of failed edits.", "isMeasurement": true },
"uniqueUris": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The number of unique URIs edited.", "isMeasurement": true },
"individualEdits": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The number of individual text edits made.", "isMeasurement": true }
}
*/
this.telemetryService.sendMSFTTelemetryEvent('multiStringReplaceCall', {
requestId: this._promptContext?.requestId,
model: await this.modelForTelemetry(options),
}, {
successes,
failures,
individualEdits,
uniqueUris: uniqueUris.size,
});
for (let i = 0; i < prepared.length; i++) {
const e1 = prepared[i];
uniqueUris.add(e1.uri);
if (!e1.generatedEdit.success) {
failures++;
continue;
}
successes++;
for (let k = i + 1; k < prepared.length; k++) {
const e2 = prepared[k];
// Merge successful edits of the same type and URI so that edits come in
// a single correct batch and positions aren't later clobbered.
if (!e2.generatedEdit.success || e2.uri.toString() !== e1.uri.toString() || (!!e2.generatedEdit.notebookEdits !== !!e1.generatedEdit.notebookEdits)) {
continue;
}
prepared.splice(k, 1);
k--;
if (e2.generatedEdit.notebookEdits) {
e1.generatedEdit.notebookEdits = mergeNotebookAndTextEdits(e1.generatedEdit.notebookEdits!, e2.generatedEdit.notebookEdits);
} else {
e1.generatedEdit.textEdits = e1.generatedEdit.textEdits.concat(e2.generatedEdit.textEdits);
e1.generatedEdit.textEdits.sort(textEditSorter);
}
}
}
return this.applyAllEdits(options, prepared, token);
}
protected override toolName(): ToolName {
return MultiReplaceStringTool.toolName;
}
}
ToolRegistry.registerTool(MultiReplaceStringTool);
function textEditSorter(a: vscode.TextEdit, b: vscode.TextEdit) {
return b.range.end.compareTo(a.range.end) || b.range.start.compareTo(a.range.start);
}
/**
* Merge two arrays of notebook edits or text edits grouped by URI.
* Text edits for the same URI are concatenated and sorted in reverse file order (descending by start position).
*/
function mergeNotebookAndTextEdits(left: CellOrNotebookEdit[], right: CellOrNotebookEdit[]): CellOrNotebookEdit[] {
const notebookEdits: vscode.NotebookEdit[] = [];
const textEditsByUri = new ResourceMap<vscode.TextEdit[]>();
const add = (item: vscode.NotebookEdit | [URI, vscode.TextEdit[]]) => {
if (Array.isArray(item)) {
const [uri, edits] = item;
let bucket = textEditsByUri.get(uri);
if (!bucket) {
bucket = [];
textEditsByUri.set(uri, bucket);
}
bucket.push(...edits);
} else {
notebookEdits.push(item);
}
};
left.forEach(add);
right.forEach(add);
const mergedTextEditTuples: [URI, vscode.TextEdit[]][] = [];
for (const [uri, edits] of textEditsByUri.entries()) {
edits.sort(textEditSorter);
mergedTextEditTuples.push([uri, edits]);
}
return [...notebookEdits, ...mergedTextEditTuples];
}