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