fix: miller filter when previous item is out of bounds
4 files changed, +102 -41
+21-21src/views/browser.rs
| @@ -308,32 +308,26 @@ impl BrowserView { | ||
| 308 | 308 | crate::widgets::miller::pane_title(ui, &mid_title); |
| 309 | 309 | let filter_resp = self.filter.show(ui); |
| 310 | 310 | |
| 311 | - // Filter entries: (original_index, &Entry) | |
| 312 | - let filtered: Vec<(usize, &Entry)> = current_entries.iter() | |
| 313 | - .enumerate() | |
| 314 | - .filter(|(_, e)| self.filter.matches(&e.label)) | |
| 311 | + // Filter + snap selection | |
| 312 | + let fr = self.filter.apply( | |
| 313 | + current_entries.iter().map(|e| &e.label), self.selection, | |
| 314 | + ); | |
| 315 | + self.selection = fr.selection; | |
| 316 | + let filtered: Vec<(usize, &Entry)> = fr.indices.iter() | |
| 317 | + .map(|&i| (i, ¤t_entries[i])) | |
| 315 | 318 | .collect(); |
| 316 | 319 | |
| 317 | 320 | // Ctrl-N/P navigate filtered entries, Enter accepts |
| 318 | 321 | if !filtered.is_empty() { |
| 319 | 322 | if filter_resp.next { |
| 320 | - // Find next filtered entry after current selection | |
| 321 | - let next = filtered.iter() | |
| 322 | - .find(|(orig, _)| *orig > self.selection) | |
| 323 | - .or(filtered.first()) | |
| 324 | - .map(|(orig, _)| *orig); | |
| 325 | - if let Some(idx) = next { | |
| 326 | - self.selection = idx; | |
| 327 | - } | |
| 323 | + let cur_pos = fr.filtered_pos; | |
| 324 | + let next_pos = (cur_pos + 1).min(filtered.len() - 1); | |
| 325 | + self.selection = filtered[next_pos].0; | |
| 328 | 326 | } |
| 329 | 327 | if filter_resp.prev { |
| 330 | - let prev = filtered.iter().rev() | |
| 331 | - .find(|(orig, _)| *orig < self.selection) | |
| 332 | - .or(filtered.last()) | |
| 333 | - .map(|(orig, _)| *orig); | |
| 334 | - if let Some(idx) = prev { | |
| 335 | - self.selection = idx; | |
| 336 | - } | |
| 328 | + let cur_pos = fr.filtered_pos; | |
| 329 | + let prev_pos = cur_pos.saturating_sub(1); | |
| 330 | + self.selection = filtered[prev_pos].0; | |
| 337 | 331 | } |
| 338 | 332 | if filter_resp.accept { |
| 339 | 333 | if let Some(entry) = current_entries.get(self.selection) { |
| @@ -578,7 +572,10 @@ impl BrowserView { | ||
| 578 | 572 | let mut clicked = None; |
| 579 | 573 | let mut dbl_clicked: Option<usize> = None; |
| 580 | 574 | |
| 581 | - self.current_list.selection = self.selection; | |
| 575 | + // Map original selection index → filtered list position | |
| 576 | + let filtered_pos = entries.iter().position(|(orig, _)| *orig == self.selection) | |
| 577 | + .unwrap_or(0); | |
| 578 | + self.current_list.selection = filtered_pos; | |
| 582 | 579 | self.current_list.show( |
| 583 | 580 | ui, |
| 584 | 581 | "browser_current", |
| @@ -704,7 +701,10 @@ impl BrowserView { | ||
| 704 | 701 | }, |
| 705 | 702 | ); |
| 706 | 703 | |
| 707 | - self.selection = self.current_list.selection; | |
| 704 | + // Map filtered position back to original index | |
| 705 | + if let Some((orig, _)) = entries.get(self.current_list.selection) { | |
| 706 | + self.selection = *orig; | |
| 707 | + } | |
| 708 | 708 | (clicked, dbl_clicked) |
| 709 | 709 | } |
| 710 | 710 |
+18-5src/widgets/jq_bar.rs
| @@ -127,9 +127,13 @@ impl JqBar { | ||
| 127 | 127 | let count = self.completions.len(); |
| 128 | 128 | let mut selected: Option<usize> = None; |
| 129 | 129 | if cycle_down { |
| 130 | - self.comp_list.down(count, &mut |idx| { selected = Some(idx); }); | |
| 130 | + self.comp_list.down(count, &mut |idx| { | |
| 131 | + selected = Some(idx); | |
| 132 | + }); | |
| 131 | 133 | } else if cycle_up { |
| 132 | - self.comp_list.up(&mut |idx| { selected = Some(idx); }); | |
| 134 | + self.comp_list.up(&mut |idx| { | |
| 135 | + selected = Some(idx); | |
| 136 | + }); | |
| 133 | 137 | } |
| 134 | 138 | if let Some(idx) = selected { |
| 135 | 139 | let comp = self.completions[idx].clone(); |
| @@ -140,7 +144,8 @@ impl JqBar { | ||
| 140 | 144 | |
| 141 | 145 | // Enter/Tab: accept current selection |
| 142 | 146 | if enter || tab { |
| 143 | - accepted_completion = Some(self.completions[self.comp_list.selection].clone()); | |
| 147 | + accepted_completion = | |
| 148 | + Some(self.completions[self.comp_list.selection].clone()); | |
| 144 | 149 | } |
| 145 | 150 | } else if enter { |
| 146 | 151 | response.run = true; |
| @@ -244,7 +249,11 @@ impl JqBar { | ||
| 244 | 249 | .into_iter() |
| 245 | 250 | .filter_map(|path| { |
| 246 | 251 | let score = fuzzy_score(&needle_lower, &path.to_lowercase()); |
| 247 | - if score > 0 { Some((score, path)) } else { None } | |
| 252 | + if score > 0 { | |
| 253 | + Some((score, path)) | |
| 254 | + } else { | |
| 255 | + None | |
| 256 | + } | |
| 248 | 257 | }) |
| 249 | 258 | .collect(); |
| 250 | 259 | scored.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1))); |
| @@ -358,7 +367,11 @@ fn fuzzy_score(needle: &str, haystack: &str) -> i64 { | ||
| 358 | 367 | ni += 1; |
| 359 | 368 | } |
| 360 | 369 | } |
| 361 | - if ni == needle_chars.len() { score } else { 0 } | |
| 370 | + if ni == needle_chars.len() { | |
| 371 | + score | |
| 372 | + } else { | |
| 373 | + 0 | |
| 374 | + } | |
| 362 | 375 | } |
| 363 | 376 | |
| 364 | 377 | pub fn extract_current_segment(query: &str) -> &str { |
+51-0src/widgets/miller.rs
| @@ -201,6 +201,57 @@ impl MillerFilter { | ||
| 201 | 201 | } |
| 202 | 202 | fuzzy_matches(&self.query, label) |
| 203 | 203 | } |
| 204 | + | |
| 205 | + /// Filter a list of labels and manage selection mapping. | |
| 206 | + /// | |
| 207 | + /// Takes the original entries' labels and the current selection (original index). | |
| 208 | + /// Returns `FilteredResult` with filtered indices, snapped selection, and the | |
| 209 | + /// position within the filtered list for rendering. | |
| 210 | + pub fn apply( | |
| 211 | + &self, | |
| 212 | + labels: impl Iterator<Item = impl AsRef<str>>, | |
| 213 | + selection: usize, | |
| 214 | + ) -> FilteredResult { | |
| 215 | + let filtered_indices: Vec<usize> = labels | |
| 216 | + .enumerate() | |
| 217 | + .filter(|(_, label)| self.matches(label.as_ref())) | |
| 218 | + .map(|(i, _)| i) | |
| 219 | + .collect(); | |
| 220 | + | |
| 221 | + let selection = self.snap_selection(selection, &filtered_indices); | |
| 222 | + let filtered_pos = filtered_indices.iter() | |
| 223 | + .position(|&orig| orig == selection) | |
| 224 | + .unwrap_or(0); | |
| 225 | + | |
| 226 | + FilteredResult { | |
| 227 | + indices: filtered_indices, | |
| 228 | + selection, | |
| 229 | + filtered_pos, | |
| 230 | + } | |
| 231 | + } | |
| 232 | + | |
| 233 | + fn snap_selection(&self, selection: usize, filtered_indices: &[usize]) -> usize { | |
| 234 | + if filtered_indices.is_empty() { | |
| 235 | + return selection; | |
| 236 | + } | |
| 237 | + if filtered_indices.contains(&selection) { | |
| 238 | + return selection; | |
| 239 | + } | |
| 240 | + if let Some(&idx) = filtered_indices.iter().find(|&&orig| orig >= selection) { | |
| 241 | + return idx; | |
| 242 | + } | |
| 243 | + filtered_indices[0] | |
| 244 | + } | |
| 245 | +} | |
| 246 | + | |
| 247 | +/// Result of applying a filter to entries. | |
| 248 | +pub struct FilteredResult { | |
| 249 | + /// Original indices that passed the filter. | |
| 250 | + pub indices: Vec<usize>, | |
| 251 | + /// The snapped selection (original index). | |
| 252 | + pub selection: usize, | |
| 253 | + /// Position of the selection within the filtered list (for rendering). | |
| 254 | + pub filtered_pos: usize, | |
| 204 | 255 | } |
| 205 | 256 | |
| 206 | 257 | /// Simple fuzzy match: all chars in needle appear in order in haystack (case-insensitive). |