Skip to main content

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}