diff options
Diffstat (limited to 'src/persist')
| -rw-r--r-- | src/persist/restore.rs | 235 | ||||
| -rw-r--r-- | src/persist/snapshot.rs | 52 |
2 files changed, 279 insertions, 8 deletions
diff --git a/src/persist/restore.rs b/src/persist/restore.rs index d5661b30..ca1748e0 100644 --- a/src/persist/restore.rs +++ b/src/persist/restore.rs @@ -57,6 +57,7 @@ type RestoredTab = ( crate::workspace::Tab, Vec<TerminalState>, HashMap<TerminalId, TerminalRuntime>, + HashMap<PaneId, u32>, ); type RestoreFailures<T> = (T, usize); @@ -154,6 +155,35 @@ fn collect_snapshot_ids_inner(node: &LayoutSnapshot, ids: &mut Vec<u32>) { } } +fn migrated_public_pane_numbers_by_old_raw( + snap: &WorkspaceSnapshot, + next_public_pane_number: &mut usize, +) -> HashMap<u32, usize> { + let mut public_numbers = snap.public_pane_numbers.clone(); + for tab in &snap.tabs { + let mut pane_ids = Vec::new(); + collect_layout_snapshot_pane_ids(&tab.layout, &mut pane_ids); + for old_raw in pane_ids { + public_numbers.entry(old_raw).or_insert_with(|| { + let number = *next_public_pane_number; + *next_public_pane_number += 1; + number + }); + } + } + public_numbers +} + +fn collect_layout_snapshot_pane_ids(node: &LayoutSnapshot, ids: &mut Vec<u32>) { + match node { + LayoutSnapshot::Pane(id) => ids.push(*id), + LayoutSnapshot::Split { first, second, .. } => { + collect_layout_snapshot_pane_ids(first, ids); + collect_layout_snapshot_pane_ids(second, ids); + } + } +} + #[cfg(unix)] fn restore_with_imports_strict( snapshot: &SessionSnapshot, @@ -269,6 +299,7 @@ fn restore_with_imports_and_failures( workspaces.push(workspace); } } + crate::workspace::reserve_workspace_ids(&workspaces); ((workspaces, terminals, terminal_runtimes), failed_imports) } @@ -284,8 +315,42 @@ fn restore_workspace( let mut tabs = Vec::new(); let mut terminals = Vec::new(); let mut terminal_runtimes = HashMap::new(); + let workspace_id = snap + .id + .clone() + .unwrap_or_else(crate::workspace::generate_workspace_id); + let mut next_public_pane_number = snap + .public_pane_numbers + .values() + .copied() + .max() + .and_then(|max| max.checked_add(1)) + .unwrap_or(1) + .max(snap.next_public_pane_number); + let public_pane_numbers_by_old_raw = + migrated_public_pane_numbers_by_old_raw(snap, &mut next_public_pane_number); + let public_pane_ids_by_old_raw: HashMap<u32, String> = public_pane_numbers_by_old_raw + .iter() + .map(|(old_raw, public_number)| { + ( + *old_raw, + format!( + "{}:p{}", + workspace_id, + crate::workspace::encode_public_number(*public_number) + ), + ) + }) + .collect(); let mut public_pane_numbers = HashMap::new(); - let mut next_public_pane_number = 1; + let mut next_public_tab_number = snap + .public_tab_numbers + .iter() + .copied() + .max() + .and_then(|max| max.checked_add(1)) + .unwrap_or(1) + .max(snap.next_public_tab_number); let mut failed_imports = 0; for (idx, tab_snap) in snap.tabs.iter().enumerate() { @@ -298,14 +363,33 @@ fn restore_workspace( runtime_context, resumed_agent_sessions, imported_panes, + &public_pane_ids_by_old_raw, ); failed_imports += tab_failed_imports; - let Some((tab, restored_terminals, restored_runtimes)) = restored_tab else { + let Some((mut tab, restored_terminals, restored_runtimes, reverse_id_map)) = restored_tab + else { continue; }; + if let Some(public_tab_number) = snap.public_tab_numbers.get(idx).copied() { + tab.number = public_tab_number; + } + next_public_tab_number = next_public_tab_number.max(tab.number + 1); for pane_id in tab.layout.pane_ids() { - public_pane_numbers.insert(pane_id, next_public_pane_number); - next_public_pane_number += 1; + let public_number = public_pane_numbers_by_old_raw + .get( + &reverse_id_map + .get(&pane_id) + .copied() + .unwrap_or(pane_id.raw()), + ) + .copied() + .unwrap_or_else(|| { + let number = next_public_pane_number; + next_public_pane_number += 1; + number + }); + public_pane_numbers.insert(pane_id, public_number); + next_public_pane_number = next_public_pane_number.max(public_number + 1); } terminals.extend(restored_terminals); terminal_runtimes.extend(restored_runtimes); @@ -320,10 +404,7 @@ fn restore_workspace( ( Some(Workspace { - id: snap - .id - .clone() - .unwrap_or_else(crate::workspace::generate_workspace_id), + id: workspace_id, custom_name: snap.custom_name.clone(), identity_cwd: snap.identity_cwd.clone(), cached_git_branch: crate::workspace::git_branch(&snap.identity_cwd), @@ -332,6 +413,7 @@ fn restore_workspace( worktree_space, public_pane_numbers, next_public_pane_number, + next_public_tab_number, active_tab: snap.active_tab.min(tabs.len().saturating_sub(1)), tabs, #[cfg(test)] @@ -361,6 +443,7 @@ fn restore_tab( runtime_context: &RestoreRuntimeContext<'_>, resumed_agent_sessions: &mut HashSet<String>, imported_panes: &mut HashMap<u32, crate::handoff_runtime::ImportedHandoffRuntime>, + public_pane_ids_by_old_raw: &HashMap<u32, String>, ) -> RestoreFailures<Option<RestoredTab>> { let (node, id_map) = restore_node_remapped(&snap.layout); let reverse_id_map: HashMap<PaneId, u32> = id_map @@ -416,6 +499,9 @@ fn restore_tab( .and_then(|plan| crate::detect::parse_agent_label(&plan.agent)); let old_pane_id = reverse_id_map.get(id).copied(); + let public_pane_id = old_pane_id + .and_then(|old_id| public_pane_ids_by_old_raw.get(&old_id)) + .map(String::as_str); let imported_runtime = old_pane_id.and_then(|old_id| imported_panes.remove(&old_id)); let was_imported = imported_runtime.is_some(); let pending_native_agent_restore = if was_imported { @@ -488,6 +574,7 @@ fn restore_tab( runtime_context.events.clone(), runtime_context.render_notify.clone(), runtime_context.render_dirty.clone(), + public_pane_id, ) } @@ -505,6 +592,7 @@ fn restore_tab( runtime_context.events.clone(), runtime_context.render_notify.clone(), runtime_context.render_dirty.clone(), + public_pane_id, ) } }; @@ -611,6 +699,7 @@ fn restore_tab( }, terminals, terminal_runtimes, + reverse_id_map, )), failed_imports, ) @@ -1057,6 +1146,10 @@ mod tests { custom_name: None, identity_cwd: cwd.clone(), worktree_space: None, + public_pane_numbers: HashMap::new(), + next_public_pane_number: 0, + public_tab_numbers: Vec::new(), + next_public_tab_number: 0, tabs: vec![TabSnapshot { custom_name: None, layout: LayoutSnapshot::Pane(0), @@ -1122,6 +1215,124 @@ mod tests { } #[tokio::test] + async fn restore_preserves_public_id_mapping_after_pane_id_remap() { + let cwd = std::env::current_dir().unwrap(); + let snapshot = SessionSnapshot { + version: super::super::snapshot::SNAPSHOT_VERSION, + workspaces: vec![WorkspaceSnapshot { + id: Some("w1".into()), + custom_name: None, + identity_cwd: cwd.clone(), + worktree_space: None, + public_pane_numbers: HashMap::from([(10, 1), (20, 3)]), + next_public_pane_number: 4, + public_tab_numbers: vec![5], + next_public_tab_number: 6, + tabs: vec![TabSnapshot { + custom_name: None, + layout: LayoutSnapshot::Split { + direction: super::super::snapshot::DirectionSnapshot::Horizontal, + ratio: 0.5, + first: Box::new(LayoutSnapshot::Pane(10)), + second: Box::new(LayoutSnapshot::Pane(20)), + }, + panes: HashMap::from([ + ( + 10, + super::super::snapshot::PaneSnapshot { + cwd: cwd.clone(), + label: None, + agent_name: None, + agent_session: None, + launch_argv: None, + }, + ), + ( + 20, + super::super::snapshot::PaneSnapshot { + cwd: cwd.clone(), + label: None, + agent_name: None, + agent_session: None, + launch_argv: None, + }, + ), + ]), + zoomed: false, + focused: Some(10), + root_pane: Some(10), + }], + active_tab: 0, + }], + active: Some(0), + selected: 0, + agent_panel_scope: Default::default(), + sidebar_width: None, + sidebar_section_split: None, + collapsed_space_keys: Default::default(), + }; + let (events, _event_rx) = mpsc::channel(4); + + let (workspaces, _terminals, _runtimes) = restore( + &snapshot, + None, + 24, + 80, + 0, + test_restore_shell(), + crate::config::ShellModeConfig::NonLogin, + false, + events, + Arc::new(Notify::new()), + Arc::new(AtomicBool::new(false)), + ); + + let workspace = workspaces.first().expect("workspace should restore"); + let mut public_numbers: Vec<_> = workspace.public_pane_numbers.values().copied().collect(); + public_numbers.sort_unstable(); + assert_eq!(public_numbers, vec![1, 3]); + assert_eq!(workspace.next_public_pane_number, 4); + assert_eq!(workspace.tabs[0].number, 5); + assert_eq!(workspace.next_public_tab_number, 6); + } + + #[test] + fn legacy_restore_precomputes_missing_public_pane_numbers() { + let cwd = std::env::current_dir().unwrap(); + let snapshot = WorkspaceSnapshot { + id: Some("w1".into()), + custom_name: None, + identity_cwd: cwd, + worktree_space: None, + public_pane_numbers: HashMap::new(), + next_public_pane_number: 0, + public_tab_numbers: Vec::new(), + next_public_tab_number: 0, + tabs: vec![TabSnapshot { + custom_name: None, + layout: LayoutSnapshot::Split { + direction: super::super::snapshot::DirectionSnapshot::Horizontal, + ratio: 0.5, + first: Box::new(LayoutSnapshot::Pane(10)), + second: Box::new(LayoutSnapshot::Pane(20)), + }, + panes: HashMap::new(), + zoomed: false, + focused: Some(10), + root_pane: Some(10), + }], + active_tab: 0, + }; + let mut next_public_pane_number = 1; + + let public_numbers = + migrated_public_pane_numbers_by_old_raw(&snapshot, &mut next_public_pane_number); + + assert_eq!(public_numbers, HashMap::from([(10, 1), (20, 2)])); + assert_eq!(next_public_pane_number, 3); + } + + #[tokio::test] #[cfg(unix)] async fn native_agent_restore_defers_runtime_launch() { let cwd = std::env::current_dir().unwrap(); @@ -1132,6 +1343,10 @@ mod tests { custom_name: None, identity_cwd: cwd.clone(), worktree_space: None, + public_pane_numbers: HashMap::new(), + next_public_pane_number: 0, + public_tab_numbers: Vec::new(), + next_public_tab_number: 0, tabs: vec![TabSnapshot { custom_name: None, layout: LayoutSnapshot::Pane(0), @@ -1333,6 +1548,10 @@ mod tests { custom_name: None, identity_cwd: cwd, worktree_space: None, + public_pane_numbers: HashMap::new(), + next_public_pane_number: 0, + public_tab_numbers: Vec::new(), + next_public_tab_number: 0, tabs: vec![TabSnapshot { custom_name: None, layout: LayoutSnapshot::Pane(0), diff --git a/src/persist/snapshot.rs b/src/persist/snapshot.rs index c04c280f..765fa2b6 100644 --- a/src/persist/snapshot.rs +++ b/src/persist/snapshot.rs @@ -57,6 +57,14 @@ pub struct WorkspaceSnapshot { pub identity_cwd: PathBuf, #[serde(default, skip_serializing_if = "Option::is_none")] pub worktree_space: Option<crate::workspace::WorktreeSpaceMembership>, + #[serde(default)] + pub public_pane_numbers: HashMap<u32, usize>, + #[serde(default)] + pub next_public_pane_number: usize, + #[serde(default)] + pub public_tab_numbers: Vec<usize>, + #[serde(default)] + pub next_public_tab_number: usize, pub tabs: Vec<TabSnapshot>, #[serde(default)] pub active_tab: usize, @@ -150,6 +158,10 @@ impl From<LegacyWorkspaceSnapshot> for WorkspaceSnapshot { custom_name: snap.custom_name, identity_cwd, worktree_space: None, + public_pane_numbers: HashMap::new(), + next_public_pane_number: 0, + public_tab_numbers: Vec::new(), + next_public_tab_number: 0, tabs: vec![tab], active_tab: 0, } @@ -284,6 +296,14 @@ fn capture_workspace( .resolved_identity_cwd_from(terminals, terminal_runtimes) .unwrap_or_else(|| ws.identity_cwd.clone()), worktree_space: ws.worktree_space.clone(), + public_pane_numbers: ws + .public_pane_numbers + .iter() + .map(|(pane_id, number)| (pane_id.raw(), *number)) + .collect(), + next_public_pane_number: ws.next_public_pane_number, + public_tab_numbers: ws.tabs.iter().map(|tab| tab.number).collect(), + next_public_tab_number: ws.next_public_tab_number, tabs: ws .tabs .iter() @@ -616,6 +636,10 @@ mod tests { custom_name: Some("pi-mono".to_string()), identity_cwd: PathBuf::from("/home/can/Projects/herdr"), worktree_space: None, + public_pane_numbers: HashMap::from([(0, 1), (1, 2)]), + next_public_pane_number: 3, + public_tab_numbers: vec![1], + next_public_tab_number: 2, tabs: vec![TabSnapshot { custom_name: Some("api".to_string()), layout: LayoutSnapshot::Split { @@ -930,6 +954,30 @@ mod tests { } #[test] + fn capture_contract_tracks_public_id_counters() { + let mut state = state_with_workspaces(&["one"]); + let second = state.workspaces[0].test_split(Direction::Horizontal); + let third = state.workspaces[0].test_split(Direction::Vertical); + let second_tab = state.workspaces[0].test_add_tab(None); + + state.workspaces[0].close_pane(second); + + let snapshot = capture_from_state(&state); + let workspace = &snapshot.workspaces[0]; + assert_eq!( + workspace.public_pane_numbers, + HashMap::from([ + (state.workspaces[0].tabs[0].root_pane.raw(), 1), + (third.raw(), 3), + (state.workspaces[0].tabs[second_tab].root_pane.raw(), 4), + ]) + ); + assert_eq!(workspace.next_public_pane_number, 5); + assert_eq!(workspace.public_tab_numbers, vec![1, 2]); + assert_eq!(workspace.next_public_tab_number, 3); + } + + #[test] fn capture_contract_tracks_workspace_identity_and_pane_cwds() { let mut state = state_with_workspaces(&["one"]); let root = state.workspaces[0].tabs[0].root_pane; @@ -1153,6 +1201,10 @@ mod tests { custom_name: Some("fallback test".to_string()), identity_cwd: PathBuf::from("/tmp"), worktree_space: None, + public_pane_numbers: HashMap::new(), + next_public_pane_number: 0, + public_tab_numbers: Vec::new(), + next_public_tab_number: 0, tabs: vec