2025-05-31 18:41:00 +00:00
import { z } from "zod"
import * as path from "path"
import { Tool } from "./tool"
import { FileTimes } from "./util/file-times"
import { LSP } from "../lsp"
2025-06-03 17:44:46 +00:00
import { createTwoFilesPatch , diffLines } from "diff"
2025-06-02 23:51:26 +00:00
import { Permission } from "../permission"
2025-06-04 17:12:13 +00:00
import DESCRIPTION from "./edit.txt"
2025-05-19 23:29:38 +00:00
2025-05-31 21:12:16 +00:00
export const EditTool = Tool . define ( {
id : "opencode.edit" ,
2025-05-19 23:29:38 +00:00
description : DESCRIPTION ,
parameters : z.object ( {
2025-05-27 02:08:50 +00:00
filePath : z.string ( ) . describe ( "The absolute path to the file to modify" ) ,
oldString : z.string ( ) . describe ( "The text to replace" ) ,
2025-06-04 17:12:13 +00:00
newString : z
. string ( )
. describe (
"The text to replace it with (must be different from old_string)" ,
) ,
replaceAll : z
. boolean ( )
. default ( false )
. describe ( "Replace all occurences of old_string (default false)" ) ,
2025-05-19 23:29:38 +00:00
} ) ,
2025-06-02 23:51:26 +00:00
async execute ( params , ctx ) {
2025-05-27 02:08:50 +00:00
if ( ! params . filePath ) {
2025-05-31 18:41:00 +00:00
throw new Error ( "filePath is required" )
2025-05-19 23:29:38 +00:00
}
2025-06-04 17:12:13 +00:00
const filepath = path . isAbsolute ( params . filePath )
? params . filePath
: path . join ( process . cwd ( ) , params . filePath )
2025-05-19 23:29:38 +00:00
2025-06-02 23:51:26 +00:00
await Permission . ask ( {
id : "opencode.edit" ,
sessionID : ctx.sessionID ,
2025-06-04 17:12:13 +00:00
title : "Edit this file: " + filepath ,
2025-06-02 23:51:26 +00:00
metadata : {
2025-06-04 17:12:13 +00:00
filePath : filepath ,
2025-06-02 23:51:26 +00:00
oldString : params.oldString ,
newString : params.newString ,
} ,
} )
2025-05-31 22:42:43 +00:00
let contentOld = ""
let contentNew = ""
2025-05-21 02:00:00 +00:00
await ( async ( ) = > {
2025-05-27 02:08:50 +00:00
if ( params . oldString === "" ) {
2025-05-31 22:42:43 +00:00
contentNew = params . newString
2025-06-04 17:12:13 +00:00
await Bun . write ( filepath , params . newString )
2025-05-31 18:41:00 +00:00
return
2025-05-21 02:00:00 +00:00
}
2025-05-19 23:29:38 +00:00
2025-06-04 17:12:13 +00:00
const read = FileTimes . get ( ctx . sessionID , filepath )
2025-05-21 02:00:00 +00:00
if ( ! read )
throw new Error (
2025-06-04 17:12:13 +00:00
` You must read the file ${ filepath } before editing it. Use the View tool first ` ,
2025-05-31 18:41:00 +00:00
)
2025-06-04 17:12:13 +00:00
const file = Bun . file ( filepath )
if ( ! ( await file . exists ( ) ) ) throw new Error ( ` File ${ filepath } not found ` )
2025-05-31 18:41:00 +00:00
const stats = await file . stat ( )
2025-05-21 02:00:00 +00:00
if ( stats . isDirectory ( ) )
2025-06-04 17:12:13 +00:00
throw new Error ( ` Path is a directory, not a file: ${ filepath } ` )
2025-05-21 02:00:00 +00:00
if ( stats . mtime . getTime ( ) > read . getTime ( ) )
throw new Error (
2025-06-04 17:12:13 +00:00
` File ${ filepath } has been modified since it was last read. \ nLast modification: ${ read . toISOString ( ) } \ nLast read: ${ stats . mtime . toISOString ( ) } \ n \ nPlease read the file again before modifying it. ` ,
2025-05-31 18:41:00 +00:00
)
2025-05-21 02:00:00 +00:00
2025-05-31 22:42:43 +00:00
contentOld = await file . text ( )
const index = contentOld . indexOf ( params . oldString )
2025-05-21 02:00:00 +00:00
if ( index === - 1 )
throw new Error (
2025-05-27 02:08:50 +00:00
` oldString not found in file. Make sure it matches exactly, including whitespace and line breaks ` ,
2025-05-31 18:41:00 +00:00
)
2025-05-21 02:00:00 +00:00
2025-06-04 17:12:13 +00:00
if ( params . replaceAll ) {
contentNew = contentOld . replaceAll ( params . oldString , params . newString )
}
if ( ! params . replaceAll ) {
const lastIndex = contentOld . lastIndexOf ( params . oldString )
if ( index !== lastIndex )
throw new Error (
` oldString appears multiple times in the file. Please provide more context to ensure a unique match ` ,
)
contentNew =
contentOld . substring ( 0 , index ) +
params . newString +
contentOld . substring ( index + params . oldString . length )
}
2025-05-21 02:00:00 +00:00
2025-05-31 22:42:43 +00:00
await file . write ( contentNew )
2025-05-31 18:41:00 +00:00
} ) ( )
2025-05-21 02:00:00 +00:00
2025-05-31 22:42:43 +00:00
const changes = diffLines ( contentOld , contentNew )
2025-06-04 17:12:13 +00:00
const diff = createTwoFilesPatch ( filepath , filepath , contentOld , contentNew )
2025-05-31 22:42:43 +00:00
2025-06-04 17:12:13 +00:00
FileTimes . read ( ctx . sessionID , filepath )
2025-05-21 02:00:00 +00:00
2025-05-31 18:41:00 +00:00
let output = ""
2025-06-04 17:12:13 +00:00
await LSP . file ( filepath )
2025-05-31 18:41:00 +00:00
const diagnostics = await LSP . diagnostics ( )
2025-05-26 21:15:41 +00:00
for ( const [ file , issues ] of Object . entries ( diagnostics ) ) {
2025-05-31 18:41:00 +00:00
if ( issues . length === 0 ) continue
2025-06-04 17:12:13 +00:00
if ( file === filepath ) {
2025-05-31 18:41:00 +00:00
output += ` \ nThis file has errors, please fix \ n<file_diagnostics> \ n ${ issues . map ( LSP . Diagnostic . pretty ) . join ( "\n" ) } \ n</file_diagnostics> \ n `
continue
2025-05-21 02:00:00 +00:00
}
2025-05-31 18:41:00 +00:00
output += ` \ n<project_diagnostics> \ n ${ file } \ n ${ issues . map ( LSP . Diagnostic . pretty ) . join ( "\n" ) } \ n</project_diagnostics> \ n `
2025-05-19 23:29:38 +00:00
}
2025-05-20 15:11:06 +00:00
return {
2025-05-21 20:35:33 +00:00
metadata : {
diagnostics ,
2025-05-31 22:42:43 +00:00
changes ,
2025-06-03 17:44:46 +00:00
diff ,
2025-05-21 20:35:33 +00:00
} ,
2025-05-21 02:00:00 +00:00
output ,
2025-05-31 18:41:00 +00:00
}
2025-05-19 23:29:38 +00:00
} ,
2025-05-31 18:41:00 +00:00
} )