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.filesystem.is_remote_connected() {
111 anyhow::bail!(
112 "Cannot open file: remote connection lost ({})",
113 self.filesystem
114 .remote_connection_info()
115 .unwrap_or("unknown host")
116 );
117 }
118
119 let base_dir = if self.filesystem.remote_connection_info().is_some() {
122 self.filesystem
123 .home_dir()
124 .unwrap_or_else(|_| self.working_dir.clone())
125 } else {
126 self.working_dir.clone()
127 };
128
129 let resolved_path = if path.is_relative() {
130 base_dir.join(path)
131 } else {
132 path.to_path_buf()
133 };
134
135 let file_exists = self.filesystem.exists(&resolved_path);
138
139 let display_path = resolved_path.clone();
143
144 let canonical_path = if file_exists {
148 self.filesystem
149 .canonicalize(&resolved_path)
150 .unwrap_or_else(|_| resolved_path.clone())
151 } else {
152 if let Some(parent) = resolved_path.parent() {
154 let canonical_parent = if parent.as_os_str().is_empty() {
155 base_dir.clone()
157 } else {
158 self.filesystem
159 .canonicalize(parent)
160 .unwrap_or_else(|_| parent.to_path_buf())
161 };
162 if let Some(filename) = resolved_path.file_name() {
163 canonical_parent.join(filename)
164 } else {
165 resolved_path
166 }
167 } else {
168 resolved_path
169 }
170 };
171 let path = canonical_path.as_path();
172
173 if self.filesystem.is_dir(path).unwrap_or(false) {
177 anyhow::bail!(t!("buffer.cannot_open_directory"));
178 }
179
180 let already_open = self
182 .buffers
183 .iter()
184 .find(|(_, state)| state.buffer.file_path() == Some(path))
185 .map(|(id, _)| *id);
186
187 if let Some(id) = already_open {
188 return Ok(id);
189 }
190
191 let replace_current = {
194 let current_state = self.buffers.get(&self.active_buffer()).unwrap();
195 !current_state.is_composite_buffer
196 && current_state.buffer.is_empty()
197 && !current_state.buffer.is_modified()
198 && current_state.buffer.file_path().is_none()
199 };
200
201 let buffer_id = if replace_current {
202 self.active_buffer()
204 } else {
205 let id = BufferId(self.next_buffer_id);
207 self.next_buffer_id += 1;
208 id
209 };
210
211 tracing::info!(
213 "[SYNTAX DEBUG] open_file_no_focus: path={:?}, extension={:?}, catalog={}",
214 path,
215 path.extension(),
216 self.grammar_registry.catalog().len(),
217 );
218 let mut state = if file_exists {
219 let buffer = crate::model::buffer::Buffer::load_from_file(
222 &canonical_path,
223 self.config.editor.large_file_threshold_bytes as usize,
224 Arc::clone(&self.filesystem),
225 )?;
226 let first_line = buffer.first_line_lossy();
227 let detected =
228 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
229 &display_path,
230 first_line.as_deref(),
231 &self.grammar_registry,
232 &self.config.languages,
233 self.config.default_language.as_deref(),
234 );
235 EditorState::from_buffer_with_language(buffer, detected)
236 } else {
237 EditorState::new_with_path(
239 self.config.editor.large_file_threshold_bytes as usize,
240 Arc::clone(&self.filesystem),
241 path.to_path_buf(),
242 )
243 };
244 let is_binary = state.buffer.is_binary();
248 if is_binary {
249 state.editing_disabled = true;
251 tracing::info!("Detected binary file: {}", path.display());
252 }
253
254 let mut whitespace =
258 crate::config::WhitespaceVisibility::from_editor_config(&self.config.editor);
259 state.buffer_settings.auto_close = self.config.editor.auto_close;
260 state.buffer_settings.auto_surround = self.config.editor.auto_surround;
261 if let Some(lang_config) = self.config.languages.get(&state.language) {
262 whitespace = whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
263 state.buffer_settings.use_tabs =
264 lang_config.use_tabs.unwrap_or(self.config.editor.use_tabs);
265 state.buffer_settings.tab_size =
267 lang_config.tab_size.unwrap_or(self.config.editor.tab_size);
268 if state.buffer_settings.auto_close {
270 if let Some(lang_auto_close) = lang_config.auto_close {
271 state.buffer_settings.auto_close = lang_auto_close;
272 }
273 }
274 if state.buffer_settings.auto_surround {
276 if let Some(lang_auto_surround) = lang_config.auto_surround {
277 state.buffer_settings.auto_surround = lang_auto_surround;
278 }
279 }
280 } else {
281 state.buffer_settings.tab_size = self.config.editor.tab_size;
282 state.buffer_settings.use_tabs = self.config.editor.use_tabs;
283 }
284 state.buffer_settings.whitespace = whitespace;
285
286 state
288 .margins
289 .configure_for_line_numbers(self.config.editor.line_numbers);
290
291 self.buffers.insert(buffer_id, state);
292 self.event_logs
293 .insert(buffer_id, crate::model::event::EventLog::new());
294
295 let mut metadata = super::types::BufferMetadata::with_file(
297 path.to_path_buf(),
298 &display_path,
299 &self.working_dir,
300 );
301
302 if is_binary {
304 metadata.binary = true;
305 metadata.read_only = true;
306 metadata.disable_lsp(t!("buffer.binary_file").to_string());
307 }
308
309 if file_exists && !metadata.read_only && !self.filesystem.is_writable(path) {
311 metadata.read_only = true;
312 }
313
314 if metadata.read_only {
316 if let Some(state) = self.buffers.get_mut(&buffer_id) {
317 state.editing_disabled = true;
318 }
319 }
320
321 if !is_binary {
323 self.notify_lsp_file_opened(path, buffer_id, &mut metadata);
324 }
325
326 self.buffer_metadata.insert(buffer_id, metadata);
328
329 let target_split = self.preferred_split_for_file();
332 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
333 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
334 let page_view = self.resolve_page_view_for_buffer(buffer_id);
335 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
336 view_state.add_buffer(buffer_id);
337 let buf_state = view_state.ensure_buffer_state(buffer_id);
339 buf_state.apply_config_defaults(
340 self.config.editor.line_numbers,
341 self.config.editor.highlight_current_line,
342 line_wrap,
343 self.config.editor.wrap_indent,
344 wrap_column,
345 self.config.editor.rulers.clone(),
346 );
347 if let Some(page_width) = page_view {
349 buf_state.activate_page_view(page_width);
350 }
351 }
352
353 self.restore_global_file_state(buffer_id, path, target_split);
356
357 self.emit_event(
359 crate::model::control_event::events::FILE_OPENED.name,
360 serde_json::json!({
361 "path": path.display().to_string(),
362 "buffer_id": buffer_id.0
363 }),
364 );
365
366 self.watch_file(path);
368
369 self.plugin_manager.run_hook(
371 "after_file_open",
372 crate::services::plugins::hooks::HookArgs::AfterFileOpen {
373 buffer_id,
374 path: path.to_path_buf(),
375 },
376 );
377
378 Ok(buffer_id)
379 }
380
381 pub fn open_local_file(&mut self, path: &Path) -> anyhow::Result<BufferId> {
387 let resolved_path = if path.is_relative() {
389 self.working_dir.join(path)
390 } else {
391 path.to_path_buf()
392 };
393
394 let display_path = resolved_path.clone();
396
397 let canonical_path = resolved_path
399 .canonicalize()
400 .unwrap_or_else(|_| resolved_path.clone());
401 let path = canonical_path.as_path();
402
403 let already_open = self
405 .buffers
406 .iter()
407 .find(|(_, state)| state.buffer.file_path() == Some(path))
408 .map(|(id, _)| *id);
409
410 if let Some(id) = already_open {
411 self.set_active_buffer(id);
412 return Ok(id);
413 }
414
415 let buffer_id = BufferId(self.next_buffer_id);
417 self.next_buffer_id += 1;
418
419 let buffer = crate::model::buffer::Buffer::load_from_file(
422 &canonical_path,
423 self.config.editor.large_file_threshold_bytes as usize,
424 Arc::clone(&self.local_filesystem),
425 )?;
426 let first_line = buffer.first_line_lossy();
427 let detected =
428 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
429 &display_path,
430 first_line.as_deref(),
431 &self.grammar_registry,
432 &self.config.languages,
433 self.config.default_language.as_deref(),
434 );
435 let state = EditorState::from_buffer_with_language(buffer, detected);
436
437 self.buffers.insert(buffer_id, state);
438 self.event_logs
439 .insert(buffer_id, crate::model::event::EventLog::new());
440
441 let metadata = super::types::BufferMetadata::with_file(
443 path.to_path_buf(),
444 &display_path,
445 &self.working_dir,
446 );
447 self.buffer_metadata.insert(buffer_id, metadata);
448
449 let target_split = self.preferred_split_for_file();
451 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
452 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
453 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
454 view_state.add_buffer(buffer_id);
455 let buf_state = view_state.ensure_buffer_state(buffer_id);
456 buf_state.apply_config_defaults(
457 self.config.editor.line_numbers,
458 self.config.editor.highlight_current_line,
459 line_wrap,
460 self.config.editor.wrap_indent,
461 wrap_column,
462 self.config.editor.rulers.clone(),
463 );
464 }
465
466 self.set_active_buffer(buffer_id);
467
468 let display_name = path.display().to_string();
469 self.status_message = Some(t!("buffer.opened", name = display_name).to_string());
470
471 Ok(buffer_id)
472 }
473
474 pub fn open_file_with_encoding(
479 &mut self,
480 path: &Path,
481 encoding: crate::model::buffer::Encoding,
482 ) -> anyhow::Result<BufferId> {
483 let base_dir = self.working_dir.clone();
485
486 let resolved_path = if path.is_relative() {
487 base_dir.join(path)
488 } else {
489 path.to_path_buf()
490 };
491
492 let display_path = resolved_path.clone();
494
495 let canonical_path = self
497 .filesystem
498 .canonicalize(&resolved_path)
499 .unwrap_or_else(|_| resolved_path.clone());
500 let path = canonical_path.as_path();
501
502 let already_open = self
504 .buffers
505 .iter()
506 .find(|(_, state)| state.buffer.file_path() == Some(path))
507 .map(|(id, _)| *id);
508
509 if let Some(id) = already_open {
510 if let Some(state) = self.buffers.get_mut(&id) {
512 state.buffer.set_encoding(encoding);
513 }
514 self.set_active_buffer(id);
515 return Ok(id);
516 }
517
518 let buffer_id = BufferId(self.next_buffer_id);
520 self.next_buffer_id += 1;
521
522 let buffer = crate::model::buffer::Buffer::load_from_file_with_encoding(
524 path,
525 encoding,
526 Arc::clone(&self.filesystem),
527 crate::model::buffer::BufferConfig {
528 estimated_line_length: self.config.editor.estimated_line_length,
529 },
530 )?;
531 let first_line = buffer.first_line_lossy();
532 let detected =
535 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
536 &display_path,
537 first_line.as_deref(),
538 &self.grammar_registry,
539 &self.config.languages,
540 self.config.default_language.as_deref(),
541 );
542
543 let mut state = EditorState::from_buffer_with_language(buffer, detected);
544
545 state
546 .margins
547 .configure_for_line_numbers(self.config.editor.line_numbers);
548
549 self.buffers.insert(buffer_id, state);
550 self.event_logs
551 .insert(buffer_id, crate::model::event::EventLog::new());
552
553 let metadata = super::types::BufferMetadata::with_file(
554 path.to_path_buf(),
555 &display_path,
556 &self.working_dir,
557 );
558 self.buffer_metadata.insert(buffer_id, metadata);
559
560 let target_split = self.preferred_split_for_file();
562 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
563 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
564 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
565 view_state.add_buffer(buffer_id);
566 let buf_state = view_state.ensure_buffer_state(buffer_id);
567 buf_state.apply_config_defaults(
568 self.config.editor.line_numbers,
569 self.config.editor.highlight_current_line,
570 line_wrap,
571 self.config.editor.wrap_indent,
572 wrap_column,
573 self.config.editor.rulers.clone(),
574 );
575 }
576
577 self.set_active_buffer(buffer_id);
578
579 Ok(buffer_id)
580 }
581
582 pub fn reload_with_encoding(
586 &mut self,
587 encoding: crate::model::buffer::Encoding,
588 ) -> anyhow::Result<()> {
589 let buffer_id = self.active_buffer();
590
591 let path = self
593 .buffers
594 .get(&buffer_id)
595 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()))
596 .ok_or_else(|| anyhow::anyhow!("Buffer has no file path"))?;
597
598 if let Some(state) = self.buffers.get(&buffer_id) {
600 if state.buffer.is_modified() {
601 anyhow::bail!("Cannot reload: buffer has unsaved modifications");
602 }
603 }
604
605 let new_buffer = crate::model::buffer::Buffer::load_from_file_with_encoding(
607 &path,
608 encoding,
609 Arc::clone(&self.filesystem),
610 crate::model::buffer::BufferConfig {
611 estimated_line_length: self.config.editor.estimated_line_length,
612 },
613 )?;
614
615 if let Some(state) = self.buffers.get_mut(&buffer_id) {
617 state.buffer = new_buffer;
618 state.highlighter.invalidate_all();
620 }
621
622 let split_id = self.split_manager.active_split();
624 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
625 if let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) {
626 buf_state.cursors = crate::model::cursor::Cursors::new();
627 }
628 }
629
630 Ok(())
631 }
632
633 pub fn open_file_large_encoding_confirmed(&mut self, path: &Path) -> anyhow::Result<BufferId> {
638 let base_dir = self.working_dir.clone();
640
641 let resolved_path = if path.is_relative() {
642 base_dir.join(path)
643 } else {
644 path.to_path_buf()
645 };
646
647 let display_path = resolved_path.clone();
649
650 let canonical_path = self
652 .filesystem
653 .canonicalize(&resolved_path)
654 .unwrap_or_else(|_| resolved_path.clone());
655 let path = canonical_path.as_path();
656
657 let already_open = self
659 .buffers
660 .iter()
661 .find(|(_, state)| state.buffer.file_path() == Some(path))
662 .map(|(id, _)| *id);
663
664 if let Some(id) = already_open {
665 self.set_active_buffer(id);
666 return Ok(id);
667 }
668
669 let buffer_id = BufferId(self.next_buffer_id);
671 self.next_buffer_id += 1;
672
673 let buffer = crate::model::buffer::Buffer::load_large_file_confirmed(
675 path,
676 Arc::clone(&self.filesystem),
677 )?;
678 let first_line = buffer.first_line_lossy();
679 let detected =
682 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
683 &display_path,
684 first_line.as_deref(),
685 &self.grammar_registry,
686 &self.config.languages,
687 self.config.default_language.as_deref(),
688 );
689
690 let mut state = EditorState::from_buffer_with_language(buffer, detected);
691
692 state
693 .margins
694 .configure_for_line_numbers(self.config.editor.line_numbers);
695
696 self.buffers.insert(buffer_id, state);
697 self.event_logs
698 .insert(buffer_id, crate::model::event::EventLog::new());
699
700 let metadata = super::types::BufferMetadata::with_file(
701 path.to_path_buf(),
702 &display_path,
703 &self.working_dir,
704 );
705 self.buffer_metadata.insert(buffer_id, metadata);
706
707 let target_split = self.preferred_split_for_file();
709 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
710 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
711 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
712 view_state.add_buffer(buffer_id);
713 let buf_state = view_state.ensure_buffer_state(buffer_id);
714 buf_state.apply_config_defaults(
715 self.config.editor.line_numbers,
716 self.config.editor.highlight_current_line,
717 line_wrap,
718 self.config.editor.wrap_indent,
719 wrap_column,
720 self.config.editor.rulers.clone(),
721 );
722 }
723
724 self.set_active_buffer(buffer_id);
725
726 let display_name = self
728 .buffer_metadata
729 .get(&buffer_id)
730 .map(|m| m.display_name.clone())
731 .unwrap_or_else(|| path.display().to_string());
732
733 self.status_message = Some(t!("buffer.opened", name = display_name).to_string());
734
735 Ok(buffer_id)
736 }
737
738 fn restore_global_file_state(&mut self, buffer_id: BufferId, path: &Path, split_id: LeafId) {
743 use crate::workspace::PersistedFileWorkspace;
744
745 let file_state = match PersistedFileWorkspace::load(path) {
747 Some(state) => state,
748 None => return, };
750
751 let max_pos = match self.buffers.get(&buffer_id) {
753 Some(buffer) => buffer.buffer.len(),
754 None => return,
755 };
756
757 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
759 if let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) {
760 let cursor_pos = file_state.cursor.position.min(max_pos);
761 buf_state.cursors.primary_mut().position = cursor_pos;
762 buf_state.cursors.primary_mut().anchor =
763 file_state.cursor.anchor.map(|a| a.min(max_pos));
764 }
765 view_state.viewport.top_byte = file_state.scroll.top_byte;
766 view_state.viewport.left_column = file_state.scroll.left_column;
767 }
768 }
769
770 pub(super) fn save_file_state_on_close(&self, buffer_id: BufferId) {
772 use crate::workspace::{
773 PersistedFileWorkspace, SerializedCursor, SerializedFileState, SerializedScroll,
774 };
775
776 let abs_path = match self.buffer_metadata.get(&buffer_id) {
778 Some(metadata) => match metadata.file_path() {
779 Some(path) => path.to_path_buf(),
780 None => return, },
782 None => return,
783 };
784
785 let view_state = self
787 .split_view_states
788 .values()
789 .find(|vs| vs.has_buffer(buffer_id));
790
791 let view_state = match view_state {
792 Some(vs) => vs,
793 None => return, };
795
796 let buf_state = match view_state.keyed_states.get(&buffer_id) {
798 Some(bs) => bs,
799 None => return,
800 };
801
802 let primary_cursor = buf_state.cursors.primary();
804 let file_state = SerializedFileState {
805 cursor: SerializedCursor {
806 position: primary_cursor.position,
807 anchor: primary_cursor.anchor,
808 sticky_column: primary_cursor.sticky_column,
809 },
810 additional_cursors: buf_state
811 .cursors
812 .iter()
813 .skip(1)
814 .map(|(_, cursor)| SerializedCursor {
815 position: cursor.position,
816 anchor: cursor.anchor,
817 sticky_column: cursor.sticky_column,
818 })
819 .collect(),
820 scroll: SerializedScroll {
821 top_byte: buf_state.viewport.top_byte,
822 top_view_line_offset: buf_state.viewport.top_view_line_offset,
823 left_column: buf_state.viewport.left_column,
824 },
825 view_mode: Default::default(),
826 compose_width: None,
827 plugin_state: std::collections::HashMap::new(),
828 folds: Vec::new(),
829 };
830
831 PersistedFileWorkspace::save(&abs_path, file_state);
833 tracing::debug!("Saved file state on close for {:?}", abs_path);
834 }
835}