Skip to main content

fresh/app/
buffer_close.rs

1//! Buffer-close and tab-management orchestrators on `Editor`.
2//!
3//! Closing a buffer in this editor is non-trivial: it involves removing
4//! the buffer from the registry, cleaning up LSP state and semantic
5//! tokens, deciding what to focus next via the focus-history LRU,
6//! adjusting split tab lists, and (for terminal buffers) tearing down
7//! the terminal manager. The whole cluster lives here.
8//!
9//! Also includes tab navigation (next/prev/cycle, navigate_back/forward,
10//! switch_buffer) which depends on the same focus-history machinery.
11
12use rust_i18n::t;
13
14use crate::model::event::{BufferId, Event, LeafId};
15use crate::view::prompt::PromptType;
16
17use super::Editor;
18
19impl Editor {
20    /// Close the given buffer
21    pub fn close_buffer(&mut self, id: BufferId) -> anyhow::Result<()> {
22        // Check for unsaved changes
23        if let Some(state) = self.buffers.get(&id) {
24            if state.buffer.is_modified() {
25                return Err(anyhow::anyhow!("Buffer has unsaved changes"));
26            }
27        }
28        self.close_buffer_internal(id)
29    }
30
31    /// Force close the given buffer without checking for unsaved changes
32    /// Use this when the user has already confirmed they want to discard changes
33    pub fn force_close_buffer(&mut self, id: BufferId) -> anyhow::Result<()> {
34        self.close_buffer_internal(id)
35    }
36
37    /// Internal helper to close a buffer (shared by close_buffer and force_close_buffer)
38    fn close_buffer_internal(&mut self, id: BufferId) -> anyhow::Result<()> {
39        // Clear preview tracking if we're closing the current preview buffer.
40        // This keeps `preview` from pointing at a freed buffer id.
41        if let Some((_, preview_id)) = self.preview {
42            if preview_id == id {
43                self.preview = None;
44            }
45        }
46
47        // Complete any --wait tracking for this buffer
48        if let Some((wait_id, _)) = self.wait_tracking.remove(&id) {
49            self.completed_waits.push(wait_id);
50        }
51
52        // Save file state before closing (for per-file session persistence)
53        self.save_file_state_on_close(id);
54
55        // Delete recovery data for explicitly closed buffers (including unnamed)
56        if let Err(e) = self.delete_buffer_recovery(id) {
57            tracing::debug!("Failed to delete buffer recovery on close: {}", e);
58        }
59
60        // If closing a terminal buffer, clean up terminal-related data structures
61        if let Some(terminal_id) = self.terminal_buffers.remove(&id) {
62            // Close the terminal process
63            self.terminal_manager.close(terminal_id);
64
65            // Clean up backing/rendering file
66            let backing_file = self.terminal_backing_files.remove(&terminal_id);
67            if let Some(ref path) = backing_file {
68                // Best-effort cleanup of temporary terminal files.
69                #[allow(clippy::let_underscore_must_use)]
70                let _ = self.filesystem.remove_file(path);
71            }
72            // Clean up raw log file
73            if let Some(log_file) = self.terminal_log_files.remove(&terminal_id) {
74                if backing_file.as_ref() != Some(&log_file) {
75                    // Best-effort cleanup of temporary terminal files.
76                    #[allow(clippy::let_underscore_must_use)]
77                    let _ = self.filesystem.remove_file(&log_file);
78                }
79            }
80
81            // Remove from terminal_mode_resume to prevent stale entries
82            self.terminal_mode_resume.remove(&id);
83
84            // Exit terminal mode if we were in it
85            if self.terminal_mode {
86                self.terminal_mode = false;
87                self.key_context = crate::input::keybindings::KeyContext::Normal;
88            }
89        }
90
91        // Walk the focus-history LRU (most recent first) to find the tab
92        // the user should land on. This naturally handles both buffer and
93        // group tabs — whichever the user was looking at most recently wins.
94        let active_split = self.split_manager.active_split();
95
96        let replacement_target: Option<crate::view::split::TabTarget> =
97            self.split_view_states.get(&active_split).and_then(|vs| {
98                use crate::view::split::TabTarget;
99                vs.focus_history.iter().rev().find_map(|t| match t {
100                    TabTarget::Buffer(bid) if *bid == id => None, // skip the closing buffer
101                    TabTarget::Buffer(bid) => {
102                        // Skip hidden-from-tabs buffers (panel helpers etc.)
103                        let hidden = self
104                            .buffer_metadata
105                            .get(bid)
106                            .map(|m| m.hidden_from_tabs)
107                            .unwrap_or(false);
108                        if hidden || !self.buffers.contains_key(bid) {
109                            None
110                        } else {
111                            Some(*t)
112                        }
113                    }
114                    TabTarget::Group(leaf) => {
115                        // Only if the group still exists
116                        if self.grouped_subtrees.contains_key(leaf) {
117                            Some(*t)
118                        } else {
119                            None
120                        }
121                    }
122                })
123            });
124
125        // Any visible buffer other than the one being closed. Used as the
126        // general fallback (no LRU target or LRU points at a gone group).
127        let fallback_buffer: Option<BufferId> = self
128            .buffers
129            .keys()
130            .find(|&&bid| {
131                bid != id
132                    && !self
133                        .buffer_metadata
134                        .get(&bid)
135                        .map(|m| m.hidden_from_tabs)
136                        .unwrap_or(false)
137            })
138            .copied();
139
140        // Capture before the replacement computation — new_buffer() has the
141        // side effect of calling set_active_buffer which changes active_buffer().
142        let closing_active = self.active_buffer() == id;
143
144        // Pick the BufferId that becomes the host split's `active_buffer`.
145        // When `return_to_group` is set, `active_buffer` is a housekeeping
146        // fiction — nothing renders it — so any existing buffer works; we
147        // just need to avoid synthesizing a phantom `[No Name]` when a real
148        // option exists. A synthetic buffer fires only when the editor has
149        // literally no other buffer left.
150        let return_to_group = match replacement_target {
151            Some(crate::view::split::TabTarget::Group(leaf)) => Some(leaf),
152            _ => None,
153        };
154
155        let direct_replacement = match replacement_target {
156            Some(crate::view::split::TabTarget::Buffer(bid)) => Some(bid),
157            _ => None,
158        };
159
160        // Prefer a buffer already keyed in the host split: `switch_buffer`
161        // inserts a default BufferViewState for any new active_buffer, which
162        // for hidden panel buffers becomes a shadow entry (cursor=0) that
163        // the plugin-state snapshot could non-deterministically prefer over
164        // the panel split's authoritative copy. Picking something already
165        // keyed sidesteps that insert. (We clean up after the fact if a
166        // shadow does get created — see below.)
167        let already_keyed = return_to_group.and_then(|_| {
168            self.split_view_states
169                .get(&active_split)?
170                .keyed_states
171                .keys()
172                .find(|&&bid| bid != id)
173                .copied()
174        });
175
176        // Absolute last-resort pool for the Group case: any buffer at all,
177        // including hidden panel ones. The shadow cleanup below keeps
178        // those invisible.
179        let any_remaining =
180            return_to_group.and_then(|_| self.buffers.keys().copied().find(|&bid| bid != id));
181
182        let (replacement_buffer, created_empty_buffer) = match direct_replacement
183            .or(already_keyed)
184            .or(fallback_buffer)
185            .or(any_remaining)
186        {
187            Some(bid) => (bid, false),
188            None => (self.new_buffer(), true),
189        };
190
191        // Switch to replacement buffer BEFORE updating splits.
192        // Only needed when the closing buffer is the one the user is
193        // looking at — otherwise the current active buffer stays.
194        if closing_active {
195            self.set_active_buffer(replacement_buffer);
196
197            // If we landed on a hidden panel buffer to fill the Group-case
198            // housekeeping slot, scrub the *visible* side effects
199            // (`open_buffers`, `focus_history`) so the panel buffer doesn't
200            // appear as a tab. The `keyed_states` entry `switch_buffer`
201            // inserted has to stay — `active_state()` requires
202            // `active_buffer ∈ keyed_states` — but it's harmless as long as
203            // the plugin-snapshot lookup skips it; see
204            // `snapshot_source_split` in `update_plugin_state_snapshot`.
205            let hidden = self
206                .buffer_metadata
207                .get(&replacement_buffer)
208                .is_some_and(|m| m.hidden_from_tabs);
209            if return_to_group.is_some() && hidden {
210                use crate::view::split::TabTarget;
211                if let Some(vs) = self.split_view_states.get_mut(&active_split) {
212                    vs.open_buffers
213                        .retain(|t| *t != TabTarget::Buffer(replacement_buffer));
214                    vs.focus_history
215                        .retain(|t| *t != TabTarget::Buffer(replacement_buffer));
216                }
217            }
218        }
219
220        // Update all splits that are showing this buffer to show the replacement
221        let splits_to_update = self.split_manager.splits_for_buffer(id);
222        for split_id in splits_to_update {
223            self.split_manager
224                .set_split_buffer(split_id, replacement_buffer);
225        }
226
227        self.buffers.remove(&id);
228        self.event_logs.remove(&id);
229        self.seen_byte_ranges.remove(&id);
230        self.buffer_metadata.remove(&id);
231        if let Some((request_id, _, _)) = self.semantic_tokens_in_flight.remove(&id) {
232            self.pending_semantic_token_requests.remove(&request_id);
233        }
234        if let Some((request_id, _, _, _)) = self.semantic_tokens_range_in_flight.remove(&id) {
235            self.pending_semantic_token_range_requests
236                .remove(&request_id);
237        }
238        self.semantic_tokens_range_last_request.remove(&id);
239        self.semantic_tokens_range_applied.remove(&id);
240        self.semantic_tokens_full_debounce.remove(&id);
241
242        // Remove buffer from panel_ids mapping if it was a panel buffer
243        // This prevents stale entries when the same panel_id is reused later
244        self.panel_ids.retain(|_, &mut buf_id| buf_id != id);
245
246        // Remove buffer from all splits' open_buffers lists and focus history
247        for view_state in self.split_view_states.values_mut() {
248            view_state.remove_buffer(id);
249            view_state.remove_from_history(id);
250        }
251
252        if closing_active {
253            if created_empty_buffer {
254                self.focus_file_explorer();
255            }
256            if let Some(group_leaf) = return_to_group {
257                self.activate_group_tab(group_leaf);
258            }
259        }
260
261        // Notify plugins so they can reset any state tied to this buffer
262        // (e.g. a plugin that owns a buffer group clears its `isOpen` flag
263        // when the group is closed via the tab's close button rather than
264        // through the plugin's own close command).
265        self.plugin_manager.run_hook(
266            "buffer_closed",
267            fresh_core::hooks::HookArgs::BufferClosed { buffer_id: id },
268        );
269
270        Ok(())
271    }
272
273    /// Switch to the given buffer
274    pub fn switch_buffer(&mut self, id: BufferId) {
275        if self.buffers.contains_key(&id) && id != self.active_buffer() {
276            // Save current position before switching buffers
277            self.position_history.commit_pending_movement();
278
279            // Also explicitly record current position (in case there was no pending movement)
280            let cursors = self.active_cursors();
281            let position = cursors.primary().position;
282            let anchor = cursors.primary().anchor;
283            self.position_history
284                .record_movement(self.active_buffer(), position, anchor);
285            self.position_history.commit_pending_movement();
286
287            self.set_active_buffer(id);
288        }
289    }
290
291    /// Close the current tab in the current split view.
292    /// If the tab is the last viewport of the underlying buffer, do the same as close_buffer
293    /// (including triggering the save/discard prompt for modified buffers).
294    ///
295    /// When the active tab is a buffer group (its `active_group_tab` is set),
296    /// this closes the entire group rather than the currently-focused inner
297    /// panel buffer. Individual panels are internal details of the group —
298    /// the user closes them all together by closing the group tab.
299    pub fn close_tab(&mut self) {
300        // If the active split has a group tab active, close the whole group
301        // rather than just the focused panel buffer — only the Close-Tab
302        // command (or keybinding) can express "close the group I'm viewing",
303        // so this prelude stays here rather than in `close_tab_in_split`.
304        let active_split = self.split_manager.active_split();
305        if let Some(group_leaf_id) = self
306            .split_view_states
307            .get(&active_split)
308            .and_then(|vs| vs.active_group_tab)
309        {
310            self.close_buffer_group_by_leaf(group_leaf_id);
311            self.set_status_message(t!("buffer.tab_closed").to_string());
312            return;
313        }
314
315        // Delegate to `close_tab_in_split` so the Close-Buffer command,
316        // Alt+W, and the mouse × button all run the same code path —
317        // there should be no difference in behavior between them.
318        let buffer_id = self.active_buffer();
319        self.close_tab_in_split(buffer_id, active_split);
320    }
321
322    /// Close a specific tab (buffer) in a specific split.
323    ///
324    /// This is the single shared implementation used by:
325    ///   * the mouse × button on a tab,
326    ///   * the Close Buffer command (via `close_tab`),
327    ///   * the Close Tab command and the `Alt+W` keybinding (via `close_tab`).
328    ///
329    /// All three paths should behave identically; keep new logic here.
330    /// Returns true if the tab was closed without needing a prompt.
331    pub fn close_tab_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) -> bool {
332        // If closing a terminal buffer while in terminal mode, exit terminal mode
333        if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
334            self.terminal_mode = false;
335            self.key_context = crate::input::keybindings::KeyContext::Normal;
336        }
337
338        // Count how many splits have this buffer in their open_buffers
339        let buffer_in_other_splits = self
340            .split_view_states
341            .iter()
342            .filter(|(&sid, view_state)| sid != split_id && view_state.has_buffer(buffer_id))
343            .count();
344
345        // Get the split's open buffers
346        let split_tabs = self
347            .split_view_states
348            .get(&split_id)
349            .map(|vs| vs.buffer_tab_ids_vec())
350            .unwrap_or_default();
351
352        let is_last_viewport = buffer_in_other_splits == 0;
353
354        if is_last_viewport {
355            // Last viewport of this buffer - need to close buffer entirely
356            if let Some(state) = self.buffers.get(&buffer_id) {
357                if state.buffer.is_modified() {
358                    // Buffer has unsaved changes - prompt for confirmation
359                    let name = self.get_buffer_display_name(buffer_id);
360                    let save_key = t!("prompt.key.save").to_string();
361                    let discard_key = t!("prompt.key.discard").to_string();
362                    let cancel_key = t!("prompt.key.cancel").to_string();
363                    self.start_prompt(
364                        t!(
365                            "prompt.buffer_modified",
366                            name = name,
367                            save_key = save_key,
368                            discard_key = discard_key,
369                            cancel_key = cancel_key
370                        )
371                        .to_string(),
372                        PromptType::ConfirmCloseBuffer { buffer_id },
373                    );
374                    return false;
375                }
376            }
377            // If this is the only tab in this split AND there are other
378            // splits, close the split rather than swap it to a fallback
379            // buffer.  Mirrors `close_tab()` so mouse-click close and
380            // Close Buffer/Close Tab commands behave the same — without
381            // this, the × button leaves a leftover split showing some
382            // unrelated buffer (observed with the Search/Replace panel).
383            let has_other_splits = self.split_manager.root().count_leaves() > 1;
384            if split_tabs.len() <= 1 && has_other_splits {
385                self.handle_close_split(split_id.into());
386                // handle_close_split also disposes the buffer-less split;
387                // buffer lifetime cleanup happens via its own path.
388                if let Err(e) = self.close_buffer(buffer_id) {
389                    tracing::debug!(
390                        "close_tab_in_split: buffer cleanup after split close failed: {}",
391                        e
392                    );
393                }
394                self.set_status_message(t!("buffer.tab_closed").to_string());
395                return true;
396            }
397            if let Err(e) = self.close_buffer(buffer_id) {
398                self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
399            } else {
400                self.set_status_message(t!("buffer.tab_closed").to_string());
401            }
402        } else {
403            // There are other viewports of this buffer - just remove from this split's tabs
404            if split_tabs.len() <= 1 {
405                // This is the only tab in this split - close the split
406                self.handle_close_split(split_id.into());
407                return true;
408            }
409
410            // Find replacement buffer for this split
411            let current_idx = split_tabs
412                .iter()
413                .position(|&id| id == buffer_id)
414                .unwrap_or(0);
415            let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
416            let replacement_buffer = split_tabs[replacement_idx];
417
418            // Remove buffer from this split's tabs
419            if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
420                view_state.remove_buffer(buffer_id);
421            }
422
423            // Update the split to show the replacement buffer
424            self.split_manager
425                .set_split_buffer(split_id, replacement_buffer);
426
427            self.set_status_message(t!("buffer.tab_closed").to_string());
428        }
429        true
430    }
431
432    /// Close all other tabs in a split, keeping only the specified buffer
433    pub fn close_other_tabs_in_split(&mut self, keep_buffer_id: BufferId, split_id: LeafId) {
434        // Get the split's open buffers
435        let split_tabs = self
436            .split_view_states
437            .get(&split_id)
438            .map(|vs| vs.buffer_tab_ids_vec())
439            .unwrap_or_default();
440
441        // Close all tabs except the one we want to keep
442        let tabs_to_close: Vec<_> = split_tabs
443            .iter()
444            .filter(|&&id| id != keep_buffer_id)
445            .copied()
446            .collect();
447
448        let mut closed = 0;
449        let mut skipped_modified = 0;
450        for buffer_id in tabs_to_close {
451            if self.close_tab_in_split_silent(buffer_id, split_id) {
452                closed += 1;
453            } else {
454                skipped_modified += 1;
455            }
456        }
457
458        // Make sure the kept buffer is active
459        self.split_manager
460            .set_split_buffer(split_id, keep_buffer_id);
461
462        self.set_batch_close_status_message(closed, skipped_modified);
463    }
464
465    /// Close tabs to the right of the specified buffer in a split
466    pub fn close_tabs_to_right_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) {
467        // Get the split's open buffers
468        let split_tabs = self
469            .split_view_states
470            .get(&split_id)
471            .map(|vs| vs.buffer_tab_ids_vec())
472            .unwrap_or_default();
473
474        // Find the index of the target buffer
475        let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
476            return;
477        };
478
479        // Close all tabs after the target
480        let tabs_to_close: Vec<_> = split_tabs.iter().skip(target_idx + 1).copied().collect();
481
482        let mut closed = 0;
483        let mut skipped_modified = 0;
484        for buf_id in tabs_to_close {
485            if self.close_tab_in_split_silent(buf_id, split_id) {
486                closed += 1;
487            } else {
488                skipped_modified += 1;
489            }
490        }
491
492        self.set_batch_close_status_message(closed, skipped_modified);
493    }
494
495    /// Close tabs to the left of the specified buffer in a split
496    pub fn close_tabs_to_left_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) {
497        // Get the split's open buffers
498        let split_tabs = self
499            .split_view_states
500            .get(&split_id)
501            .map(|vs| vs.buffer_tab_ids_vec())
502            .unwrap_or_default();
503
504        // Find the index of the target buffer
505        let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
506            return;
507        };
508
509        // Close all tabs before the target
510        let tabs_to_close: Vec<_> = split_tabs.iter().take(target_idx).copied().collect();
511
512        let mut closed = 0;
513        let mut skipped_modified = 0;
514        for buf_id in tabs_to_close {
515            if self.close_tab_in_split_silent(buf_id, split_id) {
516                closed += 1;
517            } else {
518                skipped_modified += 1;
519            }
520        }
521
522        self.set_batch_close_status_message(closed, skipped_modified);
523    }
524
525    /// Close all tabs in a split
526    pub fn close_all_tabs_in_split(&mut self, split_id: LeafId) {
527        // Get the split's open buffers
528        let split_tabs = self
529            .split_view_states
530            .get(&split_id)
531            .map(|vs| vs.buffer_tab_ids_vec())
532            .unwrap_or_default();
533
534        let mut closed = 0;
535        let mut skipped_modified = 0;
536
537        // Close all tabs (this will eventually close the split when empty)
538        for buffer_id in split_tabs {
539            if self.close_tab_in_split_silent(buffer_id, split_id) {
540                closed += 1;
541            } else {
542                skipped_modified += 1;
543            }
544        }
545
546        self.set_batch_close_status_message(closed, skipped_modified);
547    }
548
549    /// Set status message for batch close operations
550    fn set_batch_close_status_message(&mut self, closed: usize, skipped_modified: usize) {
551        let message = match (closed, skipped_modified) {
552            (0, 0) => t!("buffer.no_tabs_to_close").to_string(),
553            (0, n) => t!("buffer.skipped_modified", count = n).to_string(),
554            (n, 0) => t!("buffer.closed_tabs", count = n).to_string(),
555            (c, s) => t!("buffer.closed_tabs_skipped", closed = c, skipped = s).to_string(),
556        };
557        self.set_status_message(message);
558    }
559
560    /// Close a tab silently (without setting status message)
561    /// Used internally by batch close operations
562    /// Returns true if the tab was closed, false if it was skipped (e.g., modified buffer)
563    fn close_tab_in_split_silent(&mut self, buffer_id: BufferId, split_id: LeafId) -> bool {
564        // If closing a terminal buffer while in terminal mode, exit terminal mode
565        if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
566            self.terminal_mode = false;
567            self.key_context = crate::input::keybindings::KeyContext::Normal;
568        }
569
570        // Count how many splits have this buffer in their open_buffers
571        let buffer_in_other_splits = self
572            .split_view_states
573            .iter()
574            .filter(|(&sid, view_state)| sid != split_id && view_state.has_buffer(buffer_id))
575            .count();
576
577        // Get the split's open buffers
578        let split_tabs = self
579            .split_view_states
580            .get(&split_id)
581            .map(|vs| vs.buffer_tab_ids_vec())
582            .unwrap_or_default();
583
584        let is_last_viewport = buffer_in_other_splits == 0;
585
586        if is_last_viewport {
587            // Last viewport of this buffer - need to close buffer entirely
588            // Skip modified buffers to avoid prompting during batch operations
589            if let Some(state) = self.buffers.get(&buffer_id) {
590                if state.buffer.is_modified() {
591                    // Skip modified buffers - don't close them
592                    return false;
593                }
594            }
595            if let Err(e) = self.close_buffer(buffer_id) {
596                tracing::warn!("Failed to close buffer: {}", e);
597            }
598            true
599        } else {
600            // There are other viewports of this buffer - just remove from this split's tabs
601            if split_tabs.len() <= 1 {
602                // This is the only tab in this split - close the split
603                self.handle_close_split(split_id.into());
604                return true;
605            }
606
607            // Find replacement buffer for this split
608            let current_idx = split_tabs
609                .iter()
610                .position(|&id| id == buffer_id)
611                .unwrap_or(0);
612            let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
613            let replacement_buffer = split_tabs.get(replacement_idx).copied();
614
615            // Remove buffer from this split's tabs
616            if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
617                view_state.remove_buffer(buffer_id);
618            }
619
620            // Update the split to show the replacement buffer
621            if let Some(replacement) = replacement_buffer {
622                self.split_manager.set_split_buffer(split_id, replacement);
623            }
624            true
625        }
626    }
627
628    /// Switch to next buffer in current split's tabs
629    pub fn next_buffer(&mut self) {
630        self.cycle_tab(1);
631    }
632
633    /// Switch to previous buffer in current split's tabs
634    pub fn prev_buffer(&mut self) {
635        self.cycle_tab(-1);
636    }
637
638    /// Cycle through the active split's tab targets (buffers AND groups).
639    /// Direction: +1 = next, -1 = previous.
640    fn cycle_tab(&mut self, direction: i32) {
641        use crate::view::split::TabTarget;
642
643        let active_split = self.split_manager.active_split();
644        let Some(view_state) = self.split_view_states.get(&active_split) else {
645            return;
646        };
647
648        // Collect visible tab targets, filtering out hidden buffers.
649        let targets: Vec<TabTarget> = view_state
650            .open_buffers
651            .iter()
652            .copied()
653            .filter(|t| match t {
654                TabTarget::Buffer(id) => !self
655                    .buffer_metadata
656                    .get(id)
657                    .map(|m| m.hidden_from_tabs)
658                    .unwrap_or(false),
659                TabTarget::Group(_) => true,
660            })
661            .collect();
662
663        if targets.len() < 2 {
664            return;
665        }
666
667        let current_target = view_state.active_target();
668        let Some(idx) = targets.iter().position(|t| *t == current_target) else {
669            return;
670        };
671
672        let next_idx = if direction > 0 {
673            (idx + 1) % targets.len()
674        } else if idx == 0 {
675            targets.len() - 1
676        } else {
677            idx - 1
678        };
679
680        if targets[next_idx] == current_target {
681            return;
682        }
683
684        // Save current position before switching
685        self.position_history.commit_pending_movement();
686        let cursors = self.active_cursors();
687        let position = cursors.primary().position;
688        let anchor = cursors.primary().anchor;
689        self.position_history
690            .record_movement(self.active_buffer(), position, anchor);
691        self.position_history.commit_pending_movement();
692
693        match targets[next_idx] {
694            TabTarget::Buffer(buffer_id) => {
695                self.set_active_buffer(buffer_id);
696            }
697            TabTarget::Group(group_leaf_id) => {
698                self.activate_group_tab(group_leaf_id);
699            }
700        }
701    }
702
703    /// Navigate back in position history
704    pub fn navigate_back(&mut self) {
705        // Set flag to prevent recording this navigation movement
706        self.in_navigation = true;
707
708        // Commit any pending movement
709        self.position_history.commit_pending_movement();
710
711        // If we're at the end of history (haven't used back yet), save current position
712        // so we can navigate forward to it later
713        if self.position_history.can_go_back() && !self.position_history.can_go_forward() {
714            let cursors = self.active_cursors();
715            let position = cursors.primary().position;
716            let anchor = cursors.primary().anchor;
717            self.position_history
718                .record_movement(self.active_buffer(), position, anchor);
719            self.position_history.commit_pending_movement();
720        }
721
722        // Navigate to the previous position
723        if let Some(entry) = self.position_history.back() {
724            let target_buffer = entry.buffer_id;
725            let target_position = entry.position;
726            let target_anchor = entry.anchor;
727
728            // Switch to the target buffer
729            if self.buffers.contains_key(&target_buffer) {
730                self.set_active_buffer(target_buffer);
731
732                // Move cursor to the saved position
733                let cursors = self.active_cursors();
734                let cursor_id = cursors.primary_id();
735                let old_position = cursors.primary().position;
736                let old_anchor = cursors.primary().anchor;
737                let old_sticky_column = cursors.primary().sticky_column;
738                let event = Event::MoveCursor {
739                    cursor_id,
740                    old_position,
741                    new_position: target_position,
742                    old_anchor,
743                    new_anchor: target_anchor,
744                    old_sticky_column,
745                    new_sticky_column: 0, // Reset sticky column for navigation
746                };
747                let split_id = self.split_manager.active_split();
748                let state = self.buffers.get_mut(&target_buffer).unwrap();
749                let view_state = self.split_view_states.get_mut(&split_id).unwrap();
750                state.apply(&mut view_state.cursors, &event);
751            }
752        }
753
754        // Clear the flag
755        self.in_navigation = false;
756    }
757
758    /// Navigate forward in position history
759    pub fn navigate_forward(&mut self) {
760        // Set flag to prevent recording this navigation movement
761        self.in_navigation = true;
762
763        if let Some(entry) = self.position_history.forward() {
764            let target_buffer = entry.buffer_id;
765            let target_position = entry.position;
766            let target_anchor = entry.anchor;
767
768            // Switch to the target buffer
769            if self.buffers.contains_key(&target_buffer) {
770                self.set_active_buffer(target_buffer);
771
772                // Move cursor to the saved position
773                let cursors = self.active_cursors();
774                let cursor_id = cursors.primary_id();
775                let old_position = cursors.primary().position;
776                let old_anchor = cursors.primary().anchor;
777                let old_sticky_column = cursors.primary().sticky_column;
778                let event = Event::MoveCursor {
779                    cursor_id,
780                    old_position,
781                    new_position: target_position,
782                    old_anchor,
783                    new_anchor: target_anchor,
784                    old_sticky_column,
785                    new_sticky_column: 0, // Reset sticky column for navigation
786                };
787                let split_id = self.split_manager.active_split();
788                let state = self.buffers.get_mut(&target_buffer).unwrap();
789                let view_state = self.split_view_states.get_mut(&split_id).unwrap();
790                state.apply(&mut view_state.cursors, &event);
791            }
792        }
793
794        // Clear the flag
795        self.in_navigation = false;
796    }
797}