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.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 pub fn force_close_buffer(&mut self, id: BufferId) -> anyhow::Result<()> {
34 self.close_buffer_internal(id)
35 }
36
37 fn close_buffer_internal(&mut self, id: BufferId) -> anyhow::Result<()> {
39 if let Some((_, preview_id)) = self.preview {
42 if preview_id == id {
43 self.preview = None;
44 }
45 }
46
47 if let Some((wait_id, _)) = self.wait_tracking.remove(&id) {
49 self.completed_waits.push(wait_id);
50 }
51
52 self.save_file_state_on_close(id);
54
55 if let Err(e) = self.delete_buffer_recovery(id) {
57 tracing::debug!("Failed to delete buffer recovery on close: {}", e);
58 }
59
60 if let Some(terminal_id) = self.terminal_buffers.remove(&id) {
62 self.terminal_manager.close(terminal_id);
64
65 let backing_file = self.terminal_backing_files.remove(&terminal_id);
67 if let Some(ref path) = backing_file {
68 #[allow(clippy::let_underscore_must_use)]
70 let _ = self.authority.filesystem.remove_file(path);
71 }
72 if let Some(log_file) = self.terminal_log_files.remove(&terminal_id) {
74 if backing_file.as_ref() != Some(&log_file) {
75 #[allow(clippy::let_underscore_must_use)]
77 let _ = self.authority.filesystem.remove_file(&log_file);
78 }
79 }
80
81 self.terminal_mode_resume.remove(&id);
83
84 if self.terminal_mode {
86 self.terminal_mode = false;
87 self.key_context = crate::input::keybindings::KeyContext::Normal;
88 }
89 }
90
91 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, TabTarget::Buffer(bid) => {
102 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 if self.grouped_subtrees.contains_key(leaf) {
117 Some(*t)
118 } else {
119 None
120 }
121 }
122 })
123 });
124
125 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 let closing_active = self.active_buffer() == id;
143
144 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 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 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 if closing_active {
195 self.set_active_buffer(replacement_buffer);
196
197 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 let splits_to_update = self.split_manager.splits_for_buffer(id);
226 for split_id in splits_to_update {
227 self.set_pane_buffer(split_id, replacement_buffer);
228 }
229
230 self.buffers.remove(&id);
231 self.event_logs.remove(&id);
232 self.seen_byte_ranges.remove(&id);
233 self.buffer_metadata.remove(&id);
234 if let Some((request_id, _, _)) = self.semantic_tokens_in_flight.remove(&id) {
235 self.pending_semantic_token_requests.remove(&request_id);
236 }
237 if let Some((request_id, _, _, _)) = self.semantic_tokens_range_in_flight.remove(&id) {
238 self.pending_semantic_token_range_requests
239 .remove(&request_id);
240 }
241 self.semantic_tokens_range_last_request.remove(&id);
242 self.semantic_tokens_range_applied.remove(&id);
243 self.semantic_tokens_full_debounce.remove(&id);
244
245 self.panel_ids.retain(|_, &mut buf_id| buf_id != id);
248
249 for view_state in self.split_view_states.values_mut() {
251 view_state.remove_buffer(id);
252 view_state.remove_from_history(id);
253 }
254
255 if closing_active {
256 if created_empty_buffer {
257 self.focus_file_explorer();
258 }
259 if let Some(group_leaf) = return_to_group {
260 self.activate_group_tab(active_split, group_leaf);
261 }
262 }
263
264 self.plugin_manager.run_hook(
269 "buffer_closed",
270 fresh_core::hooks::HookArgs::BufferClosed { buffer_id: id },
271 );
272
273 Ok(())
274 }
275
276 pub fn switch_buffer(&mut self, id: BufferId) {
278 if self.buffers.contains_key(&id) && id != self.active_buffer() {
279 self.position_history.commit_pending_movement();
281
282 let cursors = self.active_cursors();
284 let position = cursors.primary().position;
285 let anchor = cursors.primary().anchor;
286 self.position_history
287 .record_movement(self.active_buffer(), position, anchor);
288 self.position_history.commit_pending_movement();
289
290 self.set_active_buffer(id);
291 }
292 }
293
294 pub fn close_tab(&mut self) {
303 let active_split = self.split_manager.active_split();
308 if let Some(group_leaf_id) = self
309 .split_view_states
310 .get(&active_split)
311 .and_then(|vs| vs.active_group_tab)
312 {
313 self.close_buffer_group_by_leaf(group_leaf_id);
314 self.set_status_message(t!("buffer.tab_closed").to_string());
315 return;
316 }
317
318 let buffer_id = self.active_buffer();
322 self.close_tab_in_split(buffer_id, active_split);
323 }
324
325 pub fn close_tab_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) -> bool {
335 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
337 self.terminal_mode = false;
338 self.key_context = crate::input::keybindings::KeyContext::Normal;
339 }
340
341 let buffer_in_other_splits = self
343 .split_view_states
344 .iter()
345 .filter(|(&sid, view_state)| sid != split_id && view_state.has_buffer(buffer_id))
346 .count();
347
348 let split_tabs = self
350 .split_view_states
351 .get(&split_id)
352 .map(|vs| vs.buffer_tab_ids_vec())
353 .unwrap_or_default();
354
355 let is_last_viewport = buffer_in_other_splits == 0;
356
357 if is_last_viewport {
358 if let Some(state) = self.buffers.get(&buffer_id) {
360 if state.buffer.is_modified() {
361 let name = self.get_buffer_display_name(buffer_id);
363 let save_key = t!("prompt.key.save").to_string();
364 let discard_key = t!("prompt.key.discard").to_string();
365 let cancel_key = t!("prompt.key.cancel").to_string();
366 self.start_prompt(
367 t!(
368 "prompt.buffer_modified",
369 name = name,
370 save_key = save_key,
371 discard_key = discard_key,
372 cancel_key = cancel_key
373 )
374 .to_string(),
375 PromptType::ConfirmCloseBuffer { buffer_id },
376 );
377 return false;
378 }
379 }
380 let has_other_splits = self.split_manager.root().count_leaves() > 1;
387 if split_tabs.len() <= 1 && has_other_splits {
388 self.handle_close_split(split_id.into());
389 if let Err(e) = self.close_buffer(buffer_id) {
392 tracing::debug!(
393 "close_tab_in_split: buffer cleanup after split close failed: {}",
394 e
395 );
396 }
397 self.set_status_message(t!("buffer.tab_closed").to_string());
398 return true;
399 }
400 if let Err(e) = self.close_buffer(buffer_id) {
401 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
402 } else {
403 self.set_status_message(t!("buffer.tab_closed").to_string());
404 }
405 } else {
406 if split_tabs.len() <= 1 {
408 self.handle_close_split(split_id.into());
410 return true;
411 }
412
413 let current_idx = split_tabs
415 .iter()
416 .position(|&id| id == buffer_id)
417 .unwrap_or(0);
418 let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
419 let replacement_buffer = split_tabs[replacement_idx];
420
421 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
423 view_state.remove_buffer(buffer_id);
424 }
425
426 self.split_manager
428 .set_split_buffer(split_id, replacement_buffer);
429
430 self.set_status_message(t!("buffer.tab_closed").to_string());
431 }
432 true
433 }
434
435 pub fn close_other_tabs_in_split(&mut self, keep_buffer_id: BufferId, split_id: LeafId) {
437 let split_tabs = self
439 .split_view_states
440 .get(&split_id)
441 .map(|vs| vs.buffer_tab_ids_vec())
442 .unwrap_or_default();
443
444 let tabs_to_close: Vec<_> = split_tabs
446 .iter()
447 .filter(|&&id| id != keep_buffer_id)
448 .copied()
449 .collect();
450
451 let mut closed = 0;
452 let mut skipped_modified = 0;
453 for buffer_id in tabs_to_close {
454 if self.close_tab_in_split_silent(buffer_id, split_id) {
455 closed += 1;
456 } else {
457 skipped_modified += 1;
458 }
459 }
460
461 self.split_manager
463 .set_split_buffer(split_id, keep_buffer_id);
464
465 self.set_batch_close_status_message(closed, skipped_modified);
466 }
467
468 pub fn close_tabs_to_right_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) {
470 let split_tabs = self
472 .split_view_states
473 .get(&split_id)
474 .map(|vs| vs.buffer_tab_ids_vec())
475 .unwrap_or_default();
476
477 let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
479 return;
480 };
481
482 let tabs_to_close: Vec<_> = split_tabs.iter().skip(target_idx + 1).copied().collect();
484
485 let mut closed = 0;
486 let mut skipped_modified = 0;
487 for buf_id in tabs_to_close {
488 if self.close_tab_in_split_silent(buf_id, split_id) {
489 closed += 1;
490 } else {
491 skipped_modified += 1;
492 }
493 }
494
495 self.set_batch_close_status_message(closed, skipped_modified);
496 }
497
498 pub fn close_tabs_to_left_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) {
500 let split_tabs = self
502 .split_view_states
503 .get(&split_id)
504 .map(|vs| vs.buffer_tab_ids_vec())
505 .unwrap_or_default();
506
507 let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
509 return;
510 };
511
512 let tabs_to_close: Vec<_> = split_tabs.iter().take(target_idx).copied().collect();
514
515 let mut closed = 0;
516 let mut skipped_modified = 0;
517 for buf_id in tabs_to_close {
518 if self.close_tab_in_split_silent(buf_id, split_id) {
519 closed += 1;
520 } else {
521 skipped_modified += 1;
522 }
523 }
524
525 self.set_batch_close_status_message(closed, skipped_modified);
526 }
527
528 pub fn close_all_tabs_in_split(&mut self, split_id: LeafId) {
530 let split_tabs = self
532 .split_view_states
533 .get(&split_id)
534 .map(|vs| vs.buffer_tab_ids_vec())
535 .unwrap_or_default();
536
537 let mut closed = 0;
538 let mut skipped_modified = 0;
539
540 for buffer_id in split_tabs {
542 if self.close_tab_in_split_silent(buffer_id, split_id) {
543 closed += 1;
544 } else {
545 skipped_modified += 1;
546 }
547 }
548
549 self.set_batch_close_status_message(closed, skipped_modified);
550 }
551
552 fn set_batch_close_status_message(&mut self, closed: usize, skipped_modified: usize) {
554 let message = match (closed, skipped_modified) {
555 (0, 0) => t!("buffer.no_tabs_to_close").to_string(),
556 (0, n) => t!("buffer.skipped_modified", count = n).to_string(),
557 (n, 0) => t!("buffer.closed_tabs", count = n).to_string(),
558 (c, s) => t!("buffer.closed_tabs_skipped", closed = c, skipped = s).to_string(),
559 };
560 self.set_status_message(message);
561 }
562
563 fn close_tab_in_split_silent(&mut self, buffer_id: BufferId, split_id: LeafId) -> bool {
567 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
569 self.terminal_mode = false;
570 self.key_context = crate::input::keybindings::KeyContext::Normal;
571 }
572
573 let buffer_in_other_splits = self
575 .split_view_states
576 .iter()
577 .filter(|(&sid, view_state)| sid != split_id && view_state.has_buffer(buffer_id))
578 .count();
579
580 let split_tabs = self
582 .split_view_states
583 .get(&split_id)
584 .map(|vs| vs.buffer_tab_ids_vec())
585 .unwrap_or_default();
586
587 let is_last_viewport = buffer_in_other_splits == 0;
588
589 if is_last_viewport {
590 if let Some(state) = self.buffers.get(&buffer_id) {
593 if state.buffer.is_modified() {
594 return false;
596 }
597 }
598 if let Err(e) = self.close_buffer(buffer_id) {
599 tracing::warn!("Failed to close buffer: {}", e);
600 }
601 true
602 } else {
603 if split_tabs.len() <= 1 {
605 self.handle_close_split(split_id.into());
607 return true;
608 }
609
610 let current_idx = split_tabs
612 .iter()
613 .position(|&id| id == buffer_id)
614 .unwrap_or(0);
615 let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
616 let replacement_buffer = split_tabs.get(replacement_idx).copied();
617
618 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
620 view_state.remove_buffer(buffer_id);
621 }
622
623 if let Some(replacement) = replacement_buffer {
626 self.set_pane_buffer(split_id, replacement);
627 }
628 true
629 }
630 }
631
632 pub fn next_buffer(&mut self) {
634 self.cycle_tab(1);
635 }
636
637 pub fn prev_buffer(&mut self) {
639 self.cycle_tab(-1);
640 }
641
642 fn cycle_tab(&mut self, direction: i32) {
645 use crate::view::split::TabTarget;
646
647 let active_split = self.split_manager.active_split();
648 let Some(view_state) = self.split_view_states.get(&active_split) else {
649 return;
650 };
651
652 let targets: Vec<TabTarget> = view_state
654 .open_buffers
655 .iter()
656 .copied()
657 .filter(|t| match t {
658 TabTarget::Buffer(id) => !self
659 .buffer_metadata
660 .get(id)
661 .map(|m| m.hidden_from_tabs)
662 .unwrap_or(false),
663 TabTarget::Group(_) => true,
664 })
665 .collect();
666
667 if targets.len() < 2 {
668 return;
669 }
670
671 let current_target = view_state.active_target();
672 let Some(idx) = targets.iter().position(|t| *t == current_target) else {
673 return;
674 };
675
676 let next_idx = if direction > 0 {
677 (idx + 1) % targets.len()
678 } else if idx == 0 {
679 targets.len() - 1
680 } else {
681 idx - 1
682 };
683
684 if targets[next_idx] == current_target {
685 return;
686 }
687
688 self.position_history.commit_pending_movement();
690 let cursors = self.active_cursors();
691 let position = cursors.primary().position;
692 let anchor = cursors.primary().anchor;
693 self.position_history
694 .record_movement(self.active_buffer(), position, anchor);
695 self.position_history.commit_pending_movement();
696
697 match targets[next_idx] {
698 TabTarget::Buffer(buffer_id) => {
699 self.set_active_buffer(buffer_id);
700 }
701 TabTarget::Group(group_leaf_id) => {
702 self.activate_group_tab(active_split, group_leaf_id);
703 }
704 }
705 }
706
707 pub fn navigate_back(&mut self) {
709 self.in_navigation = true;
711
712 self.position_history.commit_pending_movement();
714
715 if self.position_history.can_go_back() && !self.position_history.can_go_forward() {
718 let cursors = self.active_cursors();
719 let position = cursors.primary().position;
720 let anchor = cursors.primary().anchor;
721 self.position_history
722 .record_movement(self.active_buffer(), position, anchor);
723 self.position_history.commit_pending_movement();
724 }
725
726 if let Some(entry) = self.position_history.back() {
728 let target_buffer = entry.buffer_id;
729 let target_position = entry.position;
730 let target_anchor = entry.anchor;
731
732 if self.buffers.contains_key(&target_buffer) {
734 self.set_active_buffer(target_buffer);
735
736 let cursors = self.active_cursors();
738 let cursor_id = cursors.primary_id();
739 let old_position = cursors.primary().position;
740 let old_anchor = cursors.primary().anchor;
741 let old_sticky_column = cursors.primary().sticky_column;
742 let event = Event::MoveCursor {
743 cursor_id,
744 old_position,
745 new_position: target_position,
746 old_anchor,
747 new_anchor: target_anchor,
748 old_sticky_column,
749 new_sticky_column: 0, };
751 let split_id = self.split_manager.active_split();
752 let state = self.buffers.get_mut(&target_buffer).unwrap();
753 let view_state = self.split_view_states.get_mut(&split_id).unwrap();
754 state.apply(&mut view_state.cursors, &event);
755 self.ensure_active_cursor_visible_for_navigation(true);
759 }
760 }
761
762 self.in_navigation = false;
764 }
765
766 pub fn navigate_forward(&mut self) {
768 self.in_navigation = true;
770
771 if let Some(entry) = self.position_history.forward() {
772 let target_buffer = entry.buffer_id;
773 let target_position = entry.position;
774 let target_anchor = entry.anchor;
775
776 if self.buffers.contains_key(&target_buffer) {
778 self.set_active_buffer(target_buffer);
779
780 let cursors = self.active_cursors();
782 let cursor_id = cursors.primary_id();
783 let old_position = cursors.primary().position;
784 let old_anchor = cursors.primary().anchor;
785 let old_sticky_column = cursors.primary().sticky_column;
786 let event = Event::MoveCursor {
787 cursor_id,
788 old_position,
789 new_position: target_position,
790 old_anchor,
791 new_anchor: target_anchor,
792 old_sticky_column,
793 new_sticky_column: 0, };
795 let split_id = self.split_manager.active_split();
796 let state = self.buffers.get_mut(&target_buffer).unwrap();
797 let view_state = self.split_view_states.get_mut(&split_id).unwrap();
798 state.apply(&mut view_state.cursors, &event);
799 self.ensure_active_cursor_visible_for_navigation(true);
803 }
804 }
805
806 self.in_navigation = false;
808 }
809}