From 4e9604ffdc3611f668a1d56b8220b8e020fee195 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:54:29 +0800 Subject: [PATCH 1/2] fix(terminal): accept ASCII keystrokes in the Ctrl+R reverse-search prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reverse-search query only took text from the IME commit path (input_text -> push_query), so a CJK input source could type into it but a plain ASCII source — and Linux, where prefers_ime_for_printable_keys is false — delivered printable keys via on_key_down, where handle_key ignored them. Ctrl+R opened, but the search box swallowed every keystroke. Forward printable key_char events from handle_reverse_search_key into push_query, mirroring the editor's existing key_char path; control/Cmd/Alt chords and non-printable keys still fall through to the control-key handling. Fixes ASCII typing on macOS and all typing on Linux. Reported on V2EX. Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF --- src/terminal/reverse_search.rs | 5 ++++- src/terminal/view.rs | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/terminal/reverse_search.rs b/src/terminal/reverse_search.rs index 163c10b7..80b245dc 100644 --- a/src/terminal/reverse_search.rs +++ b/src/terminal/reverse_search.rs @@ -65,7 +65,10 @@ impl ReverseSearch { } } - /// Append typed text (arriving via the IME path) to the query and re-search. + /// Append typed text to the query and re-search. Text arrives either via the + /// IME path (`replace_text_in_range` → the view's `input_text`) or, for a plain + /// ASCII input source, as a direct `key_char` the view forwards from + /// `handle_reverse_search_key`. pub(super) fn push_query(&mut self, text: &str, history: &[String]) { self.query.push_str(text); self.update(history, false); diff --git a/src/terminal/view.rs b/src/terminal/view.rs index f84d26f7..25abf8c8 100644 --- a/src/terminal/view.rs +++ b/src/terminal/view.rs @@ -1980,6 +1980,26 @@ impl TerminalView { /// query/match logic (`reverse_search` module); the view just applies the /// resulting [`reverse_search::Action`] and repaints. fn handle_reverse_search_key(&mut self, ks: &gpui::Keystroke, cx: &mut Context) { + // Printable text typed into the query. A CJK input source routes it through + // the IME (`input_text` → `push_query`), but a plain ASCII input source — + // and Linux, where `prefers_ime_for_printable_keys` is false — delivers it + // here as an ordinary key event carrying `key_char`. Without this the search + // field can only be typed into via an IME: Ctrl+R opens, but ASCII + // keystrokes vanish. Mirror the editor's `key_char` path (`handle_editor_key`); + // control / Cmd / Alt chords and non-printable keys (Enter/Backspace/Esc have + // no printable `key_char`) fall through to the control-key handling below. + let m = &ks.modifiers; + if !m.control && !m.platform && !m.alt { + if let Some(ch) = ks.key_char.as_deref() { + if !ch.is_empty() && ch.chars().all(|c| c >= '\u{20}' && c != '\u{7f}') { + if let Some(rs) = self.reverse_search.as_mut() { + rs.push_query(ch, &self.history); + } + cx.notify(); + return; + } + } + } let Some(rs) = self.reverse_search.as_mut() else { return; }; From 03fda9c33d5cf7cbda6b215b0e15327c8eab2f79 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:55:21 +0800 Subject: [PATCH 2/2] feat(menu): show shortcut hints in the right-click menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy / Paste / Select All / Find are dispatched inline in `handle_cmd_shortcut` with no registered `KeyBinding`, so gpui-component's PopupMenu couldn't auto-derive their hints the way it does for the keybinding-backed items (Clear, Split, Maximize, New Tab, Close Pane) — the menu showed shortcuts on some rows and nothing on others. Render the hints ourselves via `menu_row_with_hint`, styled to match the auto-rendered ones, instead of registering real bindings (which would risk the Ctrl+C SIGINT fall-through on Windows/Linux). ⌘A / ⌘F are macOS-only, so those hints are gated behind `mac_only`. Also fix the "Select All" menu item diverging from ⌘A: both now go through `select_all_contextual` (select the edited command line at the prompt, otherwise the whole terminal buffer) so click and shortcut stay in sync. --- src/terminal/view.rs | 96 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 13 deletions(-) diff --git a/src/terminal/view.rs b/src/terminal/view.rs index 25abf8c8..3d1a144a 100644 --- a/src/terminal/view.rs +++ b/src/terminal/view.rs @@ -11,8 +11,9 @@ use gpui::{ KeyDownEvent, Modifiers, MouseButton, MouseDownEvent, Pixels, ScrollDelta, ScrollWheelEvent, Window, actions, div, prelude::*, px, }; +use gpui_component::kbd::Kbd; use gpui_component::menu::ContextMenuExt; -use gpui_component::{ActiveTheme as _, Icon, IconName, Size}; +use gpui_component::{ActiveTheme as _, Icon, IconName, Size, h_flex}; use super::TermSize; use super::cmd_editor::CmdEditor; @@ -910,12 +911,7 @@ impl TerminalView { "a" => { // At the prompt, ⌘A selects the whole edited line; otherwise it // selects the whole terminal buffer (scrollback included). - if self.input_active() { - self.cmd.select_all(); - cx.notify(); - } else { - self.select_all(cx); - } + self.select_all_contextual(cx); CmdKey::Consumed } // The following are editor-only (macOS line editing); they're swallowed @@ -1302,7 +1298,7 @@ impl TerminalView { } /// Select the entire buffer — from the top of scrollback to the last cell — - /// so Cmd+A then Cmd+C copies everything. Also used by the right-click menu. + /// so Cmd+A then Cmd+C copies everything. pub fn select_all(&mut self, cx: &mut Context) { let mut term = self.terminal.term.lock(); let grid = term.grid(); @@ -1315,6 +1311,19 @@ impl TerminalView { cx.notify(); } + /// "Select All" as the user means it in context: at the prompt, select the + /// edited command line; otherwise select the whole terminal buffer. Shared by + /// the ⌘A shortcut and the right-click "Select All" item so the two never + /// drift apart. + pub fn select_all_contextual(&mut self, cx: &mut Context) { + if self.input_active() { + self.cmd.select_all(); + cx.notify(); + } else { + self.select_all(cx); + } + } + /// Paste clipboard text. While idle at the prompt it goes into the local /// command editor (a single trailing newline is dropped so a copied line /// doesn't auto-submit). Otherwise it's written to the PTY, wrapped in @@ -2894,7 +2903,7 @@ impl Render for TerminalView { // same menu fall through to `Tty7App`. .on_action(cx.listener(|this, _: &CopyText, _w, cx| this.copy_selection(cx))) .on_action(cx.listener(|this, _: &PasteText, _w, cx| this.paste_from_clipboard(cx))) - .on_action(cx.listener(|this, _: &SelectAll, _w, cx| this.select_all(cx))) + .on_action(cx.listener(|this, _: &SelectAll, _w, cx| this.select_all_contextual(cx))) .on_action( cx.listener(|this, _: &FindInTerminal, window, cx| this.open_search(window, cx)), ) @@ -2932,14 +2941,33 @@ impl Render for TerminalView { // Small size = tighter 20px rows; the default 26px felt too airy. // A fixed min-width keeps the menu a consistent, intentional size // instead of hugging the longest label (which reads ragged). + // Copy/Paste/Select All/Find are dispatched inline (see + // `handle_cmd_shortcut`) with no registered `KeyBinding`, so the menu + // can't auto-derive their hints the way it does for the items below. + // We render the hint ourselves via `menu_row_with_hint` to keep the + // whole menu consistent, rather than register real bindings (which + // would risk the Ctrl+C SIGINT fall-through on Windows/Linux). menu.with_size(Size::Small) .min_w(px(220.)) .action_context(menu_focus.clone()) - .menu_with_disabled("Copy", Box::new(CopyText), !has_selection) - .menu("Paste", Box::new(PasteText)) - .menu("Select All", Box::new(SelectAll)) + .menu_element_with_disabled( + Box::new(CopyText), + !has_selection, + menu_row_with_hint("Copy", Some("secondary-c")), + ) + .menu_element( + Box::new(PasteText), + menu_row_with_hint("Paste", Some("secondary-v")), + ) + .menu_element( + Box::new(SelectAll), + menu_row_with_hint("Select All", mac_only("secondary-a")), + ) .separator() - .menu("Find…", Box::new(FindInTerminal)) + .menu_element( + Box::new(FindInTerminal), + menu_row_with_hint("Find…", mac_only("secondary-f")), + ) .menu("Clear", Box::new(ClearScrollback)) .separator() .menu("Split Right", Box::new(SplitRight)) @@ -2952,6 +2980,48 @@ impl Render for TerminalView { } } +/// Build a context-menu row that shows its shortcut right-aligned, matching the +/// hint gpui-component auto-renders for items whose action has a registered +/// keybinding. `key` is `None` when the action has no shortcut on this platform, +/// leaving the row hint-less like a plain item. +fn menu_row_with_hint( + label: &'static str, + key: Option<&'static str>, +) -> impl Fn(&mut Window, &mut App) -> gpui::AnyElement { + move |_window, _cx| { + let hint = key.map(|k| { + // Strip Kbd's keycap box (filled bg + border) so it reads as the same + // quiet muted-foreground hint the auto-rendered items show — see + // gpui-component's `PopupMenu::render_key_binding`. + Kbd::new(gpui::Keystroke::parse(k).expect("valid static keystroke")) + .p_0() + .flex_nowrap() + .border_0() + .bg(gpui::transparent_white()) + }); + h_flex() + .w_full() + .gap_3() + .items_center() + .justify_between() + .child(label) + .children(hint) + .into_any_element() + } +} + +/// `Some(key)` on macOS, `None` elsewhere. ⌘A (Select All) and ⌘F (Find) are +/// wired only on macOS; on Windows/Linux those chords keep their readline meaning +/// (line-start / forward-char), so the menu must not advertise them there. +#[cfg(target_os = "macos")] +fn mac_only(key: &'static str) -> Option<&'static str> { + Some(key) +} +#[cfg(not(target_os = "macos"))] +fn mac_only(_key: &'static str) -> Option<&'static str> { + None +} + /// Approximate terminal display width of a char in cells: 2 for East-Asian /// wide / fullwidth glyphs and most emoji, 1 otherwise. Mirrors how the grid /// (alacritty) lays out wide characters, so the editor's per-char cells and