Skip to main content

sbom_tools/tui/
app_impl_search.rs

1//! Search-related methods for App.
2
3use super::app::{App, TabKind};
4use super::app_states::{
5    ChangeType, ComponentFilter, DiffSearchResult, VulnChangeType, VulnFilter, VulnSort,
6};
7
8impl App {
9    /// Start searching
10    pub fn start_search(&mut self) {
11        self.overlays.search.active = true;
12        self.overlays.search.clear();
13        self.overlays.show_help = false;
14        self.overlays.show_export = false;
15        self.overlays.show_legend = false;
16    }
17
18    /// Stop searching
19    pub fn stop_search(&mut self) {
20        self.overlays.search.active = false;
21    }
22
23    /// Add character to search query
24    pub fn search_push(&mut self, c: char) {
25        self.overlays.search.push_char(c);
26    }
27
28    /// Remove character from search query
29    pub fn search_pop(&mut self) {
30        self.overlays.search.pop_char();
31    }
32
33    /// Execute search with current query
34    pub fn execute_search(&mut self) {
35        if self.overlays.search.query.len() < 2 {
36            self.overlays.search.results.clear();
37            return;
38        }
39
40        let query_lower = self.overlays.search.query.to_lowercase();
41        let mut results = Vec::new();
42
43        // Search through diff results if available
44        if let Some(ref diff) = self.data.diff_result {
45            // Search added components
46            for comp in &diff.components.added {
47                if comp.name.to_lowercase().contains(&query_lower) {
48                    results.push(DiffSearchResult::Component {
49                        name: comp.name.clone(),
50                        version: comp.new_version.clone(),
51                        change_type: ChangeType::Added,
52                        match_field: "name".to_string(),
53                    });
54                }
55            }
56
57            // Search removed components
58            for comp in &diff.components.removed {
59                if comp.name.to_lowercase().contains(&query_lower) {
60                    results.push(DiffSearchResult::Component {
61                        name: comp.name.clone(),
62                        version: comp.old_version.clone(),
63                        change_type: ChangeType::Removed,
64                        match_field: "name".to_string(),
65                    });
66                }
67            }
68
69            // Search modified components
70            for change in &diff.components.modified {
71                if change.name.to_lowercase().contains(&query_lower) {
72                    results.push(DiffSearchResult::Component {
73                        name: change.name.clone(),
74                        version: change.new_version.clone(),
75                        change_type: ChangeType::Modified,
76                        match_field: "name".to_string(),
77                    });
78                }
79            }
80
81            // Search introduced vulnerabilities
82            for vuln in &diff.vulnerabilities.introduced {
83                if vuln.id.to_lowercase().contains(&query_lower) {
84                    results.push(DiffSearchResult::Vulnerability {
85                        id: vuln.id.clone(),
86                        component_name: vuln.component_name.clone(),
87                        severity: Some(vuln.severity.clone()),
88                        change_type: VulnChangeType::Introduced,
89                    });
90                }
91            }
92
93            // Search resolved vulnerabilities
94            for vuln in &diff.vulnerabilities.resolved {
95                if vuln.id.to_lowercase().contains(&query_lower) {
96                    results.push(DiffSearchResult::Vulnerability {
97                        id: vuln.id.clone(),
98                        component_name: vuln.component_name.clone(),
99                        severity: Some(vuln.severity.clone()),
100                        change_type: VulnChangeType::Resolved,
101                    });
102                }
103            }
104
105            // Search license changes (new licenses)
106            for lic_change in &diff.licenses.new_licenses {
107                if lic_change.license.to_lowercase().contains(&query_lower) {
108                    let component_name = lic_change
109                        .components
110                        .first()
111                        .cloned()
112                        .unwrap_or_else(|| "multiple".to_string());
113                    results.push(DiffSearchResult::License {
114                        license: lic_change.license.clone(),
115                        component_name,
116                        change_type: ChangeType::Added,
117                    });
118                }
119            }
120
121            // Search license changes (removed licenses)
122            for lic_change in &diff.licenses.removed_licenses {
123                if lic_change.license.to_lowercase().contains(&query_lower) {
124                    let component_name = lic_change
125                        .components
126                        .first()
127                        .cloned()
128                        .unwrap_or_else(|| "multiple".to_string());
129                    results.push(DiffSearchResult::License {
130                        license: lic_change.license.clone(),
131                        component_name,
132                        change_type: ChangeType::Removed,
133                    });
134                }
135            }
136        }
137
138        // Limit results
139        results.truncate(50);
140        self.overlays.search.results = results;
141        self.overlays.search.selected = 0;
142    }
143
144    /// Jump to the currently selected search result
145    pub fn jump_to_search_result(&mut self) {
146        if let Some(result) = self
147            .overlays.search
148            .results
149            .get(self.overlays.search.selected)
150            .cloned()
151        {
152            match result {
153                DiffSearchResult::Component {
154                    name,
155                    version,
156                    change_type,
157                    ..
158                } => {
159                    // Prefer matching by change type + version when possible
160                    if let Some(index) =
161                        self.find_component_index_all(&name, Some(change_type), version.as_deref())
162                    {
163                        self.tabs.components.filter = ComponentFilter::All;
164                        self.tabs.components.selected = index;
165                        self.select_tab(TabKind::Components);
166                        self.stop_search();
167                        return;
168                    }
169
170                    // Fall back to name-only match across all components
171                    if let Some(index) = self.find_component_index_all(&name, None, None) {
172                        self.tabs.components.filter = ComponentFilter::All;
173                        self.tabs.components.selected = index;
174                        self.select_tab(TabKind::Components);
175                        self.stop_search();
176                        return;
177                    }
178
179                    self.tabs.components.filter = ComponentFilter::All;
180                    self.select_tab(TabKind::Components);
181                }
182                DiffSearchResult::Vulnerability {
183                    id, change_type, ..
184                } => {
185                    // Align filter/sort so the selection is stable
186                    self.tabs.vulnerabilities.sort_by = VulnSort::Id;
187                    self.tabs.vulnerabilities.filter = match change_type {
188                        VulnChangeType::Introduced => VulnFilter::Introduced,
189                        VulnChangeType::Resolved => VulnFilter::Resolved,
190                        VulnChangeType::Persistent => VulnFilter::All,
191                    };
192
193                    if let Some(index) = self.find_vulnerability_index(&id) {
194                        self.tabs.vulnerabilities.selected = index;
195                    }
196
197                    self.select_tab(TabKind::Vulnerabilities);
198                }
199                DiffSearchResult::License { license, .. } => {
200                    // Find the license index
201                    if let Some(ref diff) = self.data.diff_result {
202                        let mut index = 0;
203
204                        // Search new licenses first
205                        for lic in &diff.licenses.new_licenses {
206                            if lic.license == license {
207                                self.tabs.licenses.selected = index;
208                                self.select_tab(TabKind::Licenses);
209                                self.stop_search();
210                                return;
211                            }
212                            index += 1;
213                        }
214
215                        // Then removed licenses
216                        for lic in &diff.licenses.removed_licenses {
217                            if lic.license == license {
218                                self.tabs.licenses.selected = index;
219                                self.select_tab(TabKind::Licenses);
220                                self.stop_search();
221                                return;
222                            }
223                            index += 1;
224                        }
225                    }
226                    self.select_tab(TabKind::Licenses);
227                }
228            }
229            self.stop_search();
230        }
231    }
232}