pub fn match_ranges(haystack: &str, needles: &[String]) -> Vec<(usize, usize)> {
let hay: Vec<(usize, char)> = haystack.char_indices().collect();
let mut ranges: Vec<(usize, usize)> = Vec::new();
for needle in needles {
collect_needle(&hay, haystack.len(), needle, &mut ranges);
}
ranges.sort_unstable_by_key(|(s, e)| (*s, std::cmp::Reverse(*e)));
ranges.dedup();
let mut kept: Vec<(usize, usize)> = Vec::new();
let mut pos = 0;
for (start, end) in ranges {
if start < pos {
continue; }
kept.push((start, end));
pos = end;
}
kept
}
fn collect_needle(
hay: &[(usize, char)],
hay_len: usize,
needle: &str,
out: &mut Vec<(usize, usize)>,
) {
let needle_chars: Vec<char> = needle.chars().collect();
if needle_chars.is_empty() {
return;
}
let n = needle_chars.len();
let mut i = 0;
while i + n <= hay.len() {
if (0..n).all(|j| chars_eq_ignore_case(hay[i + j].1, needle_chars[j])) {
let start = hay[i].0;
let end = hay.get(i + n).map(|(b, _)| *b).unwrap_or(hay_len);
out.push((start, end));
i += n; } else {
i += 1;
}
}
}
fn chars_eq_ignore_case(a: char, b: char) -> bool {
a == b || a.to_lowercase().eq(b.to_lowercase())
}
pub fn style_ranges<'a, T>(
line: &'a str,
ranges: &[(usize, usize)],
mut mk: impl FnMut(&'a str, bool) -> T,
) -> Vec<T> {
let mut out = Vec::new();
let mut pos = 0;
for &(start, end) in ranges {
if start > pos {
out.push(mk(&line[pos..start], false));
}
out.push(mk(&line[start..end], true));
pos = end;
}
if pos < line.len() {
out.push(mk(&line[pos..], false));
}
out
}
pub fn wrap_line(line: &str, max_width: usize) -> Vec<String> {
if max_width == 0 || line.chars().count() <= max_width {
return vec![line.to_string()];
}
let mut result = Vec::new();
let mut remaining = line;
while remaining.chars().count() > max_width {
let byte_limit = remaining
.char_indices()
.nth(max_width)
.map(|(i, _)| i)
.unwrap_or(remaining.len());
let break_at = remaining[..byte_limit]
.rfind(' ')
.map(|i| i + 1) .unwrap_or(byte_limit);
result.push(remaining[..break_at].trim_end().to_string());
remaining = &remaining[break_at..];
}
if !remaining.is_empty() {
result.push(remaining.to_string());
}
result
}
#[cfg(test)]
mod tests {
use super::*;
fn needles(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn matches_are_case_insensitive() {
assert_eq!(match_ranges("Hello World", &needles(&["world"])), [(6, 11)]);
assert_eq!(match_ranges("HELLO", &needles(&["hell"])), [(0, 4)]);
}
#[test]
fn all_occurrences_per_needle() {
assert_eq!(match_ranges("aa aa", &needles(&["aa"])), [(0, 2), (3, 5)]);
}
#[test]
fn overlapping_needles_keep_longest() {
let r = match_ranges("foobar", &needles(&["foo", "foobar"]));
assert_eq!(r, [(0, 6)]);
}
#[test]
fn empty_needles_contribute_nothing() {
assert!(match_ranges("anything", &needles(&[""])).is_empty());
assert!(match_ranges("anything", &[]).is_empty());
}
#[test]
fn ascii_needle_matches_on_line_containing_a_length_changing_fold() {
let hay = "Hİ there";
let r = match_ranges(hay, &needles(&["there"]));
assert_eq!(r.len(), 1, "ascii needle must still match: {r:?}");
let (s, e) = r[0];
assert!(hay.is_char_boundary(s) && hay.is_char_boundary(e));
assert_eq!(&hay[s..e], "there");
}
#[test]
fn multibyte_haystack_offsets_are_valid() {
let hay = "日本語テスト";
let r = match_ranges(hay, &needles(&["テスト"]));
assert_eq!(r.len(), 1);
let (s, e) = r[0];
assert_eq!(&hay[s..e], "テスト");
}
#[test]
fn style_ranges_alternates_gaps_and_matches() {
let line = "see widget and gadget";
let ranges = match_ranges(line, &needles(&["widget", "gadget"]));
let segs: Vec<(String, bool)> = style_ranges(line, &ranges, |s, hit| (s.to_string(), hit));
assert_eq!(
segs,
vec![
("see ".to_string(), false),
("widget".to_string(), true),
(" and ".to_string(), false),
("gadget".to_string(), true),
]
);
}
#[test]
fn style_ranges_empty_is_one_non_match_segment() {
let segs: Vec<(String, bool)> =
style_ranges("no matches here", &[], |s, hit| (s.to_string(), hit));
assert_eq!(segs, vec![("no matches here".to_string(), false)]);
}
#[test]
fn wrap_line_fits_within_width() {
assert_eq!(wrap_line("short", 20), vec!["short"]);
}
#[test]
fn wrap_line_breaks_at_word_boundary() {
assert_eq!(
wrap_line("hello world foo bar", 12),
vec!["hello world", "foo bar"]
);
}
#[test]
fn wrap_line_hard_breaks_long_word() {
assert_eq!(wrap_line("abcdefghij", 5), vec!["abcde", "fghij"]);
}
#[test]
fn wrap_line_handles_multibyte_chars() {
assert_eq!(wrap_line("日本語テスト", 3), vec!["日本語", "テスト"]);
}
#[test]
fn wrap_line_empty_string() {
assert_eq!(wrap_line("", 10), vec![""]);
}
}