sql_cli/ui/search/
search_operations.rs

1//! Search and filter operations
2//!
3//! This module contains search and filter operations extracted from the monolithic TUI
4//! to improve maintainability and testability. The search system is complex with multiple
5//! modes and state dependencies, so we start with extracting coordination logic.
6
7use crate::app_state_container::AppStateContainer;
8use crate::buffer::BufferAPI;
9use crate::data::data_view::DataView;
10use crate::widgets::search_modes_widget::SearchMode;
11// Arc import removed - no longer needed
12
13/// Context for search operations
14/// Provides the minimal interface needed for search operations
15pub struct SearchContext<'a> {
16    pub state_container: &'a AppStateContainer,
17    pub buffer: &'a mut dyn BufferAPI,
18    pub current_data: Option<&'a DataView>,
19}
20
21/// Result of a search operation
22#[derive(Debug, Clone, PartialEq)]
23pub struct SearchResult {
24    pub matches_found: usize,
25    pub first_match: Option<(usize, usize)>, // (row, col)
26    pub status_message: String,
27}
28
29/// Result of search action execution
30#[derive(Debug, Clone, PartialEq)]
31pub enum SearchActionResult {
32    /// Search was executed successfully
33    Success(SearchResult),
34    /// Search was started but needs follow-up action
35    InProgress(String),
36    /// Search failed
37    Error(String),
38}
39
40/// Execute a search action based on mode and pattern
41/// This coordinates the search process but delegates the actual searching
42pub fn execute_search_action(
43    mode: SearchMode,
44    pattern: String,
45    ctx: &mut SearchContext,
46) -> SearchActionResult {
47    match mode {
48        SearchMode::Search => {
49            // Set search pattern in state container
50            ctx.state_container.start_search(pattern.clone());
51            ctx.buffer.set_search_pattern(pattern);
52
53            // Perform the actual search (this will need to be extracted separately)
54            let search_result = perform_search_with_context(ctx);
55
56            match search_result {
57                Ok(result) => SearchActionResult::Success(result),
58                Err(e) => SearchActionResult::Error(e),
59            }
60        }
61        SearchMode::Filter => {
62            SearchActionResult::InProgress(format!("Filter mode with pattern: {pattern}"))
63        }
64        SearchMode::FuzzyFilter => {
65            SearchActionResult::InProgress(format!("Fuzzy filter mode with pattern: {pattern}"))
66        }
67        SearchMode::ColumnSearch => {
68            SearchActionResult::InProgress(format!("Column search mode with pattern: {pattern}"))
69        }
70    }
71}
72
73/// Perform search with the given context
74/// This is where the actual search logic will eventually be extracted
75fn perform_search_with_context(ctx: &mut SearchContext) -> Result<SearchResult, String> {
76    if let Some(dataview) = ctx.current_data {
77        // Convert DataView rows to Vec<Vec<String>> for search
78        let data: Vec<Vec<String>> = (0..dataview.row_count())
79            .filter_map(|i| dataview.get_row(i))
80            .map(|row| {
81                row.values
82                    .iter()
83                    .map(std::string::ToString::to_string)
84                    .collect()
85            })
86            .collect();
87
88        // Perform search using AppStateContainer
89        let matches = ctx.state_container.perform_search(&data);
90
91        // Convert matches to our format
92        let buffer_matches: Vec<(usize, usize)> = matches
93            .iter()
94            .map(|(row, col, _, _)| (*row, *col))
95            .collect();
96
97        if buffer_matches.is_empty() {
98            ctx.buffer.set_search_matches(Vec::new());
99
100            Ok(SearchResult {
101                matches_found: 0,
102                first_match: None,
103                status_message: "No matches found".to_string(),
104            })
105        } else {
106            let first_match = buffer_matches[0];
107
108            // Update buffer state
109            ctx.buffer.set_search_matches(buffer_matches.clone());
110            ctx.buffer.set_search_match_index(0);
111            ctx.buffer.set_current_match(Some(first_match));
112
113            Ok(SearchResult {
114                matches_found: buffer_matches.len(),
115                first_match: Some(first_match),
116                status_message: format!("Found {} matches", buffer_matches.len()),
117            })
118        }
119    } else {
120        Err("No data available for search".to_string())
121    }
122}
123
124/// Apply search result to UI state
125pub fn apply_search_result(result: &SearchResult, ctx: &mut SearchContext) {
126    if let Some((row, _col)) = result.first_match {
127        ctx.state_container.set_table_selected_row(Some(row));
128    }
129
130    ctx.buffer.set_status_message(result.status_message.clone());
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_search_action_result_types() {
139        // Test that our result types work correctly
140        let success = SearchActionResult::Success(SearchResult {
141            matches_found: 5,
142            first_match: Some((0, 1)),
143            status_message: "Found 5 matches".to_string(),
144        });
145
146        match success {
147            SearchActionResult::Success(result) => {
148                assert_eq!(result.matches_found, 5);
149                assert_eq!(result.first_match, Some((0, 1)));
150                assert_eq!(result.status_message, "Found 5 matches");
151            }
152            _ => panic!("Expected Success result"),
153        }
154
155        let in_progress = SearchActionResult::InProgress("Filtering...".to_string());
156        match in_progress {
157            SearchActionResult::InProgress(msg) => {
158                assert_eq!(msg, "Filtering...");
159            }
160            _ => panic!("Expected InProgress result"),
161        }
162
163        let error = SearchActionResult::Error("No data".to_string());
164        match error {
165            SearchActionResult::Error(msg) => {
166                assert_eq!(msg, "No data");
167            }
168            _ => panic!("Expected Error result"),
169        }
170    }
171
172    #[test]
173    fn test_search_result_creation() {
174        let result = SearchResult {
175            matches_found: 0,
176            first_match: None,
177            status_message: "No matches found".to_string(),
178        };
179
180        assert_eq!(result.matches_found, 0);
181        assert_eq!(result.first_match, None);
182        assert_eq!(result.status_message, "No matches found");
183    }
184}