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