diff --git a/crates/agent/src/agent/tools/fs_write.rs b/crates/agent/src/agent/tools/fs_write.rs index 2b99f81ed3..00a697252a 100644 --- a/crates/agent/src/agent/tools/fs_write.rs +++ b/crates/agent/src/agent/tools/fs_write.rs @@ -220,7 +220,10 @@ impl StrReplace { .await .map_err(|e| ToolExecutionError::io(format!("failed to read {}", path.to_string_lossy()), e))?; - let matches = file.match_indices(&self.old_str).collect::>(); + let line_ending = if file.contains("\r\n") { "\r\n" } else { "\n" }; + let old_str = self.old_str.replace("\r\n", "\n").replace('\n', line_ending); + let new_str = self.new_str.replace("\r\n", "\n").replace('\n', line_ending); + let matches = file.match_indices(&old_str).collect::>(); match matches.len() { 0 => { return Err(ToolExecutionError::Custom(format!( @@ -229,7 +232,7 @@ impl StrReplace { ))); }, 1 => { - let file = file.replacen(&self.old_str, &self.new_str, 1); + let file = file.replacen(&old_str, &new_str, 1); tokio::fs::write(path, file) .await .map_err(|e| ToolExecutionError::io(format!("failed to read {}", path.to_string_lossy()), e))?; @@ -240,7 +243,7 @@ impl StrReplace { "{x} occurrences of old_str were found when only 1 is expected" ))); } - let file = file.replace(&self.old_str, &self.new_str); + let file = file.replace(&old_str, &new_str); tokio::fs::write(path, file) .await .map_err(|e| ToolExecutionError::io(format!("failed to read {}", path.to_string_lossy()), e))?; @@ -421,6 +424,25 @@ mod tests { assert_eq!(content, "baz bar baz"); } + #[tokio::test] + async fn test_str_replace_preserves_crlf() { + let test_base = TestBase::new() + .await + .with_file(("test.txt", "first\r\nsecond\r\nthird")) + .await; + + let tool = FsWrite::StrReplace(StrReplace { + path: test_base.join("test.txt").to_string_lossy().to_string(), + old_str: "first\nsecond".to_string(), + new_str: "one\ntwo".to_string(), + replace_all: false, + }); + + assert!(tool.execute(None, &test_base).await.is_ok()); + let content = tokio::fs::read_to_string(test_base.join("test.txt")).await.unwrap(); + assert_eq!(content, "one\r\ntwo\r\nthird"); + } + #[tokio::test] async fn test_str_replace_no_match() { let test_base = TestBase::new().await.with_file(("test.txt", "hello world")).await; diff --git a/crates/chat-cli/src/cli/chat/tools/fs_write.rs b/crates/chat-cli/src/cli/chat/tools/fs_write.rs index 09a64058a1..af24b7138a 100644 --- a/crates/chat-cli/src/cli/chat/tools/fs_write.rs +++ b/crates/chat-cli/src/cli/chat/tools/fs_write.rs @@ -133,7 +133,10 @@ impl FsWrite { }, FsWrite::StrReplace { old_str, new_str, .. } => { let file = os.fs.read_to_string(&path).await?; - let matches = file.match_indices(old_str).collect::>(); + let line_ending = if file.contains("\r\n") { "\r\n" } else { "\n" }; + let old_str = old_str.replace("\r\n", "\n").replace('\n', line_ending); + let new_str = new_str.replace("\r\n", "\n").replace('\n', line_ending); + let matches = file.match_indices(&old_str).collect::>(); queue!( output, style::Print("Updating: "), @@ -145,7 +148,7 @@ impl FsWrite { match matches.len() { 0 => return Err(eyre!("no occurrences of \"{old_str}\" were found")), 1 => { - let file = file.replacen(old_str, new_str, 1); + let file = file.replacen(&old_str, &new_str, 1); os.fs.write(&path, file).await?; }, x => return Err(eyre!("{x} occurrences of old_str were found when only 1 is expected")), @@ -1068,6 +1071,29 @@ mod tests { ); } + #[tokio::test] + async fn test_fs_write_tool_str_replace_preserves_crlf() { + let os = Os::new().await.unwrap(); + let mut stdout = std::io::stdout(); + let mut line_tracker = HashMap::new(); + let path = "/crlf.txt"; + os.fs.write(path, "first\r\nsecond\r\nthird").await.unwrap(); + + let v = serde_json::json!({ + "path": path, + "command": "str_replace", + "old_str": "first\nsecond", + "new_str": "one\ntwo", + }); + serde_json::from_value::(v) + .unwrap() + .invoke(&os, &mut stdout, &mut line_tracker) + .await + .unwrap(); + + assert_eq!(os.fs.read_to_string(path).await.unwrap(), "one\r\ntwo\r\nthird"); + } + #[tokio::test] async fn test_fs_write_tool_insert_at_beginning() { let os = setup_test_directory().await;