Skip to content

Commit bd3c5ef

Browse files
feat: add tests for handling empty lines and Unicode normalization in search-replace logic and enhance candidate matching logic
1 parent 99ea859 commit bd3c5ef

2 files changed

Lines changed: 95 additions & 4 deletions

File tree

src/tools/file_write_or_edit.rs

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -337,25 +337,59 @@ fn find_contiguous_candidates(
337337
return Vec::new();
338338
}
339339

340+
if ignore_empty_lines {
341+
return find_empty_line_tolerant_candidates(lines, block, offset, &search_lines);
342+
}
343+
340344
let max_start = lines.len() - search_lines.len();
341345
(offset..=max_start)
342-
.filter_map(|start| match_candidate(lines, &search_lines, block, start, ignore_empty_lines))
346+
.filter_map(|start| {
347+
let end = start + search_lines.len();
348+
let actual_lines: Vec<&String> = lines[start..end].iter().collect();
349+
match_candidate(lines, &actual_lines, &search_lines, block, start, end, false)
350+
})
351+
.collect()
352+
}
353+
354+
fn find_empty_line_tolerant_candidates(
355+
lines: &[String],
356+
block: &SearchReplaceBlock,
357+
offset: usize,
358+
search_lines: &[String],
359+
) -> Vec<MatchCandidate> {
360+
let compact_lines: Vec<(usize, &String)> =
361+
lines.iter().enumerate().skip(offset).filter(|(_, line)| !line.trim().is_empty()).collect();
362+
363+
if compact_lines.len() < search_lines.len() {
364+
return Vec::new();
365+
}
366+
367+
let max_start = compact_lines.len() - search_lines.len();
368+
(0..=max_start)
369+
.filter_map(|compact_start| {
370+
let compact_end = compact_start + search_lines.len();
371+
let start = compact_lines[compact_start].0;
372+
let end = compact_lines[compact_end - 1].0 + 1;
373+
let actual_lines: Vec<&String> =
374+
compact_lines[compact_start..compact_end].iter().map(|(_, line)| *line).collect();
375+
match_candidate(lines, &actual_lines, search_lines, block, start, end, true)
376+
})
343377
.collect()
344378
}
345379

346380
fn match_candidate(
347381
lines: &[String],
382+
actual_lines: &[&String],
348383
search_lines: &[String],
349384
block: &SearchReplaceBlock,
350385
start: usize,
386+
end: usize,
351387
ignore_empty_lines: bool,
352388
) -> Option<MatchCandidate> {
353-
let end = start + search_lines.len();
354-
let matched = &lines[start..end];
355389
let mut tolerances = Vec::new();
356390
let mut score = 0;
357391

358-
for (actual, expected) in matched.iter().zip(search_lines) {
392+
for (actual, expected) in actual_lines.iter().zip(search_lines) {
359393
let line_match = matching_tolerance(actual, expected)?;
360394
if let LineMatch::Tolerated(tolerance) = line_match {
361395
score += tolerance.score();
@@ -374,6 +408,7 @@ fn match_candidate(
374408
replace = replace.into_iter().map(|line| remove_leading_line_number(&line)).collect();
375409
}
376410
if tolerances.contains(&ToleranceKind::IgnoreIndentation) {
411+
let matched = &lines[start..end];
377412
replace = fix_indentation(matched, search_lines, &replace);
378413
}
379414

tests/file_write_edit_test.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,62 @@ class Example:
516516
Ok(())
517517
}
518518

519+
#[tokio::test(flavor = "multi_thread")]
520+
async fn test_search_replace_matches_across_extra_blank_lines() -> Result<()> {
521+
let temp_dir = TempDir::new()?;
522+
let bash_state_arc = create_initialized_state(&temp_dir, "blank-lines").await?;
523+
524+
let file_path = temp_dir.path().join("blank_lines.txt");
525+
std::fs::write(&file_path, "alpha\n\nbeta\ngamma\n")?;
526+
read_file_before_edit(&bash_state_arc, &file_path).await?;
527+
528+
let edit = FileWriteOrEdit {
529+
file_path: file_path.to_string_lossy().to_string(),
530+
percentage_to_change: 10,
531+
text_or_search_replace_blocks: r"<<<<<<< SEARCH
532+
alpha
533+
beta
534+
=======
535+
alpha
536+
beta-updated
537+
>>>>>>> REPLACE"
538+
.to_string(),
539+
thread_id: "blanklines".to_string(),
540+
};
541+
542+
winx_code_agent::tools::file_write_or_edit::handle_tool_call(&bash_state_arc, edit).await?;
543+
544+
let content = std::fs::read_to_string(&file_path)?;
545+
assert_eq!(content, "alpha\nbeta-updated\ngamma\n");
546+
547+
Ok(())
548+
}
549+
550+
#[tokio::test(flavor = "multi_thread")]
551+
async fn test_search_replace_normalizes_common_unicode_mistakes() -> Result<()> {
552+
let temp_dir = TempDir::new()?;
553+
let bash_state_arc = create_initialized_state(&temp_dir, "unicode-mistakes").await?;
554+
555+
let file_path = temp_dir.path().join("unicode.txt");
556+
std::fs::write(&file_path, "println!(\"hello - world...\");\n")?;
557+
read_file_before_edit(&bash_state_arc, &file_path).await?;
558+
559+
let edit = FileWriteOrEdit {
560+
file_path: file_path.to_string_lossy().to_string(),
561+
percentage_to_change: 10,
562+
text_or_search_replace_blocks: "<<<<<<< SEARCH\nprintln!(\u{201c}hello \u{2014} world\u{2026}\u{201d});\n=======\nprintln!(\"updated\");\n>>>>>>> REPLACE"
563+
.to_string(),
564+
thread_id: "unicodemistakes".to_string(),
565+
};
566+
567+
winx_code_agent::tools::file_write_or_edit::handle_tool_call(&bash_state_arc, edit).await?;
568+
569+
let content = std::fs::read_to_string(&file_path)?;
570+
assert_eq!(content, "println!(\"updated\");\n");
571+
572+
Ok(())
573+
}
574+
519575
#[tokio::test(flavor = "multi_thread")]
520576
async fn test_search_replace_removes_readfiles_line_numbers() -> Result<()> {
521577
let temp_dir = TempDir::new()?;

0 commit comments

Comments
 (0)