edit.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import { z } from "zod"
  2. import * as path from "path"
  3. import { Tool } from "./tool"
  4. import { FileTimes } from "./util/file-times"
  5. import { LSP } from "../lsp"
  6. import { createTwoFilesPatch } from "diff"
  7. import { Permission } from "../permission"
  8. import DESCRIPTION from "./edit.txt"
  9. import { App } from "../app/app"
  10. export const EditTool = Tool.define({
  11. id: "opencode.edit",
  12. description: DESCRIPTION,
  13. parameters: z.object({
  14. filePath: z.string().describe("The absolute path to the file to modify"),
  15. oldString: z.string().describe("The text to replace"),
  16. newString: z
  17. .string()
  18. .describe(
  19. "The text to replace it with (must be different from old_string)",
  20. ),
  21. replaceAll: z
  22. .boolean()
  23. .nullable()
  24. .describe("Replace all occurences of old_string (default false)"),
  25. }),
  26. async execute(params, ctx) {
  27. if (!params.filePath) {
  28. throw new Error("filePath is required")
  29. }
  30. const app = App.info()
  31. const filepath = path.isAbsolute(params.filePath)
  32. ? params.filePath
  33. : path.join(app.path.cwd, params.filePath)
  34. await Permission.ask({
  35. id: "opencode.edit",
  36. sessionID: ctx.sessionID,
  37. title: "Edit this file: " + filepath,
  38. metadata: {
  39. filePath: filepath,
  40. oldString: params.oldString,
  41. newString: params.newString,
  42. },
  43. })
  44. let contentOld = ""
  45. let contentNew = ""
  46. await (async () => {
  47. if (params.oldString === "") {
  48. contentNew = params.newString
  49. await Bun.write(filepath, params.newString)
  50. return
  51. }
  52. const file = Bun.file(filepath)
  53. if (!(await file.exists())) throw new Error(`File ${filepath} not found`)
  54. const stats = await file.stat()
  55. if (stats.isDirectory())
  56. throw new Error(`Path is a directory, not a file: ${filepath}`)
  57. await FileTimes.assert(ctx.sessionID, filepath)
  58. contentOld = await file.text()
  59. const index = contentOld.indexOf(params.oldString)
  60. if (index === -1)
  61. throw new Error(
  62. `oldString not found in file. Make sure it matches exactly, including whitespace and line breaks`,
  63. )
  64. if (params.replaceAll) {
  65. contentNew = contentOld.replaceAll(params.oldString, params.newString)
  66. }
  67. if (!params.replaceAll) {
  68. const lastIndex = contentOld.lastIndexOf(params.oldString)
  69. if (index !== lastIndex)
  70. throw new Error(
  71. `oldString appears multiple times in the file. Please provide more context to ensure a unique match`,
  72. )
  73. contentNew =
  74. contentOld.substring(0, index) +
  75. params.newString +
  76. contentOld.substring(index + params.oldString.length)
  77. }
  78. await file.write(contentNew)
  79. })()
  80. const diff = createTwoFilesPatch(filepath, filepath, contentOld, contentNew)
  81. FileTimes.read(ctx.sessionID, filepath)
  82. let output = ""
  83. await LSP.touchFile(filepath, true)
  84. const diagnostics = await LSP.diagnostics()
  85. for (const [file, issues] of Object.entries(diagnostics)) {
  86. if (issues.length === 0) continue
  87. if (file === filepath) {
  88. output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
  89. continue
  90. }
  91. output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`
  92. }
  93. return {
  94. metadata: {
  95. diagnostics,
  96. diff,
  97. title: `${path.relative(app.path.root, filepath)}`,
  98. },
  99. output,
  100. }
  101. },
  102. })