1use anyhow::Result as AnyhowResult;
12use rust_i18n::t;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15
16use crate::app::warning_domains::WarningDomain;
17use crate::model::event::{BufferId, Event, LeafId};
18use crate::state::EditorState;
19use crate::view::prompt::PromptType;
20use crate::view::split::SplitViewState;
21
22use super::help;
23use super::Editor;
24
25impl Editor {
26 pub(super) fn resolve_line_wrap_for_buffer(&self, buffer_id: BufferId) -> bool {
30 if let Some(state) = self.buffers.get(&buffer_id) {
31 if let Some(lang_config) = self.config.languages.get(&state.language) {
32 if let Some(line_wrap) = lang_config.line_wrap {
33 return line_wrap;
34 }
35 }
36 }
37 self.config.editor.line_wrap
38 }
39
40 pub(super) fn resolve_page_view_for_buffer(
45 &self,
46 buffer_id: BufferId,
47 ) -> Option<Option<usize>> {
48 let state = self.buffers.get(&buffer_id)?;
49 let lang_config = self.config.languages.get(&state.language)?;
50 if lang_config.page_view == Some(true) {
51 Some(lang_config.page_width.or(self.config.editor.page_width))
52 } else {
53 None
54 }
55 }
56
57 pub(super) fn resolve_wrap_column_for_buffer(&self, buffer_id: BufferId) -> Option<usize> {
61 if let Some(state) = self.buffers.get(&buffer_id) {
62 if let Some(lang_config) = self.config.languages.get(&state.language) {
63 if lang_config.wrap_column.is_some() {
64 return lang_config.wrap_column;
65 }
66 }
67 }
68 self.config.editor.wrap_column
69 }
70
71 fn preferred_split_for_file(&self) -> LeafId {
75 let active = self.split_manager.active_split();
76 if self.split_manager.get_label(active.into()).is_none() {
77 return active;
78 }
79 self.split_manager.find_unlabeled_leaf().unwrap_or(active)
80 }
81
82 pub fn open_file(&mut self, path: &Path) -> anyhow::Result<BufferId> {
87 let active_had_path = self
91 .buffers
92 .get(&self.active_buffer())
93 .and_then(|s| s.buffer.file_path())
94 .is_some();
95
96 let buffer_id = self.open_file_no_focus(path)?;
97
98 let is_new_buffer = self.active_buffer() != buffer_id;
102
103 if is_new_buffer {
104 self.position_history.commit_pending_movement();
106
107 let cursors = self.active_cursors();
109 let position = cursors.primary().position;
110 let anchor = cursors.primary().anchor;
111 self.position_history
112 .record_movement(self.active_buffer(), position, anchor);
113 self.position_history.commit_pending_movement();
114 }
115
116 self.set_active_buffer(buffer_id);
117
118 if !is_new_buffer && !active_had_path {
125 #[cfg(feature = "plugins")]
126 self.update_plugin_state_snapshot();
127
128 self.plugin_manager.run_hook(
129 "buffer_activated",
130 crate::services::plugins::hooks::HookArgs::BufferActivated { buffer_id },
131 );
132 }
133
134 let display_name = self
136 .buffer_metadata
137 .get(&buffer_id)
138 .map(|m| m.display_name.clone())
139 .unwrap_or_else(|| path.display().to_string());
140
141 let is_binary = self
143 .buffers
144 .get(&buffer_id)
145 .map(|s| s.buffer.is_binary())
146 .unwrap_or(false);
147
148 if is_binary {
150 self.status_message = Some(t!("buffer.opened_binary", name = display_name).to_string());
151 } else {
152 self.status_message = Some(t!("buffer.opened", name = display_name).to_string());
153 }
154
155 Ok(buffer_id)
156 }
157
158 pub fn open_file_no_focus(&mut self, path: &Path) -> anyhow::Result<BufferId> {
165 if !self.filesystem.is_remote_connected() {
168 anyhow::bail!(
169 "Cannot open file: remote connection lost ({})",
170 self.filesystem
171 .remote_connection_info()
172 .unwrap_or("unknown host")
173 );
174 }
175
176 let base_dir = if self.filesystem.remote_connection_info().is_some() {
179 self.filesystem
180 .home_dir()
181 .unwrap_or_else(|_| self.working_dir.clone())
182 } else {
183 self.working_dir.clone()
184 };
185
186 let resolved_path = if path.is_relative() {
187 base_dir.join(path)
188 } else {
189 path.to_path_buf()
190 };
191
192 let file_exists = self.filesystem.exists(&resolved_path);
195
196 let display_path = resolved_path.clone();
200
201 let canonical_path = if file_exists {
205 self.filesystem
206 .canonicalize(&resolved_path)
207 .unwrap_or_else(|_| resolved_path.clone())
208 } else {
209 if let Some(parent) = resolved_path.parent() {
211 let canonical_parent = if parent.as_os_str().is_empty() {
212 base_dir.clone()
214 } else {
215 self.filesystem
216 .canonicalize(parent)
217 .unwrap_or_else(|_| parent.to_path_buf())
218 };
219 if let Some(filename) = resolved_path.file_name() {
220 canonical_parent.join(filename)
221 } else {
222 resolved_path
223 }
224 } else {
225 resolved_path
226 }
227 };
228 let path = canonical_path.as_path();
229
230 if self.filesystem.is_dir(path).unwrap_or(false) {
234 anyhow::bail!(t!("buffer.cannot_open_directory"));
235 }
236
237 let already_open = self
239 .buffers
240 .iter()
241 .find(|(_, state)| state.buffer.file_path() == Some(path))
242 .map(|(id, _)| *id);
243
244 if let Some(id) = already_open {
245 return Ok(id);
246 }
247
248 let replace_current = {
251 let current_state = self.buffers.get(&self.active_buffer()).unwrap();
252 !current_state.is_composite_buffer
253 && current_state.buffer.is_empty()
254 && !current_state.buffer.is_modified()
255 && current_state.buffer.file_path().is_none()
256 };
257
258 let buffer_id = if replace_current {
259 self.active_buffer()
261 } else {
262 let id = BufferId(self.next_buffer_id);
264 self.next_buffer_id += 1;
265 id
266 };
267
268 tracing::info!(
270 "[SYNTAX DEBUG] open_file_no_focus: path={:?}, extension={:?}, registry_syntaxes={}, user_extensions={:?}",
271 path,
272 path.extension(),
273 self.grammar_registry.available_syntaxes().len(),
274 self.grammar_registry.user_extensions_debug()
275 );
276 let mut state = if file_exists {
277 let buffer = crate::model::buffer::Buffer::load_from_file(
280 &canonical_path,
281 self.config.editor.large_file_threshold_bytes as usize,
282 Arc::clone(&self.filesystem),
283 )?;
284 let detected =
285 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
286 &display_path,
287 &self.grammar_registry,
288 &self.config.languages,
289 self.config.default_language.as_deref(),
290 );
291 EditorState::from_buffer_with_language(buffer, detected)
292 } else {
293 EditorState::new_with_path(
295 self.config.editor.large_file_threshold_bytes as usize,
296 Arc::clone(&self.filesystem),
297 path.to_path_buf(),
298 )
299 };
300 let is_binary = state.buffer.is_binary();
304 if is_binary {
305 state.editing_disabled = true;
307 tracing::info!("Detected binary file: {}", path.display());
308 }
309
310 let mut whitespace =
314 crate::config::WhitespaceVisibility::from_editor_config(&self.config.editor);
315 state.buffer_settings.auto_close = self.config.editor.auto_close;
316 state.buffer_settings.auto_surround = self.config.editor.auto_surround;
317 if let Some(lang_config) = self.config.languages.get(&state.language) {
318 whitespace = whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
319 state.buffer_settings.use_tabs =
320 lang_config.use_tabs.unwrap_or(self.config.editor.use_tabs);
321 state.buffer_settings.tab_size =
323 lang_config.tab_size.unwrap_or(self.config.editor.tab_size);
324 if state.buffer_settings.auto_close {
326 if let Some(lang_auto_close) = lang_config.auto_close {
327 state.buffer_settings.auto_close = lang_auto_close;
328 }
329 }
330 if state.buffer_settings.auto_surround {
332 if let Some(lang_auto_surround) = lang_config.auto_surround {
333 state.buffer_settings.auto_surround = lang_auto_surround;
334 }
335 }
336 } else {
337 state.buffer_settings.tab_size = self.config.editor.tab_size;
338 state.buffer_settings.use_tabs = self.config.editor.use_tabs;
339 }
340 state.buffer_settings.whitespace = whitespace;
341
342 state
344 .margins
345 .configure_for_line_numbers(self.config.editor.line_numbers);
346
347 self.buffers.insert(buffer_id, state);
348 self.event_logs
349 .insert(buffer_id, crate::model::event::EventLog::new());
350
351 let mut metadata = super::types::BufferMetadata::with_file(
353 path.to_path_buf(),
354 &display_path,
355 &self.working_dir,
356 );
357
358 if is_binary {
360 metadata.binary = true;
361 metadata.read_only = true;
362 metadata.disable_lsp(t!("buffer.binary_file").to_string());
363 }
364
365 if file_exists && !metadata.read_only && !self.filesystem.is_writable(path) {
367 metadata.read_only = true;
368 }
369
370 if metadata.read_only {
372 if let Some(state) = self.buffers.get_mut(&buffer_id) {
373 state.editing_disabled = true;
374 }
375 }
376
377 if !is_binary {
379 self.notify_lsp_file_opened(path, buffer_id, &mut metadata);
380 }
381
382 self.buffer_metadata.insert(buffer_id, metadata);
384
385 let target_split = self.preferred_split_for_file();
388 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
389 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
390 let page_view = self.resolve_page_view_for_buffer(buffer_id);
391 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
392 view_state.add_buffer(buffer_id);
393 let buf_state = view_state.ensure_buffer_state(buffer_id);
395 buf_state.apply_config_defaults(
396 self.config.editor.line_numbers,
397 self.config.editor.highlight_current_line,
398 line_wrap,
399 self.config.editor.wrap_indent,
400 wrap_column,
401 self.config.editor.rulers.clone(),
402 );
403 if let Some(page_width) = page_view {
405 buf_state.activate_page_view(page_width);
406 }
407 }
408
409 self.restore_global_file_state(buffer_id, path, target_split);
412
413 self.emit_event(
415 crate::model::control_event::events::FILE_OPENED.name,
416 serde_json::json!({
417 "path": path.display().to_string(),
418 "buffer_id": buffer_id.0
419 }),
420 );
421
422 self.watch_file(path);
424
425 self.plugin_manager.run_hook(
427 "after_file_open",
428 crate::services::plugins::hooks::HookArgs::AfterFileOpen {
429 buffer_id,
430 path: path.to_path_buf(),
431 },
432 );
433
434 Ok(buffer_id)
435 }
436
437 pub fn open_local_file(&mut self, path: &Path) -> anyhow::Result<BufferId> {
443 let resolved_path = if path.is_relative() {
445 self.working_dir.join(path)
446 } else {
447 path.to_path_buf()
448 };
449
450 let display_path = resolved_path.clone();
452
453 let canonical_path = resolved_path
455 .canonicalize()
456 .unwrap_or_else(|_| resolved_path.clone());
457 let path = canonical_path.as_path();
458
459 let already_open = self
461 .buffers
462 .iter()
463 .find(|(_, state)| state.buffer.file_path() == Some(path))
464 .map(|(id, _)| *id);
465
466 if let Some(id) = already_open {
467 self.set_active_buffer(id);
468 return Ok(id);
469 }
470
471 let buffer_id = BufferId(self.next_buffer_id);
473 self.next_buffer_id += 1;
474
475 let buffer = crate::model::buffer::Buffer::load_from_file(
478 &canonical_path,
479 self.config.editor.large_file_threshold_bytes as usize,
480 Arc::clone(&self.local_filesystem),
481 )?;
482 let detected =
483 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
484 &display_path,
485 &self.grammar_registry,
486 &self.config.languages,
487 self.config.default_language.as_deref(),
488 );
489 let state = EditorState::from_buffer_with_language(buffer, detected);
490
491 self.buffers.insert(buffer_id, state);
492 self.event_logs
493 .insert(buffer_id, crate::model::event::EventLog::new());
494
495 let metadata = super::types::BufferMetadata::with_file(
497 path.to_path_buf(),
498 &display_path,
499 &self.working_dir,
500 );
501 self.buffer_metadata.insert(buffer_id, metadata);
502
503 let target_split = self.preferred_split_for_file();
505 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
506 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
507 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
508 view_state.add_buffer(buffer_id);
509 let buf_state = view_state.ensure_buffer_state(buffer_id);
510 buf_state.apply_config_defaults(
511 self.config.editor.line_numbers,
512 self.config.editor.highlight_current_line,
513 line_wrap,
514 self.config.editor.wrap_indent,
515 wrap_column,
516 self.config.editor.rulers.clone(),
517 );
518 }
519
520 self.set_active_buffer(buffer_id);
521
522 let display_name = path.display().to_string();
523 self.status_message = Some(t!("buffer.opened", name = display_name).to_string());
524
525 Ok(buffer_id)
526 }
527
528 pub fn open_file_with_encoding(
533 &mut self,
534 path: &Path,
535 encoding: crate::model::buffer::Encoding,
536 ) -> anyhow::Result<BufferId> {
537 let base_dir = self.working_dir.clone();
539
540 let resolved_path = if path.is_relative() {
541 base_dir.join(path)
542 } else {
543 path.to_path_buf()
544 };
545
546 let display_path = resolved_path.clone();
548
549 let canonical_path = self
551 .filesystem
552 .canonicalize(&resolved_path)
553 .unwrap_or_else(|_| resolved_path.clone());
554 let path = canonical_path.as_path();
555
556 let already_open = self
558 .buffers
559 .iter()
560 .find(|(_, state)| state.buffer.file_path() == Some(path))
561 .map(|(id, _)| *id);
562
563 if let Some(id) = already_open {
564 if let Some(state) = self.buffers.get_mut(&id) {
566 state.buffer.set_encoding(encoding);
567 }
568 self.set_active_buffer(id);
569 return Ok(id);
570 }
571
572 let buffer_id = BufferId(self.next_buffer_id);
574 self.next_buffer_id += 1;
575
576 let buffer = crate::model::buffer::Buffer::load_from_file_with_encoding(
578 path,
579 encoding,
580 Arc::clone(&self.filesystem),
581 crate::model::buffer::BufferConfig {
582 estimated_line_length: self.config.editor.estimated_line_length,
583 },
584 )?;
585 let detected =
588 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
589 &display_path,
590 &self.grammar_registry,
591 &self.config.languages,
592 self.config.default_language.as_deref(),
593 );
594
595 let mut state = EditorState::from_buffer_with_language(buffer, detected);
596
597 state
598 .margins
599 .configure_for_line_numbers(self.config.editor.line_numbers);
600
601 self.buffers.insert(buffer_id, state);
602 self.event_logs
603 .insert(buffer_id, crate::model::event::EventLog::new());
604
605 let metadata = super::types::BufferMetadata::with_file(
606 path.to_path_buf(),
607 &display_path,
608 &self.working_dir,
609 );
610 self.buffer_metadata.insert(buffer_id, metadata);
611
612 let target_split = self.preferred_split_for_file();
614 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
615 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
616 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
617 view_state.add_buffer(buffer_id);
618 let buf_state = view_state.ensure_buffer_state(buffer_id);
619 buf_state.apply_config_defaults(
620 self.config.editor.line_numbers,
621 self.config.editor.highlight_current_line,
622 line_wrap,
623 self.config.editor.wrap_indent,
624 wrap_column,
625 self.config.editor.rulers.clone(),
626 );
627 }
628
629 self.set_active_buffer(buffer_id);
630
631 Ok(buffer_id)
632 }
633
634 pub fn reload_with_encoding(
638 &mut self,
639 encoding: crate::model::buffer::Encoding,
640 ) -> anyhow::Result<()> {
641 let buffer_id = self.active_buffer();
642
643 let path = self
645 .buffers
646 .get(&buffer_id)
647 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()))
648 .ok_or_else(|| anyhow::anyhow!("Buffer has no file path"))?;
649
650 if let Some(state) = self.buffers.get(&buffer_id) {
652 if state.buffer.is_modified() {
653 anyhow::bail!("Cannot reload: buffer has unsaved modifications");
654 }
655 }
656
657 let new_buffer = crate::model::buffer::Buffer::load_from_file_with_encoding(
659 &path,
660 encoding,
661 Arc::clone(&self.filesystem),
662 crate::model::buffer::BufferConfig {
663 estimated_line_length: self.config.editor.estimated_line_length,
664 },
665 )?;
666
667 if let Some(state) = self.buffers.get_mut(&buffer_id) {
669 state.buffer = new_buffer;
670 state.highlighter.invalidate_all();
672 }
673
674 let split_id = self.split_manager.active_split();
676 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
677 if let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) {
678 buf_state.cursors = crate::model::cursor::Cursors::new();
679 }
680 }
681
682 Ok(())
683 }
684
685 pub fn open_file_large_encoding_confirmed(&mut self, path: &Path) -> anyhow::Result<BufferId> {
690 let base_dir = self.working_dir.clone();
692
693 let resolved_path = if path.is_relative() {
694 base_dir.join(path)
695 } else {
696 path.to_path_buf()
697 };
698
699 let display_path = resolved_path.clone();
701
702 let canonical_path = self
704 .filesystem
705 .canonicalize(&resolved_path)
706 .unwrap_or_else(|_| resolved_path.clone());
707 let path = canonical_path.as_path();
708
709 let already_open = self
711 .buffers
712 .iter()
713 .find(|(_, state)| state.buffer.file_path() == Some(path))
714 .map(|(id, _)| *id);
715
716 if let Some(id) = already_open {
717 self.set_active_buffer(id);
718 return Ok(id);
719 }
720
721 let buffer_id = BufferId(self.next_buffer_id);
723 self.next_buffer_id += 1;
724
725 let buffer = crate::model::buffer::Buffer::load_large_file_confirmed(
727 path,
728 Arc::clone(&self.filesystem),
729 )?;
730 let detected =
733 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
734 &display_path,
735 &self.grammar_registry,
736 &self.config.languages,
737 self.config.default_language.as_deref(),
738 );
739
740 let mut state = EditorState::from_buffer_with_language(buffer, detected);
741
742 state
743 .margins
744 .configure_for_line_numbers(self.config.editor.line_numbers);
745
746 self.buffers.insert(buffer_id, state);
747 self.event_logs
748 .insert(buffer_id, crate::model::event::EventLog::new());
749
750 let metadata = super::types::BufferMetadata::with_file(
751 path.to_path_buf(),
752 &display_path,
753 &self.working_dir,
754 );
755 self.buffer_metadata.insert(buffer_id, metadata);
756
757 let target_split = self.preferred_split_for_file();
759 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
760 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
761 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
762 view_state.add_buffer(buffer_id);
763 let buf_state = view_state.ensure_buffer_state(buffer_id);
764 buf_state.apply_config_defaults(
765 self.config.editor.line_numbers,
766 self.config.editor.highlight_current_line,
767 line_wrap,
768 self.config.editor.wrap_indent,
769 wrap_column,
770 self.config.editor.rulers.clone(),
771 );
772 }
773
774 self.set_active_buffer(buffer_id);
775
776 let display_name = self
778 .buffer_metadata
779 .get(&buffer_id)
780 .map(|m| m.display_name.clone())
781 .unwrap_or_else(|| path.display().to_string());
782
783 self.status_message = Some(t!("buffer.opened", name = display_name).to_string());
784
785 Ok(buffer_id)
786 }
787
788 fn restore_global_file_state(&mut self, buffer_id: BufferId, path: &Path, split_id: LeafId) {
793 use crate::workspace::PersistedFileWorkspace;
794
795 let file_state = match PersistedFileWorkspace::load(path) {
797 Some(state) => state,
798 None => return, };
800
801 let max_pos = match self.buffers.get(&buffer_id) {
803 Some(buffer) => buffer.buffer.len(),
804 None => return,
805 };
806
807 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
809 if let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) {
810 let cursor_pos = file_state.cursor.position.min(max_pos);
811 buf_state.cursors.primary_mut().position = cursor_pos;
812 buf_state.cursors.primary_mut().anchor =
813 file_state.cursor.anchor.map(|a| a.min(max_pos));
814 }
815 view_state.viewport.top_byte = file_state.scroll.top_byte;
816 view_state.viewport.left_column = file_state.scroll.left_column;
817 }
818 }
819
820 fn save_file_state_on_close(&self, buffer_id: BufferId) {
822 use crate::workspace::{
823 PersistedFileWorkspace, SerializedCursor, SerializedFileState, SerializedScroll,
824 };
825
826 let abs_path = match self.buffer_metadata.get(&buffer_id) {
828 Some(metadata) => match metadata.file_path() {
829 Some(path) => path.to_path_buf(),
830 None => return, },
832 None => return,
833 };
834
835 let view_state = self
837 .split_view_states
838 .values()
839 .find(|vs| vs.has_buffer(buffer_id));
840
841 let view_state = match view_state {
842 Some(vs) => vs,
843 None => return, };
845
846 let buf_state = match view_state.keyed_states.get(&buffer_id) {
848 Some(bs) => bs,
849 None => return,
850 };
851
852 let primary_cursor = buf_state.cursors.primary();
854 let file_state = SerializedFileState {
855 cursor: SerializedCursor {
856 position: primary_cursor.position,
857 anchor: primary_cursor.anchor,
858 sticky_column: primary_cursor.sticky_column,
859 },
860 additional_cursors: buf_state
861 .cursors
862 .iter()
863 .skip(1)
864 .map(|(_, cursor)| SerializedCursor {
865 position: cursor.position,
866 anchor: cursor.anchor,
867 sticky_column: cursor.sticky_column,
868 })
869 .collect(),
870 scroll: SerializedScroll {
871 top_byte: buf_state.viewport.top_byte,
872 top_view_line_offset: buf_state.viewport.top_view_line_offset,
873 left_column: buf_state.viewport.left_column,
874 },
875 view_mode: Default::default(),
876 compose_width: None,
877 plugin_state: std::collections::HashMap::new(),
878 folds: Vec::new(),
879 };
880
881 PersistedFileWorkspace::save(&abs_path, file_state);
883 tracing::debug!("Saved file state on close for {:?}", abs_path);
884 }
885
886 pub fn goto_line_col(&mut self, line: usize, column: Option<usize>) {
892 if line == 0 {
893 return; }
895
896 let buffer_id = self.active_buffer();
897
898 let cursors = self.active_cursors();
900 let cursor_id = cursors.primary_id();
901 let old_position = cursors.primary().position;
902 let old_anchor = cursors.primary().anchor;
903 let old_sticky_column = cursors.primary().sticky_column;
904
905 if let Some(state) = self.buffers.get(&buffer_id) {
906 let has_line_index = state.buffer.line_count().is_some();
907 let has_line_scan = state.buffer.has_line_feed_scan();
908 let buffer_len = state.buffer.len();
909
910 let target_line = line.saturating_sub(1);
912 let target_col = column.map(|c| c.saturating_sub(1)).unwrap_or(0);
914
915 let mut known_line: Option<usize> = None;
918
919 let position = if has_line_scan && has_line_index {
920 let max_line = state.buffer.line_count().unwrap_or(1).saturating_sub(1);
922 let actual_line = target_line.min(max_line);
923 known_line = Some(actual_line);
924 if let Some(state) = self.buffers.get_mut(&buffer_id) {
926 state
927 .buffer
928 .resolve_line_byte_offset(actual_line)
929 .map(|offset| (offset + target_col).min(buffer_len))
930 .unwrap_or(0)
931 } else {
932 0
933 }
934 } else {
935 let max_line = state.buffer.line_count().unwrap_or(1).saturating_sub(1);
938 let actual_line = target_line.min(max_line);
939 state.buffer.line_col_to_position(actual_line, target_col)
940 };
941
942 let event = Event::MoveCursor {
943 cursor_id,
944 old_position,
945 new_position: position,
946 old_anchor,
947 new_anchor: None,
948 old_sticky_column,
949 new_sticky_column: target_col,
950 };
951
952 let split_id = self.split_manager.active_split();
953 let state = self.buffers.get_mut(&buffer_id).unwrap();
954 let view_state = self.split_view_states.get_mut(&split_id).unwrap();
955 state.apply(&mut view_state.cursors, &event);
956
957 if let Some(line) = known_line {
960 state.primary_cursor_line_number = crate::model::buffer::LineNumber::Absolute(line);
961 }
962 }
963 }
964
965 pub fn select_range(
969 &mut self,
970 start_line: usize,
971 start_col: Option<usize>,
972 end_line: usize,
973 end_col: Option<usize>,
974 ) {
975 if start_line == 0 || end_line == 0 {
976 return;
977 }
978
979 let buffer_id = self.active_buffer();
980
981 let cursors = self.active_cursors();
982 let cursor_id = cursors.primary_id();
983 let old_position = cursors.primary().position;
984 let old_anchor = cursors.primary().anchor;
985 let old_sticky_column = cursors.primary().sticky_column;
986
987 if let Some(state) = self.buffers.get(&buffer_id) {
988 let buffer_len = state.buffer.len();
989
990 let start_line_0 = start_line.saturating_sub(1);
992 let start_col_0 = start_col.map(|c| c.saturating_sub(1)).unwrap_or(0);
993 let end_line_0 = end_line.saturating_sub(1);
994 let end_col_0 = end_col.map(|c| c.saturating_sub(1)).unwrap_or(0);
995
996 let max_line = state.buffer.line_count().unwrap_or(1).saturating_sub(1);
997
998 let start_pos = state
999 .buffer
1000 .line_col_to_position(start_line_0.min(max_line), start_col_0)
1001 .min(buffer_len);
1002 let end_pos = state
1003 .buffer
1004 .line_col_to_position(end_line_0.min(max_line), end_col_0)
1005 .min(buffer_len);
1006
1007 let event = Event::MoveCursor {
1008 cursor_id,
1009 old_position,
1010 new_position: end_pos,
1011 old_anchor,
1012 new_anchor: Some(start_pos),
1013 old_sticky_column,
1014 new_sticky_column: end_col_0,
1015 };
1016
1017 let split_id = self.split_manager.active_split();
1018 let state = self.buffers.get_mut(&buffer_id).unwrap();
1019 let view_state = self.split_view_states.get_mut(&split_id).unwrap();
1020 state.apply(&mut view_state.cursors, &event);
1021 }
1022 }
1023
1024 pub fn goto_byte_offset(&mut self, offset: usize) {
1026 let buffer_id = self.active_buffer();
1027
1028 let cursors = self.active_cursors();
1029 let cursor_id = cursors.primary_id();
1030 let old_position = cursors.primary().position;
1031 let old_anchor = cursors.primary().anchor;
1032 let old_sticky_column = cursors.primary().sticky_column;
1033
1034 if let Some(state) = self.buffers.get(&buffer_id) {
1035 let buffer_len = state.buffer.len();
1036 let position = offset.min(buffer_len);
1037
1038 let event = Event::MoveCursor {
1039 cursor_id,
1040 old_position,
1041 new_position: position,
1042 old_anchor,
1043 new_anchor: None,
1044 old_sticky_column,
1045 new_sticky_column: 0,
1046 };
1047
1048 let split_id = self.split_manager.active_split();
1049 let state = self.buffers.get_mut(&buffer_id).unwrap();
1050 let view_state = self.split_view_states.get_mut(&split_id).unwrap();
1051 state.apply(&mut view_state.cursors, &event);
1052 }
1053 }
1054
1055 pub fn new_buffer(&mut self) -> BufferId {
1057 self.position_history.commit_pending_movement();
1059
1060 let cursors = self.active_cursors();
1062 let position = cursors.primary().position;
1063 let anchor = cursors.primary().anchor;
1064 self.position_history
1065 .record_movement(self.active_buffer(), position, anchor);
1066 self.position_history.commit_pending_movement();
1067
1068 let buffer_id = BufferId(self.next_buffer_id);
1069 self.next_buffer_id += 1;
1070
1071 let mut state = EditorState::new(
1072 self.terminal_width,
1073 self.terminal_height,
1074 self.config.editor.large_file_threshold_bytes as usize,
1075 Arc::clone(&self.filesystem),
1076 );
1077 state
1079 .margins
1080 .configure_for_line_numbers(self.config.editor.line_numbers);
1081 state
1083 .buffer
1084 .set_default_line_ending(self.config.editor.default_line_ending.to_line_ending());
1085 self.buffers.insert(buffer_id, state);
1086 self.event_logs
1087 .insert(buffer_id, crate::model::event::EventLog::new());
1088 self.buffer_metadata
1089 .insert(buffer_id, crate::app::types::BufferMetadata::new());
1090
1091 self.set_active_buffer(buffer_id);
1092
1093 let active_split = self.split_manager.active_split();
1097 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
1098 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
1099 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1100 view_state.apply_config_defaults(
1101 self.config.editor.line_numbers,
1102 self.config.editor.highlight_current_line,
1103 line_wrap,
1104 self.config.editor.wrap_indent,
1105 wrap_column,
1106 self.config.editor.rulers.clone(),
1107 );
1108 }
1109
1110 self.status_message = Some(t!("buffer.new").to_string());
1111
1112 buffer_id
1113 }
1114
1115 pub fn open_stdin_buffer(
1125 &mut self,
1126 temp_path: &Path,
1127 thread_handle: Option<std::thread::JoinHandle<anyhow::Result<()>>>,
1128 ) -> AnyhowResult<BufferId> {
1129 self.position_history.commit_pending_movement();
1131
1132 let cursors = self.active_cursors();
1134 let position = cursors.primary().position;
1135 let anchor = cursors.primary().anchor;
1136 self.position_history
1137 .record_movement(self.active_buffer(), position, anchor);
1138 self.position_history.commit_pending_movement();
1139
1140 let replace_current = {
1143 let current_state = self.buffers.get(&self.active_buffer()).unwrap();
1144 !current_state.is_composite_buffer
1145 && current_state.buffer.is_empty()
1146 && !current_state.buffer.is_modified()
1147 && current_state.buffer.file_path().is_none()
1148 };
1149
1150 let buffer_id = if replace_current {
1151 self.active_buffer()
1153 } else {
1154 let id = BufferId(self.next_buffer_id);
1156 self.next_buffer_id += 1;
1157 id
1158 };
1159
1160 let file_size = self.filesystem.metadata(temp_path)?.size as usize;
1162
1163 let mut state = EditorState::from_file_with_languages(
1166 temp_path,
1167 self.terminal_width,
1168 self.terminal_height,
1169 self.config.editor.large_file_threshold_bytes as usize,
1170 &self.grammar_registry,
1171 &self.config.languages,
1172 Arc::clone(&self.filesystem),
1173 )?;
1174
1175 state.buffer.clear_file_path();
1178 state.buffer.clear_modified();
1180
1181 state.buffer_settings.tab_size = self.config.editor.tab_size;
1183 state.buffer_settings.auto_close = self.config.editor.auto_close;
1184 state.buffer_settings.auto_surround = self.config.editor.auto_surround;
1185
1186 state
1188 .margins
1189 .configure_for_line_numbers(self.config.editor.line_numbers);
1190
1191 self.buffers.insert(buffer_id, state);
1192 self.event_logs
1193 .insert(buffer_id, crate::model::event::EventLog::new());
1194
1195 let metadata =
1197 super::types::BufferMetadata::new_unnamed(t!("stdin.display_name").to_string());
1198 self.buffer_metadata.insert(buffer_id, metadata);
1199
1200 let active_split = self.split_manager.active_split();
1202 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
1203 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
1204 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1205 view_state.add_buffer(buffer_id);
1206 let buf_state = view_state.ensure_buffer_state(buffer_id);
1207 buf_state.apply_config_defaults(
1208 self.config.editor.line_numbers,
1209 self.config.editor.highlight_current_line,
1210 line_wrap,
1211 self.config.editor.wrap_indent,
1212 wrap_column,
1213 self.config.editor.rulers.clone(),
1214 );
1215 }
1216
1217 self.set_active_buffer(buffer_id);
1218
1219 let complete = thread_handle.is_none();
1222 self.stdin_streaming = Some(super::StdinStreamingState {
1223 temp_path: temp_path.to_path_buf(),
1224 buffer_id,
1225 last_known_size: file_size,
1226 complete,
1227 thread_handle,
1228 });
1229
1230 self.status_message = Some(t!("stdin.streaming").to_string());
1232
1233 Ok(buffer_id)
1234 }
1235
1236 pub fn poll_stdin_streaming(&mut self) -> bool {
1239 let Some(ref mut stream_state) = self.stdin_streaming else {
1240 return false;
1241 };
1242
1243 if stream_state.complete {
1244 return false;
1245 }
1246
1247 let mut changed = false;
1248
1249 let current_size = self
1251 .filesystem
1252 .metadata(&stream_state.temp_path)
1253 .map(|m| m.size as usize)
1254 .unwrap_or(stream_state.last_known_size);
1255
1256 if current_size > stream_state.last_known_size {
1258 if let Some(editor_state) = self.buffers.get_mut(&stream_state.buffer_id) {
1259 editor_state
1260 .buffer
1261 .extend_streaming(&stream_state.temp_path, current_size);
1262 }
1263 stream_state.last_known_size = current_size;
1264
1265 self.status_message =
1267 Some(t!("stdin.streaming_bytes", bytes = current_size).to_string());
1268 changed = true;
1269 }
1270
1271 let thread_finished = stream_state
1273 .thread_handle
1274 .as_ref()
1275 .map(|h| h.is_finished())
1276 .unwrap_or(true);
1277
1278 if thread_finished {
1279 if let Some(handle) = stream_state.thread_handle.take() {
1281 match handle.join() {
1282 Ok(Ok(())) => {
1283 tracing::info!("Stdin streaming completed successfully");
1284 }
1285 Ok(Err(e)) => {
1286 tracing::warn!("Stdin streaming error: {}", e);
1287 self.status_message =
1288 Some(t!("stdin.read_error", error = e.to_string()).to_string());
1289 }
1290 Err(_) => {
1291 tracing::warn!("Stdin streaming thread panicked");
1292 self.status_message = Some(t!("stdin.read_error_panic").to_string());
1293 }
1294 }
1295 }
1296 self.complete_stdin_streaming();
1297 changed = true;
1298 }
1299
1300 changed
1301 }
1302
1303 pub fn complete_stdin_streaming(&mut self) {
1306 if let Some(ref mut stream_state) = self.stdin_streaming {
1307 stream_state.complete = true;
1308
1309 let final_size = self
1311 .filesystem
1312 .metadata(&stream_state.temp_path)
1313 .map(|m| m.size as usize)
1314 .unwrap_or(stream_state.last_known_size);
1315
1316 if final_size > stream_state.last_known_size {
1317 if let Some(editor_state) = self.buffers.get_mut(&stream_state.buffer_id) {
1318 editor_state
1319 .buffer
1320 .extend_streaming(&stream_state.temp_path, final_size);
1321 }
1322 stream_state.last_known_size = final_size;
1323 }
1324
1325 self.status_message =
1326 Some(t!("stdin.read_complete", bytes = stream_state.last_known_size).to_string());
1327 }
1328 }
1329
1330 pub fn is_stdin_streaming(&self) -> bool {
1332 self.stdin_streaming
1333 .as_ref()
1334 .map(|s| !s.complete)
1335 .unwrap_or(false)
1336 }
1337
1338 pub fn create_virtual_buffer(
1348 &mut self,
1349 name: String,
1350 mode: String,
1351 read_only: bool,
1352 ) -> BufferId {
1353 let buffer_id = BufferId(self.next_buffer_id);
1354 self.next_buffer_id += 1;
1355
1356 let mut state = EditorState::new(
1357 self.terminal_width,
1358 self.terminal_height,
1359 self.config.editor.large_file_threshold_bytes as usize,
1360 Arc::clone(&self.filesystem),
1361 );
1362 state.set_language_from_name(&name, &self.grammar_registry);
1366
1367 state
1369 .margins
1370 .configure_for_line_numbers(self.config.editor.line_numbers);
1371
1372 self.buffers.insert(buffer_id, state);
1373 self.event_logs
1374 .insert(buffer_id, crate::model::event::EventLog::new());
1375
1376 let metadata = super::types::BufferMetadata::virtual_buffer(name, mode, read_only);
1378 self.buffer_metadata.insert(buffer_id, metadata);
1379
1380 let active_split = self.split_manager.active_split();
1382 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
1383 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
1384 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1385 view_state.add_buffer(buffer_id);
1386 let buf_state = view_state.ensure_buffer_state(buffer_id);
1387 buf_state.apply_config_defaults(
1388 self.config.editor.line_numbers,
1389 self.config.editor.highlight_current_line,
1390 line_wrap,
1391 self.config.editor.wrap_indent,
1392 wrap_column,
1393 self.config.editor.rulers.clone(),
1394 );
1395 } else {
1396 let mut view_state =
1398 SplitViewState::with_buffer(self.terminal_width, self.terminal_height, buffer_id);
1399 view_state.apply_config_defaults(
1400 self.config.editor.line_numbers,
1401 self.config.editor.highlight_current_line,
1402 line_wrap,
1403 self.config.editor.wrap_indent,
1404 wrap_column,
1405 self.config.editor.rulers.clone(),
1406 );
1407 self.split_view_states.insert(active_split, view_state);
1408 }
1409
1410 buffer_id
1411 }
1412
1413 pub fn set_virtual_buffer_content(
1419 &mut self,
1420 buffer_id: BufferId,
1421 entries: Vec<crate::primitives::text_property::TextPropertyEntry>,
1422 ) -> Result<(), String> {
1423 let old_cursor_pos = self
1425 .split_view_states
1426 .values()
1427 .find(|vs| vs.has_buffer(buffer_id))
1428 .and_then(|vs| vs.keyed_states.get(&buffer_id))
1429 .map(|bs| bs.cursors.primary().position)
1430 .unwrap_or(0);
1431
1432 let state = self
1433 .buffers
1434 .get_mut(&buffer_id)
1435 .ok_or_else(|| "Buffer not found".to_string())?;
1436
1437 let (text, properties, collected_overlays) =
1439 crate::primitives::text_property::TextPropertyManager::from_entries(entries);
1440
1441 state.overlays.clear(&mut state.marker_list);
1446
1447 let current_len = state.buffer.len();
1448 if current_len > 0 {
1449 state.buffer.delete_bytes(0, current_len);
1450 }
1451 state.buffer.insert(0, &text);
1452
1453 state.buffer.clear_modified();
1455
1456 state.text_properties = properties;
1458
1459 {
1461 use crate::view::overlay::{Overlay, OverlayFace};
1462 use fresh_core::overlay::OverlayNamespace;
1463
1464 let inline_ns = OverlayNamespace::from_string("_inline".to_string());
1465
1466 for co in collected_overlays {
1467 let face = OverlayFace::from_options(&co.options);
1468 let mut overlay = Overlay::with_namespace(
1469 &mut state.marker_list,
1470 co.range,
1471 face,
1472 inline_ns.clone(),
1473 );
1474 overlay.extend_to_line_end = co.options.extend_to_line_end;
1475 if let Some(url) = co.options.url {
1476 overlay.url = Some(url);
1477 }
1478 state.overlays.add(overlay);
1479 }
1480 }
1481
1482 let new_len = state.buffer.len();
1484 let clamped_pos = old_cursor_pos.min(new_len);
1485 let new_cursor_pos = state.buffer.snap_to_char_boundary(clamped_pos);
1487
1488 for view_state in self.split_view_states.values_mut() {
1490 if let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) {
1491 buf_state.cursors.primary_mut().position = new_cursor_pos;
1492 buf_state.cursors.primary_mut().anchor = None;
1493 }
1494 }
1495
1496 Ok(())
1497 }
1498
1499 pub fn open_help_manual(&mut self) {
1503 let existing_buffer = self
1505 .buffer_metadata
1506 .iter()
1507 .find(|(_, m)| m.display_name == help::HELP_MANUAL_BUFFER_NAME)
1508 .map(|(id, _)| *id);
1509
1510 if let Some(buffer_id) = existing_buffer {
1511 self.set_active_buffer(buffer_id);
1513 return;
1514 }
1515
1516 let buffer_id = self.create_virtual_buffer(
1518 help::HELP_MANUAL_BUFFER_NAME.to_string(),
1519 "special".to_string(),
1520 true,
1521 );
1522
1523 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1525 state.buffer.insert(0, help::HELP_MANUAL_CONTENT);
1526 state.buffer.clear_modified();
1527 state.editing_disabled = true;
1528
1529 state.margins.configure_for_line_numbers(false);
1531 }
1532
1533 self.set_active_buffer(buffer_id);
1534 }
1535
1536 pub fn open_keyboard_shortcuts(&mut self) {
1541 let existing_buffer = self
1543 .buffer_metadata
1544 .iter()
1545 .find(|(_, m)| m.display_name == help::KEYBOARD_SHORTCUTS_BUFFER_NAME)
1546 .map(|(id, _)| *id);
1547
1548 if let Some(buffer_id) = existing_buffer {
1549 self.set_active_buffer(buffer_id);
1551 return;
1552 }
1553
1554 let bindings = self.keybindings.read().unwrap().get_all_bindings();
1556
1557 let mut content = String::from("Keyboard Shortcuts\n");
1559 content.push_str("==================\n\n");
1560 content.push_str("Press 'q' to close this buffer.\n\n");
1561
1562 let mut current_context = String::new();
1564 for (key, action) in &bindings {
1565 let (context, action_name) = if let Some(bracket_end) = action.find("] ") {
1567 let ctx = &action[1..bracket_end];
1568 let name = &action[bracket_end + 2..];
1569 (ctx.to_string(), name.to_string())
1570 } else {
1571 ("Normal".to_string(), action.clone())
1572 };
1573
1574 if context != current_context {
1576 if !current_context.is_empty() {
1577 content.push('\n');
1578 }
1579 content.push_str(&format!("── {} Mode ──\n\n", context));
1580 current_context = context;
1581 }
1582
1583 content.push_str(&format!(" {:20} {}\n", key, action_name));
1585 }
1586
1587 let buffer_id = self.create_virtual_buffer(
1589 help::KEYBOARD_SHORTCUTS_BUFFER_NAME.to_string(),
1590 "special".to_string(),
1591 true,
1592 );
1593
1594 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1596 state.buffer.insert(0, &content);
1597 state.buffer.clear_modified();
1598 state.editing_disabled = true;
1599
1600 state.margins.configure_for_line_numbers(false);
1602 }
1603
1604 self.set_active_buffer(buffer_id);
1605 }
1606
1607 pub fn show_warnings_popup(&mut self) {
1612 if !self.warning_domains.has_any_warnings() {
1613 self.status_message = Some(t!("warnings.none").to_string());
1614 return;
1615 }
1616
1617 self.open_warning_log();
1619 }
1620
1621 pub fn show_lsp_status_popup(&mut self) {
1624 use crate::services::async_bridge::LspServerStatus;
1625
1626 let has_error = self.warning_domains.lsp.level() == crate::app::WarningLevel::Error;
1627
1628 let language = self
1630 .buffers
1631 .get(&self.active_buffer())
1632 .map(|s| s.language.clone())
1633 .unwrap_or_else(|| "unknown".to_string());
1634
1635 self.plugin_manager.run_hook(
1637 "lsp_status_clicked",
1638 crate::services::plugins::hooks::HookArgs::LspStatusClicked {
1639 language: language.clone(),
1640 has_error,
1641 },
1642 );
1643
1644 let running_statuses: std::collections::HashMap<String, LspServerStatus> = self
1647 .lsp_server_statuses
1648 .iter()
1649 .filter(|((lang, _), _)| lang == &language)
1650 .map(|((_, name), status)| (name.clone(), *status))
1651 .collect();
1652
1653 let configured_servers: Vec<String> = self
1654 .config
1655 .lsp
1656 .get(&language)
1657 .map(|cfg| {
1658 cfg.as_slice()
1659 .iter()
1660 .filter(|c| !c.command.is_empty())
1661 .map(|c| c.display_name())
1662 .collect()
1663 })
1664 .unwrap_or_default();
1665
1666 if configured_servers.is_empty() && running_statuses.is_empty() {
1667 self.status_message = Some(t!("lsp.no_server_active").to_string());
1668 return;
1669 }
1670
1671 let mut all_servers: Vec<String> = configured_servers;
1674 for name in running_statuses.keys() {
1675 if !all_servers.contains(name) {
1676 all_servers.push(name.clone());
1677 }
1678 }
1679 all_servers.sort();
1680
1681 let mut items: Vec<crate::model::event::PopupListItemData> = Vec::new();
1682 let mut action_keys: Vec<(String, String)> = Vec::new();
1683
1684 for name in &all_servers {
1685 let status = running_statuses.get(name).copied();
1686 let is_active = status
1687 .map(|s| !matches!(s, LspServerStatus::Shutdown))
1688 .unwrap_or(false);
1689
1690 let (icon, label) = match status {
1692 Some(LspServerStatus::Running) => ("●", "ready"),
1693 Some(LspServerStatus::Error) => ("✗", "error"),
1694 Some(LspServerStatus::Starting) => ("◌", "starting"),
1695 Some(LspServerStatus::Initializing) => ("◌", "initializing"),
1696 Some(LspServerStatus::Shutdown) | None => ("○", "not running"),
1697 };
1698 items.push(crate::model::event::PopupListItemData {
1699 text: format!("{} {} ({})", icon, name, label),
1700 detail: None,
1701 icon: None,
1702 data: None,
1703 });
1704
1705 if is_active {
1706 let restart_key = format!("restart:{}/{}", language, name);
1708 items.push(crate::model::event::PopupListItemData {
1709 text: format!(" Restart {}", name),
1710 detail: None,
1711 icon: None,
1712 data: Some(restart_key.clone()),
1713 });
1714 action_keys.push((restart_key, format!("Restart {}", name)));
1715
1716 let stop_key = format!("stop:{}/{}", language, name);
1718 items.push(crate::model::event::PopupListItemData {
1719 text: format!(" Stop {}", name),
1720 detail: None,
1721 icon: None,
1722 data: Some(stop_key.clone()),
1723 });
1724 action_keys.push((stop_key, format!("Stop {}", name)));
1725 } else {
1726 let start_key = format!("start:{}", language);
1728 if !action_keys.iter().any(|(k, _)| k == &start_key) {
1729 items.push(crate::model::event::PopupListItemData {
1730 text: format!(" Start {}", name),
1731 detail: None,
1732 icon: None,
1733 data: Some(start_key.clone()),
1734 });
1735 action_keys.push((start_key, format!("Start {}", name)));
1736 }
1737 }
1738 }
1739
1740 let log_key = format!("log:{}", language);
1742 items.push(crate::model::event::PopupListItemData {
1743 text: " View Log".to_string(),
1744 detail: None,
1745 icon: None,
1746 data: Some(log_key.clone()),
1747 });
1748 action_keys.push((log_key, "View Log".to_string()));
1749
1750 self.pending_lsp_status_popup = Some(action_keys);
1752
1753 let max_item_width = items.iter().map(|i| i.text.len()).max().unwrap_or(20);
1755 let popup_width = (max_item_width as u16 + 4).clamp(30, 70);
1756
1757 let first_actionable = items.iter().position(|i| i.data.is_some()).unwrap_or(0);
1759
1760 let popup = crate::model::event::PopupData {
1761 kind: crate::model::event::PopupKindHint::List,
1762 title: Some(format!("LSP Servers ({})", language)),
1763 description: None,
1764 transient: false,
1765 content: crate::model::event::PopupContentData::List {
1766 items,
1767 selected: first_actionable,
1768 },
1769 position: crate::model::event::PopupPositionData::BottomRight,
1770 width: popup_width,
1771 max_height: 15,
1772 bordered: true,
1773 };
1774
1775 self.show_popup(popup);
1776 }
1777
1778 pub fn show_file_message_popup(&mut self, message: &str) {
1781 use crate::view::popup::{Popup, PopupPosition};
1782 use ratatui::style::Style;
1783
1784 let md = format!("{}\n\n*esc to dismiss*", message);
1786 let content_width = message.lines().map(|l| l.len()).max().unwrap_or(0) as u16;
1788 let hint_width = 16u16; let popup_width = (content_width.max(hint_width) + 4).clamp(20, 60);
1790
1791 let mut popup = Popup::markdown(&md, &self.theme, Some(&self.grammar_registry));
1792 popup.transient = false;
1793 popup.position = PopupPosition::BelowCursor;
1794 popup.width = popup_width;
1795 popup.max_height = 15;
1796 popup.border_style = Style::default().fg(self.theme.popup_border_fg);
1797 popup.background_style = Style::default().bg(self.theme.popup_bg);
1798
1799 let buffer_id = self.active_buffer();
1800 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1801 state.popups.show(popup);
1802 }
1803 }
1804
1805 pub fn get_text_properties_at_cursor(
1807 &self,
1808 ) -> Option<Vec<&crate::primitives::text_property::TextProperty>> {
1809 let state = self.buffers.get(&self.active_buffer())?;
1810 let cursor_pos = self.active_cursors().primary().position;
1811 Some(state.text_properties.get_at(cursor_pos))
1812 }
1813
1814 pub fn close_buffer(&mut self, id: BufferId) -> anyhow::Result<()> {
1816 if let Some(state) = self.buffers.get(&id) {
1818 if state.buffer.is_modified() {
1819 return Err(anyhow::anyhow!("Buffer has unsaved changes"));
1820 }
1821 }
1822 self.close_buffer_internal(id)
1823 }
1824
1825 pub fn force_close_buffer(&mut self, id: BufferId) -> anyhow::Result<()> {
1828 self.close_buffer_internal(id)
1829 }
1830
1831 fn close_buffer_internal(&mut self, id: BufferId) -> anyhow::Result<()> {
1833 if let Some((wait_id, _)) = self.wait_tracking.remove(&id) {
1835 self.completed_waits.push(wait_id);
1836 }
1837
1838 self.save_file_state_on_close(id);
1840
1841 if let Err(e) = self.delete_buffer_recovery(id) {
1843 tracing::debug!("Failed to delete buffer recovery on close: {}", e);
1844 }
1845
1846 if let Some(terminal_id) = self.terminal_buffers.remove(&id) {
1848 self.terminal_manager.close(terminal_id);
1850
1851 let backing_file = self.terminal_backing_files.remove(&terminal_id);
1853 if let Some(ref path) = backing_file {
1854 #[allow(clippy::let_underscore_must_use)]
1856 let _ = self.filesystem.remove_file(path);
1857 }
1858 if let Some(log_file) = self.terminal_log_files.remove(&terminal_id) {
1860 if backing_file.as_ref() != Some(&log_file) {
1861 #[allow(clippy::let_underscore_must_use)]
1863 let _ = self.filesystem.remove_file(&log_file);
1864 }
1865 }
1866
1867 self.terminal_mode_resume.remove(&id);
1869
1870 if self.terminal_mode {
1872 self.terminal_mode = false;
1873 self.key_context = crate::input::keybindings::KeyContext::Normal;
1874 }
1875 }
1876
1877 let active_split = self.split_manager.active_split();
1881
1882 let replacement_target: Option<crate::view::split::TabTarget> =
1883 self.split_view_states.get(&active_split).and_then(|vs| {
1884 use crate::view::split::TabTarget;
1885 vs.focus_history.iter().rev().find_map(|t| match t {
1886 TabTarget::Buffer(bid) if *bid == id => None, TabTarget::Buffer(bid) => {
1888 let hidden = self
1890 .buffer_metadata
1891 .get(bid)
1892 .map(|m| m.hidden_from_tabs)
1893 .unwrap_or(false);
1894 if hidden || !self.buffers.contains_key(bid) {
1895 None
1896 } else {
1897 Some(*t)
1898 }
1899 }
1900 TabTarget::Group(leaf) => {
1901 if self.grouped_subtrees.contains_key(leaf) {
1903 Some(*t)
1904 } else {
1905 None
1906 }
1907 }
1908 })
1909 });
1910
1911 let fallback_buffer: Option<BufferId> = if replacement_target.is_none() {
1913 self.buffers
1914 .keys()
1915 .find(|&&bid| {
1916 bid != id
1917 && !self
1918 .buffer_metadata
1919 .get(&bid)
1920 .map(|m| m.hidden_from_tabs)
1921 .unwrap_or(false)
1922 })
1923 .copied()
1924 } else {
1925 None
1926 };
1927
1928 let closing_active = self.active_buffer() == id;
1931
1932 let return_to_group = match replacement_target {
1935 Some(crate::view::split::TabTarget::Group(leaf)) => Some(leaf),
1936 _ => None,
1937 };
1938 let replacement_buffer = match replacement_target {
1939 Some(crate::view::split::TabTarget::Buffer(bid)) => bid,
1940 Some(crate::view::split::TabTarget::Group(group_leaf)) => {
1941 self.grouped_subtrees
1945 .get(&group_leaf)
1946 .and_then(|node| {
1947 if let crate::view::split::SplitNode::Grouped {
1948 active_inner_leaf, ..
1949 } = node
1950 {
1951 self.split_view_states
1952 .get(active_inner_leaf)
1953 .map(|vs| vs.active_buffer)
1954 } else {
1955 None
1956 }
1957 })
1958 .unwrap_or_else(|| fallback_buffer.unwrap_or_else(|| self.new_buffer()))
1959 }
1960 None => fallback_buffer.unwrap_or_else(|| self.new_buffer()),
1961 };
1962
1963 let created_empty_buffer = replacement_target.is_none() && fallback_buffer.is_none();
1964
1965 if closing_active {
1969 self.set_active_buffer(replacement_buffer);
1970
1971 if return_to_group.is_some() {
1975 if let Some(vs) = self.split_view_states.get_mut(&active_split) {
1976 use crate::view::split::TabTarget;
1977 vs.open_buffers
1978 .retain(|t| *t != TabTarget::Buffer(replacement_buffer));
1979 vs.focus_history
1980 .retain(|t| *t != TabTarget::Buffer(replacement_buffer));
1981 }
1982 }
1983 }
1984
1985 let splits_to_update = self.split_manager.splits_for_buffer(id);
1987 for split_id in splits_to_update {
1988 self.split_manager
1989 .set_split_buffer(split_id, replacement_buffer);
1990 }
1991
1992 self.buffers.remove(&id);
1993 self.event_logs.remove(&id);
1994 self.seen_byte_ranges.remove(&id);
1995 self.buffer_metadata.remove(&id);
1996 if let Some((request_id, _, _)) = self.semantic_tokens_in_flight.remove(&id) {
1997 self.pending_semantic_token_requests.remove(&request_id);
1998 }
1999 if let Some((request_id, _, _, _)) = self.semantic_tokens_range_in_flight.remove(&id) {
2000 self.pending_semantic_token_range_requests
2001 .remove(&request_id);
2002 }
2003 self.semantic_tokens_range_last_request.remove(&id);
2004 self.semantic_tokens_range_applied.remove(&id);
2005 self.semantic_tokens_full_debounce.remove(&id);
2006
2007 self.panel_ids.retain(|_, &mut buf_id| buf_id != id);
2010
2011 for view_state in self.split_view_states.values_mut() {
2013 view_state.remove_buffer(id);
2014 view_state.remove_from_history(id);
2015 }
2016
2017 if closing_active {
2018 if created_empty_buffer {
2019 self.focus_file_explorer();
2020 }
2021 if let Some(group_leaf) = return_to_group {
2022 self.activate_group_tab(group_leaf);
2023 }
2024 }
2025
2026 Ok(())
2027 }
2028
2029 pub fn switch_buffer(&mut self, id: BufferId) {
2031 if self.buffers.contains_key(&id) && id != self.active_buffer() {
2032 self.position_history.commit_pending_movement();
2034
2035 let cursors = self.active_cursors();
2037 let position = cursors.primary().position;
2038 let anchor = cursors.primary().anchor;
2039 self.position_history
2040 .record_movement(self.active_buffer(), position, anchor);
2041 self.position_history.commit_pending_movement();
2042
2043 self.set_active_buffer(id);
2044 }
2045 }
2046
2047 pub fn close_tab(&mut self) {
2056 let active_split = self.split_manager.active_split();
2059 if let Some(group_leaf_id) = self
2060 .split_view_states
2061 .get(&active_split)
2062 .and_then(|vs| vs.active_group_tab)
2063 {
2064 self.close_buffer_group_by_leaf(group_leaf_id);
2065 self.set_status_message(t!("buffer.tab_closed").to_string());
2066 return;
2067 }
2068
2069 let buffer_id = self.active_buffer();
2070
2071 let buffer_in_other_splits = self
2073 .split_view_states
2074 .iter()
2075 .filter(|(&split_id, view_state)| {
2076 split_id != active_split && view_state.has_buffer(buffer_id)
2077 })
2078 .count();
2079
2080 let current_split_tabs = self
2082 .split_view_states
2083 .get(&active_split)
2084 .map(|vs| vs.buffer_tab_ids_vec())
2085 .unwrap_or_default();
2086
2087 let is_last_viewport = buffer_in_other_splits == 0;
2090
2091 if is_last_viewport {
2092 let has_other_splits = self.split_manager.root().count_leaves() > 1;
2095 if current_split_tabs.len() <= 1 && has_other_splits {
2096 if self.active_state().buffer.is_modified() {
2098 let name = self.get_buffer_display_name(buffer_id);
2099 let save_key = t!("prompt.key.save").to_string();
2100 let discard_key = t!("prompt.key.discard").to_string();
2101 let cancel_key = t!("prompt.key.cancel").to_string();
2102 self.start_prompt(
2103 t!(
2104 "prompt.buffer_modified",
2105 name = name,
2106 save_key = save_key,
2107 discard_key = discard_key,
2108 cancel_key = cancel_key
2109 )
2110 .to_string(),
2111 PromptType::ConfirmCloseBuffer { buffer_id },
2112 );
2113 return;
2114 }
2115 if let Err(e) = self.close_buffer(buffer_id) {
2117 tracing::warn!("Failed to close buffer: {}", e);
2118 }
2119 self.close_active_split();
2120 return;
2121 }
2122
2123 if self.active_state().buffer.is_modified() {
2125 let name = self.get_buffer_display_name(buffer_id);
2127 let save_key = t!("prompt.key.save").to_string();
2128 let discard_key = t!("prompt.key.discard").to_string();
2129 let cancel_key = t!("prompt.key.cancel").to_string();
2130 self.start_prompt(
2131 t!(
2132 "prompt.buffer_modified",
2133 name = name,
2134 save_key = save_key,
2135 discard_key = discard_key,
2136 cancel_key = cancel_key
2137 )
2138 .to_string(),
2139 PromptType::ConfirmCloseBuffer { buffer_id },
2140 );
2141 } else if let Err(e) = self.close_buffer(buffer_id) {
2142 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
2143 } else {
2144 self.set_status_message(t!("buffer.tab_closed").to_string());
2145 }
2146 } else {
2147 if current_split_tabs.len() <= 1 {
2149 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
2152 self.terminal_mode = false;
2153 self.key_context = crate::input::keybindings::KeyContext::Normal;
2154 }
2155 self.close_active_split();
2156 return;
2157 }
2158
2159 let current_idx = current_split_tabs
2161 .iter()
2162 .position(|&id| id == buffer_id)
2163 .unwrap_or(0);
2164 let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
2165 let replacement_buffer = current_split_tabs[replacement_idx];
2166
2167 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
2169 self.terminal_mode = false;
2170 self.key_context = crate::input::keybindings::KeyContext::Normal;
2171 }
2172
2173 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
2175 view_state.remove_buffer(buffer_id);
2176 }
2177
2178 self.split_manager
2180 .set_split_buffer(active_split, replacement_buffer);
2181
2182 self.set_status_message(t!("buffer.tab_closed").to_string());
2183 }
2184 }
2185
2186 pub fn close_tab_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) -> bool {
2190 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
2192 self.terminal_mode = false;
2193 self.key_context = crate::input::keybindings::KeyContext::Normal;
2194 }
2195
2196 let buffer_in_other_splits = self
2198 .split_view_states
2199 .iter()
2200 .filter(|(&sid, view_state)| sid != split_id && view_state.has_buffer(buffer_id))
2201 .count();
2202
2203 let split_tabs = self
2205 .split_view_states
2206 .get(&split_id)
2207 .map(|vs| vs.buffer_tab_ids_vec())
2208 .unwrap_or_default();
2209
2210 let is_last_viewport = buffer_in_other_splits == 0;
2211
2212 if is_last_viewport {
2213 if let Some(state) = self.buffers.get(&buffer_id) {
2215 if state.buffer.is_modified() {
2216 let name = self.get_buffer_display_name(buffer_id);
2218 let save_key = t!("prompt.key.save").to_string();
2219 let discard_key = t!("prompt.key.discard").to_string();
2220 let cancel_key = t!("prompt.key.cancel").to_string();
2221 self.start_prompt(
2222 t!(
2223 "prompt.buffer_modified",
2224 name = name,
2225 save_key = save_key,
2226 discard_key = discard_key,
2227 cancel_key = cancel_key
2228 )
2229 .to_string(),
2230 PromptType::ConfirmCloseBuffer { buffer_id },
2231 );
2232 return false;
2233 }
2234 }
2235 if let Err(e) = self.close_buffer(buffer_id) {
2236 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
2237 } else {
2238 self.set_status_message(t!("buffer.tab_closed").to_string());
2239 }
2240 } else {
2241 if split_tabs.len() <= 1 {
2243 self.handle_close_split(split_id.into());
2245 return true;
2246 }
2247
2248 let current_idx = split_tabs
2250 .iter()
2251 .position(|&id| id == buffer_id)
2252 .unwrap_or(0);
2253 let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
2254 let replacement_buffer = split_tabs[replacement_idx];
2255
2256 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2258 view_state.remove_buffer(buffer_id);
2259 }
2260
2261 self.split_manager
2263 .set_split_buffer(split_id, replacement_buffer);
2264
2265 self.set_status_message(t!("buffer.tab_closed").to_string());
2266 }
2267 true
2268 }
2269
2270 pub fn close_other_tabs_in_split(&mut self, keep_buffer_id: BufferId, split_id: LeafId) {
2272 let split_tabs = self
2274 .split_view_states
2275 .get(&split_id)
2276 .map(|vs| vs.buffer_tab_ids_vec())
2277 .unwrap_or_default();
2278
2279 let tabs_to_close: Vec<_> = split_tabs
2281 .iter()
2282 .filter(|&&id| id != keep_buffer_id)
2283 .copied()
2284 .collect();
2285
2286 let mut closed = 0;
2287 let mut skipped_modified = 0;
2288 for buffer_id in tabs_to_close {
2289 if self.close_tab_in_split_silent(buffer_id, split_id) {
2290 closed += 1;
2291 } else {
2292 skipped_modified += 1;
2293 }
2294 }
2295
2296 self.split_manager
2298 .set_split_buffer(split_id, keep_buffer_id);
2299
2300 self.set_batch_close_status_message(closed, skipped_modified);
2301 }
2302
2303 pub fn close_tabs_to_right_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) {
2305 let split_tabs = self
2307 .split_view_states
2308 .get(&split_id)
2309 .map(|vs| vs.buffer_tab_ids_vec())
2310 .unwrap_or_default();
2311
2312 let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
2314 return;
2315 };
2316
2317 let tabs_to_close: Vec<_> = split_tabs.iter().skip(target_idx + 1).copied().collect();
2319
2320 let mut closed = 0;
2321 let mut skipped_modified = 0;
2322 for buf_id in tabs_to_close {
2323 if self.close_tab_in_split_silent(buf_id, split_id) {
2324 closed += 1;
2325 } else {
2326 skipped_modified += 1;
2327 }
2328 }
2329
2330 self.set_batch_close_status_message(closed, skipped_modified);
2331 }
2332
2333 pub fn close_tabs_to_left_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) {
2335 let split_tabs = self
2337 .split_view_states
2338 .get(&split_id)
2339 .map(|vs| vs.buffer_tab_ids_vec())
2340 .unwrap_or_default();
2341
2342 let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
2344 return;
2345 };
2346
2347 let tabs_to_close: Vec<_> = split_tabs.iter().take(target_idx).copied().collect();
2349
2350 let mut closed = 0;
2351 let mut skipped_modified = 0;
2352 for buf_id in tabs_to_close {
2353 if self.close_tab_in_split_silent(buf_id, split_id) {
2354 closed += 1;
2355 } else {
2356 skipped_modified += 1;
2357 }
2358 }
2359
2360 self.set_batch_close_status_message(closed, skipped_modified);
2361 }
2362
2363 pub fn close_all_tabs_in_split(&mut self, split_id: LeafId) {
2365 let split_tabs = self
2367 .split_view_states
2368 .get(&split_id)
2369 .map(|vs| vs.buffer_tab_ids_vec())
2370 .unwrap_or_default();
2371
2372 let mut closed = 0;
2373 let mut skipped_modified = 0;
2374
2375 for buffer_id in split_tabs {
2377 if self.close_tab_in_split_silent(buffer_id, split_id) {
2378 closed += 1;
2379 } else {
2380 skipped_modified += 1;
2381 }
2382 }
2383
2384 self.set_batch_close_status_message(closed, skipped_modified);
2385 }
2386
2387 fn set_batch_close_status_message(&mut self, closed: usize, skipped_modified: usize) {
2389 let message = match (closed, skipped_modified) {
2390 (0, 0) => t!("buffer.no_tabs_to_close").to_string(),
2391 (0, n) => t!("buffer.skipped_modified", count = n).to_string(),
2392 (n, 0) => t!("buffer.closed_tabs", count = n).to_string(),
2393 (c, s) => t!("buffer.closed_tabs_skipped", closed = c, skipped = s).to_string(),
2394 };
2395 self.set_status_message(message);
2396 }
2397
2398 fn close_tab_in_split_silent(&mut self, buffer_id: BufferId, split_id: LeafId) -> bool {
2402 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
2404 self.terminal_mode = false;
2405 self.key_context = crate::input::keybindings::KeyContext::Normal;
2406 }
2407
2408 let buffer_in_other_splits = self
2410 .split_view_states
2411 .iter()
2412 .filter(|(&sid, view_state)| sid != split_id && view_state.has_buffer(buffer_id))
2413 .count();
2414
2415 let split_tabs = self
2417 .split_view_states
2418 .get(&split_id)
2419 .map(|vs| vs.buffer_tab_ids_vec())
2420 .unwrap_or_default();
2421
2422 let is_last_viewport = buffer_in_other_splits == 0;
2423
2424 if is_last_viewport {
2425 if let Some(state) = self.buffers.get(&buffer_id) {
2428 if state.buffer.is_modified() {
2429 return false;
2431 }
2432 }
2433 if let Err(e) = self.close_buffer(buffer_id) {
2434 tracing::warn!("Failed to close buffer: {}", e);
2435 }
2436 true
2437 } else {
2438 if split_tabs.len() <= 1 {
2440 self.handle_close_split(split_id.into());
2442 return true;
2443 }
2444
2445 let current_idx = split_tabs
2447 .iter()
2448 .position(|&id| id == buffer_id)
2449 .unwrap_or(0);
2450 let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
2451 let replacement_buffer = split_tabs.get(replacement_idx).copied();
2452
2453 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2455 view_state.remove_buffer(buffer_id);
2456 }
2457
2458 if let Some(replacement) = replacement_buffer {
2460 self.split_manager.set_split_buffer(split_id, replacement);
2461 }
2462 true
2463 }
2464 }
2465
2466 pub fn next_buffer(&mut self) {
2468 self.cycle_tab(1);
2469 }
2470
2471 pub fn prev_buffer(&mut self) {
2473 self.cycle_tab(-1);
2474 }
2475
2476 fn cycle_tab(&mut self, direction: i32) {
2479 use crate::view::split::TabTarget;
2480
2481 let active_split = self.split_manager.active_split();
2482 let Some(view_state) = self.split_view_states.get(&active_split) else {
2483 return;
2484 };
2485
2486 let targets: Vec<TabTarget> = view_state
2488 .open_buffers
2489 .iter()
2490 .copied()
2491 .filter(|t| match t {
2492 TabTarget::Buffer(id) => !self
2493 .buffer_metadata
2494 .get(id)
2495 .map(|m| m.hidden_from_tabs)
2496 .unwrap_or(false),
2497 TabTarget::Group(_) => true,
2498 })
2499 .collect();
2500
2501 if targets.len() < 2 {
2502 return;
2503 }
2504
2505 let current_target = view_state.active_target();
2506 let Some(idx) = targets.iter().position(|t| *t == current_target) else {
2507 return;
2508 };
2509
2510 let next_idx = if direction > 0 {
2511 (idx + 1) % targets.len()
2512 } else if idx == 0 {
2513 targets.len() - 1
2514 } else {
2515 idx - 1
2516 };
2517
2518 if targets[next_idx] == current_target {
2519 return;
2520 }
2521
2522 self.position_history.commit_pending_movement();
2524 let cursors = self.active_cursors();
2525 let position = cursors.primary().position;
2526 let anchor = cursors.primary().anchor;
2527 self.position_history
2528 .record_movement(self.active_buffer(), position, anchor);
2529 self.position_history.commit_pending_movement();
2530
2531 match targets[next_idx] {
2532 TabTarget::Buffer(buffer_id) => {
2533 self.set_active_buffer(buffer_id);
2534 }
2535 TabTarget::Group(group_leaf_id) => {
2536 self.activate_group_tab(group_leaf_id);
2537 }
2538 }
2539 }
2540
2541 pub fn navigate_back(&mut self) {
2543 self.in_navigation = true;
2545
2546 self.position_history.commit_pending_movement();
2548
2549 if self.position_history.can_go_back() && !self.position_history.can_go_forward() {
2552 let cursors = self.active_cursors();
2553 let position = cursors.primary().position;
2554 let anchor = cursors.primary().anchor;
2555 self.position_history
2556 .record_movement(self.active_buffer(), position, anchor);
2557 self.position_history.commit_pending_movement();
2558 }
2559
2560 if let Some(entry) = self.position_history.back() {
2562 let target_buffer = entry.buffer_id;
2563 let target_position = entry.position;
2564 let target_anchor = entry.anchor;
2565
2566 if self.buffers.contains_key(&target_buffer) {
2568 self.set_active_buffer(target_buffer);
2569
2570 let cursors = self.active_cursors();
2572 let cursor_id = cursors.primary_id();
2573 let old_position = cursors.primary().position;
2574 let old_anchor = cursors.primary().anchor;
2575 let old_sticky_column = cursors.primary().sticky_column;
2576 let event = Event::MoveCursor {
2577 cursor_id,
2578 old_position,
2579 new_position: target_position,
2580 old_anchor,
2581 new_anchor: target_anchor,
2582 old_sticky_column,
2583 new_sticky_column: 0, };
2585 let split_id = self.split_manager.active_split();
2586 let state = self.buffers.get_mut(&target_buffer).unwrap();
2587 let view_state = self.split_view_states.get_mut(&split_id).unwrap();
2588 state.apply(&mut view_state.cursors, &event);
2589 }
2590 }
2591
2592 self.in_navigation = false;
2594 }
2595
2596 pub fn navigate_forward(&mut self) {
2598 self.in_navigation = true;
2600
2601 if let Some(entry) = self.position_history.forward() {
2602 let target_buffer = entry.buffer_id;
2603 let target_position = entry.position;
2604 let target_anchor = entry.anchor;
2605
2606 if self.buffers.contains_key(&target_buffer) {
2608 self.set_active_buffer(target_buffer);
2609
2610 let cursors = self.active_cursors();
2612 let cursor_id = cursors.primary_id();
2613 let old_position = cursors.primary().position;
2614 let old_anchor = cursors.primary().anchor;
2615 let old_sticky_column = cursors.primary().sticky_column;
2616 let event = Event::MoveCursor {
2617 cursor_id,
2618 old_position,
2619 new_position: target_position,
2620 old_anchor,
2621 new_anchor: target_anchor,
2622 old_sticky_column,
2623 new_sticky_column: 0, };
2625 let split_id = self.split_manager.active_split();
2626 let state = self.buffers.get_mut(&target_buffer).unwrap();
2627 let view_state = self.split_view_states.get_mut(&split_id).unwrap();
2628 state.apply(&mut view_state.cursors, &event);
2629 }
2630 }
2631
2632 self.in_navigation = false;
2634 }
2635
2636 pub fn get_mouse_hover_state(&self) -> Option<(usize, u16, u16)> {
2639 self.mouse_state
2640 .lsp_hover_state
2641 .map(|(pos, _, x, y)| (pos, x, y))
2642 }
2643
2644 pub fn has_transient_popup(&self) -> bool {
2646 self.active_state()
2647 .popups
2648 .top()
2649 .is_some_and(|p| p.transient)
2650 }
2651
2652 pub fn force_check_mouse_hover(&mut self) -> bool {
2655 if let Some((byte_pos, _, screen_x, screen_y)) = self.mouse_state.lsp_hover_state {
2656 if !self.mouse_state.lsp_hover_request_sent {
2657 self.mouse_hover_screen_position = Some((screen_x, screen_y));
2658 match self.request_hover_at_position(byte_pos) {
2659 Ok(true) => {
2660 self.mouse_state.lsp_hover_request_sent = true;
2661 return true;
2662 }
2663 Ok(false) => return false, Err(e) => {
2665 tracing::debug!("Failed to request hover: {}", e);
2666 return false;
2667 }
2668 }
2669 }
2670 }
2671 false
2672 }
2673
2674 pub fn schedule_hot_exit_recovery(&mut self) {
2681 if self.config.editor.hot_exit {
2682 self.pending_hot_exit_recovery = true;
2683 }
2684 }
2685
2686 #[allow(clippy::too_many_arguments)]
2687 pub fn queue_file_open(
2688 &mut self,
2689 path: PathBuf,
2690 line: Option<usize>,
2691 column: Option<usize>,
2692 end_line: Option<usize>,
2693 end_column: Option<usize>,
2694 message: Option<String>,
2695 wait_id: Option<u64>,
2696 ) {
2697 self.pending_file_opens.push(super::PendingFileOpen {
2698 path,
2699 line,
2700 column,
2701 end_line,
2702 end_column,
2703 message,
2704 wait_id,
2705 });
2706 }
2707
2708 pub fn process_pending_file_opens(&mut self) -> bool {
2713 if self.pending_file_opens.is_empty() {
2714 return false;
2715 }
2716
2717 let pending = std::mem::take(&mut self.pending_file_opens);
2719 let mut processed_any = false;
2720
2721 for pending_file in pending {
2722 tracing::info!(
2723 "[SYNTAX DEBUG] Processing pending file open: {:?}",
2724 pending_file.path
2725 );
2726
2727 match self.open_file(&pending_file.path) {
2728 Ok(_) => {
2729 if let (Some(line), Some(end_line)) = (pending_file.line, pending_file.end_line)
2731 {
2732 self.select_range(
2733 line,
2734 pending_file.column,
2735 end_line,
2736 pending_file.end_column,
2737 );
2738 } else if let Some(line) = pending_file.line {
2739 self.goto_line_col(line, pending_file.column);
2740 }
2741 let has_popup = pending_file.message.is_some();
2743 if let Some(ref msg) = pending_file.message {
2744 self.show_file_message_popup(msg);
2745 }
2746 if let Some(wait_id) = pending_file.wait_id {
2748 let buffer_id = self.active_buffer();
2749 self.wait_tracking.insert(buffer_id, (wait_id, has_popup));
2750 }
2751 processed_any = true;
2752 }
2753 Err(e) => {
2754 if let Some(confirmation) =
2757 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
2758 {
2759 self.start_large_file_encoding_confirmation(confirmation);
2760 } else {
2761 self.set_status_message(
2763 t!("file.error_opening", error = e.to_string()).to_string(),
2764 );
2765 }
2766 processed_any = true;
2767 }
2768 }
2769 }
2770
2771 if processed_any && self.pending_hot_exit_recovery {
2773 self.pending_hot_exit_recovery = false;
2774 match self.apply_hot_exit_recovery() {
2775 Ok(count) if count > 0 => {
2776 tracing::info!("Hot exit: restored unsaved changes for {} buffer(s)", count);
2777 }
2778 Ok(_) => {}
2779 Err(e) => {
2780 tracing::warn!("Failed to apply hot exit recovery: {}", e);
2781 }
2782 }
2783 }
2784
2785 processed_any
2786 }
2787
2788 pub fn take_completed_waits(&mut self) -> Vec<u64> {
2790 std::mem::take(&mut self.completed_waits)
2791 }
2792
2793 pub fn remove_wait_tracking(&mut self, wait_id: u64) {
2795 self.wait_tracking.retain(|_, (wid, _)| *wid != wait_id);
2796 }
2797
2798 pub fn start_incremental_line_scan(&mut self, open_goto_line: bool) {
2807 let buffer_id = self.active_buffer();
2808 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2809 let (chunks, total_bytes) = state.buffer.prepare_line_scan();
2810 let leaves = state.buffer.piece_tree_leaves();
2811 self.line_scan_state = Some(super::LineScanState {
2812 buffer_id,
2813 leaves,
2814 chunks,
2815 next_chunk: 0,
2816 total_bytes,
2817 scanned_bytes: 0,
2818 updates: Vec::new(),
2819 open_goto_line_on_complete: open_goto_line,
2820 });
2821 self.set_status_message(t!("goto.scanning_progress", percent = 0).to_string());
2822 }
2823 }
2824
2825 pub fn process_line_scan(&mut self) -> bool {
2828 let _span = tracing::info_span!("process_line_scan").entered();
2829 let scan = match self.line_scan_state.as_mut() {
2830 Some(s) => s,
2831 None => return false,
2832 };
2833
2834 let buffer_id = scan.buffer_id;
2835
2836 if let Err(e) = self.process_line_scan_batch(buffer_id) {
2837 tracing::warn!("Line scan error: {e}");
2838 self.finish_line_scan_with_error(e);
2839 return true;
2840 }
2841
2842 let scan = self.line_scan_state.as_ref().unwrap();
2843 if scan.next_chunk >= scan.chunks.len() {
2844 self.finish_line_scan_ok();
2845 } else {
2846 let pct = if scan.total_bytes > 0 {
2847 (scan.scanned_bytes * 100) / scan.total_bytes
2848 } else {
2849 100
2850 };
2851 self.set_status_message(t!("goto.scanning_progress", percent = pct).to_string());
2852 }
2853 true
2854 }
2855
2856 fn process_line_scan_batch(&mut self, buffer_id: BufferId) -> std::io::Result<()> {
2864 let _span = tracing::info_span!("process_line_scan_batch").entered();
2865 let concurrency = self.config.editor.read_concurrency.max(1);
2866
2867 let state = self.buffers.get(&buffer_id);
2868 let scan = self.line_scan_state.as_mut().unwrap();
2869
2870 let mut results: Vec<(usize, usize)> = Vec::new();
2871 let mut io_work: Vec<(usize, std::path::PathBuf, u64, usize)> = Vec::new();
2872
2873 while scan.next_chunk < scan.chunks.len() && (results.len() + io_work.len()) < concurrency {
2874 let chunk = scan.chunks[scan.next_chunk].clone();
2875 scan.next_chunk += 1;
2876 scan.scanned_bytes += chunk.byte_len;
2877
2878 if chunk.already_known {
2879 continue;
2880 }
2881
2882 if let Some(state) = state {
2883 let leaf = &scan.leaves[chunk.leaf_index];
2884
2885 match state.buffer.leaf_io_params(leaf) {
2889 None => {
2890 let count = state.buffer.scan_leaf(leaf)?;
2892 results.push((chunk.leaf_index, count));
2893 }
2894 Some((path, offset, len)) => {
2895 io_work.push((chunk.leaf_index, path, offset, len));
2897 }
2898 }
2899 }
2900 }
2901
2902 if !io_work.is_empty() {
2904 let fs = match state {
2905 Some(s) => s.buffer.filesystem().clone(),
2906 None => return Ok(()),
2907 };
2908
2909 let rt = self
2910 .tokio_runtime
2911 .as_ref()
2912 .ok_or_else(|| std::io::Error::other("async runtime not available"))?;
2913
2914 let io_results: Vec<std::io::Result<(usize, usize)>> = rt.block_on(async {
2915 let mut handles = Vec::with_capacity(io_work.len());
2916 for (leaf_idx, path, offset, len) in io_work {
2917 let fs = fs.clone();
2918 handles.push(tokio::task::spawn_blocking(move || {
2919 let count = fs.count_line_feeds_in_range(&path, offset, len)?;
2920 Ok((leaf_idx, count))
2921 }));
2922 }
2923
2924 let mut results = Vec::with_capacity(handles.len());
2925 for handle in handles {
2926 results.push(handle.await.unwrap());
2927 }
2928 results
2929 });
2930
2931 for result in io_results {
2932 results.push(result?);
2933 }
2934 }
2935
2936 for (leaf_idx, count) in results {
2937 scan.updates.push((leaf_idx, count));
2938 }
2939
2940 Ok(())
2941 }
2942
2943 fn finish_line_scan_ok(&mut self) {
2944 let _span = tracing::info_span!("finish_line_scan_ok").entered();
2945 let scan = self.line_scan_state.take().unwrap();
2946 let open_goto = scan.open_goto_line_on_complete;
2947 if let Some(state) = self.buffers.get_mut(&scan.buffer_id) {
2948 let _span = tracing::info_span!(
2949 "rebuild_with_pristine_saved_root",
2950 updates = scan.updates.len()
2951 )
2952 .entered();
2953 state.buffer.rebuild_with_pristine_saved_root(&scan.updates);
2954 }
2955 self.set_status_message(t!("goto.scan_complete").to_string());
2956 if open_goto {
2957 self.open_goto_line_if_active(scan.buffer_id);
2958 }
2959 }
2960
2961 fn finish_line_scan_with_error(&mut self, e: std::io::Error) {
2962 let scan = self.line_scan_state.take().unwrap();
2963 let open_goto = scan.open_goto_line_on_complete;
2964 self.set_status_message(t!("goto.scan_failed", error = e.to_string()).to_string());
2965 if open_goto {
2966 self.open_goto_line_if_active(scan.buffer_id);
2967 }
2968 }
2969
2970 fn open_goto_line_if_active(&mut self, buffer_id: BufferId) {
2971 if self.active_buffer() == buffer_id {
2972 self.start_prompt(
2973 t!("file.goto_line_prompt").to_string(),
2974 PromptType::GotoLine,
2975 );
2976 }
2977 }
2978
2979 pub fn process_search_scan(&mut self) -> bool {
2984 let scan = match self.search_scan_state.as_mut() {
2985 Some(s) => s,
2986 None => return false,
2987 };
2988
2989 let buffer_id = scan.buffer_id;
2990
2991 if let Err(e) = self.process_search_scan_batch(buffer_id) {
2992 tracing::warn!("Search scan error: {e}");
2993 let _scan = self.search_scan_state.take().unwrap();
2994 self.set_status_message(format!("Search failed: {e}"));
2995 return true;
2996 }
2997
2998 let scan = self.search_scan_state.as_ref().unwrap();
2999 if scan.scan.is_done() {
3000 self.finish_search_scan();
3001 } else {
3002 let pct = scan.scan.progress_percent();
3003 let match_count = scan.scan.matches.len();
3004 self.set_status_message(format!(
3005 "Searching... {}% ({} matches so far)",
3006 pct, match_count
3007 ));
3008 }
3009 true
3010 }
3011
3012 fn process_search_scan_batch(
3015 &mut self,
3016 buffer_id: crate::model::event::BufferId,
3017 ) -> std::io::Result<()> {
3018 let concurrency = self.config.editor.read_concurrency.max(1);
3019
3020 for _ in 0..concurrency {
3021 let is_done = {
3022 let scan_state = match self.search_scan_state.as_ref() {
3023 Some(s) => s,
3024 None => return Ok(()),
3025 };
3026 scan_state.scan.is_done()
3027 };
3028 if is_done {
3029 break;
3030 }
3031
3032 let mut scan = self.search_scan_state.take().unwrap();
3035 let result = if let Some(state) = self.buffers.get_mut(&buffer_id) {
3036 state.buffer.search_scan_next_chunk(&mut scan.scan)
3037 } else {
3038 Ok(false)
3039 };
3040 self.search_scan_state = Some(scan);
3041
3042 match result {
3043 Ok(false) => break, Ok(true) => {} Err(e) => return Err(e),
3046 }
3047 }
3048
3049 Ok(())
3050 }
3051
3052 fn finish_search_scan(&mut self) {
3056 let scan = self.search_scan_state.take().unwrap();
3057 let buffer_id = scan.buffer_id;
3058 let match_ranges: Vec<(usize, usize)> = scan
3059 .scan
3060 .matches
3061 .iter()
3062 .map(|m| (m.byte_offset, m.length))
3063 .collect();
3064 let capped = scan.scan.capped;
3065 let query = scan.query;
3066
3067 if let Some(state) = self.buffers.get_mut(&buffer_id) {
3071 state.buffer.refresh_saved_root_if_unmodified();
3072 }
3073
3074 if match_ranges.is_empty() {
3075 self.search_state = None;
3076 self.set_status_message(format!("No matches found for '{}'", query));
3077 return;
3078 }
3079
3080 self.finalize_search(&query, match_ranges, capped, None);
3081 }
3082}