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 node in the split tree
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub enum SplitNode {
41    /// Leaf node: displays a single buffer
42    Leaf {
43        /// Which buffer to display
44        buffer_id: BufferId,
45        /// Unique ID for this split pane
46        split_id: LeafId,
47    },
48    /// Internal node: contains two child splits
49    Split {
50        /// Direction of the split
51        direction: SplitDirection,
52        /// First child (top or left)
53        first: Box<Self>,
54        /// Second child (bottom or right)
55        second: Box<Self>,
56        /// Size ratio (0.0 to 1.0) - how much space the first child gets
57        /// 0.5 = equal split, 0.3 = first gets 30%, etc.
58        ratio: f32,
59        /// Unique ID for this split container
60        split_id: ContainerId,
61    },
62}
63
64/// Per-buffer view state within a split.
65///
66/// Each buffer opened in a split gets its own `BufferViewState` stored in the
67/// split's `keyed_states` map. This ensures that switching buffers within a split
68/// preserves cursor position, scroll state, view mode, and compose settings
69/// independently for each buffer.
70#[derive(Debug)]
71pub struct BufferViewState {
72    /// Independent cursor set (supports multi-cursor)
73    pub cursors: Cursors,
74
75    /// Independent scroll position
76    pub viewport: Viewport,
77
78    /// View mode (Source/Compose) for this buffer in this split
79    pub view_mode: ViewMode,
80
81    /// Optional compose width for centering/wrapping
82    pub compose_width: Option<u16>,
83
84    /// Column guides (e.g., tables)
85    pub compose_column_guides: Option<Vec<u16>>,
86
87    /// Vertical ruler positions (initialized from config, mutable per-buffer)
88    pub rulers: Vec<usize>,
89
90    /// Per-split line number visibility.
91    /// This is the single source of truth for whether line numbers are shown
92    /// in this split. Initialized from config when the split is created.
93    /// Compose mode forces this to false; leaving compose restores from config.
94    pub show_line_numbers: bool,
95
96    /// Optional view transform payload
97    pub view_transform: Option<ViewTransformPayload>,
98
99    /// True when the buffer was edited since the last view_transform_request hook fired.
100    /// While true, incoming SubmitViewTransform commands are rejected as stale
101    /// (their tokens have source_offsets from before the edit).
102    pub view_transform_stale: bool,
103
104    /// Plugin-managed state (arbitrary key-value pairs).
105    /// Plugins can store per-buffer-per-split state here via the `setViewState`/`getViewState` API.
106    /// Persisted across sessions via workspace serialization.
107    pub plugin_state: std::collections::HashMap<String, serde_json::Value>,
108
109    /// Collapsed folding ranges for this buffer/view.
110    pub folds: FoldManager,
111}
112
113impl BufferViewState {
114    /// Resolve fold ranges and ensure the primary cursor is visible.
115    ///
116    /// This is the preferred entry point for all non-rendering callers — it
117    /// resolves hidden fold byte ranges from the marker list and passes them
118    /// to `viewport.ensure_visible` so that line counting skips folded lines.
119    pub fn ensure_cursor_visible(&mut self, buffer: &mut Buffer, marker_list: &MarkerList) {
120        let hidden: Vec<(usize, usize)> = self
121            .folds
122            .resolved_ranges(buffer, marker_list)
123            .into_iter()
124            .map(|r| (r.start_byte, r.end_byte))
125            .collect();
126        let cursor = *self.cursors.primary();
127        self.viewport.ensure_visible(buffer, &cursor, &hidden);
128    }
129
130    /// Create a new buffer view state with defaults
131    pub fn new(width: u16, height: u16) -> Self {
132        Self {
133            cursors: Cursors::new(),
134            viewport: Viewport::new(width, height),
135            view_mode: ViewMode::Source,
136            compose_width: None,
137            compose_column_guides: None,
138            rulers: Vec::new(),
139            show_line_numbers: true,
140            view_transform: None,
141            view_transform_stale: false,
142            plugin_state: std::collections::HashMap::new(),
143            folds: FoldManager::new(),
144        }
145    }
146
147    /// Apply editor config defaults for display settings.
148    ///
149    /// Sets `show_line_numbers`, `line_wrap`, and `rulers` from the given
150    /// config values. Call this after creating a new `BufferViewState` (via
151    /// `new()` or `ensure_buffer_state()`) to ensure the view respects the
152    /// user's settings.
153    pub fn apply_config_defaults(
154        &mut self,
155        line_numbers: bool,
156        line_wrap: bool,
157        wrap_indent: bool,
158        rulers: Vec<usize>,
159    ) {
160        self.show_line_numbers = line_numbers;
161        self.viewport.line_wrap_enabled = line_wrap;
162        self.viewport.wrap_indent = wrap_indent;
163        self.rulers = rulers;
164    }
165}
166
167impl Clone for BufferViewState {
168    fn clone(&self) -> Self {
169        Self {
170            cursors: self.cursors.clone(),
171            viewport: self.viewport.clone(),
172            view_mode: self.view_mode.clone(),
173            compose_width: self.compose_width,
174            compose_column_guides: self.compose_column_guides.clone(),
175            rulers: self.rulers.clone(),
176            show_line_numbers: self.show_line_numbers,
177            view_transform: self.view_transform.clone(),
178            view_transform_stale: self.view_transform_stale,
179            plugin_state: self.plugin_state.clone(),
180            // Fold markers are per-view; clones start with no folded ranges.
181            folds: FoldManager::new(),
182        }
183    }
184}
185
186/// Per-split view state (independent of buffer content)
187///
188/// Following the Emacs model where each window (split) has its own:
189/// - Point (cursor position) - independent per split
190/// - Window-start (scroll position) - independent per split
191/// - Tabs (open buffers) - independent per split
192///
193/// Buffer-specific state (cursors, viewport, view_mode, compose settings) is stored
194/// in the `keyed_states` map, keyed by `BufferId`. The active buffer's state is
195/// accessible via `Deref`/`DerefMut` (so `vs.cursors` transparently accesses the
196/// active buffer's cursors), or explicitly via `active_state()`/`active_state_mut()`.
197#[derive(Debug, Clone)]
198pub struct SplitViewState {
199    /// Which buffer is currently active in this split
200    pub active_buffer: BufferId,
201
202    /// Per-buffer view state map. The active buffer always has an entry.
203    pub keyed_states: HashMap<BufferId, BufferViewState>,
204
205    /// List of buffer IDs open in this split's tab bar (in order)
206    /// The currently displayed buffer is tracked in the SplitNode::Leaf
207    pub open_buffers: Vec<BufferId>,
208
209    /// Horizontal scroll offset for the tabs in this split
210    pub tab_scroll_offset: usize,
211
212    /// Computed layout for this view (from view_transform or base tokens)
213    /// This is View state - each split has its own Layout
214    pub layout: Option<Layout>,
215
216    /// Whether the layout needs to be rebuilt (buffer changed, transform changed, etc.)
217    pub layout_dirty: bool,
218
219    /// Focus history stack for this split (most recent at end)
220    /// Used for "Switch to Previous Tab" and for returning to previous buffer when closing
221    pub focus_history: Vec<BufferId>,
222
223    /// Sync group ID for synchronized scrolling
224    /// Splits with the same sync_group will scroll together
225    pub sync_group: Option<u32>,
226
227    /// When set, this split renders a composite view (e.g., side-by-side diff).
228    /// The split's buffer_id is the focused source buffer, but rendering uses
229    /// the composite layout. This makes the source buffer the "active buffer"
230    /// so normal keybindings work directly.
231    pub composite_view: Option<BufferId>,
232}
233
234impl std::ops::Deref for SplitViewState {
235    type Target = BufferViewState;
236
237    fn deref(&self) -> &BufferViewState {
238        self.active_state()
239    }
240}
241
242impl std::ops::DerefMut for SplitViewState {
243    fn deref_mut(&mut self) -> &mut BufferViewState {
244        self.active_state_mut()
245    }
246}
247
248impl SplitViewState {
249    /// Create a new split view state with an initial buffer open
250    pub fn with_buffer(width: u16, height: u16, buffer_id: BufferId) -> Self {
251        let buf_state = BufferViewState::new(width, height);
252        let mut keyed_states = HashMap::new();
253        keyed_states.insert(buffer_id, buf_state);
254        Self {
255            active_buffer: buffer_id,
256            keyed_states,
257            open_buffers: vec![buffer_id],
258            tab_scroll_offset: 0,
259            layout: None,
260            layout_dirty: true,
261            focus_history: Vec::new(),
262            sync_group: None,
263            composite_view: None,
264        }
265    }
266
267    /// Get the active buffer's view state
268    pub fn active_state(&self) -> &BufferViewState {
269        self.keyed_states
270            .get(&self.active_buffer)
271            .expect("active_buffer must always have an entry in keyed_states")
272    }
273
274    /// Get a mutable reference to the active buffer's view state
275    pub fn active_state_mut(&mut self) -> &mut BufferViewState {
276        self.keyed_states
277            .get_mut(&self.active_buffer)
278            .expect("active_buffer must always have an entry in keyed_states")
279    }
280
281    /// Switch the active buffer in this split.
282    ///
283    /// If the new buffer has a saved state in `keyed_states`, it is restored.
284    /// Otherwise a default `BufferViewState` is created with the split's current
285    /// viewport dimensions.
286    pub fn switch_buffer(&mut self, new_buffer_id: BufferId) {
287        if new_buffer_id == self.active_buffer {
288            return;
289        }
290        // Ensure the new buffer has keyed state (create default if first time)
291        if !self.keyed_states.contains_key(&new_buffer_id) {
292            let active = self.active_state();
293            let width = active.viewport.width;
294            let height = active.viewport.height;
295            self.keyed_states
296                .insert(new_buffer_id, BufferViewState::new(width, height));
297        }
298        self.active_buffer = new_buffer_id;
299        // Invalidate layout since we're now showing different buffer content
300        self.layout_dirty = true;
301    }
302
303    /// Get the view state for a specific buffer (if it exists)
304    pub fn buffer_state(&self, buffer_id: BufferId) -> Option<&BufferViewState> {
305        self.keyed_states.get(&buffer_id)
306    }
307
308    /// Get a mutable reference to the view state for a specific buffer (if it exists)
309    pub fn buffer_state_mut(&mut self, buffer_id: BufferId) -> Option<&mut BufferViewState> {
310        self.keyed_states.get_mut(&buffer_id)
311    }
312
313    /// Ensure a buffer has keyed state, creating a default if needed.
314    /// Returns a mutable reference to the buffer's view state.
315    pub fn ensure_buffer_state(&mut self, buffer_id: BufferId) -> &mut BufferViewState {
316        let (width, height) = {
317            let active = self.active_state();
318            (active.viewport.width, active.viewport.height)
319        };
320        self.keyed_states
321            .entry(buffer_id)
322            .or_insert_with(|| BufferViewState::new(width, height))
323    }
324
325    /// Remove keyed state for a buffer (when buffer is closed from this split)
326    pub fn remove_buffer_state(&mut self, buffer_id: BufferId) {
327        if buffer_id != self.active_buffer {
328            self.keyed_states.remove(&buffer_id);
329        }
330    }
331
332    /// Mark layout as needing rebuild (call after buffer changes)
333    pub fn invalidate_layout(&mut self) {
334        self.layout_dirty = true;
335    }
336
337    /// Ensure layout is valid, rebuilding if needed.
338    /// Returns the Layout - never returns None. Following VSCode's ViewModel pattern.
339    ///
340    /// # Arguments
341    /// * `tokens` - ViewTokenWire array (from view_transform or built from buffer)
342    /// * `source_range` - The byte range this layout covers
343    /// * `tab_size` - Tab width for rendering
344    pub fn ensure_layout(
345        &mut self,
346        tokens: &[fresh_core::api::ViewTokenWire],
347        source_range: std::ops::Range<usize>,
348        tab_size: usize,
349    ) -> &Layout {
350        if self.layout.is_none() || self.layout_dirty {
351            self.layout = Some(Layout::from_tokens(tokens, source_range, tab_size));
352            self.layout_dirty = false;
353        }
354        self.layout.as_ref().unwrap()
355    }
356
357    /// Get the current layout if it exists and is valid
358    pub fn get_layout(&self) -> Option<&Layout> {
359        if self.layout_dirty {
360            None
361        } else {
362            self.layout.as_ref()
363        }
364    }
365
366    /// Add a buffer to this split's tabs (if not already present)
367    pub fn add_buffer(&mut self, buffer_id: BufferId) {
368        if !self.open_buffers.contains(&buffer_id) {
369            self.open_buffers.push(buffer_id);
370        }
371    }
372
373    /// Remove a buffer from this split's tabs and clean up its keyed state
374    pub fn remove_buffer(&mut self, buffer_id: BufferId) {
375        self.open_buffers.retain(|&id| id != buffer_id);
376        // Clean up keyed state (but never remove the active buffer's state)
377        if buffer_id != self.active_buffer {
378            self.keyed_states.remove(&buffer_id);
379        }
380    }
381
382    /// Check if a buffer is open in this split
383    pub fn has_buffer(&self, buffer_id: BufferId) -> bool {
384        self.open_buffers.contains(&buffer_id)
385    }
386
387    /// Push a buffer to the focus history (LRU-style)
388    /// If the buffer is already in history, it's moved to the end
389    pub fn push_focus(&mut self, buffer_id: BufferId) {
390        // Remove if already in history (LRU-style)
391        self.focus_history.retain(|&id| id != buffer_id);
392        self.focus_history.push(buffer_id);
393        // Limit to 50 entries
394        if self.focus_history.len() > 50 {
395            self.focus_history.remove(0);
396        }
397    }
398
399    /// Get the most recently focused buffer (without removing it)
400    pub fn previous_buffer(&self) -> Option<BufferId> {
401        self.focus_history.last().copied()
402    }
403
404    /// Pop the most recent buffer from focus history
405    pub fn pop_focus(&mut self) -> Option<BufferId> {
406        self.focus_history.pop()
407    }
408
409    /// Remove a buffer from the focus history (called when buffer is closed)
410    pub fn remove_from_history(&mut self, buffer_id: BufferId) {
411        self.focus_history.retain(|&id| id != buffer_id);
412    }
413}
414
415impl SplitNode {
416    /// Create a new leaf node
417    pub fn leaf(buffer_id: BufferId, split_id: SplitId) -> Self {
418        Self::Leaf {
419            buffer_id,
420            split_id: LeafId(split_id),
421        }
422    }
423
424    /// Create a new split node with two children
425    pub fn split(
426        direction: SplitDirection,
427        first: SplitNode,
428        second: SplitNode,
429        ratio: f32,
430        split_id: SplitId,
431    ) -> Self {
432        SplitNode::Split {
433            direction,
434            first: Box::new(first),
435            second: Box::new(second),
436            ratio: ratio.clamp(0.1, 0.9), // Prevent extreme ratios
437            split_id: ContainerId(split_id),
438        }
439    }
440
441    /// Get the split ID for this node
442    pub fn id(&self) -> SplitId {
443        match self {
444            Self::Leaf { split_id, .. } => split_id.0,
445            Self::Split { split_id, .. } => split_id.0,
446        }
447    }
448
449    /// Get the buffer ID if this is a leaf node
450    pub fn buffer_id(&self) -> Option<BufferId> {
451        match self {
452            Self::Leaf { buffer_id, .. } => Some(*buffer_id),
453            Self::Split { .. } => None,
454        }
455    }
456
457    /// Find a split by ID (returns mutable reference)
458    pub fn find_mut(&mut self, target_id: SplitId) -> Option<&mut Self> {
459        if self.id() == target_id {
460            return Some(self);
461        }
462
463        match self {
464            Self::Leaf { .. } => None,
465            Self::Split { first, second, .. } => first
466                .find_mut(target_id)
467                .or_else(|| second.find_mut(target_id)),
468        }
469    }
470
471    /// Find a split by ID (returns immutable reference)
472    pub fn find(&self, target_id: SplitId) -> Option<&Self> {
473        if self.id() == target_id {
474            return Some(self);
475        }
476
477        match self {
478            Self::Leaf { .. } => None,
479            Self::Split { first, second, .. } => {
480                first.find(target_id).or_else(|| second.find(target_id))
481            }
482        }
483    }
484
485    /// Find the parent container of a given split node
486    pub fn parent_container_of(&self, target_id: SplitId) -> Option<ContainerId> {
487        match self {
488            Self::Leaf { .. } => None,
489            Self::Split {
490                split_id,
491                first,
492                second,
493                ..
494            } => {
495                if first.id() == target_id || second.id() == target_id {
496                    Some(*split_id)
497                } else {
498                    first
499                        .parent_container_of(target_id)
500                        .or_else(|| second.parent_container_of(target_id))
501                }
502            }
503        }
504    }
505
506    /// Get all leaf nodes (buffer views) with their rectangles
507    pub fn get_leaves_with_rects(&self, rect: Rect) -> Vec<(LeafId, BufferId, Rect)> {
508        match self {
509            Self::Leaf {
510                buffer_id,
511                split_id,
512            } => {
513                vec![(*split_id, *buffer_id, rect)]
514            }
515            Self::Split {
516                direction,
517                first,
518                second,
519                ratio,
520                ..
521            } => {
522                let (first_rect, second_rect) = split_rect(rect, *direction, *ratio);
523                let mut leaves = first.get_leaves_with_rects(first_rect);
524                leaves.extend(second.get_leaves_with_rects(second_rect));
525                leaves
526            }
527        }
528    }
529
530    /// Get all split separator lines (for rendering borders)
531    /// Returns (direction, x, y, length) tuples
532    pub fn get_separators(&self, rect: Rect) -> Vec<(SplitDirection, u16, u16, u16)> {
533        self.get_separators_with_ids(rect)
534            .into_iter()
535            .map(|(_, dir, x, y, len)| (dir, x, y, len))
536            .collect()
537    }
538
539    /// Get all split separator lines with their split IDs (for mouse hit testing)
540    /// Returns (split_id, direction, x, y, length) tuples
541    pub fn get_separators_with_ids(
542        &self,
543        rect: Rect,
544    ) -> Vec<(ContainerId, SplitDirection, u16, u16, u16)> {
545        match self {
546            Self::Leaf { .. } => vec![],
547            Self::Split {
548                direction,
549                first,
550                second,
551                ratio,
552                split_id,
553            } => {
554                let (first_rect, second_rect) = split_rect(rect, *direction, *ratio);
555                let mut separators = Vec::new();
556
557                // Add separator for this split (in the 1-char gap between first and second)
558                match direction {
559                    SplitDirection::Horizontal => {
560                        // Horizontal split: separator line is between first and second
561                        // y position is at the end of first rect (the gap line)
562                        separators.push((
563                            *split_id,
564                            SplitDirection::Horizontal,
565                            rect.x,
566                            first_rect.y + first_rect.height,
567                            rect.width,
568                        ));
569                    }
570                    SplitDirection::Vertical => {
571                        // Vertical split: separator line is between first and second
572                        // x position is at the end of first rect (the gap column)
573                        separators.push((
574                            *split_id,
575                            SplitDirection::Vertical,
576                            first_rect.x + first_rect.width,
577                            rect.y,
578                            rect.height,
579                        ));
580                    }
581                }
582
583                // Recursively get separators from children
584                separators.extend(first.get_separators_with_ids(first_rect));
585                separators.extend(second.get_separators_with_ids(second_rect));
586                separators
587            }
588        }
589    }
590
591    /// Collect all split IDs in the tree
592    pub fn all_split_ids(&self) -> Vec<SplitId> {
593        let mut ids = vec![self.id()];
594        match self {
595            Self::Leaf { .. } => ids,
596            Self::Split { first, second, .. } => {
597                ids.extend(first.all_split_ids());
598                ids.extend(second.all_split_ids());
599                ids
600            }
601        }
602    }
603
604    /// Collect only leaf split IDs (visible buffer splits, not container nodes)
605    pub fn leaf_split_ids(&self) -> Vec<LeafId> {
606        match self {
607            Self::Leaf { split_id, .. } => vec![*split_id],
608            Self::Split { first, second, .. } => {
609                let mut ids = first.leaf_split_ids();
610                ids.extend(second.leaf_split_ids());
611                ids
612            }
613        }
614    }
615
616    /// Count the number of leaf nodes (visible buffers)
617    pub fn count_leaves(&self) -> usize {
618        match self {
619            Self::Leaf { .. } => 1,
620            Self::Split { first, second, .. } => first.count_leaves() + second.count_leaves(),
621        }
622    }
623}
624
625/// Split a rectangle into two parts based on direction and ratio
626/// Leaves 1 character space for the separator line between splits
627fn split_rect(rect: Rect, direction: SplitDirection, ratio: f32) -> (Rect, Rect) {
628    match direction {
629        SplitDirection::Horizontal => {
630            // Split into top and bottom, with 1 line for separator
631            let total_height = rect.height.saturating_sub(1); // Reserve 1 line for separator
632            let first_height = (total_height as f32 * ratio).round() as u16;
633            let second_height = total_height.saturating_sub(first_height);
634
635            let first = Rect {
636                x: rect.x,
637                y: rect.y,
638                width: rect.width,
639                height: first_height,
640            };
641
642            let second = Rect {
643                x: rect.x,
644                y: rect.y + first_height + 1, // +1 for separator
645                width: rect.width,
646                height: second_height,
647            };
648
649            (first, second)
650        }
651        SplitDirection::Vertical => {
652            // Split into left and right, with 1 column for separator
653            let total_width = rect.width.saturating_sub(1); // Reserve 1 column for separator
654            let first_width = (total_width as f32 * ratio).round() as u16;
655            let second_width = total_width.saturating_sub(first_width);
656
657            let first = Rect {
658                x: rect.x,
659                y: rect.y,
660                width: first_width,
661                height: rect.height,
662            };
663
664            let second = Rect {
665                x: rect.x + first_width + 1, // +1 for separator
666                y: rect.y,
667                width: second_width,
668                height: rect.height,
669            };
670
671            (first, second)
672        }
673    }
674}
675
676/// Manager for the split view system
677#[derive(Debug)]
678pub struct SplitManager {
679    /// Root of the split tree
680    root: SplitNode,
681
682    /// Currently active split (receives input) — always a leaf
683    active_split: LeafId,
684
685    /// Next split ID to assign
686    next_split_id: usize,
687
688    /// Currently maximized split (if any). When set, only this split is visible.
689    maximized_split: Option<SplitId>,
690
691    /// Labels for leaf splits (e.g., "sidebar" to mark managed splits)
692    labels: HashMap<SplitId, String>,
693}
694
695impl SplitManager {
696    /// Create a new split manager with a single buffer
697    pub fn new(buffer_id: BufferId) -> Self {
698        let split_id = SplitId(0);
699        Self {
700            root: SplitNode::leaf(buffer_id, split_id),
701            active_split: LeafId(split_id),
702            next_split_id: 1,
703            maximized_split: None,
704            labels: HashMap::new(),
705        }
706    }
707
708    /// Get the root split node
709    pub fn root(&self) -> &SplitNode {
710        &self.root
711    }
712
713    /// Get the currently active split ID
714    pub fn active_split(&self) -> LeafId {
715        self.active_split
716    }
717
718    /// Set the active split (must be a leaf)
719    pub fn set_active_split(&mut self, split_id: LeafId) -> bool {
720        // Verify the split exists
721        if self.root.find(split_id.into()).is_some() {
722            self.active_split = split_id;
723            true
724        } else {
725            false
726        }
727    }
728
729    /// Get the buffer ID of the active split (if it's a leaf)
730    pub fn active_buffer_id(&self) -> Option<BufferId> {
731        self.root
732            .find(self.active_split.into())
733            .and_then(|node| node.buffer_id())
734    }
735
736    /// Get the buffer ID for a specific split (if it's a leaf)
737    pub fn get_buffer_id(&self, split_id: SplitId) -> Option<BufferId> {
738        self.root.find(split_id).and_then(|node| node.buffer_id())
739    }
740
741    /// Update the buffer ID of the active split
742    pub fn set_active_buffer_id(&mut self, new_buffer_id: BufferId) -> bool {
743        if let Some(SplitNode::Leaf { buffer_id, .. }) =
744            self.root.find_mut(self.active_split.into())
745        {
746            *buffer_id = new_buffer_id;
747            return true;
748        }
749        false
750    }
751
752    /// Update the buffer ID of a specific leaf split
753    pub fn set_split_buffer(&mut self, leaf_id: LeafId, new_buffer_id: BufferId) {
754        match self.root.find_mut(leaf_id.into()) {
755            Some(SplitNode::Leaf { buffer_id, .. }) => {
756                *buffer_id = new_buffer_id;
757            }
758            Some(SplitNode::Split { .. }) => {
759                unreachable!("LeafId {:?} points to a container", leaf_id)
760            }
761            None => {
762                unreachable!("LeafId {:?} not found in split tree", leaf_id)
763            }
764        }
765    }
766
767    /// Allocate a new split ID
768    fn allocate_split_id(&mut self) -> SplitId {
769        let id = SplitId(self.next_split_id);
770        self.next_split_id += 1;
771        id
772    }
773
774    /// Split the currently active pane
775    pub fn split_active(
776        &mut self,
777        direction: SplitDirection,
778        new_buffer_id: BufferId,
779        ratio: f32,
780    ) -> Result<LeafId, String> {
781        self.split_active_positioned(direction, new_buffer_id, ratio, false)
782    }
783
784    /// Split the active pane, placing the new buffer before (left/top) the existing content.
785    /// `ratio` still controls the first child's proportion of space.
786    pub fn split_active_before(
787        &mut self,
788        direction: SplitDirection,
789        new_buffer_id: BufferId,
790        ratio: f32,
791    ) -> Result<LeafId, String> {
792        self.split_active_positioned(direction, new_buffer_id, ratio, true)
793    }
794
795    pub fn split_active_positioned(
796        &mut self,
797        direction: SplitDirection,
798        new_buffer_id: BufferId,
799        ratio: f32,
800        before: bool,
801    ) -> Result<LeafId, String> {
802        let active_id: SplitId = self.active_split.into();
803
804        // Find the parent of the active split
805        let result =
806            self.replace_split_with_split(active_id, direction, new_buffer_id, ratio, before);
807
808        if let Ok(new_split_id) = &result {
809            // Set the new split as active
810            self.active_split = *new_split_id;
811        }
812        result
813    }
814
815    /// Replace a split with a new split container.
816    /// When `before` is true, the new buffer is placed as the first child (left/top).
817    fn replace_split_with_split(
818        &mut self,
819        target_id: SplitId,
820        direction: SplitDirection,
821        new_buffer_id: BufferId,
822        ratio: f32,
823        before: bool,
824    ) -> Result<LeafId, String> {
825        // Pre-allocate all IDs before any borrowing
826        let temp_id = self.allocate_split_id();
827        let new_split_id = self.allocate_split_id();
828        let new_leaf_id = self.allocate_split_id();
829
830        // Special case: if target is root, replace root
831        if self.root.id() == target_id {
832            let old_root =
833                std::mem::replace(&mut self.root, SplitNode::leaf(new_buffer_id, temp_id));
834            let new_leaf = SplitNode::leaf(new_buffer_id, new_leaf_id);
835
836            let (first, second) = if before {
837                (new_leaf, old_root)
838            } else {
839                (old_root, new_leaf)
840            };
841
842            self.root = SplitNode::split(direction, first, second, ratio, new_split_id);
843
844            return Ok(LeafId(new_leaf_id));
845        }
846
847        // Find and replace the target node
848        if let Some(node) = self.root.find_mut(target_id) {
849            let old_node = std::mem::replace(node, SplitNode::leaf(new_buffer_id, temp_id));
850            let new_leaf = SplitNode::leaf(new_buffer_id, new_leaf_id);
851
852            let (first, second) = if before {
853                (new_leaf, old_node)
854            } else {
855                (old_node, new_leaf)
856            };
857
858            *node = SplitNode::split(direction, first, second, ratio, new_split_id);
859
860            Ok(LeafId(new_leaf_id))
861        } else {
862            Err(format!("Split {:?} not found", target_id))
863        }
864    }
865
866    /// Close a split pane (if not the last one)
867    pub fn close_split(&mut self, split_id: LeafId) -> Result<(), String> {
868        // Can't close if it's the only split
869        if self.root.count_leaves() <= 1 {
870            return Err("Cannot close the last split".to_string());
871        }
872
873        // Can't close if it's the root and root is a leaf
874        if self.root.id() == split_id.into() && self.root.buffer_id().is_some() {
875            return Err("Cannot close the only split".to_string());
876        }
877
878        // If the split being closed is maximized, unmaximize first
879        if self.maximized_split == Some(split_id.into()) {
880            self.maximized_split = None;
881        }
882
883        // Collect all split IDs that will be removed (the target and its children)
884        let removed_ids: Vec<SplitId> = self
885            .root
886            .find(split_id.into())
887            .map(|node| node.all_split_ids())
888            .unwrap_or_default();
889
890        // Find the parent of the split to close
891        // This requires a parent-tracking traversal
892        let result = self.remove_split_node(split_id.into());
893
894        if result.is_ok() {
895            // Clean up labels for all removed splits
896            for id in &removed_ids {
897                self.labels.remove(id);
898            }
899
900            // If we closed the active split, update active_split to another split
901            if self.active_split == split_id {
902                let leaf_ids = self.root.leaf_split_ids();
903                if let Some(&first_leaf) = leaf_ids.first() {
904                    self.active_split = first_leaf;
905                }
906            }
907        }
908
909        result
910    }
911
912    /// Remove a split node from the tree
913    fn remove_split_node(&mut self, target_id: SplitId) -> Result<(), String> {
914        // Special case: removing root
915        if self.root.id() == target_id {
916            if let SplitNode::Split { first, .. } = &self.root {
917                // Replace root with the other child
918                // Choose first child arbitrarily
919                self.root = (**first).clone();
920                return Ok(());
921            }
922        }
923
924        // Recursively find and remove
925        Self::remove_child_static(&mut self.root, target_id)
926    }
927
928    /// Helper to remove a child from a split node (static to avoid borrow issues)
929    fn remove_child_static(node: &mut SplitNode, target_id: SplitId) -> Result<(), String> {
930        match node {
931            SplitNode::Leaf { .. } => Err("Target not found".to_string()),
932            SplitNode::Split { first, second, .. } => {
933                // Check if either child is the target
934                if first.id() == target_id {
935                    // Replace this node with the second child
936                    *node = (**second).clone();
937                    Ok(())
938                } else if second.id() == target_id {
939                    // Replace this node with the first child
940                    *node = (**first).clone();
941                    Ok(())
942                } else {
943                    // Recurse into children
944                    Self::remove_child_static(first, target_id)
945                        .or_else(|_| Self::remove_child_static(second, target_id))
946                }
947            }
948        }
949    }
950
951    /// Adjust the split ratio of a container
952    pub fn adjust_ratio(&mut self, container_id: ContainerId, delta: f32) {
953        match self.root.find_mut(container_id.into()) {
954            Some(SplitNode::Split { ratio, .. }) => {
955                *ratio = (*ratio + delta).clamp(0.1, 0.9);
956            }
957            Some(SplitNode::Leaf { .. }) => {
958                unreachable!("ContainerId {:?} points to a leaf", container_id)
959            }
960            None => {
961                unreachable!("ContainerId {:?} not found in split tree", container_id)
962            }
963        }
964    }
965
966    /// Find the parent container of a leaf
967    pub fn parent_container_of(&self, leaf_id: LeafId) -> Option<ContainerId> {
968        self.root.parent_container_of(leaf_id.into())
969    }
970
971    /// Get all visible buffer views with their rectangles
972    pub fn get_visible_buffers(&self, viewport_rect: Rect) -> Vec<(LeafId, BufferId, Rect)> {
973        // If a split is maximized, only show that split taking up the full viewport
974        if let Some(maximized_id) = self.maximized_split {
975            if let Some(node) = self.root.find(maximized_id) {
976                if let SplitNode::Leaf {
977                    buffer_id,
978                    split_id,
979                } = node
980                {
981                    return vec![(*split_id, *buffer_id, viewport_rect)];
982                }
983            }
984            // Maximized split no longer exists, clear it and fall through
985        }
986        self.root.get_leaves_with_rects(viewport_rect)
987    }
988
989    /// Get all split separator positions for rendering borders
990    /// Returns (direction, x, y, length) tuples
991    pub fn get_separators(&self, viewport_rect: Rect) -> Vec<(SplitDirection, u16, u16, u16)> {
992        // No separators when a split is maximized
993        if self.maximized_split.is_some() {
994            return vec![];
995        }
996        self.root.get_separators(viewport_rect)
997    }
998
999    /// Get all split separator positions with their split IDs (for mouse hit testing)
1000    /// Returns (container_id, direction, x, y, length) tuples
1001    pub fn get_separators_with_ids(
1002        &self,
1003        viewport_rect: Rect,
1004    ) -> Vec<(ContainerId, SplitDirection, u16, u16, u16)> {
1005        // No separators when a split is maximized
1006        if self.maximized_split.is_some() {
1007            return vec![];
1008        }
1009        self.root.get_separators_with_ids(viewport_rect)
1010    }
1011
1012    /// Get the current ratio of a split container
1013    pub fn get_ratio(&self, split_id: SplitId) -> Option<f32> {
1014        if let Some(SplitNode::Split { ratio, .. }) = self.root.find(split_id) {
1015            Some(*ratio)
1016        } else {
1017            None
1018        }
1019    }
1020
1021    /// Set the exact ratio of a split container
1022    pub fn set_ratio(&mut self, container_id: ContainerId, new_ratio: f32) {
1023        match self.root.find_mut(container_id.into()) {
1024            Some(SplitNode::Split { ratio, .. }) => {
1025                *ratio = new_ratio.clamp(0.1, 0.9);
1026            }
1027            Some(SplitNode::Leaf { .. }) => {
1028                unreachable!("ContainerId {:?} points to a leaf", container_id)
1029            }
1030            None => {
1031                unreachable!("ContainerId {:?} not found in split tree", container_id)
1032            }
1033        }
1034    }
1035
1036    /// Distribute all visible splits evenly
1037    /// This sets the ratios of all container splits so that leaf splits get equal space
1038    pub fn distribute_splits_evenly(&mut self) {
1039        Self::distribute_node_evenly(&mut self.root);
1040    }
1041
1042    /// Recursively distribute a node's splits evenly
1043    /// Returns the number of leaves in this subtree
1044    fn distribute_node_evenly(node: &mut SplitNode) -> usize {
1045        match node {
1046            SplitNode::Leaf { .. } => 1,
1047            SplitNode::Split {
1048                first,
1049                second,
1050                ratio,
1051                ..
1052            } => {
1053                let first_leaves = Self::distribute_node_evenly(first);
1054                let second_leaves = Self::distribute_node_evenly(second);
1055                let total_leaves = first_leaves + second_leaves;
1056
1057                // Set ratio so each leaf gets equal space
1058                // ratio = proportion for first pane
1059                *ratio = (first_leaves as f32 / total_leaves as f32).clamp(0.1, 0.9);
1060
1061                total_leaves
1062            }
1063        }
1064    }
1065
1066    /// Navigate to the next split (circular)
1067    pub fn next_split(&mut self) {
1068        let leaf_ids = self.root.leaf_split_ids();
1069        if let Some(pos) = leaf_ids.iter().position(|id| *id == self.active_split) {
1070            let next_pos = (pos + 1) % leaf_ids.len();
1071            self.active_split = leaf_ids[next_pos];
1072        }
1073    }
1074
1075    /// Navigate to the previous split (circular)
1076    pub fn prev_split(&mut self) {
1077        let leaf_ids = self.root.leaf_split_ids();
1078        if let Some(pos) = leaf_ids.iter().position(|id| *id == self.active_split) {
1079            let prev_pos = if pos == 0 { leaf_ids.len() } else { pos } - 1;
1080            self.active_split = leaf_ids[prev_pos];
1081        }
1082    }
1083
1084    /// Get all split IDs that display a specific buffer
1085    pub fn splits_for_buffer(&self, target_buffer_id: BufferId) -> Vec<LeafId> {
1086        self.root
1087            .get_leaves_with_rects(Rect {
1088                x: 0,
1089                y: 0,
1090                width: 1,
1091                height: 1,
1092            })
1093            .into_iter()
1094            .filter(|(_, buffer_id, _)| *buffer_id == target_buffer_id)
1095            .map(|(split_id, _, _)| split_id)
1096            .collect()
1097    }
1098
1099    /// Get the buffer ID for a specific leaf split
1100    pub fn buffer_for_split(&self, target_split_id: LeafId) -> Option<BufferId> {
1101        self.root
1102            .get_leaves_with_rects(Rect {
1103                x: 0,
1104                y: 0,
1105                width: 1,
1106                height: 1,
1107            })
1108            .into_iter()
1109            .find(|(split_id, _, _)| *split_id == target_split_id)
1110            .map(|(_, buffer_id, _)| buffer_id)
1111    }
1112
1113    /// Maximize the active split (hide all other splits temporarily)
1114    /// Returns Ok(()) if successful, Err if there's only one split
1115    pub fn maximize_split(&mut self) -> Result<(), String> {
1116        // Can't maximize if there's only one split
1117        if self.root.count_leaves() <= 1 {
1118            return Err("Cannot maximize: only one split exists".to_string());
1119        }
1120
1121        // Can't maximize if already maximized
1122        if self.maximized_split.is_some() {
1123            return Err("A split is already maximized".to_string());
1124        }
1125
1126        // Maximize the active split
1127        self.maximized_split = Some(self.active_split.into());
1128        Ok(())
1129    }
1130
1131    /// Unmaximize the currently maximized split (restore all splits)
1132    /// Returns Ok(()) if successful, Err if no split is maximized
1133    pub fn unmaximize_split(&mut self) -> Result<(), String> {
1134        if self.maximized_split.is_none() {
1135            return Err("No split is maximized".to_string());
1136        }
1137
1138        self.maximized_split = None;
1139        Ok(())
1140    }
1141
1142    /// Check if a split is currently maximized
1143    pub fn is_maximized(&self) -> bool {
1144        self.maximized_split.is_some()
1145    }
1146
1147    /// Get the currently maximized split ID (if any)
1148    pub fn maximized_split(&self) -> Option<SplitId> {
1149        self.maximized_split
1150    }
1151
1152    /// Toggle maximize state for the active split
1153    /// If maximized, unmaximize. If not maximized, maximize.
1154    /// Returns true if maximized, false if ununmaximized.
1155    pub fn toggle_maximize(&mut self) -> Result<bool, String> {
1156        if self.is_maximized() {
1157            self.unmaximize_split()?;
1158            Ok(false)
1159        } else {
1160            self.maximize_split()?;
1161            Ok(true)
1162        }
1163    }
1164
1165    /// Get all leaf split IDs that belong to a specific sync group
1166    pub fn get_splits_in_group(
1167        &self,
1168        group_id: u32,
1169        view_states: &std::collections::HashMap<LeafId, SplitViewState>,
1170    ) -> Vec<LeafId> {
1171        self.root
1172            .leaf_split_ids()
1173            .into_iter()
1174            .filter(|id| {
1175                view_states
1176                    .get(id)
1177                    .and_then(|vs| vs.sync_group)
1178                    .is_some_and(|g| g == group_id)
1179            })
1180            .collect()
1181    }
1182
1183    // === Split labels ===
1184
1185    /// Set a label on a leaf split (e.g., "sidebar")
1186    pub fn set_label(&mut self, split_id: LeafId, label: String) {
1187        self.labels.insert(split_id.into(), label);
1188    }
1189
1190    /// Remove a label from a split
1191    pub fn clear_label(&mut self, split_id: SplitId) {
1192        self.labels.remove(&split_id);
1193    }
1194
1195    /// Get the label for a split (if any)
1196    pub fn get_label(&self, split_id: SplitId) -> Option<&str> {
1197        self.labels.get(&split_id).map(|s| s.as_str())
1198    }
1199
1200    /// Get all split labels (for workspace serialization)
1201    pub fn labels(&self) -> &HashMap<SplitId, String> {
1202        &self.labels
1203    }
1204
1205    /// Find the first leaf split with the given label
1206    pub fn find_split_by_label(&self, label: &str) -> Option<LeafId> {
1207        self.root
1208            .leaf_split_ids()
1209            .into_iter()
1210            .find(|id| self.labels.get(&(*id).into()).is_some_and(|l| l == label))
1211    }
1212
1213    /// Find the first leaf split without a label
1214    pub fn find_unlabeled_leaf(&self) -> Option<LeafId> {
1215        self.root
1216            .leaf_split_ids()
1217            .into_iter()
1218            .find(|id| !self.labels.contains_key(&(*id).into()))
1219    }
1220}
1221
1222#[cfg(test)]
1223mod tests {
1224    use super::*;
1225
1226    #[test]
1227    fn test_create_split_manager() {
1228        let buffer_id = BufferId(0);
1229        let manager = SplitManager::new(buffer_id);
1230
1231        assert_eq!(manager.active_buffer_id(), Some(buffer_id));
1232        assert_eq!(manager.root().count_leaves(), 1);
1233    }
1234
1235    #[test]
1236    fn test_horizontal_split() {
1237        let buffer_a = BufferId(0);
1238        let buffer_b = BufferId(1);
1239
1240        let mut manager = SplitManager::new(buffer_a);
1241        let result = manager.split_active(SplitDirection::Horizontal, buffer_b, 0.5);
1242
1243        assert!(result.is_ok());
1244        assert_eq!(manager.root().count_leaves(), 2);
1245    }
1246
1247    #[test]
1248    fn test_vertical_split() {
1249        let buffer_a = BufferId(0);
1250        let buffer_b = BufferId(1);
1251
1252        let mut manager = SplitManager::new(buffer_a);
1253        let result = manager.split_active(SplitDirection::Vertical, buffer_b, 0.5);
1254
1255        assert!(result.is_ok());
1256        assert_eq!(manager.root().count_leaves(), 2);
1257    }
1258
1259    #[test]
1260    fn test_nested_splits() {
1261        let buffer_a = BufferId(0);
1262        let buffer_b = BufferId(1);
1263        let buffer_c = BufferId(2);
1264
1265        let mut manager = SplitManager::new(buffer_a);
1266
1267        // Split horizontally
1268        manager
1269            .split_active(SplitDirection::Horizontal, buffer_b, 0.5)
1270            .unwrap();
1271
1272        // Split the second pane vertically
1273        manager
1274            .split_active(SplitDirection::Vertical, buffer_c, 0.5)
1275            .unwrap();
1276
1277        assert_eq!(manager.root().count_leaves(), 3);
1278    }
1279
1280    #[test]
1281    fn test_close_split() {
1282        let buffer_a = BufferId(0);
1283        let buffer_b = BufferId(1);
1284
1285        let mut manager = SplitManager::new(buffer_a);
1286        let new_split = manager
1287            .split_active(SplitDirection::Horizontal, buffer_b, 0.5)
1288            .unwrap();
1289
1290        assert_eq!(manager.root().count_leaves(), 2);
1291
1292        // Close the new split
1293        let result = manager.close_split(new_split);
1294        assert!(result.is_ok());
1295        assert_eq!(manager.root().count_leaves(), 1);
1296    }
1297
1298    #[test]
1299    fn test_cannot_close_last_split() {
1300        let buffer_a = BufferId(0);
1301        let mut manager = SplitManager::new(buffer_a);
1302
1303        let result = manager.close_split(manager.active_split());
1304        assert!(result.is_err());
1305    }
1306
1307    #[test]
1308    fn test_split_rect_horizontal() {
1309        let rect = Rect {
1310            x: 0,
1311            y: 0,
1312            width: 100,
1313            height: 100,
1314        };
1315
1316        let (first, second) = split_rect(rect, SplitDirection::Horizontal, 0.5);
1317
1318        // With 1 line reserved for separator: (100-1)/2 = 49.5 rounds to 50 and 49
1319        assert_eq!(first.height, 50);
1320        assert_eq!(second.height, 49);
1321        assert_eq!(first.width, 100);
1322        assert_eq!(second.width, 100);
1323        assert_eq!(first.y, 0);
1324        assert_eq!(second.y, 51); // first.y + first.height + 1 (separator)
1325    }
1326
1327    #[test]
1328    fn test_split_rect_vertical() {
1329        let rect = Rect {
1330            x: 0,
1331            y: 0,
1332            width: 100,
1333            height: 100,
1334        };
1335
1336        let (first, second) = split_rect(rect, SplitDirection::Vertical, 0.5);
1337
1338        // With 1 column reserved for separator: (100-1)/2 = 49.5 rounds to 50 and 49
1339        assert_eq!(first.width, 50);
1340        assert_eq!(second.width, 49);
1341        assert_eq!(first.height, 100);
1342        assert_eq!(second.height, 100);
1343        assert_eq!(first.x, 0);
1344        assert_eq!(second.x, 51); // first.x + first.width + 1 (separator)
1345    }
1346
1347    // === Split label tests ===
1348
1349    #[test]
1350    fn test_set_and_get_label() {
1351        let mut manager = SplitManager::new(BufferId(0));
1352        let split = manager.active_split();
1353
1354        assert_eq!(manager.get_label(split.into()), None);
1355
1356        manager.set_label(split, "sidebar".to_string());
1357        assert_eq!(manager.get_label(split.into()), Some("sidebar"));
1358    }
1359
1360    #[test]
1361    fn test_clear_label() {
1362        let mut manager = SplitManager::new(BufferId(0));
1363        let split = manager.active_split();
1364
1365        manager.set_label(split, "sidebar".to_string());
1366        assert!(manager.get_label(split.into()).is_some());
1367
1368        manager.clear_label(split.into());
1369        assert_eq!(manager.get_label(split.into()), None);
1370    }
1371
1372    #[test]
1373    fn test_find_split_by_label() {
1374        let mut manager = SplitManager::new(BufferId(0));
1375        let first_split = manager.active_split();
1376
1377        let second_split = manager
1378            .split_active(SplitDirection::Vertical, BufferId(1), 0.5)
1379            .unwrap();
1380
1381        manager.set_label(first_split, "sidebar".to_string());
1382
1383        assert_eq!(manager.find_split_by_label("sidebar"), Some(first_split));
1384        assert_eq!(manager.find_split_by_label("terminal"), None);
1385
1386        // The second split has no label
1387        assert_ne!(manager.find_split_by_label("sidebar"), Some(second_split));
1388    }
1389
1390    #[test]
1391    fn test_find_unlabeled_leaf() {
1392        let mut manager = SplitManager::new(BufferId(0));
1393        let first_split = manager.active_split();
1394
1395        let second_split = manager
1396            .split_active(SplitDirection::Vertical, BufferId(1), 0.5)
1397            .unwrap();
1398
1399        // No labels — first leaf returned
1400        assert!(manager.find_unlabeled_leaf().is_some());
1401
1402        // Label the first split — unlabeled should return the second
1403        manager.set_label(first_split, "sidebar".to_string());
1404        assert_eq!(manager.find_unlabeled_leaf(), Some(second_split));
1405
1406        // Label both — no unlabeled leaf
1407        manager.set_label(second_split, "terminal".to_string());
1408        assert_eq!(manager.find_unlabeled_leaf(), None);
1409    }
1410
1411    #[test]
1412    fn test_close_split_cleans_up_label() {
1413        let mut manager = SplitManager::new(BufferId(0));
1414        let _first_split = manager.active_split();
1415
1416        let second_split = manager
1417            .split_active(SplitDirection::Vertical, BufferId(1), 0.5)
1418            .unwrap();
1419
1420        manager.set_label(second_split, "sidebar".to_string());
1421        assert_eq!(manager.find_split_by_label("sidebar"), Some(second_split));
1422
1423        manager.close_split(second_split).unwrap();
1424
1425        // Label should be cleaned up
1426        assert_eq!(manager.find_split_by_label("sidebar"), None);
1427        assert_eq!(manager.get_label(second_split.into()), None);
1428    }
1429
1430    #[test]
1431    fn test_label_overwrite() {
1432        let mut manager = SplitManager::new(BufferId(0));
1433        let split = manager.active_split();
1434
1435        manager.set_label(split, "sidebar".to_string());
1436        assert_eq!(manager.get_label(split.into()), Some("sidebar"));
1437
1438        manager.set_label(split, "terminal".to_string());
1439        assert_eq!(manager.get_label(split.into()), Some("terminal"));
1440        assert_eq!(manager.find_split_by_label("sidebar"), None);
1441        assert_eq!(manager.find_split_by_label("terminal"), Some(split));
1442    }
1443
1444    #[test]
1445    fn test_find_unlabeled_leaf_single_split_no_label() {
1446        let manager = SplitManager::new(BufferId(0));
1447        // Single unlabeled split — should return it
1448        assert_eq!(manager.find_unlabeled_leaf(), Some(manager.active_split()));
1449    }
1450
1451    #[test]
1452    fn test_find_unlabeled_leaf_single_split_labeled() {
1453        let mut manager = SplitManager::new(BufferId(0));
1454        let split = manager.active_split();
1455        manager.set_label(split, "only".to_string());
1456        // Only split is labeled — returns None
1457        assert_eq!(manager.find_unlabeled_leaf(), None);
1458    }
1459}