1use std::path::Path;
15use std::sync::Arc;
16
17use rust_i18n::t;
18
19use crate::model::event::{BufferId, LeafId};
20use crate::state::EditorState;
21
22use super::Editor;
23
24impl Editor {
25 pub fn open_file(&mut self, path: &Path) -> anyhow::Result<BufferId> {
30 let active_had_path = self
34 .buffers
35 .get(&self.active_buffer())
36 .and_then(|s| s.buffer.file_path())
37 .is_some();
38
39 let buffer_id = self.open_file_no_focus(path)?;
40
41 let is_new_buffer = self.active_buffer() != buffer_id;
45
46 if is_new_buffer && !self.suppress_position_history_once {
47 self.position_history.commit_pending_movement();
49
50 let cursors = self.active_cursors();
52 let position = cursors.primary().position;
53 let anchor = cursors.primary().anchor;
54 self.position_history
55 .record_movement(self.active_buffer(), position, anchor);
56 self.position_history.commit_pending_movement();
57 }
58
59 self.set_active_buffer(buffer_id);
60
61 if !is_new_buffer && !active_had_path {
68 #[cfg(feature = "plugins")]
69 self.update_plugin_state_snapshot();
70
71 self.plugin_manager.run_hook(
72 "buffer_activated",
73 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
74 );
75 }
76
77 let display_name = self
79 .buffer_metadata
80 .get(&buffer_id)
81 .map(|m| m.display_name.clone())
82 .unwrap_or_else(|| path.display().to_string());
83
84 let is_binary = self
86 .buffers
87 .get(&buffer_id)
88 .map(|s| s.buffer.is_binary())
89 .unwrap_or(false);
90
91 if is_binary {
93 self.status_message = Some(t!("buffer.opened_binary", name = display_name).to_string());
94 } else {
95 self.status_message = Some(t!("buffer.opened", name = display_name).to_string());
96 }
97
98 Ok(buffer_id)
99 }
100
101 pub fn open_file_no_focus(&mut self, path: &Path) -> anyhow::Result<BufferId> {
108 if !self.authority.filesystem.is_remote_connected() {
111 anyhow::bail!(
112 "Cannot open file: remote connection lost ({})",
113 self.authority
114 .filesystem
115 .remote_connection_info()
116 .unwrap_or("unknown host")
117 );
118 }
119
120 let base_dir = if self.authority.filesystem.remote_connection_info().is_some() {
123 self.authority
124 .filesystem
125 .home_dir()
126 .unwrap_or_else(|_| self.working_dir.clone())
127 } else {
128 self.working_dir.clone()
129 };
130
131 let resolved_path = if path.is_relative() {
132 base_dir.join(path)
133 } else {
134 path.to_path_buf()
135 };
136
137 let file_exists = self.authority.filesystem.exists(&resolved_path);
140
141 let display_path = resolved_path.clone();
145
146 let canonical_path = if file_exists {
150 self.authority
151 .filesystem
152 .canonicalize(&resolved_path)
153 .unwrap_or_else(|_| resolved_path.clone())
154 } else {
155 if let Some(parent) = resolved_path.parent() {
157 let canonical_parent = if parent.as_os_str().is_empty() {
158 base_dir.clone()
160 } else {
161 self.authority
162 .filesystem
163 .canonicalize(parent)
164 .unwrap_or_else(|_| parent.to_path_buf())
165 };
166 if let Some(filename) = resolved_path.file_name() {
167 canonical_parent.join(filename)
168 } else {
169 resolved_path
170 }
171 } else {
172 resolved_path
173 }
174 };
175 let path = canonical_path.as_path();
176
177 if self.authority.filesystem.is_dir(path).unwrap_or(false) {
181 anyhow::bail!(t!("buffer.cannot_open_directory"));
182 }
183
184 let already_open = self
186 .buffers
187 .iter()
188 .find(|(_, state)| state.buffer.file_path() == Some(path))
189 .map(|(id, _)| *id);
190
191 if let Some(id) = already_open {
192 return Ok(id);
193 }
194
195 let replace_current = {
198 let current_state = self.buffers.get(&self.active_buffer()).unwrap();
199 !current_state.is_composite_buffer
200 && current_state.buffer.is_empty()
201 && !current_state.buffer.is_modified()
202 && current_state.buffer.file_path().is_none()
203 };
204
205 let buffer_id = if replace_current {
206 self.active_buffer()
208 } else {
209 let id = BufferId(self.next_buffer_id);
211 self.next_buffer_id += 1;
212 id
213 };
214
215 tracing::info!(
217 "[SYNTAX DEBUG] open_file_no_focus: path={:?}, extension={:?}, catalog={}",
218 path,
219 path.extension(),
220 self.grammar_registry.catalog().len(),
221 );
222 let mut state = if file_exists {
223 let buffer = crate::model::buffer::Buffer::load_from_file(
226 &canonical_path,
227 self.config.editor.large_file_threshold_bytes as usize,
228 Arc::clone(&self.authority.filesystem),
229 )?;
230 let first_line = buffer.first_line_lossy();
231 let detected =
232 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
233 &display_path,
234 first_line.as_deref(),
235 &self.grammar_registry,
236 &self.config.languages,
237 self.config.default_language.as_deref(),
238 );
239 EditorState::from_buffer_with_language(buffer, detected)
240 } else {
241 EditorState::new_with_path(
243 self.config.editor.large_file_threshold_bytes as usize,
244 Arc::clone(&self.authority.filesystem),
245 path.to_path_buf(),
246 )
247 };
248 let is_binary = state.buffer.is_binary();
252 if is_binary {
253 state.editing_disabled = true;
255 tracing::info!("Detected binary file: {}", path.display());
256 }
257
258 let mut whitespace =
262 crate::config::WhitespaceVisibility::from_editor_config(&self.config.editor);
263 state.buffer_settings.auto_close = self.config.editor.auto_close;
264 state.buffer_settings.auto_surround = self.config.editor.auto_surround;
265 if let Some(lang_config) = self.config.languages.get(&state.language) {
266 whitespace = whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
267 state.buffer_settings.use_tabs =
268 lang_config.use_tabs.unwrap_or(self.config.editor.use_tabs);
269 state.buffer_settings.tab_size =
271 lang_config.tab_size.unwrap_or(self.config.editor.tab_size);
272 if state.buffer_settings.auto_close {
274 if let Some(lang_auto_close) = lang_config.auto_close {
275 state.buffer_settings.auto_close = lang_auto_close;
276 }
277 }
278 if state.buffer_settings.auto_surround {
280 if let Some(lang_auto_surround) = lang_config.auto_surround {
281 state.buffer_settings.auto_surround = lang_auto_surround;
282 }
283 }
284 } else {
285 state.buffer_settings.tab_size = self.config.editor.tab_size;
286 state.buffer_settings.use_tabs = self.config.editor.use_tabs;
287 }
288 state.buffer_settings.whitespace = whitespace;
289
290 state
292 .margins
293 .configure_for_line_numbers(self.config.editor.line_numbers);
294
295 self.buffers.insert(buffer_id, state);
296 self.event_logs
297 .insert(buffer_id, crate::model::event::EventLog::new());
298
299 let mut metadata = super::types::BufferMetadata::with_file(
301 path.to_path_buf(),
302 &display_path,
303 &self.working_dir,
304 );
305
306 if is_binary {
308 metadata.binary = true;
309 metadata.read_only = true;
310 metadata.disable_lsp(t!("buffer.binary_file").to_string());
311 }
312
313 if file_exists && !metadata.read_only && !self.authority.filesystem.is_writable(path) {
315 metadata.read_only = true;
316 }
317
318 if metadata.read_only {
320 if let Some(state) = self.buffers.get_mut(&buffer_id) {
321 state.editing_disabled = true;
322 }
323 }
324
325 if !is_binary {
327 self.notify_lsp_file_opened(path, buffer_id, &mut metadata);
328 }
329
330 self.buffer_metadata.insert(buffer_id, metadata);
332
333 let target_split = self.preferred_split_for_file();
336 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
337 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
338 let page_view = self.resolve_page_view_for_buffer(buffer_id);
339 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
340 view_state.add_buffer(buffer_id);
341 let buf_state = view_state.ensure_buffer_state(buffer_id);
343 buf_state.apply_config_defaults(
344 self.config.editor.line_numbers,
345 self.config.editor.highlight_current_line,
346 line_wrap,
347 self.config.editor.wrap_indent,
348 wrap_column,
349 self.config.editor.rulers.clone(),
350 );
351 if let Some(page_width) = page_view {
353 buf_state.activate_page_view(page_width);
354 }
355 }
356
357 self.restore_global_file_state(buffer_id, path, target_split);
360
361 self.emit_event(
363 crate::model::control_event::events::FILE_OPENED.name,
364 serde_json::json!({
365 "path": path.display().to_string(),
366 "buffer_id": buffer_id.0
367 }),
368 );
369
370 self.watch_file(path);
372
373 self.plugin_manager.run_hook(
375 "after_file_open",
376 crate::services::plugins::hooks::HookArgs::AfterFileOpen {
377 buffer_id,
378 path: path.to_path_buf(),
379 },
380 );
381
382 Ok(buffer_id)
383 }
384
385 pub fn open_local_file(&mut self, path: &Path) -> anyhow::Result<BufferId> {
391 let resolved_path = if path.is_relative() {
393 self.working_dir.join(path)
394 } else {
395 path.to_path_buf()
396 };
397
398 let display_path = resolved_path.clone();
400
401 let canonical_path = resolved_path
403 .canonicalize()
404 .unwrap_or_else(|_| resolved_path.clone());
405 let path = canonical_path.as_path();
406
407 let already_open = self
409 .buffers
410 .iter()
411 .find(|(_, state)| state.buffer.file_path() == Some(path))
412 .map(|(id, _)| *id);
413
414 if let Some(id) = already_open {
415 self.set_active_buffer(id);
416 return Ok(id);
417 }
418
419 let buffer_id = BufferId(self.next_buffer_id);
421 self.next_buffer_id += 1;
422
423 let buffer = crate::model::buffer::Buffer::load_from_file(
426 &canonical_path,
427 self.config.editor.large_file_threshold_bytes as usize,
428 Arc::clone(&self.local_filesystem),
429 )?;
430 let first_line = buffer.first_line_lossy();
431 let detected =
432 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
433 &display_path,
434 first_line.as_deref(),
435 &self.grammar_registry,
436 &self.config.languages,
437 self.config.default_language.as_deref(),
438 );
439 let state = EditorState::from_buffer_with_language(buffer, detected);
440
441 self.buffers.insert(buffer_id, state);
442 self.event_logs
443 .insert(buffer_id, crate::model::event::EventLog::new());
444
445 let metadata = super::types::BufferMetadata::with_file(
447 path.to_path_buf(),
448 &display_path,
449 &self.working_dir,
450 );
451 self.buffer_metadata.insert(buffer_id, metadata);
452
453 let target_split = self.preferred_split_for_file();
455 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
456 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
457 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
458 view_state.add_buffer(buffer_id);
459 let buf_state = view_state.ensure_buffer_state(buffer_id);
460 buf_state.apply_config_defaults(
461 self.config.editor.line_numbers,
462 self.config.editor.highlight_current_line,
463 line_wrap,
464 self.config.editor.wrap_indent,
465 wrap_column,
466 self.config.editor.rulers.clone(),
467 );
468 }
469
470 self.set_active_buffer(buffer_id);
471
472 let display_name = path.display().to_string();
473 self.status_message = Some(t!("buffer.opened", name = display_name).to_string());
474
475 Ok(buffer_id)
476 }
477
478 pub fn open_file_with_encoding(
483 &mut self,
484 path: &Path,
485 encoding: crate::model::buffer::Encoding,
486 ) -> anyhow::Result<BufferId> {
487 let base_dir = self.working_dir.clone();
489
490 let resolved_path = if path.is_relative() {
491 base_dir.join(path)
492 } else {
493 path.to_path_buf()
494 };
495
496 let display_path = resolved_path.clone();
498
499 let canonical_path = self
501 .authority
502 .filesystem
503 .canonicalize(&resolved_path)
504 .unwrap_or_else(|_| resolved_path.clone());
505 let path = canonical_path.as_path();
506
507 let already_open = self
509 .buffers
510 .iter()
511 .find(|(_, state)| state.buffer.file_path() == Some(path))
512 .map(|(id, _)| *id);
513
514 if let Some(id) = already_open {
515 if let Some(state) = self.buffers.get_mut(&id) {
517 state.buffer.set_encoding(encoding);
518 }
519 self.set_active_buffer(id);
520 return Ok(id);
521 }
522
523 let buffer_id = BufferId(self.next_buffer_id);
525 self.next_buffer_id += 1;
526
527 let buffer = crate::model::buffer::Buffer::load_from_file_with_encoding(
529 path,
530 encoding,
531 Arc::clone(&self.authority.filesystem),
532 crate::model::buffer::BufferConfig {
533 estimated_line_length: self.config.editor.estimated_line_length,
534 },
535 )?;
536 let first_line = buffer.first_line_lossy();
537 let detected =
540 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
541 &display_path,
542 first_line.as_deref(),
543 &self.grammar_registry,
544 &self.config.languages,
545 self.config.default_language.as_deref(),
546 );
547
548 let mut state = EditorState::from_buffer_with_language(buffer, detected);
549
550 state
551 .margins
552 .configure_for_line_numbers(self.config.editor.line_numbers);
553
554 self.buffers.insert(buffer_id, state);
555 self.event_logs
556 .insert(buffer_id, crate::model::event::EventLog::new());
557
558 let metadata = super::types::BufferMetadata::with_file(
559 path.to_path_buf(),
560 &display_path,
561 &self.working_dir,
562 );
563 self.buffer_metadata.insert(buffer_id, metadata);
564
565 let target_split = self.preferred_split_for_file();
567 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
568 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
569 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
570 view_state.add_buffer(buffer_id);
571 let buf_state = view_state.ensure_buffer_state(buffer_id);
572 buf_state.apply_config_defaults(
573 self.config.editor.line_numbers,
574 self.config.editor.highlight_current_line,
575 line_wrap,
576 self.config.editor.wrap_indent,
577 wrap_column,
578 self.config.editor.rulers.clone(),
579 );
580 }
581
582 self.set_active_buffer(buffer_id);
583
584 Ok(buffer_id)
585 }
586
587 pub fn reload_with_encoding(
591 &mut self,
592 encoding: crate::model::buffer::Encoding,
593 ) -> anyhow::Result<()> {
594 let buffer_id = self.active_buffer();
595
596 let path = self
598 .buffers
599 .get(&buffer_id)
600 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()))
601 .ok_or_else(|| anyhow::anyhow!("Buffer has no file path"))?;
602
603 if let Some(state) = self.buffers.get(&buffer_id) {
605 if state.buffer.is_modified() {
606 anyhow::bail!("Cannot reload: buffer has unsaved modifications");
607 }
608 }
609
610 let new_buffer = crate::model::buffer::Buffer::load_from_file_with_encoding(
612 &path,
613 encoding,
614 Arc::clone(&self.authority.filesystem),
615 crate::model::buffer::BufferConfig {
616 estimated_line_length: self.config.editor.estimated_line_length,
617 },
618 )?;
619
620 if let Some(state) = self.buffers.get_mut(&buffer_id) {
622 state.buffer = new_buffer;
623 state.highlighter.invalidate_all();
625 }
626
627 let split_id = self.split_manager.active_split();
629 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
630 if let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) {
631 buf_state.cursors = crate::model::cursor::Cursors::new();
632 }
633 }
634
635 Ok(())
636 }
637
638 pub fn open_file_large_encoding_confirmed(&mut self, path: &Path) -> anyhow::Result<BufferId> {
643 let base_dir = self.working_dir.clone();
645
646 let resolved_path = if path.is_relative() {
647 base_dir.join(path)
648 } else {
649 path.to_path_buf()
650 };
651
652 let display_path = resolved_path.clone();
654
655 let canonical_path = self
657 .authority
658 .filesystem
659 .canonicalize(&resolved_path)
660 .unwrap_or_else(|_| resolved_path.clone());
661 let path = canonical_path.as_path();
662
663 let already_open = self
665 .buffers
666 .iter()
667 .find(|(_, state)| state.buffer.file_path() == Some(path))
668 .map(|(id, _)| *id);
669
670 if let Some(id) = already_open {
671 self.set_active_buffer(id);
672 return Ok(id);
673 }
674
675 let buffer_id = BufferId(self.next_buffer_id);
677 self.next_buffer_id += 1;
678
679 let buffer = crate::model::buffer::Buffer::load_large_file_confirmed(
681 path,
682 Arc::clone(&self.authority.filesystem),
683 )?;
684 let first_line = buffer.first_line_lossy();
685 let detected =
688 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
689 &display_path,
690 first_line.as_deref(),
691 &self.grammar_registry,
692 &self.config.languages,
693 self.config.default_language.as_deref(),
694 );
695
696 let mut state = EditorState::from_buffer_with_language(buffer, detected);
697
698 state
699 .margins
700 .configure_for_line_numbers(self.config.editor.line_numbers);
701
702 self.buffers.insert(buffer_id, state);
703 self.event_logs
704 .insert(buffer_id, crate::model::event::EventLog::new());
705
706 let metadata = super::types::BufferMetadata::with_file(
707 path.to_path_buf(),
708 &display_path,
709 &self.working_dir,
710 );
711 self.buffer_metadata.insert(buffer_id, metadata);
712
713 let target_split = self.preferred_split_for_file();
715 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
716 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
717 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
718 view_state.add_buffer(buffer_id);
719 let buf_state = view_state.ensure_buffer_state(buffer_id);
720 buf_state.apply_config_defaults(
721 self.config.editor.line_numbers,
722 self.config.editor.highlight_current_line,
723 line_wrap,
724 self.config.editor.wrap_indent,
725 wrap_column,
726 self.config.editor.rulers.clone(),
727 );
728 }
729
730 self.set_active_buffer(buffer_id);
731
732 let display_name = self
734 .buffer_metadata
735 .get(&buffer_id)
736 .map(|m| m.display_name.clone())
737 .unwrap_or_else(|| path.display().to_string());
738
739 self.status_message = Some(t!("buffer.opened", name = display_name).to_string());
740
741 Ok(buffer_id)
742 }
743
744 fn restore_global_file_state(&mut self, buffer_id: BufferId, path: &Path, split_id: LeafId) {
749 use crate::workspace::PersistedFileWorkspace;
750
751 let file_state = match PersistedFileWorkspace::load(path) {
753 Some(state) => state,
754 None => return, };
756
757 let max_pos = match self.buffers.get(&buffer_id) {
759 Some(buffer) => buffer.buffer.len(),
760 None => return,
761 };
762
763 let view_state_opt = self.split_view_states.get_mut(&split_id);
767 let buffer_state_opt = self.buffers.get_mut(&buffer_id);
768 if let (Some(view_state), Some(buffer_state)) = (view_state_opt, buffer_state_opt) {
769 if let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) {
770 let cursor_pos = file_state.cursor.position.min(max_pos);
771 buf_state.cursors.primary_mut().position = cursor_pos;
772 buf_state.cursors.primary_mut().anchor =
773 file_state.cursor.anchor.map(|a| a.min(max_pos));
774 }
775 view_state.viewport.top_byte = file_state.scroll.top_byte;
776 view_state.viewport.left_column = file_state.scroll.left_column;
777 super::navigation::reconcile_restored_buffer_view(view_state, &mut buffer_state.buffer);
784 }
785 }
786
787 pub(super) fn save_file_state_on_close(&self, buffer_id: BufferId) {
789 use crate::workspace::{
790 PersistedFileWorkspace, SerializedCursor, SerializedFileState, SerializedScroll,
791 };
792
793 let abs_path = match self.buffer_metadata.get(&buffer_id) {
795 Some(metadata) => match metadata.file_path() {
796 Some(path) => path.to_path_buf(),
797 None => return, },
799 None => return,
800 };
801
802 let view_state = self
804 .split_view_states
805 .values()
806 .find(|vs| vs.has_buffer(buffer_id));
807
808 let view_state = match view_state {
809 Some(vs) => vs,
810 None => return, };
812
813 let buf_state = match view_state.keyed_states.get(&buffer_id) {
815 Some(bs) => bs,
816 None => return,
817 };
818
819 let primary_cursor = buf_state.cursors.primary();
821 let file_state = SerializedFileState {
822 cursor: SerializedCursor {
823 position: primary_cursor.position,
824 anchor: primary_cursor.anchor,
825 sticky_column: primary_cursor.sticky_column,
826 },
827 additional_cursors: buf_state
828 .cursors
829 .iter()
830 .skip(1)
831 .map(|(_, cursor)| SerializedCursor {
832 position: cursor.position,
833 anchor: cursor.anchor,
834 sticky_column: cursor.sticky_column,
835 })
836 .collect(),
837 scroll: SerializedScroll {
838 top_byte: buf_state.viewport.top_byte,
839 top_view_line_offset: buf_state.viewport.top_view_line_offset,
840 left_column: buf_state.viewport.left_column,
841 },
842 view_mode: Default::default(),
843 compose_width: None,
844 plugin_state: std::collections::HashMap::new(),
845 folds: Vec::new(),
846 };
847
848 PersistedFileWorkspace::save(&abs_path, file_state);
850 tracing::debug!("Saved file state on close for {:?}", abs_path);
851 }
852}