1use std::ops::Range;
2
3use crate::{EditorBuffer, EditorError};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct SearchQuery {
9 pub pattern: String,
11 pub case_sensitive: bool,
13 pub whole_word: bool,
15 pub is_regex: bool,
17}
18
19impl SearchQuery {
20 pub fn literal(pattern: &str) -> Self {
22 Self {
23 pattern: pattern.to_string(),
24 case_sensitive: true,
25 whole_word: false,
26 is_regex: false,
27 }
28 }
29
30 pub fn new(pattern: &str, case_sensitive: bool, whole_word: bool, is_regex: bool) -> Self {
32 Self {
33 pattern: pattern.to_string(),
34 case_sensitive,
35 whole_word,
36 is_regex,
37 }
38 }
39}
40
41impl Default for SearchQuery {
42 fn default() -> Self {
43 Self::literal("")
44 }
45}
46
47pub fn compile_query(query: &SearchQuery) -> Result<regex::Regex, EditorError> {
52 if query.pattern.is_empty() {
53 return Err(EditorError::EmptySearchPattern);
54 }
55 let mut source = if query.is_regex {
56 query.pattern.clone()
57 } else {
58 regex::escape(&query.pattern)
59 };
60 if query.whole_word {
61 source = format!(r"\b(?:{source})\b");
62 }
63 if !query.case_sensitive {
64 source = format!(r"(?i){source}");
65 }
66 regex::Regex::new(&source).map_err(|e| EditorError::InvalidRegex {
67 pattern: query.pattern.clone(),
68 message: e.to_string(),
69 })
70}
71
72pub fn find_matches(
74 buffer: &EditorBuffer,
75 query: &SearchQuery,
76) -> Result<Vec<Range<usize>>, EditorError> {
77 let re = compile_query(query)?;
78 let text = buffer.text().to_string();
79 Ok(re
80 .find_iter(&text)
81 .map(|m| m.start()..m.end())
82 .filter(|r| buffer.is_char_boundary(r.start) && buffer.is_char_boundary(r.end))
83 .collect())
84}
85
86pub fn find_next(
89 buffer: &EditorBuffer,
90 query: &SearchQuery,
91 from_offset: usize,
92 wrap: bool,
93) -> Result<Option<Range<usize>>, EditorError> {
94 let matches = find_matches(buffer, query)?;
95 if matches.is_empty() {
96 return Ok(None);
97 }
98 let from = from_offset.min(buffer.len_bytes());
99 if let Some(m) = matches.iter().find(|m| m.start >= from) {
100 return Ok(Some(m.clone()));
101 }
102 if wrap {
103 Ok(matches.into_iter().next())
104 } else {
105 Ok(None)
106 }
107}
108
109pub fn find_prev(
112 buffer: &EditorBuffer,
113 query: &SearchQuery,
114 from_offset: usize,
115 wrap: bool,
116) -> Result<Option<Range<usize>>, EditorError> {
117 let matches = find_matches(buffer, query)?;
118 if matches.is_empty() {
119 return Ok(None);
120 }
121 let from = from_offset.min(buffer.len_bytes());
122 if let Some(m) = matches.iter().rev().find(|m| m.start < from) {
125 return Ok(Some(m.clone()));
126 }
127 if wrap {
128 Ok(matches.into_iter().next_back())
129 } else {
130 Ok(None)
131 }
132}
133
134#[derive(Debug, Clone, Default)]
139pub struct SearchState {
140 query: Option<SearchQuery>,
141 matches: Vec<Range<usize>>,
142 current: Option<usize>,
143 version: usize,
144}
145
146impl SearchState {
147 pub fn new() -> Self {
149 Self::default()
150 }
151
152 pub fn set_query(&mut self, query: SearchQuery) {
154 if self.query.as_ref() != Some(&query) {
155 self.query = Some(query);
156 self.matches.clear();
157 self.current = None;
158 self.version = usize::MAX;
161 }
162 }
163
164 pub fn query(&self) -> Option<&SearchQuery> {
166 self.query.as_ref()
167 }
168
169 pub fn refresh(&mut self, buffer: &EditorBuffer) -> Result<(), EditorError> {
171 if self.version == buffer.version() {
172 return Ok(());
173 }
174 let Some(query) = self.query.clone() else {
175 self.matches.clear();
176 self.current = None;
177 return Ok(());
178 };
179 let matches = find_matches(buffer, &query)?;
180 self.version = buffer.version();
181 if self.current.is_some_and(|i| i >= matches.len()) {
182 self.current = None;
183 }
184 self.matches = matches;
185 Ok(())
186 }
187
188 pub fn matches(&self) -> &[Range<usize>] {
190 &self.matches
191 }
192
193 pub fn match_count(&self) -> usize {
195 self.matches.len()
196 }
197
198 pub fn current_index(&self) -> Option<usize> {
200 self.current
201 }
202
203 pub fn current_match(&self) -> Option<Range<usize>> {
205 self.current.and_then(|i| self.matches.get(i).cloned())
206 }
207
208 pub fn next(
210 &mut self,
211 buffer: &EditorBuffer,
212 from_offset: usize,
213 wrap: bool,
214 ) -> Result<Option<Range<usize>>, EditorError> {
215 self.refresh(buffer)?;
216 if self.matches.is_empty() {
217 self.current = None;
218 return Ok(None);
219 }
220 let from = from_offset.min(buffer.len_bytes());
221 let idx = self
222 .matches
223 .iter()
224 .position(|m| m.start >= from)
225 .or_else(|| wrap.then_some(0));
226 match idx {
227 Some(i) => {
228 self.current = Some(i);
229 Ok(Some(self.matches[i].clone()))
230 }
231 None => Ok(None),
232 }
233 }
234
235 pub fn prev(
237 &mut self,
238 buffer: &EditorBuffer,
239 from_offset: usize,
240 wrap: bool,
241 ) -> Result<Option<Range<usize>>, EditorError> {
242 self.refresh(buffer)?;
243 if self.matches.is_empty() {
244 self.current = None;
245 return Ok(None);
246 }
247 let from = from_offset.min(buffer.len_bytes());
248 let idx = self
249 .matches
250 .iter()
251 .rposition(|m| m.start < from)
252 .or_else(|| wrap.then(|| self.matches.len() - 1));
253 match idx {
254 Some(i) => {
255 self.current = Some(i);
256 Ok(Some(self.matches[i].clone()))
257 }
258 None => Ok(None),
259 }
260 }
261}
262
263pub fn collect_replacements(
272 text: &str,
273 query: &SearchQuery,
274 replacement: &str,
275) -> Result<Vec<(Range<usize>, String)>, EditorError> {
276 let re = compile_query(query)?;
277 let mut replacements = Vec::new();
278 if query.is_regex {
279 for caps in re.captures_iter(text) {
281 let m = caps.get(0).expect("captures_iter always yields group 0");
282 let range = m.start()..m.end();
283 if range.is_empty() {
284 continue;
285 }
286 if !(text.is_char_boundary(range.start) && text.is_char_boundary(range.end)) {
287 continue;
288 }
289 let mut expanded = String::new();
290 caps.expand(replacement, &mut expanded);
291 replacements.push((range, expanded));
292 }
293 } else {
294 for m in re.find_iter(text) {
295 let range = m.start()..m.end();
296 if range.is_empty() {
297 continue;
298 }
299 if !(text.is_char_boundary(range.start) && text.is_char_boundary(range.end)) {
300 continue;
301 }
302 replacements.push((range, replacement.to_string()));
303 }
304 }
305 Ok(replacements)
306}
307
308pub fn replace_all_query(
313 buffer: &mut EditorBuffer,
314 query: &SearchQuery,
315 replacement: &str,
316) -> Result<usize, EditorError> {
317 let text = buffer.text().to_string();
318 let replacements = collect_replacements(&text, query, replacement)?;
319 Ok(buffer.replace_many(replacements))
320}
321
322pub fn replace_one_query(
328 buffer: &mut EditorBuffer,
329 query: &SearchQuery,
330 range: Range<usize>,
331 replacement: &str,
332) -> Result<bool, EditorError> {
333 if range.is_empty() || range.end > buffer.len_bytes() {
334 return Ok(false);
335 }
336 if !buffer.is_char_boundary(range.start) || !buffer.is_char_boundary(range.end) {
337 return Ok(false);
338 }
339 let text = buffer.text().to_string();
340 let expanded = collect_replacements(&text, query, replacement)?
341 .into_iter()
342 .find(|(r, _)| *r == range)
343 .map(|(_, expanded)| expanded);
344 match expanded {
345 Some(expanded) => {
346 buffer.replace_range(range, &expanded);
347 Ok(true)
348 }
349 None => Ok(false),
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn literal_find_returns_ascending_byte_ranges() {
359 let buffer = EditorBuffer::new("hello world hello");
360 let matches = find_matches(&buffer, &SearchQuery::literal("hello")).unwrap();
361 assert_eq!(matches, vec![0..5, 12..17]);
362 }
363
364 #[test]
365 fn literal_is_case_sensitive_by_default() {
366 let buffer = EditorBuffer::new("Hello hello");
367 let matches = find_matches(&buffer, &SearchQuery::literal("hello")).unwrap();
368 assert_eq!(matches, vec![6..11]);
369 }
370
371 #[test]
372 fn case_insensitive_matches_all_cases() {
373 let buffer = EditorBuffer::new("Hello HELLO hello");
374 let query = SearchQuery::new("hello", false, false, false);
375 let matches = find_matches(&buffer, &query).unwrap();
376 assert_eq!(matches, vec![0..5, 6..11, 12..17]);
377 }
378
379 #[test]
380 fn regex_mode_matches_pattern_class() {
381 let buffer = EditorBuffer::new("hello hallo hxllo");
382 let query = SearchQuery::new("h.llo", true, false, true);
383 let matches = find_matches(&buffer, &query).unwrap();
384 assert_eq!(matches, vec![0..5, 6..11, 12..17]);
385 }
386
387 #[test]
388 fn literal_mode_does_not_treat_dot_as_wildcard() {
389 let buffer = EditorBuffer::new("h.llo hello");
390 let matches = find_matches(&buffer, &SearchQuery::literal("h.llo")).unwrap();
391 assert_eq!(matches, vec![0..5]);
392 }
393
394 #[test]
395 fn whole_word_skips_substring_matches() {
396 let buffer = EditorBuffer::new("foo foobar foo");
397 let query = SearchQuery::new("foo", true, true, false);
398 let matches = find_matches(&buffer, &query).unwrap();
399 assert_eq!(matches, vec![0..3, 11..14]);
400 }
401
402 #[test]
403 fn empty_pattern_is_an_error() {
404 let buffer = EditorBuffer::new("hello");
405 let err = find_matches(&buffer, &SearchQuery::literal("")).unwrap_err();
406 assert!(matches!(err, EditorError::EmptySearchPattern));
407 }
408
409 #[test]
410 fn invalid_regex_is_an_error() {
411 let buffer = EditorBuffer::new("hello");
412 let query = SearchQuery::new("(", true, false, true);
413 let err = find_matches(&buffer, &query).unwrap_err();
414 assert!(matches!(err, EditorError::InvalidRegex { .. }));
415 }
416
417 #[test]
418 fn no_match_returns_empty_vec() {
419 let buffer = EditorBuffer::new("hello world");
420 let matches = find_matches(&buffer, &SearchQuery::literal("xyz")).unwrap();
421 assert!(matches.is_empty());
422 }
423
424 #[test]
425 fn unicode_matches_land_on_char_boundaries() {
426 let buffer = EditorBuffer::new("héllo héllo");
427 let matches = find_matches(&buffer, &SearchQuery::literal("héllo")).unwrap();
429 assert_eq!(matches, vec![0..6, 7..13]);
430 for m in &matches {
431 assert!(buffer.is_char_boundary(m.start));
432 assert!(buffer.is_char_boundary(m.end));
433 }
434 }
435
436 #[test]
437 fn find_next_advances_and_wraps() {
438 let buffer = EditorBuffer::new("aa aa aa");
439 let query = SearchQuery::literal("aa");
440 assert_eq!(find_next(&buffer, &query, 0, true).unwrap(), Some(0..2));
441 assert_eq!(find_next(&buffer, &query, 1, true).unwrap(), Some(3..5));
442 assert_eq!(find_next(&buffer, &query, 7, true).unwrap(), Some(0..2));
443 assert_eq!(find_next(&buffer, &query, 7, false).unwrap(), None);
444 }
445
446 #[test]
447 fn find_prev_retreats_and_wraps() {
448 let buffer = EditorBuffer::new("aa aa aa");
449 let query = SearchQuery::literal("aa");
450 assert_eq!(find_prev(&buffer, &query, 8, true).unwrap(), Some(6..8));
451 assert_eq!(find_prev(&buffer, &query, 6, true).unwrap(), Some(3..5));
452 assert_eq!(find_prev(&buffer, &query, 0, true).unwrap(), Some(6..8));
453 assert_eq!(find_prev(&buffer, &query, 0, false).unwrap(), None);
454 }
455
456 #[test]
457 fn search_state_caches_and_rescans_on_edit() {
458 let mut buffer = EditorBuffer::new("foo foo");
459 let mut state = SearchState::new();
460 state.set_query(SearchQuery::literal("foo"));
461 state.refresh(&buffer).unwrap();
462 assert_eq!(state.match_count(), 2);
463
464 buffer.set_cursor_offset(buffer.len_bytes());
465 buffer.insert(" foo");
466 state.refresh(&buffer).unwrap();
467 assert_eq!(state.match_count(), 3);
468 assert_eq!(state.matches(), &[0..3, 4..7, 8..11]);
469 }
470
471 #[test]
472 fn search_state_navigation_tracks_current() {
473 let buffer = EditorBuffer::new("aa aa aa");
474 let mut state = SearchState::new();
475 state.set_query(SearchQuery::literal("aa"));
476 assert_eq!(state.next(&buffer, 0, true).unwrap(), Some(0..2));
477 assert_eq!(state.current_index(), Some(0));
478 assert_eq!(state.next(&buffer, 1, true).unwrap(), Some(3..5));
479 assert_eq!(state.current_index(), Some(1));
480 assert_eq!(state.prev(&buffer, 4, true).unwrap(), Some(3..5));
481 assert_eq!(state.current_match(), Some(3..5));
482 }
483
484 #[test]
485 fn replace_many_is_a_single_undo_step() {
486 let mut buffer = EditorBuffer::new("a a a");
487 let n = buffer.replace_many(vec![
488 (0..1, "b".to_string()),
489 (2..3, "b".to_string()),
490 (4..5, "b".to_string()),
491 ]);
492 assert_eq!(n, 3);
493 assert_eq!(buffer.text().to_string(), "b b b");
494
495 buffer.undo();
496 assert_eq!(buffer.text().to_string(), "a a a");
497
498 buffer.redo();
499 assert_eq!(buffer.text().to_string(), "b b b");
500 }
501
502 #[test]
503 fn replace_many_with_no_matches_changes_nothing() {
504 let mut buffer = EditorBuffer::new("hello");
505 let version = buffer.version();
506 let n = buffer.replace_many(vec![]);
507 assert_eq!(n, 0);
508 assert_eq!(buffer.version(), version);
509 assert_eq!(buffer.text().to_string(), "hello");
510 }
511
512 #[test]
513 fn replace_many_handles_growing_replacements() {
514 let mut buffer = EditorBuffer::new("ab ab");
515 let n = buffer.replace_many(vec![(0..2, "abcd".to_string()), (3..5, "abcd".to_string())]);
516 assert_eq!(n, 2);
517 assert_eq!(buffer.text().to_string(), "abcd abcd");
518 buffer.undo();
519 assert_eq!(buffer.text().to_string(), "ab ab");
520 buffer.redo();
521 assert_eq!(buffer.text().to_string(), "abcd abcd");
522 }
523
524 #[test]
525 fn replace_all_query_literal_replaces_everything_at_once() {
526 let mut buffer = EditorBuffer::new("foo bar foo");
527 let n = replace_all_query(&mut buffer, &SearchQuery::literal("foo"), "baz").unwrap();
528 assert_eq!(n, 2);
529 assert_eq!(buffer.text().to_string(), "baz bar baz");
530 buffer.undo();
531 assert_eq!(buffer.text().to_string(), "foo bar foo");
532 }
533
534 #[test]
535 fn replace_all_query_expands_regex_captures() {
536 let mut buffer = EditorBuffer::new("2024-01-02");
537 let query = SearchQuery::new(r"(\d+)-(\d+)-(\d+)", true, false, true);
538 let n = replace_all_query(&mut buffer, &query, "$3/$2/$1").unwrap();
539 assert_eq!(n, 1);
540 assert_eq!(buffer.text().to_string(), "02/01/2024");
541 buffer.undo();
542 assert_eq!(buffer.text().to_string(), "2024-01-02");
543 }
544
545 #[test]
546 fn replace_all_query_with_no_matches_is_a_noop() {
547 let mut buffer = EditorBuffer::new("hello");
548 let version = buffer.version();
549 let n = replace_all_query(&mut buffer, &SearchQuery::literal("xyz"), "baz").unwrap();
550 assert_eq!(n, 0);
551 assert_eq!(buffer.version(), version);
552 }
553
554 #[test]
555 fn replace_all_query_rejects_empty_and_invalid_patterns() {
556 let mut buffer = EditorBuffer::new("hello");
557 let err = replace_all_query(&mut buffer, &SearchQuery::literal(""), "x").unwrap_err();
558 assert!(matches!(err, EditorError::EmptySearchPattern));
559
560 let bad = SearchQuery::new("(", true, false, true);
561 let err = replace_all_query(&mut buffer, &bad, "x").unwrap_err();
562 assert!(matches!(err, EditorError::InvalidRegex { .. }));
563 }
564
565 #[test]
566 fn replace_one_query_replaces_exact_match_only() {
567 let mut buffer = EditorBuffer::new("foo bar foo");
568 assert!(replace_one_query(&mut buffer, &SearchQuery::literal("foo"), 0..3, "baz").unwrap());
569 assert_eq!(buffer.text().to_string(), "baz bar foo");
570 buffer.undo();
571 assert_eq!(buffer.text().to_string(), "foo bar foo");
572
573 assert!(
575 !replace_one_query(&mut buffer, &SearchQuery::literal("foo"), 1..4, "baz").unwrap()
576 );
577 assert_eq!(buffer.text().to_string(), "foo bar foo");
578
579 assert!(
581 !replace_one_query(&mut buffer, &SearchQuery::literal("foo"), 0..0, "baz").unwrap()
582 );
583 assert!(
584 !replace_one_query(&mut buffer, &SearchQuery::literal("foo"), 0..99, "baz").unwrap()
585 );
586 }
587
588 #[test]
589 fn replace_one_query_expands_regex_captures() {
590 let mut buffer = EditorBuffer::new("ab cd");
591 let query = SearchQuery::new(r"(\w)(\w)", true, false, true);
592 assert!(replace_one_query(&mut buffer, &query, 0..2, "$2$1").unwrap());
593 assert_eq!(buffer.text().to_string(), "ba cd");
594 }
595
596 #[test]
597 fn collect_replacements_supports_line_scoped_offsets() {
598 let mut buffer = EditorBuffer::new("foo one\nfoo two\n");
599 let query = SearchQuery::literal("foo");
600 let line_start = buffer.point_to_offset(crate::Point::new(1, 0));
602 let line = buffer.line_to_string(1);
603 let local = collect_replacements(&line, &query, "bar").unwrap();
604 assert_eq!(local.len(), 1);
605 let scoped: Vec<(Range<usize>, String)> = local
606 .into_iter()
607 .map(|(r, s)| (r.start + line_start..r.end + line_start, s))
608 .collect();
609 assert_eq!(buffer.replace_many(scoped), 1);
610 assert_eq!(buffer.text().to_string(), "foo one\nbar two\n");
611 buffer.undo();
612 assert_eq!(buffer.text().to_string(), "foo one\nfoo two\n");
613 }
614}