fresh-editor 0.3.10

A lightweight, fast terminal-based text editor with LSP support and TypeScript plugins
Documentation
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
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
//! 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, ContainerId, LeafId, SplitDirection, SplitId};
use crate::view::folding::CollapsedFoldLineRange;
use crate::view::split::SplitViewState;

use super::Editor;

impl Editor {
    /// Split the current pane horizontally
    pub fn split_pane_horizontal(&mut self) {
        self.split_pane_impl(crate::model::event::SplitDirection::Horizontal);
    }

    /// Split the current pane vertically
    pub fn split_pane_vertical(&mut self) {
        self.split_pane_impl(crate::model::event::SplitDirection::Vertical);
    }

    /// Common split creation logic
    fn split_pane_impl(&mut self, direction: crate::model::event::SplitDirection) {
        // Splitting the layout is a commitment gesture for any preview tab:
        // the user is setting up their working environment around it. Promote
        // before touching the split tree so the invariant "preview is anchored
        // to a single split" stays consistent across the operation.
        self.active_window_mut().promote_current_preview();

        let current_buffer_id = self.active_buffer();
        let active_split = self
            .windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(mgr, _)| mgr)
            .expect("active window must have a populated split layout")
            .active_split();

        // Copy keyed states from source split so the new split inherits per-buffer state
        let source_keyed_states = self
            .windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(_, vs)| vs)
            .expect("active window must have a populated split layout")
            .get(&active_split)
            .map(|vs| {
                vs.keyed_states
                    .iter()
                    .filter(|(&buf_id, _)| buf_id != current_buffer_id)
                    .map(|(&buf_id, buf_state)| {
                        let folds = self
                            .buffers()
                            .get(&buf_id)
                            .map(|state| {
                                buf_state
                                    .folds
                                    .collapsed_line_ranges(&state.buffer, &state.marker_list)
                            })
                            .unwrap_or_default();
                        (buf_id, buf_state.clone(), folds)
                    })
                    .collect::<Vec<(
                        BufferId,
                        crate::view::split::BufferViewState,
                        Vec<CollapsedFoldLineRange>,
                    )>>()
            });

        match self
            .split_manager_mut()
            .split_active(direction, current_buffer_id, 0.5)
        {
            Ok(new_split_id) => {
                let mut view_state = SplitViewState::with_buffer(
                    self.terminal_width,
                    self.terminal_height,
                    current_buffer_id,
                );
                view_state.apply_config_defaults(
                    self.config.editor.line_numbers,
                    self.config.editor.highlight_current_line,
                    self.active_window()
                        .resolve_line_wrap_for_buffer(current_buffer_id),
                    self.config.editor.wrap_indent,
                    self.active_window()
                        .resolve_wrap_column_for_buffer(current_buffer_id),
                    self.config.editor.rulers.clone(),
                );

                // Copy keyed states from source split for OTHER buffers (not the active one).
                // The active buffer gets a fresh cursor in the new split.
                if let Some(source) = source_keyed_states {
                    for (buf_id, mut buf_state, folds) in source {
                        if let Some(state) = self
                            .windows
                            .get_mut(&self.active_window)
                            .map(|w| &mut w.buffers)
                            .expect("active window present")
                            .get_mut(&buf_id)
                        {
                            buf_state.folds.clear(&mut state.marker_list);
                            for fold in folds {
                                let start_line = fold.header_line.saturating_add(1);
                                let end_line = fold.end_line;
                                if start_line > end_line {
                                    continue;
                                }
                                let Some(start_byte) = state.buffer.line_start_offset(start_line)
                                else {
                                    continue;
                                };
                                let end_byte = state
                                    .buffer
                                    .line_start_offset(end_line.saturating_add(1))
                                    .unwrap_or_else(|| state.buffer.len());
                                buf_state.folds.add(
                                    &mut state.marker_list,
                                    start_byte,
                                    end_byte,
                                    fold.placeholder.clone(),
                                );
                            }
                        }
                        view_state.keyed_states.insert(buf_id, buf_state);
                    }
                }

                self.windows
                    .get_mut(&self.active_window)
                    .and_then(|w| w.split_view_states_mut())
                    .expect("active window must have a populated split layout")
                    .insert(new_split_id, view_state);
                let msg = match direction {
                    crate::model::event::SplitDirection::Horizontal => t!("split.horizontal"),
                    crate::model::event::SplitDirection::Vertical => t!("split.vertical"),
                };
                self.set_status_message(msg.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) {
        // Closing a split rearranges tab ownership (remaining tabs migrate
        // to the new active split). Promote any preview first so it doesn't
        // end up orphaned in a split that no longer exists, or silently
        // migrated to an unrelated pane.
        self.active_window_mut().promote_current_preview();

        let closing_split = self
            .windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(mgr, _)| mgr)
            .expect("active window must have a populated split layout")
            .active_split();

        // Get the tabs from the split we're closing before we close it
        let closing_split_tabs = self
            .windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(_, vs)| vs)
            .expect("active window must have a populated split layout")
            .get(&closing_split)
            .map(|vs| vs.open_buffers.clone())
            .unwrap_or_default();

        match self
            .windows
            .get_mut(&self.active_window)
            .and_then(|w| w.split_manager_mut())
            .expect("active window must have a populated split layout")
            .close_split(closing_split)
        {
            Ok(_) => {
                // Clean up the view state for the closed split
                self.windows
                    .get_mut(&self.active_window)
                    .and_then(|w| w.split_view_states_mut())
                    .expect("active window must have a populated split layout")
                    .remove(&closing_split);

                // Get the new active split after closing
                let new_active_split = self
                    .windows
                    .get(&self.active_window)
                    .and_then(|w| w.buffers.splits())
                    .map(|(mgr, _)| mgr)
                    .expect("active window must have a populated split layout")
                    .active_split();

                // Transfer tabs from closed split to the new active split
                if let Some(view_state) = self
                    .windows
                    .get_mut(&self.active_window)
                    .and_then(|w| w.split_view_states_mut())
                    .expect("active window must have a populated split layout")
                    .get_mut(&new_active_split)
                {
                    for target in closing_split_tabs {
                        // Only add if not already in the split's tabs
                        if !view_state.open_buffers.contains(&target) {
                            view_state.open_buffers.push(target);
                        }
                    }
                }

                // NOTE: active_buffer is now derived from split_manager, no sync needed

                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) {
        // Capture what was active before the switch so we can mirror the
        // mouse-click path in `focus_split`: leaving a terminal buffer must
        // stop routing keyboard input to it. The terminal's visible pane
        // keeps rendering live because `render_terminal_splits` ignores
        // `terminal_mode` whenever the terminal isn't the active buffer.
        let previous_buffer = self.active_buffer();

        // `next_split`/`prev_split` auto-unmaximize so the newly-active
        // split is visible (issue #1961). Detect that here so terminal
        // PTYs can be resized to match the restored layout.
        let was_maximized = self
            .windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(mgr, _)| mgr.is_maximized())
            .unwrap_or(false);

        if next {
            self.windows
                .get_mut(&self.active_window)
                .and_then(|w| w.split_manager_mut())
                .expect("active window must have a populated split layout")
                .next_split();
        } else {
            self.windows
                .get_mut(&self.active_window)
                .and_then(|w| w.split_manager_mut())
                .expect("active window must have a populated split layout")
                .prev_split();
        }

        if was_maximized {
            self.active_window_mut().resize_visible_terminals();
        }

        // Ensure the active tab is visible in the newly active split
        let split_id = self
            .windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(mgr, _)| mgr)
            .expect("active window must have a populated split layout")
            .active_split();
        // Moving focus to a different split commits the preview — walking
        // away is commitment. Matches the rule applied in `focus_split`.
        self.active_window_mut()
            .promote_preview_if_not_in_split(split_id);
        let buffer = self.active_buffer();
        let tabs_width = self.active_window().effective_tabs_width();
        self.active_window_mut()
            .ensure_active_tab_visible(split_id, buffer, tabs_width);

        let buffer_id = self.active_buffer();

        // Leaving a terminal buffer: stop capturing keyboard for the
        // terminal. Symmetric with the mouse-click path in `focus_split`.
        if self.active_window().terminal_mode
            && self.active_window().is_terminal_buffer(previous_buffer)
            && !self.active_window().is_terminal_buffer(buffer_id)
        {
            self.active_window_mut().terminal_mode = false;
            self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Normal;
        }

        // Emit buffer_activated hook for plugins
        self.plugin_manager.read().unwrap().run_hook(
            "buffer_activated",
            crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
        );

        // Enter terminal mode if switching to a terminal split
        if self.active_window().is_terminal_buffer(buffer_id) {
            self.active_window_mut().terminal_mode = true;
            self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Terminal;
        }
    }

    /// Adjust the size of the active split
    pub fn adjust_split_size(&mut self, delta: f32) {
        let active_split = self
            .windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(mgr, _)| mgr)
            .expect("active window must have a populated split layout")
            .active_split();
        if let Some(container) = self
            .windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(mgr, _)| mgr)
            .expect("active window must have a populated split layout")
            .parent_container_of(active_split)
        {
            self.windows
                .get_mut(&self.active_window)
                .and_then(|w| w.split_manager_mut())
                .expect("active window must have a populated split layout")
                .adjust_ratio(container, delta);

            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.active_window_mut().resize_visible_terminals();
        }
    }

    /// Toggle maximize state for the active split
    pub fn toggle_maximize_split(&mut self) {
        match self
            .windows
            .get_mut(&self.active_window)
            .and_then(|w| w.split_manager_mut())
            .expect("active window must have a populated split layout")
            .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.active_window_mut().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) -> &[(ContainerId, SplitDirection, u16, u16, u16)] {
        &self.active_layout().separator_areas
    }

    /// Get cached tab layouts for testing
    pub fn get_tab_layouts(
        &self,
    ) -> &std::collections::HashMap<LeafId, crate::view::ui::tabs::TabLayout> {
        &self.active_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,
    ) -> &[(
        LeafId,
        BufferId,
        ratatui::layout::Rect,
        ratatui::layout::Rect,
        usize,
        usize,
    )] {
        &self.active_layout().split_areas
    }

    /// Get the ratio of a specific split (for testing).
    ///
    /// Looks in the main split tree first, then falls back to splits
    /// that live inside stashed Grouped subtrees (buffer-group panels).
    pub fn get_split_ratio(&self, split_id: SplitId) -> Option<f32> {
        self.windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(mgr, _)| mgr)
            .expect("active window must have a populated split layout")
            .get_ratio(split_id)
            .or_else(|| self.grouped_split_ratio(crate::model::event::ContainerId(split_id)))
    }

    /// Get the active split ID (for testing)
    pub fn get_active_split(&self) -> LeafId {
        self.windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(mgr, _)| mgr)
            .expect("active window must have a populated split layout")
            .active_split()
    }

    /// Get the buffer ID for a split (for testing)
    pub fn get_split_buffer(&self, split_id: SplitId) -> Option<BufferId> {
        self.windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(mgr, _)| mgr)
            .expect("active window must have a populated split layout")
            .get_buffer_id(split_id)
    }

    /// Get the open buffers (tabs) in a split (for testing)
    pub fn get_split_tabs(&self, split_id: LeafId) -> Vec<BufferId> {
        self.windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(_, vs)| vs)
            .expect("active window must have a populated split layout")
            .get(&split_id)
            .map(|vs| vs.buffer_tab_ids_vec())
            .unwrap_or_default()
    }

    /// Get the number of splits (for testing)
    pub fn get_split_count(&self) -> usize {
        self.windows
            .get(&self.active_window)
            .and_then(|w| w.buffers.splits())
            .map(|(mgr, _)| mgr)
            .expect("active window must have a populated split layout")
            .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: LeafId,
    ) -> Option<super::types::TabDropZone> {
        self.compute_tab_drop_zone(col, row, source_split_id)
    }
}