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