1use regex::Regex;
4
5use crate::error::{CliError, Result};
6
7use super::table::{format_value, TableView};
8
9#[derive(Default)]
11pub struct SearchState {
12 active: bool,
13 query: String,
14 regex: Option<Regex>,
15 matches: Vec<(usize, usize)>,
16 current: Option<usize>,
17}
18
19impl SearchState {
20 pub fn is_active(&self) -> bool {
21 self.active
22 }
23
24 pub fn has_query(&self) -> bool {
25 !self.query.is_empty()
26 }
27
28 pub fn query(&self) -> &str {
29 &self.query
30 }
31
32 pub fn activate(&mut self) {
33 self.active = true;
34 }
35
36 pub fn deactivate(&mut self) {
37 self.active = false;
38 }
39
40 pub fn cancel(&mut self) {
41 self.active = false;
42 self.query.clear();
43 self.regex = None;
44 self.matches.clear();
45 self.current = None;
46 }
47
48 pub fn push_char(&mut self, ch: char, table: &TableView) -> Result<()> {
49 self.query.push(ch);
50 self.recompute(table)?;
51 Ok(())
52 }
53
54 pub fn backspace(&mut self, table: &TableView) -> Result<()> {
55 self.query.pop();
56 self.recompute(table)?;
57 Ok(())
58 }
59
60 pub fn next_match(&mut self, table: &TableView) -> Result<Option<usize>> {
61 if self.matches.is_empty() {
62 self.recompute(table)?;
63 }
64 if self.matches.is_empty() {
65 return Ok(None);
66 }
67 let next = match self.current {
68 None => 0,
69 Some(idx) => (idx + 1) % self.matches.len(),
70 };
71 self.current = Some(next);
72 Ok(self.matches.get(next).map(|(row, _)| *row))
73 }
74
75 pub fn prev_match(&mut self, table: &TableView) -> Result<Option<usize>> {
76 if self.matches.is_empty() {
77 self.recompute(table)?;
78 }
79 if self.matches.is_empty() {
80 return Ok(None);
81 }
82 let prev = match self.current {
83 None => 0,
84 Some(idx) => idx.saturating_sub(1),
85 };
86 self.current = Some(prev);
87 Ok(self.matches.get(prev).map(|(row, _)| *row))
88 }
89
90 pub fn current_match(&self) -> Option<usize> {
91 self.current
92 .and_then(|idx| self.matches.get(idx).map(|(row, _)| *row))
93 }
94
95 pub fn matches_cell(&self, row: usize, col: usize, text: &str) -> bool {
96 self.regex
97 .as_ref()
98 .map(|regex| regex.is_match(text) && self.matches.contains(&(row, col)))
99 .unwrap_or(false)
100 }
101
102 pub fn recompute(&mut self, table: &TableView) -> Result<()> {
103 self.matches.clear();
104 self.current = None;
105 if self.query.is_empty() {
106 self.regex = None;
107 return Ok(());
108 }
109
110 let regex = regex::RegexBuilder::new(&self.query)
111 .case_insensitive(true)
112 .build()
113 .map_err(|err| CliError::InvalidArgument(err.to_string()))?;
114
115 for (row_idx, row) in table.rows().iter().enumerate() {
116 for (col_idx, value) in row.columns.iter().enumerate() {
117 let text = format_value(value);
118 if regex.is_match(&text) {
119 self.matches.push((row_idx, col_idx));
120 }
121 }
122 }
123
124 if !self.matches.is_empty() {
125 self.current = Some(0);
126 }
127 self.regex = Some(regex);
128 Ok(())
129 }
130}