1use ropey::Rope;
9
10pub type Match = (usize, usize);
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum CaseMode {
16 #[default]
19 Smart,
20 Sensitive,
21 Insensitive,
22}
23
24impl CaseMode {
25 fn sensitive_for(self, needle: &str) -> bool {
26 match self {
27 CaseMode::Sensitive => true,
28 CaseMode::Insensitive => false,
29 CaseMode::Smart => needle.chars().any(char::is_uppercase),
30 }
31 }
32}
33
34pub fn find_all(text: &Rope, needle: &str, mode: CaseMode) -> Vec<Match> {
39 if needle.is_empty() {
40 return Vec::new();
41 }
42 let sensitive = mode.sensitive_for(needle);
43
44 let haystack = text.to_string();
48 let (haystack, needle) = if sensitive {
49 (haystack, needle.to_string())
50 } else {
51 (haystack.to_lowercase(), needle.to_lowercase())
52 };
53
54 let mut matches = Vec::new();
57 let needle_chars = needle.chars().count();
58 for (byte, _) in haystack.match_indices(&needle) {
59 let start = haystack[..byte].chars().count();
60 matches.push((start, start + needle_chars));
61 }
62 matches
63}
64
65pub fn next_from(matches: &[Match], from: usize) -> Option<usize> {
67 if matches.is_empty() {
68 return None;
69 }
70 Some(matches.iter().position(|(start, _)| *start >= from).unwrap_or(0))
71}
72
73pub fn prev_from(matches: &[Match], from: usize) -> Option<usize> {
79 if matches.is_empty() {
80 return None;
81 }
82 Some(matches.iter().rposition(|(start, _)| *start < from).unwrap_or(matches.len() - 1))
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 fn rope(s: &str) -> Rope {
90 Rope::from_str(s)
91 }
92
93 #[test]
94 fn every_occurrence_is_found_in_order() {
95 let text = rope("one two one three one");
96 let found = find_all(&text, "one", CaseMode::Sensitive);
97 assert_eq!(found, [(0, 3), (8, 11), (18, 21)]);
98 }
99
100 #[test]
101 fn nothing_matches_an_empty_query() {
102 assert!(find_all(&rope("anything"), "", CaseMode::Smart).is_empty());
104 }
105
106 #[test]
107 fn a_missing_needle_finds_nothing() {
108 assert!(find_all(&rope("abc"), "zzz", CaseMode::Smart).is_empty());
109 }
110
111 #[test]
112 fn smart_case_is_insensitive_until_you_type_a_capital() {
113 let text = rope("Error error ERROR");
114 assert_eq!(find_all(&text, "error", CaseMode::Smart).len(), 3, "all three");
115 assert_eq!(find_all(&text, "Error", CaseMode::Smart), [(0, 5)], "the capital narrows it");
116 }
117
118 #[test]
119 fn explicit_modes_override_the_smart_default() {
120 let text = rope("Error error");
121 assert_eq!(find_all(&text, "Error", CaseMode::Insensitive).len(), 2);
122 assert_eq!(find_all(&text, "error", CaseMode::Sensitive).len(), 1);
123 }
124
125 #[test]
126 fn matches_are_char_offsets_not_byte_offsets() {
127 let text = rope("héllo world héllo");
129 assert_eq!(find_all(&text, "world", CaseMode::Sensitive), [(6, 11)]);
130 }
131
132 #[test]
133 fn matches_span_lines_correctly() {
134 let text = rope("first\nsecond\nfirst\n");
135 assert_eq!(find_all(&text, "first", CaseMode::Sensitive), [(0, 5), (13, 18)]);
136 }
137
138 #[test]
139 fn overlapping_candidates_are_reported_without_overlap() {
140 assert_eq!(find_all(&rope("aaaa"), "aa", CaseMode::Sensitive), [(0, 2), (2, 4)]);
142 }
143
144 #[test]
147 fn next_finds_the_match_at_or_after_the_cursor() {
148 let matches = [(0, 3), (8, 11), (18, 21)];
149 assert_eq!(next_from(&matches, 0), Some(0));
150 assert_eq!(next_from(&matches, 1), Some(1));
151 assert_eq!(next_from(&matches, 8), Some(1), "a cursor sitting on one stays on it");
152 assert_eq!(next_from(&matches, 12), Some(2));
153 }
154
155 #[test]
156 fn next_wraps_past_the_last_match() {
157 let matches = [(0, 3), (8, 11)];
158 assert_eq!(next_from(&matches, 99), Some(0), "back to the top");
159 }
160
161 #[test]
162 fn prev_finds_the_last_match_starting_before_the_offset_and_wraps() {
163 let matches = [(0, 3), (8, 11), (18, 21)];
164 assert_eq!(prev_from(&matches, 18), Some(1));
165 assert_eq!(prev_from(&matches, 8), Some(0));
166 assert_eq!(prev_from(&matches, 0), Some(2), "wraps to the end");
167 }
168
169 #[test]
172 fn stepping_backwards_repeatedly_walks_the_matches() {
173 let matches = [(0, 3), (8, 11), (18, 21)];
174 let mut at = 2; at = prev_from(&matches, matches[at].0).unwrap();
177 assert_eq!(at, 1);
178 at = prev_from(&matches, matches[at].0).unwrap();
179 assert_eq!(at, 0);
180 at = prev_from(&matches, matches[at].0).unwrap();
181 assert_eq!(at, 2, "and wraps");
182 }
183
184 #[test]
185 fn navigating_an_empty_result_set_goes_nowhere() {
186 assert_eq!(next_from(&[], 0), None);
187 assert_eq!(prev_from(&[], 0), None);
188 }
189}