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, Event, 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.promote_current_preview();
36
37        let current_buffer_id = self.active_buffer();
38        let active_split = self.split_manager.active_split();
39
40        // Copy keyed states from source split so the new split inherits per-buffer state
41        let source_keyed_states = self.split_view_states.get(&active_split).map(|vs| {
42            vs.keyed_states
43                .iter()
44                .filter(|(&buf_id, _)| buf_id != current_buffer_id)
45                .map(|(&buf_id, buf_state)| {
46                    let folds = self
47                        .buffers
48                        .get(&buf_id)
49                        .map(|state| {
50                            buf_state
51                                .folds
52                                .collapsed_line_ranges(&state.buffer, &state.marker_list)
53                        })
54                        .unwrap_or_default();
55                    (buf_id, buf_state.clone(), folds)
56                })
57                .collect::<Vec<(
58                    BufferId,
59                    crate::view::split::BufferViewState,
60                    Vec<CollapsedFoldLineRange>,
61                )>>()
62        });
63
64        match self
65            .split_manager
66            .split_active(direction, current_buffer_id, 0.5)
67        {
68            Ok(new_split_id) => {
69                let mut view_state = SplitViewState::with_buffer(
70                    self.terminal_width,
71                    self.terminal_height,
72                    current_buffer_id,
73                );
74                view_state.apply_config_defaults(
75                    self.config.editor.line_numbers,
76                    self.config.editor.highlight_current_line,
77                    self.resolve_line_wrap_for_buffer(current_buffer_id),
78                    self.config.editor.wrap_indent,
79                    self.resolve_wrap_column_for_buffer(current_buffer_id),
80                    self.config.editor.rulers.clone(),
81                );
82
83                // Copy keyed states from source split for OTHER buffers (not the active one).
84                // The active buffer gets a fresh cursor in the new split.
85                if let Some(source) = source_keyed_states {
86                    for (buf_id, mut buf_state, folds) in source {
87                        if let Some(state) = self.buffers.get_mut(&buf_id) {
88                            buf_state.folds.clear(&mut state.marker_list);
89                            for fold in folds {
90                                let start_line = fold.header_line.saturating_add(1);
91                                let end_line = fold.end_line;
92                                if start_line > end_line {
93                                    continue;
94                                }
95                                let Some(start_byte) = state.buffer.line_start_offset(start_line)
96                                else {
97                                    continue;
98                                };
99                                let end_byte = state
100                                    .buffer
101                                    .line_start_offset(end_line.saturating_add(1))
102                                    .unwrap_or_else(|| state.buffer.len());
103                                buf_state.folds.add(
104                                    &mut state.marker_list,
105                                    start_byte,
106                                    end_byte,
107                                    fold.placeholder.clone(),
108                                );
109                            }
110                        }
111                        view_state.keyed_states.insert(buf_id, buf_state);
112                    }
113                }
114
115                self.split_view_states.insert(new_split_id, view_state);
116                let msg = match direction {
117                    crate::model::event::SplitDirection::Horizontal => t!("split.horizontal"),
118                    crate::model::event::SplitDirection::Vertical => t!("split.vertical"),
119                };
120                self.set_status_message(msg.to_string());
121            }
122            Err(e) => {
123                self.set_status_message(t!("split.error", error = e.to_string()).to_string());
124            }
125        }
126    }
127
128    /// Close the active split
129    pub fn close_active_split(&mut self) {
130        // Closing a split rearranges tab ownership (remaining tabs migrate
131        // to the new active split). Promote any preview first so it doesn't
132        // end up orphaned in a split that no longer exists, or silently
133        // migrated to an unrelated pane.
134        self.promote_current_preview();
135
136        let closing_split = self.split_manager.active_split();
137
138        // Get the tabs from the split we're closing before we close it
139        let closing_split_tabs = self
140            .split_view_states
141            .get(&closing_split)
142            .map(|vs| vs.open_buffers.clone())
143            .unwrap_or_default();
144
145        match self.split_manager.close_split(closing_split) {
146            Ok(_) => {
147                // Clean up the view state for the closed split
148                self.split_view_states.remove(&closing_split);
149
150                // Get the new active split after closing
151                let new_active_split = self.split_manager.active_split();
152
153                // Transfer tabs from closed split to the new active split
154                if let Some(view_state) = self.split_view_states.get_mut(&new_active_split) {
155                    for target in closing_split_tabs {
156                        // Only add if not already in the split's tabs
157                        if !view_state.open_buffers.contains(&target) {
158                            view_state.open_buffers.push(target);
159                        }
160                    }
161                }
162
163                // NOTE: active_buffer is now derived from split_manager, no sync needed
164
165                self.set_status_message(t!("split.closed").to_string());
166            }
167            Err(e) => {
168                self.set_status_message(
169                    t!("split.cannot_close", error = e.to_string()).to_string(),
170                );
171            }
172        }
173    }
174
175    /// Switch to next split
176    pub fn next_split(&mut self) {
177        self.switch_split(true);
178        self.set_status_message(t!("split.next").to_string());
179    }
180
181    /// Switch to previous split
182    pub fn prev_split(&mut self) {
183        self.switch_split(false);
184        self.set_status_message(t!("split.prev").to_string());
185    }
186
187    /// Common split switching logic
188    fn switch_split(&mut self, next: bool) {
189        if next {
190            self.split_manager.next_split();
191        } else {
192            self.split_manager.prev_split();
193        }
194
195        // Ensure the active tab is visible in the newly active split
196        let split_id = self.split_manager.active_split();
197        // Moving focus to a different split commits the preview — walking
198        // away is commitment. Matches the rule applied in `focus_split`.
199        self.promote_preview_if_not_in_split(split_id);
200        self.ensure_active_tab_visible(split_id, self.active_buffer(), self.effective_tabs_width());
201
202        let buffer_id = self.active_buffer();
203
204        // Emit buffer_activated hook for plugins
205        self.plugin_manager.run_hook(
206            "buffer_activated",
207            crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
208        );
209
210        // Enter terminal mode if switching to a terminal split
211        if self.is_terminal_buffer(buffer_id) {
212            self.terminal_mode = true;
213            self.key_context = crate::input::keybindings::KeyContext::Terminal;
214        }
215    }
216
217    /// Adjust cursors in other splits that share the same buffer after an edit
218    pub(crate) fn adjust_other_split_cursors_for_event(&mut self, event: &Event) {
219        // Handle BulkEdit - cursors are managed by the event
220        if let Event::BulkEdit { new_cursors, .. } = event {
221            // Get the current buffer and split
222            let current_buffer_id = self.active_buffer();
223            let current_split_id = self.split_manager.active_split();
224
225            // Find all other splits that share the same buffer
226            let splits_for_buffer = self.split_manager.splits_for_buffer(current_buffer_id);
227
228            // Get buffer length to clamp cursor positions
229            let buffer_len = self
230                .buffers
231                .get(&current_buffer_id)
232                .map(|s| s.buffer.len())
233                .unwrap_or(0);
234
235            // Reset cursors in each other split to primary cursor position
236            for split_id in splits_for_buffer {
237                if split_id == current_split_id {
238                    continue;
239                }
240
241                if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
242                    // Use the primary cursor position from the event
243                    if let Some((_, pos, _)) = new_cursors.first() {
244                        let new_pos = (*pos).min(buffer_len);
245                        view_state.cursors.primary_mut().position = new_pos;
246                        view_state.cursors.primary_mut().anchor = None;
247                    }
248                }
249            }
250            return;
251        }
252
253        // Find the edit parameters from the event
254        let adjustments = match event {
255            Event::Insert { position, text, .. } => {
256                vec![(*position, 0, text.len())]
257            }
258            Event::Delete { range, .. } => {
259                vec![(range.start, range.len(), 0)]
260            }
261            Event::Batch { events, .. } => {
262                // Collect all edits from the batch
263                events
264                    .iter()
265                    .filter_map(|e| match e {
266                        Event::Insert { position, text, .. } => Some((*position, 0, text.len())),
267                        Event::Delete { range, .. } => Some((range.start, range.len(), 0)),
268                        _ => None,
269                    })
270                    .collect()
271            }
272            _ => vec![],
273        };
274
275        if adjustments.is_empty() {
276            return;
277        }
278
279        // Get the current buffer and split
280        let current_buffer_id = self.active_buffer();
281        let current_split_id = self.split_manager.active_split();
282
283        // Find all other splits that share the same buffer
284        let splits_for_buffer = self.split_manager.splits_for_buffer(current_buffer_id);
285
286        // Adjust cursors in each other split's view state
287        for split_id in splits_for_buffer {
288            if split_id == current_split_id {
289                continue; // Skip the current split (already adjusted by BufferState::apply)
290            }
291
292            if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
293                for (edit_pos, old_len, new_len) in &adjustments {
294                    view_state
295                        .cursors
296                        .adjust_for_edit(*edit_pos, *old_len, *new_len);
297                }
298            }
299        }
300    }
301
302    /// Adjust the size of the active split
303    pub fn adjust_split_size(&mut self, delta: f32) {
304        let active_split = self.split_manager.active_split();
305        if let Some(container) = self.split_manager.parent_container_of(active_split) {
306            self.split_manager.adjust_ratio(container, delta);
307
308            let percent = (delta * 100.0) as i32;
309            self.set_status_message(t!("split.size_adjusted", percent = percent).to_string());
310            // Resize visible terminals to match new split dimensions
311            self.resize_visible_terminals();
312        }
313    }
314
315    /// Toggle maximize state for the active split
316    pub fn toggle_maximize_split(&mut self) {
317        match self.split_manager.toggle_maximize() {
318            Ok(maximized) => {
319                if maximized {
320                    self.set_status_message(t!("split.maximized").to_string());
321                } else {
322                    self.set_status_message(t!("split.restored").to_string());
323                }
324                // Resize visible terminals to match new split dimensions
325                self.resize_visible_terminals();
326            }
327            Err(e) => self.set_status_message(e),
328        }
329    }
330
331    /// Get cached separator areas for testing
332    /// Returns (split_id, direction, x, y, length) tuples
333    pub fn get_separator_areas(&self) -> &[(ContainerId, SplitDirection, u16, u16, u16)] {
334        &self.cached_layout.separator_areas
335    }
336
337    /// Get cached tab layouts for testing
338    pub fn get_tab_layouts(
339        &self,
340    ) -> &std::collections::HashMap<LeafId, crate::view::ui::tabs::TabLayout> {
341        &self.cached_layout.tab_layouts
342    }
343
344    /// Get cached split content areas for testing
345    /// Returns (split_id, buffer_id, content_rect, scrollbar_rect, thumb_start, thumb_end) tuples
346    pub fn get_split_areas(
347        &self,
348    ) -> &[(
349        LeafId,
350        BufferId,
351        ratatui::layout::Rect,
352        ratatui::layout::Rect,
353        usize,
354        usize,
355    )] {
356        &self.cached_layout.split_areas
357    }
358
359    /// Get the ratio of a specific split (for testing).
360    ///
361    /// Looks in the main split tree first, then falls back to splits
362    /// that live inside stashed Grouped subtrees (buffer-group panels).
363    pub fn get_split_ratio(&self, split_id: SplitId) -> Option<f32> {
364        self.split_manager
365            .get_ratio(split_id)
366            .or_else(|| self.grouped_split_ratio(crate::model::event::ContainerId(split_id)))
367    }
368
369    /// Get the active split ID (for testing)
370    pub fn get_active_split(&self) -> LeafId {
371        self.split_manager.active_split()
372    }
373
374    /// Get the buffer ID for a split (for testing)
375    pub fn get_split_buffer(&self, split_id: SplitId) -> Option<BufferId> {
376        self.split_manager.get_buffer_id(split_id)
377    }
378
379    /// Get the open buffers (tabs) in a split (for testing)
380    pub fn get_split_tabs(&self, split_id: LeafId) -> Vec<BufferId> {
381        self.split_view_states
382            .get(&split_id)
383            .map(|vs| vs.buffer_tab_ids_vec())
384            .unwrap_or_default()
385    }
386
387    /// Get the number of splits (for testing)
388    pub fn get_split_count(&self) -> usize {
389        self.split_manager.root().count_leaves()
390    }
391
392    /// Compute the drop zone for a tab drag at a given position (for testing)
393    pub fn compute_drop_zone(
394        &self,
395        col: u16,
396        row: u16,
397        source_split_id: LeafId,
398    ) -> Option<super::types::TabDropZone> {
399        self.compute_tab_drop_zone(col, row, source_split_id)
400    }
401}