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