excel_cli/app/
search.rs

1use crate::app::AppState;
2use crate::app::InputMode;
3
4impl AppState<'_> {
5    pub fn start_search_forward(&mut self) {
6        self.input_mode = InputMode::SearchForward;
7        self.input_buffer = String::new();
8        self.text_area = ratatui_textarea::TextArea::default();
9        self.add_notification("Search forward mode".to_string());
10        self.highlight_enabled = true;
11    }
12
13    pub fn start_search_backward(&mut self) {
14        self.input_mode = InputMode::SearchBackward;
15        self.input_buffer = String::new();
16        self.text_area = ratatui_textarea::TextArea::default();
17        self.add_notification("Search backward mode".to_string());
18        self.highlight_enabled = true;
19    }
20
21    pub fn execute_search(&mut self) {
22        let query = self.text_area.lines().join("\n");
23        self.input_buffer = query.clone();
24
25        if query.is_empty() {
26            self.input_mode = InputMode::Normal;
27            return;
28        }
29
30        // Save the query for n/N commands
31        self.search_query = query.clone();
32
33        // Set search direction based on mode
34        match self.input_mode {
35            InputMode::SearchForward => self.search_direction = true,
36            InputMode::SearchBackward => self.search_direction = false,
37            _ => {}
38        }
39
40        self.search_results = self.find_all_matches(&query);
41
42        if self.search_results.is_empty() {
43            self.add_notification(format!("Pattern not found: {}", query));
44            self.current_search_idx = None;
45        } else {
46            // Find the appropriate result to jump to based on search direction and current position
47            self.jump_to_next_search_result();
48            self.add_notification(format!(
49                "{} matches found for: {}",
50                self.search_results.len(),
51                query
52            ));
53        }
54
55        self.input_mode = InputMode::Normal;
56        self.input_buffer = String::new();
57        self.text_area = ratatui_textarea::TextArea::default();
58    }
59
60    pub fn find_all_matches(&self, query: &str) -> Vec<(usize, usize)> {
61        let sheet = self.workbook.get_current_sheet();
62        let query_lower = query.to_lowercase();
63
64        // Pre-allocate with reasonable capacity
65        let mut results = Vec::with_capacity(32);
66
67        // row-first, column-second order
68        for row in 1..=sheet.max_rows {
69            for col in 1..=sheet.max_cols {
70                if row < sheet.data.len() && col < sheet.data[row].len() {
71                    let cell_content = &sheet.data[row][col].value;
72
73                    if cell_content.is_empty() {
74                        continue;
75                    }
76
77                    if self.case_insensitive_contains(cell_content, &query_lower) {
78                        results.push((row, col));
79                    }
80                }
81            }
82        }
83
84        results
85    }
86
87    fn case_insensitive_contains(&self, haystack: &str, needle: &str) -> bool {
88        if needle.is_empty() {
89            return true;
90        }
91        if haystack.is_empty() {
92            return false;
93        }
94
95        haystack.to_lowercase().contains(needle)
96    }
97
98    pub fn jump_to_next_search_result(&mut self) {
99        if self.search_results.is_empty() {
100            return;
101        }
102
103        self.highlight_enabled = true;
104
105        let current_pos = self.selected_cell;
106
107        if self.search_direction {
108            // Forward search
109            let next_idx = self.search_results.iter().position(|&pos| {
110                pos.0 > current_pos.0 || (pos.0 == current_pos.0 && pos.1 > current_pos.1)
111            });
112
113            match next_idx {
114                Some(idx) => {
115                    self.current_search_idx = Some(idx);
116                    self.selected_cell = self.search_results[idx];
117                }
118                None => {
119                    // Wrap around to the first result
120                    self.current_search_idx = Some(0);
121                    self.selected_cell = self.search_results[0];
122                    self.add_notification("Search wrapped to top".to_string());
123                }
124            }
125        } else {
126            // Backward search
127            let prev_idx = self.search_results.iter().rposition(|&pos| {
128                pos.0 < current_pos.0 || (pos.0 == current_pos.0 && pos.1 < current_pos.1)
129            });
130
131            match prev_idx {
132                Some(idx) => {
133                    self.current_search_idx = Some(idx);
134                    self.selected_cell = self.search_results[idx];
135                }
136                None => {
137                    // Wrap around to the last result
138                    let last_idx = self.search_results.len() - 1;
139                    self.current_search_idx = Some(last_idx);
140                    self.selected_cell = self.search_results[last_idx];
141                    self.add_notification("Search wrapped to bottom".to_string());
142                }
143            }
144        }
145
146        self.handle_scrolling();
147    }
148
149    pub fn jump_to_prev_search_result(&mut self) {
150        if self.search_results.is_empty() {
151            return;
152        }
153
154        // Temporarily flip the search direction
155        self.search_direction = !self.search_direction;
156        self.jump_to_next_search_result();
157        // Restore original search direction
158        self.search_direction = !self.search_direction;
159    }
160
161    pub fn disable_search_highlight(&mut self) {
162        self.highlight_enabled = false;
163        self.add_notification("Search highlighting disabled".to_string());
164    }
165}