@@ -44,6 +44,7 @@ impl FileSystemService {
4444 edits : Vec < EditOperation > ,
4545 dry_run : Option < bool > ,
4646 save_to : Option < & Path > ,
47+ replace_all : Option < bool > ,
4748 ) -> ServiceResult < String > {
4849 let allowed_directories = self . allowed_directories ( ) . await ;
4950 let valid_path = self . validate_path ( file_path, allowed_directories) ?;
@@ -59,9 +60,25 @@ impl FileSystemService {
5960 for edit in edits {
6061 let normalized_old = normalize_line_endings ( & edit. old_text ) ;
6162 let normalized_new = normalize_line_endings ( & edit. new_text ) ;
63+ let do_replace_all = replace_all. unwrap_or ( false ) ;
64+
6265 // If exact match exists, use it
6366 if modified_content. contains ( & normalized_old) {
64- modified_content = modified_content. replacen ( & normalized_old, & normalized_new, 1 ) ;
67+ let count = modified_content. matches ( & normalized_old) . count ( ) ;
68+ if !do_replace_all && count > 1 {
69+ return Err ( RpcError :: internal_error ( )
70+ . with_message ( format ! (
71+ "Multiple occurrences of oldText found ({}). Use replace_all=true to replace all occurrences" ,
72+ count
73+ ) )
74+ . into ( ) ) ;
75+ }
76+ if do_replace_all {
77+ modified_content = modified_content. replace ( & normalized_old, & normalized_new) ;
78+ } else {
79+ modified_content =
80+ modified_content. replacen ( & normalized_old, & normalized_new, 1 ) ;
81+ }
6582 continue ;
6683 }
6784
@@ -78,8 +95,6 @@ impl FileSystemService {
7895 . map ( |s| s. to_string ( ) )
7996 . collect ( ) ;
8097
81- let mut match_found = false ;
82-
8398 // skip when the match is impossible:
8499 if old_lines. len ( ) > content_lines. len ( ) {
85100 let error_message = format ! (
@@ -94,6 +109,8 @@ impl FileSystemService {
94109 }
95110
96111 let max_start = content_lines. len ( ) . saturating_sub ( old_lines. len ( ) ) ;
112+ let mut match_count = 0 ;
113+ let mut last_match_idx = 0 ;
97114 for i in 0 ..=max_start {
98115 let potential_match = & content_lines[ i..i + old_lines. len ( ) ] ;
99116
@@ -104,71 +121,155 @@ impl FileSystemService {
104121 } ) ;
105122
106123 if is_match {
107- // Preserve original indentation of first line
108- let original_indent = content_lines[ i]
109- . chars ( )
110- . take_while ( |& c| c. is_whitespace ( ) )
111- . collect :: < String > ( ) ;
112-
113- let new_lines: Vec < String > = normalized_new
114- . split ( '\n' )
115- . enumerate ( )
116- . map ( |( j, line) | {
117- // Keep indentation of the first line
118- if j == 0 {
119- return format ! ( "{}{}" , original_indent, line. trim_start( ) ) ;
120- }
121-
122- // For subsequent lines, preserve relative indentation and original whitespace type
123- let old_indent = old_lines
124- . get ( j)
125- . map ( |line| {
126- line. chars ( )
127- . take_while ( |& c| c. is_whitespace ( ) )
128- . collect :: < String > ( )
129- } )
130- . unwrap_or_default ( ) ;
131-
132- let new_indent = line
133- . chars ( )
134- . take_while ( |& c| c. is_whitespace ( ) )
135- . collect :: < String > ( ) ;
136-
137- // Use the same whitespace character as original_indent (tabs or spaces)
138- let indent_char = if original_indent. contains ( '\t' ) {
139- "\t "
140- } else {
141- " "
142- } ;
143- let relative_indent = if new_indent. len ( ) >= old_indent. len ( ) {
144- new_indent. len ( ) - old_indent. len ( )
145- } else {
146- 0 // Don't reduce indentation below original
147- } ;
148- format ! (
149- "{}{}{}" ,
150- & original_indent,
151- & indent_char. repeat( relative_indent) ,
152- line. trim_start( )
153- )
154- } )
155- . collect ( ) ;
156-
157- let mut content_lines = content_lines. clone ( ) ;
158- content_lines. splice ( i..i + old_lines. len ( ) , new_lines) ;
159- modified_content = content_lines. join ( "\n " ) ;
160- match_found = true ;
161- break ;
124+ match_count += 1 ;
125+ last_match_idx = i;
126+ if !do_replace_all {
127+ break ;
128+ }
162129 }
163130 }
164- if !match_found {
131+
132+ if match_count == 0 {
165133 return Err ( RpcError :: internal_error ( )
166134 . with_message ( format ! (
167135 "Could not find exact match for edit:\n {}" ,
168136 edit. old_text
169137 ) )
170138 . into ( ) ) ;
171139 }
140+
141+ if !do_replace_all && match_count > 1 {
142+ return Err ( RpcError :: internal_error ( )
143+ . with_message ( format ! (
144+ "Multiple occurrences of oldText found ({}). Use replaceAll:true to replace all occurrences" ,
145+ match_count
146+ ) )
147+ . into ( ) ) ;
148+ }
149+
150+ // Apply the edit(s)
151+ let mut content_lines = content_lines. clone ( ) ;
152+ if do_replace_all {
153+ let mut i = 0 ;
154+ while i <= content_lines. len ( ) . saturating_sub ( old_lines. len ( ) ) {
155+ let potential_match = & content_lines[ i..i + old_lines. len ( ) ] ;
156+ let is_match = old_lines. iter ( ) . enumerate ( ) . all ( |( j, old_line) | {
157+ let content_line = & potential_match[ j] ;
158+ old_line. trim ( ) == content_line. trim ( )
159+ } ) ;
160+
161+ if is_match {
162+ let original_indent = content_lines[ i]
163+ . chars ( )
164+ . take_while ( |& c| c. is_whitespace ( ) )
165+ . collect :: < String > ( ) ;
166+
167+ let new_lines: Vec < String > = normalized_new
168+ . split ( '\n' )
169+ . enumerate ( )
170+ . map ( |( j, line) | {
171+ if j == 0 {
172+ return format ! ( "{}{}" , original_indent, line. trim_start( ) ) ;
173+ }
174+
175+ let old_indent = old_lines
176+ . get ( j)
177+ . map ( |line| {
178+ line. chars ( )
179+ . take_while ( |& c| c. is_whitespace ( ) )
180+ . collect :: < String > ( )
181+ } )
182+ . unwrap_or_default ( ) ;
183+
184+ let new_indent = line
185+ . chars ( )
186+ . take_while ( |& c| c. is_whitespace ( ) )
187+ . collect :: < String > ( ) ;
188+
189+ let indent_char = if original_indent. contains ( '\t' ) {
190+ "\t "
191+ } else {
192+ " "
193+ } ;
194+ let relative_indent = if new_indent. len ( ) >= old_indent. len ( ) {
195+ new_indent. len ( ) - old_indent. len ( )
196+ } else {
197+ 0
198+ } ;
199+ format ! (
200+ "{}{}{}" ,
201+ & original_indent,
202+ & indent_char. repeat( relative_indent) ,
203+ line. trim_start( )
204+ )
205+ } )
206+ . collect ( ) ;
207+
208+ content_lines. splice ( i..i + old_lines. len ( ) , new_lines) ;
209+ // Don't increment i since we replaced the block and need to check again
210+ } else {
211+ i += 1 ;
212+ }
213+ }
214+ modified_content = content_lines. join ( "\n " ) ;
215+ } else {
216+ // Single match case - use last_match_idx
217+ let i = last_match_idx;
218+ let original_indent = content_lines[ i]
219+ . chars ( )
220+ . take_while ( |& c| c. is_whitespace ( ) )
221+ . collect :: < String > ( ) ;
222+
223+ let new_lines: Vec < String > = normalized_new
224+ . split ( '\n' )
225+ . enumerate ( )
226+ . map ( |( j, line) | {
227+ if j == 0 {
228+ return format ! ( "{}{}" , original_indent, line. trim_start( ) ) ;
229+ }
230+
231+ let old_indent = old_lines
232+ . get ( j)
233+ . map ( |line| {
234+ line. chars ( )
235+ . take_while ( |& c| c. is_whitespace ( ) )
236+ . collect :: < String > ( )
237+ } )
238+ . unwrap_or_default ( ) ;
239+
240+ let new_indent = line
241+ . chars ( )
242+ . take_while ( |& c| c. is_whitespace ( ) )
243+ . collect :: < String > ( ) ;
244+
245+ let indent_char = if original_indent. contains ( '\t' ) {
246+ "\t "
247+ } else {
248+ " "
249+ } ;
250+ let relative_indent = if new_indent. len ( ) >= old_indent. len ( ) {
251+ new_indent. len ( ) - old_indent. len ( )
252+ } else {
253+ 0
254+ } ;
255+ format ! (
256+ "{}{}{}" ,
257+ & original_indent,
258+ & indent_char. repeat( relative_indent) ,
259+ line. trim_start( )
260+ )
261+ } )
262+ . collect ( ) ;
263+
264+ content_lines. splice ( i..i + old_lines. len ( ) , new_lines) ;
265+ modified_content = content_lines. join ( "\n " ) ;
266+ }
267+ if !do_replace_all && match_count == 1 {
268+ continue ;
269+ }
270+ if do_replace_all {
271+ continue ;
272+ }
172273 }
173274
174275 let diff = self . create_unified_diff (
0 commit comments