excel_cli/app/
search.rs

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