use std::ops::Range;
pub fn fuzzy_match_ranges(needle: &str, haystack: &str) -> Vec<Range<usize>> {
let norm_needle: Vec<&str> = needle.lines().map(str::trim_end).collect();
if norm_needle.is_empty() {
return vec![];
}
let n = norm_needle.len();
let hay_lines = collect_lines(haystack);
let h = hay_lines.len();
let mut matches = Vec::new();
for start in 0..h {
if start + n > h {
break;
}
let hit = norm_needle
.iter()
.enumerate()
.all(|(j, &nline)| hay_lines[start + j].2.trim_end() == nline);
if hit {
let match_start = hay_lines[start].0;
let match_end = hay_lines[start + n - 1].1;
let end = if needle.ends_with('\n') && match_end < haystack.len() {
match_end + 1
} else {
match_end
};
matches.push(match_start..end);
}
}
matches
}
fn collect_lines(text: &str) -> Vec<(usize, usize, &str)> {
let mut result = Vec::new();
let mut offset = 0usize;
let mut rest = text;
loop {
match rest.find('\n') {
Some(nl) => {
let line = &rest[..nl];
result.push((offset, offset + line.len(), line));
offset += nl + 1;
rest = &rest[nl + 1..];
}
None => {
result.push((offset, offset + rest.len(), rest));
break;
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
fn apply_one(needle: &str, haystack: &str, replacement: &str) -> String {
let ranges = fuzzy_match_ranges(needle, haystack);
assert_eq!(ranges.len(), 1, "expected exactly one fuzzy match");
let r = ranges.into_iter().next().unwrap();
format!(
"{}{}{}",
&haystack[..r.start],
replacement,
&haystack[r.end..]
)
}
#[test]
fn exact_match_returns_nothing_here() {
let hay = "fn foo() {\n let x = 1;\n}\n";
let needle = " let x = 1;";
let r = fuzzy_match_ranges(needle, hay);
assert_eq!(r.len(), 1);
}
#[test]
fn trailing_space_on_needle_matches() {
let hay = "fn foo() {\n let x = 1; \n}\n";
let needle = " let x = 1;";
let result = apply_one(needle, hay, " let x = 2;");
assert_eq!(result, "fn foo() {\n let x = 2;\n}\n");
}
#[test]
fn trailing_space_on_file_multiline() {
let hay = "a \nb \nc\n";
let needle = "a\nb";
let result = apply_one(needle, hay, "X\nY");
assert_eq!(result, "X\nY\nc\n");
}
#[test]
fn crlf_file_matches_lf_needle() {
let hay = "fn foo() {\r\n let x = 1;\r\n}\r\n";
let needle = " let x = 1;";
let ranges = fuzzy_match_ranges(needle, hay);
assert_eq!(ranges.len(), 1, "CRLF file should match LF needle");
}
#[test]
fn no_match_returns_empty() {
let hay = "hello world\n";
let needle = "does not exist";
assert!(fuzzy_match_ranges(needle, hay).is_empty());
}
#[test]
fn ambiguous_match_returns_multiple() {
let hay = "foo \nbar\nfoo \nbar\n";
let needle = "foo\nbar";
let ranges = fuzzy_match_ranges(needle, hay);
assert_eq!(ranges.len(), 2, "should detect ambiguity");
}
#[test]
fn needle_with_trailing_newline_includes_newline_in_range() {
let hay = "a\nb\nc\n";
let needle = "a\nb\n";
let ranges = fuzzy_match_ranges(needle, hay);
assert_eq!(ranges.len(), 1);
assert_eq!(&hay[ranges[0].clone()], "a\nb\n");
}
#[test]
fn single_line_trailing_space() {
let hay = " return Ok(value); \n";
let needle = " return Ok(value);";
let result = apply_one(needle, hay, " return Ok(new_value);");
assert_eq!(result, " return Ok(new_value);\n");
}
}