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) {
150 let root_path = if self.authority.filesystem.remote_connection_info().is_some()
155 && !self
156 .authority
157 .filesystem
158 .is_dir(&self.working_dir)
159 .unwrap_or(false)
160 {
161 match self.authority.filesystem.home_dir() {
162 Ok(home) => home,
163 Err(e) => {
164 tracing::error!("Failed to get remote home directory: {}", e);
165 self.set_status_message(format!("Failed to get remote home: {}", e));
166 return;
167 }
168 }
169 } else {
170 self.working_dir.clone()
171 };
172
173 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
174 let fs_manager = Arc::clone(&self.fs_manager);
175 let sender = bridge.sender();
176
177 runtime.spawn(async move {
178 match FileTree::new(root_path, fs_manager).await {
179 Ok(mut tree) => {
180 let root_id = tree.root_id();
181 if let Err(e) = tree.expand_node(root_id).await {
182 tracing::warn!("Failed to expand root directory: {}", e);
183 }
184
185 let view = FileTreeView::new(tree);
186 #[allow(clippy::let_underscore_must_use)]
188 let _ = sender.send(AsyncMessage::FileExplorerInitialized(view));
189 }
190 Err(e) => {
191 tracing::error!("Failed to initialize file explorer: {}", e);
192 }
193 }
194 });
195
196 self.set_status_message(t!("explorer.initializing").to_string());
197 }
198 }
199
200 pub fn file_explorer_navigate_up(&mut self) {
201 if let Some(explorer) = self.file_explorer_mut() {
202 explorer.select_prev_match();
203 explorer.update_scroll_for_selection();
204 }
205 self.file_explorer_preview_selected();
206 }
207
208 pub fn file_explorer_navigate_down(&mut self) {
209 if let Some(explorer) = self.file_explorer_mut() {
210 explorer.select_next_match();
211 explorer.update_scroll_for_selection();
212 }
213 self.file_explorer_preview_selected();
214 }
215
216 pub fn file_explorer_page_up(&mut self) {
217 if let Some(explorer) = self.file_explorer_mut() {
218 explorer.select_page_up();
219 explorer.update_scroll_for_selection();
220 }
221 self.file_explorer_preview_selected();
222 }
223
224 pub fn file_explorer_page_down(&mut self) {
225 if let Some(explorer) = self.file_explorer_mut() {
226 explorer.select_page_down();
227 explorer.update_scroll_for_selection();
228 }
229 self.file_explorer_preview_selected();
230 }
231
232 fn file_explorer_preview_selected(&mut self) {
240 if !self.config.file_explorer.preview_tabs {
243 return;
244 }
245
246 let path = match self
247 .file_explorer()
248 .as_ref()
249 .and_then(|explorer| explorer.get_selected_entry())
250 {
251 Some(entry) if !entry.is_dir() => entry.path.clone(),
252 _ => return,
253 };
254
255 if let Err(e) = self.open_file_preview(&path) {
256 tracing::debug!(
257 "file_explorer_preview_selected: skipping preview for {:?}: {}",
258 path,
259 e
260 );
261 }
262 }
263
264 pub fn file_explorer_collapse(&mut self) {
268 let Some(explorer) = self.file_explorer() else {
269 return;
270 };
271
272 let Some(selected_id) = explorer.get_selected() else {
273 return;
274 };
275
276 let Some(node) = explorer.tree().get_node(selected_id) else {
277 return;
278 };
279
280 if node.is_dir() && node.is_expanded() {
282 self.file_explorer_toggle_expand();
283 return;
284 }
285
286 if let Some(explorer) = self.file_explorer_mut() {
288 explorer.select_parent();
289 explorer.update_scroll_for_selection();
290 }
291 }
292
293 pub fn file_explorer_toggle_expand(&mut self) {
294 let selected_id = if let Some(explorer) = self.file_explorer() {
295 explorer.get_selected()
296 } else {
297 return;
298 };
299
300 let Some(selected_id) = selected_id else {
301 return;
302 };
303
304 let (is_dir, is_expanded, name) = if let Some(explorer) = self.file_explorer() {
305 let node = explorer.tree().get_node(selected_id);
306 if let Some(node) = node {
307 (node.is_dir(), node.is_expanded(), node.entry.name.clone())
308 } else {
309 return;
310 }
311 } else {
312 return;
313 };
314
315 if !is_dir {
316 return;
317 }
318
319 let status_msg = if is_expanded {
320 t!("explorer.collapsing").to_string()
321 } else {
322 t!("explorer.loading_dir", name = &name).to_string()
323 };
324 self.set_status_message(status_msg);
325
326 let active_id = self.active_window;
327 if let (Some(runtime), Some(explorer)) = (
332 self.tokio_runtime.as_ref(),
333 self.windows
334 .get_mut(&active_id)
335 .and_then(|w| w.file_explorer.as_mut()),
336 ) {
337 let result = runtime.block_on(explorer.toggle_with_chain(selected_id));
338
339 let final_name = explorer
340 .tree()
341 .get_node(selected_id)
342 .map(|n| n.entry.name.clone());
343 let final_expanded = explorer
344 .tree()
345 .get_node(selected_id)
346 .map(|n| n.is_expanded())
347 .unwrap_or(false);
348
349 let mut needs_decoration_rebuild = false;
351
352 match result {
353 Ok(()) => {
354 if final_expanded {
355 let node_info = explorer
356 .tree()
357 .get_node(selected_id)
358 .map(|n| (n.entry.path.clone(), n.entry.is_symlink()));
359
360 if let Some((dir_path, is_symlink)) = node_info {
361 crate::app::file_operations::load_gitignore_via_fs(
362 self.authority.filesystem.as_ref(),
363 explorer,
364 &dir_path,
365 );
366
367 if is_symlink {
371 tracing::debug!(
372 "Symlink directory expanded, will rebuild decoration cache: {:?}",
373 dir_path
374 );
375 needs_decoration_rebuild = true;
376 }
377 }
378 }
379
380 if let Some(name) = final_name {
381 let msg = if final_expanded {
382 t!("explorer.expanded", name = &name).to_string()
383 } else {
384 t!("explorer.collapsed", name = &name).to_string()
385 };
386 self.set_status_message(msg);
387 }
388 }
389 Err(e) => {
390 self.set_status_message(
391 t!("explorer.error", error = e.to_string()).to_string(),
392 );
393 }
394 }
395
396 if needs_decoration_rebuild {
398 self.active_window_mut()
399 .rebuild_file_explorer_decoration_cache();
400 }
401 }
402 }
403
404 pub fn file_explorer_open_file(&mut self) -> AnyhowResult<()> {
405 let entry_type = self
406 .file_explorer()
407 .as_ref()
408 .and_then(|explorer| explorer.get_selected_entry())
409 .map(|entry| (entry.is_dir(), entry.path.clone(), entry.name.clone()));
410
411 if let Some((is_dir, path, name)) = entry_type {
412 if is_dir {
413 self.file_explorer_toggle_expand();
414 } else {
415 tracing::info!("[SYNTAX DEBUG] file_explorer opening file: {:?}", path);
416 match self.open_file(&path) {
417 Ok(id) => {
418 self.active_window_mut().promote_buffer_from_preview(id);
422 self.set_status_message(
423 t!("explorer.opened_file", name = &name).to_string(),
424 );
425 self.active_window_mut().focus_editor();
426 }
427 Err(e) => {
428 if let Some(confirmation) =
431 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
432 {
433 self.start_large_file_encoding_confirmation(confirmation);
434 } else {
435 self.set_status_message(
436 t!("file.error_opening", error = e.to_string()).to_string(),
437 );
438 }
439 }
440 }
441 }
442 }
443 Ok(())
444 }
445
446 pub fn file_explorer_refresh(&mut self) {
447 let (selected_id, node_name) = if let Some(explorer) = self.file_explorer() {
448 if let Some(selected_id) = explorer.get_selected() {
449 let node_name = explorer
450 .tree()
451 .get_node(selected_id)
452 .map(|n| n.entry.name.clone());
453 (Some(selected_id), node_name)
454 } else {
455 (None, None)
456 }
457 } else {
458 return;
459 };
460
461 let Some(selected_id) = selected_id else {
462 return;
463 };
464
465 if let Some(name) = &node_name {
466 self.set_status_message(t!("explorer.refreshing", name = name).to_string());
467 }
468
469 let active_id = self.active_window;
470 if let (Some(runtime), Some(explorer)) = (
471 self.tokio_runtime.as_ref(),
472 self.windows
473 .get_mut(&active_id)
474 .and_then(|w| w.file_explorer.as_mut()),
475 ) {
476 let tree = explorer.tree_mut();
477 let result = runtime.block_on(tree.refresh_node(selected_id));
478 match result {
479 Ok(()) => {
480 if let Some(name) = node_name {
481 self.set_status_message(t!("explorer.refreshed", name = &name).to_string());
482 } else {
483 self.set_status_message(t!("explorer.refreshed_default").to_string());
484 }
485 }
486 Err(e) => {
487 self.set_status_message(
488 t!("explorer.error_refreshing", error = e.to_string()).to_string(),
489 );
490 }
491 }
492 }
493 }
494
495 pub fn file_explorer_new_file(&mut self) {
496 let active_id = self.active_window;
497 if let Some(explorer) = self
498 .windows
499 .get_mut(&active_id)
500 .and_then(|w| w.file_explorer.as_mut())
501 {
502 if let Some(selected_id) = explorer.get_selected() {
503 let node = explorer.tree().get_node(selected_id);
504 if let Some(node) = node {
505 let parent_path = get_parent_dir_path(node);
506 let filename = format!("untitled_{}.txt", timestamp_suffix());
507 let file_path = parent_path.join(&filename);
508
509 if let Some(runtime) = &self.tokio_runtime {
510 let path_clone = file_path.clone();
511 let result = self
512 .authority
513 .filesystem
514 .create_file(&path_clone)
515 .map(|_| ());
516
517 match result {
518 Ok(_) => {
519 let parent_id =
520 get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
521 let tree = explorer.tree_mut();
522 if let Err(e) =
523 runtime.block_on(tree.reload_expanded_node(parent_id))
524 {
525 tracing::warn!("Failed to refresh file tree: {}", e);
526 }
527 if let Some(explorer) = self.file_explorer_mut().as_mut() {
528 explorer.navigate_to_path(&path_clone);
529 }
530 self.set_status_message(
531 t!("explorer.created_file", name = &filename).to_string(),
532 );
533 self.notify_file_explorer_change(&path_clone);
534
535 if let Err(e) = self.open_file(&path_clone) {
537 tracing::warn!("Failed to open new file: {}", e);
538 }
539
540 let prompt = crate::view::prompt::Prompt::new(
541 t!("explorer.new_file_prompt").to_string(),
542 crate::view::prompt::PromptType::FileExplorerRename {
543 original_path: path_clone,
544 original_name: filename.clone(),
545 is_new_file: true,
546 },
547 );
548 self.active_window_mut().prompt = Some(prompt);
549 }
550 Err(e) => {
551 self.set_status_message(
552 t!("explorer.error_creating_file", error = e.to_string())
553 .to_string(),
554 );
555 }
556 }
557 }
558 }
559 }
560 }
561 }
562
563 pub fn file_explorer_new_directory(&mut self) {
564 let active_id = self.active_window;
565 if let Some(explorer) = self
566 .windows
567 .get_mut(&active_id)
568 .and_then(|w| w.file_explorer.as_mut())
569 {
570 if let Some(selected_id) = explorer.get_selected() {
571 let node = explorer.tree().get_node(selected_id);
572 if let Some(node) = node {
573 let parent_path = get_parent_dir_path(node);
574 let dirname = format!("New Folder {}", timestamp_suffix());
575 let dir_path = parent_path.join(&dirname);
576
577 if let Some(runtime) = &self.tokio_runtime {
578 let path_clone = dir_path.clone();
579 let dirname_clone = dirname.clone();
580 let result = self.authority.filesystem.create_dir(&path_clone);
581
582 match result {
583 Ok(_) => {
584 let parent_id =
585 get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
586 let tree = explorer.tree_mut();
587 if let Err(e) =
588 runtime.block_on(tree.reload_expanded_node(parent_id))
589 {
590 tracing::warn!("Failed to refresh file tree: {}", e);
591 }
592 if let Some(explorer) = self.file_explorer_mut().as_mut() {
593 explorer.navigate_to_path(&path_clone);
594 }
595 self.set_status_message(
596 t!("explorer.created_dir", name = &dirname_clone).to_string(),
597 );
598 self.notify_file_explorer_change(&path_clone);
599
600 let prompt = crate::view::prompt::Prompt::with_initial_text(
601 t!("explorer.new_directory_prompt").to_string(),
602 crate::view::prompt::PromptType::FileExplorerRename {
603 original_path: path_clone,
604 original_name: dirname_clone,
605 is_new_file: true,
606 },
607 dirname,
608 );
609 self.active_window_mut().prompt = Some(prompt);
610 }
611 Err(e) => {
612 self.set_status_message(
613 t!("explorer.error_creating_dir", error = e.to_string())
614 .to_string(),
615 );
616 }
617 }
618 }
619 }
620 }
621 }
622 }
623
624 pub fn file_explorer_delete(&mut self) {
625 let Some(explorer) = self.file_explorer() else {
626 return;
627 };
628 let root_id = explorer.tree().root_id();
629 let selected_ids = explorer.effective_selection();
630
631 let paths: Vec<(PathBuf, bool)> = selected_ids
632 .iter()
633 .filter(|&&id| id != root_id)
634 .filter_map(|&id| {
635 explorer
636 .tree()
637 .get_node(id)
638 .map(|n| (n.entry.path.clone(), n.is_dir()))
639 })
640 .collect();
641
642 if paths.is_empty() {
643 self.set_status_message(t!("explorer.cannot_delete_root").to_string());
644 return;
645 }
646
647 if paths.len() == 1 {
648 let (path, is_dir) = paths.into_iter().next().unwrap();
649 let name = path
650 .file_name()
651 .unwrap_or_default()
652 .to_string_lossy()
653 .to_string();
654 let type_str = if is_dir { "directory" } else { "file" };
655 self.start_prompt(
656 t!("explorer.delete_confirm", "type" = type_str, name = &name).to_string(),
657 PromptType::ConfirmDeleteFile { path, is_dir },
658 );
659 } else {
660 let count = paths.len();
661 let all_paths: Vec<PathBuf> = paths.into_iter().map(|(p, _)| p).collect();
662 let names = format_path_preview_for_prompt(&all_paths, 3);
666 self.start_prompt(
667 t!(
668 "explorer.delete_multi_confirm",
669 count = count,
670 names = &names
671 )
672 .to_string(),
673 PromptType::ConfirmMultiDelete { paths: all_paths },
674 );
675 }
676 }
677
678 pub fn perform_file_explorer_delete(&mut self, path: std::path::PathBuf, _is_dir: bool) {
682 let name = path
683 .file_name()
684 .map(|n| n.to_string_lossy().to_string())
685 .unwrap_or_default();
686
687 let delete_result = if self.authority.filesystem.remote_connection_info().is_some() {
690 self.move_to_remote_trash(&path)
691 } else {
692 trash::delete(&path).map_err(std::io::Error::other)
693 };
694
695 match delete_result {
696 Ok(_) => {
697 let to_close = self.buffer_ids_under_path(&path);
707 for id in to_close {
708 if let Err(e) = self.force_close_buffer(id) {
709 tracing::warn!(
710 "Failed to close buffer {:?} after delete of {:?}: {}",
711 id,
712 path,
713 e
714 );
715 }
716 }
717
718 let active_id = self.active_window;
720 if let Some(explorer) = self
721 .windows
722 .get_mut(&active_id)
723 .and_then(|w| w.file_explorer.as_mut())
724 {
725 if let Some(runtime) = &self.tokio_runtime {
726 if let Some(node) = explorer.tree().get_node_by_path(&path) {
728 let node_id = node.id;
729 let parent_id = get_parent_node_id(explorer.tree(), node_id, false);
730
731 let deleted_index = explorer.get_selected_index();
733
734 if let Err(e) = runtime
735 .block_on(explorer.tree_mut().reload_expanded_node(parent_id))
736 {
737 tracing::warn!("Failed to refresh file tree after delete: {}", e);
738 }
739
740 explorer.clear_multi_selection();
745
746 let count = explorer.visible_count();
749 if count > 0 {
750 let new_index = if let Some(idx) = deleted_index {
751 idx.min(count.saturating_sub(1))
752 } else {
753 0
754 };
755 if let Some(node_id) = explorer.get_node_at_index(new_index) {
756 explorer.set_selected(Some(node_id));
757 }
758 } else {
759 explorer.set_selected(Some(parent_id));
761 }
762 }
763 }
764 }
765 self.set_status_message(t!("explorer.moved_to_trash", name = &name).to_string());
766 self.notify_file_explorer_change(&path);
767
768 self.active_window_mut().key_context = KeyContext::FileExplorer;
770 }
771 Err(e) => {
772 self.set_status_message(
773 t!("explorer.error_trash", error = e.to_string()).to_string(),
774 );
775 }
776 }
777 }
778
779 fn move_to_remote_trash(&self, path: &std::path::Path) -> std::io::Result<()> {
781 let home = self.authority.filesystem.home_dir()?;
783 let trash_dir = home.join(".local/share/fresh/trash");
784
785 if !self.authority.filesystem.exists(&trash_dir) {
787 self.authority.filesystem.create_dir_all(&trash_dir)?;
788 }
789
790 let file_name = path
792 .file_name()
793 .unwrap_or_else(|| std::ffi::OsStr::new("unnamed"));
794 let timestamp = std::time::SystemTime::now()
795 .duration_since(std::time::UNIX_EPOCH)
796 .map(|d| d.as_secs())
797 .unwrap_or(0);
798 let trash_name = format!("{}.{}", file_name.to_string_lossy(), timestamp);
799 let trash_path = trash_dir.join(trash_name);
800
801 self.authority.filesystem.rename(path, &trash_path)
803 }
804
805 pub fn file_explorer_rename(&mut self) {
806 if let Some(explorer) = self.file_explorer() {
807 if let Some(selected_id) = explorer.get_selected() {
808 if selected_id == explorer.tree().root_id() {
810 self.set_status_message(t!("explorer.cannot_rename_root").to_string());
811 return;
812 }
813
814 let node = explorer.tree().get_node(selected_id);
815 if let Some(node) = node {
816 let old_path = node.entry.path.clone();
817 let old_name = node.entry.name.clone();
818
819 let prompt = crate::view::prompt::Prompt::with_initial_text_for_edit(
824 t!("explorer.rename_prompt").to_string(),
825 crate::view::prompt::PromptType::FileExplorerRename {
826 original_path: old_path,
827 original_name: old_name.clone(),
828 is_new_file: false,
829 },
830 old_name,
831 );
832 self.active_window_mut().prompt = Some(prompt);
833 }
834 }
835 }
836 }
837
838 pub fn perform_file_explorer_rename(
840 &mut self,
841 original_path: std::path::PathBuf,
842 original_name: String,
843 new_name: String,
844 is_new_file: bool,
845 ) {
846 if new_name.is_empty() || new_name == original_name {
847 self.set_status_message(t!("explorer.rename_cancelled").to_string());
848 return;
849 }
850
851 if new_name.chars().any(std::path::is_separator) {
856 self.set_status_message(t!("explorer.rename_invalid_separator").to_string());
857 return;
858 }
859 if new_name == "." || new_name == ".." {
860 self.set_status_message(t!("explorer.rename_invalid_dot").to_string());
861 return;
862 }
863
864 let new_path = original_path
865 .parent()
866 .map(|p| p.join(&new_name))
867 .unwrap_or_else(|| original_path.clone());
868
869 if self.tokio_runtime.is_some() {
870 let result = self.authority.filesystem.rename(&original_path, &new_path);
871
872 match result {
873 Ok(_) => {
874 let active_id = self.active_window;
878 if let (Some(runtime), Some(explorer)) = (
879 self.tokio_runtime.as_ref(),
880 self.windows
881 .get_mut(&active_id)
882 .and_then(|w| w.file_explorer.as_mut()),
883 ) {
884 if let Some(selected_id) = explorer.get_selected() {
885 let parent_id = get_parent_node_id(explorer.tree(), selected_id, false);
886 let tree = explorer.tree_mut();
887 if let Err(e) = runtime.block_on(tree.reload_expanded_node(parent_id)) {
888 tracing::warn!("Failed to refresh file tree after rename: {}", e);
889 }
890 }
891 explorer.clear_multi_selection();
895 explorer.navigate_to_path(&new_path);
897 }
898
899 let relocated = self.relocate_buffers_for_rename(&original_path, &new_path);
907
908 if is_new_file && !relocated.is_empty() {
912 self.active_window_mut().key_context = KeyContext::Normal;
913 }
914
915 self.set_status_message(
916 t!("explorer.renamed", old = &original_name, new = &new_name).to_string(),
917 );
918 self.notify_file_explorer_change(&new_path);
919 }
920 Err(e) => {
921 self.set_status_message(
922 t!("explorer.error_renaming", error = e.to_string()).to_string(),
923 );
924 }
925 }
926 }
927 }
928
929 pub fn file_explorer_toggle_hidden(&mut self) {
930 let show_hidden = if let Some(explorer) = self.file_explorer_mut() {
931 explorer.toggle_show_hidden();
932 explorer.ignore_patterns().show_hidden()
933 } else {
934 return;
935 };
936
937 let msg = if show_hidden {
938 t!("explorer.showing_hidden")
939 } else {
940 t!("explorer.hiding_hidden")
941 };
942 self.set_status_message(msg.to_string());
943
944 self.config_mut().file_explorer.show_hidden = show_hidden;
946 self.persist_config_change(
947 "/file_explorer/show_hidden",
948 serde_json::Value::Bool(show_hidden),
949 );
950 }
951
952 pub fn file_explorer_toggle_gitignored(&mut self) {
953 let show_gitignored = if let Some(explorer) = self.file_explorer_mut() {
954 explorer.toggle_show_gitignored();
955 explorer.ignore_patterns().show_gitignored()
956 } else {
957 return;
958 };
959
960 let msg = if show_gitignored {
961 t!("explorer.showing_gitignored")
962 } else {
963 t!("explorer.hiding_gitignored")
964 };
965 self.set_status_message(msg.to_string());
966
967 self.config_mut().file_explorer.show_gitignored = show_gitignored;
969 self.persist_config_change(
970 "/file_explorer/show_gitignored",
971 serde_json::Value::Bool(show_gitignored),
972 );
973 }
974
975 pub fn file_explorer_paste(&mut self) {
995 let clipboard = match self.active_window().file_explorer_clipboard.clone() {
996 Some(c) => c,
997 None => {
998 self.set_status_message(t!("explorer.paste_no_source").to_string());
999 return;
1000 }
1001 };
1002
1003 let dst_dir = if let Some(explorer) = self.file_explorer() {
1004 if let Some(selected_id) = explorer.get_selected() {
1005 if let Some(node) = explorer.tree().get_node(selected_id) {
1006 get_parent_dir_path(node)
1007 } else {
1008 return;
1009 }
1010 } else {
1011 return;
1012 }
1013 } else {
1014 return;
1015 };
1016
1017 let is_cut = clipboard.is_cut;
1018
1019 if clipboard.paths.len() == 1 {
1020 let src = clipboard.paths[0].clone();
1021 let file_name = match src.file_name() {
1022 Some(n) => n.to_os_string(),
1023 None => return,
1024 };
1025 let dst_path = dst_dir.join(&file_name);
1026
1027 if src.parent().map(|p| p == dst_dir).unwrap_or(false) {
1028 if is_cut {
1029 self.active_window_mut().file_explorer_clipboard = None;
1034 self.set_status_message(t!("explorer.cut_cancelled").to_string());
1035 return;
1036 } else {
1037 let unique = unique_paste_name(
1038 &*self.authority.filesystem,
1039 &dst_dir,
1040 &file_name.to_string_lossy(),
1041 );
1042 self.perform_file_explorer_paste(src, unique, false);
1043 return;
1044 }
1045 }
1046
1047 if self.authority.filesystem.exists(&dst_path) {
1048 let name = truncate_name_for_prompt(&file_name.to_string_lossy(), 40);
1049 self.start_prompt(
1050 t!("explorer.paste_conflict", name = &name).to_string(),
1051 crate::view::prompt::PromptType::ConfirmPasteConflict {
1052 src,
1053 dst: dst_path,
1054 is_cut,
1055 },
1056 );
1057 } else {
1058 self.perform_file_explorer_paste(src, dst_path, is_cut);
1059 }
1060 } else {
1061 let mut safe: Vec<(PathBuf, PathBuf)> = Vec::new();
1063 let mut conflicts: Vec<(PathBuf, PathBuf)> = Vec::new();
1064
1065 for src in &clipboard.paths {
1066 let file_name = match src.file_name() {
1067 Some(n) => n.to_os_string(),
1068 None => continue,
1069 };
1070 let dst_path = dst_dir.join(&file_name);
1071 let is_same_location = src.parent().map(|p| p == dst_dir).unwrap_or(false);
1072
1073 if is_same_location {
1074 if !is_cut {
1075 let unique = unique_paste_name(
1077 &*self.authority.filesystem,
1078 &dst_dir,
1079 &file_name.to_string_lossy(),
1080 );
1081 safe.push((src.clone(), unique));
1082 }
1083 } else if self.authority.filesystem.exists(&dst_path) {
1085 conflicts.push((src.clone(), dst_path));
1086 } else {
1087 safe.push((src.clone(), dst_path));
1088 }
1089 }
1090
1091 if safe.is_empty() && conflicts.is_empty() {
1092 if is_cut {
1096 self.active_window_mut().file_explorer_clipboard = None;
1097 self.set_status_message(t!("explorer.cut_cancelled").to_string());
1098 } else {
1099 self.set_status_message(t!("explorer.paste_same_location").to_string());
1100 }
1101 return;
1102 }
1103
1104 if conflicts.is_empty() {
1105 self.execute_resolved_multi_paste(safe, vec![], is_cut);
1106 } else {
1107 let name = truncate_name_for_prompt(
1108 &conflicts[0]
1109 .1
1110 .file_name()
1111 .unwrap_or_default()
1112 .to_string_lossy(),
1113 40,
1114 );
1115 self.start_prompt(
1116 t!("explorer.paste_conflict_multi", name = &name).to_string(),
1117 crate::view::prompt::PromptType::ConfirmMultiPasteConflict {
1118 safe,
1119 confirmed: Vec::new(),
1120 pending: conflicts,
1121 is_cut,
1122 },
1123 );
1124 }
1125 }
1126 }
1127
1128 pub(super) fn execute_resolved_multi_paste(
1136 &mut self,
1137 safe: Vec<(PathBuf, PathBuf)>,
1138 to_overwrite: Vec<(PathBuf, PathBuf)>,
1139 is_cut: bool,
1140 ) {
1141 let total = safe.len() + to_overwrite.len();
1142 if total == 0 {
1143 return;
1144 }
1145
1146 let mut succeeded: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(total);
1147 let mut clean_moves: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(total);
1154 let mut first_error: Option<std::io::Error> = None;
1155 let mut partial_moves: Vec<(PathBuf, std::io::Error)> = Vec::new();
1156 for (src, dst) in safe.into_iter().chain(to_overwrite) {
1157 match self.paste_one_fs_op(&src, &dst, is_cut) {
1158 PasteOpOutcome::Ok => {
1159 clean_moves.push((src.clone(), dst.clone()));
1160 succeeded.push((src, dst));
1161 }
1162 PasteOpOutcome::SourceRemovalFailed {
1163 dst: landed_dst,
1164 err,
1165 } => {
1166 succeeded.push((src, landed_dst.clone()));
1170 partial_moves.push((landed_dst, err));
1171 }
1172 PasteOpOutcome::Failed(e) => {
1173 if first_error.is_none() {
1174 first_error = Some(e);
1175 }
1176 }
1177 }
1178 }
1179
1180 if is_cut {
1186 for (src, dst) in &clean_moves {
1187 self.relocate_buffers_for_rename(src, dst);
1188 }
1189 }
1190
1191 if !succeeded.is_empty() {
1192 let first_dst = succeeded[0].1.clone();
1193 let any_src = succeeded[0].0.clone();
1194 self.refresh_tree_after_paste(&any_src, &first_dst, is_cut);
1195 }
1196
1197 if !partial_moves.is_empty() {
1198 let (first_dst, first_err) = &partial_moves[0];
1201 let name = first_dst
1202 .file_name()
1203 .map(|n| n.to_string_lossy().to_string())
1204 .unwrap_or_default();
1205 let msg = if partial_moves.len() == 1 {
1206 t!(
1207 "explorer.move_source_removal_failed",
1208 name = &name,
1209 error = first_err.to_string()
1210 )
1211 .to_string()
1212 } else {
1213 t!(
1214 "explorer.move_source_removal_failed_n",
1215 count = partial_moves.len()
1216 )
1217 .to_string()
1218 };
1219 self.set_status_message(msg);
1220 } else if let Some(e) = &first_error {
1221 let msg = if is_cut {
1222 t!("explorer.error_moving", error = e.to_string()).to_string()
1223 } else {
1224 t!("explorer.error_copying", error = e.to_string()).to_string()
1225 };
1226 self.set_status_message(msg);
1227 } else if total > 1 {
1228 let msg = if is_cut {
1229 t!("explorer.pasted_moved_n", count = total).to_string()
1230 } else {
1231 t!("explorer.pasted_n", count = total).to_string()
1232 };
1233 self.set_status_message(msg);
1234 } else if let Some((_, dst)) = succeeded.first() {
1235 let name = dst
1236 .file_name()
1237 .map(|n| n.to_string_lossy().to_string())
1238 .unwrap_or_default();
1239 let msg = if is_cut {
1240 t!("explorer.pasted_moved", name = &name).to_string()
1241 } else {
1242 t!("explorer.pasted", name = &name).to_string()
1243 };
1244 self.set_status_message(msg);
1245 }
1246
1247 if is_cut && first_error.is_none() && partial_moves.is_empty() {
1251 self.active_window_mut().file_explorer_clipboard = None;
1252 }
1253 self.active_window_mut().key_context = KeyContext::FileExplorer;
1254 }
1255
1256 fn paste_one_fs_op(&self, src: &Path, dst: &Path, is_cut: bool) -> PasteOpOutcome {
1260 let src_is_dir = self.authority.filesystem.is_dir(src).unwrap_or(false);
1261
1262 if src_is_dir && dst.starts_with(src) {
1270 return PasteOpOutcome::Failed(std::io::Error::new(
1271 std::io::ErrorKind::InvalidInput,
1272 "Cannot paste a directory into itself",
1273 ));
1274 }
1275
1276 if is_cut {
1277 match self.authority.filesystem.rename(src, dst) {
1282 Ok(()) => PasteOpOutcome::Ok,
1283 Err(e) if e.kind() == std::io::ErrorKind::CrossesDevices => {
1284 let copy_result = if src_is_dir {
1285 self.authority.filesystem.copy_dir_all(src, dst)
1286 } else {
1287 self.authority.filesystem.copy(src, dst).map(|_| ())
1288 };
1289 match copy_result {
1290 Ok(()) => {
1291 let remove_result = if src_is_dir {
1297 self.authority.filesystem.remove_dir_all(src)
1298 } else {
1299 self.authority.filesystem.remove_file(src)
1300 };
1301 match remove_result {
1302 Ok(()) => PasteOpOutcome::Ok,
1303 Err(remove_err) => PasteOpOutcome::SourceRemovalFailed {
1304 dst: dst.to_path_buf(),
1305 err: remove_err,
1306 },
1307 }
1308 }
1309 Err(copy_err) => {
1310 let cleanup = if src_is_dir {
1316 self.authority.filesystem.remove_dir_all(dst)
1317 } else {
1318 self.authority.filesystem.remove_file(dst)
1319 };
1320 if let Err(cleanup_err) = cleanup {
1321 tracing::warn!(
1322 "Failed to roll back partial destination {:?} after copy \
1323 fallback failed: {}",
1324 dst,
1325 cleanup_err
1326 );
1327 }
1328 PasteOpOutcome::Failed(copy_err)
1329 }
1330 }
1331 }
1332 Err(e) => PasteOpOutcome::Failed(e),
1333 }
1334 } else if src_is_dir {
1335 match self.authority.filesystem.copy_dir_all(src, dst) {
1336 Ok(()) => PasteOpOutcome::Ok,
1337 Err(e) => PasteOpOutcome::Failed(e),
1338 }
1339 } else {
1340 match self.authority.filesystem.copy(src, dst) {
1341 Ok(_) => PasteOpOutcome::Ok,
1342 Err(e) => PasteOpOutcome::Failed(e),
1343 }
1344 }
1345 }
1346
1347 fn refresh_tree_after_paste(&mut self, src: &Path, dst: &Path, is_cut: bool) {
1352 let active_id = self.active_window;
1353 let Some(explorer) = self
1356 .windows
1357 .get_mut(&active_id)
1358 .and_then(|w| w.file_explorer.as_mut())
1359 else {
1360 return;
1361 };
1362 if let Some(runtime) = &self.tokio_runtime {
1363 if let Some(dst_parent) = dst.parent() {
1365 if let Some(dst_parent_node) = explorer.tree().get_node_by_path(dst_parent) {
1366 let pid = dst_parent_node.id;
1367 if let Err(e) = runtime.block_on(explorer.tree_mut().reload_expanded_node(pid))
1368 {
1369 tracing::warn!("Failed to reload destination directory after paste: {}", e);
1370 }
1371 }
1372 }
1373 if is_cut {
1383 if let Some(src_parent) = src.parent() {
1384 if let Some(src_parent_node) = explorer.tree().get_node_by_path(src_parent) {
1385 let pid = src_parent_node.id;
1386 if let Err(e) =
1387 runtime.block_on(explorer.tree_mut().reload_expanded_node(pid))
1388 {
1389 tracing::warn!("Failed to refresh source directory after move: {}", e);
1390 }
1391 }
1392 }
1393 }
1394 }
1395 explorer.clear_multi_selection();
1400 explorer.navigate_to_path(dst);
1401
1402 self.notify_file_explorer_change(dst);
1403 }
1404
1405 pub(super) fn notify_file_explorer_change(&self, path: &Path) {
1417 self.plugin_manager.read().unwrap().run_hook(
1418 "after_file_explorer_change",
1419 crate::services::plugins::hooks::HookArgs::AfterFileExplorerChange {
1420 path: path.to_path_buf(),
1421 },
1422 );
1423 }
1424
1425 pub fn perform_file_explorer_paste(&mut self, src: PathBuf, dst: PathBuf, is_cut: bool) {
1426 let name = dst
1427 .file_name()
1428 .map(|n| n.to_string_lossy().to_string())
1429 .unwrap_or_default();
1430
1431 match self.paste_one_fs_op(&src, &dst, is_cut) {
1432 PasteOpOutcome::Ok => {
1433 if is_cut {
1440 self.relocate_buffers_for_rename(&src, &dst);
1441 }
1442 self.refresh_tree_after_paste(&src, &dst, is_cut);
1443 if is_cut {
1444 self.active_window_mut().file_explorer_clipboard = None;
1445 self.set_status_message(t!("explorer.pasted_moved", name = &name).to_string());
1446 } else {
1447 self.set_status_message(t!("explorer.pasted", name = &name).to_string());
1448 }
1449 self.active_window_mut().key_context = KeyContext::FileExplorer;
1450 }
1451 PasteOpOutcome::SourceRemovalFailed {
1452 dst: landed_dst,
1453 err,
1454 } => {
1455 self.refresh_tree_after_paste(&src, &landed_dst, is_cut);
1460 self.set_status_message(
1461 t!(
1462 "explorer.move_source_removal_failed",
1463 name = &name,
1464 error = err.to_string()
1465 )
1466 .to_string(),
1467 );
1468 self.active_window_mut().key_context = KeyContext::FileExplorer;
1471 }
1472 PasteOpOutcome::Failed(e) => {
1473 let msg = if is_cut {
1474 t!("explorer.error_moving", error = e.to_string()).to_string()
1475 } else {
1476 t!("explorer.error_copying", error = e.to_string()).to_string()
1477 };
1478 self.set_status_message(msg);
1479 }
1480 }
1481 }
1482
1483 pub fn file_explorer_duplicate(&mut self) {
1489 let Some(explorer) = self.file_explorer() else {
1490 return;
1491 };
1492 let root_id = explorer.tree().root_id();
1493 let selected_ids = explorer.effective_selection();
1494 let sources: Vec<PathBuf> = selected_ids
1495 .iter()
1496 .filter(|&&id| id != root_id)
1497 .filter_map(|&id| explorer.tree().get_node(id).map(|n| n.entry.path.clone()))
1498 .collect();
1499
1500 if sources.is_empty() {
1501 self.set_status_message(t!("explorer.cannot_duplicate_root").to_string());
1502 return;
1503 }
1504
1505 let mut ops: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(sources.len());
1509 for src in &sources {
1510 let Some(parent) = src.parent() else {
1511 continue;
1512 };
1513 let Some(file_name) = src.file_name() else {
1514 continue;
1515 };
1516 let dst = unique_paste_name(
1517 &*self.authority.filesystem,
1518 parent,
1519 &file_name.to_string_lossy(),
1520 );
1521 ops.push((src.clone(), dst));
1522 }
1523
1524 if ops.is_empty() {
1525 return;
1526 }
1527
1528 let mut succeeded: Vec<(PathBuf, PathBuf)> = Vec::with_capacity(ops.len());
1529 let mut first_error: Option<std::io::Error> = None;
1530 for (src, dst) in ops {
1531 match self.paste_one_fs_op(&src, &dst, false) {
1532 PasteOpOutcome::Ok => succeeded.push((src, dst)),
1533 PasteOpOutcome::SourceRemovalFailed { .. } => {
1534 unreachable!("paste_one_fs_op returned SourceRemovalFailed for a non-cut op");
1536 }
1537 PasteOpOutcome::Failed(e) => {
1538 if first_error.is_none() {
1539 first_error = Some(e);
1540 }
1541 }
1542 }
1543 }
1544
1545 if !succeeded.is_empty() {
1546 let (first_src, first_dst) = succeeded[0].clone();
1547 self.refresh_tree_after_paste(&first_src, &first_dst, false);
1548 }
1549
1550 let msg = if let Some(e) = &first_error {
1551 t!("explorer.error_copying", error = e.to_string()).to_string()
1552 } else if succeeded.len() == 1 {
1553 let name = succeeded[0]
1554 .1
1555 .file_name()
1556 .map(|n| n.to_string_lossy().to_string())
1557 .unwrap_or_default();
1558 t!("explorer.duplicated", name = &name).to_string()
1559 } else {
1560 t!("explorer.duplicated_n", count = succeeded.len()).to_string()
1561 };
1562 self.set_status_message(msg);
1563 self.active_window_mut().key_context = KeyContext::FileExplorer;
1564 }
1565
1566 pub fn file_explorer_copy_path(&mut self, relative: bool) {
1573 let Some(explorer) = self.file_explorer() else {
1574 return;
1575 };
1576 let selected_ids = explorer.effective_selection();
1577 let paths: Vec<PathBuf> = selected_ids
1578 .iter()
1579 .filter_map(|&id| explorer.tree().get_node(id).map(|n| n.entry.path.clone()))
1580 .collect();
1581
1582 if paths.is_empty() {
1583 self.set_status_message(t!("clipboard.no_file_path").to_string());
1584 return;
1585 }
1586
1587 let working_dir = self.working_dir.clone();
1588 let rendered: Vec<String> = paths
1589 .iter()
1590 .map(|p| {
1591 if relative {
1592 p.strip_prefix(&working_dir)
1593 .unwrap_or(p)
1594 .to_string_lossy()
1595 .into_owned()
1596 } else {
1597 p.to_string_lossy().into_owned()
1598 }
1599 })
1600 .collect();
1601
1602 let joined = rendered.join("\n");
1603 self.clipboard.copy(joined.clone());
1604
1605 let msg = if rendered.len() == 1 {
1606 t!("clipboard.copied_path", path = &rendered[0]).to_string()
1607 } else {
1608 t!("clipboard.copied_paths_n", count = rendered.len()).to_string()
1609 };
1610 self.set_status_message(msg);
1611 }
1612}
1613
1614impl crate::app::window::Window {
1615 pub fn focus_editor(&mut self) {
1618 self.key_context = KeyContext::Normal;
1619 self.set_status_message(t!("editor.focused").to_string());
1620 }
1621
1622 pub fn file_explorer_search_clear(&mut self) {
1629 if matches!(
1630 self.file_explorer_clipboard,
1631 Some(FileExplorerClipboard { is_cut: true, .. })
1632 ) {
1633 self.file_explorer_clipboard = None;
1634 self.set_status_message(t!("explorer.cut_cancelled").to_string());
1635 return;
1636 }
1637 let action = self.file_explorer.as_mut().map(|explorer| {
1638 if explorer.has_multi_selection() {
1639 explorer.clear_multi_selection();
1640 None
1641 } else if explorer.is_search_active() {
1642 explorer.search_clear();
1643 None
1644 } else {
1645 Some(())
1646 }
1647 });
1648 if let Some(Some(())) = action {
1649 self.focus_editor();
1650 }
1651 }
1652
1653 pub fn handle_set_file_explorer_decorations(
1658 &mut self,
1659 namespace: String,
1660 decorations: Vec<crate::view::file_tree::FileExplorerDecoration>,
1661 ) {
1662 let root = self.root.clone();
1663 let normalized: Vec<crate::view::file_tree::FileExplorerDecoration> = decorations
1664 .into_iter()
1665 .filter_map(|mut decoration| {
1666 let path = if decoration.path.is_absolute() {
1667 decoration.path
1668 } else {
1669 root.join(&decoration.path)
1670 };
1671 let path = crate::app::normalize_path(&path);
1672 if path.starts_with(&root) {
1673 decoration.path = path;
1674 Some(decoration)
1675 } else {
1676 None
1677 }
1678 })
1679 .collect();
1680
1681 self.file_explorer_decorations.insert(namespace, normalized);
1682 self.rebuild_file_explorer_decoration_cache();
1683 }
1684
1685 pub fn handle_clear_file_explorer_decorations(&mut self, namespace: &str) {
1688 self.file_explorer_decorations.remove(namespace);
1689 self.rebuild_file_explorer_decoration_cache();
1690 }
1691
1692 pub fn rebuild_file_explorer_decoration_cache(&mut self) {
1696 let decorations: Vec<_> = self
1697 .file_explorer_decorations
1698 .values()
1699 .flat_map(|entries| entries.iter().cloned())
1700 .collect();
1701
1702 let symlink_mappings = self
1703 .file_explorer
1704 .as_ref()
1705 .map(|fe| fe.collect_symlink_mappings())
1706 .unwrap_or_default();
1707
1708 self.file_explorer_decoration_cache =
1709 crate::view::file_tree::FileExplorerDecorationCache::rebuild(
1710 decorations.into_iter(),
1711 &self.root,
1712 &symlink_mappings,
1713 );
1714 }
1715
1716 pub fn file_explorer_clipboard(&self) -> Option<&FileExplorerClipboard> {
1718 self.file_explorer_clipboard.as_ref()
1719 }
1720
1721 pub fn file_explorer_copy(&mut self) {
1723 self.set_explorer_clipboard(false);
1724 }
1725
1726 pub fn file_explorer_cut(&mut self) {
1728 self.set_explorer_clipboard(true);
1729 }
1730
1731 fn set_explorer_clipboard(&mut self, is_cut: bool) {
1736 let Some(explorer) = self.file_explorer.as_ref() else {
1737 return;
1738 };
1739 let root_id = explorer.tree().root_id();
1740 let selected_ids = explorer.effective_selection();
1741 let paths: Vec<PathBuf> = selected_ids
1742 .iter()
1743 .filter(|&&id| id != root_id)
1744 .filter_map(|&id| explorer.tree().get_node(id).map(|n| n.entry.path.clone()))
1745 .collect();
1746 if paths.is_empty() {
1747 let msg = if is_cut {
1748 t!("explorer.cannot_cut_root").to_string()
1749 } else {
1750 t!("explorer.cannot_copy_root").to_string()
1751 };
1752 self.set_status_message(msg);
1753 return;
1754 }
1755 let msg = if paths.len() == 1 {
1756 let name = paths[0]
1757 .file_name()
1758 .unwrap_or_default()
1759 .to_string_lossy()
1760 .to_string();
1761 if is_cut {
1762 t!("explorer.cut", name = &name).to_string()
1763 } else {
1764 t!("explorer.copied", name = &name).to_string()
1765 }
1766 } else {
1767 let count = paths.len();
1768 if is_cut {
1769 t!("explorer.cut_n", count = count).to_string()
1770 } else {
1771 t!("explorer.copied_n", count = count).to_string()
1772 }
1773 };
1774 self.file_explorer_clipboard = Some(FileExplorerClipboard { paths, is_cut });
1775 self.set_status_message(msg);
1776 }
1777
1778 pub fn sync_file_explorer_to_active_file(&mut self) {
1783 if !self.file_explorer_visible {
1784 return;
1785 }
1786
1787 if self.file_explorer_sync_in_progress {
1789 return;
1790 }
1791
1792 let active_buf = self.active_buffer();
1793 let Some(metadata) = self.buffer_metadata.get(&active_buf) else {
1794 return;
1795 };
1796 let Some(file_path) = metadata.file_path() else {
1797 return;
1798 };
1799 let target_path = file_path.clone();
1800
1801 if !target_path.starts_with(&self.root) {
1802 return;
1803 }
1804
1805 let Some(mut view) = self.file_explorer.take() else {
1806 return;
1807 };
1808 tracing::trace!(
1809 "sync_file_explorer_to_active_file: taking file_explorer for async expand to {:?}",
1810 target_path
1811 );
1812 let runtime_handle = self
1813 .resources
1814 .tokio_runtime
1815 .as_ref()
1816 .map(|r| r.handle().clone());
1817 let sender = self.resources.async_bridge.as_ref().map(|b| b.sender());
1818 if let (Some(runtime), Some(sender)) = (runtime_handle, sender) {
1819 self.file_explorer_sync_in_progress = true;
1821
1822 runtime.spawn(async move {
1823 let _success = view.expand_and_select_file(&target_path).await;
1824 #[allow(clippy::let_underscore_must_use)]
1826 let _ = sender.send(
1827 crate::services::async_bridge::AsyncMessage::FileExplorerExpandedToPath(view),
1828 );
1829 });
1830 } else {
1831 self.file_explorer = Some(view);
1832 }
1833 }
1834}
1835
1836fn unique_paste_name(
1839 fs: &dyn crate::model::filesystem::FileSystem,
1840 dst_dir: &Path,
1841 name: &str,
1842) -> PathBuf {
1843 let (stem, ext) = split_stem_ext(name);
1844 let mut n = 1u32;
1845 loop {
1846 let candidate = if n == 1 {
1847 if ext.is_empty() {
1848 format!("{} copy", stem)
1849 } else {
1850 format!("{} copy.{}", stem, ext)
1851 }
1852 } else if ext.is_empty() {
1853 format!("{} copy {}", stem, n)
1854 } else {
1855 format!("{} copy {}.{}", stem, n, ext)
1856 };
1857 let path = dst_dir.join(&candidate);
1858 if !fs.exists(&path) {
1859 return path;
1860 }
1861 n += 1;
1862 if n > 1000 {
1863 return dst_dir.join(format!("{} copy {}", stem, timestamp_suffix()));
1865 }
1866 }
1867}
1868
1869pub(super) fn truncate_name_for_prompt(name: &str, max: usize) -> String {
1871 if name.chars().count() <= max {
1872 name.to_string()
1873 } else {
1874 let truncated: String = name.chars().take(max.saturating_sub(1)).collect();
1875 format!("{}\u{2026}", truncated)
1876 }
1877}
1878
1879pub(super) fn format_path_preview_for_prompt(paths: &[PathBuf], max_shown: usize) -> String {
1884 let names: Vec<String> = paths
1885 .iter()
1886 .map(|p| {
1887 let raw = p
1888 .file_name()
1889 .map(|n| n.to_string_lossy().to_string())
1890 .unwrap_or_default();
1891 format!("'{}'", truncate_name_for_prompt(&raw, 24))
1892 })
1893 .collect();
1894 if names.len() <= max_shown {
1895 names.join(", ")
1896 } else {
1897 let shown = names[..max_shown].join(", ");
1898 let more = names.len() - max_shown;
1899 format!("{}, \u{2026} ({} more)", shown, more)
1900 }
1901}
1902
1903fn split_stem_ext(name: &str) -> (&str, &str) {
1904 if let Some(dot_pos) = name.rfind('.') {
1906 if dot_pos > 0 {
1907 return (&name[..dot_pos], &name[dot_pos + 1..]);
1908 }
1909 }
1910 (name, "")
1911}