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_export = false;
14        self.overlays.show_legend = false;
15    }
16
17    /// Stop searching
18    pub const fn stop_search(&mut self) {
19        self.overlays.search.active = false;
20    }
21
22    /// Add character to search query
23    pub fn search_push(&mut self, c: char) {
24        self.overlays.search.push_char(c);
25    }
26
27    /// Remove character from search query
28    pub fn search_pop(&mut self) {
29        self.overlays.search.pop_char();
30    }
31
32    /// Execute search with current query
33    pub fn execute_search(&mut self) {
34        if self.overlays.search.query.len() < 2 {
35            self.overlays.search.results.clear();
36            return;
37        }
38
39        // One shared matching semantic across every search surface.
40        let matcher = match crate::tui::app_states::SearchMatcher::build(
41            &self.overlays.search.query,
42            self.overlays.search.mode,
43        ) {
44            Ok(m) => {
45                self.overlays.search.search_error = None;
46                m
47            }
48            Err(e) => {
49                self.overlays.search.search_error = Some(e);
50                self.overlays.search.results.clear();
51                return;
52            }
53        };
54        let matches_query = |text: &str| -> bool { matcher.is_match(text) };
55
56        let mut results = Vec::new();
57
58        // Search through diff results if available (Diff mode)
59        if let Some(ref diff) = self.data.diff_result {
60            // Search added components
61            for comp in &diff.components.added {
62                if matches_query(&comp.name) {
63                    results.push(DiffSearchResult::Component {
64                        name: comp.name.clone(),
65                        version: comp.new_version.clone(),
66                        change_type: ChangeType::Added,
67                    });
68                }
69            }
70
71            // Search removed components
72            for comp in &diff.components.removed {
73                if matches_query(&comp.name) {
74                    results.push(DiffSearchResult::Component {
75                        name: comp.name.clone(),
76                        version: comp.old_version.clone(),
77                        change_type: ChangeType::Removed,
78                    });
79                }
80            }
81
82            // Search modified components
83            for change in &diff.components.modified {
84                if matches_query(&change.name) {
85                    let change_type = if change.change_type == crate::diff::ChangeType::Unchanged {
86                        ChangeType::Unchanged
87                    } else {
88                        ChangeType::Modified
89                    };
90                    results.push(DiffSearchResult::Component {
91                        name: change.name.clone(),
92                        version: change.new_version.clone(),
93                        change_type,
94                    });
95                }
96            }
97
98            // Search introduced vulnerabilities
99            for vuln in &diff.vulnerabilities.introduced {
100                if matches_query(&vuln.id) {
101                    results.push(DiffSearchResult::Vulnerability {
102                        id: vuln.id.clone(),
103                        component_name: vuln.component_name.clone(),
104                        severity: Some(vuln.severity.clone()),
105                        change_type: VulnChangeType::Introduced,
106                    });
107                }
108            }
109
110            // Search resolved vulnerabilities
111            for vuln in &diff.vulnerabilities.resolved {
112                if matches_query(&vuln.id) {
113                    results.push(DiffSearchResult::Vulnerability {
114                        id: vuln.id.clone(),
115                        component_name: vuln.component_name.clone(),
116                        severity: Some(vuln.severity.clone()),
117                        change_type: VulnChangeType::Resolved,
118                    });
119                }
120            }
121
122            // Search license changes (new licenses)
123            for lic_change in &diff.licenses.new_licenses {
124                if matches_query(&lic_change.license) {
125                    let component_name = lic_change
126                        .components
127                        .first()
128                        .cloned()
129                        .unwrap_or_else(|| "multiple".to_string());
130                    results.push(DiffSearchResult::License {
131                        license: lic_change.license.clone(),
132                        component_name,
133                        change_type: ChangeType::Added,
134                    });
135                }
136            }
137
138            // Search license changes (removed licenses)
139            for lic_change in &diff.licenses.removed_licenses {
140                if matches_query(&lic_change.license) {
141                    let component_name = lic_change
142                        .components
143                        .first()
144                        .cloned()
145                        .unwrap_or_else(|| "multiple".to_string());
146                    results.push(DiffSearchResult::License {
147                        license: lic_change.license.clone(),
148                        component_name,
149                        change_type: ChangeType::Removed,
150                    });
151                }
152            }
153        }
154
155        // Search through single SBOM if available (View mode)
156        if self.data.diff_result.is_none()
157            && let Some(ref sbom) = self.data.sbom
158        {
159            // Search components by name
160            for comp in sbom.components.values() {
161                if matches_query(&comp.name) {
162                    results.push(DiffSearchResult::Component {
163                        name: comp.name.clone(),
164                        version: comp.version.clone(),
165                        change_type: ChangeType::Added, // reuse Added as "present"
166                    });
167                }
168            }
169
170            // Search vulnerabilities
171            for comp in sbom.components.values() {
172                for vuln in &comp.vulnerabilities {
173                    if matches_query(&vuln.id) {
174                        results.push(DiffSearchResult::Vulnerability {
175                            id: vuln.id.clone(),
176                            component_name: comp.name.clone(),
177                            severity: vuln.severity.as_ref().map(|s| format!("{s:?}")),
178                            change_type: VulnChangeType::Introduced, // reuse as "present"
179                        });
180                    }
181                }
182            }
183
184            // Search licenses
185            for comp in sbom.components.values() {
186                for lic in &comp.licenses.declared {
187                    if matches_query(&lic.expression) {
188                        results.push(DiffSearchResult::License {
189                            license: lic.expression.clone(),
190                            component_name: comp.name.clone(),
191                            change_type: ChangeType::Added, // reuse as "present"
192                        });
193                    }
194                }
195            }
196        }
197
198        // F2: Filter component results to match the current component filter.
199        // Vulnerability and license results are kept regardless.
200        let comp_filter = self.components_state().filter;
201        if comp_filter != ComponentFilter::All {
202            results.retain(|r| match r {
203                DiffSearchResult::Component { change_type, .. } => match comp_filter {
204                    ComponentFilter::Added => *change_type == ChangeType::Added,
205                    ComponentFilter::Removed => *change_type == ChangeType::Removed,
206                    ComponentFilter::Modified => *change_type == ChangeType::Modified,
207                    // EolOnly/EolRisk don't map to search change types — keep all
208                    _ => true,
209                },
210                // Keep vulnerability and license results regardless of filter
211                DiffSearchResult::Vulnerability { .. } | DiffSearchResult::License { .. } => true,
212            });
213        }
214
215        // Limit results
216        results.truncate(50);
217        self.overlays.search.results = results;
218        self.overlays.search.selected = 0;
219    }
220
221    /// Jump to the currently selected search result
222    pub fn jump_to_search_result(&mut self) {
223        if let Some(result) = self
224            .overlays
225            .search
226            .results
227            .get(self.overlays.search.selected)
228            .cloned()
229        {
230            match result {
231                DiffSearchResult::Component {
232                    name,
233                    version,
234                    change_type,
235                    ..
236                } => {
237                    // Prefer matching by change type + version when possible
238                    if let Some(index) =
239                        self.find_component_index_all(&name, Some(change_type), version.as_deref())
240                    {
241                        self.components_state_mut().filter = ComponentFilter::All;
242                        self.components_state_mut().selected = index;
243                        self.select_tab(TabKind::Components);
244                        self.stop_search();
245                        return;
246                    }
247
248                    // Fall back to name-only match across all components
249                    if let Some(index) = self.find_component_index_all(&name, None, None) {
250                        self.components_state_mut().filter = ComponentFilter::All;
251                        self.components_state_mut().selected = index;
252                        self.select_tab(TabKind::Components);
253                        self.stop_search();
254                        return;
255                    }
256
257                    self.components_state_mut().filter = ComponentFilter::All;
258                    self.select_tab(TabKind::Components);
259                }
260                DiffSearchResult::Vulnerability {
261                    id, change_type, ..
262                } => {
263                    // Align filter/sort so the selection is stable
264                    self.vulnerabilities_state_mut().sort_by = VulnSort::Id;
265                    self.vulnerabilities_state_mut().filter = match change_type {
266                        VulnChangeType::Introduced => VulnFilter::Introduced,
267                        VulnChangeType::Resolved => VulnFilter::Resolved,
268                    };
269
270                    if let Some(index) = self.find_vulnerability_index(&id) {
271                        self.vulnerabilities_state_mut().selected = index;
272                    }
273
274                    self.select_tab(TabKind::Vulnerabilities);
275                }
276                DiffSearchResult::License { license, .. } => {
277                    // Find the license index
278                    if let Some(ref diff) = self.data.diff_result {
279                        let mut index = 0;
280
281                        // Search new licenses first
282                        for lic in &diff.licenses.new_licenses {
283                            if lic.license == license {
284                                self.licenses_state_mut().selected = index;
285                                self.select_tab(TabKind::Licenses);
286                                self.stop_search();
287                                return;
288                            }
289                            index += 1;
290                        }
291
292                        // Then removed licenses
293                        for lic in &diff.licenses.removed_licenses {
294                            if lic.license == license {
295                                self.licenses_state_mut().selected = index;
296                                self.select_tab(TabKind::Licenses);
297                                self.stop_search();
298                                return;
299                            }
300                            index += 1;
301                        }
302                    }
303                    self.select_tab(TabKind::Licenses);
304                }
305            }
306            self.stop_search();
307        }
308    }
309}