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 const 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 (Diff mode)
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                    });
53                }
54            }
55
56            // Search removed components
57            for comp in &diff.components.removed {
58                if comp.name.to_lowercase().contains(&query_lower) {
59                    results.push(DiffSearchResult::Component {
60                        name: comp.name.clone(),
61                        version: comp.old_version.clone(),
62                        change_type: ChangeType::Removed,
63                    });
64                }
65            }
66
67            // Search modified components
68            for change in &diff.components.modified {
69                if change.name.to_lowercase().contains(&query_lower) {
70                    results.push(DiffSearchResult::Component {
71                        name: change.name.clone(),
72                        version: change.new_version.clone(),
73                        change_type: ChangeType::Modified,
74                    });
75                }
76            }
77
78            // Search introduced vulnerabilities
79            for vuln in &diff.vulnerabilities.introduced {
80                if vuln.id.to_lowercase().contains(&query_lower) {
81                    results.push(DiffSearchResult::Vulnerability {
82                        id: vuln.id.clone(),
83                        component_name: vuln.component_name.clone(),
84                        severity: Some(vuln.severity.clone()),
85                        change_type: VulnChangeType::Introduced,
86                    });
87                }
88            }
89
90            // Search resolved vulnerabilities
91            for vuln in &diff.vulnerabilities.resolved {
92                if vuln.id.to_lowercase().contains(&query_lower) {
93                    results.push(DiffSearchResult::Vulnerability {
94                        id: vuln.id.clone(),
95                        component_name: vuln.component_name.clone(),
96                        severity: Some(vuln.severity.clone()),
97                        change_type: VulnChangeType::Resolved,
98                    });
99                }
100            }
101
102            // Search license changes (new licenses)
103            for lic_change in &diff.licenses.new_licenses {
104                if lic_change.license.to_lowercase().contains(&query_lower) {
105                    let component_name = lic_change
106                        .components
107                        .first()
108                        .cloned()
109                        .unwrap_or_else(|| "multiple".to_string());
110                    results.push(DiffSearchResult::License {
111                        license: lic_change.license.clone(),
112                        component_name,
113                        change_type: ChangeType::Added,
114                    });
115                }
116            }
117
118            // Search license changes (removed licenses)
119            for lic_change in &diff.licenses.removed_licenses {
120                if lic_change.license.to_lowercase().contains(&query_lower) {
121                    let component_name = lic_change
122                        .components
123                        .first()
124                        .cloned()
125                        .unwrap_or_else(|| "multiple".to_string());
126                    results.push(DiffSearchResult::License {
127                        license: lic_change.license.clone(),
128                        component_name,
129                        change_type: ChangeType::Removed,
130                    });
131                }
132            }
133        }
134
135        // Search through single SBOM if available (View mode)
136        if self.data.diff_result.is_none()
137            && let Some(ref sbom) = self.data.sbom
138        {
139            // Search components by name
140            for comp in sbom.components.values() {
141                if comp.name.to_lowercase().contains(&query_lower) {
142                    results.push(DiffSearchResult::Component {
143                        name: comp.name.clone(),
144                        version: comp.version.clone(),
145                        change_type: ChangeType::Added, // reuse Added as "present"
146                    });
147                }
148            }
149
150            // Search vulnerabilities
151            for comp in sbom.components.values() {
152                for vuln in &comp.vulnerabilities {
153                    if vuln.id.to_lowercase().contains(&query_lower) {
154                        results.push(DiffSearchResult::Vulnerability {
155                            id: vuln.id.clone(),
156                            component_name: comp.name.clone(),
157                            severity: vuln.severity.as_ref().map(|s| format!("{s:?}")),
158                            change_type: VulnChangeType::Introduced, // reuse as "present"
159                        });
160                    }
161                }
162            }
163
164            // Search licenses
165            for comp in sbom.components.values() {
166                for lic in &comp.licenses.declared {
167                    if lic.expression.to_lowercase().contains(&query_lower) {
168                        results.push(DiffSearchResult::License {
169                            license: lic.expression.clone(),
170                            component_name: comp.name.clone(),
171                            change_type: ChangeType::Added, // reuse as "present"
172                        });
173                    }
174                }
175            }
176        }
177
178        // Limit results
179        results.truncate(50);
180        self.overlays.search.results = results;
181        self.overlays.search.selected = 0;
182    }
183
184    /// Jump to the currently selected search result
185    pub fn jump_to_search_result(&mut self) {
186        if let Some(result) = self
187            .overlays
188            .search
189            .results
190            .get(self.overlays.search.selected)
191            .cloned()
192        {
193            match result {
194                DiffSearchResult::Component {
195                    name,
196                    version,
197                    change_type,
198                    ..
199                } => {
200                    // Prefer matching by change type + version when possible
201                    if let Some(index) =
202                        self.find_component_index_all(&name, Some(change_type), version.as_deref())
203                    {
204                        self.tabs.components.filter = ComponentFilter::All;
205                        self.tabs.components.selected = index;
206                        self.select_tab(TabKind::Components);
207                        self.stop_search();
208                        return;
209                    }
210
211                    // Fall back to name-only match across all components
212                    if let Some(index) = self.find_component_index_all(&name, None, None) {
213                        self.tabs.components.filter = ComponentFilter::All;
214                        self.tabs.components.selected = index;
215                        self.select_tab(TabKind::Components);
216                        self.stop_search();
217                        return;
218                    }
219
220                    self.tabs.components.filter = ComponentFilter::All;
221                    self.select_tab(TabKind::Components);
222                }
223                DiffSearchResult::Vulnerability {
224                    id, change_type, ..
225                } => {
226                    // Align filter/sort so the selection is stable
227                    self.tabs.vulnerabilities.sort_by = VulnSort::Id;
228                    self.tabs.vulnerabilities.filter = match change_type {
229                        VulnChangeType::Introduced => VulnFilter::Introduced,
230                        VulnChangeType::Resolved => VulnFilter::Resolved,
231                    };
232
233                    if let Some(index) = self.find_vulnerability_index(&id) {
234                        self.tabs.vulnerabilities.selected = index;
235                    }
236
237                    self.select_tab(TabKind::Vulnerabilities);
238                }
239                DiffSearchResult::License { license, .. } => {
240                    // Find the license index
241                    if let Some(ref diff) = self.data.diff_result {
242                        let mut index = 0;
243
244                        // Search new licenses first
245                        for lic in &diff.licenses.new_licenses {
246                            if lic.license == license {
247                                self.tabs.licenses.selected = index;
248                                self.select_tab(TabKind::Licenses);
249                                self.stop_search();
250                                return;
251                            }
252                            index += 1;
253                        }
254
255                        // Then removed licenses
256                        for lic in &diff.licenses.removed_licenses {
257                            if lic.license == license {
258                                self.tabs.licenses.selected = index;
259                                self.select_tab(TabKind::Licenses);
260                                self.stop_search();
261                                return;
262                            }
263                            index += 1;
264                        }
265                    }
266                    self.select_tab(TabKind::Licenses);
267                }
268            }
269            self.stop_search();
270        }
271    }
272}