1use rust_i18n::t;
13
14use crate::model::event::{BufferId, Event, LeafId};
15use crate::view::prompt::PromptType;
16
17use super::Editor;
18
19impl Editor {
20 pub fn close_buffer(&mut self, id: BufferId) -> anyhow::Result<()> {
22 if let Some(state) = self
24 .windows
25 .get(&self.active_window)
26 .map(|w| &w.buffers)
27 .expect("active window present")
28 .get(&id)
29 {
30 if state.buffer.is_modified() {
31 return Err(anyhow::anyhow!("Buffer has unsaved changes"));
32 }
33 }
34 self.close_buffer_internal(id)
35 }
36
37 pub fn force_close_buffer(&mut self, id: BufferId) -> anyhow::Result<()> {
40 self.close_buffer_internal(id)
41 }
42
43 fn close_buffer_internal(&mut self, id: BufferId) -> anyhow::Result<()> {
45 self.cancel_pending_pastes_for_buffer(id);
51
52 if let Some((_, preview_id)) = self.active_window().preview {
55 if preview_id == id {
56 self.active_window_mut().preview = None;
57 }
58 }
59
60 if let Some((wait_id, _)) = self.active_window_mut().wait_tracking.remove(&id) {
62 self.active_window_mut().completed_waits.push(wait_id);
63 }
64
65 self.active_window().save_file_state_on_close(id);
67
68 if let Err(e) = self.delete_buffer_recovery(id) {
70 tracing::debug!("Failed to delete buffer recovery on close: {}", e);
71 }
72
73 if let Some(terminal_id) = self.active_window_mut().terminal_buffers.remove(&id) {
75 self.active_window_mut().terminal_manager.close(terminal_id);
77 self.active_window_mut()
81 .terminal_explicit_titles
82 .remove(&id);
83 self.active_window_mut().terminal_fg_cache.remove(&id);
84
85 let backing_file = self
91 .active_window_mut()
92 .terminal_backing_files
93 .remove(&terminal_id);
94 if let Some(ref path) = backing_file {
95 self.retain_closed_terminal_backing(path);
96 }
97 if let Some(log_file) = self
99 .active_window_mut()
100 .terminal_log_files
101 .remove(&terminal_id)
102 {
103 if backing_file.as_ref() != Some(&log_file) {
104 #[allow(clippy::let_underscore_must_use)]
106 let _ = self.authority().filesystem.remove_file(&log_file);
107 }
108 }
109
110 self.active_window_mut().terminal_mode_resume.remove(&id);
112
113 if self.active_window().terminal_mode {
115 self.active_window_mut().terminal_mode = false;
116 self.active_window_mut().key_context =
117 crate::input::keybindings::KeyContext::Normal;
118 }
119 }
120
121 let active_split = self
125 .windows
126 .get(&self.active_window)
127 .and_then(|w| w.buffers.splits())
128 .map(|(mgr, _)| mgr)
129 .expect("active window must have a populated split layout")
130 .active_split();
131
132 let replacement_target: Option<crate::view::split::TabTarget> = self
133 .windows
134 .get(&self.active_window)
135 .and_then(|w| w.buffers.splits())
136 .map(|(_, vs)| vs)
137 .expect("active window must have a populated split layout")
138 .get(&active_split)
139 .and_then(|vs| {
140 use crate::view::split::TabTarget;
141 vs.focus_history.iter().rev().find_map(|t| match t {
142 TabTarget::Buffer(bid) if *bid == id => None, TabTarget::Buffer(bid) => {
144 let hidden = self
146 .active_window()
147 .buffer_metadata
148 .get(bid)
149 .map(|m| m.hidden_from_tabs)
150 .unwrap_or(false);
151 if hidden
152 || !self
153 .windows
154 .get(&self.active_window)
155 .map(|w| &w.buffers)
156 .expect("active window present")
157 .contains_key(bid)
158 {
159 None
160 } else {
161 Some(*t)
162 }
163 }
164 TabTarget::Group(leaf) => {
165 if self.active_window().grouped_subtrees.contains_key(leaf) {
167 Some(*t)
168 } else {
169 None
170 }
171 }
172 })
173 });
174
175 let fallback_buffer: Option<BufferId> = self.buffers().find_id(|bid, _| {
178 bid != id
179 && !self
180 .active_window()
181 .buffer_metadata
182 .get(&bid)
183 .map(|m| m.hidden_from_tabs)
184 .unwrap_or(false)
185 });
186
187 let closing_active = self.active_buffer() == id;
190
191 let return_to_group = match replacement_target {
198 Some(crate::view::split::TabTarget::Group(leaf)) => Some(leaf),
199 _ => None,
200 };
201
202 let direct_replacement = match replacement_target {
203 Some(crate::view::split::TabTarget::Buffer(bid)) => Some(bid),
204 _ => None,
205 };
206
207 let already_keyed = return_to_group.and_then(|_| {
215 self.windows
216 .get(&self.active_window)
217 .and_then(|w| w.buffers.splits())
218 .map(|(_, vs)| vs)
219 .expect("active window must have a populated split layout")
220 .get(&active_split)?
221 .keyed_states
222 .keys()
223 .find(|&&bid| bid != id)
224 .copied()
225 });
226
227 let any_remaining = return_to_group.and_then(|_| {
231 self.windows
232 .get(&self.active_window)
233 .map(|w| &w.buffers)
234 .expect("active window present")
235 .find_id(|bid, _| bid != id)
236 });
237
238 let (replacement_buffer, created_empty_buffer) = match direct_replacement
239 .or(already_keyed)
240 .or(fallback_buffer)
241 .or(any_remaining)
242 {
243 Some(bid) => (bid, false),
244 None => {
245 let new_id = self.new_buffer();
251 if !self
252 .config
253 .editor
254 .auto_create_empty_buffer_on_last_buffer_close
255 {
256 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(&new_id) {
257 meta.hidden_from_tabs = true;
258 meta.synthetic_placeholder = true;
259 }
260 }
261 (new_id, true)
262 }
263 };
264
265 if closing_active {
269 self.set_active_buffer(replacement_buffer);
270
271 let hidden = self
280 .active_window()
281 .buffer_metadata
282 .get(&replacement_buffer)
283 .is_some_and(|m| m.hidden_from_tabs);
284 if return_to_group.is_some() && hidden {
285 use crate::view::split::TabTarget;
286 if let Some(vs) = self
287 .windows
288 .get_mut(&self.active_window)
289 .and_then(|w| w.split_view_states_mut())
290 .expect("active window must have a populated split layout")
291 .get_mut(&active_split)
292 {
293 vs.open_buffers
294 .retain(|t| *t != TabTarget::Buffer(replacement_buffer));
295 vs.focus_history
296 .retain(|t| *t != TabTarget::Buffer(replacement_buffer));
297 }
298 }
299 }
300
301 let splits_to_update = self
307 .windows
308 .get(&self.active_window)
309 .and_then(|w| w.buffers.splits())
310 .map(|(mgr, _)| mgr)
311 .expect("active window must have a populated split layout")
312 .splits_for_buffer(id);
313 for split_id in splits_to_update {
314 self.active_window_mut()
315 .set_pane_buffer(split_id, replacement_buffer);
316 }
317
318 self.windows
319 .get_mut(&self.active_window)
320 .map(|w| &mut w.buffers)
321 .expect("active window present")
322 .remove(&id);
323 self.detach_buffer_from_all_windows(id);
324 self.active_window_mut().event_logs.remove(&id);
325 self.active_window_mut().seen_byte_ranges.remove(&id);
326 self.active_window_mut().buffer_metadata.remove(&id);
327 self.active_window_mut().status_bar_values.remove(&id);
328 if let Some((request_id, _, _)) = self
329 .active_window_mut()
330 .semantic_tokens_in_flight
331 .remove(&id)
332 {
333 self.active_window_mut()
334 .pending_semantic_token_requests
335 .remove(&request_id);
336 }
337 if let Some((request_id, _, _, _)) = self
338 .active_window_mut()
339 .semantic_tokens_range_in_flight
340 .remove(&id)
341 {
342 self.active_window_mut()
343 .pending_semantic_token_range_requests
344 .remove(&request_id);
345 }
346 self.active_window_mut()
347 .semantic_tokens_range_last_request
348 .remove(&id);
349 self.active_window_mut()
350 .semantic_tokens_range_applied
351 .remove(&id);
352 self.active_window_mut()
353 .semantic_tokens_full_debounce
354 .remove(&id);
355
356 self.panel_ids_mut().retain(|_, &mut buf_id| buf_id != id);
360
361 for view_state in self
363 .windows
364 .get_mut(&self.active_window)
365 .and_then(|w| w.split_view_states_mut())
366 .expect("active window must have a populated split layout")
367 .values_mut()
368 {
369 view_state.remove_buffer(id);
370 view_state.remove_from_history(id);
371 }
372
373 if closing_active {
374 if created_empty_buffer && self.config.file_explorer.auto_open_on_last_buffer_close {
375 self.focus_file_explorer();
376 }
377 if let Some(group_leaf) = return_to_group {
378 self.activate_group_tab(active_split, group_leaf);
379 }
380 }
381
382 self.plugin_manager.read().unwrap().run_hook(
387 "buffer_closed",
388 fresh_core::hooks::HookArgs::BufferClosed { buffer_id: id },
389 );
390
391 Ok(())
392 }
393
394 pub fn switch_buffer(&mut self, id: BufferId) {
396 if self
397 .windows
398 .get(&self.active_window)
399 .map(|w| &w.buffers)
400 .expect("active window present")
401 .contains_key(&id)
402 && id != self.active_buffer()
403 {
404 self.active_window_mut()
406 .position_history
407 .commit_pending_movement();
408
409 let cursors = self.active_cursors();
411 let position = cursors.primary().position;
412 let anchor = cursors.primary().anchor;
413 let buffer_id = self.active_buffer();
414 let ph = &mut self.active_window_mut().position_history;
415 ph.record_movement(buffer_id, position, anchor);
416 ph.commit_pending_movement();
417
418 self.set_active_buffer(id);
419 }
420 }
421
422 pub fn close_tab(&mut self) {
431 let active_split = self
436 .windows
437 .get(&self.active_window)
438 .and_then(|w| w.buffers.splits())
439 .map(|(mgr, _)| mgr)
440 .expect("active window must have a populated split layout")
441 .active_split();
442 if let Some(group_leaf_id) = self
443 .windows
444 .get(&self.active_window)
445 .and_then(|w| w.buffers.splits())
446 .map(|(_, vs)| vs)
447 .expect("active window must have a populated split layout")
448 .get(&active_split)
449 .and_then(|vs| vs.active_group_tab)
450 {
451 self.close_buffer_group_by_leaf(group_leaf_id);
452 self.set_status_message(t!("buffer.tab_closed").to_string());
453 return;
454 }
455
456 let buffer_id = self.active_buffer();
460 self.close_tab_in_split(buffer_id, active_split);
461 }
462
463 pub fn close_tab_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) -> bool {
473 if self.active_window().terminal_mode && self.active_window().is_terminal_buffer(buffer_id)
475 {
476 self.active_window_mut().terminal_mode = false;
477 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Normal;
478 }
479
480 let buffer_in_other_splits = self
482 .windows
483 .get(&self.active_window)
484 .and_then(|w| w.buffers.splits())
485 .map(|(_, vs)| vs)
486 .expect("active window must have a populated split layout")
487 .iter()
488 .filter(|(&sid, view_state)| sid != split_id && view_state.has_buffer(buffer_id))
489 .count();
490
491 let split_tabs = self
493 .windows
494 .get(&self.active_window)
495 .and_then(|w| w.buffers.splits())
496 .map(|(_, vs)| vs)
497 .expect("active window must have a populated split layout")
498 .get(&split_id)
499 .map(|vs| vs.buffer_tab_ids_vec())
500 .unwrap_or_default();
501
502 let is_last_viewport = buffer_in_other_splits == 0;
503
504 if is_last_viewport {
505 if let Some(state) = self
507 .windows
508 .get(&self.active_window)
509 .map(|w| &w.buffers)
510 .expect("active window present")
511 .get(&buffer_id)
512 {
513 if state.buffer.is_modified() {
514 let name = self.get_buffer_display_name(buffer_id);
516 let save_key = t!("prompt.key.save").to_string();
517 let discard_key = t!("prompt.key.discard").to_string();
518 let cancel_key = t!("prompt.key.cancel").to_string();
519 self.start_prompt(
520 t!(
521 "prompt.buffer_modified",
522 name = name,
523 save_key = save_key,
524 discard_key = discard_key,
525 cancel_key = cancel_key
526 )
527 .to_string(),
528 PromptType::ConfirmCloseBuffer { buffer_id },
529 );
530 return false;
531 }
532 }
533 let has_other_splits = self
540 .windows
541 .get(&self.active_window)
542 .and_then(|w| w.buffers.splits())
543 .map(|(mgr, _)| mgr)
544 .expect("active window must have a populated split layout")
545 .root()
546 .count_leaves()
547 > 1;
548 if split_tabs.len() <= 1 && has_other_splits {
549 self.handle_close_split(split_id.into());
550 if let Err(e) = self.close_buffer(buffer_id) {
553 tracing::debug!(
554 "close_tab_in_split: buffer cleanup after split close failed: {}",
555 e
556 );
557 }
558 self.set_status_message(t!("buffer.tab_closed").to_string());
559 return true;
560 }
561 if let Err(e) = self.close_buffer(buffer_id) {
562 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
563 } else {
564 self.set_status_message(t!("buffer.tab_closed").to_string());
565 }
566 } else {
567 use crate::view::split::TabTarget;
568
569 let targets: Vec<TabTarget> = self
576 .windows
577 .get(&self.active_window)
578 .and_then(|w| w.buffers.splits())
579 .map(|(_, vs)| vs)
580 .expect("active window must have a populated split layout")
581 .get(&split_id)
582 .map(|vs| vs.open_buffers.clone())
583 .unwrap_or_default();
584
585 let closing = TabTarget::Buffer(buffer_id);
586 let closing_idx = targets.iter().position(|t| *t == closing).unwrap_or(0);
587 let has_other_tab = targets.iter().any(|t| *t != closing);
588
589 if !has_other_tab {
590 self.handle_close_split(split_id.into());
592 return true;
593 }
594
595 let replacement = if closing_idx > 0 {
600 targets[closing_idx - 1]
601 } else {
602 *targets
604 .iter()
605 .find(|t| **t != closing)
606 .expect("has_other_tab")
607 };
608
609 if let Some(view_state) = self
611 .windows
612 .get_mut(&self.active_window)
613 .and_then(|w| w.split_view_states_mut())
614 .expect("active window must have a populated split layout")
615 .get_mut(&split_id)
616 {
617 view_state.remove_buffer(buffer_id);
618 }
619
620 match replacement {
623 TabTarget::Buffer(replacement_buffer) => {
624 self.windows
625 .get_mut(&self.active_window)
626 .and_then(|w| w.split_manager_mut())
627 .expect("active window must have a populated split layout")
628 .set_split_buffer(split_id, replacement_buffer);
629 }
630 TabTarget::Group(group_leaf) => {
631 self.activate_group_tab(split_id, group_leaf);
632 }
633 }
634
635 self.set_status_message(t!("buffer.tab_closed").to_string());
636 }
637 true
638 }
639
640 pub fn close_other_tabs_in_split(&mut self, keep_buffer_id: BufferId, split_id: LeafId) {
642 let split_tabs = self
644 .windows
645 .get(&self.active_window)
646 .and_then(|w| w.buffers.splits())
647 .map(|(_, vs)| vs)
648 .expect("active window must have a populated split layout")
649 .get(&split_id)
650 .map(|vs| vs.buffer_tab_ids_vec())
651 .unwrap_or_default();
652
653 let tabs_to_close: Vec<_> = split_tabs
655 .iter()
656 .filter(|&&id| id != keep_buffer_id)
657 .copied()
658 .collect();
659
660 let mut closed = 0;
661 let mut skipped_modified = 0;
662 for buffer_id in tabs_to_close {
663 if self.close_tab_in_split_silent(buffer_id, split_id) {
664 closed += 1;
665 } else {
666 skipped_modified += 1;
667 }
668 }
669
670 self.windows
672 .get_mut(&self.active_window)
673 .and_then(|w| w.split_manager_mut())
674 .expect("active window must have a populated split layout")
675 .set_split_buffer(split_id, keep_buffer_id);
676
677 self.reseat_tab_scroll_for_split(split_id);
678 self.set_batch_close_status_message(closed, skipped_modified);
679 }
680
681 pub fn close_tabs_to_right_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) {
683 let split_tabs = self
685 .windows
686 .get(&self.active_window)
687 .and_then(|w| w.buffers.splits())
688 .map(|(_, vs)| vs)
689 .expect("active window must have a populated split layout")
690 .get(&split_id)
691 .map(|vs| vs.buffer_tab_ids_vec())
692 .unwrap_or_default();
693
694 let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
696 return;
697 };
698
699 let tabs_to_close: Vec<_> = split_tabs.iter().skip(target_idx + 1).copied().collect();
701
702 let mut closed = 0;
703 let mut skipped_modified = 0;
704 for buf_id in tabs_to_close {
705 if self.close_tab_in_split_silent(buf_id, split_id) {
706 closed += 1;
707 } else {
708 skipped_modified += 1;
709 }
710 }
711
712 self.reseat_tab_scroll_for_split(split_id);
713 self.set_batch_close_status_message(closed, skipped_modified);
714 }
715
716 pub fn close_tabs_to_left_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) {
718 let split_tabs = self
720 .windows
721 .get(&self.active_window)
722 .and_then(|w| w.buffers.splits())
723 .map(|(_, vs)| vs)
724 .expect("active window must have a populated split layout")
725 .get(&split_id)
726 .map(|vs| vs.buffer_tab_ids_vec())
727 .unwrap_or_default();
728
729 let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
731 return;
732 };
733
734 let tabs_to_close: Vec<_> = split_tabs.iter().take(target_idx).copied().collect();
736
737 let mut closed = 0;
738 let mut skipped_modified = 0;
739 for buf_id in tabs_to_close {
740 if self.close_tab_in_split_silent(buf_id, split_id) {
741 closed += 1;
742 } else {
743 skipped_modified += 1;
744 }
745 }
746
747 self.reseat_tab_scroll_for_split(split_id);
748 self.set_batch_close_status_message(closed, skipped_modified);
749 }
750
751 pub fn close_all_tabs_in_split(&mut self, split_id: LeafId) {
753 let split_tabs = self
755 .windows
756 .get(&self.active_window)
757 .and_then(|w| w.buffers.splits())
758 .map(|(_, vs)| vs)
759 .expect("active window must have a populated split layout")
760 .get(&split_id)
761 .map(|vs| vs.buffer_tab_ids_vec())
762 .unwrap_or_default();
763
764 let mut closed = 0;
765 let mut skipped_modified = 0;
766
767 for buffer_id in split_tabs {
769 if self.close_tab_in_split_silent(buffer_id, split_id) {
770 closed += 1;
771 } else {
772 skipped_modified += 1;
773 }
774 }
775
776 self.reseat_tab_scroll_for_split(split_id);
781 self.set_batch_close_status_message(closed, skipped_modified);
782 }
783
784 fn reseat_tab_scroll_for_split(&mut self, split_id: LeafId) {
794 let Some(active_buffer) = self
795 .windows
796 .get(&self.active_window)
797 .and_then(|w| w.buffers.splits())
798 .and_then(|(mgr, _)| mgr.buffer_for_split(split_id))
799 else {
800 return;
801 };
802 let tabs_width = self.active_window().effective_tabs_width();
803 self.active_window_mut()
804 .ensure_active_tab_visible(split_id, active_buffer, tabs_width);
805 }
806
807 fn set_batch_close_status_message(&mut self, closed: usize, skipped_modified: usize) {
809 let message = match (closed, skipped_modified) {
810 (0, 0) => t!("buffer.no_tabs_to_close").to_string(),
811 (0, n) => t!("buffer.skipped_modified", count = n).to_string(),
812 (n, 0) => t!("buffer.closed_tabs", count = n).to_string(),
813 (c, s) => t!("buffer.closed_tabs_skipped", closed = c, skipped = s).to_string(),
814 };
815 self.set_status_message(message);
816 }
817
818 fn close_tab_in_split_silent(&mut self, buffer_id: BufferId, split_id: LeafId) -> bool {
822 if self.active_window().terminal_mode && self.active_window().is_terminal_buffer(buffer_id)
824 {
825 self.active_window_mut().terminal_mode = false;
826 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Normal;
827 }
828
829 let buffer_in_other_splits = self
831 .windows
832 .get(&self.active_window)
833 .and_then(|w| w.buffers.splits())
834 .map(|(_, vs)| vs)
835 .expect("active window must have a populated split layout")
836 .iter()
837 .filter(|(&sid, view_state)| sid != split_id && view_state.has_buffer(buffer_id))
838 .count();
839
840 let split_tabs = self
842 .windows
843 .get(&self.active_window)
844 .and_then(|w| w.buffers.splits())
845 .map(|(_, vs)| vs)
846 .expect("active window must have a populated split layout")
847 .get(&split_id)
848 .map(|vs| vs.buffer_tab_ids_vec())
849 .unwrap_or_default();
850
851 let is_last_viewport = buffer_in_other_splits == 0;
852
853 if is_last_viewport {
854 if let Some(state) = self
857 .windows
858 .get(&self.active_window)
859 .map(|w| &w.buffers)
860 .expect("active window present")
861 .get(&buffer_id)
862 {
863 if state.buffer.is_modified() {
864 return false;
866 }
867 }
868 if let Err(e) = self.close_buffer(buffer_id) {
869 tracing::warn!("Failed to close buffer: {}", e);
870 }
871 true
872 } else {
873 if split_tabs.len() <= 1 {
875 self.handle_close_split(split_id.into());
877 return true;
878 }
879
880 let current_idx = split_tabs
882 .iter()
883 .position(|&id| id == buffer_id)
884 .unwrap_or(0);
885 let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
886 let replacement_buffer = split_tabs.get(replacement_idx).copied();
887
888 if let Some(view_state) = self
890 .windows
891 .get_mut(&self.active_window)
892 .and_then(|w| w.split_view_states_mut())
893 .expect("active window must have a populated split layout")
894 .get_mut(&split_id)
895 {
896 view_state.remove_buffer(buffer_id);
897 }
898
899 if let Some(replacement) = replacement_buffer {
902 self.active_window_mut()
903 .set_pane_buffer(split_id, replacement);
904 }
905 true
906 }
907 }
908
909 pub fn next_buffer(&mut self) {
911 self.cycle_tab(1);
912 }
913
914 pub fn prev_buffer(&mut self) {
916 self.cycle_tab(-1);
917 }
918
919 fn cycle_tab(&mut self, direction: i32) {
922 use crate::view::split::TabTarget;
923
924 let active_split = self
925 .windows
926 .get(&self.active_window)
927 .and_then(|w| w.buffers.splits())
928 .map(|(mgr, _)| mgr)
929 .expect("active window must have a populated split layout")
930 .active_split();
931 let Some(view_state) = self
932 .windows
933 .get(&self.active_window)
934 .and_then(|w| w.buffers.splits())
935 .map(|(_, vs)| vs)
936 .expect("active window must have a populated split layout")
937 .get(&active_split)
938 else {
939 return;
940 };
941
942 let targets: Vec<TabTarget> = view_state
944 .open_buffers
945 .iter()
946 .copied()
947 .filter(|t| match t {
948 TabTarget::Buffer(id) => !self
949 .active_window()
950 .buffer_metadata
951 .get(id)
952 .map(|m| m.hidden_from_tabs)
953 .unwrap_or(false),
954 TabTarget::Group(_) => true,
955 })
956 .collect();
957
958 if targets.len() < 2 {
959 return;
960 }
961
962 let current_target = view_state.active_target();
963 let Some(idx) = targets.iter().position(|t| *t == current_target) else {
964 return;
965 };
966
967 let next_idx = if direction > 0 {
968 (idx + 1) % targets.len()
969 } else if idx == 0 {
970 targets.len() - 1
971 } else {
972 idx - 1
973 };
974
975 if targets[next_idx] == current_target {
976 return;
977 }
978
979 self.active_window_mut()
981 .position_history
982 .commit_pending_movement();
983 let cursors = self.active_cursors();
984 let position = cursors.primary().position;
985 let anchor = cursors.primary().anchor;
986 let buffer_id = self.active_buffer();
987 let ph = &mut self.active_window_mut().position_history;
988 ph.record_movement(buffer_id, position, anchor);
989 ph.commit_pending_movement();
990
991 self.active_window_mut()
999 .animate_tab_switch(active_split, direction.signum());
1000
1001 match targets[next_idx] {
1002 TabTarget::Buffer(buffer_id) => {
1003 self.set_active_buffer(buffer_id);
1004 }
1005 TabTarget::Group(group_leaf_id) => {
1006 self.activate_group_tab(active_split, group_leaf_id);
1007 }
1008 }
1009 }
1010
1011 pub fn navigate_back(&mut self) {
1013 self.active_window_mut().in_navigation = true;
1015
1016 self.active_window_mut()
1018 .position_history
1019 .commit_pending_movement();
1020
1021 if self.active_window_mut().position_history.can_go_back()
1024 && !self.active_window_mut().position_history.can_go_forward()
1025 {
1026 let cursors = self.active_cursors();
1027 let position = cursors.primary().position;
1028 let anchor = cursors.primary().anchor;
1029 let buffer_id = self.active_buffer();
1030 let ph = &mut self.active_window_mut().position_history;
1031 ph.record_movement(buffer_id, position, anchor);
1032 ph.commit_pending_movement();
1033 }
1034
1035 if let Some(entry) = self.active_window_mut().position_history.back() {
1037 let target_buffer = entry.buffer_id;
1038 let target_position = entry.position;
1039 let target_anchor = entry.anchor;
1040
1041 if self
1043 .windows
1044 .get(&self.active_window)
1045 .map(|w| &w.buffers)
1046 .expect("active window present")
1047 .contains_key(&target_buffer)
1048 {
1049 self.set_active_buffer(target_buffer);
1050
1051 let cursors = self.active_cursors();
1053 let cursor_id = cursors.primary_id();
1054 let old_position = cursors.primary().position;
1055 let old_anchor = cursors.primary().anchor;
1056 let old_sticky_column = cursors.primary().sticky_column;
1057 let event = Event::MoveCursor {
1058 cursor_id,
1059 old_position,
1060 new_position: target_position,
1061 old_anchor,
1062 new_anchor: target_anchor,
1063 old_sticky_column,
1064 new_sticky_column: 0, };
1066 let split_id = self
1067 .windows
1068 .get(&self.active_window)
1069 .and_then(|w| w.buffers.splits())
1070 .map(|(mgr, _)| mgr)
1071 .expect("active window must have a populated split layout")
1072 .active_split();
1073 self.active_window_mut()
1074 .apply_event_to_buffer(target_buffer, split_id, &event);
1075 self.active_window_mut()
1079 .ensure_active_cursor_visible_for_navigation(true);
1080 }
1081 }
1082
1083 self.active_window_mut().in_navigation = false;
1085 }
1086
1087 pub fn navigate_forward(&mut self) {
1089 self.active_window_mut().in_navigation = true;
1091
1092 if let Some(entry) = self.active_window_mut().position_history.forward() {
1093 let target_buffer = entry.buffer_id;
1094 let target_position = entry.position;
1095 let target_anchor = entry.anchor;
1096
1097 if self
1099 .windows
1100 .get(&self.active_window)
1101 .map(|w| &w.buffers)
1102 .expect("active window present")
1103 .contains_key(&target_buffer)
1104 {
1105 self.set_active_buffer(target_buffer);
1106
1107 let cursors = self.active_cursors();
1109 let cursor_id = cursors.primary_id();
1110 let old_position = cursors.primary().position;
1111 let old_anchor = cursors.primary().anchor;
1112 let old_sticky_column = cursors.primary().sticky_column;
1113 let event = Event::MoveCursor {
1114 cursor_id,
1115 old_position,
1116 new_position: target_position,
1117 old_anchor,
1118 new_anchor: target_anchor,
1119 old_sticky_column,
1120 new_sticky_column: 0, };
1122 let split_id = self
1123 .windows
1124 .get(&self.active_window)
1125 .and_then(|w| w.buffers.splits())
1126 .map(|(mgr, _)| mgr)
1127 .expect("active window must have a populated split layout")
1128 .active_split();
1129 self.active_window_mut()
1130 .apply_event_to_buffer(target_buffer, split_id, &event);
1131 self.active_window_mut()
1135 .ensure_active_cursor_visible_for_navigation(true);
1136 }
1137 }
1138
1139 self.active_window_mut().in_navigation = false;
1141 }
1142
1143 fn retain_closed_terminal_backing(&self, path: &std::path::Path) {
1149 use std::time::{SystemTime, UNIX_EPOCH};
1150 let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
1151 return;
1152 };
1153 let Some(parent) = path.parent() else {
1154 return;
1155 };
1156 let epoch_ms = SystemTime::now()
1157 .duration_since(UNIX_EPOCH)
1158 .map(|d| d.as_millis())
1159 .unwrap_or(0);
1160 let retained = parent.join(format!("{stem}-closed-{epoch_ms}.txt"));
1161 #[allow(clippy::let_underscore_must_use)]
1162 let _ = self.authority().filesystem.rename(path, &retained);
1163 self.gc_retained_terminal_backings(parent);
1164 }
1165
1166 fn gc_retained_terminal_backings(&self, dir: &std::path::Path) {
1171 const MAX_RETAINED: usize = 200;
1172 let Ok(entries) = self.authority().filesystem.read_dir(dir) else {
1173 return;
1174 };
1175 let mut retained: Vec<(u128, std::path::PathBuf)> = entries
1176 .into_iter()
1177 .filter_map(|e| {
1178 let rest = e.name.strip_suffix(".txt")?;
1179 let idx = rest.rfind("-closed-")?;
1180 let epoch: u128 = rest[idx + "-closed-".len()..].parse().ok()?;
1181 Some((epoch, e.path))
1182 })
1183 .collect();
1184 if retained.len() <= MAX_RETAINED {
1185 return;
1186 }
1187 retained.sort_by_key(|(epoch, _)| *epoch);
1188 let remove_count = retained.len() - MAX_RETAINED;
1189 for (_, p) in retained.into_iter().take(remove_count) {
1190 #[allow(clippy::let_underscore_must_use)]
1191 let _ = self.authority().filesystem.remove_file(&p);
1192 }
1193 }
1194}