aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/shell/copy_mode.rs26
-rw-r--r--src/client/shell/tests/copy.rs92
2 files changed, 102 insertions, 16 deletions
diff --git a/src/client/shell/copy_mode.rs b/src/client/shell/copy_mode.rs
index 07dc8d18..be5b63e3 100644
--- a/src/client/shell/copy_mode.rs
+++ b/src/client/shell/copy_mode.rs
@@ -504,13 +504,10 @@ impl ClientShellState {
}
fn move_copy_cursor(&mut self, row_delta: i16, col_delta: i16, outcome: &mut ClientShellInput) {
- let Some(hit) = self.copy_hit() else {
- self.exit_copy_mode(false, outcome);
- return;
- };
let Some(copy_mode) = self.copy_mode.as_mut() else {
return;
};
+ let (width, height) = copy_mode.geometry;
if col_delta < 0 {
copy_mode.cursor.col = copy_mode
.cursor
@@ -521,11 +518,11 @@ impl ClientShellState {
.cursor
.col
.saturating_add(col_delta as u16)
- .min(hit.inner_rect.width.saturating_sub(1));
+ .min(width.saturating_sub(1));
}
let total_rows = copy_mode
.max_offset_from_bottom
- .saturating_add(hit.inner_rect.height as usize)
+ .saturating_add(height as usize)
.max(1);
if row_delta < 0 {
copy_mode.cursor.row = copy_mode
@@ -610,16 +607,13 @@ impl ClientShellState {
}
fn reveal_copy_cursor(&mut self, outcome: &mut ClientShellInput, reserve_mode_bar_row: bool) {
- let Some(hit) = self.copy_hit() else {
- return;
- };
let request = self.copy_mode.as_mut().and_then(|copy_mode| {
let current_top = copy_mode
.max_offset_from_bottom
.saturating_sub(copy_mode.offset_from_bottom) as u32;
- let max_cursor_row = hit
- .inner_rect
- .height
+ let max_cursor_row = copy_mode
+ .geometry
+ .1
.saturating_sub(if reserve_mode_bar_row { 2 } else { 1 });
let bottom = current_top.saturating_add(u32::from(max_cursor_row));
let desired_top = if copy_mode.cursor.row < current_top {
@@ -647,12 +641,11 @@ impl ClientShellState {
}
fn begin_copy_selection(&mut self, linewise: bool) {
- let end_col = self
- .copy_hit()
- .map_or(0, |hit| hit.inner_rect.width.saturating_sub(1));
+ let width = self.copy_hit().map(|hit| hit.inner_rect.width);
let Some(copy_mode) = self.copy_mode.as_mut() else {
return;
};
+ let end_col = width.unwrap_or(copy_mode.geometry.0).saturating_sub(1);
if linewise {
copy_mode.selection = Some(ClientCopySelection::Linewise {
anchor_row: copy_mode.cursor.row,
@@ -695,7 +688,8 @@ impl ClientShellState {
anchor_row,
copy_mode.cursor.row,
self.copy_hit()
- .map_or(0, |hit| hit.inner_rect.width.saturating_sub(1)),
+ .map_or(copy_mode.geometry.0, |hit| hit.inner_rect.width)
+ .saturating_sub(1),
)
}
});
diff --git a/src/client/shell/tests/copy.rs b/src/client/shell/tests/copy.rs
index 67e36f23..c0d528e1 100644
--- a/src/client/shell/tests/copy.rs
+++ b/src/client/shell/tests/copy.rs
@@ -2216,3 +2216,95 @@ fn word_selection_result_survives_focus_snapshot_lag() {
.as_ref()
.is_some_and(crate::selection::Selection::is_visible));
}
+
+#[test]
+fn copy_mode_repeat_during_projection_gap_stays_active() {
+ for selection_before_gap in [None, Some(true), Some(false)] {
+ let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default()));
+ state.set_snapshot(Box::new(snapshot()));
+ let mut pane_surface = surface();
+ pane_surface.panes[0].scroll = Some(crate::protocol::PaneSurfaceScrollMetrics {
+ offset_from_bottom: 0,
+ max_offset_from_bottom: 20,
+ viewport_rows: 2,
+ });
+ state.set_pane_surface(pane_surface);
+ state.compose(106, 20).expect("composed frame");
+ let mut enter = ClientShellInput::default();
+ state.record_binding(
+ crate::input::KeybindMatch::Action(crate::input::KeybindAction::CopyMode),
+ &mut enter,
+ );
+ if selection_before_gap == Some(true) {
+ state.handle_input_bytes(b"V");
+ }
+ state.handle_raw_events(vec![RawInputEvent::Key(crate::input::TerminalKey::new(
+ KeyCode::Char('k'),
+ KeyModifiers::empty(),
+ ))]);
+
+ let end_col = state.copy_mode.as_ref().expect("copy mode").geometry.0 - 1;
+ let mut next = snapshot();
+ next.revision += 1;
+ state.set_snapshot(Box::new(next));
+ assert!(state.hits.panes.is_empty());
+ assert_eq!(state.mode, ClientShellMode::Copy);
+ if selection_before_gap == Some(false) {
+ state.handle_input_bytes(b"V");
+ assert_eq!(
+ state
+ .selection
+ .as_ref()
+ .expect("linewise selection")
+ .ordered_cells(),
+ ((20, 0), (20, end_col))
+ );
+ }
+
+ let kind = if selection_before_gap == Some(false) {
+ crossterm::event::KeyEventKind::Press
+ } else {
+ crossterm::event::KeyEventKind::Repeat
+ };
+ let moved = state.handle_raw_events(vec![RawInputEvent::Key(
+ crate::input::TerminalKey::new(KeyCode::Char('k'), KeyModifiers::empty())
+ .with_kind(kind),
+ )]);
+ assert!(moved.actions.iter().any(|action| matches!(
+ action,
+ ClientShellAction::Endpoint { request, .. }
+ if matches!(&request.method, crate::api::schema::Method::PaneScroll(params)
+ if params.pane_id == "pane_1" && params.offset_from_bottom == 1)
+ )));
+ if selection_before_gap.is_some() {
+ assert_eq!(
+ state
+ .selection
+ .as_ref()
+ .expect("linewise selection")
+ .ordered_cells(),
+ (
+ (19, 0),
+ (
+ if selection_before_gap == Some(true) {
+ 21
+ } else {
+ 20
+ },
+ end_col
+ )
+ )
+ );
+ }
+
+ assert_eq!(state.mode, ClientShellMode::Copy);
+ assert!(state.copy_mode.is_some());
+ assert_eq!(
+ state
+ .copy_mode
+ .as_ref()
+ .map(|copy_mode| copy_mode.cursor.row),
+ Some(19)
+ );
+ }
+}