1pub fn match_ranges(haystack: &str, needles: &[String]) -> Vec<(usize, usize)> {
19 let hay: Vec<(usize, char)> = haystack.char_indices().collect();
21 let mut ranges: Vec<(usize, usize)> = Vec::new();
22 for needle in needles {
23 collect_needle(&hay, haystack.len(), needle, &mut ranges);
24 }
25 ranges.sort_unstable_by_key(|(s, e)| (*s, std::cmp::Reverse(*e)));
28 ranges.dedup();
29
30 let mut kept: Vec<(usize, usize)> = Vec::new();
31 let mut pos = 0;
32 for (start, end) in ranges {
33 if start < pos {
34 continue; }
36 kept.push((start, end));
37 pos = end;
38 }
39 kept
40}
41
42fn collect_needle(
46 hay: &[(usize, char)],
47 hay_len: usize,
48 needle: &str,
49 out: &mut Vec<(usize, usize)>,
50) {
51 let needle_chars: Vec<char> = needle.chars().collect();
52 if needle_chars.is_empty() {
53 return;
54 }
55 let n = needle_chars.len();
56 let mut i = 0;
57 while i + n <= hay.len() {
58 if (0..n).all(|j| chars_eq_ignore_case(hay[i + j].1, needle_chars[j])) {
59 let start = hay[i].0;
60 let end = hay.get(i + n).map(|(b, _)| *b).unwrap_or(hay_len);
61 out.push((start, end));
62 i += n; } else {
64 i += 1;
65 }
66 }
67}
68
69fn chars_eq_ignore_case(a: char, b: char) -> bool {
72 a == b || a.to_lowercase().eq(b.to_lowercase())
73}
74
75pub fn style_ranges<'a, T>(
83 line: &'a str,
84 ranges: &[(usize, usize)],
85 mut mk: impl FnMut(&'a str, bool) -> T,
86) -> Vec<T> {
87 let mut out = Vec::new();
88 let mut pos = 0;
89 for &(start, end) in ranges {
90 if start > pos {
91 out.push(mk(&line[pos..start], false));
92 }
93 out.push(mk(&line[start..end], true));
94 pos = end;
95 }
96 if pos < line.len() {
97 out.push(mk(&line[pos..], false));
98 }
99 out
100}
101
102pub fn wrap_line(line: &str, max_width: usize) -> Vec<String> {
106 if max_width == 0 || line.chars().count() <= max_width {
107 return vec![line.to_string()];
108 }
109
110 let mut result = Vec::new();
111 let mut remaining = line;
112
113 while remaining.chars().count() > max_width {
114 let byte_limit = remaining
116 .char_indices()
117 .nth(max_width)
118 .map(|(i, _)| i)
119 .unwrap_or(remaining.len());
120
121 let break_at = remaining[..byte_limit]
123 .rfind(' ')
124 .map(|i| i + 1) .unwrap_or(byte_limit);
126 result.push(remaining[..break_at].trim_end().to_string());
127 remaining = &remaining[break_at..];
128 }
129 if !remaining.is_empty() {
130 result.push(remaining.to_string());
131 }
132 result
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn needles(v: &[&str]) -> Vec<String> {
140 v.iter().map(|s| s.to_string()).collect()
141 }
142
143 #[test]
144 fn matches_are_case_insensitive() {
145 assert_eq!(match_ranges("Hello World", &needles(&["world"])), [(6, 11)]);
146 assert_eq!(match_ranges("HELLO", &needles(&["hell"])), [(0, 4)]);
147 }
148
149 #[test]
150 fn all_occurrences_per_needle() {
151 assert_eq!(match_ranges("aa aa", &needles(&["aa"])), [(0, 2), (3, 5)]);
153 }
154
155 #[test]
156 fn overlapping_needles_keep_longest() {
157 let r = match_ranges("foobar", &needles(&["foo", "foobar"]));
160 assert_eq!(r, [(0, 6)]);
161 }
162
163 #[test]
164 fn empty_needles_contribute_nothing() {
165 assert!(match_ranges("anything", &needles(&[""])).is_empty());
166 assert!(match_ranges("anything", &[]).is_empty());
167 }
168
169 #[test]
170 fn ascii_needle_matches_on_line_containing_a_length_changing_fold() {
171 let hay = "Hİ there";
176 let r = match_ranges(hay, &needles(&["there"]));
177 assert_eq!(r.len(), 1, "ascii needle must still match: {r:?}");
178 let (s, e) = r[0];
179 assert!(hay.is_char_boundary(s) && hay.is_char_boundary(e));
180 assert_eq!(&hay[s..e], "there");
181 }
182
183 #[test]
184 fn multibyte_haystack_offsets_are_valid() {
185 let hay = "日本語テスト";
186 let r = match_ranges(hay, &needles(&["テスト"]));
187 assert_eq!(r.len(), 1);
188 let (s, e) = r[0];
189 assert_eq!(&hay[s..e], "テスト");
190 }
191
192 #[test]
193 fn style_ranges_alternates_gaps_and_matches() {
194 let line = "see widget and gadget";
195 let ranges = match_ranges(line, &needles(&["widget", "gadget"]));
196 let segs: Vec<(String, bool)> = style_ranges(line, &ranges, |s, hit| (s.to_string(), hit));
197 assert_eq!(
199 segs,
200 vec![
201 ("see ".to_string(), false),
202 ("widget".to_string(), true),
203 (" and ".to_string(), false),
204 ("gadget".to_string(), true),
205 ]
206 );
207 }
208
209 #[test]
210 fn style_ranges_empty_is_one_non_match_segment() {
211 let segs: Vec<(String, bool)> =
212 style_ranges("no matches here", &[], |s, hit| (s.to_string(), hit));
213 assert_eq!(segs, vec![("no matches here".to_string(), false)]);
214 }
215
216 #[test]
217 fn wrap_line_fits_within_width() {
218 assert_eq!(wrap_line("short", 20), vec!["short"]);
219 }
220
221 #[test]
222 fn wrap_line_breaks_at_word_boundary() {
223 assert_eq!(
224 wrap_line("hello world foo bar", 12),
225 vec!["hello world", "foo bar"]
226 );
227 }
228
229 #[test]
230 fn wrap_line_hard_breaks_long_word() {
231 assert_eq!(wrap_line("abcdefghij", 5), vec!["abcde", "fghij"]);
232 }
233
234 #[test]
235 fn wrap_line_handles_multibyte_chars() {
236 assert_eq!(wrap_line("日本語テスト", 3), vec!["日本語", "テスト"]);
237 }
238
239 #[test]
240 fn wrap_line_empty_string() {
241 assert_eq!(wrap_line("", 10), vec![""]);
242 }
243}