1use regex::{Regex, RegexBuilder};
2
3use crate::renderer::NodeId;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct SearchMatch {
7 pub node_id: NodeId,
8 pub occurrence: usize,
9}
10
11#[derive(Debug, Default)]
12pub struct SearchState {
13 pub active: bool,
14 pub query: String,
15 pub matches: Vec<SearchMatch>,
16 pub current: Option<usize>,
17}
18
19impl SearchState {
20 pub fn open(&mut self) {
21 self.active = true;
22 }
23
24 pub fn cancel_input(&mut self) {
25 self.active = false;
26 }
27
28 pub fn accept_input(&mut self) {
29 self.active = false;
30 }
31
32 pub fn clear_query(&mut self) {
33 self.query.clear();
34 self.matches.clear();
35 self.current = None;
36 }
37
38 pub fn set_matches(&mut self, matches: Vec<SearchMatch>) {
39 self.matches = matches;
40 self.current = if self.matches.is_empty() {
41 None
42 } else {
43 Some(0)
44 };
45 }
46
47 pub fn next(&mut self) {
48 if self.matches.is_empty() {
49 self.current = None;
50 return;
51 }
52 self.current = Some(match self.current {
53 Some(index) => (index + 1) % self.matches.len(),
54 None => 0,
55 });
56 }
57
58 pub fn previous(&mut self) {
59 if self.matches.is_empty() {
60 self.current = None;
61 return;
62 }
63 self.current = Some(match self.current {
64 Some(0) | None => self.matches.len() - 1,
65 Some(index) => index - 1,
66 });
67 }
68
69 pub fn current_match(&self) -> Option<&SearchMatch> {
70 self.current.and_then(|index| self.matches.get(index))
71 }
72
73 pub fn position_label(&self) -> Option<(usize, usize)> {
74 self.current.map(|index| (index + 1, self.matches.len()))
75 }
76}
77
78#[derive(Debug, Clone)]
79pub struct SearchMatcher {
80 regex: Regex,
81}
82
83impl SearchMatcher {
84 pub fn new(query: &str) -> Option<Self> {
85 if query.is_empty() {
86 return None;
87 }
88
89 let case_sensitive = query.chars().any(char::is_uppercase);
90 let regex = RegexBuilder::new(®ex::escape(query))
91 .case_insensitive(!case_sensitive)
92 .build()
93 .expect("escaped search queries always compile");
94 Some(Self { regex })
95 }
96
97 pub fn count(&self, text: &str) -> usize {
98 self.regex.find_iter(text).count()
99 }
100
101 pub fn ranges(&self, text: &str) -> Vec<(usize, usize)> {
102 self.regex
103 .find_iter(text)
104 .map(|m| (m.start(), m.end()))
105 .collect()
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn search_uses_smart_case() {
115 let matcher = SearchMatcher::new("markdown").unwrap();
116 assert_eq!(matcher.count("Markdown markdown MARKDOWN"), 3);
117
118 let matcher = SearchMatcher::new("Markdown").unwrap();
119 assert_eq!(matcher.count("Markdown markdown MARKDOWN"), 1);
120 }
121
122 #[test]
123 fn navigation_wraps() {
124 let mut state = SearchState::default();
125 state.set_matches(vec![
126 SearchMatch {
127 node_id: 1,
128 occurrence: 0,
129 },
130 SearchMatch {
131 node_id: 2,
132 occurrence: 0,
133 },
134 ]);
135 state.next();
136 assert_eq!(state.current, Some(1));
137 state.next();
138 assert_eq!(state.current, Some(0));
139 state.previous();
140 assert_eq!(state.current, Some(1));
141 }
142}