irongit

fix: miller filter when previous item is out of bounds

huncholanehuncholaneauthored
parent bcf9523commit 0adc2b60f2fae197a5a24f27a1178cc15218ae90Browse files

4 files changed, +102 -41

+21-21src/views/browser.rs
@@ -308,32 +308,26 @@ impl BrowserView {
308308 crate::widgets::miller::pane_title(ui, &mid_title);
309309 let filter_resp = self.filter.show(ui);
310310
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, &current_entries[i]))
315318 .collect();
316319
317320 // Ctrl-N/P navigate filtered entries, Enter accepts
318321 if !filtered.is_empty() {
319322 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;
328326 }
329327 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;
337331 }
338332 if filter_resp.accept {
339333 if let Some(entry) = current_entries.get(self.selection) {
@@ -578,7 +572,10 @@ impl BrowserView {
578572 let mut clicked = None;
579573 let mut dbl_clicked: Option<usize> = None;
580574
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;
582579 self.current_list.show(
583580 ui,
584581 "browser_current",
@@ -704,7 +701,10 @@ impl BrowserView {
704701 },
705702 );
706703
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+ }
708708 (clicked, dbl_clicked)
709709 }
710710
+12-15src/views/shared_browser.rs
@@ -227,36 +227,33 @@ impl SharedBrowserView {
227227 render_pane_title(ui, &mid_title);
228228 let filter_resp = self.filter.show(ui);
229229
230- let filtered: Vec<(usize, &Entry)> = current_entries.iter()
231- .enumerate()
232- .filter(|(_, e)| self.filter.matches(&e.label))
230+ // Filter + snap selection
231+ let fr = self.filter.apply(
232+ current_entries.iter().map(|e| &e.label), self.selection,
233+ );
234+ self.selection = fr.selection;
235+ let filtered: Vec<(usize, &Entry)> = fr.indices.iter()
236+ .map(|&i| (i, &current_entries[i]))
233237 .collect();
234238
235- // Ctrl-N/P navigate filtered entries, Enter sets flag for post-render
236239 if filter_resp.accept {
237240 filter_accepted = true;
238241 }
239242 if !filtered.is_empty() {
240243 if filter_resp.next {
241- let next = filtered.iter()
242- .find(|(orig, _)| *orig > self.selection)
243- .or(filtered.first())
244- .map(|(orig, _)| *orig);
245- if let Some(idx) = next { self.selection = idx; }
244+ let next_pos = (fr.filtered_pos + 1).min(filtered.len() - 1);
245+ self.selection = filtered[next_pos].0;
246246 }
247247 if filter_resp.prev {
248- let prev = filtered.iter().rev()
249- .find(|(orig, _)| *orig < self.selection)
250- .or(filtered.last())
251- .map(|(orig, _)| *orig);
252- if let Some(idx) = prev { self.selection = idx; }
248+ let prev_pos = fr.filtered_pos.saturating_sub(1);
249+ self.selection = filtered[prev_pos].0;
253250 }
254251 }
255252
256253 let (c, d) = render_current_column(
257254 ui,
258255 &filtered,
259- self.selection,
256+ fr.filtered_pos,
260257 self.scroll_to_selection,
261258 col_height,
262259 );
+18-5src/widgets/jq_bar.rs
@@ -127,9 +127,13 @@ impl JqBar {
127127 let count = self.completions.len();
128128 let mut selected: Option<usize> = None;
129129 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+ });
131133 } 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+ });
133137 }
134138 if let Some(idx) = selected {
135139 let comp = self.completions[idx].clone();
@@ -140,7 +144,8 @@ impl JqBar {
140144
141145 // Enter/Tab: accept current selection
142146 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());
144149 }
145150 } else if enter {
146151 response.run = true;
@@ -244,7 +249,11 @@ impl JqBar {
244249 .into_iter()
245250 .filter_map(|path| {
246251 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+ }
248257 })
249258 .collect();
250259 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 {
358367 ni += 1;
359368 }
360369 }
361- if ni == needle_chars.len() { score } else { 0 }
370+ if ni == needle_chars.len() {
371+ score
372+ } else {
373+ 0
374+ }
362375 }
363376
364377 pub fn extract_current_segment(query: &str) -> &str {
+51-0src/widgets/miller.rs
@@ -201,6 +201,57 @@ impl MillerFilter {
201201 }
202202 fuzzy_matches(&self.query, label)
203203 }
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,
204255 }
205256
206257 /// Simple fuzzy match: all chars in needle appear in order in haystack (case-insensitive).