Skip to content

Commit eb20916

Browse files
committed
fix: update toolSpec, add special character handling/escaped characters
1 parent e275124 commit eb20916

File tree

4 files changed

+61
-24
lines changed

4 files changed

+61
-24
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsReplace.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('FsReplace Tool', function () {
1515
const expectedOutput: InvokeOutput = {
1616
output: {
1717
kind: 'text',
18-
content: '',
18+
content: 'File updated successfully',
1919
},
2020
}
2121

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsReplace.ts

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class FsReplace {
3636
const sanitizedPath = sanitize(params.path)
3737
for (const diff of params.diffs) {
3838
if (diff.oldStr === diff.newStr) {
39-
throw new Error('The provided oldStr and newStr are the exact same, this is a no-op')
39+
throw new Error(`The provided oldStr and newStr "${diff.oldStr}" are the exact same, this is a no-op`)
4040
}
4141
const fileExists = await this.workspace.fs.exists(sanitizedPath)
4242
if (!fileExists) {
@@ -53,7 +53,7 @@ export class FsReplace {
5353
return {
5454
output: {
5555
kind: 'text',
56-
content: '',
56+
content: 'File updated successfully',
5757
},
5858
}
5959
}
@@ -74,19 +74,20 @@ export class FsReplace {
7474
description:
7575
'A tool for search and replace contents of an existing file.\n\n' +
7676
'## Overview\n' +
77-
'This tool replaces sections of content in an existing file using oldStr/newStr blocks that define exact changes to specific parts of the file.\n\n' +
77+
'This tool replaces sections of content in an existing file using `oldStr`/`newStr` blocks that define exact changes to specific parts of the file. You MUST ALWAYS bundle as many changes as you can by populating the diffs array with different `oldStr`/`newStr` pairs. You MUST ALWAYS read the contents of the file you want to update first before using the tool to ensure successful result.\n\n' +
7878
'## When to use\n' +
7979
'- When you need to make targeted changes to specific parts of a file\n' +
8080
'- When you need to update multiple sections of the same file\n' +
8181
'## When not to use\n' +
82+
'- When `oldStr` and `newStr` are identical, as the tool will throw an error for no-op replacements\n' +
8283
'- When you need to create a new file\n' +
8384
'- When you need to rename or move a file\n\n' +
8485
'## IMPORTANT Notes\n' +
85-
'- Use this tool to delete code by using empty `newStr` parameter.\n' +
86-
'- The `oldStr` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! Include just the changing lines, and a few surrounding lines if needed for uniqueness. Do not include long runs of unchanging lines in `oldStr`.\n' +
87-
'- The `newStr` parameter should contain the edited lines that should replace the `oldStr`.\n' +
88-
'- When multiple edits to the same file are needed, populate the diffs array with `oldStr` and `newStr` pairs. This improves efficiency by reducing the number of tool calls and ensures the file remains in a consistent state.\n' +
89-
'- Each `oldStr` must be unique within the file - if there are multiple matches, the operation will fail.',
86+
'- You MUST ALWAYS read the contents of the file you want to update first before using the tool to ensure successful result\n' +
87+
'- Use this tool to delete code by using empty `newStr` parameter\n' +
88+
'- The `oldStr` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces! Include just the changing lines, and a few surrounding lines if needed for uniqueness. Do not include long runs of unchanging lines in `oldStr`\n' +
89+
'- The `newStr` parameter should contain the edited lines that should replace the `oldStr`\n' +
90+
'- When multiple edits to the same file are needed, ALWAYS populate the diffs array with `oldStr` and `newStr` pairs. This improves efficiency by reducing the number of tool calls and ensures the file remains in a consistent state',
9091
inputSchema: {
9192
type: 'object',
9293
properties: {
@@ -96,7 +97,8 @@ export class FsReplace {
9697
type: 'string',
9798
},
9899
diffs: {
99-
description: 'List of diffs to replace contents in an existing file.',
100+
description:
101+
'List of diffs to replace contents in an existing file. For example, `[{"oldStr": "existingContent", "newStr": "newContent"}]`',
100102
type: 'array',
101103
items: {
102104
type: 'object',
@@ -136,23 +138,50 @@ const getReplaceContent = (params: ReplaceParams, fileContent: string) => {
136138
if (diff.newStr == undefined) {
137139
diff.newStr = ''
138140
}
141+
139142
// Normalize oldStr and newStr to match fileContent's line ending style
140143
const normalizedOldStr = diff.oldStr.split(/\r\n|\r|\n/).join(lineEnding)
141144
const normalizedNewStr = diff.newStr.split(/\r\n|\r|\n/).join(lineEnding)
142145

143-
const matches = [...fileContent.matchAll(new RegExp(escapeRegExp(normalizedOldStr), 'g'))]
146+
const unescapedOldStr = unescapeString(normalizedOldStr)
147+
const unescapedNewStr = unescapeString(normalizedNewStr)
148+
149+
// Use string indexOf and substring for safer replacement with special characters
150+
const startIndex = fileContent.indexOf(unescapedOldStr)
144151

145-
if (matches.length === 0) {
152+
if (startIndex === -1) {
146153
throw new Error(`No occurrences of "${diff.oldStr}" were found`)
147154
}
148-
if (matches.length > 1) {
149-
throw new Error(`${matches.length} occurrences of oldStr were found when only 1 is expected`)
155+
156+
// Check for multiple occurrences
157+
const secondIndex = fileContent.indexOf(unescapedOldStr, startIndex + 1)
158+
if (secondIndex !== -1) {
159+
throw new Error(`Multiple occurrences of "${diff.oldStr}" were found when only 1 is expected`)
150160
}
151-
fileContent = fileContent.replace(normalizedOldStr, normalizedNewStr)
161+
162+
// Perform the replacement using string operations instead of regex
163+
fileContent =
164+
fileContent.substring(0, startIndex) +
165+
unescapedNewStr +
166+
fileContent.substring(startIndex + unescapedOldStr.length)
152167
}
153168
return fileContent
154169
}
155170

156-
const escapeRegExp = (string: string) => {
157-
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
171+
// Handle all standard JavaScript escape sequences
172+
const unescapeString = (str: string): string => {
173+
return str
174+
.replace(/\\t/g, '\t') // Tab
175+
.replace(/\\n/g, '\n') // Line feed
176+
.replace(/\\r/g, '\r') // Carriage return
177+
.replace(/\\\\/g, '\\') // Backslash
178+
.replace(/\\"/g, '"') // Double quote
179+
.replace(/\\'/g, "'") // Single quote
180+
.replace(/\\b/g, '\b') // Backspace
181+
.replace(/\\f/g, '\f') // Form feed
182+
.replace(/\\v/g, '\v') // Vertical tab
183+
.replace(/\\0/g, '\0') // Null character
184+
.replace(/\\\//g, '/') // Forward slash
185+
.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))) // Unicode escape sequences
186+
.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))) // Hexadecimal escape sequences
158187
}

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsWrite.test.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ describe('FsWrite Tool', function () {
1414
const expectedOutput: InvokeOutput = {
1515
output: {
1616
kind: 'text',
17-
content: '',
17+
content: 'File created successfully',
18+
},
19+
}
20+
const expectedOutputAppend: InvokeOutput = {
21+
output: {
22+
kind: 'text',
23+
content: 'File appended successfully',
1824
},
1925
}
2026

@@ -115,7 +121,7 @@ describe('FsWrite Tool', function () {
115121
const newContent = await features.workspace.fs.readFile(filePath)
116122
assert.strictEqual(newContent, 'Line 1\nLine 2\nLine 3\nLine 4')
117123

118-
assert.deepStrictEqual(output, expectedOutput)
124+
assert.deepStrictEqual(output, expectedOutputAppend)
119125
})
120126

121127
it('adds a newline before appending if file does not end with one', async function () {
@@ -134,7 +140,7 @@ describe('FsWrite Tool', function () {
134140
const newContent = await features.workspace.fs.readFile(filePath)
135141
assert.strictEqual(newContent, 'Line 1\nLine 2\nLine 3\nLine 4')
136142

137-
assert.deepStrictEqual(output, expectedOutput)
143+
assert.deepStrictEqual(output, expectedOutputAppend)
138144
})
139145

140146
it('appends to an empty file', async function () {
@@ -152,7 +158,7 @@ describe('FsWrite Tool', function () {
152158
const newContent = await features.workspace.fs.readFile(filePath)
153159
assert.strictEqual(newContent, 'Line 1')
154160

155-
assert.deepStrictEqual(output, expectedOutput)
161+
assert.deepStrictEqual(output, expectedOutputAppend)
156162
})
157163

158164
it('appends multiple lines correctly', async function () {
@@ -169,7 +175,7 @@ describe('FsWrite Tool', function () {
169175
const newContent = await features.workspace.fs.readFile(filePath)
170176
assert.strictEqual(newContent, 'Line 1\nLine 2\nLine 3')
171177

172-
assert.deepStrictEqual(output, expectedOutput)
178+
assert.deepStrictEqual(output, expectedOutputAppend)
173179
})
174180

175181
it('throws error when file does not exist', async function () {

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/fsWrite.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,22 @@ export class FsWrite {
6363

6464
public async invoke(params: FsWriteParams): Promise<InvokeOutput> {
6565
const sanitizedPath = sanitize(params.path)
66-
66+
let content = ''
6767
switch (params.command) {
6868
case 'create':
6969
await this.handleCreate(params, sanitizedPath)
70+
content = 'File created successfully'
7071
break
7172
case 'append':
7273
await this.handleAppend(params, sanitizedPath)
74+
content = 'File appended successfully'
7375
break
7476
}
7577

7678
return {
7779
output: {
7880
kind: 'text',
79-
content: '',
81+
content,
8082
},
8183
}
8284
}

0 commit comments

Comments
 (0)