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        // Notify plugins that the viewport dimensions changed (sidebar affects available width)
71        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        // Don't start a new sync if one is already in progress
92        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                            // Mark sync as in progress so render knows to keep the layout
112                            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                                // Receiver may have been dropped during shutdown.
117                                #[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            // Dismiss transient popups and clear hover state when focusing file explorer
132            self.on_editor_focus_lost();
133
134            // Cancel search/replace prompts when switching focus away from editor
135            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        // Use working directory as root. For remote mode, fall back to the remote
152        // home directory only when working_dir doesn't exist on the remote
153        // filesystem (e.g. when no path was provided and working_dir defaulted
154        // to the local current directory).
155        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                        // Receiver may have been dropped during shutdown.
184                        #[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    /// Collapse behavior for left arrow:
226    /// - If on expanded directory: collapse it
227    /// - If on file or collapsed directory: select parent directory
228    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 expanded directory, collapse it
242        if node.is_dir() && node.is_expanded() {
243            self.file_explorer_toggle_expand();
244            return;
245        }
246
247        // Otherwise, select parent
248        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            // Track if we need to rebuild decoration cache (for symlink directories)
302            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 a symlink directory was just expanded, we need to rebuild
322                            // the decoration cache so decorations under the canonical target
323                            // also appear under the symlink path
324                            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            // Rebuild decoration cache outside the explorer borrow
351            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                        // Double-click / Enter is the "I mean it" gesture — always
372                        // promote the tab out of preview mode so subsequent clicks
373                        // on *other* files don't replace this one.
374                        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                        // Check if this is a large file encoding confirmation error
382                        // These should be shown as prompts in the UI, not as fatal errors
383                        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                                // Open the file in the buffer
468                                if let Err(e) = self.open_file(&path_clone) {
469                                    tracing::warn!("Failed to open new file: {}", e);
470                                }
471
472                                // Enter rename mode for the new file with empty prompt
473                                // so user can type the desired filename from scratch
474                                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                                // Enter rename mode for the new folder
524                                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                // Don't allow deleting the root directory
552                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    /// Perform the actual file explorer delete operation (called after prompt confirmation)
574    /// For local files: moves to system trash/recycle bin
575    /// For remote files: moves to ~/.local/share/fresh/trash/ on remote
576    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        // For remote files, move to remote trash directory
583        // For local files, use system trash
584        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                // Refresh the parent directory in the file explorer
593                if let Some(explorer) = &mut self.file_explorer {
594                    if let Some(runtime) = &self.tokio_runtime {
595                        // Find the node for the deleted path and get its parent
596                        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                            // Remember the index of the deleted node in the visible list
601                            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                            // After refresh, select the next best node:
610                            // Try to stay at the same index, or select the last visible item
611                            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                                // No visible nodes, select parent
623                                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                // Ensure focus remains on file explorer
631                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    /// Move a file/directory to the remote trash directory (~/.local/share/fresh/trash/)
642    fn move_to_remote_trash(&self, path: &std::path::Path) -> std::io::Result<()> {
643        // Get remote home directory
644        let home = self.filesystem.home_dir()?;
645        let trash_dir = home.join(".local/share/fresh/trash");
646
647        // Create trash directory if it doesn't exist
648        if !self.filesystem.exists(&trash_dir) {
649            self.filesystem.create_dir_all(&trash_dir)?;
650        }
651
652        // Generate unique name with timestamp to avoid collisions
653        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        // Move to trash
664        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                // Don't allow renaming the root directory
671                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                    // Create a prompt for the new name, pre-filled with the old name
682                    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    /// Perform the actual file explorer rename operation (called after prompt confirmation)
698    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                    // Refresh the parent directory and select the renamed item
721                    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                        // Navigate to the renamed file to restore selection
730                        explorer.navigate_to_path(&new_path);
731                    }
732
733                    // Update buffer metadata if this file is open in a buffer
734                    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                        // Update the buffer's file path after rename
742                        if let Some(state) = self.buffers.get_mut(&buffer_id) {
743                            state.buffer.rename_file_path(new_path.clone());
744                        }
745
746                        // Update the buffer metadata
747                        if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
748                            // Compute new URI
749                            let file_uri = super::types::file_path_to_lsp_uri(&new_path);
750
751                            // Update kind with new path and URI
752                            metadata.kind = super::BufferKind::File {
753                                path: new_path.clone(),
754                                uri: file_uri,
755                            };
756
757                            // Update display name
758                            metadata.display_name = super::BufferMetadata::display_name_for_path(
759                                &new_path,
760                                &self.working_dir,
761                            );
762                        }
763
764                        // Only switch focus to the buffer if this is a new file being created
765                        // For renaming existing files from the explorer, keep focus in explorer.
766                        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        // Persist to config so the setting survives across sessions
800        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        // Persist to config so the setting survives across sessions
823        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    /// Clear the file explorer search
831    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                // No active search — Escape transfers focus to the editor
837                self.focus_editor();
838            }
839        }
840    }
841
842    /// Add a character to the file explorer search
843    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    /// Remove a character from the file explorer search (backspace)
851    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        // Collect symlink mappings from the file explorer
897        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}