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 let base_dir = if self.filesystem.remote_connection_info().is_some() {
168 self.filesystem
169 .home_dir()
170 .unwrap_or_else(|_| self.working_dir.clone())
171 } else {
172 self.working_dir.clone()
173 };
174
175 let resolved_path = if path.is_relative() {
176 base_dir.join(path)
177 } else {
178 path.to_path_buf()
179 };
180
181 let file_exists = self.filesystem.exists(&resolved_path);
184
185 let display_path = resolved_path.clone();
189
190 let canonical_path = if file_exists {
194 self.filesystem
195 .canonicalize(&resolved_path)
196 .unwrap_or_else(|_| resolved_path.clone())
197 } else {
198 if let Some(parent) = resolved_path.parent() {
200 let canonical_parent = if parent.as_os_str().is_empty() {
201 base_dir.clone()
203 } else {
204 self.filesystem
205 .canonicalize(parent)
206 .unwrap_or_else(|_| parent.to_path_buf())
207 };
208 if let Some(filename) = resolved_path.file_name() {
209 canonical_parent.join(filename)
210 } else {
211 resolved_path
212 }
213 } else {
214 resolved_path
215 }
216 };
217 let path = canonical_path.as_path();
218
219 if self.filesystem.is_dir(path).unwrap_or(false) {
223 anyhow::bail!(t!("buffer.cannot_open_directory"));
224 }
225
226 let already_open = self
228 .buffers
229 .iter()
230 .find(|(_, state)| state.buffer.file_path() == Some(path))
231 .map(|(id, _)| *id);
232
233 if let Some(id) = already_open {
234 return Ok(id);
235 }
236
237 let replace_current = {
240 let current_state = self.buffers.get(&self.active_buffer()).unwrap();
241 !current_state.is_composite_buffer
242 && current_state.buffer.is_empty()
243 && !current_state.buffer.is_modified()
244 && current_state.buffer.file_path().is_none()
245 };
246
247 let buffer_id = if replace_current {
248 self.active_buffer()
250 } else {
251 let id = BufferId(self.next_buffer_id);
253 self.next_buffer_id += 1;
254 id
255 };
256
257 tracing::info!(
259 "[SYNTAX DEBUG] open_file_no_focus: path={:?}, extension={:?}, registry_syntaxes={}, user_extensions={:?}",
260 path,
261 path.extension(),
262 self.grammar_registry.available_syntaxes().len(),
263 self.grammar_registry.user_extensions_debug()
264 );
265 let mut state = if file_exists {
266 let buffer = crate::model::buffer::Buffer::load_from_file(
269 &canonical_path,
270 self.config.editor.large_file_threshold_bytes as usize,
271 Arc::clone(&self.filesystem),
272 )?;
273 let detected =
274 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
275 &display_path,
276 &self.grammar_registry,
277 &self.config.languages,
278 self.config.fallback.as_ref(),
279 );
280 EditorState::from_buffer_with_language(buffer, detected)
281 } else {
282 EditorState::new_with_path(
284 self.config.editor.large_file_threshold_bytes as usize,
285 Arc::clone(&self.filesystem),
286 path.to_path_buf(),
287 )
288 };
289 let is_binary = state.buffer.is_binary();
293 if is_binary {
294 state.editing_disabled = true;
296 tracing::info!("Detected binary file: {}", path.display());
297 }
298
299 let mut whitespace =
303 crate::config::WhitespaceVisibility::from_editor_config(&self.config.editor);
304 state.buffer_settings.auto_close = self.config.editor.auto_close;
305 state.buffer_settings.auto_surround = self.config.editor.auto_surround;
306 if let Some(lang_config) = self.config.languages.get(&state.language) {
307 whitespace = whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
308 state.buffer_settings.use_tabs =
309 lang_config.use_tabs.unwrap_or(self.config.editor.use_tabs);
310 state.buffer_settings.tab_size =
312 lang_config.tab_size.unwrap_or(self.config.editor.tab_size);
313 if state.buffer_settings.auto_close {
315 if let Some(lang_auto_close) = lang_config.auto_close {
316 state.buffer_settings.auto_close = lang_auto_close;
317 }
318 }
319 if state.buffer_settings.auto_surround {
321 if let Some(lang_auto_surround) = lang_config.auto_surround {
322 state.buffer_settings.auto_surround = lang_auto_surround;
323 }
324 }
325 } else {
326 state.buffer_settings.tab_size = self.config.editor.tab_size;
327 state.buffer_settings.use_tabs = self.config.editor.use_tabs;
328 }
329 state.buffer_settings.whitespace = whitespace;
330
331 state
333 .margins
334 .configure_for_line_numbers(self.config.editor.line_numbers);
335
336 self.buffers.insert(buffer_id, state);
337 self.event_logs
338 .insert(buffer_id, crate::model::event::EventLog::new());
339
340 let mut metadata =
342 super::types::BufferMetadata::with_file(path.to_path_buf(), &self.working_dir);
343
344 if is_binary {
346 metadata.binary = true;
347 metadata.read_only = true;
348 metadata.disable_lsp(t!("buffer.binary_file").to_string());
349 }
350
351 if file_exists && !metadata.read_only && !self.filesystem.is_writable(path) {
353 metadata.read_only = true;
354 }
355
356 if metadata.read_only {
358 if let Some(state) = self.buffers.get_mut(&buffer_id) {
359 state.editing_disabled = true;
360 }
361 }
362
363 if !is_binary {
365 self.notify_lsp_file_opened(path, buffer_id, &mut metadata);
366 }
367
368 self.buffer_metadata.insert(buffer_id, metadata);
370
371 let target_split = self.preferred_split_for_file();
374 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
375 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
376 let page_view = self.resolve_page_view_for_buffer(buffer_id);
377 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
378 view_state.add_buffer(buffer_id);
379 let buf_state = view_state.ensure_buffer_state(buffer_id);
381 buf_state.apply_config_defaults(
382 self.config.editor.line_numbers,
383 self.config.editor.highlight_current_line,
384 line_wrap,
385 self.config.editor.wrap_indent,
386 wrap_column,
387 self.config.editor.rulers.clone(),
388 );
389 if let Some(page_width) = page_view {
391 buf_state.activate_page_view(page_width);
392 }
393 }
394
395 self.restore_global_file_state(buffer_id, path, target_split);
398
399 self.emit_event(
401 crate::model::control_event::events::FILE_OPENED.name,
402 serde_json::json!({
403 "path": path.display().to_string(),
404 "buffer_id": buffer_id.0
405 }),
406 );
407
408 self.watch_file(path);
410
411 self.plugin_manager.run_hook(
413 "after_file_open",
414 crate::services::plugins::hooks::HookArgs::AfterFileOpen {
415 buffer_id,
416 path: path.to_path_buf(),
417 },
418 );
419
420 Ok(buffer_id)
421 }
422
423 pub fn open_local_file(&mut self, path: &Path) -> anyhow::Result<BufferId> {
429 let resolved_path = if path.is_relative() {
431 self.working_dir.join(path)
432 } else {
433 path.to_path_buf()
434 };
435
436 let display_path = resolved_path.clone();
438
439 let canonical_path = resolved_path
441 .canonicalize()
442 .unwrap_or_else(|_| resolved_path.clone());
443 let path = canonical_path.as_path();
444
445 let already_open = self
447 .buffers
448 .iter()
449 .find(|(_, state)| state.buffer.file_path() == Some(path))
450 .map(|(id, _)| *id);
451
452 if let Some(id) = already_open {
453 self.set_active_buffer(id);
454 return Ok(id);
455 }
456
457 let buffer_id = BufferId(self.next_buffer_id);
459 self.next_buffer_id += 1;
460
461 let buffer = crate::model::buffer::Buffer::load_from_file(
464 &canonical_path,
465 self.config.editor.large_file_threshold_bytes as usize,
466 Arc::clone(&self.local_filesystem),
467 )?;
468 let detected =
469 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
470 &display_path,
471 &self.grammar_registry,
472 &self.config.languages,
473 self.config.fallback.as_ref(),
474 );
475 let state = EditorState::from_buffer_with_language(buffer, detected);
476
477 self.buffers.insert(buffer_id, state);
478 self.event_logs
479 .insert(buffer_id, crate::model::event::EventLog::new());
480
481 let metadata =
483 super::types::BufferMetadata::with_file(path.to_path_buf(), &self.working_dir);
484 self.buffer_metadata.insert(buffer_id, metadata);
485
486 let target_split = self.preferred_split_for_file();
488 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
489 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
490 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
491 view_state.add_buffer(buffer_id);
492 let buf_state = view_state.ensure_buffer_state(buffer_id);
493 buf_state.apply_config_defaults(
494 self.config.editor.line_numbers,
495 self.config.editor.highlight_current_line,
496 line_wrap,
497 self.config.editor.wrap_indent,
498 wrap_column,
499 self.config.editor.rulers.clone(),
500 );
501 }
502
503 self.set_active_buffer(buffer_id);
504
505 let display_name = path.display().to_string();
506 self.status_message = Some(t!("buffer.opened", name = display_name).to_string());
507
508 Ok(buffer_id)
509 }
510
511 pub fn open_file_with_encoding(
516 &mut self,
517 path: &Path,
518 encoding: crate::model::buffer::Encoding,
519 ) -> anyhow::Result<BufferId> {
520 let base_dir = self.working_dir.clone();
522
523 let resolved_path = if path.is_relative() {
524 base_dir.join(path)
525 } else {
526 path.to_path_buf()
527 };
528
529 let display_path = resolved_path.clone();
531
532 let canonical_path = self
534 .filesystem
535 .canonicalize(&resolved_path)
536 .unwrap_or_else(|_| resolved_path.clone());
537 let path = canonical_path.as_path();
538
539 let already_open = self
541 .buffers
542 .iter()
543 .find(|(_, state)| state.buffer.file_path() == Some(path))
544 .map(|(id, _)| *id);
545
546 if let Some(id) = already_open {
547 if let Some(state) = self.buffers.get_mut(&id) {
549 state.buffer.set_encoding(encoding);
550 }
551 self.set_active_buffer(id);
552 return Ok(id);
553 }
554
555 let buffer_id = BufferId(self.next_buffer_id);
557 self.next_buffer_id += 1;
558
559 let buffer = crate::model::buffer::Buffer::load_from_file_with_encoding(
561 path,
562 encoding,
563 Arc::clone(&self.filesystem),
564 crate::model::buffer::BufferConfig {
565 estimated_line_length: self.config.editor.estimated_line_length,
566 },
567 )?;
568 let detected =
571 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
572 &display_path,
573 &self.grammar_registry,
574 &self.config.languages,
575 self.config.fallback.as_ref(),
576 );
577
578 let mut state = EditorState::from_buffer_with_language(buffer, detected);
579
580 state
581 .margins
582 .configure_for_line_numbers(self.config.editor.line_numbers);
583
584 self.buffers.insert(buffer_id, state);
585 self.event_logs
586 .insert(buffer_id, crate::model::event::EventLog::new());
587
588 let metadata =
589 super::types::BufferMetadata::with_file(path.to_path_buf(), &self.working_dir);
590 self.buffer_metadata.insert(buffer_id, metadata);
591
592 let target_split = self.preferred_split_for_file();
594 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
595 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
596 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
597 view_state.add_buffer(buffer_id);
598 let buf_state = view_state.ensure_buffer_state(buffer_id);
599 buf_state.apply_config_defaults(
600 self.config.editor.line_numbers,
601 self.config.editor.highlight_current_line,
602 line_wrap,
603 self.config.editor.wrap_indent,
604 wrap_column,
605 self.config.editor.rulers.clone(),
606 );
607 }
608
609 self.set_active_buffer(buffer_id);
610
611 Ok(buffer_id)
612 }
613
614 pub fn reload_with_encoding(
618 &mut self,
619 encoding: crate::model::buffer::Encoding,
620 ) -> anyhow::Result<()> {
621 let buffer_id = self.active_buffer();
622
623 let path = self
625 .buffers
626 .get(&buffer_id)
627 .and_then(|s| s.buffer.file_path().map(|p| p.to_path_buf()))
628 .ok_or_else(|| anyhow::anyhow!("Buffer has no file path"))?;
629
630 if let Some(state) = self.buffers.get(&buffer_id) {
632 if state.buffer.is_modified() {
633 anyhow::bail!("Cannot reload: buffer has unsaved modifications");
634 }
635 }
636
637 let new_buffer = crate::model::buffer::Buffer::load_from_file_with_encoding(
639 &path,
640 encoding,
641 Arc::clone(&self.filesystem),
642 crate::model::buffer::BufferConfig {
643 estimated_line_length: self.config.editor.estimated_line_length,
644 },
645 )?;
646
647 if let Some(state) = self.buffers.get_mut(&buffer_id) {
649 state.buffer = new_buffer;
650 state.highlighter.invalidate_all();
652 }
653
654 let split_id = self.split_manager.active_split();
656 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
657 if let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) {
658 buf_state.cursors = crate::model::cursor::Cursors::new();
659 }
660 }
661
662 Ok(())
663 }
664
665 pub fn open_file_large_encoding_confirmed(&mut self, path: &Path) -> anyhow::Result<BufferId> {
670 let base_dir = self.working_dir.clone();
672
673 let resolved_path = if path.is_relative() {
674 base_dir.join(path)
675 } else {
676 path.to_path_buf()
677 };
678
679 let display_path = resolved_path.clone();
681
682 let canonical_path = self
684 .filesystem
685 .canonicalize(&resolved_path)
686 .unwrap_or_else(|_| resolved_path.clone());
687 let path = canonical_path.as_path();
688
689 let already_open = self
691 .buffers
692 .iter()
693 .find(|(_, state)| state.buffer.file_path() == Some(path))
694 .map(|(id, _)| *id);
695
696 if let Some(id) = already_open {
697 self.set_active_buffer(id);
698 return Ok(id);
699 }
700
701 let buffer_id = BufferId(self.next_buffer_id);
703 self.next_buffer_id += 1;
704
705 let buffer = crate::model::buffer::Buffer::load_large_file_confirmed(
707 path,
708 Arc::clone(&self.filesystem),
709 )?;
710 let detected =
713 crate::primitives::detected_language::DetectedLanguage::from_path_with_fallback(
714 &display_path,
715 &self.grammar_registry,
716 &self.config.languages,
717 self.config.fallback.as_ref(),
718 );
719
720 let mut state = EditorState::from_buffer_with_language(buffer, detected);
721
722 state
723 .margins
724 .configure_for_line_numbers(self.config.editor.line_numbers);
725
726 self.buffers.insert(buffer_id, state);
727 self.event_logs
728 .insert(buffer_id, crate::model::event::EventLog::new());
729
730 let metadata =
731 super::types::BufferMetadata::with_file(path.to_path_buf(), &self.working_dir);
732 self.buffer_metadata.insert(buffer_id, metadata);
733
734 let target_split = self.preferred_split_for_file();
736 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
737 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
738 if let Some(view_state) = self.split_view_states.get_mut(&target_split) {
739 view_state.add_buffer(buffer_id);
740 let buf_state = view_state.ensure_buffer_state(buffer_id);
741 buf_state.apply_config_defaults(
742 self.config.editor.line_numbers,
743 self.config.editor.highlight_current_line,
744 line_wrap,
745 self.config.editor.wrap_indent,
746 wrap_column,
747 self.config.editor.rulers.clone(),
748 );
749 }
750
751 self.set_active_buffer(buffer_id);
752
753 let display_name = self
755 .buffer_metadata
756 .get(&buffer_id)
757 .map(|m| m.display_name.clone())
758 .unwrap_or_else(|| path.display().to_string());
759
760 self.status_message = Some(t!("buffer.opened", name = display_name).to_string());
761
762 Ok(buffer_id)
763 }
764
765 fn restore_global_file_state(&mut self, buffer_id: BufferId, path: &Path, split_id: LeafId) {
770 use crate::workspace::PersistedFileWorkspace;
771
772 let file_state = match PersistedFileWorkspace::load(path) {
774 Some(state) => state,
775 None => return, };
777
778 let max_pos = match self.buffers.get(&buffer_id) {
780 Some(buffer) => buffer.buffer.len(),
781 None => return,
782 };
783
784 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
786 if let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) {
787 let cursor_pos = file_state.cursor.position.min(max_pos);
788 buf_state.cursors.primary_mut().position = cursor_pos;
789 buf_state.cursors.primary_mut().anchor =
790 file_state.cursor.anchor.map(|a| a.min(max_pos));
791 }
792 view_state.viewport.top_byte = file_state.scroll.top_byte;
793 view_state.viewport.left_column = file_state.scroll.left_column;
794 }
795 }
796
797 fn save_file_state_on_close(&self, buffer_id: BufferId) {
799 use crate::workspace::{
800 PersistedFileWorkspace, SerializedCursor, SerializedFileState, SerializedScroll,
801 };
802
803 let abs_path = match self.buffer_metadata.get(&buffer_id) {
805 Some(metadata) => match metadata.file_path() {
806 Some(path) => path.to_path_buf(),
807 None => return, },
809 None => return,
810 };
811
812 let view_state = self
814 .split_view_states
815 .values()
816 .find(|vs| vs.has_buffer(buffer_id));
817
818 let view_state = match view_state {
819 Some(vs) => vs,
820 None => return, };
822
823 let buf_state = match view_state.keyed_states.get(&buffer_id) {
825 Some(bs) => bs,
826 None => return,
827 };
828
829 let primary_cursor = buf_state.cursors.primary();
831 let file_state = SerializedFileState {
832 cursor: SerializedCursor {
833 position: primary_cursor.position,
834 anchor: primary_cursor.anchor,
835 sticky_column: primary_cursor.sticky_column,
836 },
837 additional_cursors: buf_state
838 .cursors
839 .iter()
840 .skip(1)
841 .map(|(_, cursor)| SerializedCursor {
842 position: cursor.position,
843 anchor: cursor.anchor,
844 sticky_column: cursor.sticky_column,
845 })
846 .collect(),
847 scroll: SerializedScroll {
848 top_byte: buf_state.viewport.top_byte,
849 top_view_line_offset: buf_state.viewport.top_view_line_offset,
850 left_column: buf_state.viewport.left_column,
851 },
852 view_mode: Default::default(),
853 compose_width: None,
854 plugin_state: std::collections::HashMap::new(),
855 folds: Vec::new(),
856 };
857
858 PersistedFileWorkspace::save(&abs_path, file_state);
860 tracing::debug!("Saved file state on close for {:?}", abs_path);
861 }
862
863 pub fn goto_line_col(&mut self, line: usize, column: Option<usize>) {
869 if line == 0 {
870 return; }
872
873 let buffer_id = self.active_buffer();
874
875 let cursors = self.active_cursors();
877 let cursor_id = cursors.primary_id();
878 let old_position = cursors.primary().position;
879 let old_anchor = cursors.primary().anchor;
880 let old_sticky_column = cursors.primary().sticky_column;
881
882 if let Some(state) = self.buffers.get(&buffer_id) {
883 let has_line_index = state.buffer.line_count().is_some();
884 let has_line_scan = state.buffer.has_line_feed_scan();
885 let buffer_len = state.buffer.len();
886
887 let target_line = line.saturating_sub(1);
889 let target_col = column.map(|c| c.saturating_sub(1)).unwrap_or(0);
891
892 let mut known_line: Option<usize> = None;
895
896 let position = if has_line_scan && has_line_index {
897 let max_line = state.buffer.line_count().unwrap_or(1).saturating_sub(1);
899 let actual_line = target_line.min(max_line);
900 known_line = Some(actual_line);
901 if let Some(state) = self.buffers.get_mut(&buffer_id) {
903 state
904 .buffer
905 .resolve_line_byte_offset(actual_line)
906 .map(|offset| (offset + target_col).min(buffer_len))
907 .unwrap_or(0)
908 } else {
909 0
910 }
911 } else {
912 let max_line = state.buffer.line_count().unwrap_or(1).saturating_sub(1);
915 let actual_line = target_line.min(max_line);
916 state.buffer.line_col_to_position(actual_line, target_col)
917 };
918
919 let event = Event::MoveCursor {
920 cursor_id,
921 old_position,
922 new_position: position,
923 old_anchor,
924 new_anchor: None,
925 old_sticky_column,
926 new_sticky_column: target_col,
927 };
928
929 let split_id = self.split_manager.active_split();
930 let state = self.buffers.get_mut(&buffer_id).unwrap();
931 let view_state = self.split_view_states.get_mut(&split_id).unwrap();
932 state.apply(&mut view_state.cursors, &event);
933
934 if let Some(line) = known_line {
937 state.primary_cursor_line_number = crate::model::buffer::LineNumber::Absolute(line);
938 }
939 }
940 }
941
942 pub fn select_range(
946 &mut self,
947 start_line: usize,
948 start_col: Option<usize>,
949 end_line: usize,
950 end_col: Option<usize>,
951 ) {
952 if start_line == 0 || end_line == 0 {
953 return;
954 }
955
956 let buffer_id = self.active_buffer();
957
958 let cursors = self.active_cursors();
959 let cursor_id = cursors.primary_id();
960 let old_position = cursors.primary().position;
961 let old_anchor = cursors.primary().anchor;
962 let old_sticky_column = cursors.primary().sticky_column;
963
964 if let Some(state) = self.buffers.get(&buffer_id) {
965 let buffer_len = state.buffer.len();
966
967 let start_line_0 = start_line.saturating_sub(1);
969 let start_col_0 = start_col.map(|c| c.saturating_sub(1)).unwrap_or(0);
970 let end_line_0 = end_line.saturating_sub(1);
971 let end_col_0 = end_col.map(|c| c.saturating_sub(1)).unwrap_or(0);
972
973 let max_line = state.buffer.line_count().unwrap_or(1).saturating_sub(1);
974
975 let start_pos = state
976 .buffer
977 .line_col_to_position(start_line_0.min(max_line), start_col_0)
978 .min(buffer_len);
979 let end_pos = state
980 .buffer
981 .line_col_to_position(end_line_0.min(max_line), end_col_0)
982 .min(buffer_len);
983
984 let event = Event::MoveCursor {
985 cursor_id,
986 old_position,
987 new_position: end_pos,
988 old_anchor,
989 new_anchor: Some(start_pos),
990 old_sticky_column,
991 new_sticky_column: end_col_0,
992 };
993
994 let split_id = self.split_manager.active_split();
995 let state = self.buffers.get_mut(&buffer_id).unwrap();
996 let view_state = self.split_view_states.get_mut(&split_id).unwrap();
997 state.apply(&mut view_state.cursors, &event);
998 }
999 }
1000
1001 pub fn goto_byte_offset(&mut self, offset: usize) {
1003 let buffer_id = self.active_buffer();
1004
1005 let cursors = self.active_cursors();
1006 let cursor_id = cursors.primary_id();
1007 let old_position = cursors.primary().position;
1008 let old_anchor = cursors.primary().anchor;
1009 let old_sticky_column = cursors.primary().sticky_column;
1010
1011 if let Some(state) = self.buffers.get(&buffer_id) {
1012 let buffer_len = state.buffer.len();
1013 let position = offset.min(buffer_len);
1014
1015 let event = Event::MoveCursor {
1016 cursor_id,
1017 old_position,
1018 new_position: position,
1019 old_anchor,
1020 new_anchor: None,
1021 old_sticky_column,
1022 new_sticky_column: 0,
1023 };
1024
1025 let split_id = self.split_manager.active_split();
1026 let state = self.buffers.get_mut(&buffer_id).unwrap();
1027 let view_state = self.split_view_states.get_mut(&split_id).unwrap();
1028 state.apply(&mut view_state.cursors, &event);
1029 }
1030 }
1031
1032 pub fn new_buffer(&mut self) -> BufferId {
1034 self.position_history.commit_pending_movement();
1036
1037 let cursors = self.active_cursors();
1039 let position = cursors.primary().position;
1040 let anchor = cursors.primary().anchor;
1041 self.position_history
1042 .record_movement(self.active_buffer(), position, anchor);
1043 self.position_history.commit_pending_movement();
1044
1045 let buffer_id = BufferId(self.next_buffer_id);
1046 self.next_buffer_id += 1;
1047
1048 let mut state = EditorState::new(
1049 self.terminal_width,
1050 self.terminal_height,
1051 self.config.editor.large_file_threshold_bytes as usize,
1052 Arc::clone(&self.filesystem),
1053 );
1054 state
1056 .margins
1057 .configure_for_line_numbers(self.config.editor.line_numbers);
1058 state
1060 .buffer
1061 .set_default_line_ending(self.config.editor.default_line_ending.to_line_ending());
1062 self.buffers.insert(buffer_id, state);
1063 self.event_logs
1064 .insert(buffer_id, crate::model::event::EventLog::new());
1065 self.buffer_metadata
1066 .insert(buffer_id, crate::app::types::BufferMetadata::new());
1067
1068 self.set_active_buffer(buffer_id);
1069
1070 let active_split = self.split_manager.active_split();
1074 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
1075 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
1076 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1077 view_state.apply_config_defaults(
1078 self.config.editor.line_numbers,
1079 self.config.editor.highlight_current_line,
1080 line_wrap,
1081 self.config.editor.wrap_indent,
1082 wrap_column,
1083 self.config.editor.rulers.clone(),
1084 );
1085 }
1086
1087 self.status_message = Some(t!("buffer.new").to_string());
1088
1089 buffer_id
1090 }
1091
1092 pub fn open_stdin_buffer(
1102 &mut self,
1103 temp_path: &Path,
1104 thread_handle: Option<std::thread::JoinHandle<anyhow::Result<()>>>,
1105 ) -> AnyhowResult<BufferId> {
1106 self.position_history.commit_pending_movement();
1108
1109 let cursors = self.active_cursors();
1111 let position = cursors.primary().position;
1112 let anchor = cursors.primary().anchor;
1113 self.position_history
1114 .record_movement(self.active_buffer(), position, anchor);
1115 self.position_history.commit_pending_movement();
1116
1117 let replace_current = {
1120 let current_state = self.buffers.get(&self.active_buffer()).unwrap();
1121 !current_state.is_composite_buffer
1122 && current_state.buffer.is_empty()
1123 && !current_state.buffer.is_modified()
1124 && current_state.buffer.file_path().is_none()
1125 };
1126
1127 let buffer_id = if replace_current {
1128 self.active_buffer()
1130 } else {
1131 let id = BufferId(self.next_buffer_id);
1133 self.next_buffer_id += 1;
1134 id
1135 };
1136
1137 let file_size = self.filesystem.metadata(temp_path)?.size as usize;
1139
1140 let mut state = EditorState::from_file_with_languages(
1143 temp_path,
1144 self.terminal_width,
1145 self.terminal_height,
1146 self.config.editor.large_file_threshold_bytes as usize,
1147 &self.grammar_registry,
1148 &self.config.languages,
1149 Arc::clone(&self.filesystem),
1150 )?;
1151
1152 state.buffer.clear_file_path();
1155 state.buffer.clear_modified();
1157
1158 state.buffer_settings.tab_size = self.config.editor.tab_size;
1160 state.buffer_settings.auto_close = self.config.editor.auto_close;
1161 state.buffer_settings.auto_surround = self.config.editor.auto_surround;
1162
1163 state
1165 .margins
1166 .configure_for_line_numbers(self.config.editor.line_numbers);
1167
1168 self.buffers.insert(buffer_id, state);
1169 self.event_logs
1170 .insert(buffer_id, crate::model::event::EventLog::new());
1171
1172 let metadata =
1174 super::types::BufferMetadata::new_unnamed(t!("stdin.display_name").to_string());
1175 self.buffer_metadata.insert(buffer_id, metadata);
1176
1177 let active_split = self.split_manager.active_split();
1179 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
1180 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
1181 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1182 view_state.add_buffer(buffer_id);
1183 let buf_state = view_state.ensure_buffer_state(buffer_id);
1184 buf_state.apply_config_defaults(
1185 self.config.editor.line_numbers,
1186 self.config.editor.highlight_current_line,
1187 line_wrap,
1188 self.config.editor.wrap_indent,
1189 wrap_column,
1190 self.config.editor.rulers.clone(),
1191 );
1192 }
1193
1194 self.set_active_buffer(buffer_id);
1195
1196 let complete = thread_handle.is_none();
1199 self.stdin_streaming = Some(super::StdinStreamingState {
1200 temp_path: temp_path.to_path_buf(),
1201 buffer_id,
1202 last_known_size: file_size,
1203 complete,
1204 thread_handle,
1205 });
1206
1207 self.status_message = Some(t!("stdin.streaming").to_string());
1209
1210 Ok(buffer_id)
1211 }
1212
1213 pub fn poll_stdin_streaming(&mut self) -> bool {
1216 let Some(ref mut stream_state) = self.stdin_streaming else {
1217 return false;
1218 };
1219
1220 if stream_state.complete {
1221 return false;
1222 }
1223
1224 let mut changed = false;
1225
1226 let current_size = self
1228 .filesystem
1229 .metadata(&stream_state.temp_path)
1230 .map(|m| m.size as usize)
1231 .unwrap_or(stream_state.last_known_size);
1232
1233 if current_size > stream_state.last_known_size {
1235 if let Some(editor_state) = self.buffers.get_mut(&stream_state.buffer_id) {
1236 editor_state
1237 .buffer
1238 .extend_streaming(&stream_state.temp_path, current_size);
1239 }
1240 stream_state.last_known_size = current_size;
1241
1242 self.status_message =
1244 Some(t!("stdin.streaming_bytes", bytes = current_size).to_string());
1245 changed = true;
1246 }
1247
1248 let thread_finished = stream_state
1250 .thread_handle
1251 .as_ref()
1252 .map(|h| h.is_finished())
1253 .unwrap_or(true);
1254
1255 if thread_finished {
1256 if let Some(handle) = stream_state.thread_handle.take() {
1258 match handle.join() {
1259 Ok(Ok(())) => {
1260 tracing::info!("Stdin streaming completed successfully");
1261 }
1262 Ok(Err(e)) => {
1263 tracing::warn!("Stdin streaming error: {}", e);
1264 self.status_message =
1265 Some(t!("stdin.read_error", error = e.to_string()).to_string());
1266 }
1267 Err(_) => {
1268 tracing::warn!("Stdin streaming thread panicked");
1269 self.status_message = Some(t!("stdin.read_error_panic").to_string());
1270 }
1271 }
1272 }
1273 self.complete_stdin_streaming();
1274 changed = true;
1275 }
1276
1277 changed
1278 }
1279
1280 pub fn complete_stdin_streaming(&mut self) {
1283 if let Some(ref mut stream_state) = self.stdin_streaming {
1284 stream_state.complete = true;
1285
1286 let final_size = self
1288 .filesystem
1289 .metadata(&stream_state.temp_path)
1290 .map(|m| m.size as usize)
1291 .unwrap_or(stream_state.last_known_size);
1292
1293 if final_size > stream_state.last_known_size {
1294 if let Some(editor_state) = self.buffers.get_mut(&stream_state.buffer_id) {
1295 editor_state
1296 .buffer
1297 .extend_streaming(&stream_state.temp_path, final_size);
1298 }
1299 stream_state.last_known_size = final_size;
1300 }
1301
1302 self.status_message =
1303 Some(t!("stdin.read_complete", bytes = stream_state.last_known_size).to_string());
1304 }
1305 }
1306
1307 pub fn is_stdin_streaming(&self) -> bool {
1309 self.stdin_streaming
1310 .as_ref()
1311 .map(|s| !s.complete)
1312 .unwrap_or(false)
1313 }
1314
1315 pub fn create_virtual_buffer(
1325 &mut self,
1326 name: String,
1327 mode: String,
1328 read_only: bool,
1329 ) -> BufferId {
1330 let buffer_id = BufferId(self.next_buffer_id);
1331 self.next_buffer_id += 1;
1332
1333 let mut state = EditorState::new(
1334 self.terminal_width,
1335 self.terminal_height,
1336 self.config.editor.large_file_threshold_bytes as usize,
1337 Arc::clone(&self.filesystem),
1338 );
1339 state.set_language_from_name(&name, &self.grammar_registry);
1343
1344 state
1346 .margins
1347 .configure_for_line_numbers(self.config.editor.line_numbers);
1348
1349 self.buffers.insert(buffer_id, state);
1350 self.event_logs
1351 .insert(buffer_id, crate::model::event::EventLog::new());
1352
1353 let metadata = super::types::BufferMetadata::virtual_buffer(name, mode, read_only);
1355 self.buffer_metadata.insert(buffer_id, metadata);
1356
1357 let active_split = self.split_manager.active_split();
1359 let line_wrap = self.resolve_line_wrap_for_buffer(buffer_id);
1360 let wrap_column = self.resolve_wrap_column_for_buffer(buffer_id);
1361 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1362 view_state.add_buffer(buffer_id);
1363 let buf_state = view_state.ensure_buffer_state(buffer_id);
1364 buf_state.apply_config_defaults(
1365 self.config.editor.line_numbers,
1366 self.config.editor.highlight_current_line,
1367 line_wrap,
1368 self.config.editor.wrap_indent,
1369 wrap_column,
1370 self.config.editor.rulers.clone(),
1371 );
1372 } else {
1373 let mut view_state =
1375 SplitViewState::with_buffer(self.terminal_width, self.terminal_height, buffer_id);
1376 view_state.apply_config_defaults(
1377 self.config.editor.line_numbers,
1378 self.config.editor.highlight_current_line,
1379 line_wrap,
1380 self.config.editor.wrap_indent,
1381 wrap_column,
1382 self.config.editor.rulers.clone(),
1383 );
1384 self.split_view_states.insert(active_split, view_state);
1385 }
1386
1387 buffer_id
1388 }
1389
1390 pub fn set_virtual_buffer_content(
1396 &mut self,
1397 buffer_id: BufferId,
1398 entries: Vec<crate::primitives::text_property::TextPropertyEntry>,
1399 ) -> Result<(), String> {
1400 let old_cursor_pos = self
1402 .split_view_states
1403 .values()
1404 .find(|vs| vs.has_buffer(buffer_id))
1405 .and_then(|vs| vs.keyed_states.get(&buffer_id))
1406 .map(|bs| bs.cursors.primary().position)
1407 .unwrap_or(0);
1408
1409 let state = self
1410 .buffers
1411 .get_mut(&buffer_id)
1412 .ok_or_else(|| "Buffer not found".to_string())?;
1413
1414 let (text, properties, collected_overlays) =
1416 crate::primitives::text_property::TextPropertyManager::from_entries(entries);
1417
1418 state.overlays.clear(&mut state.marker_list);
1423
1424 let current_len = state.buffer.len();
1425 if current_len > 0 {
1426 state.buffer.delete_bytes(0, current_len);
1427 }
1428 state.buffer.insert(0, &text);
1429
1430 state.buffer.clear_modified();
1432
1433 state.text_properties = properties;
1435
1436 {
1438 use crate::view::overlay::{Overlay, OverlayFace};
1439 use fresh_core::overlay::OverlayNamespace;
1440
1441 let inline_ns = OverlayNamespace::from_string("_inline".to_string());
1442
1443 for co in collected_overlays {
1444 let face = OverlayFace::from_options(&co.options);
1445 let mut overlay = Overlay::with_namespace(
1446 &mut state.marker_list,
1447 co.range,
1448 face,
1449 inline_ns.clone(),
1450 );
1451 overlay.extend_to_line_end = co.options.extend_to_line_end;
1452 if let Some(url) = co.options.url {
1453 overlay.url = Some(url);
1454 }
1455 state.overlays.add(overlay);
1456 }
1457 }
1458
1459 let new_len = state.buffer.len();
1461 let clamped_pos = old_cursor_pos.min(new_len);
1462 let new_cursor_pos = state.buffer.snap_to_char_boundary(clamped_pos);
1464
1465 for view_state in self.split_view_states.values_mut() {
1467 if let Some(buf_state) = view_state.keyed_states.get_mut(&buffer_id) {
1468 buf_state.cursors.primary_mut().position = new_cursor_pos;
1469 buf_state.cursors.primary_mut().anchor = None;
1470 }
1471 }
1472
1473 Ok(())
1474 }
1475
1476 pub fn open_help_manual(&mut self) {
1480 let existing_buffer = self
1482 .buffer_metadata
1483 .iter()
1484 .find(|(_, m)| m.display_name == help::HELP_MANUAL_BUFFER_NAME)
1485 .map(|(id, _)| *id);
1486
1487 if let Some(buffer_id) = existing_buffer {
1488 self.set_active_buffer(buffer_id);
1490 return;
1491 }
1492
1493 let buffer_id = self.create_virtual_buffer(
1495 help::HELP_MANUAL_BUFFER_NAME.to_string(),
1496 "special".to_string(),
1497 true,
1498 );
1499
1500 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1502 state.buffer.insert(0, help::HELP_MANUAL_CONTENT);
1503 state.buffer.clear_modified();
1504 state.editing_disabled = true;
1505
1506 state.margins.configure_for_line_numbers(false);
1508 }
1509
1510 self.set_active_buffer(buffer_id);
1511 }
1512
1513 pub fn open_keyboard_shortcuts(&mut self) {
1518 let existing_buffer = self
1520 .buffer_metadata
1521 .iter()
1522 .find(|(_, m)| m.display_name == help::KEYBOARD_SHORTCUTS_BUFFER_NAME)
1523 .map(|(id, _)| *id);
1524
1525 if let Some(buffer_id) = existing_buffer {
1526 self.set_active_buffer(buffer_id);
1528 return;
1529 }
1530
1531 let bindings = self.keybindings.get_all_bindings();
1533
1534 let mut content = String::from("Keyboard Shortcuts\n");
1536 content.push_str("==================\n\n");
1537 content.push_str("Press 'q' to close this buffer.\n\n");
1538
1539 let mut current_context = String::new();
1541 for (key, action) in &bindings {
1542 let (context, action_name) = if let Some(bracket_end) = action.find("] ") {
1544 let ctx = &action[1..bracket_end];
1545 let name = &action[bracket_end + 2..];
1546 (ctx.to_string(), name.to_string())
1547 } else {
1548 ("Normal".to_string(), action.clone())
1549 };
1550
1551 if context != current_context {
1553 if !current_context.is_empty() {
1554 content.push('\n');
1555 }
1556 content.push_str(&format!("── {} Mode ──\n\n", context));
1557 current_context = context;
1558 }
1559
1560 content.push_str(&format!(" {:20} {}\n", key, action_name));
1562 }
1563
1564 let buffer_id = self.create_virtual_buffer(
1566 help::KEYBOARD_SHORTCUTS_BUFFER_NAME.to_string(),
1567 "special".to_string(),
1568 true,
1569 );
1570
1571 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1573 state.buffer.insert(0, &content);
1574 state.buffer.clear_modified();
1575 state.editing_disabled = true;
1576
1577 state.margins.configure_for_line_numbers(false);
1579 }
1580
1581 self.set_active_buffer(buffer_id);
1582 }
1583
1584 pub fn show_warnings_popup(&mut self) {
1589 if !self.warning_domains.has_any_warnings() {
1590 self.status_message = Some(t!("warnings.none").to_string());
1591 return;
1592 }
1593
1594 self.open_warning_log();
1596 }
1597
1598 pub fn show_lsp_status_popup(&mut self) {
1601 let has_error = self.warning_domains.lsp.level() == crate::app::WarningLevel::Error;
1602
1603 let language = self
1606 .warning_domains
1607 .lsp
1608 .language
1609 .clone()
1610 .unwrap_or_else(|| {
1611 self.buffers
1613 .get(&self.active_buffer())
1614 .map(|s| s.language.clone())
1615 .unwrap_or_else(|| "unknown".to_string())
1616 });
1617
1618 tracing::info!(
1619 "show_lsp_status_popup: language={}, has_error={}, has_warnings={}",
1620 language,
1621 has_error,
1622 self.warning_domains.lsp.has_warnings()
1623 );
1624
1625 self.plugin_manager.run_hook(
1627 "lsp_status_clicked",
1628 crate::services::plugins::hooks::HookArgs::LspStatusClicked {
1629 language: language.clone(),
1630 has_error,
1631 },
1632 );
1633 tracing::info!("show_lsp_status_popup: hook fired");
1634
1635 if !self.warning_domains.lsp.has_warnings() {
1636 if self.lsp_status.is_empty() {
1637 self.status_message = Some(t!("lsp.no_server_active").to_string());
1638 } else {
1639 self.status_message = Some(t!("lsp.status", status = &self.lsp_status).to_string());
1640 }
1641 return;
1642 }
1643
1644 if has_error && self.plugin_manager.has_hook_handlers("lsp_status_clicked") {
1648 tracing::info!(
1649 "show_lsp_status_popup: has_error=true and plugin registered, skipping warning log"
1650 );
1651 return;
1652 }
1653
1654 self.open_warning_log();
1656 }
1657
1658 pub fn show_file_message_popup(&mut self, message: &str) {
1661 use crate::view::popup::{Popup, PopupPosition};
1662 use ratatui::style::Style;
1663
1664 let md = format!("{}\n\n*esc to dismiss*", message);
1666 let content_width = message.lines().map(|l| l.len()).max().unwrap_or(0) as u16;
1668 let hint_width = 16u16; let popup_width = (content_width.max(hint_width) + 4).clamp(20, 60);
1670
1671 let mut popup = Popup::markdown(&md, &self.theme, Some(&self.grammar_registry));
1672 popup.transient = false;
1673 popup.position = PopupPosition::BelowCursor;
1674 popup.width = popup_width;
1675 popup.max_height = 15;
1676 popup.border_style = Style::default().fg(self.theme.popup_border_fg);
1677 popup.background_style = Style::default().bg(self.theme.popup_bg);
1678
1679 let buffer_id = self.active_buffer();
1680 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1681 state.popups.show(popup);
1682 }
1683 }
1684
1685 pub fn get_text_properties_at_cursor(
1687 &self,
1688 ) -> Option<Vec<&crate::primitives::text_property::TextProperty>> {
1689 let state = self.buffers.get(&self.active_buffer())?;
1690 let cursor_pos = self.active_cursors().primary().position;
1691 Some(state.text_properties.get_at(cursor_pos))
1692 }
1693
1694 pub fn close_buffer(&mut self, id: BufferId) -> anyhow::Result<()> {
1696 if let Some(state) = self.buffers.get(&id) {
1698 if state.buffer.is_modified() {
1699 return Err(anyhow::anyhow!("Buffer has unsaved changes"));
1700 }
1701 }
1702 self.close_buffer_internal(id)
1703 }
1704
1705 pub fn force_close_buffer(&mut self, id: BufferId) -> anyhow::Result<()> {
1708 self.close_buffer_internal(id)
1709 }
1710
1711 fn close_buffer_internal(&mut self, id: BufferId) -> anyhow::Result<()> {
1713 if let Some((wait_id, _)) = self.wait_tracking.remove(&id) {
1715 self.completed_waits.push(wait_id);
1716 }
1717
1718 self.save_file_state_on_close(id);
1720
1721 if let Err(e) = self.delete_buffer_recovery(id) {
1723 tracing::debug!("Failed to delete buffer recovery on close: {}", e);
1724 }
1725
1726 if let Some(terminal_id) = self.terminal_buffers.remove(&id) {
1728 self.terminal_manager.close(terminal_id);
1730
1731 let backing_file = self.terminal_backing_files.remove(&terminal_id);
1733 if let Some(ref path) = backing_file {
1734 #[allow(clippy::let_underscore_must_use)]
1736 let _ = self.filesystem.remove_file(path);
1737 }
1738 if let Some(log_file) = self.terminal_log_files.remove(&terminal_id) {
1740 if backing_file.as_ref() != Some(&log_file) {
1741 #[allow(clippy::let_underscore_must_use)]
1743 let _ = self.filesystem.remove_file(&log_file);
1744 }
1745 }
1746
1747 self.terminal_mode_resume.remove(&id);
1749
1750 if self.terminal_mode {
1752 self.terminal_mode = false;
1753 self.key_context = crate::input::keybindings::KeyContext::Normal;
1754 }
1755 }
1756
1757 let active_split = self.split_manager.active_split();
1760 let replacement_from_history = self.split_view_states.get(&active_split).and_then(|vs| {
1761 vs.focus_history
1763 .iter()
1764 .rev()
1765 .find(|&&bid| {
1766 bid != id
1767 && self.buffers.contains_key(&bid)
1768 && !self
1769 .buffer_metadata
1770 .get(&bid)
1771 .map(|m| m.hidden_from_tabs)
1772 .unwrap_or(false)
1773 })
1774 .copied()
1775 });
1776
1777 let visible_replacement = replacement_from_history.or_else(|| {
1779 self.buffers
1780 .keys()
1781 .find(|&&bid| {
1782 bid != id
1783 && !self
1784 .buffer_metadata
1785 .get(&bid)
1786 .map(|m| m.hidden_from_tabs)
1787 .unwrap_or(false)
1788 })
1789 .copied()
1790 });
1791
1792 let is_last_visible_buffer = visible_replacement.is_none();
1793 let replacement_buffer = if is_last_visible_buffer {
1794 self.new_buffer()
1795 } else {
1796 visible_replacement.unwrap()
1797 };
1798
1799 if self.active_buffer() == id {
1804 self.set_active_buffer(replacement_buffer);
1805 }
1806
1807 let splits_to_update = self.split_manager.splits_for_buffer(id);
1809 for split_id in splits_to_update {
1810 self.split_manager
1811 .set_split_buffer(split_id, replacement_buffer);
1812 }
1813
1814 self.buffers.remove(&id);
1815 self.event_logs.remove(&id);
1816 self.seen_byte_ranges.remove(&id);
1817 self.buffer_metadata.remove(&id);
1818 if let Some((request_id, _, _)) = self.semantic_tokens_in_flight.remove(&id) {
1819 self.pending_semantic_token_requests.remove(&request_id);
1820 }
1821 if let Some((request_id, _, _, _)) = self.semantic_tokens_range_in_flight.remove(&id) {
1822 self.pending_semantic_token_range_requests
1823 .remove(&request_id);
1824 }
1825 self.semantic_tokens_range_last_request.remove(&id);
1826 self.semantic_tokens_range_applied.remove(&id);
1827 self.semantic_tokens_full_debounce.remove(&id);
1828
1829 self.panel_ids.retain(|_, &mut buf_id| buf_id != id);
1832
1833 for view_state in self.split_view_states.values_mut() {
1835 view_state.remove_buffer(id);
1836 view_state.remove_from_history(id);
1837 }
1838
1839 if is_last_visible_buffer {
1841 self.focus_file_explorer();
1842 }
1843
1844 Ok(())
1845 }
1846
1847 pub fn switch_buffer(&mut self, id: BufferId) {
1849 if self.buffers.contains_key(&id) && id != self.active_buffer() {
1850 self.position_history.commit_pending_movement();
1852
1853 let cursors = self.active_cursors();
1855 let position = cursors.primary().position;
1856 let anchor = cursors.primary().anchor;
1857 self.position_history
1858 .record_movement(self.active_buffer(), position, anchor);
1859 self.position_history.commit_pending_movement();
1860
1861 self.set_active_buffer(id);
1862 }
1863 }
1864
1865 pub fn close_tab(&mut self) {
1869 let buffer_id = self.active_buffer();
1870 let active_split = self.split_manager.active_split();
1871
1872 let buffer_in_other_splits = self
1874 .split_view_states
1875 .iter()
1876 .filter(|(&split_id, view_state)| {
1877 split_id != active_split && view_state.has_buffer(buffer_id)
1878 })
1879 .count();
1880
1881 let current_split_tabs = self
1883 .split_view_states
1884 .get(&active_split)
1885 .map(|vs| vs.open_buffers.clone())
1886 .unwrap_or_default();
1887
1888 let is_last_viewport = buffer_in_other_splits == 0;
1891
1892 if is_last_viewport {
1893 let has_other_splits = self.split_manager.root().count_leaves() > 1;
1896 if current_split_tabs.len() <= 1 && has_other_splits {
1897 if self.active_state().buffer.is_modified() {
1899 let name = self.get_buffer_display_name(buffer_id);
1900 let save_key = t!("prompt.key.save").to_string();
1901 let discard_key = t!("prompt.key.discard").to_string();
1902 let cancel_key = t!("prompt.key.cancel").to_string();
1903 self.start_prompt(
1904 t!(
1905 "prompt.buffer_modified",
1906 name = name,
1907 save_key = save_key,
1908 discard_key = discard_key,
1909 cancel_key = cancel_key
1910 )
1911 .to_string(),
1912 PromptType::ConfirmCloseBuffer { buffer_id },
1913 );
1914 return;
1915 }
1916 if let Err(e) = self.close_buffer(buffer_id) {
1918 tracing::warn!("Failed to close buffer: {}", e);
1919 }
1920 self.close_active_split();
1921 return;
1922 }
1923
1924 if self.active_state().buffer.is_modified() {
1926 let name = self.get_buffer_display_name(buffer_id);
1928 let save_key = t!("prompt.key.save").to_string();
1929 let discard_key = t!("prompt.key.discard").to_string();
1930 let cancel_key = t!("prompt.key.cancel").to_string();
1931 self.start_prompt(
1932 t!(
1933 "prompt.buffer_modified",
1934 name = name,
1935 save_key = save_key,
1936 discard_key = discard_key,
1937 cancel_key = cancel_key
1938 )
1939 .to_string(),
1940 PromptType::ConfirmCloseBuffer { buffer_id },
1941 );
1942 } else if let Err(e) = self.close_buffer(buffer_id) {
1943 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
1944 } else {
1945 self.set_status_message(t!("buffer.tab_closed").to_string());
1946 }
1947 } else {
1948 if current_split_tabs.len() <= 1 {
1950 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
1953 self.terminal_mode = false;
1954 self.key_context = crate::input::keybindings::KeyContext::Normal;
1955 }
1956 self.close_active_split();
1957 return;
1958 }
1959
1960 let current_idx = current_split_tabs
1962 .iter()
1963 .position(|&id| id == buffer_id)
1964 .unwrap_or(0);
1965 let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
1966 let replacement_buffer = current_split_tabs[replacement_idx];
1967
1968 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
1970 self.terminal_mode = false;
1971 self.key_context = crate::input::keybindings::KeyContext::Normal;
1972 }
1973
1974 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1976 view_state.remove_buffer(buffer_id);
1977 }
1978
1979 self.split_manager
1981 .set_split_buffer(active_split, replacement_buffer);
1982
1983 self.set_status_message(t!("buffer.tab_closed").to_string());
1984 }
1985 }
1986
1987 pub fn close_tab_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) -> bool {
1991 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
1993 self.terminal_mode = false;
1994 self.key_context = crate::input::keybindings::KeyContext::Normal;
1995 }
1996
1997 let buffer_in_other_splits = self
1999 .split_view_states
2000 .iter()
2001 .filter(|(&sid, view_state)| sid != split_id && view_state.has_buffer(buffer_id))
2002 .count();
2003
2004 let split_tabs = self
2006 .split_view_states
2007 .get(&split_id)
2008 .map(|vs| vs.open_buffers.clone())
2009 .unwrap_or_default();
2010
2011 let is_last_viewport = buffer_in_other_splits == 0;
2012
2013 if is_last_viewport {
2014 if let Some(state) = self.buffers.get(&buffer_id) {
2016 if state.buffer.is_modified() {
2017 let name = self.get_buffer_display_name(buffer_id);
2019 let save_key = t!("prompt.key.save").to_string();
2020 let discard_key = t!("prompt.key.discard").to_string();
2021 let cancel_key = t!("prompt.key.cancel").to_string();
2022 self.start_prompt(
2023 t!(
2024 "prompt.buffer_modified",
2025 name = name,
2026 save_key = save_key,
2027 discard_key = discard_key,
2028 cancel_key = cancel_key
2029 )
2030 .to_string(),
2031 PromptType::ConfirmCloseBuffer { buffer_id },
2032 );
2033 return false;
2034 }
2035 }
2036 if let Err(e) = self.close_buffer(buffer_id) {
2037 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
2038 } else {
2039 self.set_status_message(t!("buffer.tab_closed").to_string());
2040 }
2041 } else {
2042 if split_tabs.len() <= 1 {
2044 self.handle_close_split(split_id.into());
2046 return true;
2047 }
2048
2049 let current_idx = split_tabs
2051 .iter()
2052 .position(|&id| id == buffer_id)
2053 .unwrap_or(0);
2054 let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
2055 let replacement_buffer = split_tabs[replacement_idx];
2056
2057 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2059 view_state.remove_buffer(buffer_id);
2060 }
2061
2062 self.split_manager
2064 .set_split_buffer(split_id, replacement_buffer);
2065
2066 self.set_status_message(t!("buffer.tab_closed").to_string());
2067 }
2068 true
2069 }
2070
2071 pub fn close_other_tabs_in_split(&mut self, keep_buffer_id: BufferId, split_id: LeafId) {
2073 let split_tabs = self
2075 .split_view_states
2076 .get(&split_id)
2077 .map(|vs| vs.open_buffers.clone())
2078 .unwrap_or_default();
2079
2080 let tabs_to_close: Vec<_> = split_tabs
2082 .iter()
2083 .filter(|&&id| id != keep_buffer_id)
2084 .copied()
2085 .collect();
2086
2087 let mut closed = 0;
2088 let mut skipped_modified = 0;
2089 for buffer_id in tabs_to_close {
2090 if self.close_tab_in_split_silent(buffer_id, split_id) {
2091 closed += 1;
2092 } else {
2093 skipped_modified += 1;
2094 }
2095 }
2096
2097 self.split_manager
2099 .set_split_buffer(split_id, keep_buffer_id);
2100
2101 self.set_batch_close_status_message(closed, skipped_modified);
2102 }
2103
2104 pub fn close_tabs_to_right_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) {
2106 let split_tabs = self
2108 .split_view_states
2109 .get(&split_id)
2110 .map(|vs| vs.open_buffers.clone())
2111 .unwrap_or_default();
2112
2113 let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
2115 return;
2116 };
2117
2118 let tabs_to_close: Vec<_> = split_tabs.iter().skip(target_idx + 1).copied().collect();
2120
2121 let mut closed = 0;
2122 let mut skipped_modified = 0;
2123 for buf_id in tabs_to_close {
2124 if self.close_tab_in_split_silent(buf_id, split_id) {
2125 closed += 1;
2126 } else {
2127 skipped_modified += 1;
2128 }
2129 }
2130
2131 self.set_batch_close_status_message(closed, skipped_modified);
2132 }
2133
2134 pub fn close_tabs_to_left_in_split(&mut self, buffer_id: BufferId, split_id: LeafId) {
2136 let split_tabs = self
2138 .split_view_states
2139 .get(&split_id)
2140 .map(|vs| vs.open_buffers.clone())
2141 .unwrap_or_default();
2142
2143 let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
2145 return;
2146 };
2147
2148 let tabs_to_close: Vec<_> = split_tabs.iter().take(target_idx).copied().collect();
2150
2151 let mut closed = 0;
2152 let mut skipped_modified = 0;
2153 for buf_id in tabs_to_close {
2154 if self.close_tab_in_split_silent(buf_id, split_id) {
2155 closed += 1;
2156 } else {
2157 skipped_modified += 1;
2158 }
2159 }
2160
2161 self.set_batch_close_status_message(closed, skipped_modified);
2162 }
2163
2164 pub fn close_all_tabs_in_split(&mut self, split_id: LeafId) {
2166 let split_tabs = self
2168 .split_view_states
2169 .get(&split_id)
2170 .map(|vs| vs.open_buffers.clone())
2171 .unwrap_or_default();
2172
2173 let mut closed = 0;
2174 let mut skipped_modified = 0;
2175
2176 for buffer_id in split_tabs {
2178 if self.close_tab_in_split_silent(buffer_id, split_id) {
2179 closed += 1;
2180 } else {
2181 skipped_modified += 1;
2182 }
2183 }
2184
2185 self.set_batch_close_status_message(closed, skipped_modified);
2186 }
2187
2188 fn set_batch_close_status_message(&mut self, closed: usize, skipped_modified: usize) {
2190 let message = match (closed, skipped_modified) {
2191 (0, 0) => t!("buffer.no_tabs_to_close").to_string(),
2192 (0, n) => t!("buffer.skipped_modified", count = n).to_string(),
2193 (n, 0) => t!("buffer.closed_tabs", count = n).to_string(),
2194 (c, s) => t!("buffer.closed_tabs_skipped", closed = c, skipped = s).to_string(),
2195 };
2196 self.set_status_message(message);
2197 }
2198
2199 fn close_tab_in_split_silent(&mut self, buffer_id: BufferId, split_id: LeafId) -> bool {
2203 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
2205 self.terminal_mode = false;
2206 self.key_context = crate::input::keybindings::KeyContext::Normal;
2207 }
2208
2209 let buffer_in_other_splits = self
2211 .split_view_states
2212 .iter()
2213 .filter(|(&sid, view_state)| sid != split_id && view_state.has_buffer(buffer_id))
2214 .count();
2215
2216 let split_tabs = self
2218 .split_view_states
2219 .get(&split_id)
2220 .map(|vs| vs.open_buffers.clone())
2221 .unwrap_or_default();
2222
2223 let is_last_viewport = buffer_in_other_splits == 0;
2224
2225 if is_last_viewport {
2226 if let Some(state) = self.buffers.get(&buffer_id) {
2229 if state.buffer.is_modified() {
2230 return false;
2232 }
2233 }
2234 if let Err(e) = self.close_buffer(buffer_id) {
2235 tracing::warn!("Failed to close buffer: {}", e);
2236 }
2237 true
2238 } else {
2239 if split_tabs.len() <= 1 {
2241 self.handle_close_split(split_id.into());
2243 return true;
2244 }
2245
2246 let current_idx = split_tabs
2248 .iter()
2249 .position(|&id| id == buffer_id)
2250 .unwrap_or(0);
2251 let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
2252 let replacement_buffer = split_tabs.get(replacement_idx).copied();
2253
2254 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
2256 view_state.remove_buffer(buffer_id);
2257 }
2258
2259 if let Some(replacement) = replacement_buffer {
2261 self.split_manager.set_split_buffer(split_id, replacement);
2262 }
2263 true
2264 }
2265 }
2266
2267 fn visible_buffers_for_active_split(&self) -> Vec<BufferId> {
2270 let active_split = self.split_manager.active_split();
2271 if let Some(view_state) = self.split_view_states.get(&active_split) {
2272 view_state
2273 .open_buffers
2274 .iter()
2275 .copied()
2276 .filter(|id| {
2277 !self
2278 .buffer_metadata
2279 .get(id)
2280 .map(|m| m.hidden_from_tabs)
2281 .unwrap_or(false)
2282 })
2283 .collect()
2284 } else {
2285 let mut all_ids: Vec<_> = self
2287 .buffers
2288 .keys()
2289 .copied()
2290 .filter(|id| {
2291 !self
2292 .buffer_metadata
2293 .get(id)
2294 .map(|m| m.hidden_from_tabs)
2295 .unwrap_or(false)
2296 })
2297 .collect();
2298 all_ids.sort_by_key(|id| id.0);
2299 all_ids
2300 }
2301 }
2302
2303 pub fn next_buffer(&mut self) {
2305 let ids = self.visible_buffers_for_active_split();
2306
2307 if ids.is_empty() {
2308 return;
2309 }
2310
2311 if let Some(idx) = ids.iter().position(|&id| id == self.active_buffer()) {
2312 let next_idx = (idx + 1) % ids.len();
2313 if ids[next_idx] != self.active_buffer() {
2314 self.position_history.commit_pending_movement();
2316
2317 let cursors = self.active_cursors();
2319 let position = cursors.primary().position;
2320 let anchor = cursors.primary().anchor;
2321 self.position_history
2322 .record_movement(self.active_buffer(), position, anchor);
2323 self.position_history.commit_pending_movement();
2324
2325 self.set_active_buffer(ids[next_idx]);
2326 }
2327 }
2328 }
2329
2330 pub fn prev_buffer(&mut self) {
2332 let ids = self.visible_buffers_for_active_split();
2333
2334 if ids.is_empty() {
2335 return;
2336 }
2337
2338 if let Some(idx) = ids.iter().position(|&id| id == self.active_buffer()) {
2339 let prev_idx = if idx == 0 { ids.len() - 1 } else { idx - 1 };
2340 if ids[prev_idx] != self.active_buffer() {
2341 self.position_history.commit_pending_movement();
2343
2344 let cursors = self.active_cursors();
2346 let position = cursors.primary().position;
2347 let anchor = cursors.primary().anchor;
2348 self.position_history
2349 .record_movement(self.active_buffer(), position, anchor);
2350 self.position_history.commit_pending_movement();
2351
2352 self.set_active_buffer(ids[prev_idx]);
2353 }
2354 }
2355 }
2356
2357 pub fn navigate_back(&mut self) {
2359 self.in_navigation = true;
2361
2362 self.position_history.commit_pending_movement();
2364
2365 if self.position_history.can_go_back() && !self.position_history.can_go_forward() {
2368 let cursors = self.active_cursors();
2369 let position = cursors.primary().position;
2370 let anchor = cursors.primary().anchor;
2371 self.position_history
2372 .record_movement(self.active_buffer(), position, anchor);
2373 self.position_history.commit_pending_movement();
2374 }
2375
2376 if let Some(entry) = self.position_history.back() {
2378 let target_buffer = entry.buffer_id;
2379 let target_position = entry.position;
2380 let target_anchor = entry.anchor;
2381
2382 if self.buffers.contains_key(&target_buffer) {
2384 self.set_active_buffer(target_buffer);
2385
2386 let cursors = self.active_cursors();
2388 let cursor_id = cursors.primary_id();
2389 let old_position = cursors.primary().position;
2390 let old_anchor = cursors.primary().anchor;
2391 let old_sticky_column = cursors.primary().sticky_column;
2392 let event = Event::MoveCursor {
2393 cursor_id,
2394 old_position,
2395 new_position: target_position,
2396 old_anchor,
2397 new_anchor: target_anchor,
2398 old_sticky_column,
2399 new_sticky_column: 0, };
2401 let split_id = self.split_manager.active_split();
2402 let state = self.buffers.get_mut(&target_buffer).unwrap();
2403 let view_state = self.split_view_states.get_mut(&split_id).unwrap();
2404 state.apply(&mut view_state.cursors, &event);
2405 }
2406 }
2407
2408 self.in_navigation = false;
2410 }
2411
2412 pub fn navigate_forward(&mut self) {
2414 self.in_navigation = true;
2416
2417 if let Some(entry) = self.position_history.forward() {
2418 let target_buffer = entry.buffer_id;
2419 let target_position = entry.position;
2420 let target_anchor = entry.anchor;
2421
2422 if self.buffers.contains_key(&target_buffer) {
2424 self.set_active_buffer(target_buffer);
2425
2426 let cursors = self.active_cursors();
2428 let cursor_id = cursors.primary_id();
2429 let old_position = cursors.primary().position;
2430 let old_anchor = cursors.primary().anchor;
2431 let old_sticky_column = cursors.primary().sticky_column;
2432 let event = Event::MoveCursor {
2433 cursor_id,
2434 old_position,
2435 new_position: target_position,
2436 old_anchor,
2437 new_anchor: target_anchor,
2438 old_sticky_column,
2439 new_sticky_column: 0, };
2441 let split_id = self.split_manager.active_split();
2442 let state = self.buffers.get_mut(&target_buffer).unwrap();
2443 let view_state = self.split_view_states.get_mut(&split_id).unwrap();
2444 state.apply(&mut view_state.cursors, &event);
2445 }
2446 }
2447
2448 self.in_navigation = false;
2450 }
2451
2452 pub fn get_mouse_hover_state(&self) -> Option<(usize, u16, u16)> {
2455 self.mouse_state
2456 .lsp_hover_state
2457 .map(|(pos, _, x, y)| (pos, x, y))
2458 }
2459
2460 pub fn has_transient_popup(&self) -> bool {
2462 self.active_state()
2463 .popups
2464 .top()
2465 .is_some_and(|p| p.transient)
2466 }
2467
2468 pub fn force_check_mouse_hover(&mut self) -> bool {
2471 if let Some((byte_pos, _, screen_x, screen_y)) = self.mouse_state.lsp_hover_state {
2472 if !self.mouse_state.lsp_hover_request_sent {
2473 self.mouse_hover_screen_position = Some((screen_x, screen_y));
2474 match self.request_hover_at_position(byte_pos) {
2475 Ok(true) => {
2476 self.mouse_state.lsp_hover_request_sent = true;
2477 return true;
2478 }
2479 Ok(false) => return false, Err(e) => {
2481 tracing::debug!("Failed to request hover: {}", e);
2482 return false;
2483 }
2484 }
2485 }
2486 }
2487 false
2488 }
2489
2490 pub fn schedule_hot_exit_recovery(&mut self) {
2497 if self.config.editor.hot_exit {
2498 self.pending_hot_exit_recovery = true;
2499 }
2500 }
2501
2502 #[allow(clippy::too_many_arguments)]
2503 pub fn queue_file_open(
2504 &mut self,
2505 path: PathBuf,
2506 line: Option<usize>,
2507 column: Option<usize>,
2508 end_line: Option<usize>,
2509 end_column: Option<usize>,
2510 message: Option<String>,
2511 wait_id: Option<u64>,
2512 ) {
2513 self.pending_file_opens.push(super::PendingFileOpen {
2514 path,
2515 line,
2516 column,
2517 end_line,
2518 end_column,
2519 message,
2520 wait_id,
2521 });
2522 }
2523
2524 pub fn process_pending_file_opens(&mut self) -> bool {
2529 if self.pending_file_opens.is_empty() {
2530 return false;
2531 }
2532
2533 let pending = std::mem::take(&mut self.pending_file_opens);
2535 let mut processed_any = false;
2536
2537 for pending_file in pending {
2538 tracing::info!(
2539 "[SYNTAX DEBUG] Processing pending file open: {:?}",
2540 pending_file.path
2541 );
2542
2543 match self.open_file(&pending_file.path) {
2544 Ok(_) => {
2545 if let (Some(line), Some(end_line)) = (pending_file.line, pending_file.end_line)
2547 {
2548 self.select_range(
2549 line,
2550 pending_file.column,
2551 end_line,
2552 pending_file.end_column,
2553 );
2554 } else if let Some(line) = pending_file.line {
2555 self.goto_line_col(line, pending_file.column);
2556 }
2557 let has_popup = pending_file.message.is_some();
2559 if let Some(ref msg) = pending_file.message {
2560 self.show_file_message_popup(msg);
2561 }
2562 if let Some(wait_id) = pending_file.wait_id {
2564 let buffer_id = self.active_buffer();
2565 self.wait_tracking.insert(buffer_id, (wait_id, has_popup));
2566 }
2567 processed_any = true;
2568 }
2569 Err(e) => {
2570 if let Some(confirmation) =
2573 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
2574 {
2575 self.start_large_file_encoding_confirmation(confirmation);
2576 } else {
2577 self.set_status_message(
2579 t!("file.error_opening", error = e.to_string()).to_string(),
2580 );
2581 }
2582 processed_any = true;
2583 }
2584 }
2585 }
2586
2587 if processed_any && self.pending_hot_exit_recovery {
2589 self.pending_hot_exit_recovery = false;
2590 match self.apply_hot_exit_recovery() {
2591 Ok(count) if count > 0 => {
2592 tracing::info!("Hot exit: restored unsaved changes for {} buffer(s)", count);
2593 }
2594 Ok(_) => {}
2595 Err(e) => {
2596 tracing::warn!("Failed to apply hot exit recovery: {}", e);
2597 }
2598 }
2599 }
2600
2601 processed_any
2602 }
2603
2604 pub fn take_completed_waits(&mut self) -> Vec<u64> {
2606 std::mem::take(&mut self.completed_waits)
2607 }
2608
2609 pub fn remove_wait_tracking(&mut self, wait_id: u64) {
2611 self.wait_tracking.retain(|_, (wid, _)| *wid != wait_id);
2612 }
2613
2614 pub fn start_incremental_line_scan(&mut self, open_goto_line: bool) {
2623 let buffer_id = self.active_buffer();
2624 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2625 let (chunks, total_bytes) = state.buffer.prepare_line_scan();
2626 let leaves = state.buffer.piece_tree_leaves();
2627 self.line_scan_state = Some(super::LineScanState {
2628 buffer_id,
2629 leaves,
2630 chunks,
2631 next_chunk: 0,
2632 total_bytes,
2633 scanned_bytes: 0,
2634 updates: Vec::new(),
2635 open_goto_line_on_complete: open_goto_line,
2636 });
2637 self.set_status_message(t!("goto.scanning_progress", percent = 0).to_string());
2638 }
2639 }
2640
2641 pub fn process_line_scan(&mut self) -> bool {
2644 let _span = tracing::info_span!("process_line_scan").entered();
2645 let scan = match self.line_scan_state.as_mut() {
2646 Some(s) => s,
2647 None => return false,
2648 };
2649
2650 let buffer_id = scan.buffer_id;
2651
2652 if let Err(e) = self.process_line_scan_batch(buffer_id) {
2653 tracing::warn!("Line scan error: {e}");
2654 self.finish_line_scan_with_error(e);
2655 return true;
2656 }
2657
2658 let scan = self.line_scan_state.as_ref().unwrap();
2659 if scan.next_chunk >= scan.chunks.len() {
2660 self.finish_line_scan_ok();
2661 } else {
2662 let pct = if scan.total_bytes > 0 {
2663 (scan.scanned_bytes * 100) / scan.total_bytes
2664 } else {
2665 100
2666 };
2667 self.set_status_message(t!("goto.scanning_progress", percent = pct).to_string());
2668 }
2669 true
2670 }
2671
2672 fn process_line_scan_batch(&mut self, buffer_id: BufferId) -> std::io::Result<()> {
2680 let _span = tracing::info_span!("process_line_scan_batch").entered();
2681 let concurrency = self.config.editor.read_concurrency.max(1);
2682
2683 let state = self.buffers.get(&buffer_id);
2684 let scan = self.line_scan_state.as_mut().unwrap();
2685
2686 let mut results: Vec<(usize, usize)> = Vec::new();
2687 let mut io_work: Vec<(usize, std::path::PathBuf, u64, usize)> = Vec::new();
2688
2689 while scan.next_chunk < scan.chunks.len() && (results.len() + io_work.len()) < concurrency {
2690 let chunk = scan.chunks[scan.next_chunk].clone();
2691 scan.next_chunk += 1;
2692 scan.scanned_bytes += chunk.byte_len;
2693
2694 if chunk.already_known {
2695 continue;
2696 }
2697
2698 if let Some(state) = state {
2699 let leaf = &scan.leaves[chunk.leaf_index];
2700
2701 match state.buffer.leaf_io_params(leaf) {
2705 None => {
2706 let count = state.buffer.scan_leaf(leaf)?;
2708 results.push((chunk.leaf_index, count));
2709 }
2710 Some((path, offset, len)) => {
2711 io_work.push((chunk.leaf_index, path, offset, len));
2713 }
2714 }
2715 }
2716 }
2717
2718 if !io_work.is_empty() {
2720 let fs = match state {
2721 Some(s) => s.buffer.filesystem().clone(),
2722 None => return Ok(()),
2723 };
2724
2725 let rt = self
2726 .tokio_runtime
2727 .as_ref()
2728 .ok_or_else(|| std::io::Error::other("async runtime not available"))?;
2729
2730 let io_results: Vec<std::io::Result<(usize, usize)>> = rt.block_on(async {
2731 let mut handles = Vec::with_capacity(io_work.len());
2732 for (leaf_idx, path, offset, len) in io_work {
2733 let fs = fs.clone();
2734 handles.push(tokio::task::spawn_blocking(move || {
2735 let count = fs.count_line_feeds_in_range(&path, offset, len)?;
2736 Ok((leaf_idx, count))
2737 }));
2738 }
2739
2740 let mut results = Vec::with_capacity(handles.len());
2741 for handle in handles {
2742 results.push(handle.await.unwrap());
2743 }
2744 results
2745 });
2746
2747 for result in io_results {
2748 results.push(result?);
2749 }
2750 }
2751
2752 for (leaf_idx, count) in results {
2753 scan.updates.push((leaf_idx, count));
2754 }
2755
2756 Ok(())
2757 }
2758
2759 fn finish_line_scan_ok(&mut self) {
2760 let _span = tracing::info_span!("finish_line_scan_ok").entered();
2761 let scan = self.line_scan_state.take().unwrap();
2762 let open_goto = scan.open_goto_line_on_complete;
2763 if let Some(state) = self.buffers.get_mut(&scan.buffer_id) {
2764 let _span = tracing::info_span!(
2765 "rebuild_with_pristine_saved_root",
2766 updates = scan.updates.len()
2767 )
2768 .entered();
2769 state.buffer.rebuild_with_pristine_saved_root(&scan.updates);
2770 }
2771 self.set_status_message(t!("goto.scan_complete").to_string());
2772 if open_goto {
2773 self.open_goto_line_if_active(scan.buffer_id);
2774 }
2775 }
2776
2777 fn finish_line_scan_with_error(&mut self, e: std::io::Error) {
2778 let scan = self.line_scan_state.take().unwrap();
2779 let open_goto = scan.open_goto_line_on_complete;
2780 self.set_status_message(t!("goto.scan_failed", error = e.to_string()).to_string());
2781 if open_goto {
2782 self.open_goto_line_if_active(scan.buffer_id);
2783 }
2784 }
2785
2786 fn open_goto_line_if_active(&mut self, buffer_id: BufferId) {
2787 if self.active_buffer() == buffer_id {
2788 self.start_prompt(
2789 t!("file.goto_line_prompt").to_string(),
2790 PromptType::GotoLine,
2791 );
2792 }
2793 }
2794
2795 pub fn process_search_scan(&mut self) -> bool {
2800 let scan = match self.search_scan_state.as_mut() {
2801 Some(s) => s,
2802 None => return false,
2803 };
2804
2805 let buffer_id = scan.buffer_id;
2806
2807 if let Err(e) = self.process_search_scan_batch(buffer_id) {
2808 tracing::warn!("Search scan error: {e}");
2809 let _scan = self.search_scan_state.take().unwrap();
2810 self.set_status_message(format!("Search failed: {e}"));
2811 return true;
2812 }
2813
2814 let scan = self.search_scan_state.as_ref().unwrap();
2815 if scan.scan.is_done() {
2816 self.finish_search_scan();
2817 } else {
2818 let pct = scan.scan.progress_percent();
2819 let match_count = scan.scan.matches.len();
2820 self.set_status_message(format!(
2821 "Searching... {}% ({} matches so far)",
2822 pct, match_count
2823 ));
2824 }
2825 true
2826 }
2827
2828 fn process_search_scan_batch(
2831 &mut self,
2832 buffer_id: crate::model::event::BufferId,
2833 ) -> std::io::Result<()> {
2834 let concurrency = self.config.editor.read_concurrency.max(1);
2835
2836 for _ in 0..concurrency {
2837 let is_done = {
2838 let scan_state = match self.search_scan_state.as_ref() {
2839 Some(s) => s,
2840 None => return Ok(()),
2841 };
2842 scan_state.scan.is_done()
2843 };
2844 if is_done {
2845 break;
2846 }
2847
2848 let mut scan = self.search_scan_state.take().unwrap();
2851 let result = if let Some(state) = self.buffers.get_mut(&buffer_id) {
2852 state.buffer.search_scan_next_chunk(&mut scan.scan)
2853 } else {
2854 Ok(false)
2855 };
2856 self.search_scan_state = Some(scan);
2857
2858 match result {
2859 Ok(false) => break, Ok(true) => {} Err(e) => return Err(e),
2862 }
2863 }
2864
2865 Ok(())
2866 }
2867
2868 fn finish_search_scan(&mut self) {
2872 let scan = self.search_scan_state.take().unwrap();
2873 let buffer_id = scan.buffer_id;
2874 let match_ranges: Vec<(usize, usize)> = scan
2875 .scan
2876 .matches
2877 .iter()
2878 .map(|m| (m.byte_offset, m.length))
2879 .collect();
2880 let capped = scan.scan.capped;
2881 let query = scan.query;
2882
2883 if let Some(state) = self.buffers.get_mut(&buffer_id) {
2887 state.buffer.refresh_saved_root_if_unmodified();
2888 }
2889
2890 if match_ranges.is_empty() {
2891 self.search_state = None;
2892 self.set_status_message(format!("No matches found for '{}'", query));
2893 return;
2894 }
2895
2896 self.finalize_search(&query, match_ranges, capped, None);
2897 }
2898}