Skip to content

Commit e4cac87

Browse files
committed
Changed: Optimize single-replace edit matching
1 parent 9d0809f commit e4cac87

3 files changed

Lines changed: 40 additions & 20 deletions

File tree

src/llm-coding-tools-core/src/tools/edit.rs

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ pub enum EditError {
2121
#[error("old_string not found in file content")]
2222
NotFound,
2323
/// Multiple matches found when replace_all is false.
24+
///
25+
/// This variant intentionally does not include an exact count so single-replace
26+
/// mode can stop searching as soon as it finds a second match.
2427
#[error(
25-
"oldString found {0} times and requires more code context to uniquely identify the intended match"
28+
"old_string found multiple times and requires more code context to uniquely identify the intended match"
2629
)]
27-
AmbiguousMatch(usize),
30+
AmbiguousMatch,
2831
}
2932

3033
impl From<std::io::Error> for EditError {
@@ -54,25 +57,43 @@ pub async fn edit_file<R: PathResolver>(
5457
let path = resolver.resolve(file_path)?;
5558
let content = fs::read_to_string(&path).await?;
5659

57-
let count = content.matches(old_string).count();
60+
let (new_content, replacement_count) = if replace_all {
61+
// replace_all reports the exact number of replacements, so this path
62+
// counts every match.
63+
let count = content.matches(old_string).count();
64+
if count == 0 {
65+
return Err(EditError::NotFound);
66+
}
5867

59-
if count == 0 {
60-
return Err(EditError::NotFound);
61-
}
62-
63-
if !replace_all && count > 1 {
64-
return Err(EditError::AmbiguousMatch(count));
65-
}
66-
67-
let new_content = if replace_all {
68-
content.replace(old_string, new_string)
68+
(content.replace(old_string, new_string), count)
6969
} else {
70-
content.replacen(old_string, new_string, 1)
70+
// Fast path for single replacement: advance a single non-overlapping
71+
// matcher until the second match (if any), then stop.
72+
let mut matches = content.match_indices(old_string);
73+
let Some((first_idx, _)) = matches.next() else {
74+
return Err(EditError::NotFound);
75+
};
76+
if matches.next().is_some() {
77+
return Err(EditError::AmbiguousMatch);
78+
}
79+
80+
let tail_start = first_idx + old_string.len();
81+
82+
// Build the edited string directly from slices to avoid rescanning.
83+
let mut replaced =
84+
String::with_capacity(content.len() - old_string.len() + new_string.len());
85+
replaced.push_str(&content[..first_idx]);
86+
replaced.push_str(new_string);
87+
replaced.push_str(&content[tail_start..]);
88+
(replaced, 1)
7189
};
7290

7391
fs::write(&path, &new_content).await?;
7492

75-
Ok(format!("Successfully replaced {} occurrence(s)", count))
93+
Ok(format!(
94+
"Successfully replaced {} occurrence(s)",
95+
replacement_count
96+
))
7697
}
7798

7899
#[cfg(test)]

src/llm-coding-tools-serdesai/src/absolute/edit.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ mod tests {
177177
match err {
178178
ToolError::ValidationFailed { errors, .. } => {
179179
assert!(!errors.is_empty());
180-
assert!(errors[0].message.contains("3 times"));
180+
assert!(errors[0].message.contains("multiple times"));
181181
}
182182
_ => panic!("Expected ValidationFailed"),
183183
}

src/llm-coding-tools-serdesai/src/common/edit.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,11 @@ pub(crate) fn error_to_serdes(err: EditError) -> ToolError {
1818
Some("old_string".to_string()),
1919
"old_string not found in file content".to_string(),
2020
),
21-
EditError::AmbiguousMatch(count) => ToolError::validation_error(
21+
EditError::AmbiguousMatch => ToolError::validation_error(
2222
tool_names::EDIT,
2323
Some("old_string".to_string()),
24-
format!(
25-
"old_string found {count} times and requires more code context to uniquely identify the intended match"
26-
),
24+
"old_string found multiple times and requires more code context to uniquely identify the intended match"
25+
.to_string(),
2726
),
2827
EditError::EmptyOldString => ToolError::validation_error(
2928
tool_names::EDIT,

0 commit comments

Comments
 (0)