1use anyhow::Result as AnyhowResult;
2use rust_i18n::t;
3
4use super::*;
5use crate::view::file_tree::TreeNode;
6use std::path::PathBuf;
7
8fn get_parent_dir_path(node: &TreeNode) -> PathBuf {
11 if node.is_dir() {
12 node.entry.path.clone()
13 } else {
14 node.entry
15 .path
16 .parent()
17 .map(|p| p.to_path_buf())
18 .unwrap_or_else(|| node.entry.path.clone())
19 }
20}
21
22fn timestamp_suffix() -> u64 {
24 std::time::SystemTime::now()
25 .duration_since(std::time::UNIX_EPOCH)
26 .unwrap()
27 .as_secs()
28}
29
30fn get_parent_node_id(
33 tree: &crate::view::file_tree::FileTree,
34 selected_id: crate::view::file_tree::NodeId,
35 node_is_dir: bool,
36) -> crate::view::file_tree::NodeId {
37 if node_is_dir {
38 selected_id
39 } else {
40 tree.get_node(selected_id)
41 .and_then(|n| n.parent)
42 .unwrap_or(selected_id)
43 }
44}
45
46impl Editor {
47 pub fn file_explorer_visible(&self) -> bool {
48 self.file_explorer_visible
49 }
50
51 pub fn file_explorer(&self) -> Option<&FileTreeView> {
52 self.file_explorer.as_ref()
53 }
54
55 pub fn toggle_file_explorer(&mut self) {
56 self.file_explorer_visible = !self.file_explorer_visible;
57
58 if self.file_explorer_visible {
59 if self.file_explorer.is_none() {
60 self.init_file_explorer();
61 }
62 self.key_context = KeyContext::FileExplorer;
63 self.set_status_message(t!("explorer.opened").to_string());
64 self.sync_file_explorer_to_active_file();
65 } else {
66 self.key_context = KeyContext::Normal;
67 self.set_status_message(t!("explorer.closed").to_string());
68 }
69
70 self.plugin_manager.run_hook(
72 "resize",
73 fresh_core::hooks::HookArgs::Resize {
74 width: self.terminal_width,
75 height: self.terminal_height,
76 },
77 );
78 }
79
80 pub fn show_file_explorer(&mut self) {
81 if !self.file_explorer_visible {
82 self.toggle_file_explorer();
83 }
84 }
85
86 pub fn sync_file_explorer_to_active_file(&mut self) {
87 if !self.file_explorer_visible {
88 return;
89 }
90
91 if self.file_explorer_sync_in_progress {
93 return;
94 }
95
96 if let Some(metadata) = self.buffer_metadata.get(&self.active_buffer()) {
97 if let Some(file_path) = metadata.file_path() {
98 let target_path = file_path.clone();
99 let working_dir = self.working_dir.clone();
100
101 if target_path.starts_with(&working_dir) {
102 if let Some(mut view) = self.file_explorer.take() {
103 tracing::trace!(
104 "sync_file_explorer_to_active_file: taking file_explorer for async expand to {:?}",
105 target_path
106 );
107 if let (Some(runtime), Some(bridge)) =
108 (&self.tokio_runtime, &self.async_bridge)
109 {
110 let sender = bridge.sender();
111 self.file_explorer_sync_in_progress = true;
113
114 runtime.spawn(async move {
115 let _success = view.expand_and_select_file(&target_path).await;
116 #[allow(clippy::let_underscore_must_use)]
118 let _ = sender.send(AsyncMessage::FileExplorerExpandedToPath(view));
119 });
120 } else {
121 self.file_explorer = Some(view);
122 }
123 }
124 }
125 }
126 }
127 }
128
129 pub fn focus_file_explorer(&mut self) {
130 if self.file_explorer_visible {
131 self.on_editor_focus_lost();
133
134 self.cancel_search_prompt_if_active();
136
137 self.key_context = KeyContext::FileExplorer;
138 self.set_status_message(t!("explorer.focused").to_string());
139 self.sync_file_explorer_to_active_file();
140 } else {
141 self.toggle_file_explorer();
142 }
143 }
144
145 pub fn focus_editor(&mut self) {
146 self.key_context = KeyContext::Normal;
147 self.set_status_message(t!("editor.focused").to_string());
148 }
149
150 pub(crate) fn init_file_explorer(&mut self) {
151 let root_path = if self.filesystem.remote_connection_info().is_some()
156 && !self.filesystem.is_dir(&self.working_dir).unwrap_or(false)
157 {
158 match self.filesystem.home_dir() {
159 Ok(home) => home,
160 Err(e) => {
161 tracing::error!("Failed to get remote home directory: {}", e);
162 self.set_status_message(format!("Failed to get remote home: {}", e));
163 return;
164 }
165 }
166 } else {
167 self.working_dir.clone()
168 };
169
170 if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
171 let fs_manager = Arc::clone(&self.fs_manager);
172 let sender = bridge.sender();
173
174 runtime.spawn(async move {
175 match FileTree::new(root_path, fs_manager).await {
176 Ok(mut tree) => {
177 let root_id = tree.root_id();
178 if let Err(e) = tree.expand_node(root_id).await {
179 tracing::warn!("Failed to expand root directory: {}", e);
180 }
181
182 let view = FileTreeView::new(tree);
183 #[allow(clippy::let_underscore_must_use)]
185 let _ = sender.send(AsyncMessage::FileExplorerInitialized(view));
186 }
187 Err(e) => {
188 tracing::error!("Failed to initialize file explorer: {}", e);
189 }
190 }
191 });
192
193 self.set_status_message(t!("explorer.initializing").to_string());
194 }
195 }
196
197 pub fn file_explorer_navigate_up(&mut self) {
198 if let Some(explorer) = &mut self.file_explorer {
199 explorer.select_prev_match();
200 explorer.update_scroll_for_selection();
201 }
202 }
203
204 pub fn file_explorer_navigate_down(&mut self) {
205 if let Some(explorer) = &mut self.file_explorer {
206 explorer.select_next_match();
207 explorer.update_scroll_for_selection();
208 }
209 }
210
211 pub fn file_explorer_page_up(&mut self) {
212 if let Some(explorer) = &mut self.file_explorer {
213 explorer.select_page_up();
214 explorer.update_scroll_for_selection();
215 }
216 }
217
218 pub fn file_explorer_page_down(&mut self) {
219 if let Some(explorer) = &mut self.file_explorer {
220 explorer.select_page_down();
221 explorer.update_scroll_for_selection();
222 }
223 }
224
225 pub fn file_explorer_collapse(&mut self) {
229 let Some(explorer) = &self.file_explorer else {
230 return;
231 };
232
233 let Some(selected_id) = explorer.get_selected() else {
234 return;
235 };
236
237 let Some(node) = explorer.tree().get_node(selected_id) else {
238 return;
239 };
240
241 if node.is_dir() && node.is_expanded() {
243 self.file_explorer_toggle_expand();
244 return;
245 }
246
247 if let Some(explorer) = &mut self.file_explorer {
249 explorer.select_parent();
250 explorer.update_scroll_for_selection();
251 }
252 }
253
254 pub fn file_explorer_toggle_expand(&mut self) {
255 let selected_id = if let Some(explorer) = &self.file_explorer {
256 explorer.get_selected()
257 } else {
258 return;
259 };
260
261 let Some(selected_id) = selected_id else {
262 return;
263 };
264
265 let (is_dir, is_expanded, name) = if let Some(explorer) = &self.file_explorer {
266 let node = explorer.tree().get_node(selected_id);
267 if let Some(node) = node {
268 (node.is_dir(), node.is_expanded(), node.entry.name.clone())
269 } else {
270 return;
271 }
272 } else {
273 return;
274 };
275
276 if !is_dir {
277 return;
278 }
279
280 let status_msg = if is_expanded {
281 t!("explorer.collapsing").to_string()
282 } else {
283 t!("explorer.loading_dir", name = &name).to_string()
284 };
285 self.set_status_message(status_msg);
286
287 if let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) {
288 let tree = explorer.tree_mut();
289 let result = runtime.block_on(tree.toggle_node(selected_id));
290
291 let final_name = explorer
292 .tree()
293 .get_node(selected_id)
294 .map(|n| n.entry.name.clone());
295 let final_expanded = explorer
296 .tree()
297 .get_node(selected_id)
298 .map(|n| n.is_expanded())
299 .unwrap_or(false);
300
301 let mut needs_decoration_rebuild = false;
303
304 match result {
305 Ok(()) => {
306 if final_expanded {
307 let node_info = explorer
308 .tree()
309 .get_node(selected_id)
310 .map(|n| (n.entry.path.clone(), n.entry.is_symlink()));
311
312 if let Some((dir_path, is_symlink)) = node_info {
313 if let Err(e) = explorer.load_gitignore_for_dir(&dir_path) {
314 tracing::warn!(
315 "Failed to load .gitignore from {:?}: {}",
316 dir_path,
317 e
318 );
319 }
320
321 if is_symlink {
325 tracing::debug!(
326 "Symlink directory expanded, will rebuild decoration cache: {:?}",
327 dir_path
328 );
329 needs_decoration_rebuild = true;
330 }
331 }
332 }
333
334 if let Some(name) = final_name {
335 let msg = if final_expanded {
336 t!("explorer.expanded", name = &name).to_string()
337 } else {
338 t!("explorer.collapsed", name = &name).to_string()
339 };
340 self.set_status_message(msg);
341 }
342 }
343 Err(e) => {
344 self.set_status_message(
345 t!("explorer.error", error = e.to_string()).to_string(),
346 );
347 }
348 }
349
350 if needs_decoration_rebuild {
352 self.rebuild_file_explorer_decoration_cache();
353 }
354 }
355 }
356
357 pub fn file_explorer_open_file(&mut self) -> AnyhowResult<()> {
358 let entry_type = self
359 .file_explorer
360 .as_ref()
361 .and_then(|explorer| explorer.get_selected_entry())
362 .map(|entry| (entry.is_dir(), entry.path.clone(), entry.name.clone()));
363
364 if let Some((is_dir, path, name)) = entry_type {
365 if is_dir {
366 self.file_explorer_toggle_expand();
367 } else {
368 tracing::info!("[SYNTAX DEBUG] file_explorer opening file: {:?}", path);
369 match self.open_file(&path) {
370 Ok(id) => {
371 self.promote_buffer_from_preview(id);
375 self.set_status_message(
376 t!("explorer.opened_file", name = &name).to_string(),
377 );
378 self.focus_editor();
379 }
380 Err(e) => {
381 if let Some(confirmation) =
384 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
385 {
386 self.start_large_file_encoding_confirmation(confirmation);
387 } else {
388 self.set_status_message(
389 t!("file.error_opening", error = e.to_string()).to_string(),
390 );
391 }
392 }
393 }
394 }
395 }
396 Ok(())
397 }
398
399 pub fn file_explorer_refresh(&mut self) {
400 let (selected_id, node_name) = if let Some(explorer) = &self.file_explorer {
401 if let Some(selected_id) = explorer.get_selected() {
402 let node_name = explorer
403 .tree()
404 .get_node(selected_id)
405 .map(|n| n.entry.name.clone());
406 (Some(selected_id), node_name)
407 } else {
408 (None, None)
409 }
410 } else {
411 return;
412 };
413
414 let Some(selected_id) = selected_id else {
415 return;
416 };
417
418 if let Some(name) = &node_name {
419 self.set_status_message(t!("explorer.refreshing", name = name).to_string());
420 }
421
422 if let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) {
423 let tree = explorer.tree_mut();
424 let result = runtime.block_on(tree.refresh_node(selected_id));
425 match result {
426 Ok(()) => {
427 if let Some(name) = node_name {
428 self.set_status_message(t!("explorer.refreshed", name = &name).to_string());
429 } else {
430 self.set_status_message(t!("explorer.refreshed_default").to_string());
431 }
432 }
433 Err(e) => {
434 self.set_status_message(
435 t!("explorer.error_refreshing", error = e.to_string()).to_string(),
436 );
437 }
438 }
439 }
440 }
441
442 pub fn file_explorer_new_file(&mut self) {
443 if let Some(explorer) = &mut self.file_explorer {
444 if let Some(selected_id) = explorer.get_selected() {
445 let node = explorer.tree().get_node(selected_id);
446 if let Some(node) = node {
447 let parent_path = get_parent_dir_path(node);
448 let filename = format!("untitled_{}.txt", timestamp_suffix());
449 let file_path = parent_path.join(&filename);
450
451 if let Some(runtime) = &self.tokio_runtime {
452 let path_clone = file_path.clone();
453 let result = self.filesystem.create_file(&path_clone).map(|_| ());
454
455 match result {
456 Ok(_) => {
457 let parent_id =
458 get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
459 let tree = explorer.tree_mut();
460 if let Err(e) = runtime.block_on(tree.refresh_node(parent_id)) {
461 tracing::warn!("Failed to refresh file tree: {}", e);
462 }
463 self.set_status_message(
464 t!("explorer.created_file", name = &filename).to_string(),
465 );
466
467 if let Err(e) = self.open_file(&path_clone) {
469 tracing::warn!("Failed to open new file: {}", e);
470 }
471
472 let prompt = crate::view::prompt::Prompt::new(
475 t!("explorer.rename_prompt").to_string(),
476 crate::view::prompt::PromptType::FileExplorerRename {
477 original_path: path_clone,
478 original_name: filename.clone(),
479 is_new_file: true,
480 },
481 );
482 self.prompt = Some(prompt);
483 }
484 Err(e) => {
485 self.set_status_message(
486 t!("explorer.error_creating_file", error = e.to_string())
487 .to_string(),
488 );
489 }
490 }
491 }
492 }
493 }
494 }
495 }
496
497 pub fn file_explorer_new_directory(&mut self) {
498 if let Some(explorer) = &mut self.file_explorer {
499 if let Some(selected_id) = explorer.get_selected() {
500 let node = explorer.tree().get_node(selected_id);
501 if let Some(node) = node {
502 let parent_path = get_parent_dir_path(node);
503 let dirname = format!("New Folder {}", timestamp_suffix());
504 let dir_path = parent_path.join(&dirname);
505
506 if let Some(runtime) = &self.tokio_runtime {
507 let path_clone = dir_path.clone();
508 let dirname_clone = dirname.clone();
509 let result = self.filesystem.create_dir(&path_clone);
510
511 match result {
512 Ok(_) => {
513 let parent_id =
514 get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
515 let tree = explorer.tree_mut();
516 if let Err(e) = runtime.block_on(tree.refresh_node(parent_id)) {
517 tracing::warn!("Failed to refresh file tree: {}", e);
518 }
519 self.set_status_message(
520 t!("explorer.created_dir", name = &dirname_clone).to_string(),
521 );
522
523 let prompt = crate::view::prompt::Prompt::with_initial_text(
525 t!("explorer.rename_prompt").to_string(),
526 crate::view::prompt::PromptType::FileExplorerRename {
527 original_path: path_clone,
528 original_name: dirname_clone,
529 is_new_file: true,
530 },
531 dirname,
532 );
533 self.prompt = Some(prompt);
534 }
535 Err(e) => {
536 self.set_status_message(
537 t!("explorer.error_creating_dir", error = e.to_string())
538 .to_string(),
539 );
540 }
541 }
542 }
543 }
544 }
545 }
546 }
547
548 pub fn file_explorer_delete(&mut self) {
549 if let Some(explorer) = &self.file_explorer {
550 if let Some(selected_id) = explorer.get_selected() {
551 if selected_id == explorer.tree().root_id() {
553 self.set_status_message(t!("explorer.cannot_delete_root").to_string());
554 return;
555 }
556
557 let node = explorer.tree().get_node(selected_id);
558 if let Some(node) = node {
559 let path = node.entry.path.clone();
560 let name = node.entry.name.clone();
561 let is_dir = node.is_dir();
562
563 let type_str = if is_dir { "directory" } else { "file" };
564 self.start_prompt(
565 t!("explorer.delete_confirm", "type" = type_str, name = &name).to_string(),
566 PromptType::ConfirmDeleteFile { path, is_dir },
567 );
568 }
569 }
570 }
571 }
572
573 pub fn perform_file_explorer_delete(&mut self, path: std::path::PathBuf, _is_dir: bool) {
577 let name = path
578 .file_name()
579 .map(|n| n.to_string_lossy().to_string())
580 .unwrap_or_default();
581
582 let delete_result = if self.filesystem.remote_connection_info().is_some() {
585 self.move_to_remote_trash(&path)
586 } else {
587 trash::delete(&path).map_err(std::io::Error::other)
588 };
589
590 match delete_result {
591 Ok(_) => {
592 if let Some(explorer) = &mut self.file_explorer {
594 if let Some(runtime) = &self.tokio_runtime {
595 if let Some(node) = explorer.tree().get_node_by_path(&path) {
597 let node_id = node.id;
598 let parent_id = get_parent_node_id(explorer.tree(), node_id, false);
599
600 let deleted_index = explorer.get_selected_index();
602
603 if let Err(e) =
604 runtime.block_on(explorer.tree_mut().refresh_node(parent_id))
605 {
606 tracing::warn!("Failed to refresh file tree after delete: {}", e);
607 }
608
609 let count = explorer.visible_count();
612 if count > 0 {
613 let new_index = if let Some(idx) = deleted_index {
614 idx.min(count.saturating_sub(1))
615 } else {
616 0
617 };
618 if let Some(node_id) = explorer.get_node_at_index(new_index) {
619 explorer.set_selected(Some(node_id));
620 }
621 } else {
622 explorer.set_selected(Some(parent_id));
624 }
625 }
626 }
627 }
628 self.set_status_message(t!("explorer.moved_to_trash", name = &name).to_string());
629
630 self.key_context = KeyContext::FileExplorer;
632 }
633 Err(e) => {
634 self.set_status_message(
635 t!("explorer.error_trash", error = e.to_string()).to_string(),
636 );
637 }
638 }
639 }
640
641 fn move_to_remote_trash(&self, path: &std::path::Path) -> std::io::Result<()> {
643 let home = self.filesystem.home_dir()?;
645 let trash_dir = home.join(".local/share/fresh/trash");
646
647 if !self.filesystem.exists(&trash_dir) {
649 self.filesystem.create_dir_all(&trash_dir)?;
650 }
651
652 let file_name = path
654 .file_name()
655 .unwrap_or_else(|| std::ffi::OsStr::new("unnamed"));
656 let timestamp = std::time::SystemTime::now()
657 .duration_since(std::time::UNIX_EPOCH)
658 .map(|d| d.as_secs())
659 .unwrap_or(0);
660 let trash_name = format!("{}.{}", file_name.to_string_lossy(), timestamp);
661 let trash_path = trash_dir.join(trash_name);
662
663 self.filesystem.rename(path, &trash_path)
665 }
666
667 pub fn file_explorer_rename(&mut self) {
668 if let Some(explorer) = &self.file_explorer {
669 if let Some(selected_id) = explorer.get_selected() {
670 if selected_id == explorer.tree().root_id() {
672 self.set_status_message(t!("explorer.cannot_rename_root").to_string());
673 return;
674 }
675
676 let node = explorer.tree().get_node(selected_id);
677 if let Some(node) = node {
678 let old_path = node.entry.path.clone();
679 let old_name = node.entry.name.clone();
680
681 let prompt = crate::view::prompt::Prompt::with_initial_text(
683 t!("explorer.rename_prompt").to_string(),
684 crate::view::prompt::PromptType::FileExplorerRename {
685 original_path: old_path,
686 original_name: old_name.clone(),
687 is_new_file: false,
688 },
689 old_name,
690 );
691 self.prompt = Some(prompt);
692 }
693 }
694 }
695 }
696
697 pub fn perform_file_explorer_rename(
699 &mut self,
700 original_path: std::path::PathBuf,
701 original_name: String,
702 new_name: String,
703 is_new_file: bool,
704 ) {
705 if new_name.is_empty() || new_name == original_name {
706 self.set_status_message(t!("explorer.rename_cancelled").to_string());
707 return;
708 }
709
710 let new_path = original_path
711 .parent()
712 .map(|p| p.join(&new_name))
713 .unwrap_or_else(|| original_path.clone());
714
715 if let Some(runtime) = &self.tokio_runtime {
716 let result = self.filesystem.rename(&original_path, &new_path);
717
718 match result {
719 Ok(_) => {
720 if let Some(explorer) = &mut self.file_explorer {
722 if let Some(selected_id) = explorer.get_selected() {
723 let parent_id = get_parent_node_id(explorer.tree(), selected_id, false);
724 let tree = explorer.tree_mut();
725 if let Err(e) = runtime.block_on(tree.refresh_node(parent_id)) {
726 tracing::warn!("Failed to refresh file tree after rename: {}", e);
727 }
728 }
729 explorer.navigate_to_path(&new_path);
731 }
732
733 let buffer_to_update = self
735 .buffers
736 .iter()
737 .find(|(_, state)| state.buffer.file_path() == Some(&original_path))
738 .map(|(id, _)| *id);
739
740 if let Some(buffer_id) = buffer_to_update {
741 if let Some(state) = self.buffers.get_mut(&buffer_id) {
743 state.buffer.rename_file_path(new_path.clone());
744 }
745
746 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
748 let file_uri = super::types::file_path_to_lsp_uri(&new_path);
750
751 metadata.kind = super::BufferKind::File {
753 path: new_path.clone(),
754 uri: file_uri,
755 };
756
757 metadata.display_name = super::BufferMetadata::display_name_for_path(
759 &new_path,
760 &self.working_dir,
761 );
762 }
763
764 if is_new_file {
767 self.key_context = KeyContext::Normal;
768 }
769 }
770
771 self.set_status_message(
772 t!("explorer.renamed", old = &original_name, new = &new_name).to_string(),
773 );
774 }
775 Err(e) => {
776 self.set_status_message(
777 t!("explorer.error_renaming", error = e.to_string()).to_string(),
778 );
779 }
780 }
781 }
782 }
783
784 pub fn file_explorer_toggle_hidden(&mut self) {
785 let show_hidden = if let Some(explorer) = &mut self.file_explorer {
786 explorer.toggle_show_hidden();
787 explorer.ignore_patterns().show_hidden()
788 } else {
789 return;
790 };
791
792 let msg = if show_hidden {
793 t!("explorer.showing_hidden")
794 } else {
795 t!("explorer.hiding_hidden")
796 };
797 self.set_status_message(msg.to_string());
798
799 self.config_mut().file_explorer.show_hidden = show_hidden;
801 self.persist_config_change(
802 "/file_explorer/show_hidden",
803 serde_json::Value::Bool(show_hidden),
804 );
805 }
806
807 pub fn file_explorer_toggle_gitignored(&mut self) {
808 let show_gitignored = if let Some(explorer) = &mut self.file_explorer {
809 explorer.toggle_show_gitignored();
810 explorer.ignore_patterns().show_gitignored()
811 } else {
812 return;
813 };
814
815 let msg = if show_gitignored {
816 t!("explorer.showing_gitignored")
817 } else {
818 t!("explorer.hiding_gitignored")
819 };
820 self.set_status_message(msg.to_string());
821
822 self.config_mut().file_explorer.show_gitignored = show_gitignored;
824 self.persist_config_change(
825 "/file_explorer/show_gitignored",
826 serde_json::Value::Bool(show_gitignored),
827 );
828 }
829
830 pub fn file_explorer_search_clear(&mut self) {
832 if let Some(explorer) = &mut self.file_explorer {
833 if explorer.is_search_active() {
834 explorer.search_clear();
835 } else {
836 self.focus_editor();
838 }
839 }
840 }
841
842 pub fn file_explorer_search_push_char(&mut self, c: char) {
844 if let Some(explorer) = &mut self.file_explorer {
845 explorer.search_push_char(c);
846 explorer.update_scroll_for_selection();
847 }
848 }
849
850 pub fn file_explorer_search_pop_char(&mut self) {
852 if let Some(explorer) = &mut self.file_explorer {
853 explorer.search_pop_char();
854 explorer.update_scroll_for_selection();
855 }
856 }
857
858 pub fn handle_set_file_explorer_decorations(
859 &mut self,
860 namespace: String,
861 decorations: Vec<crate::view::file_tree::FileExplorerDecoration>,
862 ) {
863 let normalized: Vec<crate::view::file_tree::FileExplorerDecoration> = decorations
864 .into_iter()
865 .filter_map(|mut decoration| {
866 let path = if decoration.path.is_absolute() {
867 decoration.path
868 } else {
869 self.working_dir.join(&decoration.path)
870 };
871 let path = normalize_path(&path);
872 if path.starts_with(&self.working_dir) {
873 decoration.path = path;
874 Some(decoration)
875 } else {
876 None
877 }
878 })
879 .collect();
880
881 self.file_explorer_decorations.insert(namespace, normalized);
882 self.rebuild_file_explorer_decoration_cache();
883 }
884
885 pub fn handle_clear_file_explorer_decorations(&mut self, namespace: &str) {
886 self.file_explorer_decorations.remove(namespace);
887 self.rebuild_file_explorer_decoration_cache();
888 }
889
890 pub(super) fn rebuild_file_explorer_decoration_cache(&mut self) {
891 let decorations = self
892 .file_explorer_decorations
893 .values()
894 .flat_map(|entries| entries.iter().cloned());
895
896 let symlink_mappings = self
898 .file_explorer
899 .as_ref()
900 .map(|fe| fe.collect_symlink_mappings())
901 .unwrap_or_default();
902
903 self.file_explorer_decoration_cache =
904 crate::view::file_tree::FileExplorerDecorationCache::rebuild(
905 decorations,
906 &self.working_dir,
907 &symlink_mappings,
908 );
909 }
910}