Skip to content

Commit 3760369

Browse files
committed
refactor: update XML parsing in insertContent and searchAndReplace tools to support XML format for operations
1 parent b8bf634 commit 3760369

File tree

5 files changed

+112
-63
lines changed

5 files changed

+112
-63
lines changed

src/core/prompts/tools/insert-content.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,35 @@ export function getInsertContentDescription(args: ToolArgs): string {
55
Description: Inserts content at specific line positions in a file. This is the primary tool for adding new content and code (functions/methods/classes, imports, attributes etc.) as it allows for precise insertions without overwriting existing content. The tool uses an efficient line-based insertion system that maintains file integrity and proper ordering of multiple insertions. Beware to use the proper indentation. This tool is the preferred way to add new content and code to files.
66
Parameters:
77
- path: (required) The path of the file to insert content into (relative to the current workspace directory ${args.cwd.toPosix()})
8-
- operations: (required) A JSON array of insertion operations. Each operation is an object with:
9-
* start_line: (required) The line number where the content should be inserted. The content currently at that line will end up below the inserted content.
10-
* content: (required) The content to insert at the specified position. IMPORTANT NOTE: If the content is a single line, it can be a string. If it's a multi-line content, it should be a string with newline characters (\n) for line breaks. Make sure to include the correct indentation for the content.
8+
- operations: (required) An XML list of insertion operations. Each operation is defined with:
9+
* <operation>: Container for each insertion operation
10+
* <start_line>: (required) The line number where the content should be inserted. The content currently at that line will end up below the inserted content.
11+
* <content>: (required) The content to insert at the specified position. Can be single line or multi-line content. Make sure to include the correct indentation for the content.
1112
Usage:
1213
<insert_content>
1314
<path>File path here</path>
14-
<operations>[
15-
{
16-
"start_line": 10,
17-
"content": "Your content here"
18-
}
19-
]</operations>
15+
<operations>
16+
<operation>
17+
<start_line>10</start_line>
18+
<content>Your content here</content>
19+
</operation>
20+
</operations>
2021
</insert_content>
2122
Example: Insert a new function and its import statement
2223
<insert_content>
2324
<path>File path here</path>
24-
<operations>[
25-
{
26-
"start_line": 1,
27-
"content": "import { sum } from './utils';"
28-
},
29-
{
30-
"start_line": 10,
31-
"content": "function calculateTotal(items: number[]): number {\n return items.reduce((sum, item) => sum + item, 0);\n}"
32-
}
33-
]</operations>
25+
<operations>
26+
<operation>
27+
<start_line>1</start_line>
28+
<content>import { sum } from './utils';</content>
29+
</operation>
30+
<operation>
31+
<start_line>10</start_line>
32+
<content>function calculateTotal(items: number[]): number {
33+
return items.reduce((sum, item) => sum + item, 0);
34+
}
35+
</content>
36+
</operation>
37+
</operations>
3438
</insert_content>`
3539
}

src/core/prompts/tools/search-and-replace.ts

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ export function getSearchAndReplaceDescription(args: ToolArgs): string {
55
Description: Request to perform search and replace operations on a file. Each operation can specify a search pattern (string or regex) and replacement text, with optional line range restrictions and regex flags. Shows a diff preview before applying changes.
66
Parameters:
77
- path: (required) The path of the file to modify (relative to the current workspace directory ${args.cwd.toPosix()})
8-
- operations: (required) A JSON array of search/replace operations. Each operation is an object with:
8+
- operations: (required) A list of search/replace operations as XML elements. Each operation can have these elements:
99
* search: (required) The text or pattern to search for
10-
* replace: (required) The text to replace matches with. If multiple lines need to be replaced, use "\n" for newlines
10+
* replace: (required) The text to replace matches with.
1111
* start_line: (optional) Starting line number for restricted replacement
1212
* end_line: (optional) Ending line number for restricted replacement
1313
* use_regex: (optional) Whether to treat search as a regex pattern
@@ -16,37 +16,61 @@ Parameters:
1616
Usage:
1717
<search_and_replace>
1818
<path>File path here</path>
19-
<operations>[
20-
{
21-
"search": "text to find",
22-
"replace": "replacement text",
23-
"start_line": 1,
24-
"end_line": 10
25-
}
26-
]</operations>
19+
<operations>
20+
<operation>
21+
<search>text to find</search>
22+
<replace>replacement text</replace>
23+
<start_line>1</start_line>
24+
<end_line>10</end_line>
25+
</operation>
26+
</operations>
2727
</search_and_replace>
2828
Example: Replace "foo" with "bar" in lines 1-10 of example.ts
2929
<search_and_replace>
3030
<path>example.ts</path>
31-
<operations>[
32-
{
33-
"search": "foo",
34-
"replace": "bar",
35-
"start_line": 1,
36-
"end_line": 10
37-
}
38-
]</operations>
31+
<operations>
32+
<operation>
33+
<search>foo</search>
34+
<replace>bar</replace>
35+
<start_line>1</start_line>
36+
<end_line>10</end_line>
37+
</operation>
38+
</operations>
3939
</search_and_replace>
4040
Example: Replace all occurrences of "old" with "new" using regex
4141
<search_and_replace>
4242
<path>example.ts</path>
43-
<operations>[
44-
{
45-
"search": "old\\w+",
46-
"replace": "new$&",
47-
"use_regex": true,
48-
"ignore_case": true
49-
}
50-
]</operations>
43+
<operations>
44+
<operation>
45+
<search>old\\w+</search>
46+
<replace>new$&</replace>
47+
<use_regex>true</use_regex>
48+
<ignore_case>true</ignore_case>
49+
</operation>
50+
</operations>
51+
</search_and_replace>
52+
53+
Example: Multiple replacements in a JavaScript file
54+
<search_and_replace>
55+
<path>app.js</path>
56+
<operations>
57+
<operation>
58+
<search>const</search>
59+
<replace>let</replace>
60+
<start_line>1</start_line>
61+
<end_line>100</end_line>
62+
</operation>
63+
<operation>
64+
<search>function\\s+(\\w+)</search>
65+
<replace>const $1 = function</replace>
66+
<use_regex>true</use_regex>
67+
</operation>
68+
<operation>
69+
<search>console\\.log</search>
70+
<replace>logger.info</replace>
71+
<use_regex>true</use_regex>
72+
<ignore_case>true</ignore_case>
73+
</operation>
74+
</operations>
5175
</search_and_replace>`
5276
}

