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(_) => {
371 self.set_status_message(
372 t!("explorer.opened_file", name = &name).to_string(),
373 );
374 self.focus_editor();
375 }
376 Err(e) => {
377 if let Some(confirmation) =
380 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
381 {
382 self.start_large_file_encoding_confirmation(confirmation);
383 } else {
384 self.set_status_message(
385 t!("file.error_opening", error = e.to_string()).to_string(),
386 );
387 }
388 }
389 }
390 }
391 }
392 Ok(())
393 }
394
395 pub fn file_explorer_refresh(&mut self) {
396 let (selected_id, node_name) = if let Some(explorer) = &self.file_explorer {
397 if let Some(selected_id) = explorer.get_selected() {
398 let node_name = explorer
399 .tree()
400 .get_node(selected_id)
401 .map(|n| n.entry.name.clone());
402 (Some(selected_id), node_name)
403 } else {
404 (None, None)
405 }
406 } else {
407 return;
408 };
409
410 let Some(selected_id) = selected_id else {
411 return;
412 };
413
414 if let Some(name) = &node_name {
415 self.set_status_message(t!("explorer.refreshing", name = name).to_string());
416 }
417
418 if let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) {
419 let tree = explorer.tree_mut();
420 let result = runtime.block_on(tree.refresh_node(selected_id));
421 match result {
422 Ok(()) => {
423 if let Some(name) = node_name {
424 self.set_status_message(t!("explorer.refreshed", name = &name).to_string());
425 } else {
426 self.set_status_message(t!("explorer.refreshed_default").to_string());
427 }
428 }
429 Err(e) => {
430 self.set_status_message(
431 t!("explorer.error_refreshing", error = e.to_string()).to_string(),
432 );
433 }
434 }
435 }
436 }
437
438 pub fn file_explorer_new_file(&mut self) {
439 if let Some(explorer) = &mut self.file_explorer {
440 if let Some(selected_id) = explorer.get_selected() {
441 let node = explorer.tree().get_node(selected_id);
442 if let Some(node) = node {
443 let parent_path = get_parent_dir_path(node);
444 let filename = format!("untitled_{}.txt", timestamp_suffix());
445 let file_path = parent_path.join(&filename);
446
447 if let Some(runtime) = &self.tokio_runtime {
448 let path_clone = file_path.clone();
449 let result = self.filesystem.create_file(&path_clone).map(|_| ());
450
451 match result {
452 Ok(_) => {
453 let parent_id =
454 get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
455 let tree = explorer.tree_mut();
456 if let Err(e) = runtime.block_on(tree.refresh_node(parent_id)) {
457 tracing::warn!("Failed to refresh file tree: {}", e);
458 }
459 self.set_status_message(
460 t!("explorer.created_file", name = &filename).to_string(),
461 );
462
463 if let Err(e) = self.open_file(&path_clone) {
465 tracing::warn!("Failed to open new file: {}", e);
466 }
467
468 let prompt = crate::view::prompt::Prompt::new(
471 t!("explorer.rename_prompt").to_string(),
472 crate::view::prompt::PromptType::FileExplorerRename {
473 original_path: path_clone,
474 original_name: filename.clone(),
475 is_new_file: true,
476 },
477 );
478 self.prompt = Some(prompt);
479 }
480 Err(e) => {
481 self.set_status_message(
482 t!("explorer.error_creating_file", error = e.to_string())
483 .to_string(),
484 );
485 }
486 }
487 }
488 }
489 }
490 }
491 }
492
493 pub fn file_explorer_new_directory(&mut self) {
494 if let Some(explorer) = &mut self.file_explorer {
495 if let Some(selected_id) = explorer.get_selected() {
496 let node = explorer.tree().get_node(selected_id);
497 if let Some(node) = node {
498 let parent_path = get_parent_dir_path(node);
499 let dirname = format!("New Folder {}", timestamp_suffix());
500 let dir_path = parent_path.join(&dirname);
501
502 if let Some(runtime) = &self.tokio_runtime {
503 let path_clone = dir_path.clone();
504 let dirname_clone = dirname.clone();
505 let result = self.filesystem.create_dir(&path_clone);
506
507 match result {
508 Ok(_) => {
509 let parent_id =
510 get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
511 let tree = explorer.tree_mut();
512 if let Err(e) = runtime.block_on(tree.refresh_node(parent_id)) {
513 tracing::warn!("Failed to refresh file tree: {}", e);
514 }
515 self.set_status_message(
516 t!("explorer.created_dir", name = &dirname_clone).to_string(),
517 );
518
519 let prompt = crate::view::prompt::Prompt::with_initial_text(
521 t!("explorer.rename_prompt").to_string(),
522 crate::view::prompt::PromptType::FileExplorerRename {
523 original_path: path_clone,
524 original_name: dirname_clone,
525 is_new_file: true,
526 },
527 dirname,
528 );
529 self.prompt = Some(prompt);
530 }
531 Err(e) => {
532 self.set_status_message(
533 t!("explorer.error_creating_dir", error = e.to_string())
534 .to_string(),
535 );
536 }
537 }
538 }
539 }
540 }
541 }
542 }
543
544 pub fn file_explorer_delete(&mut self) {
545 if let Some(explorer) = &self.file_explorer {
546 if let Some(selected_id) = explorer.get_selected() {
547 if selected_id == explorer.tree().root_id() {
549 self.set_status_message(t!("explorer.cannot_delete_root").to_string());
550 return;
551 }
552
553 let node = explorer.tree().get_node(selected_id);
554 if let Some(node) = node {
555 let path = node.entry.path.clone();
556 let name = node.entry.name.clone();
557 let is_dir = node.is_dir();
558
559 let type_str = if is_dir { "directory" } else { "file" };
560 self.start_prompt(
561 t!("explorer.delete_confirm", "type" = type_str, name = &name).to_string(),
562 PromptType::ConfirmDeleteFile { path, is_dir },
563 );
564 }
565 }
566 }
567 }
568
569 pub fn perform_file_explorer_delete(&mut self, path: std::path::PathBuf, _is_dir: bool) {
573 let name = path
574 .file_name()
575 .map(|n| n.to_string_lossy().to_string())
576 .unwrap_or_default();
577
578 let delete_result = if self.filesystem.remote_connection_info().is_some() {
581 self.move_to_remote_trash(&path)
582 } else {
583 trash::delete(&path).map_err(std::io::Error::other)
584 };
585
586 match delete_result {
587 Ok(_) => {
588 if let Some(explorer) = &mut self.file_explorer {
590 if let Some(runtime) = &self.tokio_runtime {
591 if let Some(node) = explorer.tree().get_node_by_path(&path) {
593 let node_id = node.id;
594 let parent_id = get_parent_node_id(explorer.tree(), node_id, false);
595
596 let deleted_index = explorer.get_selected_index();
598
599 if let Err(e) =
600 runtime.block_on(explorer.tree_mut().refresh_node(parent_id))
601 {
602 tracing::warn!("Failed to refresh file tree after delete: {}", e);
603 }
604
605 let count = explorer.visible_count();
608 if count > 0 {
609 let new_index = if let Some(idx) = deleted_index {
610 idx.min(count.saturating_sub(1))
611 } else {
612 0
613 };
614 if let Some(node_id) = explorer.get_node_at_index(new_index) {
615 explorer.set_selected(Some(node_id));
616 }
617 } else {
618 explorer.set_selected(Some(parent_id));
620 }
621 }
622 }
623 }
624 self.set_status_message(t!("explorer.moved_to_trash", name = &name).to_string());
625
626 self.key_context = KeyContext::FileExplorer;
628 }
629 Err(e) => {
630 self.set_status_message(
631 t!("explorer.error_trash", error = e.to_string()).to_string(),
632 );
633 }
634 }
635 }
636
637 fn move_to_remote_trash(&self, path: &std::path::Path) -> std::io::Result<()> {
639 let home = self.filesystem.home_dir()?;
641 let trash_dir = home.join(".local/share/fresh/trash");
642
643 if !self.filesystem.exists(&trash_dir) {
645 self.filesystem.create_dir_all(&trash_dir)?;
646 }
647
648 let file_name = path
650 .file_name()
651 .unwrap_or_else(|| std::ffi::OsStr::new("unnamed"));
652 let timestamp = std::time::SystemTime::now()
653 .duration_since(std::time::UNIX_EPOCH)
654 .map(|d| d.as_secs())
655 .unwrap_or(0);
656 let trash_name = format!("{}.{}", file_name.to_string_lossy(), timestamp);
657 let trash_path = trash_dir.join(trash_name);
658
659 self.filesystem.rename(path, &trash_path)
661 }
662
663 pub fn file_explorer_rename(&mut self) {
664 if let Some(explorer) = &self.file_explorer {
665 if let Some(selected_id) = explorer.get_selected() {
666 if selected_id == explorer.tree().root_id() {
668 self.set_status_message(t!("explorer.cannot_rename_root").to_string());
669 return;
670 }
671
672 let node = explorer.tree().get_node(selected_id);
673 if let Some(node) = node {
674 let old_path = node.entry.path.clone();
675 let old_name = node.entry.name.clone();
676
677 let prompt = crate::view::prompt::Prompt::with_initial_text(
679 t!("explorer.rename_prompt").to_string(),
680 crate::view::prompt::PromptType::FileExplorerRename {
681 original_path: old_path,
682 original_name: old_name.clone(),
683 is_new_file: false,
684 },
685 old_name,
686 );
687 self.prompt = Some(prompt);
688 }
689 }
690 }
691 }
692
693 pub fn perform_file_explorer_rename(
695 &mut self,
696 original_path: std::path::PathBuf,
697 original_name: String,
698 new_name: String,
699 is_new_file: bool,
700 ) {
701 if new_name.is_empty() || new_name == original_name {
702 self.set_status_message(t!("explorer.rename_cancelled").to_string());
703 return;
704 }
705
706 let new_path = original_path
707 .parent()
708 .map(|p| p.join(&new_name))
709 .unwrap_or_else(|| original_path.clone());
710
711 if let Some(runtime) = &self.tokio_runtime {
712 let result = self.filesystem.rename(&original_path, &new_path);
713
714 match result {
715 Ok(_) => {
716 if let Some(explorer) = &mut self.file_explorer {
718 if let Some(selected_id) = explorer.get_selected() {
719 let parent_id = get_parent_node_id(explorer.tree(), selected_id, false);
720 let tree = explorer.tree_mut();
721 if let Err(e) = runtime.block_on(tree.refresh_node(parent_id)) {
722 tracing::warn!("Failed to refresh file tree after rename: {}", e);
723 }
724 }
725 explorer.navigate_to_path(&new_path);
727 }
728
729 let buffer_to_update = self
731 .buffers
732 .iter()
733 .find(|(_, state)| state.buffer.file_path() == Some(&original_path))
734 .map(|(id, _)| *id);
735
736 if let Some(buffer_id) = buffer_to_update {
737 if let Some(state) = self.buffers.get_mut(&buffer_id) {
739 state.buffer.rename_file_path(new_path.clone());
740 }
741
742 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
744 let file_uri = super::types::file_path_to_lsp_uri(&new_path);
746
747 metadata.kind = super::BufferKind::File {
749 path: new_path.clone(),
750 uri: file_uri,
751 };
752
753 metadata.display_name = super::BufferMetadata::display_name_for_path(
755 &new_path,
756 &self.working_dir,
757 );
758 }
759
760 if is_new_file {
763 self.key_context = KeyContext::Normal;
764 }
765 }
766
767 self.set_status_message(
768 t!("explorer.renamed", old = &original_name, new = &new_name).to_string(),
769 );
770 }
771 Err(e) => {
772 self.set_status_message(
773 t!("explorer.error_renaming", error = e.to_string()).to_string(),
774 );
775 }
776 }
777 }
778 }
779
780 pub fn file_explorer_toggle_hidden(&mut self) {
781 let show_hidden = if let Some(explorer) = &mut self.file_explorer {
782 explorer.toggle_show_hidden();
783 explorer.ignore_patterns().show_hidden()
784 } else {
785 return;
786 };
787
788 let msg = if show_hidden {
789 t!("explorer.showing_hidden")
790 } else {
791 t!("explorer.hiding_hidden")
792 };
793 self.set_status_message(msg.to_string());
794
795 self.config.file_explorer.show_hidden = show_hidden;
797 self.persist_config_change(
798 "/file_explorer/show_hidden",
799 serde_json::Value::Bool(show_hidden),
800 );
801 }
802
803 pub fn file_explorer_toggle_gitignored(&mut self) {
804 let show_gitignored = if let Some(explorer) = &mut self.file_explorer {
805 explorer.toggle_show_gitignored();
806 explorer.ignore_patterns().show_gitignored()
807 } else {
808 return;
809 };
810
811 let msg = if show_gitignored {
812 t!("explorer.showing_gitignored")
813 } else {
814 t!("explorer.hiding_gitignored")
815 };
816 self.set_status_message(msg.to_string());
817
818 self.config.file_explorer.show_gitignored = show_gitignored;
820 self.persist_config_change(
821 "/file_explorer/show_gitignored",
822 serde_json::Value::Bool(show_gitignored),
823 );
824 }
825
826 pub fn file_explorer_search_clear(&mut self) {
828 if let Some(explorer) = &mut self.file_explorer {
829 if explorer.is_search_active() {
830 explorer.search_clear();
831 } else {
832 self.focus_editor();
834 }
835 }
836 }
837
838 pub fn file_explorer_search_push_char(&mut self, c: char) {
840 if let Some(explorer) = &mut self.file_explorer {
841 explorer.search_push_char(c);
842 explorer.update_scroll_for_selection();
843 }
844 }
845
846 pub fn file_explorer_search_pop_char(&mut self) {
848 if let Some(explorer) = &mut self.file_explorer {
849 explorer.search_pop_char();
850 explorer.update_scroll_for_selection();
851 }
852 }
853
854 pub fn handle_set_file_explorer_decorations(
855 &mut self,
856 namespace: String,
857 decorations: Vec<crate::view::file_tree::FileExplorerDecoration>,
858 ) {
859 let normalized: Vec<crate::view::file_tree::FileExplorerDecoration> = decorations
860 .into_iter()
861 .filter_map(|mut decoration| {
862 let path = if decoration.path.is_absolute() {
863 decoration.path
864 } else {
865 self.working_dir.join(&decoration.path)
866 };
867 let path = normalize_path(&path);
868 if path.starts_with(&self.working_dir) {
869 decoration.path = path;
870 Some(decoration)
871 } else {
872 None
873 }
874 })
875 .collect();
876
877 self.file_explorer_decorations.insert(namespace, normalized);
878 self.rebuild_file_explorer_decoration_cache();
879 }
880
881 pub fn handle_clear_file_explorer_decorations(&mut self, namespace: &str) {
882 self.file_explorer_decorations.remove(namespace);
883 self.rebuild_file_explorer_decoration_cache();
884 }
885
886 pub(super) fn rebuild_file_explorer_decoration_cache(&mut self) {
887 let decorations = self
888 .file_explorer_decorations
889 .values()
890 .flat_map(|entries| entries.iter().cloned());
891
892 let symlink_mappings = self
894 .file_explorer
895 .as_ref()
896 .map(|fe| fe.collect_symlink_mappings())
897 .unwrap_or_default();
898
899 self.file_explorer_decoration_cache =
900 crate::view::file_tree::FileExplorerDecorationCache::rebuild(
901 decorations,
902 &self.working_dir,
903 &symlink_mappings,
904 );
905 }
906}