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(_) => {
371                        self.set_status_message(
372                            t!("explorer.opened_file", name = &name).to_string(),
373                        );
374                        self.focus_editor();
375                    }
376                    Err(e) => {
377                        // Check if this is a large file encoding confirmation error
378                        // These should be shown as prompts in the UI, not as fatal errors
379                        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                                // Open the file in the buffer
464                                if let Err(e) = self.open_file(&path_clone) {
465                                    tracing::warn!("Failed to open new file: {}", e);
466                                }
467
468                                // Enter rename mode for the new file with empty prompt
469                                // so user can type the desired filename from scratch
470                                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                                // Enter rename mode for the new folder
520                                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                // Don't allow deleting the root directory
548                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    /// Perform the actual file explorer delete operation (called after prompt confirmation)
570    /// For local files: moves to system trash/recycle bin
571    /// For remote files: moves to ~/.local/share/fresh/trash/ on remote
572    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        // For remote files, move to remote trash directory
579        // For local files, use system trash
580        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                // Refresh the parent directory in the file explorer
589                if let Some(explorer) = &mut self.file_explorer {
590                    if let Some(runtime) = &self.tokio_runtime {
591                        // Find the node for the deleted path and get its parent
592                        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                            // Remember the index of the deleted node in the visible list
597                            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                            // After refresh, select the next best node:
606                            // Try to stay at the same index, or select the last visible item
607                            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                                // No visible nodes, select parent
619                                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                // Ensure focus remains on file explorer
627                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    /// Move a file/directory to the remote trash directory (~/.local/share/fresh/trash/)
638    fn move_to_remote_trash(&self, path: &std::path::Path) -> std::io::Result<()> {
639        // Get remote home directory
640        let home = self.filesystem.home_dir()?;
641        let trash_dir = home.join(".local/share/fresh/trash");
642
643        // Create trash directory if it doesn't exist
644        if !self.filesystem.exists(&trash_dir) {
645            self.filesystem.create_dir_all(&trash_dir)?;
646        }
647
648        // Generate unique name with timestamp to avoid collisions
649        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        // Move to trash
660        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                // Don't allow renaming the root directory
667                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                    // Create a prompt for the new name, pre-filled with the old name
678                    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    /// Perform the actual file explorer rename operation (called after prompt confirmation)
694    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                    // Refresh the parent directory and select the renamed item
717                    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                        // Navigate to the renamed file to restore selection
726                        explorer.navigate_to_path(&new_path);
727                    }
728
729                    // Update buffer metadata if this file is open in a buffer
730                    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                        // Update the buffer's file path after rename
738                        if let Some(state) = self.buffers.get_mut(&buffer_id) {
739                            state.buffer.rename_file_path(new_path.clone());
740                        }
741
742                        // Update the buffer metadata
743                        if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
744                            // Compute new URI
745                            let file_uri = super::types::file_path_to_lsp_uri(&new_path);
746
747                            // Update kind with new path and URI
748                            metadata.kind = super::BufferKind::File {
749                                path: new_path.clone(),
750                                uri: file_uri,
751                            };
752
753                            // Update display name
754                            metadata.display_name = super::BufferMetadata::display_name_for_path(
755                                &new_path,
756                                &self.working_dir,
757                            );
758                        }
759
760                        // Only switch focus to the buffer if this is a new file being created
761                        // For renaming existing files from the explorer, keep focus in explorer.
762                        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        // Persist to config so the setting survives across sessions
796        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        // Persist to config so the setting survives across sessions
819        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    /// Clear the file explorer search
827    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                // No active search — Escape transfers focus to the editor
833                self.focus_editor();
834            }
835        }
836    }
837
838    /// Add a character to the file explorer search
839    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    /// Remove a character from the file explorer search (backspace)
847    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        // Collect symlink mappings from the file explorer
893        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}