diff options
| author | JP Lew <462836+jplew@users.noreply.github.com> | 2026-08-06 17:00:23 +0530 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-06 14:30:23 +0300 |
| commit | fd0e4ff43aed7751c4cf5a4247e6581dbcfaa3b8 (patch) | |
| tree | 96f542526664565f86431f53606cbe244cc87f33 /src/app/input | |
| parent | e9222d1882effd62ddae9c4d81bc2d9390da7cac (diff) | |
feat(copy-mode): add big-word motions (#2270)
Co-authored-by: Ubuntu <ubuntu@ip-172-31-252-234.ca-west-1.compute.internal>
Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/app/input')
| -rw-r--r-- | src/app/input/copy_mode.rs | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/src/app/input/copy_mode.rs b/src/app/input/copy_mode.rs index b744d2ce..8bdbfd38 100644 --- a/src/app/input/copy_mode.rs +++ b/src/app/input/copy_mode.rs @@ -188,6 +188,9 @@ impl AppState { 'w' => self.copy_mode_word_motion(terminal_runtimes, WordMotion::NextStart), 'b' => self.copy_mode_word_motion(terminal_runtimes, WordMotion::PreviousStart), 'e' => self.copy_mode_word_motion(terminal_runtimes, WordMotion::NextEnd), + 'W' => self.copy_mode_word_motion(terminal_runtimes, WordMotion::NextBigStart), + 'B' => self.copy_mode_word_motion(terminal_runtimes, WordMotion::PreviousBigStart), + 'E' => self.copy_mode_word_motion(terminal_runtimes, WordMotion::NextBigEnd), '{' => self.copy_mode_paragraph(terminal_runtimes, -1), '}' => self.copy_mode_paragraph(terminal_runtimes, 1), _ => {} @@ -649,6 +652,9 @@ impl AppState { WordMotion::NextStart => crate::pane::TerminalWordMotion::NextStart, WordMotion::PreviousStart => crate::pane::TerminalWordMotion::PreviousStart, WordMotion::NextEnd => crate::pane::TerminalWordMotion::NextEnd, + WordMotion::NextBigStart => crate::pane::TerminalWordMotion::NextBigStart, + WordMotion::PreviousBigStart => crate::pane::TerminalWordMotion::PreviousBigStart, + WordMotion::NextBigEnd => crate::pane::TerminalWordMotion::NextBigEnd, }; let Some(target) = runtime.word_motion_target(absolute_row, copy_mode.cursor_col, motion) else { @@ -915,6 +921,9 @@ enum WordMotion { NextStart, PreviousStart, NextEnd, + NextBigStart, + PreviousBigStart, + NextBigEnd, } fn first_non_blank_col(text: &str) -> Option<u16> { @@ -1679,6 +1688,104 @@ mod tests { } #[tokio::test] + async fn copy_mode_big_word_motions_skip_punctuation_runs() { + let (mut app, _) = app_with_copy_screen(b"foo.bar baz qux\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + if let Some(copy_mode) = app.state.copy_mode.as_mut() { + copy_mode.cursor_row = 0; + copy_mode.cursor_col = 0; + } + + for expected_col in [8, 12] { + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('w'), KeyModifiers::SHIFT)); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").cursor_col, + expected_col + ); + } + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('e'), KeyModifiers::SHIFT)); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").cursor_col, + 14 + ); + for expected_col in [12, 8, 0] { + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('b'), KeyModifiers::SHIFT)); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").cursor_col, + expected_col + ); + } + + // Lowercase motions keep their punctuation-aware behavior. + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('w'), KeyModifiers::empty())); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").cursor_col, + 3 + ); + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('w'), KeyModifiers::empty())); + assert_eq!( + app.state.copy_mode.as_ref().expect("copy mode").cursor_col, + 4 + ); + } + + #[tokio::test] + async fn copy_mode_big_word_motions_accept_shifted_codepoints_and_cross_rows() { + let (mut app, pane_id) = app_with_copy_screen(b"foo.bar baz\r\nqux/quux\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + if let Some(copy_mode) = app.state.copy_mode.as_mut() { + copy_mode.cursor_row = 0; + copy_mode.cursor_col = 0; + } + + app.handle_copy_mode_key( + TerminalKey::new(KeyCode::Char('W'), KeyModifiers::SHIFT) + .with_shifted_codepoint('W' as u32), + ); + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode"); + assert_eq!( + copy_mode_viewport_top_row(&app, pane_id) + usize::from(copy_mode.cursor_row), + 0 + ); + assert_eq!(copy_mode.cursor_col, 8); + + app.handle_copy_mode_key( + TerminalKey::new(KeyCode::Char('W'), KeyModifiers::SHIFT) + .with_shifted_codepoint('W' as u32), + ); + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode"); + assert_eq!( + copy_mode_viewport_top_row(&app, pane_id) + usize::from(copy_mode.cursor_row), + 1 + ); + assert_eq!(copy_mode.cursor_col, 0); + + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('b'), KeyModifiers::SHIFT)); + let copy_mode = app.state.copy_mode.as_ref().expect("copy mode"); + assert_eq!( + copy_mode_viewport_top_row(&app, pane_id) + usize::from(copy_mode.cursor_row), + 0 + ); + assert_eq!(copy_mode.cursor_col, 8); + } + + #[tokio::test] + async fn copy_mode_big_word_motions_extend_an_active_selection() { + let (mut app, _) = app_with_copy_screen(b"foo.bar baz qux\r\n"); + app.state.enter_copy_mode(&app.terminal_runtimes); + if let Some(copy_mode) = app.state.copy_mode.as_mut() { + copy_mode.cursor_row = 0; + copy_mode.cursor_col = 0; + } + + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('v'), KeyModifiers::empty())); + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('w'), KeyModifiers::SHIFT)); + app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('y'), KeyModifiers::empty())); + + assert_eq!(copy_mode_clipboard_text(&mut app), "foo.bar b"); + } + + #[tokio::test] async fn copy_mode_search_does_not_change_live_follow_behavior() { let bytes = numbered_lines_bytes(32); let (mut app, pane_id) = app_with_copy_scrollback(&bytes); |
