aboutsummaryrefslogtreecommitdiff
path: root/src/app/input
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-07-14 15:54:28 +0300
committerGitHub <noreply@github.com>2026-07-14 15:54:28 +0300
commitb0d46fb9bc2a3e7fb58864939e2580b8a9a2f1bc (patch)
tree0c90365bc46258dea8c7aaf4e948f258712d1539 /src/app/input
parent4c9a3f380d221fe02fa8c4d2d9ef94438fa1957f (diff)
fix: stop copy mode line-end at text (#1411)
Diffstat (limited to 'src/app/input')
-rw-r--r--src/app/input/copy_mode.rs83
1 files changed, 82 insertions, 1 deletions
diff --git a/src/app/input/copy_mode.rs b/src/app/input/copy_mode.rs
index 9e6b3384..f6c322ea 100644
--- a/src/app/input/copy_mode.rs
+++ b/src/app/input/copy_mode.rs
@@ -598,12 +598,18 @@ impl AppState {
return;
};
let pane_id = copy_mode.pane_id;
+ let cursor_row = copy_mode.cursor_row;
let Some(info) = self.pane_info_by_id(pane_id) else {
self.exit_copy_mode(terminal_runtimes, false);
return;
};
let cursor_col = if end {
- info.inner_rect.width.saturating_sub(1)
+ let Some(text) = self.copy_mode_visible_row_text(terminal_runtimes, cursor_row) else {
+ return;
+ };
+ last_character_col(&text)
+ .unwrap_or(0)
+ .min(info.inner_rect.width.saturating_sub(1))
} else {
0
};
@@ -931,6 +937,19 @@ fn first_non_blank_col(text: &str) -> Option<u16> {
None
}
+fn last_character_col(text: &str) -> Option<u16> {
+ let mut col = 0u16;
+ let mut last_col = None;
+ for ch in text.chars() {
+ let width = UnicodeWidthChar::width(ch).unwrap_or(1) as u16;
+ if width > 0 {
+ last_col = Some(col);
+ col = col.saturating_add(width);
+ }
+ }
+ last_col
+}
+
fn char_cell_width(ch: char) -> u16 {
UnicodeWidthChar::width(ch).unwrap_or(1).max(1) as u16
}
@@ -1342,6 +1361,68 @@ mod tests {
}
#[tokio::test]
+ async fn copy_mode_line_end_stops_at_last_character() {
+ let (mut app, _) = app_with_copy_screen(b"hello\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('$'), KeyModifiers::empty()));
+
+ assert_eq!(
+ app.state.copy_mode.as_ref().expect("copy mode").cursor_col,
+ 4
+ );
+
+ if let Some(copy_mode) = app.state.copy_mode.as_mut() {
+ copy_mode.cursor_col = 0;
+ }
+ app.handle_copy_mode_key(TerminalKey::new(KeyCode::End, KeyModifiers::empty()));
+ assert_eq!(
+ app.state.copy_mode.as_ref().expect("copy mode").cursor_col,
+ 4
+ );
+
+ let (mut empty_app, _) = app_with_copy_screen(b"\r\n");
+ empty_app
+ .state
+ .enter_copy_mode(&empty_app.terminal_runtimes);
+ if let Some(copy_mode) = empty_app.state.copy_mode.as_mut() {
+ copy_mode.cursor_row = 0;
+ copy_mode.cursor_col = 7;
+ }
+ empty_app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('$'), KeyModifiers::empty()));
+ assert_eq!(
+ empty_app
+ .state
+ .copy_mode
+ .as_ref()
+ .expect("copy mode")
+ .cursor_col,
+ 0
+ );
+
+ let (mut wide_app, _) = app_with_copy_screen("a界\r\n".as_bytes());
+ wide_app.state.enter_copy_mode(&wide_app.terminal_runtimes);
+ if let Some(copy_mode) = wide_app.state.copy_mode.as_mut() {
+ copy_mode.cursor_row = 0;
+ copy_mode.cursor_col = 0;
+ }
+ wide_app.handle_copy_mode_key(TerminalKey::new(KeyCode::Char('$'), KeyModifiers::empty()));
+ assert_eq!(
+ wide_app
+ .state
+ .copy_mode
+ .as_ref()
+ .expect("copy mode")
+ .cursor_col,
+ 1
+ );
+ }
+
+ #[tokio::test]
async fn copy_mode_word_motions_use_visible_row_words() {
let (mut app, _) = app_with_copy_screen(b"foo bar baz\n");
app.state.enter_copy_mode(&app.terminal_runtimes);