mirror of
https://github.com/openai/codex.git
synced 2026-06-02 11:22:01 +00:00
Reset slash popup selection when filter changes (#25492)
## Summary Fixes #25295. The slash-command popup reused its previous `ScrollState` when the composer filter token changed. After scrolling the full `/` command list, typing a narrower filter such as `/st` could clamp the stale selection into the filtered results and highlight the wrong command. This resets the popup selection and viewport only when the parsed filter token changes, so normal arrow navigation is preserved while new filters start at the first match.
This commit is contained in:
@@ -98,6 +98,7 @@ impl CommandPopup {
|
||||
/// to narrow down the list of available commands.
|
||||
pub(crate) fn on_composer_text_change(&mut self, text: String) {
|
||||
let first_line = text.lines().next().unwrap_or("");
|
||||
let previous_filter = self.command_filter.clone();
|
||||
|
||||
if let Some(stripped) = first_line.strip_prefix('/') {
|
||||
// Extract the *first* token (sequence of non-whitespace
|
||||
@@ -116,6 +117,10 @@ impl CommandPopup {
|
||||
self.command_filter.clear();
|
||||
}
|
||||
|
||||
if self.command_filter != previous_filter {
|
||||
self.state.reset();
|
||||
}
|
||||
|
||||
// Reset or clamp selected index based on new filtered list.
|
||||
let matches_len = self.filtered_items().len();
|
||||
self.state.clamp_selection(matches_len);
|
||||
@@ -407,6 +412,38 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_filter_resets_selection_after_scrolling() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default(), Vec::new());
|
||||
popup.on_composer_text_change("/".to_string());
|
||||
|
||||
for _ in 0..MAX_POPUP_ROWS {
|
||||
popup.move_down();
|
||||
}
|
||||
assert!(popup.state.scroll_top > 0);
|
||||
|
||||
popup.on_composer_text_change("/st".to_string());
|
||||
|
||||
assert_eq!(
|
||||
popup.selected_item(),
|
||||
Some(CommandItem::Builtin(SlashCommand::Status))
|
||||
);
|
||||
assert_eq!(popup.state.scroll_top, 0);
|
||||
let width = 72;
|
||||
let area = Rect::new(
|
||||
/*x*/ 0,
|
||||
/*y*/ 0,
|
||||
width,
|
||||
popup.calculate_required_height(width),
|
||||
);
|
||||
let mut buf = Buffer::empty(area);
|
||||
popup.render_ref(area, &mut buf);
|
||||
insta::assert_snapshot!(
|
||||
"command_popup_filter_reset_after_scroll",
|
||||
format!("{buf:?}")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quit_hidden_in_empty_filter_but_shown_for_prefix() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default(), Vec::new());
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/command_popup.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 72, height: 3 },
|
||||
content: [
|
||||
" /status show current session configuration and token usage ",
|
||||
" /statusline configure which items appear in the status line ",
|
||||
" /stop stop all background terminals ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 65, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 3, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 5, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 15, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 62, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 3, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 5, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 15, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 44, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user