1use regex::Regex;
8
9use crate::{Buffer, Position};
10
11#[derive(Debug, Default, Clone)]
15pub(crate) struct SearchState {
16 pub(crate) pattern: Option<Regex>,
17 pub(crate) matches: Vec<Vec<(usize, usize)>>,
21 pub(crate) generations: Vec<u64>,
22}
23
24impl SearchState {
25 pub(crate) fn new() -> Self {
26 Self::default()
27 }
28
29 pub(crate) fn set_pattern(&mut self, re: Option<Regex>) {
30 self.pattern = re;
31 self.matches.clear();
32 self.generations.clear();
33 }
34
35 pub(crate) fn matches_for(
38 &mut self,
39 row: usize,
40 line: &str,
41 dirty_gen: u64,
42 ) -> &[(usize, usize)] {
43 let Some(ref re) = self.pattern else {
44 return &[];
45 };
46 if self.matches.len() <= row {
47 self.matches.resize_with(row + 1, Vec::new);
48 self.generations.resize(row + 1, u64::MAX);
49 }
50 if self.generations[row] != dirty_gen {
51 self.matches[row] = re.find_iter(line).map(|m| (m.start(), m.end())).collect();
52 self.generations[row] = dirty_gen;
53 }
54 &self.matches[row]
55 }
56}
57
58impl Buffer {
59 pub fn set_search_pattern(&mut self, re: Option<Regex>) {
64 self.search_state_mut().set_pattern(re);
65 }
66
67 pub fn search_pattern(&self) -> Option<&Regex> {
68 self.search_state().pattern.as_ref()
69 }
70
71 pub fn search_forward(&mut self, skip_current: bool) -> bool {
75 if self.search_pattern().is_none() {
76 return false;
77 }
78 let cursor = self.cursor();
79 let start_byte = self
80 .line(cursor.row)
81 .map(|l| cursor.byte_offset(l))
82 .unwrap_or(0);
83 if let Some(pos) =
85 self.find_match_in_row(cursor.row, start_byte, skip_current, true)
86 {
87 self.set_cursor(pos);
88 self.ensure_cursor_visible();
89 return true;
90 }
91 let total = self.row_count();
93 for offset in 1..=total {
94 let row = (cursor.row + offset) % total;
95 if let Some(pos) = self.find_match_in_row(row, 0, false, true) {
96 self.set_cursor(pos);
97 self.ensure_cursor_visible();
98 return true;
99 }
100 if row == cursor.row {
101 break;
102 }
103 }
104 false
105 }
106
107 pub fn search_backward(&mut self, skip_current: bool) -> bool {
111 if self.search_pattern().is_none() {
112 return false;
113 }
114 let cursor = self.cursor();
115 let cursor_byte = self
116 .line(cursor.row)
117 .map(|l| cursor.byte_offset(l))
118 .unwrap_or(0);
119 if let Some(pos) = self.find_match_in_row(
120 cursor.row,
121 cursor_byte,
122 skip_current,
123 false,
124 ) {
125 self.set_cursor(pos);
126 self.ensure_cursor_visible();
127 return true;
128 }
129 let total = self.row_count();
130 for offset in 1..=total {
131 let row = (cursor.row + total - offset) % total;
133 if let Some(pos) =
134 self.find_match_in_row(row, usize::MAX, false, false)
135 {
136 self.set_cursor(pos);
137 self.ensure_cursor_visible();
138 return true;
139 }
140 if row == cursor.row {
141 break;
142 }
143 }
144 false
145 }
146
147 pub fn search_matches(&mut self, row: usize) -> Vec<(usize, usize)> {
150 let line = self.line(row).unwrap_or("").to_string();
151 let dgen = self.dirty_gen();
152 self.search_state_mut()
153 .matches_for(row, &line, dgen)
154 .to_vec()
155 }
156
157 fn find_match_in_row(
158 &mut self,
159 row: usize,
160 anchor_byte: usize,
161 skip_current: bool,
162 forward: bool,
163 ) -> Option<Position> {
164 let line = self.line(row)?.to_string();
165 let dgen = self.dirty_gen();
166 let matches = self.search_state_mut().matches_for(row, &line, dgen);
167 if matches.is_empty() {
168 return None;
169 }
170 let m = if forward {
171 matches
172 .iter()
173 .find(|(s, _)| {
174 if skip_current {
175 *s > anchor_byte
176 } else {
177 *s >= anchor_byte
178 }
179 })
180 .copied()
181 } else {
182 matches
183 .iter()
184 .rev()
185 .find(|(s, _)| {
186 if skip_current {
187 *s < anchor_byte
188 } else {
189 *s <= anchor_byte
190 }
191 })
192 .copied()
193 }?;
194 let col = line[..m.0].chars().count();
196 Some(Position::new(row, col))
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 fn re(pat: &str) -> Regex {
205 Regex::new(pat).unwrap()
206 }
207
208 #[test]
209 fn no_pattern_returns_false() {
210 let mut b = Buffer::from_str("anything");
211 assert!(!b.search_forward(false));
212 assert!(!b.search_backward(false));
213 }
214
215 #[test]
216 fn forward_finds_first_match_on_cursor_row() {
217 let mut b = Buffer::from_str("foo bar foo baz");
218 b.set_search_pattern(Some(re("foo")));
219 assert!(b.search_forward(false));
220 assert_eq!(b.cursor(), Position::new(0, 0));
221 }
222
223 #[test]
224 fn forward_skip_current_walks_past() {
225 let mut b = Buffer::from_str("foo bar foo baz");
226 b.set_search_pattern(Some(re("foo")));
227 b.search_forward(false);
228 b.search_forward(true);
229 assert_eq!(b.cursor(), Position::new(0, 8));
230 }
231
232 #[test]
233 fn forward_wraps_to_top() {
234 let mut b = Buffer::from_str("zzz\nfoo");
235 b.set_cursor(Position::new(1, 2));
236 b.set_search_pattern(Some(re("zzz")));
237 assert!(b.search_forward(true));
238 assert_eq!(b.cursor(), Position::new(0, 0));
239 }
240
241 #[test]
242 fn backward_picks_rightmost_match_on_row() {
243 let mut b = Buffer::from_str("foo bar foo baz");
244 b.set_cursor(Position::new(0, 14));
245 b.set_search_pattern(Some(re("foo")));
246 assert!(b.search_backward(true));
247 assert_eq!(b.cursor(), Position::new(0, 8));
248 b.search_backward(true);
249 assert_eq!(b.cursor(), Position::new(0, 0));
250 }
251
252 #[test]
253 fn backward_wraps_to_bottom() {
254 let mut b = Buffer::from_str("foo\nzzz");
255 b.set_cursor(Position::new(0, 0));
256 b.set_search_pattern(Some(re("zzz")));
257 assert!(b.search_backward(true));
258 assert_eq!(b.cursor(), Position::new(1, 0));
259 }
260
261 #[test]
262 fn no_match_returns_false_and_keeps_cursor() {
263 let mut b = Buffer::from_str("alpha beta gamma");
264 b.set_cursor(Position::new(0, 5));
265 b.set_search_pattern(Some(re("zzz")));
266 let before = b.cursor();
267 assert!(!b.search_forward(false));
268 assert_eq!(b.cursor(), before);
269 }
270
271 #[test]
272 fn cache_invalidates_after_edit() {
273 use crate::Edit;
274 let mut b = Buffer::from_str("foo bar");
275 b.set_search_pattern(Some(re("bar")));
276 let initial = b.search_matches(0);
277 assert_eq!(initial, vec![(4, 7)]);
278 b.apply_edit(Edit::InsertStr {
279 at: Position::new(0, 0),
280 text: "XX ".into(),
281 });
282 let after = b.search_matches(0);
283 assert_eq!(after, vec![(7, 10)]);
284 }
285
286 #[test]
287 fn unicode_match_columns_are_charwise() {
288 let mut b = Buffer::from_str("tablé foo");
289 b.set_search_pattern(Some(re("foo")));
290 assert!(b.search_forward(false));
291 assert_eq!(b.cursor(), Position::new(0, 6));
293 }
294}