src/core/tools/insertContentTool.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getReadablePath } from "../../utils/path"
22
import { Cline } from "../Cline"
33
import { ToolUse } from "../assistant-message"
44
import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "./types"
5+
import { parseXml } from "../../utils/xml"
56
import { formatResponse } from "../prompts/responses"
67
import { ClineSayTool } from "../../shared/ExtensionMessage"
78
import path from "path"
@@ -58,23 +59,29 @@ export async function insertContentTool(
5859
return
5960
}
6061

61-
let parsedOperations: Array<{
62+
type Operation = {
6263
start_line: number
6364
content: string
64-
}>
65+
}
66+
let parsedOperations: {
67+
operation: Operation[] | Operation
68+
}
6569

6670
try {
67-
parsedOperations = JSON.parse(operations)
68-
if (!Array.isArray(parsedOperations)) {
69-
throw new Error("Operations must be an array")
71+
parsedOperations = parseXml(operations) as {
72+
operation: Operation[] | Operation
7073
}
7174
} catch (error) {
7275
cline.consecutiveMistakeCount++
73-
await cline.say("error", `Failed to parse operations JSON: ${error.message}`)
74-
pushToolResult(formatResponse.toolError("Invalid operations JSON format"))
76+
await cline.say("error", `Failed to parse operations: ${error.message}`)
77+
pushToolResult(formatResponse.toolError("Invalid operations XML format"))
7578
return
7679
}
7780

81+
const normalizedOperations = Array.isArray(parsedOperations?.operation)
82+
? parsedOperations.operation
83+
: [parsedOperations?.operation].filter((op): op is Operation => op !== undefined)
84+
7885
cline.consecutiveMistakeCount = 0
7986

8087
// Read the file
@@ -85,7 +92,7 @@ export async function insertContentTool(
8592

8693
const updatedContent = insertGroups(
8794
lines,
88-
parsedOperations.map((elem) => {
95+
normalizedOperations.map((elem) => {
8996
return {
9097
index: elem.start_line - 1,
9198
elements: elem.content.split("\n"),

src/core/tools/searchAndReplaceTool.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Cline } from "../Cline"
22
import { ToolUse } from "../assistant-message"
33
import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "./types"
44
import { formatResponse } from "../prompts/responses"
5+
import { parseXml } from "../../utils/xml"
56
import { ClineSayTool } from "../../shared/ExtensionMessage"
67
import { getReadablePath } from "../../utils/path"
78
import path from "path"
@@ -57,35 +58,45 @@ export async function searchAndReplaceTool(
5758
return
5859
}
5960

60-
let parsedOperations: Array<{
61+
type Operation = {
6162
search: string
6263
replace: string
6364
start_line?: number
6465
end_line?: number
6566
use_regex?: boolean
6667
ignore_case?: boolean
6768
regex_flags?: string
68-
}>
69+
}
70+
let parsedOperations: {
71+
operation: Operation[] | Operation
72+
}
6973

7074
try {
71-
parsedOperations = JSON.parse(operations)
72-
if (!Array.isArray(parsedOperations)) {
73-
throw new Error("Operations must be an array")
75+
parsedOperations = parseXml(operations, [
76+
"operation.search",
77+
"operation.replace",
78+
"operation.regex_flags",
79+
]) as {
80+
operation: Operation[] | Operation
7481
}
7582
} catch (error) {
7683
cline.consecutiveMistakeCount++
77-
await cline.say("error", `Failed to parse operations JSON: ${error.message}`)
78-
pushToolResult(formatResponse.toolError("Invalid operations JSON format"))
84+
await cline.say("error", `Failed to parse operations: ${error.message}`)
85+
pushToolResult(formatResponse.toolError("Invalid operations XML format"))
7986
return
8087
}
8188

89+
const normalizedOperations = Array.isArray(parsedOperations?.operation)
90+
? parsedOperations.operation
91+
: [parsedOperations?.operation].filter((op): op is Operation => op !== undefined)
92+
8293
// Read the original file content
8394
const fileContent = await fs.readFile(absolutePath, "utf-8")
8495
cline.diffViewProvider.editType = "modify"
8596
cline.diffViewProvider.originalContent = fileContent
8697
let lines = fileContent.split("\n")
8798

88-
for (const op of parsedOperations) {
99+
for (const op of normalizedOperations) {
89100
const flags = op.regex_flags ?? (op.ignore_case ? "gi" : "g")
90101
const multilineFlags = flags.includes("m") ? flags : flags + "m"
91102

src/utils/xml.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ export function parseXml(xmlString: string, stopNodes?: string[]): unknown {
1010
const _stopNodes = stopNodes ?? []
1111
try {
1212
const parser = new XMLParser({
13+
// Preserve attribute types (don't convert numbers/booleans)
1314
ignoreAttributes: false,
1415
attributeNamePrefix: "@_",
15-
parseAttributeValue: false,
16-
parseTagValue: false,
16+
// Parse numbers and booleans in text nodes
17+
parseAttributeValue: true,
18+
parseTagValue: true,
19+
// Trim whitespace from text nodes
1720
trimValues: true,
1821
stopNodes: _stopNodes,
1922
})

0 commit comments

Comments
 (0)