diff --git a/wezterm-gui/src/overlay/quickselect.rs b/wezterm-gui/src/overlay/quickselect.rs index 0b1de1bad..ce3e7ce16 100644 --- a/wezterm-gui/src/overlay/quickselect.rs +++ b/wezterm-gui/src/overlay/quickselect.rs @@ -125,6 +125,11 @@ fn compute_labels_for_alphabet_impl( .collect() } +/// Returns true if a label should be displayed given a selection prefix. +fn label_matches_selection(label: &str, lowered_prefix: &str) -> bool { + lowered_prefix.is_empty() || label.starts_with(lowered_prefix) +} + #[cfg(test)] mod alphabet_test { use super::*; @@ -193,6 +198,62 @@ mod alphabet_test { } } +#[cfg(test)] +mod label_filter_test { + use super::*; + + #[test] + fn empty_prefix_matches_all() { + let labels = ["a", "fq", "db", "av", "ac"]; + assert!(labels.iter().all(|l| label_matches_selection(l, ""))); + } + + #[test] + fn single_char_prefix_filters_non_matching() { + // Pressing 'a' should keep labels starting with 'a' and remove others + let labels = ["ai", "fq", "db", "av", "ac"]; + let visible: Vec<&str> = labels + .iter() + .copied() + .filter(|l| label_matches_selection(l, "a")) + .collect(); + assert_eq!(visible, ["ai", "av", "ac"]); + } + + #[test] + fn full_prefix_matches_exact_label() { + assert!(label_matches_selection("ai", "ai")); + assert!(!label_matches_selection("ac", "ai")); + } + + #[test] + fn prefix_longer_than_label() { + assert!(!label_matches_selection("a", "ab")); + assert!(!label_matches_selection("", "a")); + } + + #[test] + fn two_char_labels_narrowed_by_first_char() { + // With more matches than alphabet, two-char labels are generated + let labels = compute_labels_for_alphabet("abcd", 6); + assert_eq!(labels, ["a", "b", "c", "da", "db", "dc"]); + + // Typing 'd' should keep "da", "db", "dc" and remove "a", "b", "c" + let visible: Vec<&str> = labels + .iter() + .map(String::as_str) + .filter(|l| label_matches_selection(l, "d")) + .collect(); + assert_eq!(visible, ["da", "db", "dc"]); + } + + #[test] + fn no_labels_match_unknown_prefix() { + let labels = ["ai", "fq", "db"]; + assert!(!labels.iter().any(|l| label_matches_selection(l, "z"))); + } +} + pub struct QuickSelectOverlay { renderer: Mutex, delegate: Arc, @@ -420,17 +481,21 @@ impl Pane for QuickSelectOverlay { if let Some(result_index) = r.by_label.get(&lowered).cloned() { r.select_and_copy_match_number(result_index, paste); r.close(); + } else { + r.recompute_results(); } } (KeyCode::Backspace, KeyModifiers::NONE) => { // Backspace to edit the selection let mut r = self.renderer.lock(); r.selection.pop(); + r.recompute_results(); } (KeyCode::Char('u'), KeyModifiers::CTRL) => { // CTRL-u to clear the selection let mut r = self.renderer.lock(); r.selection.clear(); + r.recompute_results(); } _ => {} } @@ -553,6 +618,7 @@ impl Pane for QuickSelectOverlay { // the search UI. // For rows with search results, we want to highlight the matching ranges + let lowered_prefix = self.renderer.selection.to_lowercase(); for (idx, line) in lines.iter_mut().enumerate() { let mut line: Line = line.clone(); if disable_attr { @@ -585,6 +651,10 @@ impl Pane for QuickSelectOverlay { line.clear_appdata(); } else if let Some(matches) = self.renderer.by_line.get(&stable_idx) { for m in matches { + if !label_matches_selection(&m.label, &lowered_prefix) { + // Skip displaying this label, it doesn't match the current filter. + continue; + } // highlight for cell_idx in m.range.clone() { if let Some(cell) =