1use anyhow::Result as AnyhowResult;
2use rust_i18n::t;
3
4use super::*;
5use crate::services::async_bridge::AsyncMessage;
6use crate::view::file_tree::TreeNode;
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone)]
10pub struct FileExplorerClipboard {
11 pub paths: Vec<PathBuf>,
12 pub is_cut: bool,
13}
14
15#[derive(Debug, Clone, Copy)]
19pub(crate) struct FileExplorerViewDefaults {
20 pub show_hidden: bool,
21 pub show_gitignored: bool,
22 pub compact_directories: bool,
23}
24
25#[derive(Debug)]
31enum PasteOpOutcome {
32 Ok,
34 SourceRemovalFailed { dst: PathBuf, err: std::io::Error },
37 Failed(std::io::Error),
40}
41
42fn get_parent_dir_path(node: &TreeNode) -> PathBuf {
45 if node.is_dir() {
46 node.entry.path.clone()
47 } else {
48 node.entry
49 .path
50 .parent()
51 .map(|p| p.to_path_buf())
52 .unwrap_or_else(|| node.entry.path.clone())
53 }
54}
55
56fn timestamp_suffix() -> u64 {
58 std::time::SystemTime::now()
59 .duration_since(std::time::UNIX_EPOCH)
60 .unwrap()
61 .as_secs()
62}
63
64fn get_parent_node_id(
67 tree: &crate::view::file_tree::FileTree,
68 selected_id: crate::view::file_tree::NodeId,
69 node_is_dir: bool,
70) -> crate::view::file_tree::NodeId {
71 if node_is_dir {
72 selected_id
73 } else {
74 tree.get_node(selected_id)
75 .and_then(|n| n.parent)
76 .unwrap_or(selected_id)
77 }
78}
79
80impl Editor {
81 pub fn file_explorer_visible(&self) -> bool {
82 self.active_window().file_explorer_visible
83 }
84
85 pub(super) fn take_focus_for_file_explorer(&mut self) {
98 let win = self.active_window_mut();
99 if win.terminal_mode {
100 let active = win.active_buffer();
101 if win.is_terminal_buffer(active) {
102 win.terminal_mode_resume.insert(active);
103 }
104 win.terminal_mode = false;
105 }
106 win.key_context = KeyContext::FileExplorer;
107 }
108
109 pub fn toggle_file_explorer(&mut self) {
110 let new_visible = !self.active_window().file_explorer_visible;
111 self.active_window_mut().file_explorer_visible = new_visible;
112
113 if new_visible {
114 if self.file_explorer().is_none() {
115 self.init_file_explorer();
116 }
117 self.take_focus_for_file_explorer();
118 self.set_status_message(t!("explorer.opened").to_string());
119 self.active_window_mut().sync_file_explorer_to_active_file();
120 } else {
121 self.active_window_mut().key_context = KeyContext::Normal;
122 self.set_status_message(t!("explorer.closed").to_string());
123 }
124
125 self.relayout();
129 }
130
131 pub fn show_file_explorer(&mut self) {
132 if !self.file_explorer_visible() {
133 self.toggle_file_explorer();
134 }
135 }
136
137 pub fn focus_file_explorer(&mut self) {
138 if self.file_explorer_visible() {
139 self.active_window_mut().on_editor_focus_lost();
141
142 self.active_window_mut().cancel_search_prompt_if_active();
144
145 self.take_focus_for_file_explorer();
146 self.set_status_message(t!("explorer.focused").to_string());
147 self.active_window_mut().sync_file_explorer_to_active_file();
148 } else {
149 self.toggle_file_explorer();
150 }
151 }
152
153 pub(crate) fn init_file_explorer(&mut self) {
160 self.active_window_mut().init_file_explorer();
161 }
162
163 pub fn file_explorer_navigate_up(&mut self) {
164 if let Some(explorer) = self.file_explorer_mut() {
165 explorer.select_prev_match();
166 explorer.update_scroll_for_selection();
167 }
168 self.file_explorer_preview_selected();
169 }
170
171 pub fn file_explorer_navigate_down(&mut self) {
172 if let Some(explorer) = self.file_explorer_mut() {
173 explorer.select_next_match();
174 explorer.update_scroll_for_selection();
175 }
176 self.file_explorer_preview_selected();
177 }
178
179 pub fn file_explorer_page_up(&mut self) {
180 if let Some(explorer) = self.file_explorer_mut() {
181 explorer.select_page_up();
182 explorer.update_scroll_for_selection();
183 }
184 self.file_explorer_preview_selected();
185 }
186
187 pub fn file_explorer_page_down(&mut self) {
188 if let Some(explorer) = self.file_explorer_mut() {
189 explorer.select_page_down();
190 explorer.update_scroll_for_selection();
191 }
192 self.file_explorer_preview_selected();
193 }
194
195 fn file_explorer_preview_selected(&mut self) {
203 if !self.config.file_explorer.preview_tabs {
206 return;
207 }
208
209 let path = match self
210 .file_explorer()
211 .as_ref()
212 .and_then(|explorer| explorer.get_selected_entry())
213 {
214 Some(entry) if !entry.is_dir() => entry.path.clone(),
215 _ => return,
216 };
217
218 if let Err(e) = self.open_file_preview(&path) {
219 tracing::debug!(
220 "file_explorer_preview_selected: skipping preview for {:?}: {}",
221 path,
222 e
223 );
224 }
225 }
226
227 pub fn file_explorer_collapse(&mut self) {
231 let Some(explorer) = self.file_explorer() else {
232 return;
233 };
234
235 let Some(selected_id) = explorer.get_selected() else {
236 return;
237 };
238
239 let Some(node) = explorer.tree().get_node(selected_id) else {
240 return;
241 };
242
243 if node.is_dir() && node.is_expanded() {
245 self.file_explorer_toggle_expand();
246 return;
247 }
248
249 if let Some(explorer) = self.file_explorer_mut() {
251 explorer.select_parent();
252 explorer.update_scroll_for_selection();
253 }
254 }
255
256 pub fn file_explorer_toggle_expand(&mut self) {
257 let selected_id = if let Some(explorer) = self.file_explorer() {
258 explorer.get_selected()
259 } else {
260 return;
261 };
262
263 let Some(selected_id) = selected_id else {
264 return;
265 };
266
267 let (is_dir, is_expanded, name) = if let Some(explorer) = self.file_explorer() {
268 let node = explorer.tree().get_node(selected_id);
269 if let Some(node) = node {
270 (node.is_dir(), node.is_expanded(), node.entry.name.clone())
271 } else {
272 return;
273 }
274 } else {
275 return;
276 };
277
278 if !is_dir {
279 return;
280 }
281
282 let status_msg = if is_expanded {
283 t!("explorer.collapsing").to_string()
284 } else {
285 t!("explorer.loading_dir", name = &name).to_string()
286 };
287 self.set_status_message(status_msg);
288
289 let active_id = self.active_window;
290 let fs = std::sync::Arc::clone(&self.authority().filesystem);
295 if let (Some(runtime), Some(explorer)) = (
296 self.tokio_runtime.as_ref(),
297 self.windows
298 .get_mut(&active_id)
299 .and_then(|w| w.file_explorer.as_mut()),
300 ) {
301 let result = runtime.block_on(explorer.toggle_with_chain(selected_id));
302
303 let final_name = explorer
304 .tree()
305 .get_node(selected_id)
306 .map(|n| n.entry.name.clone());
307 let final_expanded = explorer
308 .tree()
309 .get_node(selected_id)
310 .map(|n| n.is_expanded())
311 .unwrap_or(false);
312
313 let mut needs_decoration_rebuild = false;
315
316 match result {
317 Ok(()) => {
318 if final_expanded {
319 let node_info = explorer
320 .tree()
321 .get_node(selected_id)
322 .map(|n| (n.entry.path.clone(), n.entry.is_symlink()));
323
324 if let Some((dir_path, is_symlink)) = node_info {
325 crate::app::file_operations::load_gitignore_via_fs(
326 fs.as_ref(),
327 explorer,
328 &dir_path,
329 );
330
331 if is_symlink {
335 tracing::debug!(
336 "Symlink directory expanded, will rebuild decoration cache: {:?}",
337 dir_path
338 );
339 needs_decoration_rebuild = true;
340 }
341 }
342 }
343
344 if let Some(name) = final_name {
345 let msg = if final_expanded {
346 t!("explorer.expanded", name = &name).to_string()
347 } else {
348 t!("explorer.collapsed", name = &name).to_string()
349 };
350 self.set_status_message(msg);
351 }
352 }
353 Err(e) => {
354 self.set_status_message(
355 t!("explorer.error", error = e.to_string()).to_string(),
356 );
357 }
358 }
359
360 if needs_decoration_rebuild {
363 let window = self.active_window_mut();
364 window.rebuild_file_explorer_decoration_cache();
365 window.rebuild_file_explorer_slot_override_cache();
366 }
367 }
368 }
369
370 pub fn file_explorer_open_file(&mut self) -> AnyhowResult<()> {
371 let entry_type = self
372 .file_explorer()
373 .as_ref()
374 .and_then(|explorer| explorer.get_selected_entry())
375 .map(|entry| (entry.is_dir(), entry.path.clone(), entry.name.clone()));
376
377 if let Some((is_dir, path, name)) = entry_type {
378 if is_dir {
379 self.file_explorer_toggle_expand();
380 } else {
381 tracing::info!("[SYNTAX DEBUG] file_explorer opening file: {:?}", path);
382 match self.open_file(&path) {
383 Ok(id) => {
384 self.active_window_mut().promote_buffer_from_preview(id);
388 self.set_status_message(
389 t!("explorer.opened_file", name = &name).to_string(),
390 );
391 self.active_window_mut().focus_editor();
392 }
393 Err(e) => {
394 if let Some(confirmation) =
397 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
398 {
399 self.start_large_file_encoding_confirmation(confirmation);
400 } else {
401 self.set_status_message(
402 t!("file.error_opening", error = e.to_string()).to_string(),
403 );
404 }
405 }
406 }
407 }
408 }
409 Ok(())
410 }
411
412 pub fn file_explorer_refresh(&mut self) {
413 let (selected_id, node_name) = if let Some(explorer) = self.file_explorer() {
414 if let Some(selected_id) = explorer.get_selected() {
415 let node_name = explorer
416 .tree()
417 .get_node(selected_id)
418 .map(|n| n.entry.name.clone());
419 (Some(selected_id), node_name)
420 } else {
421 (None, None)
422 }
423 } else {
424 return;
425 };
426
427 let Some(selected_id) = selected_id else {
428 return;
429 };
430
431 if let Some(name) = &node_name {
432 self.set_status_message(t!("explorer.refreshing", name = name).to_string());
433 }
434
435 let active_id = self.active_window;
436 if let (Some(runtime), Some(explorer)) = (
437 self.tokio_runtime.as_ref(),
438 self.windows
439 .get_mut(&active_id)
440 .and_then(|w| w.file_explorer.as_mut()),
441 ) {
442 let tree = explorer.tree_mut();
443 let result = runtime.block_on(tree.refresh_node(selected_id));
444 match result {
445 Ok(()) => {
446 if let Some(name) = node_name {
447 self.set_status_message(t!("explorer.refreshed", name = &name).to_string());
448 } else {
449 self.set_status_message(t!("explorer.refreshed_default").to_string());
450 }
451 }
452 Err(e) => {
453 self.set_status_message(
454 t!("explorer.error_refreshing", error = e.to_string()).to_string(),
455 );
456 }
457 }
458 }
459 }
460
461 pub fn file_explorer_new_file(&mut self) {
462 let active_id = self.active_window;
463 let fs = std::sync::Arc::clone(&self.authority().filesystem);
467 if let Some(explorer) = self
468 .windows
469 .get_mut(&active_id)
470 .and_then(|w| w.file_explorer.as_mut())
471 {
472 if let Some(selected_id) = explorer.get_selected() {
473 let node = explorer.tree().get_node(selected_id);
474 if let Some(node) = node {
475 let parent_path = get_parent_dir_path(node);
476 let filename = format!("untitled_{}.txt", timestamp_suffix());
477 let file_path = parent_path.join(&filename);
478
479 if let Some(runtime) = &self.tokio_runtime {
480 let path_clone = file_path.clone();
481 let result = fs.create_file(&path_clone).map(|_| ());
482
483 match result {
484 Ok(_) => {
485 let parent_id =
486 get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
487 let tree = explorer.tree_mut();
488 if let Err(e) =
489 runtime.block_on(tree.reload_expanded_node(parent_id))
490 {
491 tracing::warn!("Failed to refresh file tree: {}", e);
492 }
493 if let Some(explorer) = self.file_explorer_mut().as_mut() {
494 explorer.navigate_to_path(&path_clone);
495 }
496 self.set_status_message(
497 t!("explorer.created_file", name = &filename).to_string(),
498 );
499 self.notify_file_explorer_change(&path_clone);
500
501 if let Err(e) = self.open_file(&path_clone) {
503 tracing::warn!("Failed to open new file: {}", e);
504 }
505
506 let prompt = crate::view::prompt::Prompt::new(
507 t!("explorer.new_file_prompt").to_string(),
508 crate::view::prompt::PromptType::FileExplorerRename {
509 original_path: path_clone,
510 original_name: filename.clone(),
511 is_new_file: true,
512 },
513 );
514 self.active_window_mut().prompt = Some(prompt);
515 }
516 Err(e) => {
517 self.set_status_message(
518 t!("explorer.error_creating_file", error = e.to_string())
519 .to_string(),
520 );
521 }
522 }
523 }
524 }
525 }
526 }
527 }
528
529 pub fn file_explorer_new_directory(&mut self) {
530 let active_id = self.active_window;
531 let fs = std::sync::Arc::clone(&self.authority().filesystem);
532 if let Some(explorer) = self
533 .windows
534 .get_mut(&active_id)
535 .and_then(|w| w.file_explorer.as_mut())
536 {
537 if let Some(selected_id) = explorer.get_selected() {
538 let node = explorer.tree().get_node(selected_id);
539 if let Some(node) = node {
540 let parent_path = get_parent_dir_path(node);
541 let dirname = format!("New Folder {}", timestamp_suffix());
542 let dir_path = parent_path.join(&dirname);
543
544 if let Some(runtime) = &self.tokio_runtime {
545 let path_clone = dir_path.clone();
546 let dirname_clone = dirname.clone();
547 let result = fs.create_dir(&path_clone);
548
549 match result {
550 Ok(_) => {
551 let parent_id =
552 get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
553 let tree = explorer.tree_mut();
554 if let Err(e) =
555 runtime.block_on(tree.reload_expanded_node(parent_id))
556 {
557 tracing::warn!("Failed to refresh file tree: {}", e);
558 }
559 if let Some(explorer) = self.file_explorer_mut().as_mut() {
560 explorer.navigate_to_path(&path_clone);
561 }
562 self.set_status_message(
563 t!("explorer.created_dir", name = &dirname_clone).to_string(),
564 );
565 self.notify_file_explorer_change(&path_clone);
566
567 let prompt = crate::view::prompt::Prompt::with_initial_text(
568 t!("explorer.new_directory_prompt").to_string(),
569 crate::view::prompt::PromptType::FileExplorerRename {
570 original_path: path_clone,
571 original_name: dirname_clone,
572 is_new_file: true,
573 },
574 dirname,
575 );
576 self.active_window_mut().prompt = Some(prompt);
577 }
578 Err(e) => {
579 self.set_status_message(
580 t!("explorer.error_creating_dir", error = e.to_string())
581 .to_string(),
582 );
583 }
584 }
585 }
586 }
587 }
588 }
589 }
590
591 pub fn file_explorer_delete(&mut self) {
592 let Some(explorer) = self.file_explorer() else {
593 return;
594 };
595 let root_id = explorer.tree().root_id();
596 let selected_ids = explorer.effective_selection();
597
598 let paths: Vec<(PathBuf, bool)> = selected_ids
599 .iter()
600 .filter(|&&id| id != root_id)
601 .filter_map(|&id| {
602 explorer
603 .tree()
604 .get_node(id)
605 .map(|n| (n.entry.path.clone(), n.is_dir()))
606 })
607 .collect();
608
609 if paths.is_empty() {
610 self.set_status_message(t!("explorer.cannot_delete_root").to_string());
611 return;
612 }
613
614 if paths.len() == 1 {
615 let (path, is_dir) = paths.into_iter().next().unwrap();
616 let name = path
617 .file_name()
618 .unwrap_or_default()
619 .to_string_lossy()
620 .to_string();
621 let type_str = if is_dir { "directory" } else { "file" };
622 self.start_prompt(
623 t!("explorer.delete_confirm", "type" = type_str, name = &name).to_string(),
624 PromptType::ConfirmDeleteFile { path, is_dir },
625 );
626 } else {
627 let count = paths.len();
628 let all_paths: Vec<PathBuf> = paths.into_iter().map(|(p, _)| p).collect();
629 let names = format_path_preview_for_prompt(&all_paths, 3);
633 self.start_prompt(
634 t!(
635 "explorer.delete_multi_confirm",
636 count = count,
637 names = &names
638 )
639 .to_string(),
640 PromptType::ConfirmMultiDelete { paths: all_paths },
641 );
642 }
643 }
644
645 pub fn perform_file_explorer_delete(&mut self, path: std::path::PathBuf, _is_dir: bool) {
649 let name = path
650 .file_name()
651 .map(|n| n.to_string_lossy().to_string())
652 .unwrap_or_default();
653
654 let delete_result = if self
657 .authority()
658 .filesystem
659 .remote_connection_info()
660 .is_some()
661 {
662 self.move_to_remote_trash(&path)
663 } else {
664 trash::delete(&path).map_err(std::io::Error::other)
665 };
666
667 match delete_result {
668 Ok(_) => {
669 let to_close = self.buffer_ids_under_path(&path);
679 for id in to_close {
680 if let Err(e) = self.force_close_buffer(id) {
681 tracing::warn!(
682 "Failed to close buffer {:?} after delete of {:?}: {}",
683 id,
684 path,
685 e
686 );
687 }
688 }
689
690 let active_id = self.active_window;
692 if let Some(explorer) = self
693 .windows
694 .get_mut(&active_id)
695 .and_then(|w| w.file_explorer.as_mut())
696 {
697 if let Some(runtime) = &self.tokio_runtime {
698 if let Some(node) = explorer.tree().get_node_by_path(&path) {
700 let node_id = node.id;
701 let parent_id = get_parent_node_id(explorer.tree(), node_id, false);
702
703 let deleted_index = explorer.get_selected_index();
705
706 if let Err(e) = runtime
707 .block_on(explorer.tree_mut().reload_expanded_node(parent_id))
708 {
709 tracing::warn!("Failed to refresh file tree after delete: {}", e);
710 }
711
712 explorer.clear_multi_selection();
717
718 let count = explorer.visible_count();
721 if count > 0 {
722 let new_index = if let Some(idx) = deleted_index {
723 idx.min(count.saturating_sub(1))
724 } else {
725 0
726 };
727 if let Some(node_id) = explorer.get_node_at_index(new_index) {
728 explorer.set_selected(Some(node_id));
729 }
730 } else {
731 explorer.set_selected(Some(parent_id));
733 }
734 }
735 }
736 }
737 self.set_status_message(t!("explorer.moved_to_trash", name = &name).to_string());
738 self.notify_file_explorer_change(&path);
739
740 self.active_window_mut().key_context = KeyContext::FileExplorer;
742 }
743 Err(e) => {
744 self.set_status_message(
745 t!("explorer.error_trash", error = e.to_string()).to_string(),
746 );
747 }
748 }
749 }
750
751 fn move_to_remote_trash(&self, path: &std::path::Path) -> std::io::Result<()> {
753 let home = self.authority().filesystem.home_dir()?;
755 let trash_dir = home.join(".local/share/fresh/trash");
756
757 if !self.authority().filesystem.exists(&trash_dir) {
759 self.authority().filesystem.create_dir_all(&trash_dir)?;
760 }
761
762 let file_name = path
764 .file_name()
765 .unwrap_or_else(|| std::ffi::OsStr::new("unnamed"));
766 let timestamp = std::time::SystemTime::now()
767 .duration_since(std::time::UNIX_EPOCH)
768 .map(|d| d.as_secs())
769 .unwrap_or(0);
770 let trash_name = format!("{}.{}", file_name.to_string_lossy(), timestamp);
771 let trash_path = trash_dir.join(trash_name);
772
773 self.authority().filesystem.rename(path, &trash_path)
775 }
776
777 pub fn file_explorer_rename(&mut self) {
778 if let Some(explorer) = self.file_explorer() {
779 if let Some(selected_id) = explorer.get_selected() {
780 if selected_id == explorer.tree().root_id() {
782 self.set_status_message(t!("explorer.cannot_rename_root").to_string());
783 return;
784 }
785
786 let node = explorer.tree().get_node(selected_id);
787 if let Some(node) = node {
788 let old_path = node.entry.path.clone();
789 let old_name = node.entry.name.clone();
790
791 let prompt = crate::view::prompt::Prompt::with_initial_text_for_edit(
796 t!("explorer.rename_prompt").to_string(),
797 crate::view::prompt::PromptType::FileExplorerRename {
798 original_path: old_path,
799 original_name: old_name.clone(),
800 is_new_file: false,
801 },
802 old_name,
803 );
804 self.active_window_mut().prompt = Some(prompt);
805 }
806 }
807 }
808 }
809
810 pub fn perform_file_explorer_rename(
812 &mut self,
813 original_path: std::path::PathBuf,
814 original_name: String,
815 new_name: String,
816 is_new_file: bool,
817 ) {
818 if new_name.is_empty() || new_name == original_name {
819 self.set_status_message(t!("explorer.rename_cancelled").to_string());
820 return;
821 }
822
823 if new_name.chars().any(std::path::is_separator) {
828 self.set_status_message(t!("explorer.rename_invalid_separator").to_string());
829 return;
830 }
831 if new_name == "." || new_name == ".." {
832 self.set_status_message(t!("explorer.rename_invalid_dot").to_string());
833 return;
834 }
835
836 let new_path = original_path
837 .parent()
838 .map(|p| p.join(&new_name))
839 .unwrap_or_else(|| original_path.clone());
840
841 if self.tokio_runtime.is_some() {
842 let result = self
843 .authority()
844 .filesystem
845 .rename(&original_path, &new_path);
846
847 match result {
848 Ok(_) => {
849 let active_id = self.active_window;
853 if let (Some(runtime), Some(explorer)) = (
854 self.tokio_runtime.as_ref(),
855 self.windows
856 .get_mut(&active_id)
857 .and_then(|w| w.file_explorer.as_mut()),
858 ) {
859 if let Some(selected_id) = explorer.get_selected() {
860 let parent_id = get_parent_node_id(explorer.tree(), selected_id, false);
861 let tree = explorer.tree_mut();
862 if let Err(e) = runtime.block_on(tree.reload_expanded_node(parent_id)) {
863 tracing::warn!("Failed to refresh file tree after rename: {}", e);
864 }
865 }
866 explorer.clear_multi_selection();
870 explorer.navigate_to_path(&new_path);
872 }
873
874 let relocated = self.relocate_buffers_for_rename(&original_path, &new_path);
882
883 if is_new_file && !relocated.is_empty() {
887 self.active_window_mut().key_context = KeyContext::Normal;
888 }
889
890 self.set_status_message(
891 t!("explorer.renamed", old = &original_name, new = &new_name).to_string(),
892 );
893 self.notify_file_explorer_change(&new_path);
894 }
895 Err(e) => {
896 self.set_status_message(
897 t!("explorer.error_renaming", error = e.to_string()).to_string(),
898 );
899 }
900 }
901 }
902 }
903
904 pub fn file_explorer_toggle_hidden(&mut self) {
905 let show_hidden = if let Some(explorer) = self.file_explorer_mut() {
906 explorer.toggle_show_hidden();
907 explorer.ignore_patterns().show_hidden()
908 } else {
909 return;
910 };
911
912 let msg = if show_hidden {
913 t!("explorer.showing_hidden")
914 } else {
915 t!("explorer.hiding_hidden")
916 };
917 self.set_status_message(msg.to_string());
918
919 self.config_mut().file_explorer.show_hidden = show_hidden;
921 self.persist_config_change(
922 "/file_explorer/show_hidden",
923 serde_json::Value::Bool(show_hidden),
924 );
925 }
926
927 pub fn file_explorer_toggle_gitignored(&mut self) {
928 let show_gitignored = if let Some(explorer) = self.file_explorer_mut() {
929 explorer.toggle_show_gitignored();
930 explorer.ignore_patterns().show_gitignored()
931 } else {
932 return;
933 };
934
935 let msg = if show_gitignored {
936 t!("explorer.showing_gitignored")
937 } else {
938 t!("explorer.hiding_gitignored")
939 };
940 self.set_status_message(msg.to_string());
941
942 self.config_mut().file_explorer.show_gitignored = show_gitignored;
944 self.persist_config_change(
945 "/file_explorer/show_gitignored",
946 serde_json::Value::Bool(show_gitignored),
947 );
948 }
949
950 pub fn file_explorer_paste(&mut self) {
973 let clipboard = match self.active_window().file_explorer_clipboard.clone() {
974 Some(c) => c,
975 None => {
976 self.set_status_message(t!("explorer.paste_no_source").to_string());
977 return;
978 }
979 };
980
981 let dst_dir = if let Some(explorer) = self.file_explorer() {
982 if let Some(selected_id) = explorer.get_selected() {
983 if let Some(node) = explorer.tree().get_node(selected_id) {
984 get_parent_dir_path(node)
985 } else {
986 return;
987 }
988 } else {
989 return;
990 }
991 } else {
992 return;
993 };
994
995 let is_cut = clipboard.is_cut;
996
997 if clipboard.paths.len() == 1 {
998 let src = clipboard.paths[0].clone();
999 let file_name = match src.file_name() {
1000 Some(n) => n.to_os_string(),
1001 None => return,
1002 };
1003 let dst_path = dst_dir.join(&file_name);
1004
1005 if src.parent().map(|p| p == dst_dir).unwrap_or(false) {
1006 if is_cut {
1007 self.active_window_mut().file_explorer_clipboard = None;
1012 self.set_status_message(t!("explorer.cut_cancelled").to_string());
1013 return;
1014 } else {
1015 let unique = unique_paste_name(
1016 &*self.authority().filesystem,
1017 &dst_dir,
1018 &file_name.to_string_lossy(),
1019 );
1020 self.perform_file_explorer_paste(src, unique, false);
1021 return;
1022 }
1023 }
1024
1025 if self.authority().filesystem.exists(&dst_path) {
1026 let name = truncate_name_for_prompt(&file_name.to_string_lossy(), 40);
1027 self.start_prompt(
1028 t!("explorer.paste_conflict", name = &name).to_string(),
1029 crate::view::prompt::PromptType::ConfirmPasteConflict {
1030 src,
1031 dst: dst_path,
1032 is_cut,
1033 },
1034 );
1035 } else {
1036 self.perform_file_explorer_paste(src, dst_path, is_cut);
1037 }
1038 } else {
1039 let mut safe: Vec<(PathBuf, PathBuf)> = Vec::new();
1041 let mut conflicts: Vec<(PathBuf, PathBuf)> = Vec::new();
1042
1043 for src in &clipboard.paths {
1044 let file_name = match src.file_name() {
1045 Some(n) => n.to_os_string(),
1046 None => continue,
1047 };
1048 let dst_path = dst_dir.join(&file_name);
1049 let is_same_location = src.parent().map(|p| p == dst_dir).unwrap_or(false);
1050
1051 if is_same_location {
1052 if !is_cut {
1053 let unique = unique_paste_name(
1055 &*self.authority().filesystem,
1056 &dst_dir,
1057 &file_name.to_string_lossy(),
1058 );
1059 safe.push((src.clone(), unique));
1060 }
1061 } else if self.authority().filesystem.exists(&dst_path) {
1063 conflicts.push((src.clone(), dst_path));
1064 } else {
1065 safe.push((src.clone(), dst_path));
1066 }
1067 }
1068
1069 if safe.is_empty() && conflicts.is_empty() {
1070 if is_cut {
1074 self.active_window_mut().file_explorer_clipboard = None;
1075 self.set_status_message(t!("explorer.cut_cancelled").to_string());
1076 } else {
1077 self.set_status_message(t!("explorer.paste_same_location").to_string());
1078 }
1079 return;
1080 }
1081
1082 if conflicts.is_empty() {
1083 self.execute_resolved_multi_paste(safe, vec![], is_cut);
1084 } else {
1085 let name = truncate_name_for_prompt(
1086 &conflicts[0]
1087 .1
1088 .file_name()
1089 .unwrap_or_default()
1090 .to_string_lossy(),
1091 40,
1092 );
1093 self.start_prompt(
1094 t!("explorer.paste_conflict_multi", name = &name).to_string(),
1095 crate::view::prompt::PromptType::ConfirmMultiPasteConflict {
1096 safe,
1097 confirmed: Vec::new(),
1098 pending: conflicts,
1099 is_cut,
1100 },
1101 );
1102 }
1103 }
1104 }
1105
1106 pub(super) fn execute_resolved_multi_paste(
1114 &mut self,
1115 safe: Vec<(PathBuf, PathBuf)>,
1116 to_overwrite: Vec<(PathBuf, PathBuf)>,
1117 is_cut: bool,
1118 ) {
1119 let total = safe.len() + to_overwrite.len();
1120 if total == 0 {
1121 return;
1122 }
1123
1124 let mut succeeded: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(total);
1125 let mut clean_moves: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(total);
1132 let mut first_error: Option<std::io::Error> = None;
1133 let mut partial_moves: Vec<(PathBuf, std::io::Error)> = Vec::new();
1134 for (src, dst) in safe.into_iter().chain(to_overwrite) {
1135 match self.paste_one_fs_op(&src, &dst, is_cut) {
1136 PasteOpOutcome::Ok => {
1137 clean_moves.push((src.clone(), dst.clone()));
1138 succeeded.push((src, dst));
1139 }
1140 PasteOpOutcome::SourceRemovalFailed {
1141 dst: landed_dst,
1142 err,
1143 } => {
1144 succeeded.push((src, landed_dst.clone()));
1148 partial_moves.push((landed_dst, err));
1149 }
1150 PasteOpOutcome::Failed(e) => {
1151 if first_error.is_none() {
1152 first_error = Some(e);
1153 }
1154 }
1155 }
1156 }
1157
1158 if is_cut {
1164 for (src, dst) in &clean_moves {
1165 self.relocate_buffers_for_rename(src, dst);
1166 }
1167 }
1168
1169 if !succeeded.is_empty() {
1170 let first_dst = succeeded[0].1.clone();
1171 let any_src = succeeded[0].0.clone();
1172 self.refresh_tree_after_paste(&any_src, &first_dst, is_cut);
1173 }
1174
1175 if !partial_moves.is_empty() {
1176 let (first_dst, first_err) = &partial_moves[0];
1179 let name = first_dst
1180 .file_name()
1181 .map(|n| n.to_string_lossy().to_string())
1182 .unwrap_or_default();
1183 let msg = if partial_moves.len() == 1 {
1184 t!(
1185 "explorer.move_source_removal_failed",
1186 name = &name,
1187 error = first_err.to_string()
1188 )
1189 .to_string()
1190 } else {
1191 t!(
1192 "explorer.move_source_removal_failed_n",
1193 count = partial_moves.len()
1194 )
1195 .to_string()
1196 };
1197 self.set_status_message(msg);
1198 } else if let Some(e) = &first_error {
1199 let msg = if is_cut {
1200 t!("explorer.error_moving", error = e.to_string()).to_string()
1201 } else {
1202 t!("explorer.error_copying", error = e.to_string()).to_string()
1203 };
1204 self.set_status_message(msg);
1205 } else if total > 1 {
1206 let msg = if is_cut {
1207 t!("explorer.pasted_moved_n", count = total).to_string()
1208 } else {
1209 t!("explorer.pasted_n", count = total).to_string()
1210 };
1211 self.set_status_message(msg);
1212 } else if let Some((_, dst)) = succeeded.first() {
1213 let name = dst
1214 .file_name()
1215 .map(|n| n.to_string_lossy().to_string())
1216 .unwrap_or_default();
1217 let msg = if is_cut {
1218 t!("explorer.pasted_moved", name = &name).to_string()
1219 } else {
1220 t!("explorer.pasted", name = &name).to_string()
1221 };
1222 self.set_status_message(msg);
1223 }
1224
1225 if is_cut && first_error.is_none() && partial_moves.is_empty() {
1229 self.active_window_mut().file_explorer_clipboard = None;
1230 }
1231 self.active_window_mut().key_context = KeyContext::FileExplorer;
1232 }
1233
1234 fn paste_one_fs_op(&self, src: &Path, dst: &Path, is_cut: bool) -> PasteOpOutcome {
1238 let src_is_dir = self.authority().filesystem.is_dir(src).unwrap_or(false);
1239
1240 if src_is_dir && dst.starts_with(src) {
1248 return PasteOpOutcome::Failed(std::io::Error::new(
1249 std::io::ErrorKind::InvalidInput,
1250 "Cannot paste a directory into itself",
1251 ));
1252 }
1253
1254 if is_cut {
1255 match self.authority().filesystem.rename(src, dst) {
1260 Ok(()) => PasteOpOutcome::Ok,
1261 Err(e) if e.kind() == std::io::ErrorKind::CrossesDevices => {
1262 let copy_result = if src_is_dir {
1263 self.authority().filesystem.copy_dir_all(src, dst)
1264 } else {
1265 self.authority().filesystem.copy(src, dst).map(|_| ())
1266 };
1267 match copy_result {
1268 Ok(()) => {
1269 let remove_result = if src_is_dir {
1275 self.authority().filesystem.remove_dir_all(src)
1276 } else {
1277 self.authority().filesystem.remove_file(src)
1278 };
1279 match remove_result {
1280 Ok(()) => PasteOpOutcome::Ok,
1281 Err(remove_err) => PasteOpOutcome::SourceRemovalFailed {
1282 dst: dst.to_path_buf(),
1283 err: remove_err,
1284 },
1285 }
1286 }
1287 Err(copy_err) => {
1288 let cleanup = if src_is_dir {
1294 self.authority().filesystem.remove_dir_all(dst)
1295 } else {
1296 self.authority().filesystem.remove_file(dst)
1297 };
1298 if let Err(cleanup_err) = cleanup {
1299 tracing::warn!(
1300 "Failed to roll back partial destination {:?} after copy \
1301 fallback failed: {}",
1302 dst,
1303 cleanup_err
1304 );
1305 }
1306 PasteOpOutcome::Failed(copy_err)
1307 }
1308 }
1309 }
1310 Err(e) => PasteOpOutcome::Failed(e),
1311 }
1312 } else if src_is_dir {
1313 match self.authority().filesystem.copy_dir_all(src, dst) {
1314 Ok(()) => PasteOpOutcome::Ok,
1315 Err(e) => PasteOpOutcome::Failed(e),
1316 }
1317 } else {
1318 match self.authority().filesystem.copy(src, dst) {
1319 Ok(_) => PasteOpOutcome::Ok,
1320 Err(e) => PasteOpOutcome::Failed(e),
1321 }
1322 }
1323 }
1324
1325 fn refresh_tree_after_paste(&mut self, src: &Path, dst: &Path, is_cut: bool) {
1330 let active_id = self.active_window;
1331 let Some(explorer) = self
1334 .windows
1335 .get_mut(&active_id)
1336 .and_then(|w| w.file_explorer.as_mut())
1337 else {
1338 return;
1339 };
1340 if let Some(runtime) = &self.tokio_runtime {
1341 if let Some(dst_parent) = dst.parent() {
1343 if let Some(dst_parent_node) = explorer.tree().get_node_by_path(dst_parent) {
1344 let pid = dst_parent_node.id;
1345 if let Err(e) = runtime.block_on(explorer.tree_mut().reload_expanded_node(pid))
1346 {
1347 tracing::warn!("Failed to reload destination directory after paste: {}", e);
1348 }
1349 }
1350 }
1351 if is_cut {
1361 if let Some(src_parent) = src.parent() {
1362 if let Some(src_parent_node) = explorer.tree().get_node_by_path(src_parent) {
1363 let pid = src_parent_node.id;
1364 if let Err(e) =
1365 runtime.block_on(explorer.tree_mut().reload_expanded_node(pid))
1366 {
1367 tracing::warn!("Failed to refresh source directory after move: {}", e);
1368 }
1369 }
1370 }
1371 }
1372 }
1373 explorer.clear_multi_selection();
1378 explorer.navigate_to_path(dst);
1379
1380 self.notify_file_explorer_change(dst);
1381 }
1382
1383 pub(super) fn notify_file_explorer_change(&mut self, path: &Path) {
1395 self.plugin_manager.read().unwrap().run_hook(
1396 "after_file_explorer_change",
1397 crate::services::plugins::hooks::HookArgs::AfterFileExplorerChange {
1398 path: path.to_path_buf(),
1399 },
1400 );
1401 }
1402
1403 pub fn perform_file_explorer_paste(&mut self, src: PathBuf, dst: PathBuf, is_cut: bool) {
1404 let name = dst
1405 .file_name()
1406 .map(|n| n.to_string_lossy().to_string())
1407 .unwrap_or_default();
1408
1409 match self.paste_one_fs_op(&src, &dst, is_cut) {
1410 PasteOpOutcome::Ok => {
1411 if is_cut {
1418 self.relocate_buffers_for_rename(&src, &dst);
1419 }
1420 self.refresh_tree_after_paste(&src, &dst, is_cut);
1421 if is_cut {
1422 self.active_window_mut().file_explorer_clipboard = None;
1423 self.set_status_message(t!("explorer.pasted_moved", name = &name).to_string());
1424 } else {
1425 self.set_status_message(t!("explorer.pasted", name = &name).to_string());
1426 }
1427 self.active_window_mut().key_context = KeyContext::FileExplorer;
1428 }
1429 PasteOpOutcome::SourceRemovalFailed {
1430 dst: landed_dst,
1431 err,
1432 } => {
1433 self.refresh_tree_after_paste(&src, &landed_dst, is_cut);
1438 self.set_status_message(
1439 t!(
1440 "explorer.move_source_removal_failed",
1441 name = &name,
1442 error = err.to_string()
1443 )
1444 .to_string(),
1445 );
1446 self.active_window_mut().key_context = KeyContext::FileExplorer;
1449 }
1450 PasteOpOutcome::Failed(e) => {
1451 let msg = if is_cut {
1452 t!("explorer.error_moving", error = e.to_string()).to_string()
1453 } else {
1454 t!("explorer.error_copying", error = e.to_string()).to_string()
1455 };
1456 self.set_status_message(msg);
1457 }
1458 }
1459 }
1460
1461 pub fn file_explorer_duplicate(&mut self) {
1467 let Some(explorer) = self.file_explorer() else {
1468 return;
1469 };
1470 let root_id = explorer.tree().root_id();
1471 let selected_ids = explorer.effective_selection();
1472 let sources: Vec<PathBuf> = selected_ids
1473 .iter()
1474 .filter(|&&id| id != root_id)
1475 .filter_map(|&id| explorer.tree().get_node(id).map(|n| n.entry.path.clone()))
1476 .collect();
1477
1478 if sources.is_empty() {
1479 self.set_status_message(t!("explorer.cannot_duplicate_root").to_string());
1480 return;
1481 }
1482
1483 let mut ops: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(sources.len());
1487 for src in &sources {
1488 let Some(parent) = src.parent() else {
1489 continue;
1490 };
1491 let Some(file_name) = src.file_name() else {
1492 continue;
1493 };
1494 let dst = unique_paste_name(
1495 &*self.authority().filesystem,
1496 parent,
1497 &file_name.to_string_lossy(),
1498 );
1499 ops.push((src.clone(), dst));
1500 }
1501
1502 if ops.is_empty() {
1503 return;
1504 }
1505
1506 let mut succeeded: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(ops.len());
1507 let mut first_error: Option<std::io::Error> = None;
1508 for (src, dst) in ops {
1509 match self.paste_one_fs_op(&src, &dst, false) {
1510 PasteOpOutcome::Ok => succeeded.push((src, dst)),
1511 PasteOpOutcome::SourceRemovalFailed { .. } => {
1512 unreachable!("paste_one_fs_op returned SourceRemovalFailed for a non-cut op");
1514 }
1515 PasteOpOutcome::Failed(e) => {
1516 if first_error.is_none() {
1517 first_error = Some(e);
1518 }
1519 }
1520 }
1521 }
1522
1523 if !succeeded.is_empty() {
1524 let (first_src, first_dst) = succeeded[0].clone();
1525 self.refresh_tree_after_paste(&first_src, &first_dst, false);
1526 }
1527
1528 let msg = if let Some(e) = &first_error {
1529 t!("explorer.error_copying", error = e.to_string()).to_string()
1530 } else if succeeded.len() == 1 {
1531 let name = succeeded[0]
1532 .1
1533 .file_name()
1534 .map(|n| n.to_string_lossy().to_string())
1535 .unwrap_or_default();
1536 t!("explorer.duplicated", name = &name).to_string()
1537 } else {
1538 t!("explorer.duplicated_n", count = succeeded.len()).to_string()
1539 };
1540 self.set_status_message(msg);
1541 self.active_window_mut().key_context = KeyContext::FileExplorer;
1542 }
1543
1544 pub fn file_explorer_copy_path(&mut self, relative: bool) {
1551 let Some(explorer) = self.file_explorer() else {
1552 return;
1553 };
1554 let selected_ids = explorer.effective_selection();
1555 let paths: Vec<PathBuf> = selected_ids
1556 .iter()
1557 .filter_map(|&id| explorer.tree().get_node(id).map(|n| n.entry.path.clone()))
1558 .collect();
1559
1560 if paths.is_empty() {
1561 self.set_status_message(t!("clipboard.no_file_path").to_string());
1562 return;
1563 }
1564
1565 let working_dir = self.working_dir().to_path_buf();
1566 let rendered: Vec<String> = paths
1567 .iter()
1568 .map(|p| {
1569 if relative {
1570 p.strip_prefix(&working_dir)
1571 .unwrap_or(p)
1572 .to_string_lossy()
1573 .into_owned()
1574 } else {
1575 p.to_string_lossy().into_owned()
1576 }
1577 })
1578 .collect();
1579
1580 let joined = rendered.join("\n");
1581 self.clipboard.copy(joined.clone());
1582
1583 let msg = if rendered.len() == 1 {
1584 t!("clipboard.copied_path", path = &rendered[0]).to_string()
1585 } else {
1586 t!("clipboard.copied_paths_n", count = rendered.len()).to_string()
1587 };
1588 self.set_status_message(msg);
1589 }
1590}
1591
1592impl crate::app::window::Window {
1593 pub(crate) fn init_file_explorer(&mut self) {
1600 let is_remote = self
1601 .authority()
1602 .filesystem
1603 .remote_connection_info()
1604 .is_some();
1605 let root_exists = self
1606 .authority()
1607 .filesystem
1608 .is_dir(&self.root)
1609 .unwrap_or(false);
1610 let root_path = if is_remote && !root_exists {
1611 match self.authority().filesystem.home_dir() {
1612 Ok(home) => home,
1613 Err(e) => {
1614 tracing::error!("Failed to get remote home directory: {}", e);
1615 self.set_status_message(format!("Failed to get remote home: {}", e));
1616 return;
1617 }
1618 }
1619 } else {
1620 self.root.clone()
1621 };
1622
1623 let Some(runtime) = self.resources.tokio_runtime.clone() else {
1624 return;
1625 };
1626 let fs_manager = Arc::clone(&self.resources.fs_manager);
1627 let sender = self.bridge.sender();
1628 let window_id = self.id;
1631 runtime.spawn(async move {
1632 match FileTree::new(root_path, fs_manager).await {
1633 Ok(mut tree) => {
1634 let root_id = tree.root_id();
1635 if let Err(e) = tree.expand_node(root_id).await {
1636 tracing::warn!("Failed to expand root directory: {}", e);
1637 }
1638 let view = FileTreeView::new(tree);
1639 #[allow(clippy::let_underscore_must_use)]
1641 let _ = sender.send(AsyncMessage::FileExplorerInitialized {
1642 window: window_id,
1643 view,
1644 });
1645 }
1646 Err(e) => {
1647 tracing::error!("Failed to initialize file explorer: {}", e);
1648 }
1649 }
1650 });
1651 self.set_status_message(t!("explorer.initializing").to_string());
1652 }
1653
1654 pub(crate) fn install_initialized_file_explorer(
1660 &mut self,
1661 mut view: FileTreeView,
1662 defaults: FileExplorerViewDefaults,
1663 ) {
1664 let root_id = view.tree().root_id();
1665 if let Some(root_path) = view.tree().get_node(root_id).map(|n| n.entry.path.clone()) {
1666 crate::app::file_operations::load_gitignore_via_fs(
1667 self.authority().filesystem.as_ref(),
1668 &mut view,
1669 &root_path,
1670 );
1671 }
1672 let show_hidden = self
1675 .pending_file_explorer_show_hidden
1676 .take()
1677 .unwrap_or(defaults.show_hidden);
1678 view.ignore_patterns_mut().set_show_hidden(show_hidden);
1679 let show_gitignored = self
1680 .pending_file_explorer_show_gitignored
1681 .take()
1682 .unwrap_or(defaults.show_gitignored);
1683 view.ignore_patterns_mut()
1684 .set_show_gitignored(show_gitignored);
1685 view.set_compact_directories(defaults.compact_directories);
1686 self.file_explorer = Some(view);
1687 if self.file_explorer_visible {
1690 self.sync_file_explorer_to_active_file();
1691 }
1692 }
1693
1694 pub(crate) fn install_expanded_file_explorer(&mut self, mut view: FileTreeView) {
1697 view.update_scroll_for_selection();
1698 self.file_explorer = Some(view);
1699 self.file_explorer_sync_in_progress = false;
1700 }
1701
1702 pub fn focus_editor(&mut self) {
1705 self.key_context = KeyContext::Normal;
1706 self.set_status_message(t!("editor.focused").to_string());
1707 }
1708
1709 pub fn file_explorer_search_clear(&mut self) {
1716 if matches!(
1717 self.file_explorer_clipboard,
1718 Some(FileExplorerClipboard { is_cut: true, .. })
1719 ) {
1720 self.file_explorer_clipboard = None;
1721 self.set_status_message(t!("explorer.cut_cancelled").to_string());
1722 return;
1723 }
1724 let action = self.file_explorer.as_mut().map(|explorer| {
1725 if explorer.has_multi_selection() {
1726 explorer.clear_multi_selection();
1727 None
1728 } else if explorer.is_search_active() {
1729 explorer.search_clear();
1730 None
1731 } else {
1732 Some(())
1733 }
1734 });
1735 if let Some(Some(())) = action {
1736 self.focus_editor();
1737 }
1738 }
1739
1740 pub fn handle_set_file_explorer_decorations(
1745 &mut self,
1746 namespace: String,
1747 decorations: Vec<crate::view::file_tree::FileExplorerDecoration>,
1748 ) {
1749 let root = self.root.clone();
1750 let normalized: Vec<crate::view::file_tree::FileExplorerDecoration> = decorations
1751 .into_iter()
1752 .filter_map(|mut decoration| {
1753 let path = if decoration.path.is_absolute() {
1754 decoration.path
1755 } else {
1756 root.join(&decoration.path)
1757 };
1758 let path = crate::app::normalize_path(&path);
1759 if crate::app::explorer_path_under_root(&path, &root) {
1760 decoration.path = crate::app::normalize_explorer_plugin_path(&path, &root);
1761 Some(decoration)
1762 } else {
1763 None
1764 }
1765 })
1766 .collect();
1767
1768 self.file_explorer_decorations.insert(namespace, normalized);
1769 self.rebuild_file_explorer_decoration_cache();
1770 }
1771
1772 pub fn handle_clear_file_explorer_decorations(&mut self, namespace: &str) {
1775 self.file_explorer_decorations.remove(namespace);
1776 self.rebuild_file_explorer_decoration_cache();
1777 }
1778
1779 pub fn handle_set_file_explorer_slots(
1783 &mut self,
1784 namespace: String,
1785 slots: Vec<fresh_core::file_explorer::FileExplorerSlotEntry>,
1786 ) {
1787 let root = self.root.clone();
1788 let normalized: Vec<fresh_core::file_explorer::FileExplorerSlotEntry> = slots
1789 .into_iter()
1790 .filter_map(|mut slot| {
1791 let path = if slot.path.is_absolute() {
1792 slot.path
1793 } else {
1794 root.join(&slot.path)
1795 };
1796 let path = crate::app::normalize_path(&path);
1797 if crate::app::explorer_path_under_root(&path, &root) {
1798 slot.path = crate::app::normalize_explorer_plugin_path(&path, &root);
1799 Some(slot)
1800 } else {
1801 None
1802 }
1803 })
1804 .collect();
1805
1806 self.file_explorer_slot_overrides
1807 .insert(namespace, normalized);
1808 self.rebuild_file_explorer_slot_override_cache();
1809 }
1810
1811 pub fn handle_clear_file_explorer_slots(&mut self, namespace: &str) {
1814 self.file_explorer_slot_overrides.remove(namespace);
1815 self.rebuild_file_explorer_slot_override_cache();
1816 }
1817
1818 pub fn rebuild_file_explorer_decoration_cache(&mut self) {
1822 let mut namespaces: Vec<_> = self.file_explorer_decorations.keys().cloned().collect();
1823 namespaces.sort();
1824 let decorations: Vec<_> = namespaces
1825 .into_iter()
1826 .flat_map(|namespace| {
1827 self.file_explorer_decorations
1828 .get(&namespace)
1829 .into_iter()
1830 .flat_map(|entries| entries.iter().cloned())
1831 })
1832 .collect();
1833
1834 let symlink_mappings = self
1835 .file_explorer
1836 .as_ref()
1837 .map(|fe| fe.collect_symlink_mappings())
1838 .unwrap_or_default();
1839
1840 self.file_explorer_decoration_cache =
1841 crate::view::file_tree::FileExplorerDecorationCache::rebuild(
1842 decorations.into_iter(),
1843 &self.root,
1844 &symlink_mappings,
1845 );
1846 }
1847
1848 pub fn rebuild_file_explorer_slot_override_cache(&mut self) {
1851 let mut namespaces: Vec<_> = self.file_explorer_slot_overrides.keys().cloned().collect();
1852 namespaces.sort();
1853 let slots: Vec<_> = namespaces
1854 .into_iter()
1855 .flat_map(|namespace| {
1856 self.file_explorer_slot_overrides
1857 .get(&namespace)
1858 .into_iter()
1859 .flat_map(|entries| entries.iter().cloned())
1860 })
1861 .collect();
1862
1863 let symlink_mappings = self
1864 .file_explorer
1865 .as_ref()
1866 .map(|fe| fe.collect_symlink_mappings())
1867 .unwrap_or_default();
1868
1869 self.file_explorer_slot_override_cache =
1870 crate::view::file_tree::FileExplorerSlotOverrideCache::rebuild(
1871 slots.into_iter(),
1872 &self.root,
1873 &symlink_mappings,
1874 );
1875 }
1876
1877 pub fn file_explorer_clipboard(&self) -> Option<&FileExplorerClipboard> {
1879 self.file_explorer_clipboard.as_ref()
1880 }
1881
1882 pub fn file_explorer_copy(&mut self) {
1884 self.set_explorer_clipboard(false);
1885 }
1886
1887 pub fn file_explorer_cut(&mut self) {
1889 self.set_explorer_clipboard(true);
1890 }
1891
1892 fn set_explorer_clipboard(&mut self, is_cut: bool) {
1897 let Some(explorer) = self.file_explorer.as_ref() else {
1898 return;
1899 };
1900 let root_id = explorer.tree().root_id();
1901 let selected_ids = explorer.effective_selection();
1902 let paths: Vec<PathBuf> = selected_ids
1903 .iter()
1904 .filter(|&&id| id != root_id)
1905 .filter_map(|&id| explorer.tree().get_node(id).map(|n| n.entry.path.clone()))
1906 .collect();
1907 if paths.is_empty() {
1908 let msg = if is_cut {
1909 t!("explorer.cannot_cut_root").to_string()
1910 } else {
1911 t!("explorer.cannot_copy_root").to_string()
1912 };
1913 self.set_status_message(msg);
1914 return;
1915 }
1916 let msg = if paths.len() == 1 {
1917 let name = paths[0]
1918 .file_name()
1919 .unwrap_or_default()
1920 .to_string_lossy()
1921 .to_string();
1922 if is_cut {
1923 t!("explorer.cut", name = &name).to_string()
1924 } else {
1925 t!("explorer.copied", name = &name).to_string()
1926 }
1927 } else {
1928 let count = paths.len();
1929 if is_cut {
1930 t!("explorer.cut_n", count = count).to_string()
1931 } else {
1932 t!("explorer.copied_n", count = count).to_string()
1933 }
1934 };
1935 self.file_explorer_clipboard = Some(FileExplorerClipboard { paths, is_cut });
1936 self.set_status_message(msg);
1937 }
1938
1939 pub fn sync_file_explorer_to_active_file(&mut self) {
1944 if !self.file_explorer_visible {
1945 return;
1946 }
1947
1948 if self.file_explorer_sync_in_progress {
1950 return;
1951 }
1952
1953 let active_buf = self.active_buffer();
1954 let Some(metadata) = self.buffer_metadata.get(&active_buf) else {
1955 return;
1956 };
1957 let Some(file_path) = metadata.file_path() else {
1958 return;
1959 };
1960 let target_path = file_path.clone();
1961
1962 if !target_path.starts_with(&self.root) {
1963 return;
1964 }
1965
1966 let Some(mut view) = self.file_explorer.take() else {
1967 return;
1968 };
1969 tracing::trace!(
1970 "sync_file_explorer_to_active_file: taking file_explorer for async expand to {:?}",
1971 target_path
1972 );
1973 let runtime_handle = self
1974 .resources
1975 .tokio_runtime
1976 .as_ref()
1977 .map(|r| r.handle().clone());
1978 let sender = self.resources.async_bridge.as_ref().map(|b| b.sender());
1979 let window_id = self.id;
1980 if let (Some(runtime), Some(sender)) = (runtime_handle, sender) {
1981 self.file_explorer_sync_in_progress = true;
1983
1984 runtime.spawn(async move {
1985 let _success = view.expand_and_select_file(&target_path).await;
1986 #[allow(clippy::let_underscore_must_use)]
1988 let _ = sender.send(
1989 crate::services::async_bridge::AsyncMessage::FileExplorerExpandedToPath {
1990 window: window_id,
1991 view,
1992 },
1993 );
1994 });
1995 } else {
1996 self.file_explorer = Some(view);
1997 }
1998 }
1999}
2000
2001fn unique_paste_name(
2004 fs: &dyn crate::model::filesystem::FileSystem,
2005 dst_dir: &Path,
2006 name: &str,
2007) -> PathBuf {
2008 let (stem, ext) = split_stem_ext(name);
2009 let mut n = 1u32;
2010 loop {
2011 let candidate = if n == 1 {
2012 if ext.is_empty() {
2013 format!("{} copy", stem)
2014 } else {
2015 format!("{} copy.{}", stem, ext)
2016 }
2017 } else if ext.is_empty() {
2018 format!("{} copy {}", stem, n)
2019 } else {
2020 format!("{} copy {}.{}", stem, n, ext)
2021 };
2022 let path = dst_dir.join(&candidate);
2023 if !fs.exists(&path) {
2024 return path;
2025 }
2026 n += 1;
2027 if n > 1000 {
2028 return dst_dir.join(format!("{} copy {}", stem, timestamp_suffix()));
2030 }
2031 }
2032}
2033
2034pub(super) fn truncate_name_for_prompt(name: &str, max: usize) -> String {
2036 if name.chars().count() <= max {
2037 name.to_string()
2038 } else {
2039 let truncated: String = name.chars().take(max.saturating_sub(1)).collect();
2040 format!("{}\u{2026}", truncated)
2041 }
2042}
2043
2044pub(super) fn format_path_preview_for_prompt(paths: &[PathBuf], max_shown: usize) -> String {
2049 let names: Vec<String> = paths
2050 .iter()
2051 .map(|p| {
2052 let raw = p
2053 .file_name()
2054 .map(|n| n.to_string_lossy().to_string())
2055 .unwrap_or_default();
2056 format!("'{}'", truncate_name_for_prompt(&raw, 24))
2057 })
2058 .collect();
2059 if names.len() <= max_shown {
2060 names.join(", ")
2061 } else {
2062 let shown = names[..max_shown].join(", ");
2063 let more = names.len() - max_shown;
2064 format!("{}, \u{2026} ({} more)", shown, more)
2065 }
2066}
2067
2068fn split_stem_ext(name: &str) -> (&str, &str) {
2069 if let Some(dot_pos) = name.rfind('.') {
2071 if dot_pos > 0 {
2072 return (&name[..dot_pos], &name[dot_pos + 1..]);
2073 }
2074 }
2075 (name, "")
2076}