1use anyhow::Result as AnyhowResult;
2use rust_i18n::t;
3
4use super::*;
5use crate::view::file_tree::TreeNode;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone)]
9pub struct FileExplorerClipboard {
10 pub paths: Vec<PathBuf>,
11 pub is_cut: bool,
12}
13
14#[derive(Debug, Clone, Copy)]
18pub(crate) struct FileExplorerViewDefaults {
19 pub show_hidden: bool,
20 pub show_gitignored: bool,
21 pub compact_directories: bool,
22}
23
24#[derive(Debug)]
30enum PasteOpOutcome {
31 Ok,
33 SourceRemovalFailed { dst: PathBuf, err: std::io::Error },
36 Failed(std::io::Error),
39}
40
41fn get_parent_dir_path(node: &TreeNode) -> PathBuf {
44 if node.is_dir() {
45 node.entry.path.clone()
46 } else {
47 node.entry
48 .path
49 .parent()
50 .map(|p| p.to_path_buf())
51 .unwrap_or_else(|| node.entry.path.clone())
52 }
53}
54
55fn timestamp_suffix() -> u64 {
57 std::time::SystemTime::now()
58 .duration_since(std::time::UNIX_EPOCH)
59 .unwrap()
60 .as_secs()
61}
62
63fn get_parent_node_id(
66 tree: &crate::view::file_tree::FileTree,
67 selected_id: crate::view::file_tree::NodeId,
68 node_is_dir: bool,
69) -> crate::view::file_tree::NodeId {
70 if node_is_dir {
71 selected_id
72 } else {
73 tree.get_node(selected_id)
74 .and_then(|n| n.parent)
75 .unwrap_or(selected_id)
76 }
77}
78
79impl Editor {
80 pub fn file_explorer_visible(&self) -> bool {
81 self.active_window().file_explorer_visible
82 }
83
84 pub(super) fn take_focus_for_file_explorer(&mut self) {
97 let win = self.active_window_mut();
98 if win.terminal_mode {
99 let active = win.active_buffer();
100 if win.is_terminal_buffer(active) {
101 win.terminal_mode_resume.insert(active);
102 }
103 win.terminal_mode = false;
104 }
105 win.key_context = KeyContext::FileExplorer;
106 }
107
108 pub fn toggle_file_explorer(&mut self) {
109 let new_visible = !self.active_window().file_explorer_visible;
110 self.active_window_mut().file_explorer_visible = new_visible;
111
112 if new_visible {
113 if self.file_explorer().is_none() {
114 self.init_file_explorer();
115 }
116 self.take_focus_for_file_explorer();
117 self.set_status_message(t!("explorer.opened").to_string());
118 self.active_window_mut().sync_file_explorer_to_active_file();
119 } else {
120 self.active_window_mut().key_context = KeyContext::Normal;
121 self.set_status_message(t!("explorer.closed").to_string());
122 }
123
124 self.relayout();
128 }
129
130 pub fn show_file_explorer(&mut self) {
131 if !self.file_explorer_visible() {
132 self.toggle_file_explorer();
133 }
134 }
135
136 pub fn focus_file_explorer(&mut self) {
137 if self.file_explorer_visible() {
138 self.active_window_mut().on_editor_focus_lost();
140
141 self.active_window_mut().cancel_search_prompt_if_active();
143
144 self.take_focus_for_file_explorer();
145 self.set_status_message(t!("explorer.focused").to_string());
146 self.active_window_mut().sync_file_explorer_to_active_file();
147 } else {
148 self.toggle_file_explorer();
149 }
150 }
151
152 pub(crate) fn init_file_explorer(&mut self) {
159 self.active_window_mut().init_file_explorer();
160 }
161
162 pub fn file_explorer_navigate_up(&mut self) {
163 if let Some(explorer) = self.file_explorer_mut() {
164 explorer.select_prev_match();
165 explorer.update_scroll_for_selection();
166 }
167 self.file_explorer_preview_selected();
168 }
169
170 pub fn file_explorer_navigate_down(&mut self) {
171 if let Some(explorer) = self.file_explorer_mut() {
172 explorer.select_next_match();
173 explorer.update_scroll_for_selection();
174 }
175 self.file_explorer_preview_selected();
176 }
177
178 pub fn file_explorer_page_up(&mut self) {
179 if let Some(explorer) = self.file_explorer_mut() {
180 explorer.select_page_up();
181 explorer.update_scroll_for_selection();
182 }
183 self.file_explorer_preview_selected();
184 }
185
186 pub fn file_explorer_page_down(&mut self) {
187 if let Some(explorer) = self.file_explorer_mut() {
188 explorer.select_page_down();
189 explorer.update_scroll_for_selection();
190 }
191 self.file_explorer_preview_selected();
192 }
193
194 fn file_explorer_preview_selected(&mut self) {
202 if !self.config.file_explorer.preview_tabs {
205 return;
206 }
207
208 let path = match self
209 .file_explorer()
210 .as_ref()
211 .and_then(|explorer| explorer.get_selected_entry())
212 {
213 Some(entry) if !entry.is_dir() => entry.path.clone(),
214 _ => return,
215 };
216
217 if let Err(e) = self.open_file_preview(&path) {
218 tracing::debug!(
219 "file_explorer_preview_selected: skipping preview for {:?}: {}",
220 path,
221 e
222 );
223 }
224 }
225
226 pub fn file_explorer_collapse(&mut self) {
230 let Some(explorer) = self.file_explorer() else {
231 return;
232 };
233
234 let Some(selected_id) = explorer.get_selected() else {
235 return;
236 };
237
238 let Some(node) = explorer.tree().get_node(selected_id) else {
239 return;
240 };
241
242 if node.is_dir() && node.is_expanded() {
244 self.file_explorer_toggle_expand();
245 return;
246 }
247
248 if let Some(explorer) = self.file_explorer_mut() {
250 explorer.select_parent();
251 explorer.update_scroll_for_selection();
252 }
253 }
254
255 pub fn file_explorer_toggle_expand(&mut self) {
256 let selected_id = if let Some(explorer) = self.file_explorer() {
257 explorer.get_selected()
258 } else {
259 return;
260 };
261
262 let Some(selected_id) = selected_id else {
263 return;
264 };
265
266 let (is_dir, is_expanded, name) = if let Some(explorer) = self.file_explorer() {
267 let node = explorer.tree().get_node(selected_id);
268 if let Some(node) = node {
269 (node.is_dir(), node.is_expanded(), node.entry.name.clone())
270 } else {
271 return;
272 }
273 } else {
274 return;
275 };
276
277 if !is_dir {
278 return;
279 }
280
281 let status_msg = if is_expanded {
282 t!("explorer.collapsing").to_string()
283 } else {
284 t!("explorer.loading_dir", name = &name).to_string()
285 };
286 self.set_status_message(status_msg);
287
288 let active_id = self.active_window;
289 if let (Some(runtime), Some(explorer)) = (
294 self.tokio_runtime.as_ref(),
295 self.windows
296 .get_mut(&active_id)
297 .and_then(|w| w.file_explorer.as_mut()),
298 ) {
299 let result = runtime.block_on(explorer.toggle_with_chain(selected_id));
300
301 let final_name = explorer
302 .tree()
303 .get_node(selected_id)
304 .map(|n| n.entry.name.clone());
305 let final_expanded = explorer
306 .tree()
307 .get_node(selected_id)
308 .map(|n| n.is_expanded())
309 .unwrap_or(false);
310
311 let mut needs_decoration_rebuild = false;
313
314 match result {
315 Ok(()) => {
316 if final_expanded {
317 let node_info = explorer
318 .tree()
319 .get_node(selected_id)
320 .map(|n| (n.entry.path.clone(), n.entry.is_symlink()));
321
322 if let Some((dir_path, is_symlink)) = node_info {
323 crate::app::file_operations::load_gitignore_via_fs(
324 self.authority.filesystem.as_ref(),
325 explorer,
326 &dir_path,
327 );
328
329 if is_symlink {
333 tracing::debug!(
334 "Symlink directory expanded, will rebuild decoration cache: {:?}",
335 dir_path
336 );
337 needs_decoration_rebuild = true;
338 }
339 }
340 }
341
342 if let Some(name) = final_name {
343 let msg = if final_expanded {
344 t!("explorer.expanded", name = &name).to_string()
345 } else {
346 t!("explorer.collapsed", name = &name).to_string()
347 };
348 self.set_status_message(msg);
349 }
350 }
351 Err(e) => {
352 self.set_status_message(
353 t!("explorer.error", error = e.to_string()).to_string(),
354 );
355 }
356 }
357
358 if needs_decoration_rebuild {
360 self.active_window_mut()
361 .rebuild_file_explorer_decoration_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 if let Some(explorer) = self
460 .windows
461 .get_mut(&active_id)
462 .and_then(|w| w.file_explorer.as_mut())
463 {
464 if let Some(selected_id) = explorer.get_selected() {
465 let node = explorer.tree().get_node(selected_id);
466 if let Some(node) = node {
467 let parent_path = get_parent_dir_path(node);
468 let filename = format!("untitled_{}.txt", timestamp_suffix());
469 let file_path = parent_path.join(&filename);
470
471 if let Some(runtime) = &self.tokio_runtime {
472 let path_clone = file_path.clone();
473 let result = self
474 .authority
475 .filesystem
476 .create_file(&path_clone)
477 .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 if let Some(explorer) = self
528 .windows
529 .get_mut(&active_id)
530 .and_then(|w| w.file_explorer.as_mut())
531 {
532 if let Some(selected_id) = explorer.get_selected() {
533 let node = explorer.tree().get_node(selected_id);
534 if let Some(node) = node {
535 let parent_path = get_parent_dir_path(node);
536 let dirname = format!("New Folder {}", timestamp_suffix());
537 let dir_path = parent_path.join(&dirname);
538
539 if let Some(runtime) = &self.tokio_runtime {
540 let path_clone = dir_path.clone();
541 let dirname_clone = dirname.clone();
542 let result = self.authority.filesystem.create_dir(&path_clone);
543
544 match result {
545 Ok(_) => {
546 let parent_id =
547 get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
548 let tree = explorer.tree_mut();
549 if let Err(e) =
550 runtime.block_on(tree.reload_expanded_node(parent_id))
551 {
552 tracing::warn!("Failed to refresh file tree: {}", e);
553 }
554 if let Some(explorer) = self.file_explorer_mut().as_mut() {
555 explorer.navigate_to_path(&path_clone);
556 }
557 self.set_status_message(
558 t!("explorer.created_dir", name = &dirname_clone).to_string(),
559 );
560 self.notify_file_explorer_change(&path_clone);
561
562 let prompt = crate::view::prompt::Prompt::with_initial_text(
563 t!("explorer.new_directory_prompt").to_string(),
564 crate::view::prompt::PromptType::FileExplorerRename {
565 original_path: path_clone,
566 original_name: dirname_clone,
567 is_new_file: true,
568 },
569 dirname,
570 );
571 self.active_window_mut().prompt = Some(prompt);
572 }
573 Err(e) => {
574 self.set_status_message(
575 t!("explorer.error_creating_dir", error = e.to_string())
576 .to_string(),
577 );
578 }
579 }
580 }
581 }
582 }
583 }
584 }
585
586 pub fn file_explorer_delete(&mut self) {
587 let Some(explorer) = self.file_explorer() else {
588 return;
589 };
590 let root_id = explorer.tree().root_id();
591 let selected_ids = explorer.effective_selection();
592
593 let paths: Vec<(PathBuf, bool)> = selected_ids
594 .iter()
595 .filter(|&&id| id != root_id)
596 .filter_map(|&id| {
597 explorer
598 .tree()
599 .get_node(id)
600 .map(|n| (n.entry.path.clone(), n.is_dir()))
601 })
602 .collect();
603
604 if paths.is_empty() {
605 self.set_status_message(t!("explorer.cannot_delete_root").to_string());
606 return;
607 }
608
609 if paths.len() == 1 {
610 let (path, is_dir) = paths.into_iter().next().unwrap();
611 let name = path
612 .file_name()
613 .unwrap_or_default()
614 .to_string_lossy()
615 .to_string();
616 let type_str = if is_dir { "directory" } else { "file" };
617 self.start_prompt(
618 t!("explorer.delete_confirm", "type" = type_str, name = &name).to_string(),
619 PromptType::ConfirmDeleteFile { path, is_dir },
620 );
621 } else {
622 let count = paths.len();
623 let all_paths: Vec<PathBuf> = paths.into_iter().map(|(p, _)| p).collect();
624 let names = format_path_preview_for_prompt(&all_paths, 3);
628 self.start_prompt(
629 t!(
630 "explorer.delete_multi_confirm",
631 count = count,
632 names = &names
633 )
634 .to_string(),
635 PromptType::ConfirmMultiDelete { paths: all_paths },
636 );
637 }
638 }
639
640 pub fn perform_file_explorer_delete(&mut self, path: std::path::PathBuf, _is_dir: bool) {
644 let name = path
645 .file_name()
646 .map(|n| n.to_string_lossy().to_string())
647 .unwrap_or_default();
648
649 let delete_result = if self.authority.filesystem.remote_connection_info().is_some() {
652 self.move_to_remote_trash(&path)
653 } else {
654 trash::delete(&path).map_err(std::io::Error::other)
655 };
656
657 match delete_result {
658 Ok(_) => {
659 let to_close = self.buffer_ids_under_path(&path);
669 for id in to_close {
670 if let Err(e) = self.force_close_buffer(id) {
671 tracing::warn!(
672 "Failed to close buffer {:?} after delete of {:?}: {}",
673 id,
674 path,
675 e
676 );
677 }
678 }
679
680 let active_id = self.active_window;
682 if let Some(explorer) = self
683 .windows
684 .get_mut(&active_id)
685 .and_then(|w| w.file_explorer.as_mut())
686 {
687 if let Some(runtime) = &self.tokio_runtime {
688 if let Some(node) = explorer.tree().get_node_by_path(&path) {
690 let node_id = node.id;
691 let parent_id = get_parent_node_id(explorer.tree(), node_id, false);
692
693 let deleted_index = explorer.get_selected_index();
695
696 if let Err(e) = runtime
697 .block_on(explorer.tree_mut().reload_expanded_node(parent_id))
698 {
699 tracing::warn!("Failed to refresh file tree after delete: {}", e);
700 }
701
702 explorer.clear_multi_selection();
707
708 let count = explorer.visible_count();
711 if count > 0 {
712 let new_index = if let Some(idx) = deleted_index {
713 idx.min(count.saturating_sub(1))
714 } else {
715 0
716 };
717 if let Some(node_id) = explorer.get_node_at_index(new_index) {
718 explorer.set_selected(Some(node_id));
719 }
720 } else {
721 explorer.set_selected(Some(parent_id));
723 }
724 }
725 }
726 }
727 self.set_status_message(t!("explorer.moved_to_trash", name = &name).to_string());
728 self.notify_file_explorer_change(&path);
729
730 self.active_window_mut().key_context = KeyContext::FileExplorer;
732 }
733 Err(e) => {
734 self.set_status_message(
735 t!("explorer.error_trash", error = e.to_string()).to_string(),
736 );
737 }
738 }
739 }
740
741 fn move_to_remote_trash(&self, path: &std::path::Path) -> std::io::Result<()> {
743 let home = self.authority.filesystem.home_dir()?;
745 let trash_dir = home.join(".local/share/fresh/trash");
746
747 if !self.authority.filesystem.exists(&trash_dir) {
749 self.authority.filesystem.create_dir_all(&trash_dir)?;
750 }
751
752 let file_name = path
754 .file_name()
755 .unwrap_or_else(|| std::ffi::OsStr::new("unnamed"));
756 let timestamp = std::time::SystemTime::now()
757 .duration_since(std::time::UNIX_EPOCH)
758 .map(|d| d.as_secs())
759 .unwrap_or(0);
760 let trash_name = format!("{}.{}", file_name.to_string_lossy(), timestamp);
761 let trash_path = trash_dir.join(trash_name);
762
763 self.authority.filesystem.rename(path, &trash_path)
765 }
766
767 pub fn file_explorer_rename(&mut self) {
768 if let Some(explorer) = self.file_explorer() {
769 if let Some(selected_id) = explorer.get_selected() {
770 if selected_id == explorer.tree().root_id() {
772 self.set_status_message(t!("explorer.cannot_rename_root").to_string());
773 return;
774 }
775
776 let node = explorer.tree().get_node(selected_id);
777 if let Some(node) = node {
778 let old_path = node.entry.path.clone();
779 let old_name = node.entry.name.clone();
780
781 let prompt = crate::view::prompt::Prompt::with_initial_text_for_edit(
786 t!("explorer.rename_prompt").to_string(),
787 crate::view::prompt::PromptType::FileExplorerRename {
788 original_path: old_path,
789 original_name: old_name.clone(),
790 is_new_file: false,
791 },
792 old_name,
793 );
794 self.active_window_mut().prompt = Some(prompt);
795 }
796 }
797 }
798 }
799
800 pub fn perform_file_explorer_rename(
802 &mut self,
803 original_path: std::path::PathBuf,
804 original_name: String,
805 new_name: String,
806 is_new_file: bool,
807 ) {
808 if new_name.is_empty() || new_name == original_name {
809 self.set_status_message(t!("explorer.rename_cancelled").to_string());
810 return;
811 }
812
813 if new_name.chars().any(std::path::is_separator) {
818 self.set_status_message(t!("explorer.rename_invalid_separator").to_string());
819 return;
820 }
821 if new_name == "." || new_name == ".." {
822 self.set_status_message(t!("explorer.rename_invalid_dot").to_string());
823 return;
824 }
825
826 let new_path = original_path
827 .parent()
828 .map(|p| p.join(&new_name))
829 .unwrap_or_else(|| original_path.clone());
830
831 if self.tokio_runtime.is_some() {
832 let result = self.authority.filesystem.rename(&original_path, &new_path);
833
834 match result {
835 Ok(_) => {
836 let active_id = self.active_window;
840 if let (Some(runtime), Some(explorer)) = (
841 self.tokio_runtime.as_ref(),
842 self.windows
843 .get_mut(&active_id)
844 .and_then(|w| w.file_explorer.as_mut()),
845 ) {
846 if let Some(selected_id) = explorer.get_selected() {
847 let parent_id = get_parent_node_id(explorer.tree(), selected_id, false);
848 let tree = explorer.tree_mut();
849 if let Err(e) = runtime.block_on(tree.reload_expanded_node(parent_id)) {
850 tracing::warn!("Failed to refresh file tree after rename: {}", e);
851 }
852 }
853 explorer.clear_multi_selection();
857 explorer.navigate_to_path(&new_path);
859 }
860
861 let relocated = self.relocate_buffers_for_rename(&original_path, &new_path);
869
870 if is_new_file && !relocated.is_empty() {
874 self.active_window_mut().key_context = KeyContext::Normal;
875 }
876
877 self.set_status_message(
878 t!("explorer.renamed", old = &original_name, new = &new_name).to_string(),
879 );
880 self.notify_file_explorer_change(&new_path);
881 }
882 Err(e) => {
883 self.set_status_message(
884 t!("explorer.error_renaming", error = e.to_string()).to_string(),
885 );
886 }
887 }
888 }
889 }
890
891 pub fn file_explorer_toggle_hidden(&mut self) {
892 let show_hidden = if let Some(explorer) = self.file_explorer_mut() {
893 explorer.toggle_show_hidden();
894 explorer.ignore_patterns().show_hidden()
895 } else {
896 return;
897 };
898
899 let msg = if show_hidden {
900 t!("explorer.showing_hidden")
901 } else {
902 t!("explorer.hiding_hidden")
903 };
904 self.set_status_message(msg.to_string());
905
906 self.config_mut().file_explorer.show_hidden = show_hidden;
908 self.persist_config_change(
909 "/file_explorer/show_hidden",
910 serde_json::Value::Bool(show_hidden),
911 );
912 }
913
914 pub fn file_explorer_toggle_gitignored(&mut self) {
915 let show_gitignored = if let Some(explorer) = self.file_explorer_mut() {
916 explorer.toggle_show_gitignored();
917 explorer.ignore_patterns().show_gitignored()
918 } else {
919 return;
920 };
921
922 let msg = if show_gitignored {
923 t!("explorer.showing_gitignored")
924 } else {
925 t!("explorer.hiding_gitignored")
926 };
927 self.set_status_message(msg.to_string());
928
929 self.config_mut().file_explorer.show_gitignored = show_gitignored;
931 self.persist_config_change(
932 "/file_explorer/show_gitignored",
933 serde_json::Value::Bool(show_gitignored),
934 );
935 }
936
937 pub fn file_explorer_paste(&mut self) {
957 let clipboard = match self.active_window().file_explorer_clipboard.clone() {
958 Some(c) => c,
959 None => {
960 self.set_status_message(t!("explorer.paste_no_source").to_string());
961 return;
962 }
963 };
964
965 let dst_dir = if let Some(explorer) = self.file_explorer() {
966 if let Some(selected_id) = explorer.get_selected() {
967 if let Some(node) = explorer.tree().get_node(selected_id) {
968 get_parent_dir_path(node)
969 } else {
970 return;
971 }
972 } else {
973 return;
974 }
975 } else {
976 return;
977 };
978
979 let is_cut = clipboard.is_cut;
980
981 if clipboard.paths.len() == 1 {
982 let src = clipboard.paths[0].clone();
983 let file_name = match src.file_name() {
984 Some(n) => n.to_os_string(),
985 None => return,
986 };
987 let dst_path = dst_dir.join(&file_name);
988
989 if src.parent().map(|p| p == dst_dir).unwrap_or(false) {
990 if is_cut {
991 self.active_window_mut().file_explorer_clipboard = None;
996 self.set_status_message(t!("explorer.cut_cancelled").to_string());
997 return;
998 } else {
999 let unique = unique_paste_name(
1000 &*self.authority.filesystem,
1001 &dst_dir,
1002 &file_name.to_string_lossy(),
1003 );
1004 self.perform_file_explorer_paste(src, unique, false);
1005 return;
1006 }
1007 }
1008
1009 if self.authority.filesystem.exists(&dst_path) {
1010 let name = truncate_name_for_prompt(&file_name.to_string_lossy(), 40);
1011 self.start_prompt(
1012 t!("explorer.paste_conflict", name = &name).to_string(),
1013 crate::view::prompt::PromptType::ConfirmPasteConflict {
1014 src,
1015 dst: dst_path,
1016 is_cut,
1017 },
1018 );
1019 } else {
1020 self.perform_file_explorer_paste(src, dst_path, is_cut);
1021 }
1022 } else {
1023 let mut safe: Vec<(PathBuf, PathBuf)> = Vec::new();
1025 let mut conflicts: Vec<(PathBuf, PathBuf)> = Vec::new();
1026
1027 for src in &clipboard.paths {
1028 let file_name = match src.file_name() {
1029 Some(n) => n.to_os_string(),
1030 None => continue,
1031 };
1032 let dst_path = dst_dir.join(&file_name);
1033 let is_same_location = src.parent().map(|p| p == dst_dir).unwrap_or(false);
1034
1035 if is_same_location {
1036 if !is_cut {
1037 let unique = unique_paste_name(
1039 &*self.authority.filesystem,
1040 &dst_dir,
1041 &file_name.to_string_lossy(),
1042 );
1043 safe.push((src.clone(), unique));
1044 }
1045 } else if self.authority.filesystem.exists(&dst_path) {
1047 conflicts.push((src.clone(), dst_path));
1048 } else {
1049 safe.push((src.clone(), dst_path));
1050 }
1051 }
1052
1053 if safe.is_empty() && conflicts.is_empty() {
1054 if is_cut {
1058 self.active_window_mut().file_explorer_clipboard = None;
1059 self.set_status_message(t!("explorer.cut_cancelled").to_string());
1060 } else {
1061 self.set_status_message(t!("explorer.paste_same_location").to_string());
1062 }
1063 return;
1064 }
1065
1066 if conflicts.is_empty() {
1067 self.execute_resolved_multi_paste(safe, vec![], is_cut);
1068 } else {
1069 let name = truncate_name_for_prompt(
1070 &conflicts[0]
1071 .1
1072 .file_name()
1073 .unwrap_or_default()
1074 .to_string_lossy(),
1075 40,
1076 );
1077 self.start_prompt(
1078 t!("explorer.paste_conflict_multi", name = &name).to_string(),
1079 crate::view::prompt::PromptType::ConfirmMultiPasteConflict {
1080 safe,
1081 confirmed: Vec::new(),
1082 pending: conflicts,
1083 is_cut,
1084 },
1085 );
1086 }
1087 }
1088 }
1089
1090 pub(super) fn execute_resolved_multi_paste(
1098 &mut self,
1099 safe: Vec<(PathBuf, PathBuf)>,
1100 to_overwrite: Vec<(PathBuf, PathBuf)>,
1101 is_cut: bool,
1102 ) {
1103 let total = safe.len() + to_overwrite.len();
1104 if total == 0 {
1105 return;
1106 }
1107
1108 let mut succeeded: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(total);
1109 let mut clean_moves: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(total);
1116 let mut first_error: Option<std::io::Error> = None;
1117 let mut partial_moves: Vec<(PathBuf, std::io::Error)> = Vec::new();
1118 for (src, dst) in safe.into_iter().chain(to_overwrite) {
1119 match self.paste_one_fs_op(&src, &dst, is_cut) {
1120 PasteOpOutcome::Ok => {
1121 clean_moves.push((src.clone(), dst.clone()));
1122 succeeded.push((src, dst));
1123 }
1124 PasteOpOutcome::SourceRemovalFailed {
1125 dst: landed_dst,
1126 err,
1127 } => {
1128 succeeded.push((src, landed_dst.clone()));
1132 partial_moves.push((landed_dst, err));
1133 }
1134 PasteOpOutcome::Failed(e) => {
1135 if first_error.is_none() {
1136 first_error = Some(e);
1137 }
1138 }
1139 }
1140 }
1141
1142 if is_cut {
1148 for (src, dst) in &clean_moves {
1149 self.relocate_buffers_for_rename(src, dst);
1150 }
1151 }
1152
1153 if !succeeded.is_empty() {
1154 let first_dst = succeeded[0].1.clone();
1155 let any_src = succeeded[0].0.clone();
1156 self.refresh_tree_after_paste(&any_src, &first_dst, is_cut);
1157 }
1158
1159 if !partial_moves.is_empty() {
1160 let (first_dst, first_err) = &partial_moves[0];
1163 let name = first_dst
1164 .file_name()
1165 .map(|n| n.to_string_lossy().to_string())
1166 .unwrap_or_default();
1167 let msg = if partial_moves.len() == 1 {
1168 t!(
1169 "explorer.move_source_removal_failed",
1170 name = &name,
1171 error = first_err.to_string()
1172 )
1173 .to_string()
1174 } else {
1175 t!(
1176 "explorer.move_source_removal_failed_n",
1177 count = partial_moves.len()
1178 )
1179 .to_string()
1180 };
1181 self.set_status_message(msg);
1182 } else if let Some(e) = &first_error {
1183 let msg = if is_cut {
1184 t!("explorer.error_moving", error = e.to_string()).to_string()
1185 } else {
1186 t!("explorer.error_copying", error = e.to_string()).to_string()
1187 };
1188 self.set_status_message(msg);
1189 } else if total > 1 {
1190 let msg = if is_cut {
1191 t!("explorer.pasted_moved_n", count = total).to_string()
1192 } else {
1193 t!("explorer.pasted_n", count = total).to_string()
1194 };
1195 self.set_status_message(msg);
1196 } else if let Some((_, dst)) = succeeded.first() {
1197 let name = dst
1198 .file_name()
1199 .map(|n| n.to_string_lossy().to_string())
1200 .unwrap_or_default();
1201 let msg = if is_cut {
1202 t!("explorer.pasted_moved", name = &name).to_string()
1203 } else {
1204 t!("explorer.pasted", name = &name).to_string()
1205 };
1206 self.set_status_message(msg);
1207 }
1208
1209 if is_cut && first_error.is_none() && partial_moves.is_empty() {
1213 self.active_window_mut().file_explorer_clipboard = None;
1214 }
1215 self.active_window_mut().key_context = KeyContext::FileExplorer;
1216 }
1217
1218 fn paste_one_fs_op(&self, src: &Path, dst: &Path, is_cut: bool) -> PasteOpOutcome {
1222 let src_is_dir = self.authority.filesystem.is_dir(src).unwrap_or(false);
1223
1224 if src_is_dir && dst.starts_with(src) {
1232 return PasteOpOutcome::Failed(std::io::Error::new(
1233 std::io::ErrorKind::InvalidInput,
1234 "Cannot paste a directory into itself",
1235 ));
1236 }
1237
1238 if is_cut {
1239 match self.authority.filesystem.rename(src, dst) {
1244 Ok(()) => PasteOpOutcome::Ok,
1245 Err(e) if e.kind() == std::io::ErrorKind::CrossesDevices => {
1246 let copy_result = if src_is_dir {
1247 self.authority.filesystem.copy_dir_all(src, dst)
1248 } else {
1249 self.authority.filesystem.copy(src, dst).map(|_| ())
1250 };
1251 match copy_result {
1252 Ok(()) => {
1253 let remove_result = if src_is_dir {
1259 self.authority.filesystem.remove_dir_all(src)
1260 } else {
1261 self.authority.filesystem.remove_file(src)
1262 };
1263 match remove_result {
1264 Ok(()) => PasteOpOutcome::Ok,
1265 Err(remove_err) => PasteOpOutcome::SourceRemovalFailed {
1266 dst: dst.to_path_buf(),
1267 err: remove_err,
1268 },
1269 }
1270 }
1271 Err(copy_err) => {
1272 let cleanup = if src_is_dir {
1278 self.authority.filesystem.remove_dir_all(dst)
1279 } else {
1280 self.authority.filesystem.remove_file(dst)
1281 };
1282 if let Err(cleanup_err) = cleanup {
1283 tracing::warn!(
1284 "Failed to roll back partial destination {:?} after copy \
1285 fallback failed: {}",
1286 dst,
1287 cleanup_err
1288 );
1289 }
1290 PasteOpOutcome::Failed(copy_err)
1291 }
1292 }
1293 }
1294 Err(e) => PasteOpOutcome::Failed(e),
1295 }
1296 } else if src_is_dir {
1297 match self.authority.filesystem.copy_dir_all(src, dst) {
1298 Ok(()) => PasteOpOutcome::Ok,
1299 Err(e) => PasteOpOutcome::Failed(e),
1300 }
1301 } else {
1302 match self.authority.filesystem.copy(src, dst) {
1303 Ok(_) => PasteOpOutcome::Ok,
1304 Err(e) => PasteOpOutcome::Failed(e),
1305 }
1306 }
1307 }
1308
1309 fn refresh_tree_after_paste(&mut self, src: &Path, dst: &Path, is_cut: bool) {
1314 let active_id = self.active_window;
1315 let Some(explorer) = self
1318 .windows
1319 .get_mut(&active_id)
1320 .and_then(|w| w.file_explorer.as_mut())
1321 else {
1322 return;
1323 };
1324 if let Some(runtime) = &self.tokio_runtime {
1325 if let Some(dst_parent) = dst.parent() {
1327 if let Some(dst_parent_node) = explorer.tree().get_node_by_path(dst_parent) {
1328 let pid = dst_parent_node.id;
1329 if let Err(e) = runtime.block_on(explorer.tree_mut().reload_expanded_node(pid))
1330 {
1331 tracing::warn!("Failed to reload destination directory after paste: {}", e);
1332 }
1333 }
1334 }
1335 if is_cut {
1345 if let Some(src_parent) = src.parent() {
1346 if let Some(src_parent_node) = explorer.tree().get_node_by_path(src_parent) {
1347 let pid = src_parent_node.id;
1348 if let Err(e) =
1349 runtime.block_on(explorer.tree_mut().reload_expanded_node(pid))
1350 {
1351 tracing::warn!("Failed to refresh source directory after move: {}", e);
1352 }
1353 }
1354 }
1355 }
1356 }
1357 explorer.clear_multi_selection();
1362 explorer.navigate_to_path(dst);
1363
1364 self.notify_file_explorer_change(dst);
1365 }
1366
1367 pub(super) fn notify_file_explorer_change(&self, path: &Path) {
1379 self.plugin_manager.read().unwrap().run_hook(
1380 "after_file_explorer_change",
1381 crate::services::plugins::hooks::HookArgs::AfterFileExplorerChange {
1382 path: path.to_path_buf(),
1383 },
1384 );
1385 }
1386
1387 pub fn perform_file_explorer_paste(&mut self, src: PathBuf, dst: PathBuf, is_cut: bool) {
1388 let name = dst
1389 .file_name()
1390 .map(|n| n.to_string_lossy().to_string())
1391 .unwrap_or_default();
1392
1393 match self.paste_one_fs_op(&src, &dst, is_cut) {
1394 PasteOpOutcome::Ok => {
1395 if is_cut {
1402 self.relocate_buffers_for_rename(&src, &dst);
1403 }
1404 self.refresh_tree_after_paste(&src, &dst, is_cut);
1405 if is_cut {
1406 self.active_window_mut().file_explorer_clipboard = None;
1407 self.set_status_message(t!("explorer.pasted_moved", name = &name).to_string());
1408 } else {
1409 self.set_status_message(t!("explorer.pasted", name = &name).to_string());
1410 }
1411 self.active_window_mut().key_context = KeyContext::FileExplorer;
1412 }
1413 PasteOpOutcome::SourceRemovalFailed {
1414 dst: landed_dst,
1415 err,
1416 } => {
1417 self.refresh_tree_after_paste(&src, &landed_dst, is_cut);
1422 self.set_status_message(
1423 t!(
1424 "explorer.move_source_removal_failed",
1425 name = &name,
1426 error = err.to_string()
1427 )
1428 .to_string(),
1429 );
1430 self.active_window_mut().key_context = KeyContext::FileExplorer;
1433 }
1434 PasteOpOutcome::Failed(e) => {
1435 let msg = if is_cut {
1436 t!("explorer.error_moving", error = e.to_string()).to_string()
1437 } else {
1438 t!("explorer.error_copying", error = e.to_string()).to_string()
1439 };
1440 self.set_status_message(msg);
1441 }
1442 }
1443 }
1444
1445 pub fn file_explorer_duplicate(&mut self) {
1451 let Some(explorer) = self.file_explorer() else {
1452 return;
1453 };
1454 let root_id = explorer.tree().root_id();
1455 let selected_ids = explorer.effective_selection();
1456 let sources: Vec<PathBuf> = selected_ids
1457 .iter()
1458 .filter(|&&id| id != root_id)
1459 .filter_map(|&id| explorer.tree().get_node(id).map(|n| n.entry.path.clone()))
1460 .collect();
1461
1462 if sources.is_empty() {
1463 self.set_status_message(t!("explorer.cannot_duplicate_root").to_string());
1464 return;
1465 }
1466
1467 let mut ops: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(sources.len());
1471 for src in &sources {
1472 let Some(parent) = src.parent() else {
1473 continue;
1474 };
1475 let Some(file_name) = src.file_name() else {
1476 continue;
1477 };
1478 let dst = unique_paste_name(
1479 &*self.authority.filesystem,
1480 parent,
1481 &file_name.to_string_lossy(),
1482 );
1483 ops.push((src.clone(), dst));
1484 }
1485
1486 if ops.is_empty() {
1487 return;
1488 }
1489
1490 let mut succeeded: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(ops.len());
1491 let mut first_error: Option<std::io::Error> = None;
1492 for (src, dst) in ops {
1493 match self.paste_one_fs_op(&src, &dst, false) {
1494 PasteOpOutcome::Ok => succeeded.push((src, dst)),
1495 PasteOpOutcome::SourceRemovalFailed { .. } => {
1496 unreachable!("paste_one_fs_op returned SourceRemovalFailed for a non-cut op");
1498 }
1499 PasteOpOutcome::Failed(e) => {
1500 if first_error.is_none() {
1501 first_error = Some(e);
1502 }
1503 }
1504 }
1505 }
1506
1507 if !succeeded.is_empty() {
1508 let (first_src, first_dst) = succeeded[0].clone();
1509 self.refresh_tree_after_paste(&first_src, &first_dst, false);
1510 }
1511
1512 let msg = if let Some(e) = &first_error {
1513 t!("explorer.error_copying", error = e.to_string()).to_string()
1514 } else if succeeded.len() == 1 {
1515 let name = succeeded[0]
1516 .1
1517 .file_name()
1518 .map(|n| n.to_string_lossy().to_string())
1519 .unwrap_or_default();
1520 t!("explorer.duplicated", name = &name).to_string()
1521 } else {
1522 t!("explorer.duplicated_n", count = succeeded.len()).to_string()
1523 };
1524 self.set_status_message(msg);
1525 self.active_window_mut().key_context = KeyContext::FileExplorer;
1526 }
1527
1528 pub fn file_explorer_copy_path(&mut self, relative: bool) {
1535 let Some(explorer) = self.file_explorer() else {
1536 return;
1537 };
1538 let selected_ids = explorer.effective_selection();
1539 let paths: Vec<PathBuf> = selected_ids
1540 .iter()
1541 .filter_map(|&id| explorer.tree().get_node(id).map(|n| n.entry.path.clone()))
1542 .collect();
1543
1544 if paths.is_empty() {
1545 self.set_status_message(t!("clipboard.no_file_path").to_string());
1546 return;
1547 }
1548
1549 let working_dir = self.working_dir().to_path_buf();
1550 let rendered: Vec<String> = paths
1551 .iter()
1552 .map(|p| {
1553 if relative {
1554 p.strip_prefix(&working_dir)
1555 .unwrap_or(p)
1556 .to_string_lossy()
1557 .into_owned()
1558 } else {
1559 p.to_string_lossy().into_owned()
1560 }
1561 })
1562 .collect();
1563
1564 let joined = rendered.join("\n");
1565 self.clipboard.copy(joined.clone());
1566
1567 let msg = if rendered.len() == 1 {
1568 t!("clipboard.copied_path", path = &rendered[0]).to_string()
1569 } else {
1570 t!("clipboard.copied_paths_n", count = rendered.len()).to_string()
1571 };
1572 self.set_status_message(msg);
1573 }
1574}
1575
1576impl crate::app::window::Window {
1577 pub(crate) fn init_file_explorer(&mut self) {
1584 let is_remote = self
1585 .resources
1586 .authority
1587 .filesystem
1588 .remote_connection_info()
1589 .is_some();
1590 let root_exists = self
1591 .resources
1592 .authority
1593 .filesystem
1594 .is_dir(&self.root)
1595 .unwrap_or(false);
1596 let root_path = if is_remote && !root_exists {
1597 match self.resources.authority.filesystem.home_dir() {
1598 Ok(home) => home,
1599 Err(e) => {
1600 tracing::error!("Failed to get remote home directory: {}", e);
1601 self.set_status_message(format!("Failed to get remote home: {}", e));
1602 return;
1603 }
1604 }
1605 } else {
1606 self.root.clone()
1607 };
1608
1609 let Some(runtime) = self.resources.tokio_runtime.clone() else {
1610 return;
1611 };
1612 let fs_manager = Arc::clone(&self.resources.fs_manager);
1613 let sender = self.bridge.sender();
1614 let window_id = self.id;
1617 runtime.spawn(async move {
1618 match FileTree::new(root_path, fs_manager).await {
1619 Ok(mut tree) => {
1620 let root_id = tree.root_id();
1621 if let Err(e) = tree.expand_node(root_id).await {
1622 tracing::warn!("Failed to expand root directory: {}", e);
1623 }
1624 let view = FileTreeView::new(tree);
1625 #[allow(clippy::let_underscore_must_use)]
1627 let _ = sender.send(AsyncMessage::FileExplorerInitialized {
1628 window: window_id,
1629 view,
1630 });
1631 }
1632 Err(e) => {
1633 tracing::error!("Failed to initialize file explorer: {}", e);
1634 }
1635 }
1636 });
1637 self.set_status_message(t!("explorer.initializing").to_string());
1638 }
1639
1640 pub(crate) fn install_initialized_file_explorer(
1646 &mut self,
1647 mut view: FileTreeView,
1648 defaults: FileExplorerViewDefaults,
1649 ) {
1650 let root_id = view.tree().root_id();
1651 if let Some(root_path) = view.tree().get_node(root_id).map(|n| n.entry.path.clone()) {
1652 crate::app::file_operations::load_gitignore_via_fs(
1653 self.resources.authority.filesystem.as_ref(),
1654 &mut view,
1655 &root_path,
1656 );
1657 }
1658 let show_hidden = self
1661 .pending_file_explorer_show_hidden
1662 .take()
1663 .unwrap_or(defaults.show_hidden);
1664 view.ignore_patterns_mut().set_show_hidden(show_hidden);
1665 let show_gitignored = self
1666 .pending_file_explorer_show_gitignored
1667 .take()
1668 .unwrap_or(defaults.show_gitignored);
1669 view.ignore_patterns_mut()
1670 .set_show_gitignored(show_gitignored);
1671 view.set_compact_directories(defaults.compact_directories);
1672 self.file_explorer = Some(view);
1673 if self.file_explorer_visible {
1676 self.sync_file_explorer_to_active_file();
1677 }
1678 }
1679
1680 pub(crate) fn install_expanded_file_explorer(&mut self, mut view: FileTreeView) {
1683 view.update_scroll_for_selection();
1684 self.file_explorer = Some(view);
1685 self.file_explorer_sync_in_progress = false;
1686 }
1687
1688 pub fn focus_editor(&mut self) {
1691 self.key_context = KeyContext::Normal;
1692 self.set_status_message(t!("editor.focused").to_string());
1693 }
1694
1695 pub fn file_explorer_search_clear(&mut self) {
1702 if matches!(
1703 self.file_explorer_clipboard,
1704 Some(FileExplorerClipboard { is_cut: true, .. })
1705 ) {
1706 self.file_explorer_clipboard = None;
1707 self.set_status_message(t!("explorer.cut_cancelled").to_string());
1708 return;
1709 }
1710 let action = self.file_explorer.as_mut().map(|explorer| {
1711 if explorer.has_multi_selection() {
1712 explorer.clear_multi_selection();
1713 None
1714 } else if explorer.is_search_active() {
1715 explorer.search_clear();
1716 None
1717 } else {
1718 Some(())
1719 }
1720 });
1721 if let Some(Some(())) = action {
1722 self.focus_editor();
1723 }
1724 }
1725
1726 pub fn handle_set_file_explorer_decorations(
1731 &mut self,
1732 namespace: String,
1733 decorations: Vec<crate::view::file_tree::FileExplorerDecoration>,
1734 ) {
1735 let root = self.root.clone();
1736 let normalized: Vec<crate::view::file_tree::FileExplorerDecoration> = decorations
1737 .into_iter()
1738 .filter_map(|mut decoration| {
1739 let path = if decoration.path.is_absolute() {
1740 decoration.path
1741 } else {
1742 root.join(&decoration.path)
1743 };
1744 let path = crate::app::normalize_path(&path);
1745 if path.starts_with(&root) {
1746 decoration.path = path;
1747 Some(decoration)
1748 } else {
1749 None
1750 }
1751 })
1752 .collect();
1753
1754 self.file_explorer_decorations.insert(namespace, normalized);
1755 self.rebuild_file_explorer_decoration_cache();
1756 }
1757
1758 pub fn handle_clear_file_explorer_decorations(&mut self, namespace: &str) {
1761 self.file_explorer_decorations.remove(namespace);
1762 self.rebuild_file_explorer_decoration_cache();
1763 }
1764
1765 pub fn rebuild_file_explorer_decoration_cache(&mut self) {
1769 let decorations: Vec<_> = self
1770 .file_explorer_decorations
1771 .values()
1772 .flat_map(|entries| entries.iter().cloned())
1773 .collect();
1774
1775 let symlink_mappings = self
1776 .file_explorer
1777 .as_ref()
1778 .map(|fe| fe.collect_symlink_mappings())
1779 .unwrap_or_default();
1780
1781 self.file_explorer_decoration_cache =
1782 crate::view::file_tree::FileExplorerDecorationCache::rebuild(
1783 decorations.into_iter(),
1784 &self.root,
1785 &symlink_mappings,
1786 );
1787 }
1788
1789 pub fn file_explorer_clipboard(&self) -> Option<&FileExplorerClipboard> {
1791 self.file_explorer_clipboard.as_ref()
1792 }
1793
1794 pub fn file_explorer_copy(&mut self) {
1796 self.set_explorer_clipboard(false);
1797 }
1798
1799 pub fn file_explorer_cut(&mut self) {
1801 self.set_explorer_clipboard(true);
1802 }
1803
1804 fn set_explorer_clipboard(&mut self, is_cut: bool) {
1809 let Some(explorer) = self.file_explorer.as_ref() else {
1810 return;
1811 };
1812 let root_id = explorer.tree().root_id();
1813 let selected_ids = explorer.effective_selection();
1814 let paths: Vec<PathBuf> = selected_ids
1815 .iter()
1816 .filter(|&&id| id != root_id)
1817 .filter_map(|&id| explorer.tree().get_node(id).map(|n| n.entry.path.clone()))
1818 .collect();
1819 if paths.is_empty() {
1820 let msg = if is_cut {
1821 t!("explorer.cannot_cut_root").to_string()
1822 } else {
1823 t!("explorer.cannot_copy_root").to_string()
1824 };
1825 self.set_status_message(msg);
1826 return;
1827 }
1828 let msg = if paths.len() == 1 {
1829 let name = paths[0]
1830 .file_name()
1831 .unwrap_or_default()
1832 .to_string_lossy()
1833 .to_string();
1834 if is_cut {
1835 t!("explorer.cut", name = &name).to_string()
1836 } else {
1837 t!("explorer.copied", name = &name).to_string()
1838 }
1839 } else {
1840 let count = paths.len();
1841 if is_cut {
1842 t!("explorer.cut_n", count = count).to_string()
1843 } else {
1844 t!("explorer.copied_n", count = count).to_string()
1845 }
1846 };
1847 self.file_explorer_clipboard = Some(FileExplorerClipboard { paths, is_cut });
1848 self.set_status_message(msg);
1849 }
1850
1851 pub fn sync_file_explorer_to_active_file(&mut self) {
1856 if !self.file_explorer_visible {
1857 return;
1858 }
1859
1860 if self.file_explorer_sync_in_progress {
1862 return;
1863 }
1864
1865 let active_buf = self.active_buffer();
1866 let Some(metadata) = self.buffer_metadata.get(&active_buf) else {
1867 return;
1868 };
1869 let Some(file_path) = metadata.file_path() else {
1870 return;
1871 };
1872 let target_path = file_path.clone();
1873
1874 if !target_path.starts_with(&self.root) {
1875 return;
1876 }
1877
1878 let Some(mut view) = self.file_explorer.take() else {
1879 return;
1880 };
1881 tracing::trace!(
1882 "sync_file_explorer_to_active_file: taking file_explorer for async expand to {:?}",
1883 target_path
1884 );
1885 let runtime_handle = self
1886 .resources
1887 .tokio_runtime
1888 .as_ref()
1889 .map(|r| r.handle().clone());
1890 let sender = self.resources.async_bridge.as_ref().map(|b| b.sender());
1891 let window_id = self.id;
1892 if let (Some(runtime), Some(sender)) = (runtime_handle, sender) {
1893 self.file_explorer_sync_in_progress = true;
1895
1896 runtime.spawn(async move {
1897 let _success = view.expand_and_select_file(&target_path).await;
1898 #[allow(clippy::let_underscore_must_use)]
1900 let _ = sender.send(
1901 crate::services::async_bridge::AsyncMessage::FileExplorerExpandedToPath {
1902 window: window_id,
1903 view,
1904 },
1905 );
1906 });
1907 } else {
1908 self.file_explorer = Some(view);
1909 }
1910 }
1911}
1912
1913fn unique_paste_name(
1916 fs: &dyn crate::model::filesystem::FileSystem,
1917 dst_dir: &Path,
1918 name: &str,
1919) -> PathBuf {
1920 let (stem, ext) = split_stem_ext(name);
1921 let mut n = 1u32;
1922 loop {
1923 let candidate = if n == 1 {
1924 if ext.is_empty() {
1925 format!("{} copy", stem)
1926 } else {
1927 format!("{} copy.{}", stem, ext)
1928 }
1929 } else if ext.is_empty() {
1930 format!("{} copy {}", stem, n)
1931 } else {
1932 format!("{} copy {}.{}", stem, n, ext)
1933 };
1934 let path = dst_dir.join(&candidate);
1935 if !fs.exists(&path) {
1936 return path;
1937 }
1938 n += 1;
1939 if n > 1000 {
1940 return dst_dir.join(format!("{} copy {}", stem, timestamp_suffix()));
1942 }
1943 }
1944}
1945
1946pub(super) fn truncate_name_for_prompt(name: &str, max: usize) -> String {
1948 if name.chars().count() <= max {
1949 name.to_string()
1950 } else {
1951 let truncated: String = name.chars().take(max.saturating_sub(1)).collect();
1952 format!("{}\u{2026}", truncated)
1953 }
1954}
1955
1956pub(super) fn format_path_preview_for_prompt(paths: &[PathBuf], max_shown: usize) -> String {
1961 let names: Vec<String> = paths
1962 .iter()
1963 .map(|p| {
1964 let raw = p
1965 .file_name()
1966 .map(|n| n.to_string_lossy().to_string())
1967 .unwrap_or_default();
1968 format!("'{}'", truncate_name_for_prompt(&raw, 24))
1969 })
1970 .collect();
1971 if names.len() <= max_shown {
1972 names.join(", ")
1973 } else {
1974 let shown = names[..max_shown].join(", ");
1975 let more = names.len() - max_shown;
1976 format!("{}, \u{2026} ({} more)", shown, more)
1977 }
1978}
1979
1980fn split_stem_ext(name: &str) -> (&str, &str) {
1981 if let Some(dot_pos) = name.rfind('.') {
1983 if dot_pos > 0 {
1984 return (&name[..dot_pos], &name[dot_pos + 1..]);
1985 }
1986 }
1987 (name, "")
1988}