Skip to main content

fresh/view/
split.rs

1/// Split view system for displaying multiple buffers simultaneously
2///
3/// Design Philosophy (following Emacs model):
4/// - A split is a tree structure: either a leaf (single buffer) or a node (horizontal/vertical split)
5/// - Each split has a fixed size (in percentage or absolute lines/columns)
6/// - Splits can be nested arbitrarily deep
7/// - Only one split is "active" at a time (receives input)
8/// - Splits can display the same buffer multiple times (useful for viewing different parts)
9///
10/// Example split layouts:
11/// ```text
12/// ┌────────────────────┐      ┌──────────┬─────────┐
13/// │                    │      │          │         │
14/// │   Single buffer    │      │  Buffer  │ Buffer  │
15/// │                    │      │    A     │    B    │
16/// └────────────────────┘      └──────────┴─────────┘
17///   (no split)                  (vertical split)
18///
19/// ┌────────────────────┐      ┌──────────┬─────────┐
20/// │     Buffer A       │      │          │ Buffer C│
21/// ├────────────────────┤      │  Buffer  ├─────────┤
22/// │     Buffer B       │      │    A     │ Buffer D│
23/// └────────────────────┘      └──────────┴─────────┘
24///  (horizontal split)          (mixed splits)
25/// ```
26use crate::model::buffer::Buffer;
27use crate::model::cursor::Cursors;
28use crate::model::event::{BufferId, ContainerId, LeafId, SplitDirection, SplitId};
29use crate::model::marker::MarkerList;
30use crate::view::folding::FoldManager;
31use crate::view::ui::view_pipeline::Layout;
32use crate::view::viewport::Viewport;
33use crate::{services::plugins::api::ViewTransformPayload, state::ViewMode};
34use ratatui::layout::Rect;
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37
38/// A tab target — what a tab entry in a split's tab bar points to.
39///
40/// The tab bar contains a mix of regular buffer tabs and group tabs.
41/// Group tabs point to a `SplitNode::Grouped` node by its `LeafId`.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub enum TabTarget {
44    /// A regular buffer tab
45    Buffer(BufferId),
46    /// A buffer group tab — points to a `SplitNode::Grouped` node's `split_id`
47    Group(LeafId),
48}
49
50impl TabTarget {
51    pub fn as_buffer(self) -> Option<BufferId> {
52        match self {
53            Self::Buffer(id) => Some(id),
54            Self::Group(_) => None,
55        }
56    }
57
58    pub fn as_group(self) -> Option<LeafId> {
59        match self {
60            Self::Buffer(_) => None,
61            Self::Group(id) => Some(id),
62        }
63    }
64}
65
66/// Role tag for special-purpose leaves in the split tree.
67///
68/// At most one leaf in the tree carries any given role (this is the
69/// invariant that makes "tagged singleton dock" work — see
70/// `docs/internal/tui-editor-layout-design.md`, Section 2).
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
72pub enum SplitRole {
73    /// The Utility Dock — diagnostics, search-replace results, terminal,
74    /// quickfix, and other panel-like utilities all swap into this leaf
75    /// instead of spawning new splits.
76    UtilityDock,
77}
78
79/// A node in the split tree
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub enum SplitNode {
82    /// Leaf node: displays a single buffer
83    Leaf {
84        /// Which buffer to display
85        buffer_id: BufferId,
86        /// Unique ID for this split pane
87        split_id: LeafId,
88        /// Optional role tag (e.g. UtilityDock). At most one leaf in
89        /// the tree may carry any given role; the dispatcher routes
90        /// tagged buffer creation to the existing tagged leaf.
91        #[serde(default)]
92        role: Option<SplitRole>,
93    },
94    /// Internal node: contains two child splits
95    Split {
96        /// Direction of the split
97        direction: SplitDirection,
98        /// First child (top or left)
99        first: Box<Self>,
100        /// Second child (bottom or right)
101        second: Box<Self>,
102        /// Size ratio (0.0 to 1.0) - how much space the first child gets
103        /// 0.5 = equal split, 0.3 = first gets 30%, etc.
104        ratio: f32,
105        /// Unique ID for this split container
106        split_id: ContainerId,
107        /// If set, first child gets exactly this many rows/cols instead of using ratio
108        #[serde(default)]
109        fixed_first: Option<u16>,
110        /// If set, second child gets exactly this many rows/cols instead of using ratio
111        #[serde(default)]
112        fixed_second: Option<u16>,
113    },
114    /// A grouped subtree that appears as a single tab entry in its parent
115    /// split's tab bar. When that tab is active, the subtree is expanded
116    /// and rendered inside the parent split's content area. When inactive,
117    /// the node is skipped during rect computation.
118    Grouped {
119        /// Unique ID used as a tab target (see `TabTarget::Group`).
120        /// Behaves like a `LeafId` — identifies this node uniquely.
121        split_id: LeafId,
122        /// Display name shown in the tab bar
123        name: String,
124        /// The nested layout to render when this tab is active
125        layout: Box<Self>,
126        /// The preferred active leaf within the layout (for focus when activating)
127        active_inner_leaf: LeafId,
128    },
129}
130
131/// Per-buffer view state within a split.
132///
133/// Each buffer opened in a split gets its own `BufferViewState` stored in the
134/// split's `keyed_states` map. This ensures that switching buffers within a split
135/// preserves cursor position, scroll state, view mode, and compose settings
136/// independently for each buffer.
137#[derive(Debug)]
138pub struct BufferViewState {
139    /// Independent cursor set (supports multi-cursor)
140    pub cursors: Cursors,
141
142    /// Independent scroll position
143    pub viewport: Viewport,
144
145    /// View mode (Source/Compose) for this buffer in this split
146    pub view_mode: ViewMode,
147
148    /// Optional compose width for centering/wrapping
149    pub compose_width: Option<u16>,
150
151    /// Column guides (e.g., tables)
152    pub compose_column_guides: Option<Vec<u16>>,
153
154    /// Vertical ruler positions (initialized from config, mutable per-buffer)
155    pub rulers: Vec<usize>,
156
157    /// Per-split line number visibility.
158    /// This is the single source of truth for whether line numbers are shown
159    /// in this split. Initialized from config when the split is created.
160    /// Compose mode forces this to false; leaving compose restores from config.
161    pub show_line_numbers: bool,
162
163    /// Per-split current line highlight visibility.
164    /// When true, the line containing the cursor gets a distinct background color.
165    /// Initialized from config when the split is created.
166    pub highlight_current_line: bool,
167
168    /// Optional view transform payload
169    pub view_transform: Option<ViewTransformPayload>,
170
171    /// True when the buffer was edited since the last view_transform_request hook fired.
172    /// While true, incoming SubmitViewTransform commands are rejected as stale
173    /// (their tokens have source_offsets from before the edit).
174    pub view_transform_stale: bool,
175
176    /// Plugin-managed state (arbitrary key-value pairs).
177    /// Plugins can store per-buffer-per-split state here via the `setViewState`/`getViewState` API.
178    /// Persisted across sessions via workspace serialization.
179    pub plugin_state: std::collections::HashMap<String, serde_json::Value>,
180
181    /// Collapsed folding ranges for this buffer/view.
182    pub folds: FoldManager,
183}
184
185impl BufferViewState {
186    /// Resolve fold ranges and ensure the primary cursor is visible.
187    ///
188    /// This is the preferred entry point for all non-rendering callers — it
189    /// resolves hidden fold byte ranges from the marker list and passes them
190    /// to `viewport.ensure_visible` so that line counting skips folded lines.
191    pub fn ensure_cursor_visible(&mut self, buffer: &mut Buffer, marker_list: &MarkerList) {
192        let hidden: Vec<(usize, usize)> = self
193            .folds
194            .resolved_ranges(buffer, marker_list)
195            .into_iter()
196            .map(|r| (r.start_byte, r.end_byte))
197            .collect();
198        let cursor = *self.cursors.primary();
199        self.viewport.ensure_visible(buffer, &cursor, &hidden);
200    }
201
202    /// Create a new buffer view state with defaults
203    pub fn new(width: u16, height: u16) -> Self {
204        Self {
205            cursors: Cursors::new(),
206            viewport: Viewport::new(width, height),
207            view_mode: ViewMode::Source,
208            compose_width: None,
209            compose_column_guides: None,
210            rulers: Vec::new(),
211            show_line_numbers: true,
212            highlight_current_line: true,
213            view_transform: None,
214            view_transform_stale: false,
215            plugin_state: std::collections::HashMap::new(),
216            folds: FoldManager::new(),
217        }
218    }
219
220    /// Apply editor config defaults for display settings.
221    ///
222    /// Sets `show_line_numbers`, `highlight_current_line`, `line_wrap`,
223    /// `wrap_column`, and `rulers` from the given config values. Call this after
224    /// creating a new `BufferViewState` (via `new()` or `ensure_buffer_state()`)
225    /// to ensure the view respects the user's settings.
226    pub fn apply_config_defaults(
227        &mut self,
228        line_numbers: bool,
229        highlight_current_line: bool,
230        line_wrap: bool,
231        wrap_indent: bool,
232        wrap_column: Option<usize>,
233        rulers: Vec<usize>,
234    ) {
235        self.show_line_numbers = line_numbers;
236        self.highlight_current_line = highlight_current_line;
237        self.viewport.line_wrap_enabled = line_wrap;
238        self.viewport.wrap_indent = wrap_indent;
239        self.viewport.wrap_column = wrap_column;
240        self.rulers = rulers;
241    }
242
243    /// Activate page view (compose mode) with an optional page width.
244    ///
245    /// This sets the view mode to Compose, disables builtin line wrap
246    /// (the compose plugin handles wrapping), hides line numbers,
247    /// and optionally sets the compose width for centering.
248    pub fn activate_page_view(&mut self, page_width: Option<usize>) {
249        self.view_mode = ViewMode::PageView;
250        self.show_line_numbers = false;
251        self.viewport.line_wrap_enabled = false;
252        if let Some(width) = page_width {
253            self.compose_width = Some(width as u16);
254        }
255    }
256}
257
258impl Clone for BufferViewState {
259    fn clone(&self) -> Self {
260        Self {
261            cursors: self.cursors.clone(),
262            viewport: self.viewport.clone(),
263            view_mode: self.view_mode.clone(),
264            compose_width: self.compose_width,
265            compose_column_guides: self.compose_column_guides.clone(),
266            rulers: self.rulers.clone(),
267            show_line_numbers: self.show_line_numbers,
268            highlight_current_line: self.highlight_current_line,
269            view_transform: self.view_transform.clone(),
270            view_transform_stale: self.view_transform_stale,
271            plugin_state: self.plugin_state.clone(),
272            // Fold markers are per-view; clones start with no folded ranges.
273            folds: FoldManager::new(),
274        }
275    }
276}
277
278/// Per-split view state (independent of buffer content)
279///
280/// Following the Emacs model where each window (split) has its own:
281/// - Point (cursor position) - independent per split
282/// - Window-start (scroll position) - independent per split
283/// - Tabs (open buffers) - independent per split
284///
285/// Buffer-specific state (cursors, viewport, view_mode, compose settings) is stored
286/// in the `keyed_states` map, keyed by `BufferId`. The active buffer's state is
287/// accessible via `Deref`/`DerefMut` (so `vs.cursors` transparently accesses the
288/// active buffer's cursors), or explicitly via `active_state()`/`active_state_mut()`.
289#[derive(Debug, Clone)]
290pub struct SplitViewState {
291    /// Which buffer is currently active in this split
292    pub active_buffer: BufferId,
293
294    /// Per-buffer view state map. The active buffer always has an entry.
295    pub keyed_states: HashMap<BufferId, BufferViewState>,
296
297    /// List of tab targets open in this split's tab bar (in order).
298    /// Each entry is either a regular buffer or a grouped subtree.
299    /// The currently displayed target is tracked by `active_buffer`
300    /// (for buffer tabs) or by walking the tree for the active leaf
301    /// (for group tabs).
302    pub open_buffers: Vec<TabTarget>,
303
304    /// Horizontal scroll offset for the tabs in this split
305    pub tab_scroll_offset: usize,
306
307    /// Computed layout for this view (from view_transform or base tokens)
308    /// This is View state - each split has its own Layout
309    pub layout: Option<Layout>,
310
311    /// Whether the layout needs to be rebuilt (buffer changed, transform changed, etc.)
312    pub layout_dirty: bool,
313
314    /// Focus history stack for this split (most recent at end).
315    /// Tracks both buffer tabs and group tabs so that "Switch to Previous
316    /// Tab" and close-buffer replacement both work across tab types.
317    pub focus_history: Vec<TabTarget>,
318
319    /// Sync group ID for synchronized scrolling
320    /// Splits with the same sync_group will scroll together
321    pub sync_group: Option<u32>,
322
323    /// When set, this split renders a composite view (e.g., side-by-side diff).
324    /// The split's buffer_id is the focused source buffer, but rendering uses
325    /// the composite layout. This makes the source buffer the "active buffer"
326    /// so normal keybindings work directly.
327    pub composite_view: Option<BufferId>,
328
329    /// When true, suppress per-split chrome (tab bar, close/maximize buttons).
330    /// Used for splits within a buffer group where the group provides its own tab.
331    pub suppress_chrome: bool,
332
333    /// When true, hide tilde markers (~) for empty rows in this split.
334    /// Used for panels where empty space should be blank, not marked.
335    pub hide_tilde: bool,
336
337    /// When `Some(leaf_id)`, the currently "active tab" of this split is the
338    /// buffer group identified by `leaf_id` (i.e., `TabTarget::Group(leaf_id)`).
339    /// When `None`, the active tab is a regular buffer (`TabTarget::Buffer(active_buffer)`).
340    pub active_group_tab: Option<LeafId>,
341
342    /// When a group tab is active, this tracks which inner leaf inside the
343    /// group's subtree has keyboard focus.
344    pub focused_group_leaf: Option<LeafId>,
345}
346
347impl std::ops::Deref for SplitViewState {
348    type Target = BufferViewState;
349
350    fn deref(&self) -> &BufferViewState {
351        self.active_state()
352    }
353}
354
355impl std::ops::DerefMut for SplitViewState {
356    fn deref_mut(&mut self) -> &mut BufferViewState {
357        self.active_state_mut()
358    }
359}
360
361impl SplitViewState {
362    /// Create a new split view state with an initial buffer open
363    pub fn with_buffer(width: u16, height: u16, buffer_id: BufferId) -> Self {
364        let buf_state = BufferViewState::new(width, height);
365        let mut keyed_states = HashMap::new();
366        keyed_states.insert(buffer_id, buf_state);
367        Self {
368            active_buffer: buffer_id,
369            keyed_states,
370            open_buffers: vec![TabTarget::Buffer(buffer_id)],
371            tab_scroll_offset: 0,
372            layout: None,
373            layout_dirty: true,
374            focus_history: Vec::new(),
375            sync_group: None,
376            composite_view: None,
377            suppress_chrome: false,
378            hide_tilde: false,
379            active_group_tab: None,
380            focused_group_leaf: None,
381        }
382    }
383
384    /// Get the active buffer's view state
385    pub fn active_state(&self) -> &BufferViewState {
386        self.keyed_states
387            .get(&self.active_buffer)
388            .expect("active_buffer must always have an entry in keyed_states")
389    }
390
391    /// Get a mutable reference to the active buffer's view state
392    pub fn active_state_mut(&mut self) -> &mut BufferViewState {
393        self.keyed_states
394            .get_mut(&self.active_buffer)
395            .expect("active_buffer must always have an entry in keyed_states")
396    }
397
398    /// Switch the active buffer in this split.
399    ///
400    /// If the new buffer has a saved state in `keyed_states`, it is restored.
401    /// Otherwise a default `BufferViewState` is created with the split's current
402    /// viewport dimensions.
403    pub fn switch_buffer(&mut self, new_buffer_id: BufferId) {
404        if new_buffer_id == self.active_buffer {
405            return;
406        }
407        // Ensure the new buffer has keyed state (create default if first time)
408        if !self.keyed_states.contains_key(&new_buffer_id) {
409            let active = self.active_state();
410            let width = active.viewport.width;
411            let height = active.viewport.height;
412            self.keyed_states
413                .insert(new_buffer_id, BufferViewState::new(width, height));
414        }
415        self.active_buffer = new_buffer_id;
416        // Invalidate layout since we're now showing different buffer content
417        self.layout_dirty = true;
418    }
419
420    /// Get the view state for a specific buffer (if it exists)
421    pub fn buffer_state(&self, buffer_id: BufferId) -> Option<&BufferViewState> {
422        self.keyed_states.get(&buffer_id)
423    }
424
425    /// Get a mutable reference to the view state for a specific buffer (if it exists)
426    pub fn buffer_state_mut(&mut self, buffer_id: BufferId) -> Option<&mut BufferViewState> {
427        self.keyed_states.get_mut(&buffer_id)
428    }
429
430    /// Ensure a buffer has keyed state, creating a default if needed.
431    /// Returns a mutable reference to the buffer's view state.
432    pub fn ensure_buffer_state(&mut self, buffer_id: BufferId) -> &mut BufferViewState {
433        let (width, height) = {
434            let active = self.active_state();
435            (active.viewport.width, active.viewport.height)
436        };
437        self.keyed_states
438            .entry(buffer_id)
439            .or_insert_with(|| BufferViewState::new(width, height))
440    }
441
442    /// Remove keyed state for a buffer (when buffer is closed from this split)
443    pub fn remove_buffer_state(&mut self, buffer_id: BufferId) {
444        if buffer_id != self.active_buffer {
445            self.keyed_states.remove(&buffer_id);
446        }
447    }
448
449    /// Mark layout as needing rebuild (call after buffer changes)
450    pub fn invalidate_layout(&mut self) {
451        self.layout_dirty = true;
452    }
453
454    /// Ensure layout is valid, rebuilding if needed.
455    /// Returns the Layout - never returns None. Following VSCode's ViewModel pattern.
456    ///
457    /// # Arguments
458    /// * `tokens` - ViewTokenWire array (from view_transform or built from buffer)
459    /// * `source_range` - The byte range this layout covers
460    /// * `tab_size` - Tab width for rendering
461    pub fn ensure_layout(
462        &mut self,
463        tokens: &[fresh_core::api::ViewTokenWire],
464        source_range: std::ops::Range<usize>,
465        tab_size: usize,
466    ) -> &Layout {
467        if self.layout.is_none() || self.layout_dirty {
468            self.layout = Some(Layout::from_tokens(tokens, source_range, tab_size));
469            self.layout_dirty = false;
470        }
471        self.layout.as_ref().unwrap()
472    }
473
474    /// Get the current layout if it exists and is valid
475    pub fn get_layout(&self) -> Option<&Layout> {
476        if self.layout_dirty {
477            None
478        } else {
479            self.layout.as_ref()
480        }
481    }
482
483    /// Add a buffer to this split's tabs (if not already present)
484    pub fn add_buffer(&mut self, buffer_id: BufferId) {
485        if !self.has_buffer(buffer_id) {
486            self.open_buffers.push(TabTarget::Buffer(buffer_id));
487        }
488    }
489
490    /// Remove a buffer from this split's tabs and clean up its keyed state
491    pub fn remove_buffer(&mut self, buffer_id: BufferId) {
492        self.open_buffers
493            .retain(|t| *t != TabTarget::Buffer(buffer_id));
494        // Clean up keyed state (but never remove the active buffer's state)
495        if buffer_id != self.active_buffer {
496            self.keyed_states.remove(&buffer_id);
497        }
498    }
499
500    /// Check if a buffer is open in this split
501    pub fn has_buffer(&self, buffer_id: BufferId) -> bool {
502        self.open_buffers.contains(&TabTarget::Buffer(buffer_id))
503    }
504
505    /// Add a group tab to this split's tabs (if not already present)
506    pub fn add_group(&mut self, leaf_id: LeafId) {
507        if !self.has_group(leaf_id) {
508            self.open_buffers.push(TabTarget::Group(leaf_id));
509        }
510    }
511
512    /// Remove a group tab from this split's tabs
513    pub fn remove_group(&mut self, leaf_id: LeafId) {
514        self.open_buffers
515            .retain(|t| *t != TabTarget::Group(leaf_id));
516    }
517
518    /// Check if a group tab is open in this split
519    pub fn has_group(&self, leaf_id: LeafId) -> bool {
520        self.open_buffers.contains(&TabTarget::Group(leaf_id))
521    }
522
523    /// Iterate over only the buffer-tab ids in open_buffers (skipping groups).
524    pub fn buffer_tab_ids(&self) -> impl Iterator<Item = BufferId> + '_ {
525        self.open_buffers.iter().filter_map(|t| t.as_buffer())
526    }
527
528    /// Collect buffer-tab ids as a Vec<BufferId> (skipping groups).
529    /// Convenience for call sites that need ownership / indexing.
530    pub fn buffer_tab_ids_vec(&self) -> Vec<BufferId> {
531        self.buffer_tab_ids().collect()
532    }
533
534    /// Count only buffer tabs (ignoring group tabs).
535    pub fn buffer_tab_count(&self) -> usize {
536        self.open_buffers
537            .iter()
538            .filter(|t| matches!(t, TabTarget::Buffer(_)))
539            .count()
540    }
541
542    /// Return the effective active tab target for this split.
543    /// If a group tab is marked active, returns `TabTarget::Group`. Otherwise
544    /// returns `TabTarget::Buffer(active_buffer)`.
545    pub fn active_target(&self) -> TabTarget {
546        match self.active_group_tab {
547            Some(leaf_id) => TabTarget::Group(leaf_id),
548            None => TabTarget::Buffer(self.active_buffer),
549        }
550    }
551
552    /// Switch the active tab to a regular buffer target. Clears any
553    /// active group tab marker.
554    pub fn set_active_buffer_tab(&mut self, buffer_id: BufferId) {
555        self.active_group_tab = None;
556        self.focused_group_leaf = None;
557        self.switch_buffer(buffer_id);
558    }
559
560    /// Switch the active tab to a group target.
561    pub fn set_active_group_tab(&mut self, leaf_id: LeafId) {
562        self.active_group_tab = Some(leaf_id);
563    }
564
565    /// Push a tab target to the focus history (LRU-style).
566    /// If the target is already in history, it's moved to the end.
567    pub fn push_focus(&mut self, target: TabTarget) {
568        self.focus_history.retain(|t| *t != target);
569        self.focus_history.push(target);
570        if self.focus_history.len() > 50 {
571            self.focus_history.remove(0);
572        }
573    }
574
575    /// Get the most recently focused tab target (without removing it)
576    pub fn previous_tab(&self) -> Option<TabTarget> {
577        self.focus_history.last().copied()
578    }
579
580    /// Remove a buffer from the focus history (called when buffer is closed)
581    pub fn remove_from_history(&mut self, buffer_id: BufferId) {
582        self.focus_history
583            .retain(|t| *t != TabTarget::Buffer(buffer_id));
584    }
585
586    /// Remove a group from the focus history (called when group is closed)
587    pub fn remove_group_from_history(&mut self, leaf_id: LeafId) {
588        self.focus_history
589            .retain(|t| *t != TabTarget::Group(leaf_id));
590    }
591}
592
593impl SplitNode {
594    /// Create a new leaf node
595    pub fn leaf(buffer_id: BufferId, split_id: SplitId) -> Self {
596        Self::Leaf {
597            buffer_id,
598            split_id: LeafId(split_id),
599            role: None,
600        }
601    }
602
603    /// Create a new leaf node with a role tag.
604    pub fn leaf_with_role(buffer_id: BufferId, split_id: SplitId, role: SplitRole) -> Self {
605        Self::Leaf {
606            buffer_id,
607            split_id: LeafId(split_id),
608            role: Some(role),
609        }
610    }
611
612    /// Get this leaf's role, if any.
613    pub fn role(&self) -> Option<SplitRole> {
614        match self {
615            Self::Leaf { role, .. } => *role,
616            _ => None,
617        }
618    }
619
620    /// Set this leaf's role. No-op for non-leaf nodes.
621    pub fn set_role(&mut self, new_role: Option<SplitRole>) {
622        if let Self::Leaf { role, .. } = self {
623            *role = new_role;
624        }
625    }
626
627    /// Create a new split node with two children
628    pub fn split(
629        direction: SplitDirection,
630        first: SplitNode,
631        second: SplitNode,
632        ratio: f32,
633        split_id: SplitId,
634    ) -> Self {
635        SplitNode::Split {
636            direction,
637            first: Box::new(first),
638            second: Box::new(second),
639            ratio: ratio.clamp(0.1, 0.9), // Prevent extreme ratios
640            split_id: ContainerId(split_id),
641            fixed_first: None,
642            fixed_second: None,
643        }
644    }
645
646    /// Get the split ID for this node
647    pub fn id(&self) -> SplitId {
648        match self {
649            Self::Leaf { split_id, .. } => split_id.0,
650            Self::Split { split_id, .. } => split_id.0,
651            Self::Grouped { split_id, .. } => split_id.0,
652        }
653    }
654
655    /// Get the buffer ID if this is a leaf node
656    pub fn buffer_id(&self) -> Option<BufferId> {
657        match self {
658            Self::Leaf { buffer_id, .. } => Some(*buffer_id),
659            Self::Split { .. } | Self::Grouped { .. } => None,
660        }
661    }
662
663    /// Find a split by ID (returns mutable reference).
664    /// Grouped nodes are found by their `split_id`, and their inner
665    /// layout is searched as well.
666    pub fn find_mut(&mut self, target_id: SplitId) -> Option<&mut Self> {
667        if self.id() == target_id {
668            return Some(self);
669        }
670
671        match self {
672            Self::Leaf { .. } => None,
673            Self::Split { first, second, .. } => first
674                .find_mut(target_id)
675                .or_else(|| second.find_mut(target_id)),
676            Self::Grouped { layout, .. } => layout.find_mut(target_id),
677        }
678    }
679
680    /// Find a split by ID (returns immutable reference).
681    /// Grouped nodes are found by their `split_id`, and their inner
682    /// layout is searched as well.
683    pub fn find(&self, target_id: SplitId) -> Option<&Self> {
684        if self.id() == target_id {
685            return Some(self);
686        }
687
688        match self {
689            Self::Leaf { .. } => None,
690            Self::Split { first, second, .. } => {
691                first.find(target_id).or_else(|| second.find(target_id))
692            }
693            Self::Grouped { layout, .. } => layout.find(target_id),
694        }
695    }
696
697    /// Find the parent container of a given split node.
698    /// For a node inside a Grouped subtree, returns the container within
699    /// the subtree (not the Grouped node itself).
700    pub fn parent_container_of(&self, target_id: SplitId) -> Option<ContainerId> {
701        match self {
702            Self::Leaf { .. } => None,
703            Self::Split {
704                split_id,
705                first,
706                second,
707                ..
708            } => {
709                if first.id() == target_id || second.id() == target_id {
710                    Some(*split_id)
711                } else {
712                    first
713                        .parent_container_of(target_id)
714                        .or_else(|| second.parent_container_of(target_id))
715                }
716            }
717            Self::Grouped { layout, .. } => layout.parent_container_of(target_id),
718        }
719    }
720
721    /// Find the Grouped ancestor node that contains a given target id (by walking
722    /// into Grouped subtrees). Returns the Grouped node's own `split_id` if found.
723    pub fn grouped_ancestor_of(&self, target_id: SplitId) -> Option<LeafId> {
724        match self {
725            Self::Leaf { .. } => None,
726            Self::Split { first, second, .. } => first
727                .grouped_ancestor_of(target_id)
728                .or_else(|| second.grouped_ancestor_of(target_id)),
729            Self::Grouped {
730                split_id, layout, ..
731            } => {
732                if layout.find(target_id).is_some() {
733                    Some(*split_id)
734                } else {
735                    layout.grouped_ancestor_of(target_id)
736                }
737            }
738        }
739    }
740
741    /// Find the Grouped node whose `split_id` matches `target`. Returns
742    /// a reference to the Grouped node (or None).
743    pub fn find_grouped(&self, target: LeafId) -> Option<&Self> {
744        match self {
745            Self::Leaf { .. } => None,
746            Self::Split { first, second, .. } => first
747                .find_grouped(target)
748                .or_else(|| second.find_grouped(target)),
749            Self::Grouped {
750                split_id, layout, ..
751            } => {
752                if *split_id == target {
753                    Some(self)
754                } else {
755                    layout.find_grouped(target)
756                }
757            }
758        }
759    }
760
761    /// Get all leaf nodes (buffer views) with their rectangles.
762    ///
763    /// Grouped nodes always recurse into their inner layout — the layout's
764    /// leaves get the full rect that would have been given to the Grouped
765    /// node. Visibility (which group is "active") is applied elsewhere.
766    pub fn get_leaves_with_rects(&self, rect: Rect) -> Vec<(LeafId, BufferId, Rect)> {
767        match self {
768            Self::Leaf {
769                buffer_id,
770                split_id,
771                ..
772            } => {
773                vec![(*split_id, *buffer_id, rect)]
774            }
775            Self::Split {
776                direction,
777                first,
778                second,
779                ratio,
780                fixed_first,
781                fixed_second,
782                ..
783            } => {
784                let (first_rect, second_rect) =
785                    split_rect_ext(rect, *direction, *ratio, *fixed_first, *fixed_second);
786                let mut leaves = first.get_leaves_with_rects(first_rect);
787                leaves.extend(second.get_leaves_with_rects(second_rect));
788                leaves
789            }
790            Self::Grouped { layout, .. } => layout.get_leaves_with_rects(rect),
791        }
792    }
793
794    /// Walk the tree using an "active group" predicate. For each Grouped node
795    /// encountered, the predicate is called with the Grouped node's split_id;
796    /// if it returns `true`, the node's layout is recursed into (with the
797    /// Grouped node's rect). If `false`, the Grouped node and its subtree are
798    /// skipped entirely (not rendered).
799    pub fn get_visible_leaves_with_rects<F>(
800        &self,
801        rect: Rect,
802        is_group_active: &F,
803    ) -> Vec<(LeafId, BufferId, Rect)>
804    where
805        F: Fn(LeafId) -> bool,
806    {
807        match self {
808            Self::Leaf {
809                buffer_id,
810                split_id,
811                ..
812            } => {
813                vec![(*split_id, *buffer_id, rect)]
814            }
815            Self::Split {
816                direction,
817                first,
818                second,
819                ratio,
820                fixed_first,
821                fixed_second,
822                ..
823            } => {
824                let (first_rect, second_rect) =
825                    split_rect_ext(rect, *direction, *ratio, *fixed_first, *fixed_second);
826                let mut leaves = first.get_visible_leaves_with_rects(first_rect, is_group_active);
827                leaves.extend(second.get_visible_leaves_with_rects(second_rect, is_group_active));
828                leaves
829            }
830            Self::Grouped {
831                split_id, layout, ..
832            } => {
833                if is_group_active(*split_id) {
834                    layout.get_visible_leaves_with_rects(rect, is_group_active)
835                } else {
836                    Vec::new()
837                }
838            }
839        }
840    }
841
842    /// Get all split separator lines (for rendering borders)
843    /// Returns (direction, x, y, length) tuples
844    pub fn get_separators(&self, rect: Rect) -> Vec<(SplitDirection, u16, u16, u16)> {
845        self.get_separators_with_ids(rect)
846            .into_iter()
847            .map(|(_, dir, x, y, len)| (dir, x, y, len))
848            .collect()
849    }
850
851    /// Get all split separator lines with their split IDs (for mouse hit testing)
852    /// Returns (split_id, direction, x, y, length) tuples
853    pub fn get_separators_with_ids(
854        &self,
855        rect: Rect,
856    ) -> Vec<(ContainerId, SplitDirection, u16, u16, u16)> {
857        match self {
858            Self::Leaf { .. } => vec![],
859            Self::Grouped { layout, .. } => layout.get_separators_with_ids(rect),
860            Self::Split {
861                direction,
862                first,
863                second,
864                ratio,
865                split_id,
866                fixed_first,
867                fixed_second,
868            } => {
869                let (first_rect, second_rect) =
870                    split_rect_ext(rect, *direction, *ratio, *fixed_first, *fixed_second);
871                let mut separators = Vec::new();
872
873                // Add separator for this split (in the 1-char gap between first and second)
874                match direction {
875                    SplitDirection::Horizontal => {
876                        // Horizontal split: separator line is between first and second
877                        // y position is at the end of first rect (the gap line)
878                        separators.push((
879                            *split_id,
880                            SplitDirection::Horizontal,
881                            rect.x,
882                            first_rect.y + first_rect.height,
883                            rect.width,
884                        ));
885                    }
886                    SplitDirection::Vertical => {
887                        // Vertical split: separator line is between first and second
888                        // x position is at the end of first rect (the gap column)
889                        separators.push((
890                            *split_id,
891                            SplitDirection::Vertical,
892                            first_rect.x + first_rect.width,
893                            rect.y,
894                            rect.height,
895                        ));
896                    }
897                }
898
899                // Recursively get separators from children
900                separators.extend(first.get_separators_with_ids(first_rect));
901                separators.extend(second.get_separators_with_ids(second_rect));
902                separators
903            }
904        }
905    }
906
907    /// Collect all split IDs in the tree
908    pub fn all_split_ids(&self) -> Vec<SplitId> {
909        let mut ids = vec![self.id()];
910        match self {
911            Self::Leaf { .. } => ids,
912            Self::Split { first, second, .. } => {
913                ids.extend(first.all_split_ids());
914                ids.extend(second.all_split_ids());
915                ids
916            }
917            Self::Grouped { layout, .. } => {
918                ids.extend(layout.all_split_ids());
919                ids
920            }
921        }
922    }
923
924    /// Collect only leaf split IDs (visible buffer splits, not container nodes).
925    /// For Grouped nodes, returns the inner layout's leaves.
926    pub fn leaf_split_ids(&self) -> Vec<LeafId> {
927        match self {
928            Self::Leaf { split_id, .. } => vec![*split_id],
929            Self::Split { first, second, .. } => {
930                let mut ids = first.leaf_split_ids();
931                ids.extend(second.leaf_split_ids());
932                ids
933            }
934            Self::Grouped { layout, .. } => layout.leaf_split_ids(),
935        }
936    }
937
938    /// Count the number of leaf nodes (visible buffers).
939    /// Grouped subtrees count their inner leaves.
940    pub fn count_leaves(&self) -> usize {
941        match self {
942            Self::Leaf { .. } => 1,
943            Self::Split { first, second, .. } => first.count_leaves() + second.count_leaves(),
944            Self::Grouped { layout, .. } => layout.count_leaves(),
945        }
946    }
947
948    /// Collect display names for all Grouped nodes in the tree, keyed by
949    /// their LeafId (which is what `TabTarget::Group` points to).
950    pub fn collect_group_names(&self) -> HashMap<LeafId, String> {
951        let mut map = HashMap::new();
952        self.collect_group_names_into(&mut map);
953        map
954    }
955
956    fn collect_group_names_into(&self, map: &mut HashMap<LeafId, String>) {
957        match self {
958            Self::Leaf { .. } => {}
959            Self::Split { first, second, .. } => {
960                first.collect_group_names_into(map);
961                second.collect_group_names_into(map);
962            }
963            Self::Grouped {
964                split_id,
965                name,
966                layout,
967                ..
968            } => {
969                map.insert(*split_id, name.clone());
970                layout.collect_group_names_into(map);
971            }
972        }
973    }
974}
975
976/// Split a rectangle into two parts based on direction and ratio
977/// Leaves 1 character space for the separator line between splits
978#[cfg(test)]
979fn split_rect(rect: Rect, direction: SplitDirection, ratio: f32) -> (Rect, Rect) {
980    split_rect_ext(rect, direction, ratio, None, None)
981}
982
983fn split_rect_ext(
984    rect: Rect,
985    direction: SplitDirection,
986    ratio: f32,
987    fixed_first: Option<u16>,
988    fixed_second: Option<u16>,
989) -> (Rect, Rect) {
990    match direction {
991        SplitDirection::Horizontal => {
992            // Split into top and bottom, with 1 line for separator
993            let total_height = rect.height.saturating_sub(1); // Reserve 1 line for separator
994            let first_height = if let Some(f) = fixed_first {
995                f.min(total_height)
996            } else if let Some(s) = fixed_second {
997                total_height.saturating_sub(s.min(total_height))
998            } else {
999                (total_height as f32 * ratio).round() as u16
1000            };
1001            let second_height = total_height.saturating_sub(first_height);
1002
1003            let first = Rect {
1004                x: rect.x,
1005                y: rect.y,
1006                width: rect.width,
1007                height: first_height,
1008            };
1009
1010            let second = Rect {
1011                x: rect.x,
1012                y: rect.y + first_height + 1, // +1 for separator
1013                width: rect.width,
1014                height: second_height,
1015            };
1016
1017            (first, second)
1018        }
1019        SplitDirection::Vertical => {
1020            // Split into left and right, with 1 column for separator
1021            let total_width = rect.width.saturating_sub(1); // Reserve 1 column for separator
1022            let first_width = if let Some(f) = fixed_first {
1023                f.min(total_width)
1024            } else if let Some(s) = fixed_second {
1025                total_width.saturating_sub(s.min(total_width))
1026            } else {
1027                (total_width as f32 * ratio).round() as u16
1028            };
1029            let second_width = total_width.saturating_sub(first_width);
1030
1031            let first = Rect {
1032                x: rect.x,
1033                y: rect.y,
1034                width: first_width,
1035                height: rect.height,
1036            };
1037
1038            let second = Rect {
1039                x: rect.x + first_width + 1, // +1 for separator
1040                y: rect.y,
1041                width: second_width,
1042                height: rect.height,
1043            };
1044
1045            (first, second)
1046        }
1047    }
1048}
1049
1050/// Manager for the split view system
1051#[derive(Debug)]
1052pub struct SplitManager {
1053    /// Root of the split tree
1054    root: SplitNode,
1055
1056    /// Currently active split (receives input) — always a leaf
1057    active_split: LeafId,
1058
1059    /// Next split ID to assign
1060    next_split_id: usize,
1061
1062    /// Currently maximized split (if any). When set, only this split is visible.
1063    maximized_split: Option<SplitId>,
1064
1065    /// Labels for leaf splits (e.g., "sidebar" to mark managed splits)
1066    labels: HashMap<SplitId, String>,
1067
1068    /// LRU of leaves that have been the active split, oldest first.
1069    /// `set_active_split` pushes the new active and promotes any
1070    /// existing entry; `last_focused_where` lets callers query the
1071    /// history with an arbitrary predicate (e.g. "last leaf without
1072    /// `SplitRole::UtilityDock`" for file-open routing). Stale
1073    /// entries (leaves that have since been closed) are filtered at
1074    /// read time, not eagerly pruned.
1075    focus_history: Vec<LeafId>,
1076}
1077
1078/// Cap on `SplitManager::focus_history` length. Mirrors the same cap
1079/// used by `SplitViewState::focus_history` (per-split tab focus).
1080const FOCUS_HISTORY_CAP: usize = 50;
1081
1082impl SplitManager {
1083    /// Create a new split manager with a single buffer
1084    pub fn new(buffer_id: BufferId) -> Self {
1085        let split_id = SplitId(0);
1086        Self {
1087            root: SplitNode::leaf(buffer_id, split_id),
1088            active_split: LeafId(split_id),
1089            next_split_id: 1,
1090            maximized_split: None,
1091            labels: HashMap::new(),
1092            focus_history: vec![LeafId(split_id)],
1093        }
1094    }
1095
1096    /// Get the root split node
1097    pub fn root(&self) -> &SplitNode {
1098        &self.root
1099    }
1100
1101    /// Allocate a new unique split ID
1102    pub fn allocate_split_id(&mut self) -> SplitId {
1103        let id = SplitId(self.next_split_id);
1104        self.next_split_id += 1;
1105        id
1106    }
1107
1108    /// Replace the root split tree. The new tree must have unique IDs
1109    /// (allocated via `allocate_split_id`). The caller must also provide
1110    /// the new active leaf ID.
1111    pub fn replace_root(&mut self, new_root: SplitNode, new_active: LeafId) {
1112        self.root = new_root;
1113        self.active_split = new_active;
1114        // None of the previously-tracked focus-history ids exist in
1115        // the new tree. Reseed with just the new active.
1116        self.focus_history.clear();
1117        self.focus_history.push(new_active);
1118    }
1119
1120    /// Get the currently active split ID
1121    pub fn active_split(&self) -> LeafId {
1122        self.active_split
1123    }
1124
1125    /// Set the active split (must be a leaf)
1126    pub fn set_active_split(&mut self, split_id: LeafId) -> bool {
1127        // Verify the split exists
1128        if self.root.find(split_id.into()).is_some() {
1129            self.active_split = split_id;
1130            // Promote (or insert) the new active leaf in the focus
1131            // LRU. Same dedup-and-push pattern as
1132            // `SplitViewState::focus_history` for tab focus.
1133            self.focus_history.retain(|leaf| *leaf != split_id);
1134            self.focus_history.push(split_id);
1135            if self.focus_history.len() > FOCUS_HISTORY_CAP {
1136                self.focus_history.remove(0);
1137            }
1138            true
1139        } else {
1140            false
1141        }
1142    }
1143
1144    /// Role of a leaf split, or `None` if the leaf has no role tag or
1145    /// the id doesn't reference a leaf.
1146    pub fn leaf_role(&self, split_id: LeafId) -> Option<SplitRole> {
1147        self.root.find(split_id.into()).and_then(|node| node.role())
1148    }
1149
1150    /// Walk the focus history newest-first and return the first leaf
1151    /// that satisfies `predicate` and still exists in the tree. Stale
1152    /// entries (leaves closed since they were focused) are skipped.
1153    ///
1154    /// Generic by design: callers compose the dock/role/label/buffer
1155    /// rule they care about. File-open routing uses
1156    /// `|leaf| mgr.leaf_role(leaf) != Some(SplitRole::UtilityDock)`;
1157    /// future panel-aware features can pass their own filters
1158    /// without touching this method.
1159    pub fn last_focused_where<F>(&self, mut predicate: F) -> Option<LeafId>
1160    where
1161        F: FnMut(LeafId) -> bool,
1162    {
1163        self.focus_history
1164            .iter()
1165            .rev()
1166            .copied()
1167            .find(|leaf| self.root.find((*leaf).into()).is_some() && predicate(*leaf))
1168    }
1169
1170    /// Get the buffer ID of the active split (if it's a leaf)
1171    pub fn active_buffer_id(&self) -> Option<BufferId> {
1172        self.root
1173            .find(self.active_split.into())
1174            .and_then(|node| node.buffer_id())
1175    }
1176
1177    /// Get the buffer ID for a specific split (if it's a leaf)
1178    pub fn get_buffer_id(&self, split_id: SplitId) -> Option<BufferId> {
1179        self.root.find(split_id).and_then(|node| node.buffer_id())
1180    }
1181
1182    /// Update the buffer ID of the active split
1183    pub fn set_active_buffer_id(&mut self, new_buffer_id: BufferId) -> bool {
1184        if let Some(SplitNode::Leaf { buffer_id, .. }) =
1185            self.root.find_mut(self.active_split.into())
1186        {
1187            *buffer_id = new_buffer_id;
1188            return true;
1189        }
1190        false
1191    }
1192
1193    /// Update the buffer ID of a specific leaf split
1194    pub fn set_split_buffer(&mut self, leaf_id: LeafId, new_buffer_id: BufferId) {
1195        match self.root.find_mut(leaf_id.into()) {
1196            Some(SplitNode::Leaf { buffer_id, .. }) => {
1197                *buffer_id = new_buffer_id;
1198            }
1199            Some(SplitNode::Split { .. }) => {
1200                unreachable!("LeafId {:?} points to a container", leaf_id)
1201            }
1202            Some(SplitNode::Grouped { .. }) => {
1203                unreachable!("LeafId {:?} points to a Grouped node", leaf_id)
1204            }
1205            None => {
1206                unreachable!("LeafId {:?} not found in split tree", leaf_id)
1207            }
1208        }
1209    }
1210
1211    // allocate_split_id is defined as pub earlier in this impl block
1212
1213    /// Split the currently active pane
1214    pub fn split_active(
1215        &mut self,
1216        direction: SplitDirection,
1217        new_buffer_id: BufferId,
1218        ratio: f32,
1219    ) -> Result<LeafId, String> {
1220        self.split_active_positioned(direction, new_buffer_id, ratio, false)
1221    }
1222
1223    /// Split the active pane, placing the new buffer before (left/top) the existing content.
1224    /// `ratio` still controls the first child's proportion of space.
1225    pub fn split_active_before(
1226        &mut self,
1227        direction: SplitDirection,
1228        new_buffer_id: BufferId,
1229        ratio: f32,
1230    ) -> Result<LeafId, String> {
1231        self.split_active_positioned(direction, new_buffer_id, ratio, true)
1232    }
1233
1234    pub fn split_active_positioned(
1235        &mut self,
1236        direction: SplitDirection,
1237        new_buffer_id: BufferId,
1238        ratio: f32,
1239        before: bool,
1240    ) -> Result<LeafId, String> {
1241        let active_id: SplitId = self.active_split.into();
1242
1243        // Find the parent of the active split
1244        let result =
1245            self.replace_split_with_split(active_id, direction, new_buffer_id, ratio, before);
1246
1247        if let Ok(new_split_id) = &result {
1248            // Set the new split as active
1249            self.active_split = *new_split_id;
1250        }
1251        result
1252    }
1253
1254    /// Split the root of the tree (rather than the active leaf), so the
1255    /// new leaf becomes a sibling of the entire existing layout. Used
1256    /// by the Utility Dock so the dock spans the full width below any
1257    /// pre-existing horizontal-axis splits, instead of nesting under
1258    /// whichever pane happened to be active.
1259    ///
1260    /// `ratio` controls the first child's proportion. `before = false`
1261    /// places the new leaf after (right/bottom) the existing root.
1262    pub fn split_root_positioned(
1263        &mut self,
1264        direction: SplitDirection,
1265        new_buffer_id: BufferId,
1266        ratio: f32,
1267        before: bool,
1268    ) -> Result<LeafId, String> {
1269        let root_id = self.root.id();
1270        let result =
1271            self.replace_split_with_split(root_id, direction, new_buffer_id, ratio, before);
1272        if let Ok(new_split_id) = &result {
1273            self.active_split = *new_split_id;
1274        }
1275        result
1276    }
1277
1278    /// Replace a split with a new split container.
1279    /// When `before` is true, the new buffer is placed as the first child (left/top).
1280    fn replace_split_with_split(
1281        &mut self,
1282        target_id: SplitId,
1283        direction: SplitDirection,
1284        new_buffer_id: BufferId,
1285        ratio: f32,
1286        before: bool,
1287    ) -> Result<LeafId, String> {
1288        // Pre-allocate all IDs before any borrowing
1289        let temp_id = self.allocate_split_id();
1290        let new_split_id = self.allocate_split_id();
1291        let new_leaf_id = self.allocate_split_id();
1292
1293        // Special case: if target is root, replace root
1294        if self.root.id() == target_id {
1295            let old_root =
1296                std::mem::replace(&mut self.root, SplitNode::leaf(new_buffer_id, temp_id));
1297            let new_leaf = SplitNode::leaf(new_buffer_id, new_leaf_id);
1298
1299            let (first, second) = if before {
1300                (new_leaf, old_root)
1301            } else {
1302                (old_root, new_leaf)
1303            };
1304
1305            self.root = SplitNode::split(direction, first, second, ratio, new_split_id);
1306
1307            return Ok(LeafId(new_leaf_id));
1308        }
1309
1310        // Find and replace the target node
1311        if let Some(node) = self.root.find_mut(target_id) {
1312            let old_node = std::mem::replace(node, SplitNode::leaf(new_buffer_id, temp_id));
1313            let new_leaf = SplitNode::leaf(new_buffer_id, new_leaf_id);
1314
1315            let (first, second) = if before {
1316                (new_leaf, old_node)
1317            } else {
1318                (old_node, new_leaf)
1319            };
1320
1321            *node = SplitNode::split(direction, first, second, ratio, new_split_id);
1322
1323            Ok(LeafId(new_leaf_id))
1324        } else {
1325            Err(format!("Split {:?} not found", target_id))
1326        }
1327    }
1328
1329    /// Close a split pane (if not the last one)
1330    pub fn close_split(&mut self, split_id: LeafId) -> Result<(), String> {
1331        // Can't close if it's the only split
1332        if self.root.count_leaves() <= 1 {
1333            return Err("Cannot close the last split".to_string());
1334        }
1335
1336        // Can't close if it's the root and root is a leaf
1337        if self.root.id() == split_id.into() && self.root.buffer_id().is_some() {
1338            return Err("Cannot close the only split".to_string());
1339        }
1340
1341        // If the split being closed is maximized, unmaximize first
1342        if self.maximized_split == Some(split_id.into()) {
1343            self.maximized_split = None;
1344        }
1345
1346        // Collect all split IDs that will be removed (the target and its children)
1347        let removed_ids: Vec<SplitId> = self
1348            .root
1349            .find(split_id.into())
1350            .map(|node| node.all_split_ids())
1351            .unwrap_or_default();
1352
1353        // Find the parent of the split to close
1354        // This requires a parent-tracking traversal
1355        let result = self.remove_split_node(split_id.into());
1356
1357        if result.is_ok() {
1358            // Clean up labels for all removed splits
1359            for id in &removed_ids {
1360                self.labels.remove(id);
1361            }
1362
1363            // If we closed the active split, update active_split to another split
1364            if self.active_split == split_id {
1365                let leaf_ids = self.root.leaf_split_ids();
1366                if let Some(&first_leaf) = leaf_ids.first() {
1367                    self.active_split = first_leaf;
1368                }
1369            }
1370        }
1371
1372        result
1373    }
1374
1375    /// Remove a split node from the tree
1376    fn remove_split_node(&mut self, target_id: SplitId) -> Result<(), String> {
1377        // Special case: removing root
1378        if self.root.id() == target_id {
1379            if let SplitNode::Split { first, .. } = &self.root {
1380                // Replace root with the other child
1381                // Choose first child arbitrarily
1382                self.root = (**first).clone();
1383                return Ok(());
1384            }
1385        }
1386
1387        // Recursively find and remove
1388        Self::remove_child_static(&mut self.root, target_id)
1389    }
1390
1391    /// Helper to remove a child from a split node (static to avoid borrow issues)
1392    fn remove_child_static(node: &mut SplitNode, target_id: SplitId) -> Result<(), String> {
1393        match node {
1394            SplitNode::Leaf { .. } => Err("Target not found".to_string()),
1395            SplitNode::Grouped { layout, .. } => Self::remove_child_static(layout, target_id),
1396            SplitNode::Split { first, second, .. } => {
1397                // Check if either child is the target
1398                if first.id() == target_id {
1399                    // Replace this node with the second child
1400                    *node = (**second).clone();
1401                    Ok(())
1402                } else if second.id() == target_id {
1403                    // Replace this node with the first child
1404                    *node = (**first).clone();
1405                    Ok(())
1406                } else {
1407                    // Recurse into children
1408                    Self::remove_child_static(first, target_id)
1409                        .or_else(|_| Self::remove_child_static(second, target_id))
1410                }
1411            }
1412        }
1413    }
1414
1415    /// Remove a Grouped node from the tree by its split_id. Unlike
1416    /// `close_split` which requires a leaf, this removes a whole Grouped
1417    /// subtree (tab) from the split structure. The Grouped node is
1418    /// replaced with... well, nothing — so this can only succeed if the
1419    /// Grouped is inside a Split (so we can replace the Split with its
1420    /// sibling) or if the root itself is the Grouped (which we can't
1421    /// remove without a replacement).
1422    pub fn remove_grouped(&mut self, target: LeafId) -> Result<(), String> {
1423        let target_id: SplitId = target.into();
1424        if self.root.id() == target_id {
1425            return Err("Cannot remove root Grouped node".to_string());
1426        }
1427        Self::remove_child_static(&mut self.root, target_id)
1428    }
1429
1430    /// Adjust the split ratio of a container
1431    pub fn adjust_ratio(&mut self, container_id: ContainerId, delta: f32) {
1432        match self.root.find_mut(container_id.into()) {
1433            Some(SplitNode::Split { ratio, .. }) => {
1434                *ratio = (*ratio + delta).clamp(0.1, 0.9);
1435            }
1436            Some(SplitNode::Leaf { .. }) => {
1437                unreachable!("ContainerId {:?} points to a leaf", container_id)
1438            }
1439            Some(SplitNode::Grouped { .. }) => {
1440                unreachable!("ContainerId {:?} points to a Grouped node", container_id)
1441            }
1442            None => {
1443                unreachable!("ContainerId {:?} not found in split tree", container_id)
1444            }
1445        }
1446    }
1447
1448    /// Find the parent container of a leaf
1449    pub fn parent_container_of(&self, leaf_id: LeafId) -> Option<ContainerId> {
1450        self.root.parent_container_of(leaf_id.into())
1451    }
1452
1453    /// Get all visible buffer views with their rectangles
1454    pub fn get_visible_buffers(&self, viewport_rect: Rect) -> Vec<(LeafId, BufferId, Rect)> {
1455        // If a split is maximized, only show that split taking up the full viewport
1456        if let Some(maximized_id) = self.maximized_split {
1457            if let Some(SplitNode::Leaf {
1458                buffer_id,
1459                split_id,
1460                ..
1461            }) = self.root.find(maximized_id)
1462            {
1463                return vec![(*split_id, *buffer_id, viewport_rect)];
1464            }
1465            // Maximized split no longer exists, clear it and fall through
1466        }
1467        self.root.get_leaves_with_rects(viewport_rect)
1468    }
1469
1470    /// Get all split separator positions for rendering borders
1471    /// Returns (direction, x, y, length) tuples
1472    pub fn get_separators(&self, viewport_rect: Rect) -> Vec<(SplitDirection, u16, u16, u16)> {
1473        // No separators when a split is maximized
1474        if self.maximized_split.is_some() {
1475            return vec![];
1476        }
1477        self.root.get_separators(viewport_rect)
1478    }
1479
1480    /// Get all split separator positions with their split IDs (for mouse hit testing)
1481    /// Returns (container_id, direction, x, y, length) tuples
1482    pub fn get_separators_with_ids(
1483        &self,
1484        viewport_rect: Rect,
1485    ) -> Vec<(ContainerId, SplitDirection, u16, u16, u16)> {
1486        // No separators when a split is maximized
1487        if self.maximized_split.is_some() {
1488            return vec![];
1489        }
1490        self.root.get_separators_with_ids(viewport_rect)
1491    }
1492
1493    /// Get the current ratio of a split container
1494    pub fn get_ratio(&self, split_id: SplitId) -> Option<f32> {
1495        if let Some(SplitNode::Split { ratio, .. }) = self.root.find(split_id) {
1496            Some(*ratio)
1497        } else {
1498            None
1499        }
1500    }
1501
1502    /// Set the exact ratio of a split container
1503    pub fn set_ratio(&mut self, container_id: ContainerId, new_ratio: f32) {
1504        match self.root.find_mut(container_id.into()) {
1505            Some(SplitNode::Split { ratio, .. }) => {
1506                *ratio = new_ratio.clamp(0.1, 0.9);
1507            }
1508            Some(SplitNode::Leaf { .. }) => {
1509                unreachable!("ContainerId {:?} points to a leaf", container_id)
1510            }
1511            Some(SplitNode::Grouped { .. }) => {
1512                unreachable!("ContainerId {:?} points to a Grouped node", container_id)
1513            }
1514            None => {
1515                unreachable!("ContainerId {:?} not found in split tree", container_id)
1516            }
1517        }
1518    }
1519
1520    /// Set a fixed size on a split container's first or second child.
1521    /// When set, the child gets exactly this many rows/cols instead of using the ratio.
1522    pub fn set_fixed_size(
1523        &mut self,
1524        container_id: ContainerId,
1525        first: Option<u16>,
1526        second: Option<u16>,
1527    ) {
1528        if let Some(SplitNode::Split {
1529            fixed_first,
1530            fixed_second,
1531            ..
1532        }) = self.root.find_mut(container_id.into())
1533        {
1534            *fixed_first = first;
1535            *fixed_second = second;
1536        }
1537    }
1538
1539    /// Distribute all visible splits evenly
1540    /// This sets the ratios of all container splits so that leaf splits get equal space
1541    pub fn distribute_splits_evenly(&mut self) {
1542        Self::distribute_node_evenly(&mut self.root);
1543    }
1544
1545    /// Recursively distribute a node's splits evenly
1546    /// Returns the number of leaves in this subtree
1547    fn distribute_node_evenly(node: &mut SplitNode) -> usize {
1548        match node {
1549            SplitNode::Leaf { .. } => 1,
1550            SplitNode::Grouped { layout, .. } => Self::distribute_node_evenly(layout),
1551            SplitNode::Split {
1552                first,
1553                second,
1554                ratio,
1555                ..
1556            } => {
1557                let first_leaves = Self::distribute_node_evenly(first);
1558                let second_leaves = Self::distribute_node_evenly(second);
1559                let total_leaves = first_leaves + second_leaves;
1560
1561                // Set ratio so each leaf gets equal space
1562                // ratio = proportion for first pane
1563                *ratio = (first_leaves as f32 / total_leaves as f32).clamp(0.1, 0.9);
1564
1565                total_leaves
1566            }
1567        }
1568    }
1569
1570    /// Navigate to the next split (circular)
1571    pub fn next_split(&mut self) {
1572        // Switching away from a maximized split would route focus to a
1573        // hidden leaf — only the maximized split is rendered — making
1574        // the cursor disappear (issue #1961). Restore the full layout
1575        // first. Mirrors the auto-unmaximize in `close_split`.
1576        self.maximized_split = None;
1577        let leaf_ids = self.root.leaf_split_ids();
1578        if let Some(pos) = leaf_ids.iter().position(|id| *id == self.active_split) {
1579            let next_pos = (pos + 1) % leaf_ids.len();
1580            self.active_split = leaf_ids[next_pos];
1581        }
1582    }
1583
1584    /// Navigate to the previous split (circular)
1585    pub fn prev_split(&mut self) {
1586        // See `next_split` for why we clear the maximized state.
1587        self.maximized_split = None;
1588        let leaf_ids = self.root.leaf_split_ids();
1589        if let Some(pos) = leaf_ids.iter().position(|id| *id == self.active_split) {
1590            let prev_pos = if pos == 0 { leaf_ids.len() } else { pos } - 1;
1591            self.active_split = leaf_ids[prev_pos];
1592        }
1593    }
1594
1595    /// Get all split IDs that display a specific buffer
1596    pub fn splits_for_buffer(&self, target_buffer_id: BufferId) -> Vec<LeafId> {
1597        self.root
1598            .get_leaves_with_rects(Rect {
1599                x: 0,
1600                y: 0,
1601                width: 1,
1602                height: 1,
1603            })
1604            .into_iter()
1605            .filter(|(_, buffer_id, _)| *buffer_id == target_buffer_id)
1606            .map(|(split_id, _, _)| split_id)
1607            .collect()
1608    }
1609
1610    /// Get the buffer ID for a specific leaf split
1611    pub fn buffer_for_split(&self, target_split_id: LeafId) -> Option<BufferId> {
1612        self.root
1613            .get_leaves_with_rects(Rect {
1614                x: 0,
1615                y: 0,
1616                width: 1,
1617                height: 1,
1618            })
1619            .into_iter()
1620            .find(|(split_id, _, _)| *split_id == target_split_id)
1621            .map(|(_, buffer_id, _)| buffer_id)
1622    }
1623
1624    /// Maximize the active split (hide all other splits temporarily)
1625    /// Returns Ok(()) if successful, Err if there's only one split
1626    pub fn maximize_split(&mut self) -> Result<(), String> {
1627        // Can't maximize if there's only one split
1628        if self.root.count_leaves() <= 1 {
1629            return Err("Cannot maximize: only one split exists".to_string());
1630        }
1631
1632        // Can't maximize if already maximized
1633        if self.maximized_split.is_some() {
1634            return Err("A split is already maximized".to_string());
1635        }
1636
1637        // Maximize the active split
1638        self.maximized_split = Some(self.active_split.into());
1639        Ok(())
1640    }
1641
1642    /// Unmaximize the currently maximized split (restore all splits)
1643    /// Returns Ok(()) if successful, Err if no split is maximized
1644    pub fn unmaximize_split(&mut self) -> Result<(), String> {
1645        if self.maximized_split.is_none() {
1646            return Err("No split is maximized".to_string());
1647        }
1648
1649        self.maximized_split = None;
1650        Ok(())
1651    }
1652
1653    /// Check if a split is currently maximized
1654    pub fn is_maximized(&self) -> bool {
1655        self.maximized_split.is_some()
1656    }
1657
1658    /// Get the currently maximized split ID (if any)
1659    pub fn maximized_split(&self) -> Option<SplitId> {
1660        self.maximized_split
1661    }
1662
1663    /// Toggle maximize state for the active split
1664    /// If maximized, unmaximize. If not maximized, maximize.
1665    /// Returns true if maximized, false if ununmaximized.
1666    pub fn toggle_maximize(&mut self) -> Result<bool, String> {
1667        if self.is_maximized() {
1668            self.unmaximize_split()?;
1669            Ok(false)
1670        } else {
1671            self.maximize_split()?;
1672            Ok(true)
1673        }
1674    }
1675
1676    /// Toggle maximize state for a specific leaf split.
1677    ///
1678    /// Used by the mouse handler so that clicking a split's maximize
1679    /// button targets that split rather than whichever split happens
1680    /// to be active. When already maximized, this unmaximizes regardless
1681    /// of which leaf was passed (only the maximized split's chrome is
1682    /// visible while maximized, so the click can only land on it).
1683    pub fn toggle_maximize_for(&mut self, target: LeafId) -> Result<bool, String> {
1684        if self.is_maximized() {
1685            self.unmaximize_split()?;
1686            Ok(false)
1687        } else {
1688            if self.root.count_leaves() <= 1 {
1689                return Err("Cannot maximize: only one split exists".to_string());
1690            }
1691            if self.root.find(target.into()).is_none() {
1692                return Err("Cannot maximize: split not found".to_string());
1693            }
1694            self.maximized_split = Some(target.into());
1695            Ok(true)
1696        }
1697    }
1698
1699    /// Get all leaf split IDs that belong to a specific sync group
1700    pub fn get_splits_in_group(
1701        &self,
1702        group_id: u32,
1703        view_states: &std::collections::HashMap<LeafId, SplitViewState>,
1704    ) -> Vec<LeafId> {
1705        self.root
1706            .leaf_split_ids()
1707            .into_iter()
1708            .filter(|id| {
1709                view_states
1710                    .get(id)
1711                    .and_then(|vs| vs.sync_group)
1712                    .is_some_and(|g| g == group_id)
1713            })
1714            .collect()
1715    }
1716
1717    // === Split labels ===
1718
1719    /// Set a label on a leaf split (e.g., "sidebar")
1720    pub fn set_label(&mut self, split_id: LeafId, label: String) {
1721        self.labels.insert(split_id.into(), label);
1722    }
1723
1724    /// Remove a label from a split
1725    pub fn clear_label(&mut self, split_id: SplitId) {
1726        self.labels.remove(&split_id);
1727    }
1728
1729    /// Get the label for a split (if any)
1730    pub fn get_label(&self, split_id: SplitId) -> Option<&str> {
1731        self.labels.get(&split_id).map(|s| s.as_str())
1732    }
1733
1734    /// Get all split labels (for workspace serialization)
1735    pub fn labels(&self) -> &HashMap<SplitId, String> {
1736        &self.labels
1737    }
1738
1739    /// Set the role tag on a leaf. No-op if `split_id` is not a leaf.
1740    /// Caller is responsible for the "at most one leaf per role" invariant
1741    /// — call `clear_role` on the previous holder first.
1742    pub fn set_leaf_role(&mut self, split_id: LeafId, new_role: Option<SplitRole>) {
1743        if let Some(node) = self.root.find_mut(split_id.into()) {
1744            node.set_role(new_role);
1745        }
1746    }
1747
1748    /// Find the unique leaf carrying the given role, if any.
1749    pub fn find_leaf_by_role(&self, target: SplitRole) -> Option<LeafId> {
1750        fn walk(node: &SplitNode, target: SplitRole) -> Option<LeafId> {
1751            match node {
1752                SplitNode::Leaf {
1753                    role: Some(r),
1754                    split_id,
1755                    ..
1756                } if *r == target => Some(*split_id),
1757                SplitNode::Leaf { .. } => None,
1758                SplitNode::Split { first, second, .. } => {
1759                    walk(first, target).or_else(|| walk(second, target))
1760                }
1761                SplitNode::Grouped { layout, .. } => walk(layout, target),
1762            }
1763        }
1764        walk(&self.root, target)
1765    }
1766
1767    /// Clear any leaf currently carrying the given role. Returns the leaf
1768    /// id whose role was cleared, if one was found. Used to enforce the
1769    /// "at most one leaf per role" invariant when transferring a role.
1770    pub fn clear_role(&mut self, target: SplitRole) -> Option<LeafId> {
1771        let leaf = self.find_leaf_by_role(target)?;
1772        self.set_leaf_role(leaf, None);
1773        Some(leaf)
1774    }
1775
1776    /// Find the first leaf split with the given label
1777    pub fn find_split_by_label(&self, label: &str) -> Option<LeafId> {
1778        self.root
1779            .leaf_split_ids()
1780            .into_iter()
1781            .find(|id| self.labels.get(&(*id).into()).is_some_and(|l| l == label))
1782    }
1783
1784    /// Find the first leaf split without a label
1785    pub fn find_unlabeled_leaf(&self) -> Option<LeafId> {
1786        self.root
1787            .leaf_split_ids()
1788            .into_iter()
1789            .find(|id| !self.labels.contains_key(&(*id).into()))
1790    }
1791}
1792
1793#[cfg(test)]
1794mod tests {
1795    use super::*;
1796
1797    #[test]
1798    fn test_create_split_manager() {
1799        let buffer_id = BufferId(0);
1800        let manager = SplitManager::new(buffer_id);
1801
1802        assert_eq!(manager.active_buffer_id(), Some(buffer_id));
1803        assert_eq!(manager.root().count_leaves(), 1);
1804    }
1805
1806    #[test]
1807    fn test_horizontal_split() {
1808        let buffer_a = BufferId(0);
1809        let buffer_b = BufferId(1);
1810
1811        let mut manager = SplitManager::new(buffer_a);
1812        let result = manager.split_active(SplitDirection::Horizontal, buffer_b, 0.5);
1813
1814        assert!(result.is_ok());
1815        assert_eq!(manager.root().count_leaves(), 2);
1816    }
1817
1818    #[test]
1819    fn test_vertical_split() {
1820        let buffer_a = BufferId(0);
1821        let buffer_b = BufferId(1);
1822
1823        let mut manager = SplitManager::new(buffer_a);
1824        let result = manager.split_active(SplitDirection::Vertical, buffer_b, 0.5);
1825
1826        assert!(result.is_ok());
1827        assert_eq!(manager.root().count_leaves(), 2);
1828    }
1829
1830    #[test]
1831    fn test_nested_splits() {
1832        let buffer_a = BufferId(0);
1833        let buffer_b = BufferId(1);
1834        let buffer_c = BufferId(2);
1835
1836        let mut manager = SplitManager::new(buffer_a);
1837
1838        // Split horizontally
1839        manager
1840            .split_active(SplitDirection::Horizontal, buffer_b, 0.5)
1841            .unwrap();
1842
1843        // Split the second pane vertically
1844        manager
1845            .split_active(SplitDirection::Vertical, buffer_c, 0.5)
1846            .unwrap();
1847
1848        assert_eq!(manager.root().count_leaves(), 3);
1849    }
1850
1851    #[test]
1852    fn test_close_split() {
1853        let buffer_a = BufferId(0);
1854        let buffer_b = BufferId(1);
1855
1856        let mut manager = SplitManager::new(buffer_a);
1857        let new_split = manager
1858            .split_active(SplitDirection::Horizontal, buffer_b, 0.5)
1859            .unwrap();
1860
1861        assert_eq!(manager.root().count_leaves(), 2);
1862
1863        // Close the new split
1864        let result = manager.close_split(new_split);
1865        assert!(result.is_ok());
1866        assert_eq!(manager.root().count_leaves(), 1);
1867    }
1868
1869    #[test]
1870    fn test_cannot_close_last_split() {
1871        let buffer_a = BufferId(0);
1872        let mut manager = SplitManager::new(buffer_a);
1873
1874        let result = manager.close_split(manager.active_split());
1875        assert!(result.is_err());
1876    }
1877
1878    #[test]
1879    fn test_split_rect_horizontal() {
1880        let rect = Rect {
1881            x: 0,
1882            y: 0,
1883            width: 100,
1884            height: 100,
1885        };
1886
1887        let (first, second) = split_rect(rect, SplitDirection::Horizontal, 0.5);
1888
1889        // With 1 line reserved for separator: (100-1)/2 = 49.5 rounds to 50 and 49
1890        assert_eq!(first.height, 50);
1891        assert_eq!(second.height, 49);
1892        assert_eq!(first.width, 100);
1893        assert_eq!(second.width, 100);
1894        assert_eq!(first.y, 0);
1895        assert_eq!(second.y, 51); // first.y + first.height + 1 (separator)
1896    }
1897
1898    #[test]
1899    fn test_split_rect_vertical() {
1900        let rect = Rect {
1901            x: 0,
1902            y: 0,
1903            width: 100,
1904            height: 100,
1905        };
1906
1907        let (first, second) = split_rect(rect, SplitDirection::Vertical, 0.5);
1908
1909        // With 1 column reserved for separator: (100-1)/2 = 49.5 rounds to 50 and 49
1910        assert_eq!(first.width, 50);
1911        assert_eq!(second.width, 49);
1912        assert_eq!(first.height, 100);
1913        assert_eq!(second.height, 100);
1914        assert_eq!(first.x, 0);
1915        assert_eq!(second.x, 51); // first.x + first.width + 1 (separator)
1916    }
1917
1918    // === Split label tests ===
1919
1920    #[test]
1921    fn test_set_and_get_label() {
1922        let mut manager = SplitManager::new(BufferId(0));
1923        let split = manager.active_split();
1924
1925        assert_eq!(manager.get_label(split.into()), None);
1926
1927        manager.set_label(split, "sidebar".to_string());
1928        assert_eq!(manager.get_label(split.into()), Some("sidebar"));
1929    }
1930
1931    #[test]
1932    fn test_clear_label() {
1933        let mut manager = SplitManager::new(BufferId(0));
1934        let split = manager.active_split();
1935
1936        manager.set_label(split, "sidebar".to_string());
1937        assert!(manager.get_label(split.into()).is_some());
1938
1939        manager.clear_label(split.into());
1940        assert_eq!(manager.get_label(split.into()), None);
1941    }
1942
1943    #[test]
1944    fn test_find_split_by_label() {
1945        let mut manager = SplitManager::new(BufferId(0));
1946        let first_split = manager.active_split();
1947
1948        let second_split = manager
1949            .split_active(SplitDirection::Vertical, BufferId(1), 0.5)
1950            .unwrap();
1951
1952        manager.set_label(first_split, "sidebar".to_string());
1953
1954        assert_eq!(manager.find_split_by_label("sidebar"), Some(first_split));
1955        assert_eq!(manager.find_split_by_label("terminal"), None);
1956
1957        // The second split has no label
1958        assert_ne!(manager.find_split_by_label("sidebar"), Some(second_split));
1959    }
1960
1961    #[test]
1962    fn test_find_unlabeled_leaf() {
1963        let mut manager = SplitManager::new(BufferId(0));
1964        let first_split = manager.active_split();
1965
1966        let second_split = manager
1967            .split_active(SplitDirection::Vertical, BufferId(1), 0.5)
1968            .unwrap();
1969
1970        // No labels — first leaf returned
1971        assert!(manager.find_unlabeled_leaf().is_some());
1972
1973        // Label the first split — unlabeled should return the second
1974        manager.set_label(first_split, "sidebar".to_string());
1975        assert_eq!(manager.find_unlabeled_leaf(), Some(second_split));
1976
1977        // Label both — no unlabeled leaf
1978        manager.set_label(second_split, "terminal".to_string());
1979        assert_eq!(manager.find_unlabeled_leaf(), None);
1980    }
1981
1982    #[test]
1983    fn test_close_split_cleans_up_label() {
1984        let mut manager = SplitManager::new(BufferId(0));
1985        let _first_split = manager.active_split();
1986
1987        let second_split = manager
1988            .split_active(SplitDirection::Vertical, BufferId(1), 0.5)
1989            .unwrap();
1990
1991        manager.set_label(second_split, "sidebar".to_string());
1992        assert_eq!(manager.find_split_by_label("sidebar"), Some(second_split));
1993
1994        manager.close_split(second_split).unwrap();
1995
1996        // Label should be cleaned up
1997        assert_eq!(manager.find_split_by_label("sidebar"), None);
1998        assert_eq!(manager.get_label(second_split.into()), None);
1999    }
2000
2001    #[test]
2002    fn test_label_overwrite() {
2003        let mut manager = SplitManager::new(BufferId(0));
2004        let split = manager.active_split();
2005
2006        manager.set_label(split, "sidebar".to_string());
2007        assert_eq!(manager.get_label(split.into()), Some("sidebar"));
2008
2009        manager.set_label(split, "terminal".to_string());
2010        assert_eq!(manager.get_label(split.into()), Some("terminal"));
2011        assert_eq!(manager.find_split_by_label("sidebar"), None);
2012        assert_eq!(manager.find_split_by_label("terminal"), Some(split));
2013    }
2014
2015    #[test]
2016    fn test_find_unlabeled_leaf_single_split_no_label() {
2017        let manager = SplitManager::new(BufferId(0));
2018        // Single unlabeled split — should return it
2019        assert_eq!(manager.find_unlabeled_leaf(), Some(manager.active_split()));
2020    }
2021
2022    #[test]
2023    fn test_find_unlabeled_leaf_single_split_labeled() {
2024        let mut manager = SplitManager::new(BufferId(0));
2025        let split = manager.active_split();
2026        manager.set_label(split, "only".to_string());
2027        // Only split is labeled — returns None
2028        assert_eq!(manager.find_unlabeled_leaf(), None);
2029    }
2030
2031    /// Regression test: opening the Utility Dock when a vertical split
2032    /// already exists must put the dock as a sibling of the *root*, so
2033    /// it spans the full width below both side-by-side panes — not
2034    /// nested under whichever pane was active.
2035    #[test]
2036    fn test_split_root_positioned_with_existing_vertical_split() {
2037        // Set up: root is a vertical split with two leaves (left/right).
2038        let left = BufferId(0);
2039        let right = BufferId(1);
2040        let dock = BufferId(2);
2041        let mut manager = SplitManager::new(left);
2042        manager
2043            .split_active(SplitDirection::Vertical, right, 0.5)
2044            .expect("vertical split");
2045        // Sanity: root is a vertical Split with two leaves, count = 2.
2046        assert!(matches!(
2047            manager.root(),
2048            SplitNode::Split {
2049                direction: SplitDirection::Vertical,
2050                ..
2051            }
2052        ));
2053        assert_eq!(manager.root().count_leaves(), 2);
2054        // Active leaf is the right pane (vertical split sets the new
2055        // leaf active). Buggy behavior would split that leaf and nest
2056        // the dock under it.
2057        let active_before = manager.active_split();
2058
2059        // Act: split the *root* horizontally to add the dock.
2060        let dock_leaf = manager
2061            .split_root_positioned(SplitDirection::Horizontal, dock, 0.7, false)
2062            .expect("split_root_positioned");
2063
2064        // Assert: root is now a Horizontal Split whose first child is
2065        // the original Vertical split and whose second child is the
2066        // new dock leaf. The original two leaves remain siblings of
2067        // each other (still under the inner Vertical split).
2068        match manager.root() {
2069            SplitNode::Split {
2070                direction: SplitDirection::Horizontal,
2071                first,
2072                second,
2073                ..
2074            } => {
2075                assert!(
2076                    matches!(
2077                        first.as_ref(),
2078                        SplitNode::Split {
2079                            direction: SplitDirection::Vertical,
2080                            ..
2081                        }
2082                    ),
2083                    "first child of new root must be the original Vertical split, got {:?}",
2084                    first
2085                );
2086                match second.as_ref() {
2087                    SplitNode::Leaf {
2088                        buffer_id,
2089                        split_id,
2090                        ..
2091                    } => {
2092                        assert_eq!(*buffer_id, dock, "second child must be the dock leaf");
2093                        assert_eq!(
2094                            *split_id, dock_leaf,
2095                            "split_root_positioned must return the new leaf id"
2096                        );
2097                    }
2098                    other => panic!("expected dock leaf as second child, got {:?}", other),
2099                }
2100            }
2101            other => {
2102                panic!(
2103                    "root must be a Horizontal Split after split_root_positioned, got {:?}",
2104                    other
2105                );
2106            }
2107        }
2108        // Total leaf count went from 2 → 3.
2109        assert_eq!(manager.root().count_leaves(), 3);
2110        // The dock leaf must not be the previously-active leaf — it
2111        // must be a freshly-created sibling of the root.
2112        assert_ne!(
2113            dock_leaf, active_before,
2114            "dock must be a new sibling of the root, not the previously-active leaf"
2115        );
2116    }
2117
2118    /// Regression test for issue #1961: navigating to the next split
2119    /// while a split is maximized must unmaximize first, otherwise the
2120    /// newly-active split is hidden behind the maximized split's
2121    /// full-viewport rendering and the cursor "disappears".
2122    #[test]
2123    fn test_next_split_unmaximizes_when_maximized() {
2124        let buffer_a = BufferId(0);
2125        let buffer_b = BufferId(1);
2126
2127        let mut manager = SplitManager::new(buffer_a);
2128        manager
2129            .split_active(SplitDirection::Vertical, buffer_b, 0.5)
2130            .expect("vertical split");
2131        let first_active = manager.active_split();
2132
2133        manager.maximize_split().expect("maximize");
2134        assert!(manager.is_maximized());
2135
2136        manager.next_split();
2137
2138        assert!(
2139            !manager.is_maximized(),
2140            "next_split must unmaximize so the newly-active split is visible"
2141        );
2142        assert_ne!(
2143            manager.active_split(),
2144            first_active,
2145            "next_split must actually move to a different split"
2146        );
2147    }
2148
2149    /// Companion regression test for issue #1961 covering `prev_split`,
2150    /// which shares the same hidden-split hazard as `next_split`.
2151    #[test]
2152    fn test_prev_split_unmaximizes_when_maximized() {
2153        let buffer_a = BufferId(0);
2154        let buffer_b = BufferId(1);
2155
2156        let mut manager = SplitManager::new(buffer_a);
2157        manager
2158            .split_active(SplitDirection::Vertical, buffer_b, 0.5)
2159            .expect("vertical split");
2160        let first_active = manager.active_split();
2161
2162        manager.maximize_split().expect("maximize");
2163        assert!(manager.is_maximized());
2164
2165        manager.prev_split();
2166
2167        assert!(
2168            !manager.is_maximized(),
2169            "prev_split must unmaximize so the newly-active split is visible"
2170        );
2171        assert_ne!(
2172            manager.active_split(),
2173            first_active,
2174            "prev_split must actually move to a different split"
2175        );
2176    }
2177}