Skip to main content

fresh/app/
file_open_orchestrators.rs

1//! File-open orchestrators on `Editor`.
2//!
3//! The `open_file` family — open_file, open_file_no_focus,
4//! open_local_file, open_file_with_encoding, reload_with_encoding,
5//! open_file_large_encoding_confirmed — and supporting helpers
6//! restore_global_file_state and save_file_state_on_close.
7//!
8//! Opening a file in this editor coordinates: detecting the file type,
9//! choosing or creating a buffer, registering with the LSP, parsing
10//! grammar, restoring per-file UI state (cursor position, scroll), and
11//! deciding which split to focus. Each variant differs only in how it
12//! handles encoding errors, focus, and "no file at this path yet" cases.
13
14use std::path::Path;
15use std::sync::Arc;
16
17use rust_i18n::t;
18
19use crate::model::event::BufferId;
20use crate::state::EditorState;
21
22use super::Editor;
23
24/// How a file open treats the resulting buffer.
25///
26/// Threaded through the open path so the single `after_file_open` fire site
27/// in [`open_file_no_focus_inner`] can defer the hook for previews. A
28/// `Preview` open is "just looking" (file-explorer browse, live-grep
29/// overlay): the hook is withheld until the preview is escalated to a
30/// permanent buffer (see the `promote_*` methods on `Window`). `Commit` is
31/// every deliberate open and fires the hook immediately. This replaces an
32/// earlier ambient `opening_as_preview` flag — the intent now travels as a
33/// value rather than mutable window state that could be read out of band.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub(crate) enum OpenKind {
36    /// Deliberate open — fire `after_file_open` now.
37    Commit,
38    /// Ephemeral preview — defer `after_file_open` until escalation.
39    Preview,
40}
41
42impl Editor {
43    /// Helper to jump to a line/column position in the active buffer.
44    ///
45    /// Lives here (not in the plugin-gated command module) so non-plugin
46    /// callers — e.g. Ctrl+Click-to-open from the terminal — can reach it in
47    /// builds compiled without the `plugins` feature.
48    pub(crate) fn jump_to_line_column(&mut self, line: Option<usize>, column: Option<usize>) {
49        // Convert 1-indexed line/column to byte position
50        let target_line = line.unwrap_or(1).saturating_sub(1); // Convert to 0-indexed
51        let column_offset = column.unwrap_or(1).saturating_sub(1); // Convert to 0-indexed
52
53        let state = self.active_state_mut();
54        let mut iter = state.buffer.line_iterator(0, 80);
55        let mut target_byte = 0;
56
57        // Iterate through lines until we reach the target
58        for current_line in 0..=target_line {
59            if let Some((line_start, _)) = iter.next_line() {
60                if current_line == target_line {
61                    target_byte = line_start;
62                    break;
63                }
64            } else {
65                // Reached end of buffer before target line
66                break;
67            }
68        }
69
70        // Add the column offset to position within the line
71        // Column offset is byte offset from line start (matching git grep --column behavior)
72        let final_position = target_byte + column_offset;
73
74        // Ensure we don't go past the buffer end
75        let buffer_len = state.buffer.len();
76        let clamped_position = final_position.min(buffer_len);
77
78        // Update the cached line number so the status bar shows the correct
79        // position. Without this, the status bar reads a stale value from
80        // state.primary_cursor_line_number which was set before the jump.
81        state.primary_cursor_line_number = crate::model::buffer::LineNumber::Absolute(target_line);
82
83        // Funnel through the navigation primitive so the cursor is guaranteed
84        // visible in the viewport (#1689 — without this, jump_to_line_column
85        // could land off-screen if a prior scroll set skip_ensure_visible).
86        self.active_window_mut().jump_active_cursor_to(
87            clamped_position,
88            super::navigation::JumpOptions::navigation(),
89        );
90    }
91
92    /// Open a file (switching to an already-open buffer if any) and jump to the
93    /// given 1-based line/column if specified. Used by the OpenFileAtLocation
94    /// plugin command and by Ctrl+Click-to-open from the terminal.
95    pub(crate) fn handle_open_file_at_location(
96        &mut self,
97        path: std::path::PathBuf,
98        line: Option<usize>,
99        column: Option<usize>,
100    ) -> anyhow::Result<()> {
101        // Open the file (may switch to an already-open buffer)
102        if let Err(e) = self.open_file(&path) {
103            tracing::error!("Failed to open file at location: {}", e);
104            return Ok(());
105        }
106
107        // If line/column specified, jump to that location
108        if line.is_some() || column.is_some() {
109            self.jump_to_line_column(line, column);
110        }
111        Ok(())
112    }
113
114    /// Open a file and return its buffer ID
115    ///
116    /// If the file doesn't exist, creates an unsaved buffer with that filename.
117    /// Saving the buffer will create the file.
118    pub fn open_file(&mut self, path: &Path) -> anyhow::Result<BufferId> {
119        self.open_file_with_kind(path, OpenKind::Commit)
120    }
121
122    /// `open_file` with explicit [`OpenKind`]. `open_file_preview` uses
123    /// `OpenKind::Preview` to route through all of `open_file`'s
124    /// cross-cutting concerns (focus, language detection, status, the
125    /// `buffer_activated` hook) while deferring only `after_file_open`.
126    pub(crate) fn open_file_with_kind(
127        &mut self,
128        path: &Path,
129        kind: OpenKind,
130    ) -> anyhow::Result<BufferId> {
131        // If the active leaf is a utility-dock pane (Search/Replace,
132        // Quickfix, terminal-in-dock), the user almost never wants the
133        // newly-opened file to land there — the dock hosts panel-style
134        // content, not editor buffers. Snap the active leaf back to
135        // the most recent regular editor leaf BEFORE the open path
136        // runs, so both downstream routing decisions —
137        // `preferred_split_for_file` (which adds the new buffer as a
138        // tab) and `set_active_buffer` (which makes it the active
139        // buffer) — see a non-dock active leaf and route consistently.
140        self.active_window_mut()
141            .redirect_active_split_away_from_dock_if_needed();
142
143        // Check whether the active buffer had a file path before loading.
144        // If it didn't, open_file_no_focus may replace the empty initial buffer
145        // in-place (same buffer ID, new content), and we need to notify plugins.
146        let active_had_path = self
147            .buffers()
148            .get(&self.active_buffer())
149            .and_then(|s| s.buffer.file_path())
150            .is_some();
151
152        let buffer_id = self
153            .active_window_mut()
154            .open_file_no_focus_with_kind(path, kind)?;
155
156        // Check if this was an already-open buffer or a new one
157        // For already-open buffers, just switch to them
158        // For new buffers, record position history before switching
159        let is_new_buffer = self.active_buffer() != buffer_id;
160
161        if is_new_buffer && !self.active_window().suppress_position_history_once {
162            // Save current position before switching to new buffer
163            self.active_window_mut()
164                .position_history
165                .commit_pending_movement();
166
167            // Explicitly record current position before switching
168            let cursors = self.active_cursors();
169            let position = cursors.primary().position;
170            let anchor = cursors.primary().anchor;
171            let active_buffer_id = self.active_buffer();
172            let ph = &mut self.active_window_mut().position_history;
173            ph.record_movement(active_buffer_id, position, anchor);
174            ph.commit_pending_movement();
175        }
176
177        self.set_active_buffer(buffer_id);
178
179        // Opening a file focuses a buffer in the active split. If a
180        // *different* split is maximized (most commonly the docked
181        // terminal), the renderer shows only the maximized split, so the
182        // freshly-focused buffer would be invisible. Restore the layout so
183        // the user actually sees the file they just opened.
184        self.reveal_active_split_if_hidden_by_maximize();
185
186        // If the initial empty buffer was replaced in-place with file content,
187        // set_active_buffer is a no-op (same buffer ID). Fire buffer_activated
188        // explicitly so plugins see the newly loaded file.
189        // Skip this when re-opening an already-active file (active_had_path),
190        // as nothing changed and the extra hook would cause spurious refreshes
191        // in plugins like the diagnostics panel.
192        if !is_new_buffer && !active_had_path {
193            #[cfg(feature = "plugins")]
194            self.update_plugin_state_snapshot();
195
196            self.plugin_manager.read().unwrap().run_hook(
197                "buffer_activated",
198                crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
199            );
200        }
201
202        // Use display_name from metadata for relative path display
203        let display_name = self
204            .active_window()
205            .buffer_metadata
206            .get(&buffer_id)
207            .map(|m| m.display_name.clone())
208            .unwrap_or_else(|| path.display().to_string());
209
210        // Check if buffer is binary for status message
211        let is_binary = self
212            .buffers()
213            .get(&buffer_id)
214            .map(|s| s.buffer.is_binary())
215            .unwrap_or(false);
216
217        // Show appropriate status message for binary vs regular files
218        if is_binary {
219            self.active_window_mut().status_message =
220                Some(t!("buffer.opened_binary", name = display_name).to_string());
221        } else {
222            self.active_window_mut().status_message =
223                Some(t!("buffer.opened", name = display_name).to_string());
224        }
225
226        Ok(buffer_id)
227    }
228
229    /// Restore the split layout when the just-focused buffer would be
230    /// hidden behind a maximized split.
231    ///
232    /// `SplitManager::get_visible_buffers` renders *only* the maximized
233    /// split. A file open focuses its buffer in the active split, which —
234    /// after `redirect_active_split_away_from_dock_if_needed` — is a
235    /// regular editor leaf, not the maximized dock. With nothing reset, the
236    /// new buffer renders behind the maximized terminal: the user sees no
237    /// change, and an embedded `fresh <file>` that forwarded the open
238    /// blocks waiting for that invisible buffer to be closed, so the
239    /// terminal appears to hang. Un-maximize so the focused buffer shows.
240    fn reveal_active_split_if_hidden_by_maximize(&mut self) {
241        let mgr = self.split_manager();
242        let active: crate::model::event::SplitId = mgr.active_split().into();
243        let hidden = matches!(mgr.maximized_split(), Some(maximized) if maximized != active);
244        if !hidden {
245            return;
246        }
247        // `unmaximize_split` only errors when nothing is maximized, which
248        // the `hidden` guard above already excludes.
249        self.split_manager_mut()
250            .unmaximize_split()
251            .expect("a split is maximized (checked above)");
252        self.relayout();
253    }
254
255    /// If the active split leaf carries `SplitRole::UtilityDock`,
256    /// move the active leaf back to the user's last regular editor
257    /// leaf. Called from the file-open path so that opening a file
258    /// while a utility panel holds focus doesn't turn the dock into
259    /// a tab strip for ordinary files.
260    ///
261    /// Routing falls back to the first non-dock leaf in tree order
262    /// when the user has only ever interacted with the dock — a
263    /// rare boot-state path.
264    // `redirect_active_split_away_from_dock_if_needed` lives on
265    // `impl Window` — call it via
266    // `self.active_window_mut().redirect_active_split_away_from_dock_if_needed()`.
267
268    /// Open a file without switching focus to it
269    ///
270    /// Creates a new buffer for the file (or returns existing buffer ID if already open)
271    /// but does not change the active buffer. Useful for opening files in background tabs.
272    ///
273    /// If the file doesn't exist, creates an unsaved buffer with that filename.
274    ///
275    /// Thin delegator: the open-file core lives on `impl Window` (rooted
276    /// at the window's own `root` / `resources`). The editor forwards to
277    /// the active window.
278    pub fn open_file_no_focus(&mut self, path: &Path) -> anyhow::Result<BufferId> {
279        self.active_window_mut().open_file_no_focus(path)
280    }
281
282    /// Open a file without switching focus AND without ever
283    /// repurposing the active "no name" buffer. Thin delegator to the
284    /// active window's `Window::open_file_for_preview`.
285    pub(super) fn open_file_for_preview(&mut self, path: &Path) -> anyhow::Result<BufferId> {
286        self.active_window_mut().open_file_for_preview(path)
287    }
288
289    // `open_local_file` lives on `impl Window` — call it via
290    // `self.active_window_mut().open_local_file(path)`.
291
292    /// Open a file with a specific encoding (no auto-detection).
293    ///
294    /// Used when the user disables auto-detection in the file browser
295    /// and selects a specific encoding to use.
296    pub fn open_file_with_encoding(
297        &mut self,
298        path: &Path,
299        encoding: crate::model::buffer::Encoding,
300    ) -> anyhow::Result<BufferId> {
301        // Use the same base directory logic as open_file
302        let base_dir = self.working_dir().to_path_buf();
303
304        let resolved_path = if path.is_relative() {
305            base_dir.join(path)
306        } else {
307            path.to_path_buf()
308        };
309
310        // Save user-visible path for language detection before canonicalizing
311        let display_path = resolved_path.clone();
312
313        // Canonicalize the path
314        let canonical_path = self
315            .authority()
316            .filesystem
317            .canonicalize(&resolved_path)
318            .unwrap_or_else(|_| resolved_path.clone());
319        let path = canonical_path.as_path();
320
321        // Check if already open
322        let already_open = self
323            .buffers()
324            .iter()
325            .find(|(_, state)| state.buffer.file_path() == Some(path))
326            .map(|(id, _)| *id);
327
328        if let Some(id) = already_open {
329            // File is already open - update its encoding and reload
330            if let Some(state) = self
331                .windows
332                .get_mut(&self.active_window)
333                .map(|w| &mut w.buffers)
334                .expect("active window present")
335                .get_mut(&id)
336            {
337                state.buffer.set_encoding(encoding);
338            }
339            self.set_active_buffer(id);
340            return Ok(id);
341        }
342
343        // Create new buffer with specified encoding
344        let buffer_id = self.alloc_buffer_id();
345
346        // Load buffer with the specified encoding (use canonical path for I/O)
347        let buffer = crate::model::buffer::Buffer::load_from_file_with_encoding(
348            path,
349            encoding,
350            Arc::clone(&self.authority().filesystem),
351            crate::model::buffer::BufferConfig {
352                estimated_line_length: self.config.editor.estimated_line_length,
353            },
354        )?;
355        let first_line = buffer.first_line_lossy();
356        // Create editor state with the buffer
357        // Use display_path for language detection (glob patterns match user-visible paths)
358        let detected =
359            crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
360                &display_path,
361                first_line.as_deref(),
362                &self.grammar_registry,
363                &self.config.languages,
364                self.config.default_language.as_deref(),
365            );
366
367        let mut state = EditorState::from_buffer_with_language(buffer, detected);
368
369        state
370            .margins
371            .configure_for_line_numbers(self.config.editor.line_numbers);
372        state.reference_highlight_overlay.enabled = self.config.editor.highlight_occurrences;
373
374        self.windows
375            .get_mut(&self.active_window)
376            .map(|w| &mut w.buffers)
377            .expect("active window present")
378            .insert(buffer_id, state);
379        self.active_window_mut()
380            .event_logs
381            .insert(buffer_id, crate::model::event::EventLog::new());
382
383        let metadata = super::types::BufferMetadata::with_file(
384            path.to_path_buf(),
385            &display_path,
386            self.working_dir(),
387            self.authority().path_translation.as_ref(),
388            self.config.editor.auto_read_only,
389        );
390        self.active_window_mut()
391            .buffer_metadata
392            .insert(buffer_id, metadata);
393
394        // Add to preferred split's tabs (avoids labeled splits like sidebars)
395        let target_split = self.active_window().preferred_split_for_file();
396        let line_wrap = self.active_window().resolve_line_wrap_for_buffer(buffer_id);
397        let wrap_column = self
398            .active_window()
399            .resolve_wrap_column_for_buffer(buffer_id);
400        if let Some(view_state) = self
401            .windows
402            .get_mut(&self.active_window)
403            .and_then(|w| w.split_view_states_mut())
404            .expect("active window must have a populated split layout")
405            .get_mut(&target_split)
406        {
407            view_state.add_buffer(buffer_id);
408            let buf_state = view_state.ensure_buffer_state(buffer_id);
409            buf_state.apply_config_defaults(
410                self.config.editor.line_numbers,
411                self.config.editor.highlight_current_line,
412                line_wrap,
413                self.config.editor.wrap_indent,
414                wrap_column,
415                self.config.editor.rulers.clone(),
416                self.config.editor.scroll_offset,
417            );
418        }
419
420        self.set_active_buffer(buffer_id);
421
422        Ok(buffer_id)
423    }
424
425    /// Reload the current file with a specific encoding.
426    ///
427    /// Requires the buffer to have no unsaved modifications.
428    pub fn reload_with_encoding(
429        &mut self,
430        encoding: crate::model::buffer::Encoding,
431    ) -> anyhow::Result<()> {
432        let buffer_id = self.active_buffer();
433
434        // Get the file path
435        let path = self
436            .buffers()
437            .get(&buffer_id)
438            .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()))
439            .ok_or_else(|| anyhow::anyhow!("Buffer has no file path"))?;
440
441        // Check for unsaved modifications
442        if let Some(state) = self
443            .windows
444            .get(&self.active_window)
445            .map(|w| &w.buffers)
446            .expect("active window present")
447            .get(&buffer_id)
448        {
449            if state.buffer.is_modified() {
450                anyhow::bail!("Cannot reload: buffer has unsaved modifications");
451            }
452        }
453
454        // Reload the buffer with the new encoding
455        let new_buffer = crate::model::buffer::Buffer::load_from_file_with_encoding(
456            &path,
457            encoding,
458            Arc::clone(&self.authority().filesystem),
459            crate::model::buffer::BufferConfig {
460                estimated_line_length: self.config.editor.estimated_line_length,
461            },
462        )?;
463
464        // Update the buffer in the editor state
465        if let Some(state) = self
466            .windows
467            .get_mut(&self.active_window)
468            .map(|w| &mut w.buffers)
469            .expect("active window present")
470            .get_mut(&buffer_id)
471        {
472            state.buffer = new_buffer;
473            // Invalidate highlighting
474            state.highlighter.invalidate_all();
475        }
476
477        // Reset cursor to start in the split view state
478        let split_id = self
479            .windows
480            .get(&self.active_window)
481            .and_then(|w| w.buffers.splits())
482            .map(|(mgr, _)| mgr)
483            .expect("active window must have a populated split layout")
484            .active_split();
485        if let Some(view_state) = self
486            .windows
487            .get_mut(&self.active_window)
488            .and_then(|w| w.split_view_states_mut())
489            .expect("active window must have a populated split layout")
490            .get_mut(&split_id)
491        {
492            if let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) {
493                buf_state.cursors = crate::model::cursor::Cursors::new();
494            }
495        }
496
497        Ok(())
498    }
499
500    /// Open a large file with confirmed full loading for non-resynchronizable encoding.
501    ///
502    /// Called after user confirms they want to load a large file with an encoding like
503    /// GB18030, GBK, Shift-JIS, or EUC-KR that requires loading the entire file into memory.
504    pub fn open_file_large_encoding_confirmed(&mut self, path: &Path) -> anyhow::Result<BufferId> {
505        // Use the same base directory logic as open_file
506        let base_dir = self.working_dir().to_path_buf();
507
508        let resolved_path = if path.is_relative() {
509            base_dir.join(path)
510        } else {
511            path.to_path_buf()
512        };
513
514        // Save user-visible path for language detection before canonicalizing
515        let display_path = resolved_path.clone();
516
517        // Canonicalize the path
518        let canonical_path = self
519            .authority()
520            .filesystem
521            .canonicalize(&resolved_path)
522            .unwrap_or_else(|_| resolved_path.clone());
523        let path = canonical_path.as_path();
524
525        // Check if already open
526        let already_open = self
527            .buffers()
528            .iter()
529            .find(|(_, state)| state.buffer.file_path() == Some(path))
530            .map(|(id, _)| *id);
531
532        if let Some(id) = already_open {
533            self.set_active_buffer(id);
534            return Ok(id);
535        }
536
537        // Create new buffer with forced full loading
538        let buffer_id = self.alloc_buffer_id();
539
540        // Load buffer with forced full loading (bypasses the large file encoding check)
541        let buffer = crate::model::buffer::Buffer::load_large_file_confirmed(
542            path,
543            Arc::clone(&self.authority().filesystem),
544        )?;
545        let first_line = buffer.first_line_lossy();
546        // Create editor state with the buffer
547        // Use display_path for language detection (glob patterns match user-visible paths)
548        let detected =
549            crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
550                &display_path,
551                first_line.as_deref(),
552                &self.grammar_registry,
553                &self.config.languages,
554                self.config.default_language.as_deref(),
555            );
556
557        let mut state = EditorState::from_buffer_with_language(buffer, detected);
558
559        state
560            .margins
561            .configure_for_line_numbers(self.config.editor.line_numbers);
562        state.reference_highlight_overlay.enabled = self.config.editor.highlight_occurrences;
563
564        self.windows
565            .get_mut(&self.active_window)
566            .map(|w| &mut w.buffers)
567            .expect("active window present")
568            .insert(buffer_id, state);
569        self.active_window_mut()
570            .event_logs
571            .insert(buffer_id, crate::model::event::EventLog::new());
572
573        let metadata = super::types::BufferMetadata::with_file(
574            path.to_path_buf(),
575            &display_path,
576            self.working_dir(),
577            self.authority().path_translation.as_ref(),
578            self.config.editor.auto_read_only,
579        );
580        self.active_window_mut()
581            .buffer_metadata
582            .insert(buffer_id, metadata);
583
584        // Add to preferred split's tabs (avoids labeled splits like sidebars)
585        let target_split = self.active_window().preferred_split_for_file();
586        let line_wrap = self.active_window().resolve_line_wrap_for_buffer(buffer_id);
587        let wrap_column = self
588            .active_window()
589            .resolve_wrap_column_for_buffer(buffer_id);
590        if let Some(view_state) = self
591            .windows
592            .get_mut(&self.active_window)
593            .and_then(|w| w.split_view_states_mut())
594            .expect("active window must have a populated split layout")
595            .get_mut(&target_split)
596        {
597            view_state.add_buffer(buffer_id);
598            let buf_state = view_state.ensure_buffer_state(buffer_id);
599            buf_state.apply_config_defaults(
600                self.config.editor.line_numbers,
601                self.config.editor.highlight_current_line,
602                line_wrap,
603                self.config.editor.wrap_indent,
604                wrap_column,
605                self.config.editor.rulers.clone(),
606                self.config.editor.scroll_offset,
607            );
608        }
609
610        self.set_active_buffer(buffer_id);
611
612        // Use display_name from metadata for relative path display
613        let display_name = self
614            .active_window()
615            .buffer_metadata
616            .get(&buffer_id)
617            .map(|m| m.display_name.clone())
618            .unwrap_or_else(|| path.display().to_string());
619
620        self.active_window_mut().status_message =
621            Some(t!("buffer.opened", name = display_name).to_string());
622
623        Ok(buffer_id)
624    }
625
626    /// Restore global file state (cursor and scroll position) for a newly opened file
627    ///
628    /// This looks up the file's saved state from the global file states store
629    /// and applies it to both the EditorState (cursor) and SplitViewState (viewport).
630    // `restore_global_file_state` and `save_file_state_on_close` live
631    // on `impl Window` — call them via
632    // `self.active_window_mut().restore_global_file_state(...)` and
633    // `self.active_window().save_file_state_on_close(...)`.
634
635    /// Open the file an LSP response URI points at, handling the three
636    /// cases the goto-def / references / workspace-edit handlers all
637    /// have to think about:
638    ///
639    ///   * **on-host file** (the workspace bind mount, or a local
640    ///     authority): host-translate the URI and open the host file
641    ///     normally — exactly what the editor has always done.
642    ///   * **container-only file** (devcontainer attach with the
643    ///     target outside the workspace mount, e.g. a pip-installed
644    ///     `~/.local/.../site-packages/flask/app.py`): fetch the file
645    ///     bytes via the authority's process spawner
646    ///     (`docker exec <id> cat <path>`) and open them as a
647    ///     read-only buffer at the in-container path.
648    ///   * **unreachable** (no file at the host path; container fetch
649    ///     failed or no container authority): return `Err` so the
650    ///     caller can surface a user-visible status message instead
651    ///     of silently opening a phantom buffer.
652    ///
653    /// Cursor placement, focus, and any post-open hook firing are the
654    /// caller's job (this method just resolves "URI → BufferId").
655    pub(crate) fn open_lsp_uri_target(
656        &mut self,
657        uri: &crate::app::types::LspUri,
658    ) -> anyhow::Result<BufferId> {
659        let translation = self.authority().path_translation.clone();
660        let host_path = uri
661            .to_host_path(translation.as_ref())
662            .ok_or_else(|| anyhow::anyhow!("URI is not a file path"))?;
663
664        // Case 1: file is reachable on the host filesystem (either
665        // local authority, or workspace-mounted on a devcontainer).
666        // `open_file` focuses, which is what callers (goto-def,
667        // workspace edits) expect — they want the cursor to land in
668        // the destination buffer afterward.
669        if self.authority().filesystem.exists(&host_path) {
670            return self.open_file(&host_path);
671        }
672
673        // Case 2: container-only fetch. Only meaningful when the
674        // active authority can route a `cat` through to the
675        // container — `path_translation` being set is the proxy for
676        // "this is a container authority". Local + SSH authorities
677        // skip straight to the error case.
678        if translation.is_some() {
679            // The container-side path is the URI's raw path. Calling
680            // `to_host_path` with `None` returns the wire-side path
681            // verbatim (no translation applied) — exactly what we
682            // need for `cat <path>` inside the container.
683            let container_path = uri.to_host_path(None).ok_or_else(|| {
684                anyhow::anyhow!("URI is not a file path (container-side decode failed)")
685            })?;
686            let buffer_id = self.fetch_and_open_container_file(container_path, uri.clone())?;
687            // Match `open_file`'s focus behaviour so the cursor
688            // assertion in callers (goto-def's `MoveCursor` event)
689            // applies to the right buffer.
690            self.set_active_buffer(buffer_id);
691            return Ok(buffer_id);
692        }
693
694        // Case 3: nothing we can open.
695        Err(anyhow::anyhow!(
696            "could not open {}: file not found",
697            host_path.display()
698        ))
699    }
700
701    /// Run `cat <container_path>` through the active authority's
702    /// process spawner and open the result as a read-only buffer
703    /// tagged with the wire URI. Helper for [`Self::open_lsp_uri_target`].
704    ///
705    /// On `cat` exit-code 0 the bytes become the buffer's contents.
706    /// On any error (no tokio runtime, spawner failure, non-zero
707    /// exit) we return `Err` with a message that includes the
708    /// container path and stderr's first line — enough for the
709    /// caller's status-line surface.
710    fn fetch_and_open_container_file(
711        &mut self,
712        container_path: std::path::PathBuf,
713        uri: crate::app::types::LspUri,
714    ) -> anyhow::Result<BufferId> {
715        let runtime = self.tokio_runtime.as_ref().ok_or_else(|| {
716            anyhow::anyhow!(
717                "could not open {}: no tokio runtime available for container fetch",
718                container_path.display()
719            )
720        })?;
721
722        let spawner = self.authority().process_spawner.clone();
723        let path_arg = container_path.to_string_lossy().into_owned();
724        let result = runtime
725            .block_on(spawner.spawn("cat".into(), vec![path_arg], None))
726            .map_err(|e| {
727                anyhow::anyhow!(
728                    "could not open {} from container: {}",
729                    container_path.display(),
730                    e
731                )
732            })?;
733
734        if result.exit_code != 0 {
735            let first_stderr_line = result
736                .stderr
737                .lines()
738                .next()
739                .unwrap_or("(no error message)")
740                .trim();
741            anyhow::bail!(
742                "could not open {} from container: {}",
743                container_path.display(),
744                first_stderr_line
745            );
746        }
747
748        self.open_container_only_file(container_path, uri, result.stdout.into_bytes())
749    }
750
751    /// Build a buffer from already-fetched container content. The
752    /// buffer's `file_path` is the in-container path (so further LSP
753    /// requests carry the right URI) and the buffer is read-only —
754    /// there is no host writeback path for files that exist only
755    /// inside the container. LSP stays enabled so a follow-up
756    /// goto-def from the fetched buffer works.
757    pub(crate) fn open_container_only_file(
758        &mut self,
759        container_path: std::path::PathBuf,
760        uri: crate::app::types::LspUri,
761        content: Vec<u8>,
762    ) -> anyhow::Result<BufferId> {
763        // Don't double-open. The file_path matches by container path,
764        // since that's what we set after build.
765        let already_open = self
766            .buffers()
767            .iter()
768            .find(|(_, state)| state.buffer.file_path() == Some(container_path.as_path()))
769            .map(|(id, _)| *id);
770        if let Some(id) = already_open {
771            return Ok(id);
772        }
773
774        // Build the buffer from the fetched bytes and pin its
775        // file_path to the container path. The host filesystem ref
776        // here is mostly cosmetic — the buffer is read-only so save
777        // never runs through it.
778        let mut buffer = crate::model::buffer::Buffer::from_bytes(
779            content,
780            Arc::clone(&self.authority().filesystem),
781        );
782        buffer.rename_file_path(container_path.clone());
783
784        // Detect language from the container path (the basename's
785        // extension is what matters; the directory tree is
786        // container-side and won't match host-relative globs anyway).
787        let first_line = buffer.first_line_lossy();
788        let detected =
789            crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
790                &container_path,
791                first_line.as_deref(),
792                &self.grammar_registry,
793                &self.config.languages,
794                self.config.default_language.as_deref(),
795            );
796        let mut state = EditorState::from_buffer_with_language(buffer, detected);
797        state.editing_disabled = true;
798
799        // Whitespace / tab settings — same shape as `open_file_no_focus`
800        // so the rendered look is consistent. Container-fetched
801        // buffers should obey the user's editor config like any other
802        // read-only buffer.
803        let mut whitespace =
804            crate::config::WhitespaceVisibility::from_editor_config(&self.config.editor);
805        if let Some(lang_config) = self.config.languages.get(&state.language) {
806            whitespace = whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
807            state.buffer_settings.use_tabs =
808                lang_config.use_tabs.unwrap_or(self.config.editor.use_tabs);
809            state.buffer_settings.tab_size =
810                lang_config.tab_size.unwrap_or(self.config.editor.tab_size);
811        } else {
812            state.buffer_settings.tab_size = self.config.editor.tab_size;
813            state.buffer_settings.use_tabs = self.config.editor.use_tabs;
814        }
815        state.buffer_settings.whitespace = whitespace;
816        state
817            .margins
818            .configure_for_line_numbers(self.config.editor.line_numbers);
819        state.reference_highlight_overlay.enabled = self.config.editor.highlight_occurrences;
820
821        let buffer_id = self.alloc_buffer_id();
822        self.windows
823            .get_mut(&self.active_window)
824            .map(|w| &mut w.buffers)
825            .expect("active window present")
826            .insert(buffer_id, state);
827        self.active_window_mut()
828            .event_logs
829            .insert(buffer_id, crate::model::event::EventLog::new());
830
831        let mut metadata =
832            super::types::BufferMetadata::with_container_file(container_path.clone(), uri);
833        // Notify the LSP servers about the newly opened file so
834        // hover / further goto-def in the fetched buffer works. The
835        // URI we cached is already the wire-form URI, so the LSP
836        // sees the right path.
837        self.notify_lsp_file_opened(&container_path, buffer_id, &mut metadata);
838        self.active_window_mut()
839            .buffer_metadata
840            .insert(buffer_id, metadata);
841
842        // Wire the buffer into a tab on the preferred split, mirroring
843        // the host-file path. Skip `watch_file` — there's no host
844        // file to inotify, and the spawned-fetch is one-shot.
845        let target_split = self.active_window().preferred_split_for_file();
846        let line_wrap = self.active_window().resolve_line_wrap_for_buffer(buffer_id);
847        let wrap_column = self
848            .active_window()
849            .resolve_wrap_column_for_buffer(buffer_id);
850        if let Some(view_state) = self
851            .windows
852            .get_mut(&self.active_window)
853            .and_then(|w| w.split_view_states_mut())
854            .expect("active window must have a populated split layout")
855            .get_mut(&target_split)
856        {
857            view_state.add_buffer(buffer_id);
858            let buf_state = view_state.ensure_buffer_state(buffer_id);
859            buf_state.apply_config_defaults(
860                self.config.editor.line_numbers,
861                self.config.editor.highlight_current_line,
862                line_wrap,
863                self.config.editor.wrap_indent,
864                wrap_column,
865                self.config.editor.rulers.clone(),
866                self.config.editor.scroll_offset,
867            );
868        }
869
870        Ok(buffer_id)
871    }
872}
873
874impl crate::app::window::Window {
875    /// Open a file without switching focus to it.
876    ///
877    /// Window-scoped core of the open-file path: creates a new buffer
878    /// for the file (or returns the existing buffer id if already open)
879    /// without changing the active buffer. Rooted at this window's own
880    /// `root` / `resources` so it can open files directly into a
881    /// non-active window (e.g. workspace restore) with no active-window
882    /// flip. If the file doesn't exist, creates an unsaved buffer with
883    /// that filename.
884    pub fn open_file_no_focus(&mut self, path: &Path) -> anyhow::Result<BufferId> {
885        self.open_file_no_focus_inner(path, true, OpenKind::Commit)
886    }
887
888    /// `open_file_no_focus` with an explicit [`OpenKind`], so the Editor-side
889    /// `open_file_with_kind` can route a preview open through the same core
890    /// while deferring `after_file_open`.
891    pub(crate) fn open_file_no_focus_with_kind(
892        &mut self,
893        path: &Path,
894        kind: OpenKind,
895    ) -> anyhow::Result<BufferId> {
896        self.open_file_no_focus_inner(path, true, kind)
897    }
898
899    /// Open a file without switching focus AND without ever
900    /// repurposing the active "no name" buffer.
901    ///
902    /// `open_file_no_focus`'s `replace_current` heuristic reuses the
903    /// initial empty unnamed buffer for the *first* file the user
904    /// opens — convenient for the normal "fresh launch → open file"
905    /// flow. The Live Grep floating overlay's preview pane needs the
906    /// opposite: the user's current buffer (often the empty unnamed
907    /// scratch) must stay untouched as preview cycles through
908    /// results. This variant always allocates a fresh BufferId so the
909    /// background buffer never gets repurposed.
910    pub(crate) fn open_file_for_preview(&mut self, path: &Path) -> anyhow::Result<BufferId> {
911        // Live-grep preview is a browse, not a deliberate open: defer the
912        // `after_file_open` hook so plugins don't pop UI / run side effects
913        // over each result the user cycles through.
914        self.open_file_no_focus_inner(path, false, OpenKind::Preview)
915    }
916
917    /// True if `path` is an internal app artifact (terminal scrollback,
918    /// git-show output, etc.) that the user is inspecting rather than
919    /// editing — i.e. it lives under the app data dir but outside this
920    /// window's own project root. A session working tree can itself live
921    /// under the data dir (conductor / orchestrator sessions); files under
922    /// the window root are real working files, not artifacts.
923    /// Paths are canonicalized so a symlinked home (e.g. macOS
924    /// `/var` → `/private/var`) doesn't defeat the prefix checks.
925    fn is_internal_data_artifact(&self, path: &Path) -> bool {
926        let canonicalize = |p: &Path| {
927            self.authority()
928                .filesystem
929                .canonicalize(p)
930                .unwrap_or_else(|_| p.to_path_buf())
931        };
932        let canonical_data = canonicalize(&self.resources.dir_context.data_dir);
933        let canonical_root = canonicalize(&self.root);
934        path.starts_with(&canonical_data) && !path.starts_with(&canonical_root)
935    }
936
937    fn open_file_no_focus_inner(
938        &mut self,
939        path: &Path,
940        allow_replace_empty: bool,
941        kind: OpenKind,
942    ) -> anyhow::Result<BufferId> {
943        // Fail fast if the remote connection is down — don't attempt I/O that
944        // would either timeout or return confusing errors.
945        if !self.authority().filesystem.is_remote_connected() {
946            anyhow::bail!(
947                "Cannot open file: remote connection lost ({})",
948                self.authority()
949                    .filesystem
950                    .remote_connection_info()
951                    .unwrap_or("unknown host")
952            );
953        }
954
955        // Resolve relative paths against appropriate base directory.
956        // For remote mode, use the remote home directory; for local, use
957        // this window's root.
958        let base_dir = if self
959            .authority()
960            .filesystem
961            .remote_connection_info()
962            .is_some()
963        {
964            self.authority()
965                .filesystem
966                .home_dir()
967                .unwrap_or_else(|_| self.root.clone())
968        } else {
969            self.root.clone()
970        };
971
972        let resolved_path = if path.is_relative() {
973            base_dir.join(path)
974        } else {
975            path.to_path_buf()
976        };
977
978        // Determine if we're opening a non-existent file (for creating new files)
979        // Use filesystem trait method to support remote files
980        let file_exists = self.authority().filesystem.exists(&resolved_path);
981
982        // Save the user-visible (non-canonicalized) path for language detection.
983        // Glob patterns in language config should match the path as the user sees it,
984        // not the canonical path (e.g., on macOS /var -> /private/var symlinks).
985        let display_path = resolved_path.clone();
986
987        // Canonicalize the path to resolve symlinks and normalize path components
988        // This ensures consistent path representation throughout the editor
989        // For non-existent files, we need to canonicalize the parent directory and append the filename
990        let canonical_path = if file_exists {
991            self.authority()
992                .filesystem
993                .canonicalize(&resolved_path)
994                .unwrap_or_else(|_| resolved_path.clone())
995        } else {
996            // For non-existent files, canonicalize parent dir and append filename
997            if let Some(parent) = resolved_path.parent() {
998                let canonical_parent = if parent.as_os_str().is_empty() {
999                    // No parent means just a filename, use base dir
1000                    base_dir.clone()
1001                } else {
1002                    self.authority()
1003                        .filesystem
1004                        .canonicalize(parent)
1005                        .unwrap_or_else(|_| parent.to_path_buf())
1006                };
1007                if let Some(filename) = resolved_path.file_name() {
1008                    canonical_parent.join(filename)
1009                } else {
1010                    resolved_path
1011                }
1012            } else {
1013                resolved_path
1014            }
1015        };
1016        let path = canonical_path.as_path();
1017
1018        // Check if the path is a directory (after following symlinks via canonicalize)
1019        // Directories cannot be opened as files in the editor
1020        // Use filesystem trait method to support remote files
1021        if self.authority().filesystem.is_dir(path).unwrap_or(false) {
1022            anyhow::bail!(t!("buffer.cannot_open_directory"));
1023        }
1024
1025        // Check if file is already open - return existing buffer without switching
1026        let already_open = self
1027            .buffers
1028            .iter()
1029            .find(|(_, state)| state.buffer.file_path() == Some(path))
1030            .map(|(id, _)| *id);
1031
1032        if let Some(id) = already_open {
1033            return Ok(id);
1034        }
1035
1036        // If the current buffer is empty and unmodified, replace it instead of creating a new one
1037        // Note: Don't replace composite buffers (they appear empty but are special views).
1038        // Suppressed when `allow_replace_empty` is false — see
1039        // `open_file_for_preview` for the rationale.
1040        let replace_current = allow_replace_empty && {
1041            let current_state = self.buffers.get(&self.active_buffer()).unwrap();
1042            !current_state.is_composite_buffer
1043                && current_state.buffer.is_empty()
1044                && !current_state.buffer.is_modified()
1045                && current_state.buffer.file_path().is_none()
1046        };
1047
1048        let buffer_id = if replace_current {
1049            // Reuse the current empty buffer
1050            self.active_buffer()
1051        } else {
1052            // Create new buffer for this file
1053            self.alloc_buffer_id()
1054        };
1055
1056        // Create the editor state - either load from file or create empty buffer
1057        tracing::info!(
1058            "[SYNTAX DEBUG] open_file_no_focus: path={:?}, extension={:?}, catalog={}",
1059            path,
1060            path.extension(),
1061            self.resources.grammar_registry.catalog().len(),
1062        );
1063        let mut state = if file_exists {
1064            // Load from canonical path (for I/O and dedup), detect language from
1065            // display path (for glob pattern matching against user-visible names).
1066            let buffer = crate::model::buffer::Buffer::load_from_file(
1067                &canonical_path,
1068                self.resources.config.editor.large_file_threshold_bytes as usize,
1069                Arc::clone(&self.authority().filesystem),
1070            )?;
1071            let first_line = buffer.first_line_lossy();
1072            let detected =
1073                crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
1074                    &display_path,
1075                    first_line.as_deref(),
1076                    &self.resources.grammar_registry,
1077                    &self.resources.config.languages,
1078                    self.resources.config.default_language.as_deref(),
1079                );
1080            EditorState::from_buffer_with_language(buffer, detected)
1081        } else {
1082            // File doesn't exist - create empty buffer with the file path set
1083            EditorState::new_with_path(
1084                self.resources.config.editor.large_file_threshold_bytes as usize,
1085                Arc::clone(&self.authority().filesystem),
1086                path.to_path_buf(),
1087            )
1088        };
1089        // Note: line_wrap_enabled is set on SplitViewState.viewport when the split is created
1090
1091        // Check if the buffer contains binary content
1092        let is_binary = state.buffer.is_binary();
1093        if is_binary {
1094            // Make binary buffers read-only
1095            state.editing_disabled = true;
1096            tracing::info!("Detected binary file: {}", path.display());
1097        }
1098
1099        // Internal app artifacts under the data dir (e.g. terminal scrollback
1100        // backing files surfaced by Universal Search) are things the user is
1101        // inspecting, not editing — open them read-only so an accidental
1102        // keystroke can't corrupt persisted state. Files inside the window's
1103        // own root are excluded so session working trees that live under the
1104        // data dir stay editable.
1105        if self.is_internal_data_artifact(&canonical_path) {
1106            state.editing_disabled = true;
1107        }
1108
1109        // Set whitespace visibility, use_tabs, and tab_size based on language config
1110        // with fallback to global editor config for tab_size
1111        // Use the buffer's stored language (already set by from_file_with_languages)
1112        let mut whitespace =
1113            crate::config::WhitespaceVisibility::from_editor_config(&self.resources.config.editor);
1114        state.buffer_settings.auto_close = self.resources.config.editor.auto_close;
1115        state.buffer_settings.auto_surround = self.resources.config.editor.auto_surround;
1116        if let Some(lang_config) = self.resources.config.languages.get(&state.language) {
1117            whitespace = whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
1118            state.buffer_settings.use_tabs = lang_config
1119                .use_tabs
1120                .unwrap_or(self.resources.config.editor.use_tabs);
1121            // Use language-specific tab_size if set, otherwise fall back to global
1122            state.buffer_settings.tab_size = lang_config
1123                .tab_size
1124                .unwrap_or(self.resources.config.editor.tab_size);
1125            // Auto close: language override (only if globally enabled)
1126            if state.buffer_settings.auto_close {
1127                if let Some(lang_auto_close) = lang_config.auto_close {
1128                    state.buffer_settings.auto_close = lang_auto_close;
1129                }
1130            }
1131            // Auto surround: language override (only if globally enabled)
1132            if state.buffer_settings.auto_surround {
1133                if let Some(lang_auto_surround) = lang_config.auto_surround {
1134                    state.buffer_settings.auto_surround = lang_auto_surround;
1135                }
1136            }
1137        } else {
1138            state.buffer_settings.tab_size = self.resources.config.editor.tab_size;
1139            state.buffer_settings.use_tabs = self.resources.config.editor.use_tabs;
1140        }
1141        state.buffer_settings.whitespace = whitespace;
1142
1143        // Apply line_numbers default from config
1144        state
1145            .margins
1146            .configure_for_line_numbers(self.resources.config.editor.line_numbers);
1147        state.reference_highlight_overlay.enabled =
1148            self.resources.config.editor.highlight_occurrences;
1149
1150        self.buffers.insert(buffer_id, state);
1151        self.event_logs
1152            .insert(buffer_id, crate::model::event::EventLog::new());
1153
1154        // Create metadata for this buffer
1155        let mut metadata = crate::app::types::BufferMetadata::with_file(
1156            path.to_path_buf(),
1157            &display_path,
1158            &self.root,
1159            self.authority().path_translation.as_ref(),
1160            self.resources.config.editor.auto_read_only,
1161        );
1162
1163        // Mark binary files in metadata and disable LSP
1164        if is_binary {
1165            metadata.binary = true;
1166            metadata.read_only = true;
1167            metadata.disable_lsp(t!("buffer.binary_file").to_string());
1168        }
1169
1170        // Check if the file is read-only on disk (filesystem permissions),
1171        // unless the user opted out of automatic read-only via config
1172        if file_exists
1173            && !metadata.read_only
1174            && self.resources.config.editor.auto_read_only
1175            && !self.authority().filesystem.is_writable(path)
1176        {
1177            metadata.read_only = true;
1178        }
1179
1180        // Mark read-only files (library, binary, or filesystem-readonly) as editing-disabled
1181        if metadata.read_only {
1182            if let Some(state) = self.buffers.get_mut(&buffer_id) {
1183                state.editing_disabled = true;
1184            }
1185        }
1186
1187        // Notify LSP about the newly opened file (skip for binary files)
1188        if !is_binary {
1189            self.notify_lsp_file_opened(path, buffer_id, &mut metadata);
1190        }
1191
1192        // Store metadata for this buffer
1193        self.buffer_metadata.insert(buffer_id, metadata);
1194
1195        // Add buffer to the preferred split's tabs (but don't switch to it)
1196        // Uses preferred_split_for_file() to avoid opening in labeled splits (e.g., sidebars)
1197        let target_split = self.preferred_split_for_file();
1198        let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
1199        let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
1200        let page_view = self.resolve_page_view_for_buffer(buffer_id);
1201        // Snapshot config values before taking the mutable view-states borrow
1202        // so the closure body doesn't have to re-borrow `self.resources`.
1203        let cfg = self.resources.config.editor.clone();
1204        if let Some(view_state) = self
1205            .split_view_states_mut()
1206            .expect("active window must have a populated split layout")
1207            .get_mut(&target_split)
1208        {
1209            view_state.add_buffer(buffer_id);
1210            // Initialize per-buffer view state for the new buffer with config defaults
1211            let buf_state = view_state.ensure_buffer_state(buffer_id);
1212            buf_state.apply_config_defaults(
1213                cfg.line_numbers,
1214                cfg.highlight_current_line,
1215                line_wrap,
1216                cfg.wrap_indent,
1217                wrap_column,
1218                cfg.rulers,
1219                cfg.scroll_offset,
1220            );
1221            // Auto-activate page view if configured for this language
1222            if let Some(page_width) = page_view {
1223                buf_state.activate_page_view(page_width);
1224            }
1225        }
1226
1227        // Restore global file state (scroll/cursor position) if available
1228        // This persists file positions across projects and editor instances
1229        self.restore_global_file_state(buffer_id, path, target_split);
1230
1231        // Emit control event
1232        self.resources.event_broadcaster.emit_named(
1233            crate::model::control_event::events::FILE_OPENED.name,
1234            serde_json::json!({
1235                "path": path.display().to_string(),
1236                "buffer_id": buffer_id.0
1237            }),
1238        );
1239
1240        // Track file for auto-revert and conflict detection
1241        self.watch_file(path);
1242
1243        // Fire AfterFileOpen hook for plugins — but not for preview opens
1244        // (file-explorer browse, live-grep overlay). A preview is "just
1245        // looking": firing this hook lets plugins raise intrusive UI (e.g.
1246        // the asm-lsp config-offer popup) or run side effects (csharp
1247        // `dotnet restore`) over a file the user is merely glancing at as
1248        // previews replace each other. For a preview the hook is deferred —
1249        // it fires once the buffer is escalated to a permanent tab (see
1250        // `Window::promote_*`). Plugins that need to react when a preview
1251        // becomes visible use `buffer_activated`, which still fires on every
1252        // preview switch.
1253        if kind == OpenKind::Commit {
1254            self.run_after_file_open_hook(buffer_id, path.to_path_buf());
1255        }
1256
1257        Ok(buffer_id)
1258    }
1259
1260    /// Fire the `after_file_open` plugin hook for `buffer_id`. Single site so
1261    /// both the commit-time open path and the escalation (promote) path raise
1262    /// it identically.
1263    pub(crate) fn run_after_file_open_hook(&self, buffer_id: BufferId, path: std::path::PathBuf) {
1264        self.resources.plugin_manager.read().unwrap().run_hook(
1265            "after_file_open",
1266            crate::services::plugins::hooks::HookArgs::AfterFileOpen { buffer_id, path },
1267        );
1268    }
1269
1270    /// Fire the deferred `after_file_open` hook for a buffer that was opened
1271    /// as a preview and is now being escalated to a permanent tab. Looks up
1272    /// the buffer's own file path; a no-op for buffers without one.
1273    pub(crate) fn fire_deferred_after_file_open(&self, buffer_id: BufferId) {
1274        if let Some(path) = self
1275            .buffers
1276            .get(&buffer_id)
1277            .and_then(|s| s.buffer.file_path())
1278            .filter(|p| !p.as_os_str().is_empty())
1279            .map(|p| p.to_path_buf())
1280        {
1281            self.run_after_file_open_hook(buffer_id, path);
1282        }
1283    }
1284}