Skip to main content

fresh/app/
split_actions.rs

1//! Split/pane management for the Editor.
2//!
3//! This module contains all methods related to managing editor splits:
4//! - Creating horizontal/vertical splits
5//! - Closing splits
6//! - Navigating between splits
7//! - Managing per-split view states (cursors, viewport)
8//! - Split size adjustment and maximize
9
10use rust_i18n::t;
11
12use crate::model::event::{BufferId, ContainerId, LeafId, SplitDirection, SplitId};
13use crate::view::folding::CollapsedFoldLineRange;
14use crate::view::split::SplitViewState;
15
16use super::Editor;
17
18impl Editor {
19    /// Split the current pane horizontally
20    pub fn split_pane_horizontal(&mut self) {
21        self.split_pane_impl(crate::model::event::SplitDirection::Horizontal);
22    }
23
24    /// Split the current pane vertically
25    pub fn split_pane_vertical(&mut self) {
26        self.split_pane_impl(crate::model::event::SplitDirection::Vertical);
27    }
28
29    /// Common split creation logic
30    fn split_pane_impl(&mut self, direction: crate::model::event::SplitDirection) {
31        // Splitting the layout is a commitment gesture for any preview tab:
32        // the user is setting up their working environment around it. Promote
33        // before touching the split tree so the invariant "preview is anchored
34        // to a single split" stays consistent across the operation.
35        self.active_window_mut().promote_current_preview();
36
37        let current_buffer_id = self.active_buffer();
38        let active_split = self
39            .windows
40            .get(&self.active_window)
41            .and_then(|w| w.buffers.splits())
42            .map(|(mgr, _)| mgr)
43            .expect("active window must have a populated split layout")
44            .active_split();
45
46        // Copy keyed states from source split so the new split inherits per-buffer state
47        let source_keyed_states = self
48            .windows
49            .get(&self.active_window)
50            .and_then(|w| w.buffers.splits())
51            .map(|(_, vs)| vs)
52            .expect("active window must have a populated split layout")
53            .get(&active_split)
54            .map(|vs| {
55                vs.keyed_states
56                    .iter()
57                    .filter(|(&buf_id, _)| buf_id != current_buffer_id)
58                    .map(|(&buf_id, buf_state)| {
59                        let folds = self
60                            .buffers()
61                            .get(&buf_id)
62                            .map(|state| {
63                                buf_state
64                                    .folds
65                                    .collapsed_line_ranges(&state.buffer, &state.marker_list)
66                            })
67                            .unwrap_or_default();
68                        (buf_id, buf_state.clone(), folds)
69                    })
70                    .collect::<Vec<(
71                        BufferId,
72                        crate::view::split::BufferViewState,
73                        Vec<CollapsedFoldLineRange>,
74                    )>>()
75            });
76
77        match self
78            .split_manager_mut()
79            .split_active(direction, current_buffer_id, 0.5)
80        {
81            Ok(new_split_id) => {
82                let mut view_state = SplitViewState::with_buffer(
83                    self.terminal_width,
84                    self.terminal_height,
85                    current_buffer_id,
86                );
87                view_state.apply_config_defaults(
88                    self.config.editor.line_numbers,
89                    self.config.editor.highlight_current_line,
90                    self.active_window()
91                        .resolve_line_wrap_for_buffer(current_buffer_id),
92                    self.config.editor.wrap_indent,
93                    self.active_window()
94                        .resolve_wrap_column_for_buffer(current_buffer_id),
95                    self.config.editor.rulers.clone(),
96                );
97
98                // Copy keyed states from source split for OTHER buffers (not the active one).
99                // The active buffer gets a fresh cursor in the new split.
100                if let Some(source) = source_keyed_states {
101                    for (buf_id, mut buf_state, folds) in source {
102                        if let Some(state) = self
103                            .windows
104                            .get_mut(&self.active_window)
105                            .map(|w| &mut w.buffers)
106                            .expect("active window present")
107                            .get_mut(&buf_id)
108                        {
109                            buf_state.folds.clear(&mut state.marker_list);
110                            for fold in folds {
111                                let start_line = fold.header_line.saturating_add(1);
112                                let end_line = fold.end_line;
113                                if start_line > end_line {
114                                    continue;
115                                }
116                                let Some(start_byte) = state.buffer.line_start_offset(start_line)
117                                else {
118                                    continue;
119                                };
120                                let end_byte = state
121                                    .buffer
122                                    .line_start_offset(end_line.saturating_add(1))
123                                    .unwrap_or_else(|| state.buffer.len());
124                                buf_state.folds.add(
125                                    &mut state.marker_list,
126                                    start_byte,
127                                    end_byte,
128                                    fold.placeholder.clone(),
129                                );
130                            }
131                        }
132                        view_state.keyed_states.insert(buf_id, buf_state);
133                    }
134                }
135
136                self.windows
137                    .get_mut(&self.active_window)
138                    .and_then(|w| w.split_view_states_mut())
139                    .expect("active window must have a populated split layout")
140                    .insert(new_split_id, view_state);
141                let msg = match direction {
142                    crate::model::event::SplitDirection::Horizontal => t!("split.horizontal"),
143                    crate::model::event::SplitDirection::Vertical => t!("split.vertical"),
144                };
145                self.set_status_message(msg.to_string());
146            }
147            Err(e) => {
148                self.set_status_message(t!("split.error", error = e.to_string()).to_string());
149            }
150        }
151    }
152
153    /// Close the active split
154    pub fn close_active_split(&mut self) {
155        // Closing a split rearranges tab ownership (remaining tabs migrate
156        // to the new active split). Promote any preview first so it doesn't
157        // end up orphaned in a split that no longer exists, or silently
158        // migrated to an unrelated pane.
159        self.active_window_mut().promote_current_preview();
160
161        let closing_split = self
162            .windows
163            .get(&self.active_window)
164            .and_then(|w| w.buffers.splits())
165            .map(|(mgr, _)| mgr)
166            .expect("active window must have a populated split layout")
167            .active_split();
168
169        // Get the tabs from the split we're closing before we close it
170        let closing_split_tabs = self
171            .windows
172            .get(&self.active_window)
173            .and_then(|w| w.buffers.splits())
174            .map(|(_, vs)| vs)
175            .expect("active window must have a populated split layout")
176            .get(&closing_split)
177            .map(|vs| vs.open_buffers.clone())
178            .unwrap_or_default();
179
180        match self
181            .windows
182            .get_mut(&self.active_window)
183            .and_then(|w| w.split_manager_mut())
184            .expect("active window must have a populated split layout")
185            .close_split(closing_split)
186        {
187            Ok(_) => {
188                // Clean up the view state for the closed split
189                self.windows
190                    .get_mut(&self.active_window)
191                    .and_then(|w| w.split_view_states_mut())
192                    .expect("active window must have a populated split layout")
193                    .remove(&closing_split);
194
195                // Get the new active split after closing
196                let new_active_split = self
197                    .windows
198                    .get(&self.active_window)
199                    .and_then(|w| w.buffers.splits())
200                    .map(|(mgr, _)| mgr)
201                    .expect("active window must have a populated split layout")
202                    .active_split();
203
204                // Transfer tabs from closed split to the new active split
205                if let Some(view_state) = self
206                    .windows
207                    .get_mut(&self.active_window)
208                    .and_then(|w| w.split_view_states_mut())
209                    .expect("active window must have a populated split layout")
210                    .get_mut(&new_active_split)
211                {
212                    for target in closing_split_tabs {
213                        // Only add if not already in the split's tabs
214                        if !view_state.open_buffers.contains(&target) {
215                            view_state.open_buffers.push(target);
216                        }
217                    }
218                }
219
220                // NOTE: active_buffer is now derived from split_manager, no sync needed
221
222                self.set_status_message(t!("split.closed").to_string());
223            }
224            Err(e) => {
225                self.set_status_message(
226                    t!("split.cannot_close", error = e.to_string()).to_string(),
227                );
228            }
229        }
230    }
231
232    /// Switch to next split
233    pub fn next_split(&mut self) {
234        self.switch_split(true);
235        self.set_status_message(t!("split.next").to_string());
236    }
237
238    /// Switch to previous split
239    pub fn prev_split(&mut self) {
240        self.switch_split(false);
241        self.set_status_message(t!("split.prev").to_string());
242    }
243
244    /// Common split switching logic
245    fn switch_split(&mut self, next: bool) {
246        // Capture what was active before the switch so we can mirror the
247        // mouse-click path in `focus_split`: leaving a terminal buffer must
248        // stop routing keyboard input to it. The terminal's visible pane
249        // keeps rendering live because `render_terminal_splits` ignores
250        // `terminal_mode` whenever the terminal isn't the active buffer.
251        let previous_buffer = self.active_buffer();
252
253        if next {
254            self.windows
255                .get_mut(&self.active_window)
256                .and_then(|w| w.split_manager_mut())
257                .expect("active window must have a populated split layout")
258                .next_split();
259        } else {
260            self.windows
261                .get_mut(&self.active_window)
262                .and_then(|w| w.split_manager_mut())
263                .expect("active window must have a populated split layout")
264                .prev_split();
265        }
266
267        // Ensure the active tab is visible in the newly active split
268        let split_id = self
269            .windows
270            .get(&self.active_window)
271            .and_then(|w| w.buffers.splits())
272            .map(|(mgr, _)| mgr)
273            .expect("active window must have a populated split layout")
274            .active_split();
275        // Moving focus to a different split commits the preview — walking
276        // away is commitment. Matches the rule applied in `focus_split`.
277        self.active_window_mut()
278            .promote_preview_if_not_in_split(split_id);
279        let buffer = self.active_buffer();
280        let tabs_width = self.active_window().effective_tabs_width();
281        self.active_window_mut()
282            .ensure_active_tab_visible(split_id, buffer, tabs_width);
283
284        let buffer_id = self.active_buffer();
285
286        // Leaving a terminal buffer: stop capturing keyboard for the
287        // terminal. Symmetric with the mouse-click path in `focus_split`.
288        if self.active_window().terminal_mode
289            && self.active_window().is_terminal_buffer(previous_buffer)
290            && !self.active_window().is_terminal_buffer(buffer_id)
291        {
292            self.active_window_mut().terminal_mode = false;
293            self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Normal;
294        }
295
296        // Emit buffer_activated hook for plugins
297        self.plugin_manager.read().unwrap().run_hook(
298            "buffer_activated",
299            crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
300        );
301
302        // Enter terminal mode if switching to a terminal split
303        if self.active_window().is_terminal_buffer(buffer_id) {
304            self.active_window_mut().terminal_mode = true;
305            self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Terminal;
306        }
307    }
308
309    /// Adjust the size of the active split
310    pub fn adjust_split_size(&mut self, delta: f32) {
311        let active_split = self
312            .windows
313            .get(&self.active_window)
314            .and_then(|w| w.buffers.splits())
315            .map(|(mgr, _)| mgr)
316            .expect("active window must have a populated split layout")
317            .active_split();
318        if let Some(container) = self
319            .windows
320            .get(&self.active_window)
321            .and_then(|w| w.buffers.splits())
322            .map(|(mgr, _)| mgr)
323            .expect("active window must have a populated split layout")
324            .parent_container_of(active_split)
325        {
326            self.windows
327                .get_mut(&self.active_window)
328                .and_then(|w| w.split_manager_mut())
329                .expect("active window must have a populated split layout")
330                .adjust_ratio(container, delta);
331
332            let percent = (delta * 100.0) as i32;
333            self.set_status_message(t!("split.size_adjusted", percent = percent).to_string());
334            // Resize visible terminals to match new split dimensions
335            self.active_window_mut().resize_visible_terminals();
336        }
337    }
338
339    /// Toggle maximize state for the active split
340    pub fn toggle_maximize_split(&mut self) {
341        match self
342            .windows
343            .get_mut(&self.active_window)
344            .and_then(|w| w.split_manager_mut())
345            .expect("active window must have a populated split layout")
346            .toggle_maximize()
347        {
348            Ok(maximized) => {
349                if maximized {
350                    self.set_status_message(t!("split.maximized").to_string());
351                } else {
352                    self.set_status_message(t!("split.restored").to_string());
353                }
354                // Resize visible terminals to match new split dimensions
355                self.active_window_mut().resize_visible_terminals();
356            }
357            Err(e) => self.set_status_message(e),
358        }
359    }
360
361    /// Get cached separator areas for testing
362    /// Returns (split_id, direction, x, y, length) tuples
363    pub fn get_separator_areas(&self) -> &[(ContainerId, SplitDirection, u16, u16, u16)] {
364        &self.active_layout().separator_areas
365    }
366
367    /// Get cached tab layouts for testing
368    pub fn get_tab_layouts(
369        &self,
370    ) -> &std::collections::HashMap<LeafId, crate::view::ui::tabs::TabLayout> {
371        &self.active_layout().tab_layouts
372    }
373
374    /// Get cached split content areas for testing
375    /// Returns (split_id, buffer_id, content_rect, scrollbar_rect, thumb_start, thumb_end) tuples
376    pub fn get_split_areas(
377        &self,
378    ) -> &[(
379        LeafId,
380        BufferId,
381        ratatui::layout::Rect,
382        ratatui::layout::Rect,
383        usize,
384        usize,
385    )] {
386        &self.active_layout().split_areas
387    }
388
389    /// Get the ratio of a specific split (for testing).
390    ///
391    /// Looks in the main split tree first, then falls back to splits
392    /// that live inside stashed Grouped subtrees (buffer-group panels).
393    pub fn get_split_ratio(&self, split_id: SplitId) -> Option<f32> {
394        self.windows
395            .get(&self.active_window)
396            .and_then(|w| w.buffers.splits())
397            .map(|(mgr, _)| mgr)
398            .expect("active window must have a populated split layout")
399            .get_ratio(split_id)
400            .or_else(|| self.grouped_split_ratio(crate::model::event::ContainerId(split_id)))
401    }
402
403    /// Get the active split ID (for testing)
404    pub fn get_active_split(&self) -> LeafId {
405        self.windows
406            .get(&self.active_window)
407            .and_then(|w| w.buffers.splits())
408            .map(|(mgr, _)| mgr)
409            .expect("active window must have a populated split layout")
410            .active_split()
411    }
412
413    /// Get the buffer ID for a split (for testing)
414    pub fn get_split_buffer(&self, split_id: SplitId) -> Option<BufferId> {
415        self.windows
416            .get(&self.active_window)
417            .and_then(|w| w.buffers.splits())
418            .map(|(mgr, _)| mgr)
419            .expect("active window must have a populated split layout")
420            .get_buffer_id(split_id)
421    }
422
423    /// Get the open buffers (tabs) in a split (for testing)
424    pub fn get_split_tabs(&self, split_id: LeafId) -> Vec<BufferId> {
425        self.windows
426            .get(&self.active_window)
427            .and_then(|w| w.buffers.splits())
428            .map(|(_, vs)| vs)
429            .expect("active window must have a populated split layout")
430            .get(&split_id)
431            .map(|vs| vs.buffer_tab_ids_vec())
432            .unwrap_or_default()
433    }
434
435    /// Get the number of splits (for testing)
436    pub fn get_split_count(&self) -> usize {
437        self.windows
438            .get(&self.active_window)
439            .and_then(|w| w.buffers.splits())
440            .map(|(mgr, _)| mgr)
441            .expect("active window must have a populated split layout")
442            .root()
443            .count_leaves()
444    }
445
446    /// Compute the drop zone for a tab drag at a given position (for testing)
447    pub fn compute_drop_zone(
448        &self,
449        col: u16,
450        row: u16,
451        source_split_id: LeafId,
452    ) -> Option<super::types::TabDropZone> {
453        self.compute_tab_drop_zone(col, row, source_split_id)
454    }
455}