diff --git a/src/ui/app.rs b/src/ui/app.rs index d787a78f..6756fa4c 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1571,6 +1571,14 @@ impl Tty7App { let claimed = WorkspaceStore::claim(cx, id); crate::ui::windows::WindowRegistry::rebind(cx, previous, claimed); crate::ui::remote_workspace::RemoteLinks::supervise(cx, claimed); + // Forgotten on the way in as well as on the way out. `adopt_workspace` + // puts the empty session up before the pull below orders the real one, + // and it saves what it put up: a window showing nothing, syncing + // against whatever this workspace was left Primed and informed with + // the last time it was visited, which is a Full diff that closes every + // tab on the machine (#716). Arriving speaks for nothing until a pull + // says otherwise. + crate::ui::tree_sync::forget(cx, claimed); self.adopt_workspace(claimed, Session::default(), window, cx); // This method runs under the app's own update lease, so the tabs the // pull must see are the ones just adopted here — reading the app back diff --git a/src/ui/tree_sync.rs b/src/ui/tree_sync.rs index 3b083430..5ea14edc 100644 --- a/src/ui/tree_sync.rs +++ b/src/ui/tree_sync.rs @@ -3764,6 +3764,85 @@ mod tests { }); } + /// #716: switching into a workspace put its empty session up and saved + /// it — a window showing nothing, syncing at Full scope against the + /// mirror and licence the last visit left behind, which closes every tab + /// on the machine before the pull that would have populated the window + /// has even been ordered. Arriving must speak for nothing. + /// + /// The licence is what the assertion holds. The closes it authorises are + /// queued and pumped inside `adopt_workspace`, and the hydrate ordered + /// straight after clears the queue, so by the time a test can look the + /// ops are gone either way — while `informed` outliving the arrival is + /// both durable and the thing that made them possible. + #[cfg(unix)] + #[gpui::test] + fn arriving_at_a_workspace_does_not_prune_what_is_already_in_it(cx: &mut gpui::TestAppContext) { + let (app, mut vcx, _pane_stream) = crate::ui::app::test_window::harness_with_pane(cx); + let theirs = (TabId::new(), TabId::new()); + app.update_in(&mut vcx, |app, window, cx| { + crate::ui::windows::WindowRegistry::init(cx); + let here = crate::core::session::WindowView::default(); + let there = crate::core::session::WindowView::default(); + let (here_id, there_id) = (here.id, there.id); + WorkspaceStore::install_for_test( + cx, + crate::core::session::WindowViews { + views: vec![here, there], + active: Some(here_id), + }, + ); + app.workspace = here_id; + + // The workspace being switched to, as an earlier visit left it: + // primed with the tabs it holds, and licensed to prune them. + { + let state = cx + .default_global::() + .windows + .entry(there_id) + .or_default(); + state.sync = SyncPhase::Primed(WsMirror { + tabs: vec![ + TreeTab { + id: theirs.0, + name: None, + sidebar_group: None, + root: PaneNode::Leaf { pane: 11 }, + }, + TreeTab { + id: theirs.1, + name: None, + sidebar_group: None, + root: PaneNode::Leaf { pane: 12 }, + }, + ], + active: Some(theirs.0), + }); + state.informed = true; + // Keeps whatever the switch queues where the test can read it. + state.inflight = true; + } + + app.switch_workspace(Some(there_id), window, cx); + + let state = &cx.default_global::().windows[&there_id]; + assert!( + !state.informed, + "a window that has just arrived speaks for nothing in the workspace \ + until its own pull lands — least of all that it is empty" + ); + assert!( + !state + .queue + .iter() + .any(|op| matches!(op, ControlRequest::TabClose { .. })), + "and it closes nothing it never showed: {:?}", + state.queue + ); + }); + } + #[test] fn a_ratio_delta_is_clamped_to_the_servers_band_not_a_narrower_one() { let mut pane = Pane::split_node(gpui::Axis::Horizontal, 0.5, Pane::Empty, Pane::Empty);