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}