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