1use tracing::warn;
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct SearchMatch {
11 pub row: usize,
13 pub column: usize,
15 pub value: String,
17 pub highlight_range: (usize, usize),
19}
20
21#[derive(Debug, Clone)]
23pub struct SearchConfig {
24 pub case_sensitive: bool,
26 pub use_regex: bool,
28 pub visible_columns_only: bool,
30 pub wrap_around: bool,
32}
33
34impl Default for SearchConfig {
35 fn default() -> Self {
36 Self {
37 case_sensitive: false,
38 use_regex: false,
39 visible_columns_only: false,
40 wrap_around: true,
41 }
42 }
43}
44
45pub struct SearchManager {
47 pattern: String,
49 matches: Vec<SearchMatch>,
51 current_index: usize,
53 config: SearchConfig,
55 regex: Option<regex::Regex>,
57}
58
59impl Default for SearchManager {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl SearchManager {
66 #[must_use]
68 pub fn new() -> Self {
69 Self {
70 pattern: String::new(),
71 matches: Vec::new(),
72 current_index: 0,
73 config: SearchConfig::default(),
74 regex: None,
75 }
76 }
77
78 #[must_use]
80 pub fn with_config(config: SearchConfig) -> Self {
81 Self {
82 pattern: String::new(),
83 matches: Vec::new(),
84 current_index: 0,
85 config,
86 regex: None,
87 }
88 }
89
90 pub fn set_config(&mut self, config: SearchConfig) {
92 if !config.use_regex {
94 self.regex = None;
95 }
96 self.config = config;
97 }
98
99 pub fn set_case_sensitive(&mut self, case_sensitive: bool) {
101 self.config.case_sensitive = case_sensitive;
102 if self.config.use_regex && !self.pattern.is_empty() {
104 self.compile_regex();
105 }
106 }
107
108 pub fn search(
111 &mut self,
112 pattern: &str,
113 data: &[Vec<String>],
114 visible_columns: Option<&[usize]>,
115 ) -> usize {
116 self.pattern = pattern.to_string();
117 self.matches.clear();
118 self.current_index = 0;
119
120 if pattern.is_empty() {
121 return 0;
122 }
123
124 if self.config.use_regex {
126 self.compile_regex();
127 if self.regex.is_none() {
128 return 0; }
130 }
131
132 let columns_to_search: Vec<usize> = if self.config.visible_columns_only {
134 visible_columns.map(<[usize]>::to_vec).unwrap_or_else(|| {
135 if data.is_empty() {
137 vec![]
138 } else {
139 (0..data[0].len()).collect()
140 }
141 })
142 } else {
143 if data.is_empty() {
145 vec![]
146 } else {
147 (0..data[0].len()).collect()
148 }
149 };
150
151 for (row_idx, row) in data.iter().enumerate() {
153 for &col_idx in &columns_to_search {
154 if col_idx >= row.len() {
155 continue;
156 }
157
158 let cell_value = &row[col_idx];
159 if let Some(range) = self.matches_pattern(cell_value, pattern) {
160 self.matches.push(SearchMatch {
161 row: row_idx,
162 column: col_idx,
163 value: cell_value.clone(),
164 highlight_range: range,
165 });
166 }
167 }
168 }
169
170 self.matches.len()
171 }
172
173 fn matches_pattern(&self, value: &str, pattern: &str) -> Option<(usize, usize)> {
175 if self.config.use_regex {
176 if let Some(ref regex) = self.regex {
178 if let Some(m) = regex.find(value) {
179 return Some((m.start(), m.end()));
180 }
181 }
182 } else {
183 let search_value = if self.config.case_sensitive {
185 value.to_string()
186 } else {
187 value.to_lowercase()
188 };
189
190 let search_pattern = if self.config.case_sensitive {
191 pattern.to_string()
192 } else {
193 pattern.to_lowercase()
194 };
195
196 if let Some(pos) = search_value.find(&search_pattern) {
197 return Some((pos, pos + pattern.len()));
198 }
199 }
200 None
201 }
202
203 fn compile_regex(&mut self) {
205 let pattern = if self.config.case_sensitive {
206 self.pattern.clone()
207 } else {
208 format!("(?i){}", self.pattern)
209 };
210
211 match regex::Regex::new(&pattern) {
212 Ok(regex) => self.regex = Some(regex),
213 Err(e) => {
214 warn!("Invalid regex pattern: {}", e);
215 self.regex = None;
216 }
217 }
218 }
219
220 #[must_use]
222 pub fn current_match(&self) -> Option<&SearchMatch> {
223 if self.matches.is_empty() {
224 None
225 } else {
226 self.matches.get(self.current_index)
227 }
228 }
229
230 pub fn next_match(&mut self) -> Option<&SearchMatch> {
232 if self.matches.is_empty() {
233 return None;
234 }
235
236 if self.current_index + 1 < self.matches.len() {
237 self.current_index += 1;
238 } else if self.config.wrap_around {
239 self.current_index = 0;
240 }
241
242 self.current_match()
243 }
244
245 pub fn previous_match(&mut self) -> Option<&SearchMatch> {
247 if self.matches.is_empty() {
248 return None;
249 }
250
251 if self.current_index > 0 {
252 self.current_index -= 1;
253 } else if self.config.wrap_around {
254 self.current_index = self.matches.len() - 1;
255 }
256
257 self.current_match()
258 }
259
260 pub fn jump_to_match(&mut self, index: usize) -> Option<&SearchMatch> {
262 if index < self.matches.len() {
263 self.current_index = index;
264 self.current_match()
265 } else {
266 None
267 }
268 }
269
270 #[must_use]
272 pub fn first_match(&self) -> Option<&SearchMatch> {
273 self.matches.first()
274 }
275
276 #[must_use]
278 pub fn all_matches(&self) -> &[SearchMatch] {
279 &self.matches
280 }
281
282 #[must_use]
284 pub fn match_count(&self) -> usize {
285 self.matches.len()
286 }
287
288 #[must_use]
290 pub fn current_match_number(&self) -> usize {
291 if self.matches.is_empty() {
292 0
293 } else {
294 self.current_index + 1
295 }
296 }
297
298 pub fn clear(&mut self) {
300 self.pattern.clear();
301 self.matches.clear();
302 self.current_index = 0;
303 self.regex = None;
304 }
305
306 #[must_use]
308 pub fn has_active_search(&self) -> bool {
309 !self.pattern.is_empty()
310 }
311
312 #[must_use]
314 pub fn pattern(&self) -> &str {
315 &self.pattern
316 }
317
318 #[must_use]
320 pub fn calculate_scroll_offset(
321 &self,
322 match_pos: &SearchMatch,
323 viewport_height: usize,
324 current_offset: usize,
325 ) -> usize {
326 let row = match_pos.row;
327
328 if row < current_offset {
330 row
331 }
332 else if row >= current_offset + viewport_height {
334 row.saturating_sub(viewport_height / 2)
335 }
336 else {
338 current_offset
339 }
340 }
341
342 #[must_use]
344 pub fn find_next_from(&self, current_row: usize, current_col: usize) -> Option<&SearchMatch> {
345 for match_item in &self.matches {
347 if match_item.row > current_row
348 || (match_item.row == current_row && match_item.column > current_col)
349 {
350 return Some(match_item);
351 }
352 }
353
354 if self.config.wrap_around && !self.matches.is_empty() {
356 return self.matches.first();
357 }
358
359 None
360 }
361
362 #[must_use]
364 pub fn find_previous_from(
365 &self,
366 current_row: usize,
367 current_col: usize,
368 ) -> Option<&SearchMatch> {
369 for match_item in self.matches.iter().rev() {
371 if match_item.row < current_row
372 || (match_item.row == current_row && match_item.column < current_col)
373 {
374 return Some(match_item);
375 }
376 }
377
378 if self.config.wrap_around && !self.matches.is_empty() {
380 return self.matches.last();
381 }
382
383 None
384 }
385}
386
387pub struct SearchIterator<'a> {
389 manager: &'a SearchManager,
390 index: usize,
391}
392
393impl<'a> Iterator for SearchIterator<'a> {
394 type Item = &'a SearchMatch;
395
396 fn next(&mut self) -> Option<Self::Item> {
397 if self.index < self.manager.matches.len() {
398 let result = &self.manager.matches[self.index];
399 self.index += 1;
400 Some(result)
401 } else {
402 None
403 }
404 }
405}
406
407impl SearchManager {
408 #[must_use]
410 pub fn iter(&self) -> SearchIterator {
411 SearchIterator {
412 manager: self,
413 index: 0,
414 }
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn test_case_insensitive_search() {
424 let mut manager = SearchManager::new();
425 manager.set_case_sensitive(false);
426
427 let data = vec![
428 vec!["Unconfirmed".to_string(), "data1".to_string()],
429 vec!["unconfirmed".to_string(), "data2".to_string()],
430 vec!["UNCONFIRMED".to_string(), "data3".to_string()],
431 vec!["confirmed".to_string(), "data4".to_string()],
432 ];
433
434 let count = manager.search("unconfirmed", &data, None);
435 assert_eq!(count, 3);
436
437 let matches: Vec<_> = manager.iter().collect();
439 assert_eq!(matches.len(), 3);
440 assert_eq!(matches[0].row, 0);
441 assert_eq!(matches[1].row, 1);
442 assert_eq!(matches[2].row, 2);
443 }
444
445 #[test]
446 fn test_case_sensitive_search() {
447 let mut manager = SearchManager::new();
448 manager.set_case_sensitive(true);
449
450 let data = vec![
451 vec!["Unconfirmed".to_string(), "data1".to_string()],
452 vec!["unconfirmed".to_string(), "data2".to_string()],
453 vec!["UNCONFIRMED".to_string(), "data3".to_string()],
454 ];
455
456 let count = manager.search("Unconfirmed", &data, None);
457 assert_eq!(count, 1);
458
459 let first_match = manager.first_match().unwrap();
460 assert_eq!(first_match.row, 0);
461 assert_eq!(first_match.value, "Unconfirmed");
462 }
463
464 #[test]
465 fn test_navigation() {
466 let mut manager = SearchManager::new();
467
468 let data = vec![
469 vec!["apple".to_string(), "banana".to_string()],
470 vec!["apple pie".to_string(), "cherry".to_string()],
471 vec!["orange".to_string(), "apple juice".to_string()],
472 ];
473
474 manager.search("apple", &data, None);
475 assert_eq!(manager.match_count(), 3);
476
477 let first = manager.current_match().unwrap();
479 assert_eq!((first.row, first.column), (0, 0));
480
481 let second = manager.next_match().unwrap();
482 assert_eq!((second.row, second.column), (1, 0));
483
484 let third = manager.next_match().unwrap();
485 assert_eq!((third.row, third.column), (2, 1));
486
487 let wrapped = manager.next_match().unwrap();
489 assert_eq!((wrapped.row, wrapped.column), (0, 0));
490
491 let prev = manager.previous_match().unwrap();
493 assert_eq!((prev.row, prev.column), (2, 1));
494 }
495
496 #[test]
497 fn test_visible_columns_filter() {
498 let mut config = SearchConfig::default();
499 config.visible_columns_only = true;
500 let mut manager = SearchManager::with_config(config);
501
502 let data = vec![
503 vec![
504 "apple".to_string(),
505 "hidden".to_string(),
506 "banana".to_string(),
507 ],
508 vec![
509 "orange".to_string(),
510 "apple".to_string(),
511 "cherry".to_string(),
512 ],
513 ];
514
515 let visible = vec![0, 2];
517 let count = manager.search("apple", &data, Some(&visible));
518
519 assert_eq!(count, 1);
521 let match_item = manager.first_match().unwrap();
522 assert_eq!(match_item.row, 0);
523 assert_eq!(match_item.column, 0);
524 }
525
526 #[test]
527 fn test_scroll_offset_calculation() {
528 let manager = SearchManager::new();
529
530 let match_item = SearchMatch {
531 row: 50,
532 column: 0,
533 value: String::new(),
534 highlight_range: (0, 0),
535 };
536
537 let offset = manager.calculate_scroll_offset(&match_item, 20, 10);
539 assert_eq!(offset, 40); let offset = manager.calculate_scroll_offset(&match_item, 20, 60);
543 assert_eq!(offset, 50);
544
545 let offset = manager.calculate_scroll_offset(&match_item, 20, 45);
547 assert_eq!(offset, 45);
548 }
549
550 #[test]
551 fn test_find_from_position() {
552 let mut manager = SearchManager::new();
553
554 let data = vec![
555 vec!["a".to_string(), "b".to_string(), "match".to_string()],
556 vec!["match".to_string(), "c".to_string(), "d".to_string()],
557 vec!["e".to_string(), "match".to_string(), "f".to_string()],
558 ];
559
560 manager.search("match", &data, None);
561
562 let next = manager.find_next_from(0, 1).unwrap();
564 assert_eq!((next.row, next.column), (0, 2));
565
566 let next = manager.find_next_from(1, 0).unwrap();
568 assert_eq!((next.row, next.column), (2, 1));
569
570 let prev = manager.find_previous_from(2, 0).unwrap();
572 assert_eq!((prev.row, prev.column), (1, 0));
573 }
574}