diff --git a/docs/reference/keyboard-shortcuts.mdx b/docs/reference/keyboard-shortcuts.mdx
index 495e2445..be91bb37 100644
--- a/docs/reference/keyboard-shortcuts.mdx
+++ b/docs/reference/keyboard-shortcuts.mdx
@@ -53,6 +53,11 @@ it shows what *your* copy is bound to. This is the shipped default.
| Accept ghost suggestion | → | same |
| Completion menu | ⇥ | same |
+On Windows and Linux plain Ctrl V also pastes at a shell prompt.
+Inside a full-screen application — vim, less, tmux — it is passed through as
+the key, so it means what the application says it means (blockwise Visual mode
+in vim); paste there with Ctrl ⇧ V or ⇧ Insert.
+
## Git and SSH
| Action | macOS | Windows / Linux |
diff --git a/src/terminal/view.rs b/src/terminal/view.rs
index f2665f00..53fd74b6 100644
--- a/src/terminal/view.rs
+++ b/src/terminal/view.rs
@@ -1844,10 +1844,13 @@ impl TerminalView {
}
}
+ // Ctrl+Shift+C/V/X are the keymap's alone: rebinding Paste has to
+ // retire Ctrl+Shift+V, which it cannot if this path answers it too.
if cfg!(not(target_os = "macos"))
&& m.control
&& !m.platform
&& !m.alt
+ && !m.shift
&& matches!(ks.key.as_str(), "c" | "v" | "x")
{
match self.handle_cmd_shortcut(ks, window, cx) {
@@ -1983,8 +1986,16 @@ impl TerminalView {
}
}
"v" => {
- self.paste_from_clipboard(cx);
- CmdKey::Consumed
+ // Off macOS Ctrl+V is a control code first: on the alternate
+ // screen the key is the program's (vim's blockwise select), the
+ // way Ctrl+C is SIGINT when there is nothing to copy. Cmd+V
+ // pastes anywhere.
+ if m.control && !m.platform && self.on_alt_screen() {
+ CmdKey::FallThrough
+ } else {
+ self.paste_from_clipboard(cx);
+ CmdKey::Consumed
+ }
}
"a" => {
self.select_all_contextual(cx);
@@ -6139,16 +6150,16 @@ impl Render for TerminalView {
.menu_element_with_disabled(
Box::new(CopyText),
!has_selection,
- menu_row_with_hint(t(L10nKey::AppMenuCopy), Some("secondary-c")),
+ menu_row_with_hint(t(L10nKey::AppMenuCopy), mac_only("secondary-c")),
)
.menu_element_with_disabled(
Box::new(CutText),
!has_selection,
- menu_row_with_hint(t(L10nKey::AppMenuCut), Some("secondary-x")),
+ menu_row_with_hint(t(L10nKey::AppMenuCut), mac_only("secondary-x")),
)
.menu_element(
Box::new(PasteText),
- menu_row_with_hint(t(L10nKey::AppMenuPaste), Some("secondary-v")),
+ menu_row_with_hint(t(L10nKey::AppMenuPaste), mac_only("secondary-v")),
)
.menu_element(
Box::new(SelectAll),
@@ -8355,6 +8366,27 @@ mod gpui_tests {
panic!("the prompt report never reached the view");
}
+ fn alt_screen_ready(
+ window: &gpui::WindowHandle,
+ cx: &mut TestAppContext,
+ daemon: &mut UnixStream,
+ ) {
+ DaemonMsg::Output(b"\x1b[?1049h".to_vec())
+ .encode(daemon)
+ .unwrap();
+ for _ in 0..400 {
+ cx.run_until_parked();
+ if window
+ .update(cx, |view, _, _| view.on_alt_screen())
+ .unwrap()
+ {
+ return;
+ }
+ std::thread::sleep(std::time::Duration::from_millis(5));
+ }
+ panic!("the alternate-screen switch never reached the grid");
+ }
+
#[gpui::test]
fn an_agent_that_has_finished_its_turn_is_not_busy(cx: &mut TestAppContext) {
use crate::core::cli_agent::{AgentSessionState, AgentStatus, CLIAgent};
@@ -9113,6 +9145,34 @@ mod gpui_tests {
);
}
+ #[gpui::test]
+ fn ctrl_v_on_the_alternate_screen_reaches_the_pty_as_syn(cx: &mut TestAppContext) {
+ let (window, mut daemon) = harness(cx);
+ cx.update(|cx| cx.write_to_clipboard(ClipboardItem::new_string("echo hi".into())));
+ alt_screen_ready(&window, cx, &mut daemon);
+ window
+ .update(cx, |view, window, cx| {
+ assert!(!view.input_active(), "the full-screen program owns input");
+ view.on_key_down(
+ &KeyDownEvent {
+ keystroke: key("ctrl-v"),
+ is_held: false,
+ prefer_character_input: false,
+ },
+ window,
+ cx,
+ );
+ })
+ .unwrap();
+
+ assert_eq!(next_input_until_timeout(&mut daemon), Some(vec![0x16]));
+ assert_eq!(
+ next_input_until_timeout(&mut daemon),
+ None,
+ "the clipboard must stay where it is: vim's Ctrl+V is blockwise select, not paste"
+ );
+ }
+
#[gpui::test]
fn shell_vi_mode_prompt_bypasses_the_local_editor(cx: &mut TestAppContext) {
let (window, mut daemon) = harness(cx);
@@ -12068,6 +12128,49 @@ mod gpui_tests {
assert_eq!(text.as_deref(), Some("hello"));
}
+ #[gpui::test]
+ fn ctrl_v_reaches_a_tui_on_the_alternate_screen(cx: &mut TestAppContext) {
+ let (window, mut daemon) = harness(cx);
+ cx.update(|cx| cx.write_to_clipboard(ClipboardItem::new_string("echo hi".into())));
+ alt_screen_ready(&window, cx, &mut daemon);
+
+ window
+ .update(cx, |view, window, cx| {
+ let fell_through = view.handle_cmd_shortcut(&key("ctrl-v"), window, cx);
+ assert!(
+ matches!(fell_through, CmdKey::FallThrough),
+ "a full-screen program owns Ctrl+V"
+ );
+ let pasted = view.handle_cmd_shortcut(&key("cmd-v"), window, cx);
+ assert!(
+ matches!(pasted, CmdKey::Consumed),
+ "Cmd+V is a paste chord on every screen"
+ );
+ })
+ .unwrap();
+ assert_eq!(
+ next_input(&mut daemon),
+ b"echo hi".to_vec(),
+ "only the Cmd+V paste may reach the PTY"
+ );
+ assert_eq!(next_input_until_timeout(&mut daemon), None);
+ }
+
+ #[gpui::test]
+ fn ctrl_v_pastes_off_the_alternate_screen(cx: &mut TestAppContext) {
+ let (window, mut daemon) = harness(cx);
+ cx.update(|cx| cx.write_to_clipboard(ClipboardItem::new_string("echo hi".into())));
+
+ window
+ .update(cx, |view, window, cx| {
+ assert!(!view.on_alt_screen());
+ let consumed = view.handle_cmd_shortcut(&key("ctrl-v"), window, cx);
+ assert!(matches!(consumed, CmdKey::Consumed));
+ })
+ .unwrap();
+ assert_eq!(next_input(&mut daemon), b"echo hi".to_vec());
+ }
+
#[cfg(target_os = "macos")]
#[gpui::test]
fn cmd_backspace_reaches_a_foreground_tui_as_ctrl_u(cx: &mut TestAppContext) {
diff --git a/src/ui/keymap.rs b/src/ui/keymap.rs
index a514fe95..ccc07265 100644
--- a/src/ui/keymap.rs
+++ b/src/ui/keymap.rs
@@ -1328,6 +1328,12 @@ mod tests {
"{key} is a terminal chord and must not paste outside one"
);
}
+ // Plain Ctrl+V is the terminal's, not the keymap's: the pane pastes on
+ // it at a prompt and hands it to a full-screen program otherwise.
+ assert!(
+ dispatched(&effective, "ctrl-v", "Terminal").is_empty(),
+ "ctrl-v is a control code and no default may claim it"
+ );
let rebound = vec![("PasteText".to_string(), "ctrl-alt-v".to_string())];
assert!(
!extra_keystrokes(&rebound)