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.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.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);
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 self.panel_ids.retain(|_, &mut buf_id| buf_id != id);
245
246 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 self.plugin_manager.run_hook(
266 "buffer_closed",
267 fresh_core::hooks::HookArgs::BufferClosed { buffer_id: id },
268 );
269
270 Ok(())
271 }
272
273 pub fn switch_buffer(&mut self, id: BufferId) {
275 if self.buffers.contains_key(&id) && id != self.active_buffer() {
276 self.position_history.commit_pending_movement();
278
279 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 pub fn close_tab(&mut self) {
300 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 let buffer_id = self.active_buffer();
319 self.close_tab_in_split(buffer_id, active_split);
320 }
321
322 pub fn close_tab_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) -> bool {
332 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 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 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 if let Some(state) = self.buffers.get(&buffer_id) {
357 if state.buffer.is_modified() {
358 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 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 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 if split_tabs.len() <= 1 {
405 self.handle_close_split(split_id.into());
407 return true;
408 }
409
410 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 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
420 view_state.remove_buffer(buffer_id);
421 }
422
423 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 pub fn close_other_tabs_in_split(&mut self, keep_buffer_id: BufferId, split_id: LeafId) {
434 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 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 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 pub fn close_tabs_to_right_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) {
467 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 let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
476 return;
477 };
478
479 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 pub fn close_tabs_to_left_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) {
497 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 let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
506 return;
507 };
508
509 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 pub fn close_all_tabs_in_split(&mut self, split_id: LeafId) {
527 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 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 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 fn close_tab_in_split_silent(&mut self, buffer_id: BufferId, split_id: LeafId) -> bool {
564 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 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 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 if let Some(state) = self.buffers.get(&buffer_id) {
590 if state.buffer.is_modified() {
591 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 if split_tabs.len() <= 1 {
602 self.handle_close_split(split_id.into());
604 return true;
605 }
606
607 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 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
617 view_state.remove_buffer(buffer_id);
618 }
619
620 if let Some(replacement) = replacement_buffer {
622 self.split_manager.set_split_buffer(split_id, replacement);
623 }
624 true
625 }
626 }
627
628 pub fn next_buffer(&mut self) {
630 self.cycle_tab(1);
631 }
632
633 pub fn prev_buffer(&mut self) {
635 self.cycle_tab(-1);
636 }
637
638 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 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 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 pub fn navigate_back(&mut self) {
705 self.in_navigation = true;
707
708 self.position_history.commit_pending_movement();
710
711 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 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 if self.buffers.contains_key(&target_buffer) {
730 self.set_active_buffer(target_buffer);
731
732 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, };
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 self.in_navigation = false;
756 }
757
758 pub fn navigate_forward(&mut self) {
760 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 if self.buffers.contains_key(&target_buffer) {
770 self.set_active_buffer(target_buffer);
771
772 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, };
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 self.in_navigation = false;
796 }
797}