llm_coding_tools_rig/absolute/
edit.rs1use llm_coding_tools_core::operations::edit_file;
4use llm_coding_tools_core::path::AbsolutePathResolver;
5use llm_coding_tools_core::tool_names;
6pub use llm_coding_tools_core::EditError;
7use llm_coding_tools_core::ToolContext;
8use rig::completion::ToolDefinition;
9use rig::tool::Tool;
10use schemars::{schema_for, JsonSchema};
11use serde::Deserialize;
12
13#[derive(Debug, Clone, Deserialize, JsonSchema)]
15pub struct EditArgs {
16 pub file_path: String,
18 pub old_string: String,
20 pub new_string: String,
22 #[serde(default)]
24 pub replace_all: bool,
25}
26
27#[derive(Debug, Clone, Default)]
29pub struct EditTool;
30
31impl EditTool {
32 #[inline]
34 pub fn new() -> Self {
35 Self
36 }
37}
38
39impl Tool for EditTool {
40 const NAME: &'static str = tool_names::EDIT;
41
42 type Error = EditError;
43 type Args = EditArgs;
44 type Output = String;
45
46 async fn definition(&self, _prompt: String) -> ToolDefinition {
47 ToolDefinition {
48 name: <Self as Tool>::NAME.to_string(),
49 description: "Makes exact string replacements in files. Use replace_all=true to \
50 replace all occurrences."
51 .to_string(),
52 parameters: serde_json::to_value(schema_for!(EditArgs))
53 .expect("EditArgs schema generation should not fail"),
54 }
55 }
56
57 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
58 let resolver = AbsolutePathResolver;
59 edit_file(
60 &resolver,
61 &args.file_path,
62 &args.old_string,
63 &args.new_string,
64 args.replace_all,
65 )
66 .await
67 }
68}
69
70impl ToolContext for EditTool {
71 const NAME: &'static str = tool_names::EDIT;
72
73 fn context(&self) -> &'static str {
74 llm_coding_tools_core::context::EDIT_ABSOLUTE
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use llm_coding_tools_core::ToolError;
82 use std::io::Write as _;
83 use tempfile::NamedTempFile;
84
85 #[tokio::test]
86 async fn replaces_single_occurrence() {
87 let mut file = NamedTempFile::new().unwrap();
88 file.write_all(b"hello world").unwrap();
89 file.flush().unwrap();
90 let tool = EditTool::new();
91 let result = tool
92 .call(EditArgs {
93 file_path: file.path().to_string_lossy().to_string(),
94 old_string: "world".to_string(),
95 new_string: "rust".to_string(),
96 replace_all: false,
97 })
98 .await
99 .unwrap();
100 assert!(result.contains("1 occurrence"));
101 }
102
103 #[tokio::test]
104 async fn rejects_relative_path() {
105 let tool = EditTool::new();
106 let result = tool
107 .call(EditArgs {
108 file_path: "relative/path.txt".to_string(),
109 old_string: "old".to_string(),
110 new_string: "new".to_string(),
111 replace_all: false,
112 })
113 .await;
114 assert!(matches!(
115 result,
116 Err(EditError::Tool(ToolError::InvalidPath(_)))
117 ));
118 }
119}