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