Skip to content

Commit 6e415ec

Browse files
tool: edit - adding rangeEdit
1 parent 61b7e89 commit 6e415ec

File tree

2 files changed

+31
-2
lines changed

2 files changed

+31
-2
lines changed

packages/opencode/src/tool/edit.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ export const EditTool = Tool.define("edit", {
2424
oldString: z.string().describe("The text to replace"),
2525
newString: z.string().describe("The text to replace it with (must be different from oldString)"),
2626
replaceAll: z.boolean().optional().describe("Replace all occurrences of oldString (default false)"),
27+
range: z
28+
.string()
29+
.optional()
30+
.describe(
31+
"Line range in format 'start:end' (e.g., '140:233') - inclusive of both start and end lines. When provided, restricts oldString matching to the range. Ensures strong match performance.",
32+
),
2733
}),
2834
async execute(params, ctx) {
2935
if (!params.filePath) {
@@ -73,7 +79,28 @@ export const EditTool = Tool.define("edit", {
7379
if (stats.isDirectory()) throw new Error(`Path is a directory, not a file: ${filePath}`)
7480
await FileTime.assert(ctx.sessionID, filePath)
7581
contentOld = await file.text()
76-
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
82+
83+
const rangeEdit = (content: string, rangeStr: string) => {
84+
const match = rangeStr.match(/^(\d+):(\d+)$/)
85+
if (!match) {
86+
throw new Error("Invalid range format. Use 'start:end', e.g., '140:233'")
87+
}
88+
const start = parseInt(match[1], 10) - 1
89+
const end = parseInt(match[2], 10) - 1
90+
const lines = content.split("\n")
91+
if (start < 0 || end >= lines.length || start > end) {
92+
throw new Error(`Range out of bounds: file has ${lines.length} lines, got ${rangeStr}`)
93+
}
94+
const before = lines.slice(0, start)
95+
const target = lines.slice(start, end + 1).join("\n")
96+
const after = lines.slice(end + 1)
97+
const replaced = replace(target, params.oldString, params.newString, params.replaceAll)
98+
return [...before, ...replaced.split("\n"), ...after].join("\n")
99+
}
100+
101+
contentNew = params.range
102+
? rangeEdit(contentOld, params.range)
103+
: replace(contentOld, params.oldString, params.newString, params.replaceAll)
77104

78105
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
79106
if (agent.permission.edit === "ask") {
@@ -621,6 +648,6 @@ export function replace(content: string, oldString: string, newString: string, r
621648
throw new Error("oldString not found in content")
622649
}
623650
throw new Error(
624-
"oldString found multiple times and requires more code context to uniquely identify the intended match",
651+
"oldString found multiple times. Either use the `range` parameter or provide more code context to uniquely identify the intended match. You can also use `replaceAll`: true to replace all instances of oldString (make sure this is intentional).",
625652
)
626653
}

packages/opencode/src/tool/edit.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Usage:
66
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
77
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
88
- Use `replaceAll` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
9+
- Use `range` to specify a line range (e.g., '140:233') when `oldString` appears multiple times in the file. The range restricts the search and replacement to only those lines, allowing you to disambiguate which occurrence to replace.
910

1011
CRITICAL:
1112
- ALWAYS MAKE SURE TO SUPPLY SUFFICIENT CONTEXT TO UNIQUELY IDENTIFY `oldString` OTHERWISE THE EDIT WILL FAIL
13+
- SPECIFYING THE RANGE OF THE EDIT WILL ENSURE BETTER `oldString` MATCHING

0 commit comments

Comments
 (0)