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