par_term/pane/manager/mod.rs
1//! Pane manager for coordinating pane operations within a tab
2//!
3//! The PaneManager owns the pane tree and provides operations for:
4//! - Splitting panes horizontally and vertically
5//! - Closing panes
6//! - Navigating between panes
7//! - Resizing panes
8//!
9//! # Module Organisation
10//!
11//! General pane-management sub-modules (not tmux-specific):
12//! - [`creation`]: Pane creation and tree manipulation (split, remove).
13//! - [`focus`]: Focus management and directional navigation.
14//! - [`layout`]: Bounds, resize, and divider operations.
15//! - [`session`]: Session restore from saved layout (session-file → pane tree).
16//!
17//! tmux integration sub-modules (only active when a tmux session is attached):
18//! - [`tmux_layout`]: Full tmux layout integration (set, rebuild, update).
19//! - [`super::tmux_helpers`]: Helper types and free functions used by `tmux_layout`.
20//!
21//! `session.rs` is intentionally kept in this module (rather than under a
22//! `tmux/` sub-tree) because it also handles non-tmux session restore. Only
23//! `tmux_layout.rs` is exclusively tmux-specific.
24
25mod creation;
26mod focus;
27mod layout;
28mod session;
29mod tmux_convert;
30mod tmux_layout;
31mod tmux_update;
32
33use crate::config::{Config, PaneBackgroundConfig};
34use crate::pane::types::{Pane, PaneBounds, PaneId, PaneNode};
35use anyhow::Result;
36use par_term_terminal::TerminalManager;
37use std::collections::{HashMap, HashSet};
38use std::sync::Arc;
39use std::sync::atomic::AtomicBool;
40use tokio::runtime::Runtime;
41use tokio::sync::RwLock;
42
43/// Result of extracting a pane from the tree (returns live Pane ownership)
44pub enum ExtractResult {
45 /// Pane was extracted; remaining tree is returned (None if it was the only pane)
46 Extracted {
47 pane: Pane,
48 remaining: Option<PaneNode>,
49 },
50 /// The target pane was the only pane in the tree
51 OnlyPane(Pane),
52 /// Pane was not found in the tree
53 NotFound,
54}
55
56/// The ids the panes of an adopted subtree hold once
57/// [`PaneManager::insert_subtree_at`] has placed them, keyed by the id each
58/// pane arrived with.
59///
60/// Every pane of the inserted subtree appears exactly once; an entry maps to
61/// itself when the arriving id was already free here. An id that was never part
62/// of the subtree is absent, so a lookup miss means "not from this subtree"
63/// rather than "unchanged".
64pub type PaneIdRemap = HashMap<PaneId, PaneId>;
65
66/// Internal result for recursive pane extraction (carries PaneNode on NotFound for tree reconstruction)
67enum ExtractInternal {
68 Extracted { pane: Pane, remaining: PaneNode },
69 OnlyPane(Pane),
70 NotFound(PaneNode),
71}
72
73/// Manages the pane tree within a single tab
74pub struct PaneManager {
75 /// Root of the pane tree (None if no panes yet)
76 pub(super) root: Option<PaneNode>,
77 /// ID of the currently focused pane
78 pub(super) focused_pane_id: Option<PaneId>,
79 /// Counter for generating unique pane IDs
80 pub(super) next_pane_id: PaneId,
81 /// Width of dividers between panes in pixels
82 pub(super) divider_width: f32,
83 /// Width of the hit area for divider drag detection
84 pub(super) divider_hit_width: f32,
85 /// Current total bounds available for panes
86 pub(super) total_bounds: PaneBounds,
87}
88
89impl PaneManager {
90 /// Create a new empty pane manager
91 pub fn new() -> Self {
92 Self {
93 root: None,
94 focused_pane_id: None,
95 next_pane_id: 1,
96 divider_width: 1.0, // Default 1 pixel divider
97 divider_hit_width: 8.0, // Default 8 pixel hit area
98 total_bounds: PaneBounds::default(),
99 }
100 }
101
102 /// Create a pane manager pre-populated with a single primary pane that
103 /// wraps an already-running `TerminalManager`.
104 ///
105 /// This is used by `Tab::new_internal` so that every tab always starts with
106 /// a `PaneManager` (R-32). The primary pane shares the caller's `terminal`
107 /// `Arc`, so no new shell process is spawned.
108 ///
109 /// # Arguments
110 /// * `terminal` — `Arc` cloned from `Tab::terminal`
111 /// * `working_directory` — Optional CWD for the primary pane
112 /// * `is_active` — Shared atomic cloned from `Tab::is_active`
113 pub fn new_with_existing_terminal(
114 terminal: Arc<RwLock<TerminalManager>>,
115 working_directory: Option<String>,
116 is_active: Arc<AtomicBool>,
117 ) -> Self {
118 let mut manager = Self::new();
119 let primary_pane_id = manager.next_pane_id;
120 manager.next_pane_id += 1;
121
122 let pane =
123 Pane::new_wrapping_terminal(primary_pane_id, terminal, working_directory, is_active);
124
125 manager.root = Some(PaneNode::leaf(pane));
126 manager.focused_pane_id = Some(primary_pane_id);
127
128 manager
129 }
130
131 /// Create a pane manager wrapping an existing `Pane` as the single root node.
132 ///
133 /// Used by `Tab::new_from_pane()` when promoting a pane to its own tab.
134 /// The pane's ID is preserved and `next_pane_id` is advanced past it.
135 pub fn new_with_pane(pane: Pane) -> Self {
136 let pane_id = pane.id;
137 let mut manager = Self::new();
138 manager.next_pane_id = pane_id + 1;
139 manager.root = Some(PaneNode::leaf(pane));
140 manager.focused_pane_id = Some(pane_id);
141 manager
142 }
143
144 /// Create a pane manager with an initial pane
145 pub fn with_initial_pane(
146 config: &Config,
147 runtime: Arc<Runtime>,
148 working_directory: Option<String>,
149 ) -> Result<Self> {
150 let mut manager = Self::new();
151 manager.divider_width = config.panes.pane_divider_width.unwrap_or(1.0);
152 manager.divider_hit_width = config.panes.pane_divider_hit_width;
153 manager.create_initial_pane(config, runtime, working_directory)?;
154 Ok(manager)
155 }
156
157 /// Get the next pane ID that will be assigned
158 pub fn next_pane_id(&self) -> PaneId {
159 self.next_pane_id
160 }
161
162 /// Get a pane by ID
163 pub fn get_pane(&self, id: PaneId) -> Option<&Pane> {
164 self.root.as_ref()?.find_pane(id)
165 }
166
167 /// Get a mutable pane by ID
168 pub fn get_pane_mut(&mut self, id: PaneId) -> Option<&mut Pane> {
169 self.root.as_mut()?.find_pane_mut(id)
170 }
171
172 /// Get all panes
173 pub fn all_panes(&self) -> Vec<&Pane> {
174 self.root
175 .as_ref()
176 .map(|r| r.all_panes())
177 .unwrap_or_default()
178 }
179
180 /// Get all panes mutably
181 pub fn all_panes_mut(&mut self) -> Vec<&mut Pane> {
182 self.root
183 .as_mut()
184 .map(|r| r.all_panes_mut())
185 .unwrap_or_default()
186 }
187
188 /// Collect current per-pane background settings for config persistence
189 ///
190 /// Returns a `Vec<PaneBackgroundConfig>` containing only panes that have
191 /// a custom background image set. The `index` field corresponds to the
192 /// pane's position in the tree traversal order.
193 pub fn collect_pane_backgrounds(&self) -> Vec<PaneBackgroundConfig> {
194 self.all_panes()
195 .iter()
196 .enumerate()
197 .filter_map(|(index, pane)| {
198 pane.background
199 .image_path
200 .as_ref()
201 .map(|path| PaneBackgroundConfig {
202 index,
203 image: path.clone(),
204 mode: pane.background.mode,
205 opacity: pane.background.opacity,
206 darken: pane.background.darken,
207 })
208 })
209 .collect()
210 }
211
212 /// Get the number of panes
213 pub fn pane_count(&self) -> usize {
214 self.root.as_ref().map(|r| r.pane_count()).unwrap_or(0)
215 }
216
217 /// Check if there are multiple panes
218 pub fn has_multiple_panes(&self) -> bool {
219 self.pane_count() > 1
220 }
221
222 /// Get access to the root node (for rendering)
223 pub fn root(&self) -> Option<&PaneNode> {
224 self.root.as_ref()
225 }
226
227 /// Get mutable access to the root node
228 pub fn root_mut(&mut self) -> Option<&mut PaneNode> {
229 self.root.as_mut()
230 }
231
232 /// Insert a `PaneNode` subtree into the tree by splitting the target pane.
233 ///
234 /// The target leaf is replaced with a `Split` containing the original pane
235 /// as one child and the `subtree` as the other. Bounds are recalculated.
236 ///
237 /// On success, returns the ids the adopted panes hold afterwards, which are
238 /// **not** always the ids they arrived with.
239 ///
240 /// On failure — there is no tree here to insert into, or no pane holds
241 /// `target_pane_id` — the tree is left untouched and the `subtree` is handed
242 /// **back**, carrying the ids it arrived with. Handing it back is the point
243 /// of the `Err`, not a courtesy: a caller gets a subtree to pass here by
244 /// detaching it from wherever it was living, so this is the only remaining
245 /// reference to those panes, and dropping it closes them and kills their
246 /// PTYs. The caller must put it back somewhere.
247 ///
248 /// A subtree arriving from another tab ("Demote Tab to Pane") brings that
249 /// tab's ids with it, and ids are allocated per `PaneManager` — that is,
250 /// per tab — so an incoming id can already belong to a pane here: tab A's
251 /// panes 1 and 2 landing in a tab that already holds 1 and 2. `get_pane` is
252 /// a search over the tree, so a duplicate would make every id-keyed lookup
253 /// resolve to whichever pane the walk reaches first, and the adopted pane
254 /// would be unreachable. Colliding panes are therefore renumbered from this
255 /// manager's counter.
256 ///
257 /// Advancing the counter past every adopted id closes the other half: this
258 /// tab must not later hand a *new* pane an id one of the adopted panes is
259 /// already using.
260 ///
261 /// A caller holding an id from the subtree across this call must translate
262 /// it through the returned map; the pre-move id may now name a different
263 /// pane, or no pane at all.
264 #[must_use = "on success a renumbered pane is only reachable through the returned \
265 remap; on failure the returned subtree owns live terminals and dropping \
266 it kills them"]
267 pub fn insert_subtree_at(
268 &mut self,
269 target_pane_id: PaneId,
270 mut subtree: PaneNode,
271 direction: crate::pane::types::SplitDirection,
272 ratio: f32,
273 ) -> Result<PaneIdRemap, PaneNode> {
274 let Some(root) = self.root.take() else {
275 return Err(subtree);
276 };
277
278 // Reconcile before the insert, not after: `insert_subtree_at_node`
279 // searches for `target_pane_id`, and a subtree pane still carrying that
280 // id could be found first and split instead of the intended target.
281 // Still cheaper than walking the tree twice to pre-check that the target
282 // exists; a failed insert undoes the renumbering below instead.
283 let remap = self.reconcile_adopted_ids(&root, &mut subtree);
284
285 match Self::insert_subtree_at_node(root, target_pane_id, subtree, direction, ratio) {
286 Ok(new_root) => {
287 self.root = Some(new_root);
288 self.recalculate_bounds();
289
290 // Guard the mutation rather than `get_pane`: this is the only
291 // path by which a pane this manager did not allocate enters the
292 // tree, and the lookups run per frame and inside loops.
293 debug_assert!(
294 self.pane_ids_are_unique(),
295 "pane ids are not unique after adopting a subtree: {:?}",
296 self.root.as_ref().map(PaneNode::all_pane_ids)
297 );
298
299 Ok(remap)
300 }
301 Err((original_root, mut subtree)) => {
302 self.root = Some(original_root);
303 Self::undo_reconcile(&mut subtree, &remap);
304 Err(subtree)
305 }
306 }
307 }
308
309 /// Put back the ids [`Self::reconcile_adopted_ids`] replaced, so a subtree
310 /// handed back after a failed insert is the subtree that arrived.
311 ///
312 /// Without this the caller would be restoring panes that carry ids drawn
313 /// from *this* manager's counter into a tree that never agreed to them —
314 /// the collision this whole path exists to prevent, reintroduced by the
315 /// recovery. The ids a subtree arrived with are always safe to restore,
316 /// because they are the ids it already held wherever it came from.
317 ///
318 /// Inverting the map is sound because `reconcile_adopted_ids` gives every
319 /// pane a distinct id, so no two entries share a value. Panes are matched
320 /// by id rather than by traversal position, which keeps this independent of
321 /// the order `all_panes_mut` happens to walk in.
322 ///
323 /// `next_pane_id` is deliberately left advanced: the subtree goes back to
324 /// the tab it came from, whose counter is already past its own ids, and ids
325 /// skipped here cost nothing.
326 fn undo_reconcile(subtree: &mut PaneNode, remap: &PaneIdRemap) {
327 let arrived_as: HashMap<PaneId, PaneId> = remap
328 .iter()
329 .map(|(&arrived, &now)| (now, arrived))
330 .collect();
331
332 for pane in subtree.all_panes_mut() {
333 if let Some(&arrived) = arrived_as.get(&pane.id) {
334 pane.id = arrived;
335 }
336 }
337 }
338
339 /// Give every pane of an incoming subtree an id that is free in this tab,
340 /// and move `next_pane_id` past all of them.
341 ///
342 /// `existing_root` is this manager's tree, passed in because
343 /// [`Self::insert_subtree_at`] has already taken it out of `self`.
344 fn reconcile_adopted_ids(
345 &mut self,
346 existing_root: &PaneNode,
347 subtree: &mut PaneNode,
348 ) -> PaneIdRemap {
349 let existing: HashSet<PaneId> = existing_root.all_pane_ids().into_iter().collect();
350
351 // Seed above every id in *both* trees so each allocation is free
352 // without searching: a replacement must dodge the ids the subtree keeps
353 // as well as the ids already here (target {1}, subtree {2,1} would
354 // otherwise renumber the incoming 1 onto the incoming 2).
355 let mut next = self.next_pane_id;
356 for id in existing.iter().copied().chain(subtree.all_pane_ids()) {
357 next = next.max(id.saturating_add(1));
358 }
359
360 let mut remap = PaneIdRemap::new();
361 for pane in subtree.all_panes_mut() {
362 let arrived_as = pane.id;
363 if existing.contains(&arrived_as) {
364 log::info!(
365 "Adopted pane id {} is already taken in this tab; renumbering it to {}",
366 arrived_as,
367 next
368 );
369 pane.id = next;
370 next = next.saturating_add(1);
371 }
372 remap.insert(arrived_as, pane.id);
373 }
374
375 self.next_pane_id = next;
376 remap
377 }
378
379 /// Whether no two panes in the tree share an id.
380 fn pane_ids_are_unique(&self) -> bool {
381 let ids = self
382 .root
383 .as_ref()
384 .map(PaneNode::all_pane_ids)
385 .unwrap_or_default();
386 ids.iter().collect::<HashSet<_>>().len() == ids.len()
387 }
388
389 /// Extract a pane from the tree by ID, returning ownership of the live `Pane`.
390 ///
391 /// Unlike `remove_pane()` which drops the pane, this returns the pane
392 /// intact so it can be transferred to another tab or pane tree.
393 /// All processes in the pane's PTY continue running.
394 pub fn extract_pane(&mut self, target_id: PaneId) -> ExtractResult {
395 if let Some(root) = self.root.take() {
396 match Self::extract_pane_from_node(root, target_id) {
397 ExtractInternal::Extracted { pane, remaining } => {
398 self.root = Some(remaining);
399 if let Some(id) = self.focused_pane_id
400 && id == target_id
401 {
402 self.focused_pane_id = self.root.as_ref().and_then(|r| r.first_pane_id());
403 }
404 ExtractResult::Extracted {
405 pane,
406 remaining: self.root.take(),
407 }
408 }
409 ExtractInternal::OnlyPane(pane) => {
410 self.root = None;
411 self.focused_pane_id = None;
412 ExtractResult::OnlyPane(pane)
413 }
414 ExtractInternal::NotFound(node) => {
415 self.root = Some(node);
416 ExtractResult::NotFound
417 }
418 }
419 } else {
420 ExtractResult::NotFound
421 }
422 }
423
424 /// Take ownership of the root node, leaving `None` in its place.
425 ///
426 /// Used by demote (tab → pane) to extract the entire pane tree for
427 /// transplantation into another tab.
428 pub fn take_root(&mut self) -> Option<PaneNode> {
429 self.focused_pane_id = None;
430 self.root.take()
431 }
432
433 /// Replace the root node (drops any existing tree).
434 ///
435 /// Used after `extract_pane` when the remaining tree needs to be
436 /// restored and the default `ExtractResult::Extracted.remaining`
437 /// has already been taken by the caller.
438 pub fn set_root(&mut self, node: PaneNode) {
439 self.root = Some(node);
440 self.recalculate_bounds();
441 }
442}
443
444impl Default for PaneManager {
445 fn default() -> Self {
446 Self::new()
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453 use crate::pane::types::SplitDirection;
454
455 // Note: Full tests would require mocking TerminalManager
456 // These are placeholder tests for the manager logic
457
458 #[test]
459 fn test_pane_manager_new() {
460 let manager = PaneManager::new();
461 assert!(manager.root.is_none());
462 assert_eq!(manager.pane_count(), 0);
463 assert!(!manager.has_multiple_panes());
464 }
465
466 /// A pane whose terminal has no shell spawned, so it runs without a PTY on
467 /// every supported platform.
468 ///
469 /// `working_directory` carries the marker: `insert_subtree_at` rewrites
470 /// only `bounds` (via `recalculate_bounds`), so the marker survives the
471 /// insert and identifies *which* pane a lookup resolved to.
472 fn stub_pane(id: PaneId, marker: &str) -> Pane {
473 let terminal = TerminalManager::new_with_scrollback(80, 24, 100)
474 .expect("stub terminal creation without a shell");
475 Pane::new_wrapping_terminal(
476 id,
477 Arc::new(RwLock::new(terminal)),
478 Some(marker.to_string()),
479 Arc::new(AtomicBool::new(false)),
480 )
481 }
482
483 fn marker_of(manager: &PaneManager, id: PaneId) -> Option<String> {
484 manager.get_pane(id)?.working_directory.clone()
485 }
486
487 /// A two-pane manager holding ids 1 and 2, as a tab that has been split
488 /// once would.
489 fn manager_with_two_panes(first: &str, second: &str) -> PaneManager {
490 let mut manager = PaneManager::new();
491 manager.root = Some(PaneNode::split(
492 SplitDirection::Vertical,
493 0.5,
494 PaneNode::leaf(stub_pane(1, first)),
495 PaneNode::leaf(stub_pane(2, second)),
496 ));
497 manager.focused_pane_id = Some(1);
498 manager.next_pane_id = 3;
499 manager
500 }
501
502 #[test]
503 fn an_adopted_subtree_does_not_shadow_panes_that_already_hold_its_ids() {
504 // Tab A holds panes {1,2} and tab B holds {1,2}; the user demotes A
505 // into B. Ids are allocated per tab, so B is handed two panes claiming
506 // ids it already uses. `get_pane` searches the tree, so without
507 // renumbering both lookups would resolve to B's *original* panes and
508 // the demoted terminals would be unreachable — a resize, a paste, or a
509 // close would hit the wrong terminal.
510 const TARGET_1: &str = "/target/one";
511 const TARGET_2: &str = "/target/two";
512 const MOVED_1: &str = "/moved/one";
513 const MOVED_2: &str = "/moved/two";
514
515 let mut target = manager_with_two_panes(TARGET_1, TARGET_2);
516
517 // The subtree arriving from another tab, carrying that tab's ids.
518 let subtree = PaneNode::split(
519 SplitDirection::Horizontal,
520 0.5,
521 PaneNode::leaf(stub_pane(1, MOVED_1)),
522 PaneNode::leaf(stub_pane(2, MOVED_2)),
523 );
524
525 let Ok(remap) = target.insert_subtree_at(1, subtree, SplitDirection::Vertical, 0.5) else {
526 panic!("target pane 1 exists, so the insert succeeds");
527 };
528
529 assert_eq!(target.pane_count(), 4, "all four panes must survive");
530
531 // The lookups are the point: id distinctness below is only the means.
532 for (arrived_as, marker) in [(1, MOVED_1), (2, MOVED_2)] {
533 let now = remap[&arrived_as];
534 assert_eq!(
535 marker_of(&target, now).as_deref(),
536 Some(marker),
537 "get_pane(remap[{arrived_as}]) must resolve to the pane that moved in, \
538 not to the pane that was already holding that id"
539 );
540 assert_ne!(now, arrived_as, "a colliding id must not be kept");
541 }
542
543 for (id, marker) in [(1, TARGET_1), (2, TARGET_2)] {
544 assert_eq!(
545 marker_of(&target, id).as_deref(),
546 Some(marker),
547 "the pane that already held id {id} must keep it"
548 );
549 }
550 }
551
552 #[test]
553 fn an_adopted_pane_keeps_an_id_that_is_free() {
554 let mut target = manager_with_two_panes("/target/one", "/target/two");
555
556 let Ok(remap) = target.insert_subtree_at(
557 1,
558 PaneNode::leaf(stub_pane(9, "/moved")),
559 SplitDirection::Vertical,
560 0.5,
561 ) else {
562 panic!("target pane 1 exists, so the insert succeeds");
563 };
564
565 assert_eq!(remap[&9], 9, "an id that is free must be kept as-is");
566 assert_eq!(marker_of(&target, 9).as_deref(), Some("/moved"));
567 }
568
569 #[test]
570 fn an_adopted_pane_id_is_never_handed_out_again() {
571 // A subtree moved in from another tab brings that tab's ids. Without
572 // advancing the counter this tab would later allocate the same id for
573 // an unrelated pane, and every id-keyed lookup would then be ambiguous.
574 let mut target = manager_with_two_panes("/target/one", "/target/two");
575
576 let inserted = target.insert_subtree_at(
577 1,
578 PaneNode::leaf(stub_pane(9, "/moved")),
579 SplitDirection::Vertical,
580 0.5,
581 );
582 assert!(
583 inserted.is_ok(),
584 "target pane 1 exists, so the insert succeeds"
585 );
586
587 let fresh = target.next_pane_id();
588 let live = target
589 .root
590 .as_ref()
591 .map(PaneNode::all_pane_ids)
592 .unwrap_or_default();
593 assert!(
594 live.iter().all(|id| fresh > *id),
595 "next id {fresh} must be past every adopted id {live:?}"
596 );
597 assert!(
598 !live.contains(&fresh),
599 "the next id {fresh} must be free, but the tree holds {live:?}"
600 );
601 }
602
603 #[test]
604 fn a_replacement_id_dodges_the_ids_the_subtree_keeps() {
605 // Target {1}, subtree {2,1}: the incoming 1 collides and the incoming 2
606 // does not, so a counter seeded only from the target's ids would
607 // renumber the 1 straight onto the 2.
608 let mut target = PaneManager::new();
609 target.root = Some(PaneNode::leaf(stub_pane(1, "/target")));
610 target.focused_pane_id = Some(1);
611 target.next_pane_id = 2;
612
613 let subtree = PaneNode::split(
614 SplitDirection::Horizontal,
615 0.5,
616 PaneNode::leaf(stub_pane(2, "/moved/two")),
617 PaneNode::leaf(stub_pane(1, "/moved/one")),
618 );
619
620 let Ok(remap) = target.insert_subtree_at(1, subtree, SplitDirection::Vertical, 0.5) else {
621 panic!("target pane 1 exists, so the insert succeeds");
622 };
623
624 assert_eq!(remap[&2], 2, "the non-colliding incoming id is kept");
625 assert_ne!(remap[&1], 2, "the replacement must not land on the kept id");
626 assert_eq!(marker_of(&target, remap[&1]).as_deref(), Some("/moved/one"));
627 assert_eq!(marker_of(&target, 2).as_deref(), Some("/moved/two"));
628 assert_eq!(marker_of(&target, 1).as_deref(), Some("/target"));
629 }
630
631 #[test]
632 fn a_failed_insert_leaves_the_tree_untouched() {
633 let mut target = manager_with_two_panes("/target/one", "/target/two");
634
635 let result = target.insert_subtree_at(
636 99,
637 PaneNode::leaf(stub_pane(1, "/moved")),
638 SplitDirection::Vertical,
639 0.5,
640 );
641
642 assert!(result.is_err(), "no such target pane, so no insertion");
643 assert_eq!(target.pane_count(), 2, "the original tree is restored");
644 assert_eq!(marker_of(&target, 1).as_deref(), Some("/target/one"));
645 assert_eq!(marker_of(&target, 2).as_deref(), Some("/target/two"));
646 }
647
648 #[test]
649 fn a_failed_insert_hands_the_subtree_back_alive() {
650 // Dropping the subtree here closes its panes and kills their PTYs, and
651 // the caller reaches this function by detaching the subtree from
652 // somewhere else — so this is the only reference to those terminals.
653 let mut target = manager_with_two_panes("/target/one", "/target/two");
654
655 let subtree = PaneNode::split(
656 SplitDirection::Horizontal,
657 0.5,
658 PaneNode::leaf(stub_pane(1, "/moved/one")),
659 PaneNode::leaf(stub_pane(2, "/moved/two")),
660 );
661
662 // Held across the call so the terminal's liveness is observable
663 // independently of whether the subtree comes back.
664 let kept_terminal = Arc::clone(&subtree.all_panes()[0].terminal);
665 let refs_before = Arc::strong_count(&kept_terminal);
666 let counter_before = target.next_pane_id();
667
668 let Err(returned) = target.insert_subtree_at(99, subtree, SplitDirection::Vertical, 0.5)
669 else {
670 panic!("no pane holds id 99, so the insert must fail");
671 };
672
673 assert_eq!(
674 Arc::strong_count(&kept_terminal),
675 refs_before,
676 "the moved terminal must still be referenced by its pane, not dropped"
677 );
678 assert_eq!(returned.pane_count(), 2, "both moved panes must come back");
679
680 // Ids must come back as they arrived. The replacements handed out
681 // during reconciliation were drawn from *this* manager's counter, and
682 // the caller is about to restore the subtree to a tree that never
683 // agreed to them — restoring renumbered panes would reintroduce exactly
684 // the collision reconciliation exists to prevent.
685 let mut returned_ids = returned.all_pane_ids();
686 returned_ids.sort_unstable();
687 assert_eq!(
688 returned_ids,
689 vec![1, 2],
690 "the subtree must carry the ids it arrived with"
691 );
692 for (id, marker) in [(1, "/moved/one"), (2, "/moved/two")] {
693 assert_eq!(
694 returned
695 .find_pane(id)
696 .and_then(|p| p.working_directory.clone())
697 .as_deref(),
698 Some(marker),
699 "restored id {id} must name the pane that originally held it"
700 );
701 }
702
703 // The target's own panes are untouched, and its counter stays advanced:
704 // the subtree is going back to a tab whose counter is already past
705 // those ids, so ids skipped here cost nothing.
706 assert_eq!(marker_of(&target, 1).as_deref(), Some("/target/one"));
707 assert_eq!(marker_of(&target, 2).as_deref(), Some("/target/two"));
708 assert!(
709 target.next_pane_id() >= counter_before,
710 "the target's counter must not rewind onto an id its own panes hold"
711 );
712 }
713}