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)]
20enum PasteOpOutcome {
21 Ok,
23 SourceRemovalFailed { dst: PathBuf, err: std::io::Error },
26 Failed(std::io::Error),
29}
30
31fn get_parent_dir_path(node: &TreeNode) -> PathBuf {
34 if node.is_dir() {
35 node.entry.path.clone()
36 } else {
37 node.entry
38 .path
39 .parent()
40 .map(|p| p.to_path_buf())
41 .unwrap_or_else(|| node.entry.path.clone())
42 }
43}
44
45fn timestamp_suffix() -> u64 {
47 std::time::SystemTime::now()
48 .duration_since(std::time::UNIX_EPOCH)
49 .unwrap()
50 .as_secs()
51}
52
53fn get_parent_node_id(
56 tree: &crate::view::file_tree::FileTree,
57 selected_id: crate::view::file_tree::NodeId,
58 node_is_dir: bool,
59) -> crate::view::file_tree::NodeId {
60 if node_is_dir {
61 selected_id
62 } else {
63 tree.get_node(selected_id)
64 .and_then(|n| n.parent)
65 .unwrap_or(selected_id)
66 }
67}
68
69impl Editor {
70 pub fn file_explorer_visible(&self) -> bool {
71 self.active_window().file_explorer_visible
72 }
73
74 pub(super) fn take_focus_for_file_explorer(&mut self) {
87 let win = self.active_window_mut();
88 if win.terminal_mode {
89 let active = win.active_buffer();
90 if win.is_terminal_buffer(active) {
91 win.terminal_mode_resume.insert(active);
92 }
93 win.terminal_mode = false;
94 }
95 win.key_context = KeyContext::FileExplorer;
96 }
97
98 pub fn toggle_file_explorer(&mut self) {
99 let new_visible = !self.active_window().file_explorer_visible;
100 self.active_window_mut().file_explorer_visible = new_visible;
101
102 if new_visible {
103 if self.file_explorer().is_none() {
104 self.init_file_explorer();
105 }
106 self.take_focus_for_file_explorer();
107 self.set_status_message(t!("explorer.opened").to_string());
108 self.active_window_mut().sync_file_explorer_to_active_file();
109 } else {
110 self.active_window_mut().key_context = KeyContext::Normal;
111 self.set_status_message(t!("explorer.closed").to_string());
112 }
113
114 self.plugin_manager.read().unwrap().run_hook(
116 "resize",
117 fresh_core::hooks::HookArgs::Resize {
118 width: self.terminal_width,
119 height: self.terminal_height,
120 },
121 );
122 }
123
124 pub fn show_file_explorer(&mut self) {
125 if !self.file_explorer_visible() {
126 self.toggle_file_explorer();
127 }
128 }
129
130 pub fn focus_file_explorer(&mut self) {
131 if self.file_explorer_visible() {
132 self.active_window_mut().on_editor_focus_lost();
134
135 self.active_window_mut().cancel_search_prompt_if_active();
137
138 self.take_focus_for_file_explorer();
139 self.set_status_message(t!("explorer.focused").to_string());
140 self.active_window_mut().sync_file_explorer_to_active_file();
141 } else {
142 self.toggle_file_explorer();
143 }
144 }
145
146 pub(crate) fn init_file_explorer(&mut self) {
153 self.active_window_mut().init_file_explorer();
154 }
155
156 pub fn file_explorer_navigate_up(&mut self) {
157 if let Some(explorer) = self.file_explorer_mut() {
158 explorer.select_prev_match();
159 explorer.update_scroll_for_selection();
160 }
161 self.file_explorer_preview_selected();
162 }
163
164 pub fn file_explorer_navigate_down(&mut self) {
165 if let Some(explorer) = self.file_explorer_mut() {
166 explorer.select_next_match();
167 explorer.update_scroll_for_selection();
168 }
169 self.file_explorer_preview_selected();
170 }
171
172 pub fn file_explorer_page_up(&mut self) {
173 if let Some(explorer) = self.file_explorer_mut() {
174 explorer.select_page_up();
175 explorer.update_scroll_for_selection();
176 }
177 self.file_explorer_preview_selected();
178 }
179
180 pub fn file_explorer_page_down(&mut self) {
181 if let Some(explorer) = self.file_explorer_mut() {
182 explorer.select_page_down();
183 explorer.update_scroll_for_selection();
184 }
185 self.file_explorer_preview_selected();
186 }
187
188 fn file_explorer_preview_selected(&mut self) {
196 if !self.config.file_explorer.preview_tabs {
199 return;
200 }
201
202 let path = match self
203 .file_explorer()
204 .as_ref()
205 .and_then(|explorer| explorer.get_selected_entry())
206 {
207 Some(entry) if !entry.is_dir() => entry.path.clone(),
208 _ => return,
209 };
210
211 if let Err(e) = self.open_file_preview(&path) {
212 tracing::debug!(
213 "file_explorer_preview_selected: skipping preview for {:?}: {}",
214 path,
215 e
216 );
217 }
218 }
219
220 pub fn file_explorer_collapse(&mut self) {
224 let Some(explorer) = self.file_explorer() else {
225 return;
226 };
227
228 let Some(selected_id) = explorer.get_selected() else {
229 return;
230 };
231
232 let Some(node) = explorer.tree().get_node(selected_id) else {
233 return;
234 };
235
236 if node.is_dir() && node.is_expanded() {
238 self.file_explorer_toggle_expand();
239 return;
240 }
241
242 if let Some(explorer) = self.file_explorer_mut() {
244 explorer.select_parent();
245 explorer.update_scroll_for_selection();
246 }
247 }
248
249 pub fn file_explorer_toggle_expand(&mut self) {
250 let selected_id = if let Some(explorer) = self.file_explorer() {
251 explorer.get_selected()
252 } else {
253 return;
254 };
255
256 let Some(selected_id) = selected_id else {
257 return;
258 };
259
260 let (is_dir, is_expanded, name) = if let Some(explorer) = self.file_explorer() {
261 let node = explorer.tree().get_node(selected_id);
262 if let Some(node) = node {
263 (node.is_dir(), node.is_expanded(), node.entry.name.clone())
264 } else {
265 return;
266 }
267 } else {
268 return;
269 };
270
271 if !is_dir {
272 return;
273 }
274
275 let status_msg = if is_expanded {
276 t!("explorer.collapsing").to_string()
277 } else {
278 t!("explorer.loading_dir", name = &name).to_string()
279 };
280 self.set_status_message(status_msg);
281
282 let active_id = self.active_window;
283 if let (Some(runtime), Some(explorer)) = (
288 self.tokio_runtime.as_ref(),
289 self.windows
290 .get_mut(&active_id)
291 .and_then(|w| w.file_explorer.as_mut()),
292 ) {
293 let result = runtime.block_on(explorer.toggle_with_chain(selected_id));
294
295 let final_name = explorer
296 .tree()
297 .get_node(selected_id)
298 .map(|n| n.entry.name.clone());
299 let final_expanded = explorer
300 .tree()
301 .get_node(selected_id)
302 .map(|n| n.is_expanded())
303 .unwrap_or(false);
304
305 let mut needs_decoration_rebuild = false;
307
308 match result {
309 Ok(()) => {
310 if final_expanded {
311 let node_info = explorer
312 .tree()
313 .get_node(selected_id)
314 .map(|n| (n.entry.path.clone(), n.entry.is_symlink()));
315
316 if let Some((dir_path, is_symlink)) = node_info {
317 crate::app::file_operations::load_gitignore_via_fs(
318 self.authority.filesystem.as_ref(),
319 explorer,
320 &dir_path,
321 );
322
323 if is_symlink {
327 tracing::debug!(
328 "Symlink directory expanded, will rebuild decoration cache: {:?}",
329 dir_path
330 );
331 needs_decoration_rebuild = true;
332 }
333 }
334 }
335
336 if let Some(name) = final_name {
337 let msg = if final_expanded {
338 t!("explorer.expanded", name = &name).to_string()
339 } else {
340 t!("explorer.collapsed", name = &name).to_string()
341 };
342 self.set_status_message(msg);
343 }
344 }
345 Err(e) => {
346 self.set_status_message(
347 t!("explorer.error", error = e.to_string()).to_string(),
348 );
349 }
350 }
351
352 if needs_decoration_rebuild {
354 self.active_window_mut()
355 .rebuild_file_explorer_decoration_cache();
356 }
357 }
358 }
359
360 pub fn file_explorer_open_file(&mut self) -> AnyhowResult<()> {
361 let entry_type = self
362 .file_explorer()
363 .as_ref()
364 .and_then(|explorer| explorer.get_selected_entry())
365 .map(|entry| (entry.is_dir(), entry.path.clone(), entry.name.clone()));
366
367 if let Some((is_dir, path, name)) = entry_type {
368 if is_dir {
369 self.file_explorer_toggle_expand();
370 } else {
371 tracing::info!("[SYNTAX DEBUG] file_explorer opening file: {:?}", path);
372 match self.open_file(&path) {
373 Ok(id) => {
374 self.active_window_mut().promote_buffer_from_preview(id);
378 self.set_status_message(
379 t!("explorer.opened_file", name = &name).to_string(),
380 );
381 self.active_window_mut().focus_editor();
382 }
383 Err(e) => {
384 if let Some(confirmation) =
387 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
388 {
389 self.start_large_file_encoding_confirmation(confirmation);
390 } else {
391 self.set_status_message(
392 t!("file.error_opening", error = e.to_string()).to_string(),
393 );
394 }
395 }
396 }
397 }
398 }
399 Ok(())
400 }
401
402 pub fn file_explorer_refresh(&mut self) {
403 let (selected_id, node_name) = if let Some(explorer) = self.file_explorer() {
404 if let Some(selected_id) = explorer.get_selected() {
405 let node_name = explorer
406 .tree()
407 .get_node(selected_id)
408 .map(|n| n.entry.name.clone());
409 (Some(selected_id), node_name)
410 } else {
411 (None, None)
412 }
413 } else {
414 return;
415 };
416
417 let Some(selected_id) = selected_id else {
418 return;
419 };
420
421 if let Some(name) = &node_name {
422 self.set_status_message(t!("explorer.refreshing", name = name).to_string());
423 }
424
425 let active_id = self.active_window;
426 if let (Some(runtime), Some(explorer)) = (
427 self.tokio_runtime.as_ref(),
428 self.windows
429 .get_mut(&active_id)
430 .and_then(|w| w.file_explorer.as_mut()),
431 ) {
432 let tree = explorer.tree_mut();
433 let result = runtime.block_on(tree.refresh_node(selected_id));
434 match result {
435 Ok(()) => {
436 if let Some(name) = node_name {
437 self.set_status_message(t!("explorer.refreshed", name = &name).to_string());
438 } else {
439 self.set_status_message(t!("explorer.refreshed_default").to_string());
440 }
441 }
442 Err(e) => {
443 self.set_status_message(
444 t!("explorer.error_refreshing", error = e.to_string()).to_string(),
445 );
446 }
447 }
448 }
449 }
450
451 pub fn file_explorer_new_file(&mut self) {
452 let active_id = self.active_window;
453 if let Some(explorer) = self
454 .windows
455 .get_mut(&active_id)
456 .and_then(|w| w.file_explorer.as_mut())
457 {
458 if let Some(selected_id) = explorer.get_selected() {
459 let node = explorer.tree().get_node(selected_id);
460 if let Some(node) = node {
461 let parent_path = get_parent_dir_path(node);
462 let filename = format!("untitled_{}.txt", timestamp_suffix());
463 let file_path = parent_path.join(&filename);
464
465 if let Some(runtime) = &self.tokio_runtime {
466 let path_clone = file_path.clone();
467 let result = self
468 .authority
469 .filesystem
470 .create_file(&path_clone)
471 .map(|_| ());
472
473 match result {
474 Ok(_) => {
475 let parent_id =
476 get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
477 let tree = explorer.tree_mut();
478 if let Err(e) =
479 runtime.block_on(tree.reload_expanded_node(parent_id))
480 {
481 tracing::warn!("Failed to refresh file tree: {}", e);
482 }
483 if let Some(explorer) = self.file_explorer_mut().as_mut() {
484 explorer.navigate_to_path(&path_clone);
485 }
486 self.set_status_message(
487 t!("explorer.created_file", name = &filename).to_string(),
488 );
489 self.notify_file_explorer_change(&path_clone);
490
491 if let Err(e) = self.open_file(&path_clone) {
493 tracing::warn!("Failed to open new file: {}", e);
494 }
495
496 let prompt = crate::view::prompt::Prompt::new(
497 t!("explorer.new_file_prompt").to_string(),
498 crate::view::prompt::PromptType::FileExplorerRename {
499 original_path: path_clone,
500 original_name: filename.clone(),
501 is_new_file: true,
502 },
503 );
504 self.active_window_mut().prompt = Some(prompt);
505 }
506 Err(e) => {
507 self.set_status_message(
508 t!("explorer.error_creating_file", error = e.to_string())
509 .to_string(),
510 );
511 }
512 }
513 }
514 }
515 }
516 }
517 }
518
519 pub fn file_explorer_new_directory(&mut self) {
520 let active_id = self.active_window;
521 if let Some(explorer) = self
522 .windows
523 .get_mut(&active_id)
524 .and_then(|w| w.file_explorer.as_mut())
525 {
526 if let Some(selected_id) = explorer.get_selected() {
527 let node = explorer.tree().get_node(selected_id);
528 if let Some(node) = node {
529 let parent_path = get_parent_dir_path(node);
530 let dirname = format!("New Folder {}", timestamp_suffix());
531 let dir_path = parent_path.join(&dirname);
532
533 if let Some(runtime) = &self.tokio_runtime {
534 let path_clone = dir_path.clone();
535 let dirname_clone = dirname.clone();
536 let result = self.authority.filesystem.create_dir(&path_clone);
537
538 match result {
539 Ok(_) => {
540 let parent_id =
541 get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
542 let tree = explorer.tree_mut();
543 if let Err(e) =
544 runtime.block_on(tree.reload_expanded_node(parent_id))
545 {
546 tracing::warn!("Failed to refresh file tree: {}", e);
547 }
548 if let Some(explorer) = self.file_explorer_mut().as_mut() {
549 explorer.navigate_to_path(&path_clone);
550 }
551 self.set_status_message(
552 t!("explorer.created_dir", name = &dirname_clone).to_string(),
553 );
554 self.notify_file_explorer_change(&path_clone);
555
556 let prompt = crate::view::prompt::Prompt::with_initial_text(
557 t!("explorer.new_directory_prompt").to_string(),
558 crate::view::prompt::PromptType::FileExplorerRename {
559 original_path: path_clone,
560 original_name: dirname_clone,
561 is_new_file: true,
562 },
563 dirname,
564 );
565 self.active_window_mut().prompt = Some(prompt);
566 }
567 Err(e) => {
568 self.set_status_message(
569 t!("explorer.error_creating_dir", error = e.to_string())
570 .to_string(),
571 );
572 }
573 }
574 }
575 }
576 }
577 }
578 }
579
580 pub fn file_explorer_delete(&mut self) {
581 let Some(explorer) = self.file_explorer() else {
582 return;
583 };
584 let root_id = explorer.tree().root_id();
585 let selected_ids = explorer.effective_selection();
586
587 let paths: Vec<(PathBuf, bool)> = selected_ids
588 .iter()
589 .filter(|&&id| id != root_id)
590 .filter_map(|&id| {
591 explorer
592 .tree()
593 .get_node(id)
594 .map(|n| (n.entry.path.clone(), n.is_dir()))
595 })
596 .collect();
597
598 if paths.is_empty() {
599 self.set_status_message(t!("explorer.cannot_delete_root").to_string());
600 return;
601 }
602
603 if paths.len() == 1 {
604 let (path, is_dir) = paths.into_iter().next().unwrap();
605 let name = path
606 .file_name()
607 .unwrap_or_default()
608 .to_string_lossy()
609 .to_string();
610 let type_str = if is_dir { "directory" } else { "file" };
611 self.start_prompt(
612 t!("explorer.delete_confirm", "type" = type_str, name = &name).to_string(),
613 PromptType::ConfirmDeleteFile { path, is_dir },
614 );
615 } else {
616 let count = paths.len();
617 let all_paths: Vec<PathBuf> = paths.into_iter().map(|(p, _)| p).collect();
618 let names = format_path_preview_for_prompt(&all_paths, 3);
622 self.start_prompt(
623 t!(
624 "explorer.delete_multi_confirm",
625 count = count,
626 names = &names
627 )
628 .to_string(),
629 PromptType::ConfirmMultiDelete { paths: all_paths },
630 );
631 }
632 }
633
634 pub fn perform_file_explorer_delete(&mut self, path: std::path::PathBuf, _is_dir: bool) {
638 let name = path
639 .file_name()
640 .map(|n| n.to_string_lossy().to_string())
641 .unwrap_or_default();
642
643 let delete_result = if self.authority.filesystem.remote_connection_info().is_some() {
646 self.move_to_remote_trash(&path)
647 } else {
648 trash::delete(&path).map_err(std::io::Error::other)
649 };
650
651 match delete_result {
652 Ok(_) => {
653 let to_close = self.buffer_ids_under_path(&path);
663 for id in to_close {
664 if let Err(e) = self.force_close_buffer(id) {
665 tracing::warn!(
666 "Failed to close buffer {:?} after delete of {:?}: {}",
667 id,
668 path,
669 e
670 );
671 }
672 }
673
674 let active_id = self.active_window;
676 if let Some(explorer) = self
677 .windows
678 .get_mut(&active_id)
679 .and_then(|w| w.file_explorer.as_mut())
680 {
681 if let Some(runtime) = &self.tokio_runtime {
682 if let Some(node) = explorer.tree().get_node_by_path(&path) {
684 let node_id = node.id;
685 let parent_id = get_parent_node_id(explorer.tree(), node_id, false);
686
687 let deleted_index = explorer.get_selected_index();
689
690 if let Err(e) = runtime
691 .block_on(explorer.tree_mut().reload_expanded_node(parent_id))
692 {
693 tracing::warn!("Failed to refresh file tree after delete: {}", e);
694 }
695
696 explorer.clear_multi_selection();
701
702 let count = explorer.visible_count();
705 if count > 0 {
706 let new_index = if let Some(idx) = deleted_index {
707 idx.min(count.saturating_sub(1))
708 } else {
709 0
710 };
711 if let Some(node_id) = explorer.get_node_at_index(new_index) {
712 explorer.set_selected(Some(node_id));
713 }
714 } else {
715 explorer.set_selected(Some(parent_id));
717 }
718 }
719 }
720 }
721 self.set_status_message(t!("explorer.moved_to_trash", name = &name).to_string());
722 self.notify_file_explorer_change(&path);
723
724 self.active_window_mut().key_context = KeyContext::FileExplorer;
726 }
727 Err(e) => {
728 self.set_status_message(
729 t!("explorer.error_trash", error = e.to_string()).to_string(),
730 );
731 }
732 }
733 }
734
735 fn move_to_remote_trash(&self, path: &std::path::Path) -> std::io::Result<()> {
737 let home = self.authority.filesystem.home_dir()?;
739 let trash_dir = home.join(".local/share/fresh/trash");
740
741 if !self.authority.filesystem.exists(&trash_dir) {
743 self.authority.filesystem.create_dir_all(&trash_dir)?;
744 }
745
746 let file_name = path
748 .file_name()
749 .unwrap_or_else(|| std::ffi::OsStr::new("unnamed"));
750 let timestamp = std::time::SystemTime::now()
751 .duration_since(std::time::UNIX_EPOCH)
752 .map(|d| d.as_secs())
753 .unwrap_or(0);
754 let trash_name = format!("{}.{}", file_name.to_string_lossy(), timestamp);
755 let trash_path = trash_dir.join(trash_name);
756
757 self.authority.filesystem.rename(path, &trash_path)
759 }
760
761 pub fn file_explorer_rename(&mut self) {
762 if let Some(explorer) = self.file_explorer() {
763 if let Some(selected_id) = explorer.get_selected() {
764 if selected_id == explorer.tree().root_id() {
766 self.set_status_message(t!("explorer.cannot_rename_root").to_string());
767 return;
768 }
769
770 let node = explorer.tree().get_node(selected_id);
771 if let Some(node) = node {
772 let old_path = node.entry.path.clone();
773 let old_name = node.entry.name.clone();
774
775 let prompt = crate::view::prompt::Prompt::with_initial_text_for_edit(
780 t!("explorer.rename_prompt").to_string(),
781 crate::view::prompt::PromptType::FileExplorerRename {
782 original_path: old_path,
783 original_name: old_name.clone(),
784 is_new_file: false,
785 },
786 old_name,
787 );
788 self.active_window_mut().prompt = Some(prompt);
789 }
790 }
791 }
792 }
793
794 pub fn perform_file_explorer_rename(
796 &mut self,
797 original_path: std::path::PathBuf,
798 original_name: String,
799 new_name: String,
800 is_new_file: bool,
801 ) {
802 if new_name.is_empty() || new_name == original_name {
803 self.set_status_message(t!("explorer.rename_cancelled").to_string());
804 return;
805 }
806
807 if new_name.chars().any(std::path::is_separator) {
812 self.set_status_message(t!("explorer.rename_invalid_separator").to_string());
813 return;
814 }
815 if new_name == "." || new_name == ".." {
816 self.set_status_message(t!("explorer.rename_invalid_dot").to_string());
817 return;
818 }
819
820 let new_path = original_path
821 .parent()
822 .map(|p| p.join(&new_name))
823 .unwrap_or_else(|| original_path.clone());
824
825 if self.tokio_runtime.is_some() {
826 let result = self.authority.filesystem.rename(&original_path, &new_path);
827
828 match result {
829 Ok(_) => {
830 let active_id = self.active_window;
834 if let (Some(runtime), Some(explorer)) = (
835 self.tokio_runtime.as_ref(),
836 self.windows
837 .get_mut(&active_id)
838 .and_then(|w| w.file_explorer.as_mut()),
839 ) {
840 if let Some(selected_id) = explorer.get_selected() {
841 let parent_id = get_parent_node_id(explorer.tree(), selected_id, false);
842 let tree = explorer.tree_mut();
843 if let Err(e) = runtime.block_on(tree.reload_expanded_node(parent_id)) {
844 tracing::warn!("Failed to refresh file tree after rename: {}", e);
845 }
846 }
847 explorer.clear_multi_selection();
851 explorer.navigate_to_path(&new_path);
853 }
854
855 let relocated = self.relocate_buffers_for_rename(&original_path, &new_path);
863
864 if is_new_file && !relocated.is_empty() {
868 self.active_window_mut().key_context = KeyContext::Normal;
869 }
870
871 self.set_status_message(
872 t!("explorer.renamed", old = &original_name, new = &new_name).to_string(),
873 );
874 self.notify_file_explorer_change(&new_path);
875 }
876 Err(e) => {
877 self.set_status_message(
878 t!("explorer.error_renaming", error = e.to_string()).to_string(),
879 );
880 }
881 }
882 }
883 }
884
885 pub fn file_explorer_toggle_hidden(&mut self) {
886 let show_hidden = if let Some(explorer) = self.file_explorer_mut() {
887 explorer.toggle_show_hidden();
888 explorer.ignore_patterns().show_hidden()
889 } else {
890 return;
891 };
892
893 let msg = if show_hidden {
894 t!("explorer.showing_hidden")
895 } else {
896 t!("explorer.hiding_hidden")
897 };
898 self.set_status_message(msg.to_string());
899
900 self.config_mut().file_explorer.show_hidden = show_hidden;
902 self.persist_config_change(
903 "/file_explorer/show_hidden",
904 serde_json::Value::Bool(show_hidden),
905 );
906 }
907
908 pub fn file_explorer_toggle_gitignored(&mut self) {
909 let show_gitignored = if let Some(explorer) = self.file_explorer_mut() {
910 explorer.toggle_show_gitignored();
911 explorer.ignore_patterns().show_gitignored()
912 } else {
913 return;
914 };
915
916 let msg = if show_gitignored {
917 t!("explorer.showing_gitignored")
918 } else {
919 t!("explorer.hiding_gitignored")
920 };
921 self.set_status_message(msg.to_string());
922
923 self.config_mut().file_explorer.show_gitignored = show_gitignored;
925 self.persist_config_change(
926 "/file_explorer/show_gitignored",
927 serde_json::Value::Bool(show_gitignored),
928 );
929 }
930
931 pub fn file_explorer_paste(&mut self) {
951 let clipboard = match self.active_window().file_explorer_clipboard.clone() {
952 Some(c) => c,
953 None => {
954 self.set_status_message(t!("explorer.paste_no_source").to_string());
955 return;
956 }
957 };
958
959 let dst_dir = if let Some(explorer) = self.file_explorer() {
960 if let Some(selected_id) = explorer.get_selected() {
961 if let Some(node) = explorer.tree().get_node(selected_id) {
962 get_parent_dir_path(node)
963 } else {
964 return;
965 }
966 } else {
967 return;
968 }
969 } else {
970 return;
971 };
972
973 let is_cut = clipboard.is_cut;
974
975 if clipboard.paths.len() == 1 {
976 let src = clipboard.paths[0].clone();
977 let file_name = match src.file_name() {
978 Some(n) => n.to_os_string(),
979 None => return,
980 };
981 let dst_path = dst_dir.join(&file_name);
982
983 if src.parent().map(|p| p == dst_dir).unwrap_or(false) {
984 if is_cut {
985 self.active_window_mut().file_explorer_clipboard = None;
990 self.set_status_message(t!("explorer.cut_cancelled").to_string());
991 return;
992 } else {
993 let unique = unique_paste_name(
994 &*self.authority.filesystem,
995 &dst_dir,
996 &file_name.to_string_lossy(),
997 );
998 self.perform_file_explorer_paste(src, unique, false);
999 return;
1000 }
1001 }
1002
1003 if self.authority.filesystem.exists(&dst_path) {
1004 let name = truncate_name_for_prompt(&file_name.to_string_lossy(), 40);
1005 self.start_prompt(
1006 t!("explorer.paste_conflict", name = &name).to_string(),
1007 crate::view::prompt::PromptType::ConfirmPasteConflict {
1008 src,
1009 dst: dst_path,
1010 is_cut,
1011 },
1012 );
1013 } else {
1014 self.perform_file_explorer_paste(src, dst_path, is_cut);
1015 }
1016 } else {
1017 let mut safe: Vec<(PathBuf, PathBuf)> = Vec::new();
1019 let mut conflicts: Vec<(PathBuf, PathBuf)> = Vec::new();
1020
1021 for src in &clipboard.paths {
1022 let file_name = match src.file_name() {
1023 Some(n) => n.to_os_string(),
1024 None => continue,
1025 };
1026 let dst_path = dst_dir.join(&file_name);
1027 let is_same_location = src.parent().map(|p| p == dst_dir).unwrap_or(false);
1028
1029 if is_same_location {
1030 if !is_cut {
1031 let unique = unique_paste_name(
1033 &*self.authority.filesystem,
1034 &dst_dir,
1035 &file_name.to_string_lossy(),
1036 );
1037 safe.push((src.clone(), unique));
1038 }
1039 } else if self.authority.filesystem.exists(&dst_path) {
1041 conflicts.push((src.clone(), dst_path));
1042 } else {
1043 safe.push((src.clone(), dst_path));
1044 }
1045 }
1046
1047 if safe.is_empty() && conflicts.is_empty() {
1048 if is_cut {
1052 self.active_window_mut().file_explorer_clipboard = None;
1053 self.set_status_message(t!("explorer.cut_cancelled").to_string());
1054 } else {
1055 self.set_status_message(t!("explorer.paste_same_location").to_string());
1056 }
1057 return;
1058 }
1059
1060 if conflicts.is_empty() {
1061 self.execute_resolved_multi_paste(safe, vec![], is_cut);
1062 } else {
1063 let name = truncate_name_for_prompt(
1064 &conflicts[0]
1065 .1
1066 .file_name()
1067 .unwrap_or_default()
1068 .to_string_lossy(),
1069 40,
1070 );
1071 self.start_prompt(
1072 t!("explorer.paste_conflict_multi", name = &name).to_string(),
1073 crate::view::prompt::PromptType::ConfirmMultiPasteConflict {
1074 safe,
1075 confirmed: Vec::new(),
1076 pending: conflicts,
1077 is_cut,
1078 },
1079 );
1080 }
1081 }
1082 }
1083
1084 pub(super) fn execute_resolved_multi_paste(
1092 &mut self,
1093 safe: Vec<(PathBuf, PathBuf)>,
1094 to_overwrite: Vec<(PathBuf, PathBuf)>,
1095 is_cut: bool,
1096 ) {
1097 let total = safe.len() + to_overwrite.len();
1098 if total == 0 {
1099 return;
1100 }
1101
1102 let mut succeeded: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(total);
1103 let mut clean_moves: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(total);
1110 let mut first_error: Option<std::io::Error> = None;
1111 let mut partial_moves: Vec<(PathBuf, std::io::Error)> = Vec::new();
1112 for (src, dst) in safe.into_iter().chain(to_overwrite) {
1113 match self.paste_one_fs_op(&src, &dst, is_cut) {
1114 PasteOpOutcome::Ok => {
1115 clean_moves.push((src.clone(), dst.clone()));
1116 succeeded.push((src, dst));
1117 }
1118 PasteOpOutcome::SourceRemovalFailed {
1119 dst: landed_dst,
1120 err,
1121 } => {
1122 succeeded.push((src, landed_dst.clone()));
1126 partial_moves.push((landed_dst, err));
1127 }
1128 PasteOpOutcome::Failed(e) => {
1129 if first_error.is_none() {
1130 first_error = Some(e);
1131 }
1132 }
1133 }
1134 }
1135
1136 if is_cut {
1142 for (src, dst) in &clean_moves {
1143 self.relocate_buffers_for_rename(src, dst);
1144 }
1145 }
1146
1147 if !succeeded.is_empty() {
1148 let first_dst = succeeded[0].1.clone();
1149 let any_src = succeeded[0].0.clone();
1150 self.refresh_tree_after_paste(&any_src, &first_dst, is_cut);
1151 }
1152
1153 if !partial_moves.is_empty() {
1154 let (first_dst, first_err) = &partial_moves[0];
1157 let name = first_dst
1158 .file_name()
1159 .map(|n| n.to_string_lossy().to_string())
1160 .unwrap_or_default();
1161 let msg = if partial_moves.len() == 1 {
1162 t!(
1163 "explorer.move_source_removal_failed",
1164 name = &name,
1165 error = first_err.to_string()
1166 )
1167 .to_string()
1168 } else {
1169 t!(
1170 "explorer.move_source_removal_failed_n",
1171 count = partial_moves.len()
1172 )
1173 .to_string()
1174 };
1175 self.set_status_message(msg);
1176 } else if let Some(e) = &first_error {
1177 let msg = if is_cut {
1178 t!("explorer.error_moving", error = e.to_string()).to_string()
1179 } else {
1180 t!("explorer.error_copying", error = e.to_string()).to_string()
1181 };
1182 self.set_status_message(msg);
1183 } else if total > 1 {
1184 let msg = if is_cut {
1185 t!("explorer.pasted_moved_n", count = total).to_string()
1186 } else {
1187 t!("explorer.pasted_n", count = total).to_string()
1188 };
1189 self.set_status_message(msg);
1190 } else if let Some((_, dst)) = succeeded.first() {
1191 let name = dst
1192 .file_name()
1193 .map(|n| n.to_string_lossy().to_string())
1194 .unwrap_or_default();
1195 let msg = if is_cut {
1196 t!("explorer.pasted_moved", name = &name).to_string()
1197 } else {
1198 t!("explorer.pasted", name = &name).to_string()
1199 };
1200 self.set_status_message(msg);
1201 }
1202
1203 if is_cut && first_error.is_none() && partial_moves.is_empty() {
1207 self.active_window_mut().file_explorer_clipboard = None;
1208 }
1209 self.active_window_mut().key_context = KeyContext::FileExplorer;
1210 }
1211
1212 fn paste_one_fs_op(&self, src: &Path, dst: &Path, is_cut: bool) -> PasteOpOutcome {
1216 let src_is_dir = self.authority.filesystem.is_dir(src).unwrap_or(false);
1217
1218 if src_is_dir && dst.starts_with(src) {
1226 return PasteOpOutcome::Failed(std::io::Error::new(
1227 std::io::ErrorKind::InvalidInput,
1228 "Cannot paste a directory into itself",
1229 ));
1230 }
1231
1232 if is_cut {
1233 match self.authority.filesystem.rename(src, dst) {
1238 Ok(()) => PasteOpOutcome::Ok,
1239 Err(e) if e.kind() == std::io::ErrorKind::CrossesDevices => {
1240 let copy_result = if src_is_dir {
1241 self.authority.filesystem.copy_dir_all(src, dst)
1242 } else {
1243 self.authority.filesystem.copy(src, dst).map(|_| ())
1244 };
1245 match copy_result {
1246 Ok(()) => {
1247 let remove_result = if src_is_dir {
1253 self.authority.filesystem.remove_dir_all(src)
1254 } else {
1255 self.authority.filesystem.remove_file(src)
1256 };
1257 match remove_result {
1258 Ok(()) => PasteOpOutcome::Ok,
1259 Err(remove_err) => PasteOpOutcome::SourceRemovalFailed {
1260 dst: dst.to_path_buf(),
1261 err: remove_err,
1262 },
1263 }
1264 }
1265 Err(copy_err) => {
1266 let cleanup = if src_is_dir {
1272 self.authority.filesystem.remove_dir_all(dst)
1273 } else {
1274 self.authority.filesystem.remove_file(dst)
1275 };
1276 if let Err(cleanup_err) = cleanup {
1277 tracing::warn!(
1278 "Failed to roll back partial destination {:?} after copy \
1279 fallback failed: {}",
1280 dst,
1281 cleanup_err
1282 );
1283 }
1284 PasteOpOutcome::Failed(copy_err)
1285 }
1286 }
1287 }
1288 Err(e) => PasteOpOutcome::Failed(e),
1289 }
1290 } else if src_is_dir {
1291 match self.authority.filesystem.copy_dir_all(src, dst) {
1292 Ok(()) => PasteOpOutcome::Ok,
1293 Err(e) => PasteOpOutcome::Failed(e),
1294 }
1295 } else {
1296 match self.authority.filesystem.copy(src, dst) {
1297 Ok(_) => PasteOpOutcome::Ok,
1298 Err(e) => PasteOpOutcome::Failed(e),
1299 }
1300 }
1301 }
1302
1303 fn refresh_tree_after_paste(&mut self, src: &Path, dst: &Path, is_cut: bool) {
1308 let active_id = self.active_window;
1309 let Some(explorer) = self
1312 .windows
1313 .get_mut(&active_id)
1314 .and_then(|w| w.file_explorer.as_mut())
1315 else {
1316 return;
1317 };
1318 if let Some(runtime) = &self.tokio_runtime {
1319 if let Some(dst_parent) = dst.parent() {
1321 if let Some(dst_parent_node) = explorer.tree().get_node_by_path(dst_parent) {
1322 let pid = dst_parent_node.id;
1323 if let Err(e) = runtime.block_on(explorer.tree_mut().reload_expanded_node(pid))
1324 {
1325 tracing::warn!("Failed to reload destination directory after paste: {}", e);
1326 }
1327 }
1328 }
1329 if is_cut {
1339 if let Some(src_parent) = src.parent() {
1340 if let Some(src_parent_node) = explorer.tree().get_node_by_path(src_parent) {
1341 let pid = src_parent_node.id;
1342 if let Err(e) =
1343 runtime.block_on(explorer.tree_mut().reload_expanded_node(pid))
1344 {
1345 tracing::warn!("Failed to refresh source directory after move: {}", e);
1346 }
1347 }
1348 }
1349 }
1350 }
1351 explorer.clear_multi_selection();
1356 explorer.navigate_to_path(dst);
1357
1358 self.notify_file_explorer_change(dst);
1359 }
1360
1361 pub(super) fn notify_file_explorer_change(&self, path: &Path) {
1373 self.plugin_manager.read().unwrap().run_hook(
1374 "after_file_explorer_change",
1375 crate::services::plugins::hooks::HookArgs::AfterFileExplorerChange {
1376 path: path.to_path_buf(),
1377 },
1378 );
1379 }
1380
1381 pub fn perform_file_explorer_paste(&mut self, src: PathBuf, dst: PathBuf, is_cut: bool) {
1382 let name = dst
1383 .file_name()
1384 .map(|n| n.to_string_lossy().to_string())
1385 .unwrap_or_default();
1386
1387 match self.paste_one_fs_op(&src, &dst, is_cut) {
1388 PasteOpOutcome::Ok => {
1389 if is_cut {
1396 self.relocate_buffers_for_rename(&src, &dst);
1397 }
1398 self.refresh_tree_after_paste(&src, &dst, is_cut);
1399 if is_cut {
1400 self.active_window_mut().file_explorer_clipboard = None;
1401 self.set_status_message(t!("explorer.pasted_moved", name = &name).to_string());
1402 } else {
1403 self.set_status_message(t!("explorer.pasted", name = &name).to_string());
1404 }
1405 self.active_window_mut().key_context = KeyContext::FileExplorer;
1406 }
1407 PasteOpOutcome::SourceRemovalFailed {
1408 dst: landed_dst,
1409 err,
1410 } => {
1411 self.refresh_tree_after_paste(&src, &landed_dst, is_cut);
1416 self.set_status_message(
1417 t!(
1418 "explorer.move_source_removal_failed",
1419 name = &name,
1420 error = err.to_string()
1421 )
1422 .to_string(),
1423 );
1424 self.active_window_mut().key_context = KeyContext::FileExplorer;
1427 }
1428 PasteOpOutcome::Failed(e) => {
1429 let msg = if is_cut {
1430 t!("explorer.error_moving", error = e.to_string()).to_string()
1431 } else {
1432 t!("explorer.error_copying", error = e.to_string()).to_string()
1433 };
1434 self.set_status_message(msg);
1435 }
1436 }
1437 }
1438
1439 pub fn file_explorer_duplicate(&mut self) {
1445 let Some(explorer) = self.file_explorer() else {
1446 return;
1447 };
1448 let root_id = explorer.tree().root_id();
1449 let selected_ids = explorer.effective_selection();
1450 let sources: Vec<PathBuf> = selected_ids
1451 .iter()
1452 .filter(|&&id| id != root_id)
1453 .filter_map(|&id| explorer.tree().get_node(id).map(|n| n.entry.path.clone()))
1454 .collect();
1455
1456 if sources.is_empty() {
1457 self.set_status_message(t!("explorer.cannot_duplicate_root").to_string());
1458 return;
1459 }
1460
1461 let mut ops: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(sources.len());
1465 for src in &sources {
1466 let Some(parent) = src.parent() else {
1467 continue;
1468 };
1469 let Some(file_name) = src.file_name() else {
1470 continue;
1471 };
1472 let dst = unique_paste_name(
1473 &*self.authority.filesystem,
1474 parent,
1475 &file_name.to_string_lossy(),
1476 );
1477 ops.push((src.clone(), dst));
1478 }
1479
1480 if ops.is_empty() {
1481 return;
1482 }
1483
1484 let mut succeeded: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(ops.len());
1485 let mut first_error: Option<std::io::Error> = None;
1486 for (src, dst) in ops {
1487 match self.paste_one_fs_op(&src, &dst, false) {
1488 PasteOpOutcome::Ok => succeeded.push((src, dst)),
1489 PasteOpOutcome::SourceRemovalFailed { .. } => {
1490 unreachable!("paste_one_fs_op returned SourceRemovalFailed for a non-cut op");
1492 }
1493 PasteOpOutcome::Failed(e) => {
1494 if first_error.is_none() {
1495 first_error = Some(e);
1496 }
1497 }
1498 }
1499 }
1500
1501 if !succeeded.is_empty() {
1502 let (first_src, first_dst) = succeeded[0].clone();
1503 self.refresh_tree_after_paste(&first_src, &first_dst, false);
1504 }
1505
1506 let msg = if let Some(e) = &first_error {
1507 t!("explorer.error_copying", error = e.to_string()).to_string()
1508 } else if succeeded.len() == 1 {
1509 let name = succeeded[0]
1510 .1
1511 .file_name()
1512 .map(|n| n.to_string_lossy().to_string())
1513 .unwrap_or_default();
1514 t!("explorer.duplicated", name = &name).to_string()
1515 } else {
1516 t!("explorer.duplicated_n", count = succeeded.len()).to_string()
1517 };
1518 self.set_status_message(msg);
1519 self.active_window_mut().key_context = KeyContext::FileExplorer;
1520 }
1521
1522 pub fn file_explorer_copy_path(&mut self, relative: bool) {
1529 let Some(explorer) = self.file_explorer() else {
1530 return;
1531 };
1532 let selected_ids = explorer.effective_selection();
1533 let paths: Vec<PathBuf> = selected_ids
1534 .iter()
1535 .filter_map(|&id| explorer.tree().get_node(id).map(|n| n.entry.path.clone()))
1536 .collect();
1537
1538 if paths.is_empty() {
1539 self.set_status_message(t!("clipboard.no_file_path").to_string());
1540 return;
1541 }
1542
1543 let working_dir = self.working_dir().to_path_buf();
1544 let rendered: Vec<String> = paths
1545 .iter()
1546 .map(|p| {
1547 if relative {
1548 p.strip_prefix(&working_dir)
1549 .unwrap_or(p)
1550 .to_string_lossy()
1551 .into_owned()
1552 } else {
1553 p.to_string_lossy().into_owned()
1554 }
1555 })
1556 .collect();
1557
1558 let joined = rendered.join("\n");
1559 self.clipboard.copy(joined.clone());
1560
1561 let msg = if rendered.len() == 1 {
1562 t!("clipboard.copied_path", path = &rendered[0]).to_string()
1563 } else {
1564 t!("clipboard.copied_paths_n", count = rendered.len()).to_string()
1565 };
1566 self.set_status_message(msg);
1567 }
1568}
1569
1570impl crate::app::window::Window {
1571 pub(crate) fn init_file_explorer(&mut self) {
1578 let is_remote = self
1579 .resources
1580 .authority
1581 .filesystem
1582 .remote_connection_info()
1583 .is_some();
1584 let root_exists = self
1585 .resources
1586 .authority
1587 .filesystem
1588 .is_dir(&self.root)
1589 .unwrap_or(false);
1590 let root_path = if is_remote && !root_exists {
1591 match self.resources.authority.filesystem.home_dir() {
1592 Ok(home) => home,
1593 Err(e) => {
1594 tracing::error!("Failed to get remote home directory: {}", e);
1595 self.set_status_message(format!("Failed to get remote home: {}", e));
1596 return;
1597 }
1598 }
1599 } else {
1600 self.root.clone()
1601 };
1602
1603 let Some(runtime) = self.resources.tokio_runtime.clone() else {
1604 return;
1605 };
1606 let fs_manager = Arc::clone(&self.resources.fs_manager);
1607 let sender = self.bridge.sender();
1608 runtime.spawn(async move {
1609 match FileTree::new(root_path, fs_manager).await {
1610 Ok(mut tree) => {
1611 let root_id = tree.root_id();
1612 if let Err(e) = tree.expand_node(root_id).await {
1613 tracing::warn!("Failed to expand root directory: {}", e);
1614 }
1615 let view = FileTreeView::new(tree);
1616 #[allow(clippy::let_underscore_must_use)]
1618 let _ = sender.send(AsyncMessage::FileExplorerInitialized(view));
1619 }
1620 Err(e) => {
1621 tracing::error!("Failed to initialize file explorer: {}", e);
1622 }
1623 }
1624 });
1625 self.set_status_message(t!("explorer.initializing").to_string());
1626 }
1627
1628 pub fn focus_editor(&mut self) {
1631 self.key_context = KeyContext::Normal;
1632 self.set_status_message(t!("editor.focused").to_string());
1633 }
1634
1635 pub fn file_explorer_search_clear(&mut self) {
1642 if matches!(
1643 self.file_explorer_clipboard,
1644 Some(FileExplorerClipboard { is_cut: true, .. })
1645 ) {
1646 self.file_explorer_clipboard = None;
1647 self.set_status_message(t!("explorer.cut_cancelled").to_string());
1648 return;
1649 }
1650 let action = self.file_explorer.as_mut().map(|explorer| {
1651 if explorer.has_multi_selection() {
1652 explorer.clear_multi_selection();
1653 None
1654 } else if explorer.is_search_active() {
1655 explorer.search_clear();
1656 None
1657 } else {
1658 Some(())
1659 }
1660 });
1661 if let Some(Some(())) = action {
1662 self.focus_editor();
1663 }
1664 }
1665
1666 pub fn handle_set_file_explorer_decorations(
1671 &mut self,
1672 namespace: String,
1673 decorations: Vec<crate::view::file_tree::FileExplorerDecoration>,
1674 ) {
1675 let root = self.root.clone();
1676 let normalized: Vec<crate::view::file_tree::FileExplorerDecoration> = decorations
1677 .into_iter()
1678 .filter_map(|mut decoration| {
1679 let path = if decoration.path.is_absolute() {
1680 decoration.path
1681 } else {
1682 root.join(&decoration.path)
1683 };
1684 let path = crate::app::normalize_path(&path);
1685 if path.starts_with(&root) {
1686 decoration.path = path;
1687 Some(decoration)
1688 } else {
1689 None
1690 }
1691 })
1692 .collect();
1693
1694 self.file_explorer_decorations.insert(namespace, normalized);
1695 self.rebuild_file_explorer_decoration_cache();
1696 }
1697
1698 pub fn handle_clear_file_explorer_decorations(&mut self, namespace: &str) {
1701 self.file_explorer_decorations.remove(namespace);
1702 self.rebuild_file_explorer_decoration_cache();
1703 }
1704
1705 pub fn rebuild_file_explorer_decoration_cache(&mut self) {
1709 let decorations: Vec<_> = self
1710 .file_explorer_decorations
1711 .values()
1712 .flat_map(|entries| entries.iter().cloned())
1713 .collect();
1714
1715 let symlink_mappings = self
1716 .file_explorer
1717 .as_ref()
1718 .map(|fe| fe.collect_symlink_mappings())
1719 .unwrap_or_default();
1720
1721 self.file_explorer_decoration_cache =
1722 crate::view::file_tree::FileExplorerDecorationCache::rebuild(
1723 decorations.into_iter(),
1724 &self.root,
1725 &symlink_mappings,
1726 );
1727 }
1728
1729 pub fn file_explorer_clipboard(&self) -> Option<&FileExplorerClipboard> {
1731 self.file_explorer_clipboard.as_ref()
1732 }
1733
1734 pub fn file_explorer_copy(&mut self) {
1736 self.set_explorer_clipboard(false);
1737 }
1738
1739 pub fn file_explorer_cut(&mut self) {
1741 self.set_explorer_clipboard(true);
1742 }
1743
1744 fn set_explorer_clipboard(&mut self, is_cut: bool) {
1749 let Some(explorer) = self.file_explorer.as_ref() else {
1750 return;
1751 };
1752 let root_id = explorer.tree().root_id();
1753 let selected_ids = explorer.effective_selection();
1754 let paths: Vec<PathBuf> = selected_ids
1755 .iter()
1756 .filter(|&&id| id != root_id)
1757 .filter_map(|&id| explorer.tree().get_node(id).map(|n| n.entry.path.clone()))
1758 .collect();
1759 if paths.is_empty() {
1760 let msg = if is_cut {
1761 t!("explorer.cannot_cut_root").to_string()
1762 } else {
1763 t!("explorer.cannot_copy_root").to_string()
1764 };
1765 self.set_status_message(msg);
1766 return;
1767 }
1768 let msg = if paths.len() == 1 {
1769 let name = paths[0]
1770 .file_name()
1771 .unwrap_or_default()
1772 .to_string_lossy()
1773 .to_string();
1774 if is_cut {
1775 t!("explorer.cut", name = &name).to_string()
1776 } else {
1777 t!("explorer.copied", name = &name).to_string()
1778 }
1779 } else {
1780 let count = paths.len();
1781 if is_cut {
1782 t!("explorer.cut_n", count = count).to_string()
1783 } else {
1784 t!("explorer.copied_n", count = count).to_string()
1785 }
1786 };
1787 self.file_explorer_clipboard = Some(FileExplorerClipboard { paths, is_cut });
1788 self.set_status_message(msg);
1789 }
1790
1791 pub fn sync_file_explorer_to_active_file(&mut self) {
1796 if !self.file_explorer_visible {
1797 return;
1798 }
1799
1800 if self.file_explorer_sync_in_progress {
1802 return;
1803 }
1804
1805 let active_buf = self.active_buffer();
1806 let Some(metadata) = self.buffer_metadata.get(&active_buf) else {
1807 return;
1808 };
1809 let Some(file_path) = metadata.file_path() else {
1810 return;
1811 };
1812 let target_path = file_path.clone();
1813
1814 if !target_path.starts_with(&self.root) {
1815 return;
1816 }
1817
1818 let Some(mut view) = self.file_explorer.take() else {
1819 return;
1820 };
1821 tracing::trace!(
1822 "sync_file_explorer_to_active_file: taking file_explorer for async expand to {:?}",
1823 target_path
1824 );
1825 let runtime_handle = self
1826 .resources
1827 .tokio_runtime
1828 .as_ref()
1829 .map(|r| r.handle().clone());
1830 let sender = self.resources.async_bridge.as_ref().map(|b| b.sender());
1831 if let (Some(runtime), Some(sender)) = (runtime_handle, sender) {
1832 self.file_explorer_sync_in_progress = true;
1834
1835 runtime.spawn(async move {
1836 let _success = view.expand_and_select_file(&target_path).await;
1837 #[allow(clippy::let_underscore_must_use)]
1839 let _ = sender.send(
1840 crate::services::async_bridge::AsyncMessage::FileExplorerExpandedToPath(view),
1841 );
1842 });
1843 } else {
1844 self.file_explorer = Some(view);
1845 }
1846 }
1847}
1848
1849fn unique_paste_name(
1852 fs: &dyn crate::model::filesystem::FileSystem,
1853 dst_dir: &Path,
1854 name: &str,
1855) -> PathBuf {
1856 let (stem, ext) = split_stem_ext(name);
1857 let mut n = 1u32;
1858 loop {
1859 let candidate = if n == 1 {
1860 if ext.is_empty() {
1861 format!("{} copy", stem)
1862 } else {
1863 format!("{} copy.{}", stem, ext)
1864 }
1865 } else if ext.is_empty() {
1866 format!("{} copy {}", stem, n)
1867 } else {
1868 format!("{} copy {}.{}", stem, n, ext)
1869 };
1870 let path = dst_dir.join(&candidate);
1871 if !fs.exists(&path) {
1872 return path;
1873 }
1874 n += 1;
1875 if n > 1000 {
1876 return dst_dir.join(format!("{} copy {}", stem, timestamp_suffix()));
1878 }
1879 }
1880}
1881
1882pub(super) fn truncate_name_for_prompt(name: &str, max: usize) -> String {
1884 if name.chars().count() <= max {
1885 name.to_string()
1886 } else {
1887 let truncated: String = name.chars().take(max.saturating_sub(1)).collect();
1888 format!("{}\u{2026}", truncated)
1889 }
1890}
1891
1892pub(super) fn format_path_preview_for_prompt(paths: &[PathBuf], max_shown: usize) -> String {
1897 let names: Vec<String> = paths
1898 .iter()
1899 .map(|p| {
1900 let raw = p
1901 .file_name()
1902 .map(|n| n.to_string_lossy().to_string())
1903 .unwrap_or_default();
1904 format!("'{}'", truncate_name_for_prompt(&raw, 24))
1905 })
1906 .collect();
1907 if names.len() <= max_shown {
1908 names.join(", ")
1909 } else {
1910 let shown = names[..max_shown].join(", ");
1911 let more = names.len() - max_shown;
1912 format!("{}, \u{2026} ({} more)", shown, more)
1913 }
1914}
1915
1916fn split_stem_ext(name: &str) -> (&str, &str) {
1917 if let Some(dot_pos) = name.rfind('.') {
1919 if dot_pos > 0 {
1920 return (&name[..dot_pos], &name[dot_pos + 1..]);
1921 }
1922 }
1923 (name, "")
1924}