diff options
Diffstat (limited to 'src/client/shell')
| -rw-r--r-- | src/client/shell/endpoints.rs | 14 | ||||
| -rw-r--r-- | src/client/shell/state.rs | 25 | ||||
| -rw-r--r-- | src/client/shell/surface_patch.rs | 3 | ||||
| -rw-r--r-- | src/client/shell/tests/endpoints.rs | 47 | ||||
| -rw-r--r-- | src/client/shell/tests/mouse_selection.rs | 27 |
5 files changed, 108 insertions, 8 deletions
diff --git a/src/client/shell/endpoints.rs b/src/client/shell/endpoints.rs index d3823d54..c27358e5 100644 --- a/src/client/shell/endpoints.rs +++ b/src/client/shell/endpoints.rs @@ -208,6 +208,7 @@ impl ClientShellState { let Some(snapshot) = endpoint.snapshot.clone() else { return false; }; + let generation = endpoint.snapshot_generation; let switching_endpoint = endpoint_id != &self.active_endpoint_id; let agent_scroll = self.agent_scroll; if switching_endpoint { @@ -215,7 +216,7 @@ impl ClientShellState { self.pane_surface = None; self.pending_pane_surface = None; } - self.apply_active_snapshot(snapshot); + self.apply_active_snapshot(snapshot, generation); if switching_endpoint { // The aggregate agent list belongs to the client, not one endpoint. self.agent_scroll = agent_scroll; @@ -653,16 +654,21 @@ impl ClientShellState { } fn apply_cached_endpoint_snapshot(&mut self, endpoint_id: &ClientEndpointId) { - let Some(snapshot) = self + let Some((snapshot, generation)) = self .endpoints .iter() .find(|endpoint| &endpoint.endpoint_id == endpoint_id) - .and_then(|endpoint| endpoint.snapshot.clone()) + .and_then(|endpoint| { + endpoint + .snapshot + .clone() + .map(|snapshot| (snapshot, endpoint.snapshot_generation)) + }) else { return; }; if endpoint_id == &self.active_endpoint_id { - self.apply_active_snapshot(snapshot); + self.apply_active_snapshot(snapshot, generation); } } } diff --git a/src/client/shell/state.rs b/src/client/shell/state.rs index 8f2aec43..04e87169 100644 --- a/src/client/shell/state.rs +++ b/src/client/shell/state.rs @@ -895,6 +895,8 @@ pub(super) struct ClientCopyModeState { pub(crate) struct ClientShellState { pub(super) config: ClientShellConfig, pub(super) snapshot: Option<Box<ClientShellSnapshot>>, + pub(super) active_snapshot_generation: Option<u64>, + pub(super) pane_surface_generation: Option<u64>, pub(super) pane_surface: Option<PaneSurfaceFrame>, /// A future projection surface waits here until its matching snapshot arrives. The visible /// pane surface always remains an exact snapshot pair. @@ -1054,6 +1056,8 @@ impl ClientShellState { Self { config, snapshot: None, + active_snapshot_generation: None, + pane_surface_generation: None, pane_surface: None, pending_pane_surface: None, graphics: crate::kitty_graphics::surface::ClientState::default(), @@ -1309,7 +1313,11 @@ impl ClientShellState { self.dismissed_product_announcement = None; } - pub(super) fn apply_active_snapshot(&mut self, mut snapshot: Box<ClientShellSnapshot>) { + pub(super) fn apply_active_snapshot( + &mut self, + mut snapshot: Box<ClientShellSnapshot>, + generation: Option<u64>, + ) { snapshot .commands .retain(|command| command.action != crate::protocol::ClientShellCommandAction::Unknown); @@ -1320,13 +1328,21 @@ impl ClientShellState { }; let endpoint_boot_changed = self.snapshot.is_some() && self.graphics.scope() != graphics_scope; + let generation_changed = self.active_snapshot_generation != generation; if !endpoint_boot_changed + && !generation_changed && self.snapshot.as_ref().is_some_and(|current| { current.boot_id == snapshot.boot_id && snapshot.revision < current.revision }) { return; } + // Screen revisions restart per connection. Keep the displayed surface for selection + // content comparisons, but retire speculative frames from the old connection. + if generation_changed { + self.pending_pane_surface = None; + } + self.active_snapshot_generation = generation; self.graphics.set_scope(&graphics_scope); let command_bindings_changed = self.snapshot.as_ref().is_none_or(|current| { current.commands.len() != snapshot.commands.len() @@ -1618,7 +1634,8 @@ impl ClientShellState { return; } if self.pane_surface.as_ref().is_some_and(|current| { - current.boot_id == surface.boot_id + self.pane_surface_generation == self.active_snapshot_generation + && current.boot_id == surface.boot_id && (surface.projection_revision < current.projection_revision || (surface.projection_revision == current.projection_revision && surface.surface_revision < current.surface_revision)) @@ -1646,7 +1663,8 @@ impl ClientShellState { || surface.projection_revision < snapshot.revision || (!retain_future && surface.projection_revision != snapshot.revision) || self.pane_surface.as_ref().is_some_and(|current| { - current.boot_id == surface.boot_id + self.pane_surface_generation == self.active_snapshot_generation + && current.boot_id == surface.boot_id && (surface.projection_revision < current.projection_revision || (surface.projection_revision == current.projection_revision && surface.surface_revision < current.surface_revision)) @@ -1817,6 +1835,7 @@ impl ClientShellState { self.graphics .set_scene(std::mem::take(&mut surface.graphics)); self.pane_surface = Some(surface); + self.pane_surface_generation = self.active_snapshot_generation; self.invalidate_link_hover(); self.resume_mobile_switcher_if_ready(); self.reconcile_input_source(); diff --git a/src/client/shell/surface_patch.rs b/src/client/shell/surface_patch.rs index 87f947fa..6c829acf 100644 --- a/src/client/shell/surface_patch.rs +++ b/src/client/shell/surface_patch.rs @@ -113,7 +113,8 @@ impl ClientShellState { let Some(current) = self.pane_surface.as_ref() else { return ClientPaneSurfacePatchOutcome::Rejected; }; - if patch.boot_id != current.boot_id + if self.pane_surface_generation != self.active_snapshot_generation + || patch.boot_id != current.boot_id || patch.projection_revision != current.projection_revision || patch.base_surface_revision != current.surface_revision || patch.surface_revision != current.surface_revision.saturating_add(1) diff --git a/src/client/shell/tests/endpoints.rs b/src/client/shell/tests/endpoints.rs index 1edba8e6..4ac8183c 100644 --- a/src/client/shell/tests/endpoints.rs +++ b/src/client/shell/tests/endpoints.rs @@ -1527,6 +1527,53 @@ fn new_connection_generation_accepts_a_lower_same_boot_projection_revision() { } #[test] +fn reconnect_same_endpoint_accepts_new_generation_surface_revision() { + for previous_revision in [9, 1] { + let (mut state, endpoint_id) = state_with_remote(); + let mut previous = snapshot(); + previous.boot_id = "shared-server-boot".into(); + previous.revision = previous_revision; + state.cache_endpoint_snapshot_inactive_for_generation(&endpoint_id, 4, Box::new(previous)); + assert!(state.activate_endpoint_projection(&endpoint_id)); + let mut previous_surface = surface(); + previous_surface.boot_id = "shared-server-boot".into(); + previous_surface.projection_revision = previous_revision; + previous_surface.surface_revision = 9; + state.set_pane_surface(previous_surface.clone()); + previous_surface.projection_revision += 1; + state.set_pane_surface(previous_surface); + assert!(state.pending_pane_surface.is_some()); + state.agent_scroll = 7; + + state.mark_endpoint_disconnected(&endpoint_id); + let mut reconnected = snapshot(); + reconnected.boot_id = "shared-server-boot".into(); + reconnected.revision = 1; + state.cache_endpoint_snapshot_inactive_for_generation( + &endpoint_id, + 5, + Box::new(reconnected), + ); + assert_eq!(state.snapshot.as_ref().unwrap().revision, previous_revision); + assert_eq!(state.pane_surface.as_ref().unwrap().surface_revision, 9); + + state.set_endpoint_status(&endpoint_id, ClientEndpointStatus::Online); + assert!(state.activate_endpoint_projection(&endpoint_id)); + let mut reconnected_surface = surface(); + reconnected_surface.boot_id = "shared-server-boot".into(); + reconnected_surface.projection_revision = 1; + reconnected_surface.surface_revision = 1; + state.set_pane_surface(reconnected_surface); + + assert_eq!(state.snapshot.as_ref().unwrap().revision, 1); + assert_eq!(state.pane_surface.as_ref().unwrap().projection_revision, 1); + assert_eq!(state.pane_surface.as_ref().unwrap().surface_revision, 1); + assert!(state.pending_pane_surface.is_none()); + assert_eq!(state.agent_scroll, 7); + } +} + +#[test] fn reconnect_snapshot_waits_for_coherent_activation_before_replacing_projection() { let (mut state, endpoint_id) = state_with_remote(); assert!(state.activate_endpoint_projection(&endpoint_id)); diff --git a/src/client/shell/tests/mouse_selection.rs b/src/client/shell/tests/mouse_selection.rs index 0a54203e..ccb9b86d 100644 --- a/src/client/shell/tests/mouse_selection.rs +++ b/src/client/shell/tests/mouse_selection.rs @@ -579,6 +579,33 @@ fn double_click_drag_invalidates_cached_boundaries_outside_selected_cells() { } #[test] +fn reconnect_word_selection_tracks_content_changes() { + for content_changed in [false, true] { + let mut state = word_drag_state(true); + let initial = start_word_drag(&mut state); + word_row_reply(&mut state, &initial, "alpha bravo charlie"); + let mut next_surface = state.pane_surface.as_ref().unwrap().clone(); + if content_changed { + next_surface.panes[0].content_revision += 2; + next_surface.frame.cells[14].symbol = " ".into(); + } + let endpoint_id = state.active_endpoint_id.clone(); + let snapshot = state.snapshot.as_ref().unwrap().clone(); + state.mark_endpoint_disconnected(&endpoint_id); + state.cache_endpoint_snapshot_inactive_for_generation(&endpoint_id, 1, snapshot); + state.set_endpoint_status( + &endpoint_id, + crate::client::endpoint::ClientEndpointStatus::Online, + ); + assert!(state.activate_endpoint_projection(&endpoint_id)); + state.set_pane_surface(next_surface); + + assert_eq!(state.selection.is_some(), !content_changed); + assert_eq!(state.word_selection_gesture.is_some(), !content_changed); + } +} + +#[test] fn double_click_release_ignores_reply_after_focus_or_content_changes() { for focus_changed in [false, true] { let mut state = word_drag_state(true); |
