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