Skip to content

Commit 25da5a6

Browse files
authored
fix: issue 12 edit_file tool panics (#14)
* fix: improve edit file logic to prevent negative indentation * test: add more tests * chore: update tests * feat: preserve line ending * cleanup * chore: fix clippy warnings
1 parent a98e0fe commit 25da5a6

File tree

3 files changed

+343
-23
lines changed

3 files changed

+343
-23
lines changed

src/fs_service.rs

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ impl FileSystemService {
6262
let expanded_path = expand_home(requested_path.to_path_buf());
6363

6464
// Resolve the absolute path
65-
let absolute_path = expanded_path
66-
.as_path()
67-
.is_absolute()
68-
.then(|| expanded_path.clone())
69-
.unwrap_or_else(|| env::current_dir().unwrap().join(&expanded_path));
65+
let absolute_path = if expanded_path.as_path().is_absolute() {
66+
expanded_path.clone()
67+
} else {
68+
env::current_dir().unwrap().join(&expanded_path)
69+
};
7070

7171
// Normalize the path
7272
let normalized_requested = normalize_path(&absolute_path);
@@ -121,6 +121,16 @@ impl FileSystemService {
121121
})
122122
}
123123

124+
fn detect_line_ending(&self, text: &str) -> &str {
125+
if text.contains("\r\n") {
126+
"\r\n"
127+
} else if text.contains('\r') {
128+
"\r"
129+
} else {
130+
"\n"
131+
}
132+
}
133+
124134
pub async fn zip_directory(
125135
&self,
126136
input_dir: String,
@@ -472,6 +482,7 @@ impl FileSystemService {
472482

473483
// Read file content and normalize line endings
474484
let content_str = tokio::fs::read_to_string(&valid_path).await?;
485+
let original_line_ending = self.detect_line_ending(&content_str);
475486
let content_str = normalize_line_endings(&content_str);
476487

477488
// Apply edits sequentially
@@ -480,15 +491,13 @@ impl FileSystemService {
480491
for edit in edits {
481492
let normalized_old = normalize_line_endings(&edit.old_text);
482493
let normalized_new = normalize_line_endings(&edit.new_text);
483-
484494
// If exact match exists, use it
485495
if modified_content.contains(&normalized_old) {
486496
modified_content = modified_content.replacen(&normalized_old, &normalized_new, 1);
487497
continue;
488498
}
489499

490500
// Otherwise, try line-by-line matching with flexibility for whitespace
491-
// trim ends help to avoid inconsistencies empty lines at the end that may break the comparison
492501
let old_lines: Vec<String> = normalized_old
493502
.trim_end()
494503
.split('\n')
@@ -514,7 +523,6 @@ impl FileSystemService {
514523

515524
if is_match {
516525
// Preserve original indentation of first line
517-
// leading spaces
518526
let original_indent = content_lines[i]
519527
.chars()
520528
.take_while(|&c| c.is_whitespace())
@@ -524,12 +532,12 @@ impl FileSystemService {
524532
.split('\n')
525533
.enumerate()
526534
.map(|(j, line)| {
527-
// keep indentation of the first line
535+
// Keep indentation of the first line
528536
if j == 0 {
529537
return format!("{}{}", original_indent, line.trim_start());
530538
}
531539

532-
// For subsequent lines, try to preserve relative indentation
540+
// For subsequent lines, preserve relative indentation and original whitespace type
533541
let old_indent = old_lines
534542
.get(j)
535543
.map(|line| {
@@ -544,12 +552,22 @@ impl FileSystemService {
544552
.take_while(|&c| c.is_whitespace())
545553
.collect::<String>();
546554

547-
let relative_indent = new_indent.len() - old_indent.len();
548-
555+
// Use the same whitespace character as original_indent (tabs or spaces)
556+
let indent_char = if original_indent.contains('\t') {
557+
"\t"
558+
} else {
559+
" "
560+
};
561+
let relative_indent = if new_indent.len() >= old_indent.len() {
562+
new_indent.len() - old_indent.len()
563+
} else {
564+
0 // Don't reduce indentation below original
565+
};
549566
format!(
550-
"{}{}",
551-
original_indent,
552-
" ".repeat(relative_indent.max(0)) + line.trim_start()
567+
"{}{}{}",
568+
&original_indent,
569+
&indent_char.repeat(relative_indent),
570+
line.trim_start()
553571
)
554572
})
555573
.collect();
@@ -593,6 +611,7 @@ impl FileSystemService {
593611

594612
if !is_dry_run {
595613
let target = save_to.unwrap_or(valid_path.as_path());
614+
let modified_content = modified_content.replace("\n", original_line_ending);
596615
tokio::fs::write(target, modified_content).await?;
597616
}
598617

src/fs_service/utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ pub async fn write_zip_entry(
103103
}
104104

105105
pub fn normalize_line_endings(text: &str) -> String {
106-
text.replace("\r\n", "\n")
106+
text.replace("\r\n", "\n").replace('\r', "\n")
107107
}
108108

109109
// checks if path component is a Prefix::VerbatimDisk

0 commit comments

Comments
 (0)