mirror of
https://github.com/openai/codex.git
synced 2026-10-15 16:48:52 +00:00
scrollable slash menu, esc to exit
This commit is contained in:
@@ -43,6 +43,10 @@ pub(crate) struct ChatComposer<'a> {
|
||||
ctrl_c_quit_hint: bool,
|
||||
use_shift_enter_hint: bool,
|
||||
dismissed_file_popup_token: Option<String>,
|
||||
// When set, suppress reopening the slash-command popup while the first-line
|
||||
// '/token' remains equal to this value. Cleared when the first line no longer
|
||||
// starts with '/'.
|
||||
dismissed_command_popup_token: Option<String>,
|
||||
current_file_query: Option<String>,
|
||||
pending_pastes: Vec<(String, String)>,
|
||||
}
|
||||
@@ -74,6 +78,7 @@ impl ChatComposer<'_> {
|
||||
ctrl_c_quit_hint: false,
|
||||
use_shift_enter_hint,
|
||||
dismissed_file_popup_token: None,
|
||||
dismissed_command_popup_token: None,
|
||||
current_file_query: None,
|
||||
pending_pastes: Vec::new(),
|
||||
};
|
||||
@@ -216,6 +221,23 @@ impl ChatComposer<'_> {
|
||||
popup.move_down();
|
||||
(InputResult::None, true)
|
||||
}
|
||||
Input { key: Key::Esc, .. } => {
|
||||
// Hide popup without modifying text. Remember current '/token' so
|
||||
// we don't immediately reopen until the token changes.
|
||||
let first_line = self
|
||||
.textarea
|
||||
.lines()
|
||||
.first()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("");
|
||||
if let Some(stripped) = first_line.strip_prefix('/') {
|
||||
let token = stripped.trim_start();
|
||||
let cmd_token = token.split_whitespace().next().unwrap_or("");
|
||||
self.dismissed_command_popup_token = Some(cmd_token.to_string());
|
||||
}
|
||||
self.active_popup = ActivePopup::None;
|
||||
(InputResult::None, true)
|
||||
}
|
||||
Input { key: Key::Tab, .. } => {
|
||||
if let Some(cmd) = popup.selected_command() {
|
||||
let first_line = self
|
||||
@@ -590,21 +612,42 @@ impl ChatComposer<'_> {
|
||||
.unwrap_or("");
|
||||
|
||||
let input_starts_with_slash = first_line.starts_with('/');
|
||||
// Determine the current '/token' on the first line (may be empty).
|
||||
let current_cmd_token: Option<String> = if input_starts_with_slash {
|
||||
if let Some(stripped) = first_line.strip_prefix('/') {
|
||||
let token = stripped.trim_start();
|
||||
Some(token.split_whitespace().next().unwrap_or("").to_string())
|
||||
} else {
|
||||
Some(String::new())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match &mut self.active_popup {
|
||||
ActivePopup::Command(popup) => {
|
||||
if input_starts_with_slash {
|
||||
popup.on_composer_text_change(first_line.to_string());
|
||||
} else {
|
||||
self.active_popup = ActivePopup::None;
|
||||
self.dismissed_command_popup_token = None;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if input_starts_with_slash {
|
||||
// Avoid reopening immediately after user pressed Esc until
|
||||
// the '/token' changes.
|
||||
if self
|
||||
.dismissed_command_popup_token
|
||||
.as_ref()
|
||||
.is_some_and(|t| Some(t) == current_cmd_token.as_ref())
|
||||
{
|
||||
return;
|
||||
}
|
||||
let mut command_popup = CommandPopup::new();
|
||||
command_popup.on_composer_text_change(first_line.to_string());
|
||||
self.active_popup = ActivePopup::Command(command_popup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,10 +123,13 @@ impl CommandPopup {
|
||||
Some(idx) if idx + 1 < matches_len => {
|
||||
self.selected_idx = Some(idx + 1);
|
||||
}
|
||||
Some(_idx_last) => {
|
||||
// Wrap around when moving down from the last item.
|
||||
self.selected_idx = Some(0);
|
||||
}
|
||||
None => {
|
||||
self.selected_idx = Some(0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,10 +145,21 @@ impl WidgetRef for CommandPopup {
|
||||
let matches = self.filtered_commands();
|
||||
|
||||
let mut rows: Vec<Row> = Vec::new();
|
||||
let visible_matches: Vec<&SlashCommand> =
|
||||
matches.into_iter().take(MAX_POPUP_ROWS).collect();
|
||||
|
||||
if visible_matches.is_empty() {
|
||||
let start_idx = match (self.selected_idx, matches.len()) {
|
||||
(Some(sel), len) if len > 0 => sel.saturating_sub(MAX_POPUP_ROWS.saturating_sub(1)),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let enumerated_visible = matches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(start_idx)
|
||||
.take(MAX_POPUP_ROWS)
|
||||
.map(|(i, cmd)| (i, *cmd))
|
||||
.collect::<Vec<(usize, &SlashCommand)>>();
|
||||
|
||||
if enumerated_visible.is_empty() {
|
||||
rows.push(Row::new(vec![
|
||||
Cell::from(""),
|
||||
Cell::from("No matching commands").add_modifier(Modifier::ITALIC),
|
||||
@@ -153,10 +167,10 @@ impl WidgetRef for CommandPopup {
|
||||
} else {
|
||||
let default_style = Style::default();
|
||||
let command_style = Style::default().fg(Color::LightBlue);
|
||||
for (idx, cmd) in visible_matches.iter().enumerate() {
|
||||
for (global_idx, cmd) in enumerated_visible.into_iter() {
|
||||
rows.push(Row::new(vec![
|
||||
Cell::from(Line::from(vec![
|
||||
if Some(idx) == self.selected_idx {
|
||||
if Some(global_idx) == self.selected_idx {
|
||||
Span::styled(
|
||||
"›",
|
||||
Style::default().bg(Color::DarkGray).fg(Color::LightCyan),
|
||||
|
||||
Reference in New Issue
Block a user