@@ -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
3033impl 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) ]
0 commit comments