Skip to main content

fresh/app/
file_explorer.rs

1use anyhow::Result as AnyhowResult;
2use rust_i18n::t;
3
4use super::*;
5use crate::view::file_tree::TreeNode;
6use std::path::PathBuf;
7
8/// Get the parent directory path from a file tree node.
9/// If the node is a directory, returns its path. If it's a file, returns the parent directory.
10fn 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
22/// Generate a timestamp suffix for naming new files/directories.
23fn timestamp_suffix() -> u64 {
24    std::time::SystemTime::now()
25        .duration_since(std::time::UNIX_EPOCH)
26        .unwrap()
27        .as_secs()
28}
29
30/// Get the parent node ID for refreshing after file operations.
31/// If the node is a directory, the node itself is the parent. Otherwise, look up the actual parent.
32fn 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        // Don't start a new sync if one is already in progress
83        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                            // Mark sync as in progress so render knows to keep the layout
103                            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                                let _ = sender.send(AsyncMessage::FileExplorerExpandedToPath(view));
108                            });
109                        } else {
110                            self.file_explorer = Some(view);
111                        }
112                    }
113                }
114            }
115        }
116    }
117
118    pub fn focus_file_explorer(&mut self) {
119        if self.file_explorer_visible {
120            // Dismiss transient popups and clear hover state when focusing file explorer
121            self.on_editor_focus_lost();
122
123            // Cancel search/replace prompts when switching focus away from editor
124            self.cancel_search_prompt_if_active();
125
126            self.key_context = KeyContext::FileExplorer;
127            self.set_status_message(t!("explorer.focused").to_string());
128            self.sync_file_explorer_to_active_file();
129        } else {
130            self.toggle_file_explorer();
131        }
132    }
133
134    pub fn focus_editor(&mut self) {
135        self.key_context = KeyContext::Normal;
136        self.set_status_message(t!("editor.focused").to_string());
137    }
138
139    pub(crate) fn init_file_explorer(&mut self) {
140        // Use remote home directory if in remote mode, otherwise local working directory
141        let root_path = if self.filesystem.remote_connection_info().is_some() {
142            match self.filesystem.home_dir() {
143                Ok(home) => home,
144                Err(e) => {
145                    tracing::error!("Failed to get remote home directory: {}", e);
146                    self.set_status_message(format!("Failed to get remote home: {}", e));
147                    return;
148                }
149            }
150        } else {
151            self.working_dir.clone()
152        };
153
154        if let (Some(runtime), Some(bridge)) = (&self.tokio_runtime, &self.async_bridge) {
155            let fs_manager = Arc::clone(&self.fs_manager);
156            let sender = bridge.sender();
157
158            runtime.spawn(async move {
159                match FileTree::new(root_path, fs_manager).await {
160                    Ok(mut tree) => {
161                        let root_id = tree.root_id();
162                        if let Err(e) = tree.expand_node(root_id).await {
163                            tracing::warn!("Failed to expand root directory: {}", e);
164                        }
165
166                        let view = FileTreeView::new(tree);
167                        let _ = sender.send(AsyncMessage::FileExplorerInitialized(view));
168                    }
169                    Err(e) => {
170                        tracing::error!("Failed to initialize file explorer: {}", e);
171                    }
172                }
173            });
174
175            self.set_status_message(t!("explorer.initializing").to_string());
176        }
177    }
178
179    pub fn file_explorer_navigate_up(&mut self) {
180        if let Some(explorer) = &mut self.file_explorer {
181            explorer.select_prev();
182            explorer.update_scroll_for_selection();
183        }
184    }
185
186    pub fn file_explorer_navigate_down(&mut self) {
187        if let Some(explorer) = &mut self.file_explorer {
188            explorer.select_next();
189            explorer.update_scroll_for_selection();
190        }
191    }
192
193    pub fn file_explorer_page_up(&mut self) {
194        if let Some(explorer) = &mut self.file_explorer {
195            explorer.select_page_up();
196            explorer.update_scroll_for_selection();
197        }
198    }
199
200    pub fn file_explorer_page_down(&mut self) {
201        if let Some(explorer) = &mut self.file_explorer {
202            explorer.select_page_down();
203            explorer.update_scroll_for_selection();
204        }
205    }
206
207    /// Collapse behavior for left arrow:
208    /// - If on expanded directory: collapse it
209    /// - If on file or collapsed directory: select parent directory
210    pub fn file_explorer_collapse(&mut self) {
211        let Some(explorer) = &self.file_explorer else {
212            return;
213        };
214
215        let Some(selected_id) = explorer.get_selected() else {
216            return;
217        };
218
219        let Some(node) = explorer.tree().get_node(selected_id) else {
220            return;
221        };
222
223        // If expanded directory, collapse it
224        if node.is_dir() && node.is_expanded() {
225            self.file_explorer_toggle_expand();
226            return;
227        }
228
229        // Otherwise, select parent
230        if let Some(explorer) = &mut self.file_explorer {
231            explorer.select_parent();
232            explorer.update_scroll_for_selection();
233        }
234    }
235
236    pub fn file_explorer_toggle_expand(&mut self) {
237        let selected_id = if let Some(explorer) = &self.file_explorer {
238            explorer.get_selected()
239        } else {
240            return;
241        };
242
243        let Some(selected_id) = selected_id else {
244            return;
245        };
246
247        let (is_dir, is_expanded, name) = if let Some(explorer) = &self.file_explorer {
248            let node = explorer.tree().get_node(selected_id);
249            if let Some(node) = node {
250                (node.is_dir(), node.is_expanded(), node.entry.name.clone())
251            } else {
252                return;
253            }
254        } else {
255            return;
256        };
257
258        if !is_dir {
259            return;
260        }
261
262        let status_msg = if is_expanded {
263            t!("explorer.collapsing").to_string()
264        } else {
265            t!("explorer.loading_dir", name = &name).to_string()
266        };
267        self.set_status_message(status_msg);
268
269        if let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) {
270            let tree = explorer.tree_mut();
271            let result = runtime.block_on(tree.toggle_node(selected_id));
272
273            let final_name = explorer
274                .tree()
275                .get_node(selected_id)
276                .map(|n| n.entry.name.clone());
277            let final_expanded = explorer
278                .tree()
279                .get_node(selected_id)
280                .map(|n| n.is_expanded())
281                .unwrap_or(false);
282
283            match result {
284                Ok(()) => {
285                    if final_expanded {
286                        let dir_path = explorer
287                            .tree()
288                            .get_node(selected_id)
289                            .map(|n| n.entry.path.clone());
290
291                        if let Some(dir_path) = dir_path {
292                            if let Err(e) = explorer.load_gitignore_for_dir(&dir_path) {
293                                tracing::warn!(
294                                    "Failed to load .gitignore from {:?}: {}",
295                                    dir_path,
296                                    e
297                                );
298                            }
299                        }
300                    }
301
302                    if let Some(name) = final_name {
303                        let msg = if final_expanded {
304                            t!("explorer.expanded", name = &name).to_string()
305                        } else {
306                            t!("explorer.collapsed", name = &name).to_string()
307                        };
308                        self.set_status_message(msg);
309                    }
310                }
311                Err(e) => {
312                    self.set_status_message(
313                        t!("explorer.error", error = e.to_string()).to_string(),
314                    );
315                }
316            }
317        }
318    }
319
320    pub fn file_explorer_open_file(&mut self) -> AnyhowResult<()> {
321        let entry_type = self
322            .file_explorer
323            .as_ref()
324            .and_then(|explorer| explorer.get_selected_entry())
325            .map(|entry| (entry.is_dir(), entry.path.clone(), entry.name.clone()));
326
327        if let Some((is_dir, path, name)) = entry_type {
328            if is_dir {
329                self.file_explorer_toggle_expand();
330            } else {
331                tracing::info!("[SYNTAX DEBUG] file_explorer opening file: {:?}", path);
332                self.open_file(&path)?;
333                self.set_status_message(t!("explorer.opened_file", name = &name).to_string());
334                self.focus_editor();
335            }
336        }
337        Ok(())
338    }
339
340    pub fn file_explorer_refresh(&mut self) {
341        let (selected_id, node_name) = if let Some(explorer) = &self.file_explorer {
342            if let Some(selected_id) = explorer.get_selected() {
343                let node_name = explorer
344                    .tree()
345                    .get_node(selected_id)
346                    .map(|n| n.entry.name.clone());
347                (Some(selected_id), node_name)
348            } else {
349                (None, None)
350            }
351        } else {
352            return;
353        };
354
355        let Some(selected_id) = selected_id else {
356            return;
357        };
358
359        if let Some(name) = &node_name {
360            self.set_status_message(t!("explorer.refreshing", name = name).to_string());
361        }
362
363        if let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) {
364            let tree = explorer.tree_mut();
365            let result = runtime.block_on(tree.refresh_node(selected_id));
366            match result {
367                Ok(()) => {
368                    if let Some(name) = node_name {
369                        self.set_status_message(t!("explorer.refreshed", name = &name).to_string());
370                    } else {
371                        self.set_status_message(t!("explorer.refreshed_default").to_string());
372                    }
373                }
374                Err(e) => {
375                    self.set_status_message(
376                        t!("explorer.error_refreshing", error = e.to_string()).to_string(),
377                    );
378                }
379            }
380        }
381    }
382
383    pub fn file_explorer_new_file(&mut self) {
384        if let Some(explorer) = &mut self.file_explorer {
385            if let Some(selected_id) = explorer.get_selected() {
386                let node = explorer.tree().get_node(selected_id);
387                if let Some(node) = node {
388                    let parent_path = get_parent_dir_path(node);
389                    let filename = format!("untitled_{}.txt", timestamp_suffix());
390                    let file_path = parent_path.join(&filename);
391
392                    if let Some(runtime) = &self.tokio_runtime {
393                        let path_clone = file_path.clone();
394                        let result = self.filesystem.create_file(&path_clone).map(|_| ());
395
396                        match result {
397                            Ok(_) => {
398                                let parent_id =
399                                    get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
400                                let tree = explorer.tree_mut();
401                                let _ = runtime.block_on(tree.refresh_node(parent_id));
402                                self.set_status_message(
403                                    t!("explorer.created_file", name = &filename).to_string(),
404                                );
405
406                                // Open the file in the buffer
407                                let _ = self.open_file(&path_clone);
408
409                                // Enter rename mode for the new file with empty prompt
410                                // so user can type the desired filename from scratch
411                                let prompt = crate::view::prompt::Prompt::new(
412                                    t!("explorer.rename_prompt").to_string(),
413                                    crate::view::prompt::PromptType::FileExplorerRename {
414                                        original_path: path_clone,
415                                        original_name: filename.clone(),
416                                        is_new_file: true,
417                                    },
418                                );
419                                self.prompt = Some(prompt);
420                            }
421                            Err(e) => {
422                                self.set_status_message(
423                                    t!("explorer.error_creating_file", error = e.to_string())
424                                        .to_string(),
425                                );
426                            }
427                        }
428                    }
429                }
430            }
431        }
432    }
433
434    pub fn file_explorer_new_directory(&mut self) {
435        if let Some(explorer) = &mut self.file_explorer {
436            if let Some(selected_id) = explorer.get_selected() {
437                let node = explorer.tree().get_node(selected_id);
438                if let Some(node) = node {
439                    let parent_path = get_parent_dir_path(node);
440                    let dirname = format!("New Folder {}", timestamp_suffix());
441                    let dir_path = parent_path.join(&dirname);
442
443                    if let Some(runtime) = &self.tokio_runtime {
444                        let path_clone = dir_path.clone();
445                        let dirname_clone = dirname.clone();
446                        let result = self.filesystem.create_dir(&path_clone);
447
448                        match result {
449                            Ok(_) => {
450                                let parent_id =
451                                    get_parent_node_id(explorer.tree(), selected_id, node.is_dir());
452                                let tree = explorer.tree_mut();
453                                let _ = runtime.block_on(tree.refresh_node(parent_id));
454                                self.set_status_message(
455                                    t!("explorer.created_dir", name = &dirname_clone).to_string(),
456                                );
457
458                                // Enter rename mode for the new folder
459                                let prompt = crate::view::prompt::Prompt::with_initial_text(
460                                    t!("explorer.rename_prompt").to_string(),
461                                    crate::view::prompt::PromptType::FileExplorerRename {
462                                        original_path: path_clone,
463                                        original_name: dirname_clone,
464                                        is_new_file: true,
465                                    },
466                                    dirname,
467                                );
468                                self.prompt = Some(prompt);
469                            }
470                            Err(e) => {
471                                self.set_status_message(
472                                    t!("explorer.error_creating_dir", error = e.to_string())
473                                        .to_string(),
474                                );
475                            }
476                        }
477                    }
478                }
479            }
480        }
481    }
482
483    pub fn file_explorer_delete(&mut self) {
484        if let Some(explorer) = &self.file_explorer {
485            if let Some(selected_id) = explorer.get_selected() {
486                // Don't allow deleting the root directory
487                if selected_id == explorer.tree().root_id() {
488                    self.set_status_message(t!("explorer.cannot_delete_root").to_string());
489                    return;
490                }
491
492                let node = explorer.tree().get_node(selected_id);
493                if let Some(node) = node {
494                    let path = node.entry.path.clone();
495                    let name = node.entry.name.clone();
496                    let is_dir = node.is_dir();
497
498                    let type_str = if is_dir { "directory" } else { "file" };
499                    self.start_prompt(
500                        t!("explorer.delete_confirm", "type" = type_str, name = &name).to_string(),
501                        PromptType::ConfirmDeleteFile { path, is_dir },
502                    );
503                }
504            }
505        }
506    }
507
508    /// Perform the actual file explorer delete operation (called after prompt confirmation)
509    /// For local files: moves to system trash/recycle bin
510    /// For remote files: moves to ~/.local/share/fresh/trash/ on remote
511    pub fn perform_file_explorer_delete(&mut self, path: std::path::PathBuf, _is_dir: bool) {
512        let name = path
513            .file_name()
514            .map(|n| n.to_string_lossy().to_string())
515            .unwrap_or_default();
516
517        // For remote files, move to remote trash directory
518        // For local files, use system trash
519        let delete_result = if self.filesystem.remote_connection_info().is_some() {
520            self.move_to_remote_trash(&path)
521        } else {
522            trash::delete(&path).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
523        };
524
525        match delete_result {
526            Ok(_) => {
527                // Refresh the parent directory in the file explorer
528                if let Some(explorer) = &mut self.file_explorer {
529                    if let Some(runtime) = &self.tokio_runtime {
530                        // Find the node for the deleted path and get its parent
531                        if let Some(node) = explorer.tree().get_node_by_path(&path) {
532                            let node_id = node.id;
533                            let parent_id = get_parent_node_id(explorer.tree(), node_id, false);
534
535                            // Remember the index of the deleted node in the visible list
536                            let deleted_index = explorer.get_selected_index();
537
538                            let _ = runtime.block_on(explorer.tree_mut().refresh_node(parent_id));
539
540                            // After refresh, select the next best node:
541                            // Try to stay at the same index, or select the last visible item
542                            let visible = explorer.tree().get_visible_nodes();
543                            if !visible.is_empty() {
544                                let new_index = if let Some(idx) = deleted_index {
545                                    idx.min(visible.len().saturating_sub(1))
546                                } else {
547                                    0
548                                };
549                                explorer.set_selected(Some(visible[new_index]));
550                            } else {
551                                // No visible nodes, select parent
552                                explorer.set_selected(Some(parent_id));
553                            }
554                        }
555                    }
556                }
557                self.set_status_message(t!("explorer.moved_to_trash", name = &name).to_string());
558
559                // Ensure focus remains on file explorer
560                self.key_context = KeyContext::FileExplorer;
561            }
562            Err(e) => {
563                self.set_status_message(
564                    t!("explorer.error_trash", error = e.to_string()).to_string(),
565                );
566            }
567        }
568    }
569
570    /// Move a file/directory to the remote trash directory (~/.local/share/fresh/trash/)
571    fn move_to_remote_trash(&self, path: &std::path::Path) -> std::io::Result<()> {
572        // Get remote home directory
573        let home = self.filesystem.home_dir()?;
574        let trash_dir = home.join(".local/share/fresh/trash");
575
576        // Create trash directory if it doesn't exist
577        if !self.filesystem.exists(&trash_dir) {
578            self.filesystem.create_dir_all(&trash_dir)?;
579        }
580
581        // Generate unique name with timestamp to avoid collisions
582        let file_name = path
583            .file_name()
584            .unwrap_or_else(|| std::ffi::OsStr::new("unnamed"));
585        let timestamp = std::time::SystemTime::now()
586            .duration_since(std::time::UNIX_EPOCH)
587            .map(|d| d.as_secs())
588            .unwrap_or(0);
589        let trash_name = format!("{}.{}", file_name.to_string_lossy(), timestamp);
590        let trash_path = trash_dir.join(trash_name);
591
592        // Move to trash
593        self.filesystem.rename(path, &trash_path)
594    }
595
596    pub fn file_explorer_rename(&mut self) {
597        if let Some(explorer) = &self.file_explorer {
598            if let Some(selected_id) = explorer.get_selected() {
599                // Don't allow renaming the root directory
600                if selected_id == explorer.tree().root_id() {
601                    self.set_status_message(t!("explorer.cannot_rename_root").to_string());
602                    return;
603                }
604
605                let node = explorer.tree().get_node(selected_id);
606                if let Some(node) = node {
607                    let old_path = node.entry.path.clone();
608                    let old_name = node.entry.name.clone();
609
610                    // Create a prompt for the new name, pre-filled with the old name
611                    let prompt = crate::view::prompt::Prompt::with_initial_text(
612                        t!("explorer.rename_prompt").to_string(),
613                        crate::view::prompt::PromptType::FileExplorerRename {
614                            original_path: old_path,
615                            original_name: old_name.clone(),
616                            is_new_file: false,
617                        },
618                        old_name,
619                    );
620                    self.prompt = Some(prompt);
621                }
622            }
623        }
624    }
625
626    /// Perform the actual file explorer rename operation (called after prompt confirmation)
627    pub fn perform_file_explorer_rename(
628        &mut self,
629        original_path: std::path::PathBuf,
630        original_name: String,
631        new_name: String,
632        is_new_file: bool,
633    ) {
634        if new_name.is_empty() || new_name == original_name {
635            self.set_status_message(t!("explorer.rename_cancelled").to_string());
636            return;
637        }
638
639        let new_path = original_path
640            .parent()
641            .map(|p| p.join(&new_name))
642            .unwrap_or_else(|| original_path.clone());
643
644        if let Some(runtime) = &self.tokio_runtime {
645            let result = self.filesystem.rename(&original_path, &new_path);
646
647            match result {
648                Ok(_) => {
649                    // Refresh the parent directory and select the renamed item
650                    if let Some(explorer) = &mut self.file_explorer {
651                        if let Some(selected_id) = explorer.get_selected() {
652                            let parent_id = get_parent_node_id(explorer.tree(), selected_id, false);
653                            let tree = explorer.tree_mut();
654                            let _ = runtime.block_on(tree.refresh_node(parent_id));
655                        }
656                        // Navigate to the renamed file to restore selection
657                        explorer.navigate_to_path(&new_path);
658                    }
659
660                    // Update buffer metadata if this file is open in a buffer
661                    let buffer_to_update = self
662                        .buffers
663                        .iter()
664                        .find(|(_, state)| state.buffer.file_path() == Some(&original_path))
665                        .map(|(id, _)| *id);
666
667                    if let Some(buffer_id) = buffer_to_update {
668                        // Update the buffer's file path
669                        if let Some(state) = self.buffers.get_mut(&buffer_id) {
670                            state.buffer.set_file_path(new_path.clone());
671                        }
672
673                        // Update the buffer metadata
674                        if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
675                            // Compute new URI
676                            let file_uri = url::Url::from_file_path(&new_path)
677                                .ok()
678                                .and_then(|u| u.as_str().parse::<lsp_types::Uri>().ok());
679
680                            // Update kind with new path and URI
681                            metadata.kind = super::BufferKind::File {
682                                path: new_path.clone(),
683                                uri: file_uri,
684                            };
685
686                            // Update display name
687                            metadata.display_name = super::BufferMetadata::display_name_for_path(
688                                &new_path,
689                                &self.working_dir,
690                            );
691                        }
692
693                        // Only switch focus to the buffer if this is a new file being created
694                        // For renaming existing files from the explorer, keep focus in explorer.
695                        if is_new_file {
696                            self.key_context = KeyContext::Normal;
697                        }
698                    }
699
700                    self.set_status_message(
701                        t!("explorer.renamed", old = &original_name, new = &new_name).to_string(),
702                    );
703                }
704                Err(e) => {
705                    self.set_status_message(
706                        t!("explorer.error_renaming", error = e.to_string()).to_string(),
707                    );
708                }
709            }
710        }
711    }
712
713    pub fn file_explorer_toggle_hidden(&mut self) {
714        if let Some(explorer) = &mut self.file_explorer {
715            explorer.toggle_show_hidden();
716            let msg = if explorer.ignore_patterns().show_hidden() {
717                t!("explorer.showing_hidden")
718            } else {
719                t!("explorer.hiding_hidden")
720            };
721            self.set_status_message(msg.to_string());
722        }
723    }
724
725    pub fn file_explorer_toggle_gitignored(&mut self) {
726        if let Some(explorer) = &mut self.file_explorer {
727            explorer.toggle_show_gitignored();
728            let show = explorer.ignore_patterns().show_gitignored();
729            let msg = if show {
730                t!("explorer.showing_gitignored")
731            } else {
732                t!("explorer.hiding_gitignored")
733            };
734            self.set_status_message(msg.to_string());
735        }
736    }
737
738    pub fn handle_set_file_explorer_decorations(
739        &mut self,
740        namespace: String,
741        decorations: Vec<crate::view::file_tree::FileExplorerDecoration>,
742    ) {
743        let normalized: Vec<crate::view::file_tree::FileExplorerDecoration> = decorations
744            .into_iter()
745            .filter_map(|mut decoration| {
746                let path = if decoration.path.is_absolute() {
747                    decoration.path
748                } else {
749                    self.working_dir.join(&decoration.path)
750                };
751                let path = normalize_path(&path);
752                if path.starts_with(&self.working_dir) {
753                    decoration.path = path;
754                    Some(decoration)
755                } else {
756                    None
757                }
758            })
759            .collect();
760
761        self.file_explorer_decorations.insert(namespace, normalized);
762        self.rebuild_file_explorer_decoration_cache();
763    }
764
765    pub fn handle_clear_file_explorer_decorations(&mut self, namespace: &str) {
766        self.file_explorer_decorations.remove(namespace);
767        self.rebuild_file_explorer_decoration_cache();
768    }
769
770    fn rebuild_file_explorer_decoration_cache(&mut self) {
771        let decorations = self
772            .file_explorer_decorations
773            .values()
774            .flat_map(|entries| entries.iter().cloned());
775        self.file_explorer_decoration_cache =
776            crate::view::file_tree::FileExplorerDecorationCache::rebuild(
777                decorations,
778                &self.working_dir,
779            );
780    }
781}