1use anyhow::Result as AnyhowResult;
12use rust_i18n::t;
13use std::path::Path;
14use std::sync::Arc;
15
16use crate::app::warning_domains::WarningDomain;
17use crate::model::event::{BufferId, Event, SplitId};
18use crate::services::lsp::manager::detect_language;
19use crate::state::EditorState;
20use crate::view::prompt::PromptType;
21use crate::view::split::SplitViewState;
22
23use super::help;
24use super::Editor;
25
26impl Editor {
27 pub fn open_file(&mut self, path: &Path) -> anyhow::Result<BufferId> {
32 let buffer_id = self.open_file_no_focus(path)?;
33
34 let is_new_buffer = self.active_buffer() != buffer_id;
38
39 if is_new_buffer {
40 self.position_history.commit_pending_movement();
42
43 let current_state = self.active_state();
45 let position = current_state.cursors.primary().position;
46 let anchor = current_state.cursors.primary().anchor;
47 self.position_history
48 .record_movement(self.active_buffer(), position, anchor);
49 self.position_history.commit_pending_movement();
50 }
51
52 self.set_active_buffer(buffer_id);
53
54 let display_name = self
56 .buffer_metadata
57 .get(&buffer_id)
58 .map(|m| m.display_name.clone())
59 .unwrap_or_else(|| path.display().to_string());
60
61 let is_binary = self
63 .buffers
64 .get(&buffer_id)
65 .map(|s| s.buffer.is_binary())
66 .unwrap_or(false);
67
68 if is_binary {
70 self.status_message = Some(t!("buffer.opened_binary", name = display_name).to_string());
71 } else {
72 self.status_message = Some(t!("buffer.opened", name = display_name).to_string());
73 }
74
75 Ok(buffer_id)
76 }
77
78 pub fn open_file_no_focus(&mut self, path: &Path) -> anyhow::Result<BufferId> {
85 let base_dir = if self.filesystem.remote_connection_info().is_some() {
88 self.filesystem
89 .home_dir()
90 .unwrap_or_else(|_| self.working_dir.clone())
91 } else {
92 self.working_dir.clone()
93 };
94
95 let resolved_path = if path.is_relative() {
96 base_dir.join(path)
97 } else {
98 path.to_path_buf()
99 };
100
101 let file_exists = self.filesystem.exists(&resolved_path);
104
105 let canonical_path = if file_exists {
109 self.filesystem
110 .canonicalize(&resolved_path)
111 .unwrap_or_else(|_| resolved_path.clone())
112 } else {
113 if let Some(parent) = resolved_path.parent() {
115 let canonical_parent = if parent.as_os_str().is_empty() {
116 base_dir.clone()
118 } else {
119 self.filesystem
120 .canonicalize(parent)
121 .unwrap_or_else(|_| parent.to_path_buf())
122 };
123 if let Some(filename) = resolved_path.file_name() {
124 canonical_parent.join(filename)
125 } else {
126 resolved_path
127 }
128 } else {
129 resolved_path
130 }
131 };
132 let path = canonical_path.as_path();
133
134 if self.filesystem.is_dir(path).unwrap_or(false) {
138 anyhow::bail!(t!("buffer.cannot_open_directory"));
139 }
140
141 let already_open = self
143 .buffers
144 .iter()
145 .find(|(_, state)| state.buffer.file_path() == Some(path))
146 .map(|(id, _)| *id);
147
148 if let Some(id) = already_open {
149 return Ok(id);
150 }
151
152 let replace_current = {
155 let current_state = self.buffers.get(&self.active_buffer()).unwrap();
156 !current_state.is_composite_buffer
157 && current_state.buffer.is_empty()
158 && !current_state.buffer.is_modified()
159 && current_state.buffer.file_path().is_none()
160 };
161
162 let buffer_id = if replace_current {
163 self.active_buffer()
165 } else {
166 let id = BufferId(self.next_buffer_id);
168 self.next_buffer_id += 1;
169 id
170 };
171
172 tracing::info!(
174 "[SYNTAX DEBUG] open_file_no_focus: path={:?}, extension={:?}, registry_syntaxes={}, user_extensions={:?}",
175 path,
176 path.extension(),
177 self.grammar_registry.available_syntaxes().len(),
178 self.grammar_registry.user_extensions_debug()
179 );
180 let mut state = if file_exists {
181 EditorState::from_file_with_languages(
182 path,
183 self.terminal_width,
184 self.terminal_height,
185 self.config.editor.large_file_threshold_bytes as usize,
186 &self.grammar_registry,
187 &self.config.languages,
188 Arc::clone(&self.filesystem),
189 )?
190 } else {
191 let mut new_state = EditorState::new(
193 self.terminal_width,
194 self.terminal_height,
195 self.config.editor.large_file_threshold_bytes as usize,
196 Arc::clone(&self.filesystem),
197 );
198 new_state.buffer.set_file_path(path.to_path_buf());
200 new_state
201 };
202 let is_binary = state.buffer.is_binary();
206 if is_binary {
207 state.editing_disabled = true;
209 tracing::info!("Detected binary file: {}", path.display());
210 }
211
212 if let Some(language) = detect_language(path, &self.config.languages) {
215 if let Some(lang_config) = self.config.languages.get(&language) {
216 state.show_whitespace_tabs = lang_config.show_whitespace_tabs;
217 state.use_tabs = lang_config.use_tabs;
218 state.tab_size = lang_config.tab_size.unwrap_or(self.config.editor.tab_size);
220 } else {
221 state.tab_size = self.config.editor.tab_size;
222 }
223 } else {
224 state.tab_size = self.config.editor.tab_size;
225 }
226
227 state
229 .margins
230 .set_line_numbers(self.config.editor.line_numbers);
231
232 self.buffers.insert(buffer_id, state);
233 self.event_logs
234 .insert(buffer_id, crate::model::event::EventLog::new());
235
236 let mut metadata =
238 super::types::BufferMetadata::with_file(path.to_path_buf(), &self.working_dir);
239
240 if is_binary {
242 metadata.binary = true;
243 metadata.read_only = true;
244 metadata.disable_lsp(t!("buffer.binary_file").to_string());
245 }
246
247 if !is_binary {
249 self.notify_lsp_file_opened(path, buffer_id, &mut metadata);
250 }
251
252 self.buffer_metadata.insert(buffer_id, metadata);
254
255 let active_split = self.split_manager.active_split();
257 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
258 view_state.add_buffer(buffer_id);
259 view_state.viewport.line_wrap_enabled = self.config.editor.line_wrap;
261 }
262
263 self.restore_global_file_state(buffer_id, path, active_split);
266
267 self.emit_event(
269 crate::model::control_event::events::FILE_OPENED.name,
270 serde_json::json!({
271 "path": path.display().to_string(),
272 "buffer_id": buffer_id.0
273 }),
274 );
275
276 self.watch_file(path);
278
279 self.plugin_manager.run_hook(
281 "after_file_open",
282 crate::services::plugins::hooks::HookArgs::AfterFileOpen {
283 buffer_id,
284 path: path.to_path_buf(),
285 },
286 );
287
288 Ok(buffer_id)
289 }
290
291 pub fn open_local_file(&mut self, path: &Path) -> anyhow::Result<BufferId> {
297 let resolved_path = if path.is_relative() {
299 self.working_dir.join(path)
300 } else {
301 path.to_path_buf()
302 };
303
304 let canonical_path = resolved_path
306 .canonicalize()
307 .unwrap_or_else(|_| resolved_path.clone());
308 let path = canonical_path.as_path();
309
310 let already_open = self
312 .buffers
313 .iter()
314 .find(|(_, state)| state.buffer.file_path() == Some(path))
315 .map(|(id, _)| *id);
316
317 if let Some(id) = already_open {
318 self.set_active_buffer(id);
319 return Ok(id);
320 }
321
322 let buffer_id = BufferId(self.next_buffer_id);
324 self.next_buffer_id += 1;
325
326 let state = EditorState::from_file_with_languages(
328 path,
329 self.terminal_width,
330 self.terminal_height,
331 self.config.editor.large_file_threshold_bytes as usize,
332 &self.grammar_registry,
333 &self.config.languages,
334 Arc::clone(&self.local_filesystem),
335 )?;
336
337 self.buffers.insert(buffer_id, state);
338 self.event_logs
339 .insert(buffer_id, crate::model::event::EventLog::new());
340
341 let metadata =
343 super::types::BufferMetadata::with_file(path.to_path_buf(), &self.working_dir);
344 self.buffer_metadata.insert(buffer_id, metadata);
345
346 let active_split = self.split_manager.active_split();
348 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
349 view_state.add_buffer(buffer_id);
350 view_state.viewport.line_wrap_enabled = self.config.editor.line_wrap;
351 }
352
353 self.set_active_buffer(buffer_id);
354
355 let display_name = path.display().to_string();
356 self.status_message = Some(t!("buffer.opened", name = display_name).to_string());
357
358 Ok(buffer_id)
359 }
360
361 fn restore_global_file_state(&mut self, buffer_id: BufferId, path: &Path, split_id: SplitId) {
366 use crate::session::PersistedFileSession;
367
368 let file_state = match PersistedFileSession::load(path) {
370 Some(state) => state,
371 None => return, };
373
374 let max_pos = match self.buffers.get(&buffer_id) {
376 Some(buffer) => buffer.buffer.len(),
377 None => return,
378 };
379
380 if let Some(editor_state) = self.buffers.get_mut(&buffer_id) {
382 let cursor_pos = file_state.cursor.position.min(max_pos);
383 editor_state.cursors.primary_mut().position = cursor_pos;
384 editor_state.cursors.primary_mut().anchor =
385 file_state.cursor.anchor.map(|a| a.min(max_pos));
386 }
387
388 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
390 view_state.viewport.top_byte = file_state.scroll.top_byte;
391 view_state.viewport.left_column = file_state.scroll.left_column;
392 }
393 }
394
395 fn save_file_state_on_close(&self, buffer_id: BufferId) {
397 use crate::session::{
398 PersistedFileSession, SerializedCursor, SerializedFileState, SerializedScroll,
399 };
400
401 let abs_path = match self.buffer_metadata.get(&buffer_id) {
403 Some(metadata) => match metadata.file_path() {
404 Some(path) => path.to_path_buf(),
405 None => return, },
407 None => return,
408 };
409
410 let view_state = self
412 .split_view_states
413 .values()
414 .find(|vs| vs.has_buffer(buffer_id));
415
416 let view_state = match view_state {
417 Some(vs) => vs,
418 None => return, };
420
421 let primary_cursor = view_state.cursors.primary();
423 let file_state = SerializedFileState {
424 cursor: SerializedCursor {
425 position: primary_cursor.position,
426 anchor: primary_cursor.anchor,
427 sticky_column: primary_cursor.sticky_column,
428 },
429 additional_cursors: view_state
430 .cursors
431 .iter()
432 .skip(1)
433 .map(|(_, cursor)| SerializedCursor {
434 position: cursor.position,
435 anchor: cursor.anchor,
436 sticky_column: cursor.sticky_column,
437 })
438 .collect(),
439 scroll: SerializedScroll {
440 top_byte: view_state.viewport.top_byte,
441 top_view_line_offset: view_state.viewport.top_view_line_offset,
442 left_column: view_state.viewport.left_column,
443 },
444 };
445
446 PersistedFileSession::save(&abs_path, file_state);
448 tracing::debug!("Saved file state on close for {:?}", abs_path);
449 }
450
451 pub fn goto_line_col(&mut self, line: usize, column: Option<usize>) {
457 if line == 0 {
458 return; }
460
461 let buffer_id = self.active_buffer();
462 let estimated_line_length = self.config.editor.estimated_line_length;
463
464 if let Some(state) = self.buffers.get(&buffer_id) {
465 let cursor_id = state.cursors.primary_id();
466 let old_position = state.cursors.primary().position;
467 let old_anchor = state.cursors.primary().anchor;
468 let old_sticky_column = state.cursors.primary().sticky_column;
469 let is_large_file = state.buffer.line_count().is_none();
470 let buffer_len = state.buffer.len();
471
472 let target_line = line.saturating_sub(1);
474 let target_col = column.map(|c| c.saturating_sub(1)).unwrap_or(0);
476
477 let position = if is_large_file {
478 let estimated_offset = target_line * estimated_line_length;
480 let clamped_offset = estimated_offset.min(buffer_len);
481
482 if let Some(state) = self.buffers.get_mut(&buffer_id) {
484 let iter = state
485 .buffer
486 .line_iterator(clamped_offset, estimated_line_length);
487 let line_start = iter.current_position();
488 (line_start + target_col).min(buffer_len)
490 } else {
491 clamped_offset
492 }
493 } else {
494 let max_line = state.buffer.line_count().unwrap_or(1).saturating_sub(1);
496 let actual_line = target_line.min(max_line);
497 state.buffer.line_col_to_position(actual_line, target_col)
498 };
499
500 let event = Event::MoveCursor {
501 cursor_id,
502 old_position,
503 new_position: position,
504 old_anchor,
505 new_anchor: None,
506 old_sticky_column,
507 new_sticky_column: target_col,
508 };
509
510 if let Some(state) = self.buffers.get_mut(&buffer_id) {
511 state.apply(&event);
512 }
513 }
514 }
515
516 pub fn new_buffer(&mut self) -> BufferId {
518 self.position_history.commit_pending_movement();
520
521 let current_state = self.active_state();
523 let position = current_state.cursors.primary().position;
524 let anchor = current_state.cursors.primary().anchor;
525 self.position_history
526 .record_movement(self.active_buffer(), position, anchor);
527 self.position_history.commit_pending_movement();
528
529 let buffer_id = BufferId(self.next_buffer_id);
530 self.next_buffer_id += 1;
531
532 let mut state = EditorState::new(
533 self.terminal_width,
534 self.terminal_height,
535 self.config.editor.large_file_threshold_bytes as usize,
536 Arc::clone(&self.filesystem),
537 );
538 state
540 .margins
541 .set_line_numbers(self.config.editor.line_numbers);
542 state
544 .buffer
545 .set_default_line_ending(self.config.editor.default_line_ending.to_line_ending());
546 self.buffers.insert(buffer_id, state);
547 self.event_logs
548 .insert(buffer_id, crate::model::event::EventLog::new());
549 self.buffer_metadata
550 .insert(buffer_id, crate::app::types::BufferMetadata::new());
551
552 let active_split = self.split_manager.active_split();
554 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
555 view_state.viewport.line_wrap_enabled = self.config.editor.line_wrap;
556 }
557
558 self.set_active_buffer(buffer_id);
559 self.status_message = Some(t!("buffer.new").to_string());
560
561 buffer_id
562 }
563
564 pub fn open_stdin_buffer(
574 &mut self,
575 temp_path: &Path,
576 thread_handle: Option<std::thread::JoinHandle<anyhow::Result<()>>>,
577 ) -> AnyhowResult<BufferId> {
578 self.position_history.commit_pending_movement();
580
581 let current_state = self.active_state();
583 let position = current_state.cursors.primary().position;
584 let anchor = current_state.cursors.primary().anchor;
585 self.position_history
586 .record_movement(self.active_buffer(), position, anchor);
587 self.position_history.commit_pending_movement();
588
589 let replace_current = {
592 let current_state = self.buffers.get(&self.active_buffer()).unwrap();
593 !current_state.is_composite_buffer
594 && current_state.buffer.is_empty()
595 && !current_state.buffer.is_modified()
596 && current_state.buffer.file_path().is_none()
597 };
598
599 let buffer_id = if replace_current {
600 self.active_buffer()
602 } else {
603 let id = BufferId(self.next_buffer_id);
605 self.next_buffer_id += 1;
606 id
607 };
608
609 let file_size = self.filesystem.metadata(temp_path)?.size as usize;
611
612 let mut state = EditorState::from_file_with_languages(
615 temp_path,
616 self.terminal_width,
617 self.terminal_height,
618 self.config.editor.large_file_threshold_bytes as usize,
619 &self.grammar_registry,
620 &self.config.languages,
621 Arc::clone(&self.filesystem),
622 )?;
623
624 state.buffer.clear_file_path();
627 state.buffer.clear_modified();
629
630 state.tab_size = self.config.editor.tab_size;
632
633 state
635 .margins
636 .set_line_numbers(self.config.editor.line_numbers);
637
638 self.buffers.insert(buffer_id, state);
639 self.event_logs
640 .insert(buffer_id, crate::model::event::EventLog::new());
641
642 let metadata =
644 super::types::BufferMetadata::new_unnamed(t!("stdin.display_name").to_string());
645 self.buffer_metadata.insert(buffer_id, metadata);
646
647 let active_split = self.split_manager.active_split();
649 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
650 view_state.add_buffer(buffer_id);
651 view_state.viewport.line_wrap_enabled = self.config.editor.line_wrap;
653 }
654
655 self.set_active_buffer(buffer_id);
656
657 let complete = thread_handle.is_none();
660 self.stdin_streaming = Some(super::StdinStreamingState {
661 temp_path: temp_path.to_path_buf(),
662 buffer_id,
663 last_known_size: file_size,
664 complete,
665 thread_handle,
666 });
667
668 self.status_message = Some(t!("stdin.streaming").to_string());
670
671 Ok(buffer_id)
672 }
673
674 pub fn poll_stdin_streaming(&mut self) -> bool {
677 let Some(ref mut stream_state) = self.stdin_streaming else {
678 return false;
679 };
680
681 if stream_state.complete {
682 return false;
683 }
684
685 let mut changed = false;
686
687 let current_size = self
689 .filesystem
690 .metadata(&stream_state.temp_path)
691 .map(|m| m.size as usize)
692 .unwrap_or(stream_state.last_known_size);
693
694 if current_size > stream_state.last_known_size {
696 if let Some(editor_state) = self.buffers.get_mut(&stream_state.buffer_id) {
697 editor_state
698 .buffer
699 .extend_streaming(&stream_state.temp_path, current_size);
700 }
701 stream_state.last_known_size = current_size;
702
703 self.status_message =
705 Some(t!("stdin.streaming_bytes", bytes = current_size).to_string());
706 changed = true;
707 }
708
709 let thread_finished = stream_state
711 .thread_handle
712 .as_ref()
713 .map(|h| h.is_finished())
714 .unwrap_or(true);
715
716 if thread_finished {
717 if let Some(handle) = stream_state.thread_handle.take() {
719 match handle.join() {
720 Ok(Ok(())) => {
721 tracing::info!("Stdin streaming completed successfully");
722 }
723 Ok(Err(e)) => {
724 tracing::warn!("Stdin streaming error: {}", e);
725 self.status_message =
726 Some(t!("stdin.read_error", error = e.to_string()).to_string());
727 }
728 Err(_) => {
729 tracing::warn!("Stdin streaming thread panicked");
730 self.status_message = Some(t!("stdin.read_error_panic").to_string());
731 }
732 }
733 }
734 self.complete_stdin_streaming();
735 changed = true;
736 }
737
738 changed
739 }
740
741 pub fn complete_stdin_streaming(&mut self) {
744 if let Some(ref mut stream_state) = self.stdin_streaming {
745 stream_state.complete = true;
746
747 let final_size = self
749 .filesystem
750 .metadata(&stream_state.temp_path)
751 .map(|m| m.size as usize)
752 .unwrap_or(stream_state.last_known_size);
753
754 if final_size > stream_state.last_known_size {
755 if let Some(editor_state) = self.buffers.get_mut(&stream_state.buffer_id) {
756 editor_state
757 .buffer
758 .extend_streaming(&stream_state.temp_path, final_size);
759 }
760 stream_state.last_known_size = final_size;
761 }
762
763 self.status_message =
764 Some(t!("stdin.read_complete", bytes = stream_state.last_known_size).to_string());
765 }
766 }
767
768 pub fn is_stdin_streaming(&self) -> bool {
770 self.stdin_streaming
771 .as_ref()
772 .map(|s| !s.complete)
773 .unwrap_or(false)
774 }
775
776 pub fn create_virtual_buffer(
786 &mut self,
787 name: String,
788 mode: String,
789 read_only: bool,
790 ) -> BufferId {
791 let buffer_id = BufferId(self.next_buffer_id);
792 self.next_buffer_id += 1;
793
794 let mut state = EditorState::new(
795 self.terminal_width,
796 self.terminal_height,
797 self.config.editor.large_file_threshold_bytes as usize,
798 Arc::clone(&self.filesystem),
799 );
800 state.set_language_from_name(&name, &self.grammar_registry);
804
805 state
807 .margins
808 .set_line_numbers(self.config.editor.line_numbers);
809
810 self.buffers.insert(buffer_id, state);
811 self.event_logs
812 .insert(buffer_id, crate::model::event::EventLog::new());
813
814 let metadata = super::types::BufferMetadata::virtual_buffer(name, mode, read_only);
816 self.buffer_metadata.insert(buffer_id, metadata);
817
818 let active_split = self.split_manager.active_split();
820 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
821 view_state.add_buffer(buffer_id);
822 } else {
823 let mut view_state =
825 SplitViewState::with_buffer(self.terminal_width, self.terminal_height, buffer_id);
826 view_state.viewport.line_wrap_enabled = self.config.editor.line_wrap;
827 self.split_view_states.insert(active_split, view_state);
828 }
829
830 buffer_id
831 }
832
833 pub fn set_virtual_buffer_content(
839 &mut self,
840 buffer_id: BufferId,
841 entries: Vec<crate::primitives::text_property::TextPropertyEntry>,
842 ) -> Result<(), String> {
843 let state = self
844 .buffers
845 .get_mut(&buffer_id)
846 .ok_or_else(|| "Buffer not found".to_string())?;
847
848 let old_cursor_pos = state.cursors.primary().position;
850
851 let (text, properties) =
853 crate::primitives::text_property::TextPropertyManager::from_entries(entries);
854
855 let current_len = state.buffer.len();
857 if current_len > 0 {
858 state.buffer.delete_bytes(0, current_len);
859 }
860 state.buffer.insert(0, &text);
861
862 state.buffer.clear_modified();
864
865 state.text_properties = properties;
867
868 let new_len = state.buffer.len();
870 let clamped_pos = old_cursor_pos.min(new_len);
871 let new_cursor_pos = state.buffer.snap_to_char_boundary(clamped_pos);
873 state.cursors.primary_mut().position = new_cursor_pos;
874 state.cursors.primary_mut().anchor = None;
875
876 Ok(())
877 }
878
879 pub fn open_help_manual(&mut self) {
883 let existing_buffer = self
885 .buffer_metadata
886 .iter()
887 .find(|(_, m)| m.display_name == help::HELP_MANUAL_BUFFER_NAME)
888 .map(|(id, _)| *id);
889
890 if let Some(buffer_id) = existing_buffer {
891 self.set_active_buffer(buffer_id);
893 return;
894 }
895
896 let buffer_id = self.create_virtual_buffer(
898 help::HELP_MANUAL_BUFFER_NAME.to_string(),
899 "special".to_string(),
900 true,
901 );
902
903 if let Some(state) = self.buffers.get_mut(&buffer_id) {
905 state.buffer.insert(0, help::HELP_MANUAL_CONTENT);
906 state.buffer.clear_modified();
907 state.editing_disabled = true;
908
909 state.margins.set_line_numbers(false);
911 }
912
913 self.set_active_buffer(buffer_id);
914 }
915
916 pub fn open_keyboard_shortcuts(&mut self) {
921 let existing_buffer = self
923 .buffer_metadata
924 .iter()
925 .find(|(_, m)| m.display_name == help::KEYBOARD_SHORTCUTS_BUFFER_NAME)
926 .map(|(id, _)| *id);
927
928 if let Some(buffer_id) = existing_buffer {
929 self.set_active_buffer(buffer_id);
931 return;
932 }
933
934 let bindings = self.keybindings.get_all_bindings();
936
937 let mut content = String::from("Keyboard Shortcuts\n");
939 content.push_str("==================\n\n");
940 content.push_str("Press 'q' to close this buffer.\n\n");
941
942 let mut current_context = String::new();
944 for (key, action) in &bindings {
945 let (context, action_name) = if let Some(bracket_end) = action.find("] ") {
947 let ctx = &action[1..bracket_end];
948 let name = &action[bracket_end + 2..];
949 (ctx.to_string(), name.to_string())
950 } else {
951 ("Normal".to_string(), action.clone())
952 };
953
954 if context != current_context {
956 if !current_context.is_empty() {
957 content.push('\n');
958 }
959 content.push_str(&format!("── {} Mode ──\n\n", context));
960 current_context = context;
961 }
962
963 content.push_str(&format!(" {:20} {}\n", key, action_name));
965 }
966
967 let buffer_id = self.create_virtual_buffer(
969 help::KEYBOARD_SHORTCUTS_BUFFER_NAME.to_string(),
970 "special".to_string(),
971 true,
972 );
973
974 if let Some(state) = self.buffers.get_mut(&buffer_id) {
976 state.buffer.insert(0, &content);
977 state.buffer.clear_modified();
978 state.editing_disabled = true;
979
980 state.margins.set_line_numbers(false);
982 }
983
984 self.set_active_buffer(buffer_id);
985 }
986
987 pub fn show_warnings_popup(&mut self) {
992 if !self.warning_domains.has_any_warnings() {
993 self.status_message = Some(t!("warnings.none").to_string());
994 return;
995 }
996
997 self.open_warning_log();
999 }
1000
1001 pub fn show_lsp_status_popup(&mut self) {
1004 let has_error = self.warning_domains.lsp.level() == crate::app::WarningLevel::Error;
1005
1006 let language = self
1009 .warning_domains
1010 .lsp
1011 .language
1012 .clone()
1013 .unwrap_or_else(|| {
1014 self.buffer_metadata
1015 .get(&self.active_buffer())
1016 .and_then(|m| m.file_path())
1017 .and_then(|path| detect_language(path, &self.config.languages))
1018 .unwrap_or_else(|| "unknown".to_string())
1019 });
1020
1021 tracing::info!(
1022 "show_lsp_status_popup: language={}, has_error={}, has_warnings={}",
1023 language,
1024 has_error,
1025 self.warning_domains.lsp.has_warnings()
1026 );
1027
1028 self.plugin_manager.run_hook(
1030 "lsp_status_clicked",
1031 crate::services::plugins::hooks::HookArgs::LspStatusClicked {
1032 language: language.clone(),
1033 has_error,
1034 },
1035 );
1036 tracing::info!("show_lsp_status_popup: hook fired");
1037
1038 if !self.warning_domains.lsp.has_warnings() {
1039 if self.lsp_status.is_empty() {
1040 self.status_message = Some(t!("lsp.no_server_active").to_string());
1041 } else {
1042 self.status_message = Some(t!("lsp.status", status = &self.lsp_status).to_string());
1043 }
1044 return;
1045 }
1046
1047 if has_error && self.plugin_manager.has_hook_handlers("lsp_status_clicked") {
1051 tracing::info!(
1052 "show_lsp_status_popup: has_error=true and plugin registered, skipping warning log"
1053 );
1054 return;
1055 }
1056
1057 self.open_warning_log();
1059 }
1060
1061 pub fn get_text_properties_at_cursor(
1063 &self,
1064 ) -> Option<Vec<&crate::primitives::text_property::TextProperty>> {
1065 let state = self.buffers.get(&self.active_buffer())?;
1066 let cursor_pos = state.cursors.primary().position;
1067 Some(state.text_properties.get_at(cursor_pos))
1068 }
1069
1070 pub fn close_buffer(&mut self, id: BufferId) -> anyhow::Result<()> {
1072 if let Some(state) = self.buffers.get(&id) {
1074 if state.buffer.is_modified() {
1075 return Err(anyhow::anyhow!("Buffer has unsaved changes"));
1076 }
1077 }
1078 self.close_buffer_internal(id)
1079 }
1080
1081 pub fn force_close_buffer(&mut self, id: BufferId) -> anyhow::Result<()> {
1084 self.close_buffer_internal(id)
1085 }
1086
1087 fn close_buffer_internal(&mut self, id: BufferId) -> anyhow::Result<()> {
1089 self.save_file_state_on_close(id);
1091
1092 if let Some(terminal_id) = self.terminal_buffers.remove(&id) {
1094 self.terminal_manager.close(terminal_id);
1096
1097 let backing_file = self.terminal_backing_files.remove(&terminal_id);
1099 if let Some(ref path) = backing_file {
1100 let _ = self.filesystem.remove_file(path);
1101 }
1102 if let Some(log_file) = self.terminal_log_files.remove(&terminal_id) {
1104 if backing_file.as_ref() != Some(&log_file) {
1105 let _ = self.filesystem.remove_file(&log_file);
1106 }
1107 }
1108
1109 self.terminal_mode_resume.remove(&id);
1111
1112 if self.terminal_mode {
1114 self.terminal_mode = false;
1115 self.key_context = crate::input::keybindings::KeyContext::Normal;
1116 }
1117 }
1118
1119 let active_split = self.split_manager.active_split();
1122 let replacement_from_history = self.split_view_states.get(&active_split).and_then(|vs| {
1123 vs.focus_history
1125 .iter()
1126 .rev()
1127 .find(|&&bid| {
1128 bid != id
1129 && self.buffers.contains_key(&bid)
1130 && !self
1131 .buffer_metadata
1132 .get(&bid)
1133 .map(|m| m.hidden_from_tabs)
1134 .unwrap_or(false)
1135 })
1136 .copied()
1137 });
1138
1139 let visible_replacement = replacement_from_history.or_else(|| {
1141 self.buffers
1142 .keys()
1143 .find(|&&bid| {
1144 bid != id
1145 && !self
1146 .buffer_metadata
1147 .get(&bid)
1148 .map(|m| m.hidden_from_tabs)
1149 .unwrap_or(false)
1150 })
1151 .copied()
1152 });
1153
1154 let is_last_visible_buffer = visible_replacement.is_none();
1155 let replacement_buffer = if is_last_visible_buffer {
1156 self.new_buffer()
1157 } else {
1158 visible_replacement.unwrap()
1159 };
1160
1161 if self.active_buffer() == id {
1166 self.set_active_buffer(replacement_buffer);
1167 }
1168
1169 let splits_to_update = self.split_manager.splits_for_buffer(id);
1171 for split_id in splits_to_update {
1172 let _ = self
1173 .split_manager
1174 .set_split_buffer(split_id, replacement_buffer);
1175 }
1176
1177 self.buffers.remove(&id);
1178 self.event_logs.remove(&id);
1179 self.seen_byte_ranges.remove(&id);
1180 self.buffer_metadata.remove(&id);
1181 if let Some((request_id, _, _)) = self.semantic_tokens_in_flight.remove(&id) {
1182 self.pending_semantic_token_requests.remove(&request_id);
1183 }
1184 if let Some((request_id, _, _, _)) = self.semantic_tokens_range_in_flight.remove(&id) {
1185 self.pending_semantic_token_range_requests
1186 .remove(&request_id);
1187 }
1188 self.semantic_tokens_range_last_request.remove(&id);
1189 self.semantic_tokens_range_applied.remove(&id);
1190 self.semantic_tokens_full_debounce.remove(&id);
1191
1192 self.panel_ids.retain(|_, &mut buf_id| buf_id != id);
1195
1196 for view_state in self.split_view_states.values_mut() {
1198 view_state.remove_buffer(id);
1199 view_state.remove_from_history(id);
1200 }
1201
1202 if is_last_visible_buffer {
1204 self.focus_file_explorer();
1205 }
1206
1207 Ok(())
1208 }
1209
1210 pub fn switch_buffer(&mut self, id: BufferId) {
1212 if self.buffers.contains_key(&id) && id != self.active_buffer() {
1213 self.position_history.commit_pending_movement();
1215
1216 let current_state = self.active_state();
1218 let position = current_state.cursors.primary().position;
1219 let anchor = current_state.cursors.primary().anchor;
1220 self.position_history
1221 .record_movement(self.active_buffer(), position, anchor);
1222 self.position_history.commit_pending_movement();
1223
1224 self.set_active_buffer(id);
1225 }
1226 }
1227
1228 pub fn close_tab(&mut self) {
1232 let buffer_id = self.active_buffer();
1233 let active_split = self.split_manager.active_split();
1234
1235 let buffer_in_other_splits = self
1237 .split_view_states
1238 .iter()
1239 .filter(|(&split_id, view_state)| {
1240 split_id != active_split && view_state.has_buffer(buffer_id)
1241 })
1242 .count();
1243
1244 let current_split_tabs = self
1246 .split_view_states
1247 .get(&active_split)
1248 .map(|vs| vs.open_buffers.clone())
1249 .unwrap_or_default();
1250
1251 let is_last_viewport = buffer_in_other_splits == 0;
1254
1255 if is_last_viewport {
1256 let has_other_splits = self.split_manager.root().count_leaves() > 1;
1259 if current_split_tabs.len() <= 1 && has_other_splits {
1260 if self.active_state().buffer.is_modified() {
1262 let name = self.get_buffer_display_name(buffer_id);
1263 let save_key = t!("prompt.key.save").to_string();
1264 let discard_key = t!("prompt.key.discard").to_string();
1265 let cancel_key = t!("prompt.key.cancel").to_string();
1266 self.start_prompt(
1267 t!(
1268 "prompt.buffer_modified",
1269 name = name,
1270 save_key = save_key,
1271 discard_key = discard_key,
1272 cancel_key = cancel_key
1273 )
1274 .to_string(),
1275 PromptType::ConfirmCloseBuffer { buffer_id },
1276 );
1277 return;
1278 }
1279 let _ = self.close_buffer(buffer_id);
1281 self.close_active_split();
1282 return;
1283 }
1284
1285 if self.active_state().buffer.is_modified() {
1287 let name = self.get_buffer_display_name(buffer_id);
1289 let save_key = t!("prompt.key.save").to_string();
1290 let discard_key = t!("prompt.key.discard").to_string();
1291 let cancel_key = t!("prompt.key.cancel").to_string();
1292 self.start_prompt(
1293 t!(
1294 "prompt.buffer_modified",
1295 name = name,
1296 save_key = save_key,
1297 discard_key = discard_key,
1298 cancel_key = cancel_key
1299 )
1300 .to_string(),
1301 PromptType::ConfirmCloseBuffer { buffer_id },
1302 );
1303 } else if let Err(e) = self.close_buffer(buffer_id) {
1304 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
1305 } else {
1306 self.set_status_message(t!("buffer.tab_closed").to_string());
1307 }
1308 } else {
1309 if current_split_tabs.len() <= 1 {
1311 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
1314 self.terminal_mode = false;
1315 self.key_context = crate::input::keybindings::KeyContext::Normal;
1316 }
1317 self.close_active_split();
1318 return;
1319 }
1320
1321 let current_idx = current_split_tabs
1323 .iter()
1324 .position(|&id| id == buffer_id)
1325 .unwrap_or(0);
1326 let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
1327 let replacement_buffer = current_split_tabs[replacement_idx];
1328
1329 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
1331 self.terminal_mode = false;
1332 self.key_context = crate::input::keybindings::KeyContext::Normal;
1333 }
1334
1335 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
1337 view_state.remove_buffer(buffer_id);
1338 }
1339
1340 let _ = self
1342 .split_manager
1343 .set_split_buffer(active_split, replacement_buffer);
1344
1345 self.set_status_message(t!("buffer.tab_closed").to_string());
1346 }
1347 }
1348
1349 pub fn close_tab_in_split(&mut self, buffer_id: BufferId, split_id: SplitId) -> bool {
1353 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
1355 self.terminal_mode = false;
1356 self.key_context = crate::input::keybindings::KeyContext::Normal;
1357 }
1358
1359 let buffer_in_other_splits = self
1361 .split_view_states
1362 .iter()
1363 .filter(|(&sid, view_state)| sid != split_id && view_state.has_buffer(buffer_id))
1364 .count();
1365
1366 let split_tabs = self
1368 .split_view_states
1369 .get(&split_id)
1370 .map(|vs| vs.open_buffers.clone())
1371 .unwrap_or_default();
1372
1373 let is_last_viewport = buffer_in_other_splits == 0;
1374
1375 if is_last_viewport {
1376 if let Some(state) = self.buffers.get(&buffer_id) {
1378 if state.buffer.is_modified() {
1379 let name = self.get_buffer_display_name(buffer_id);
1381 let save_key = t!("prompt.key.save").to_string();
1382 let discard_key = t!("prompt.key.discard").to_string();
1383 let cancel_key = t!("prompt.key.cancel").to_string();
1384 self.start_prompt(
1385 t!(
1386 "prompt.buffer_modified",
1387 name = name,
1388 save_key = save_key,
1389 discard_key = discard_key,
1390 cancel_key = cancel_key
1391 )
1392 .to_string(),
1393 PromptType::ConfirmCloseBuffer { buffer_id },
1394 );
1395 return false;
1396 }
1397 }
1398 if let Err(e) = self.close_buffer(buffer_id) {
1399 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
1400 } else {
1401 self.set_status_message(t!("buffer.tab_closed").to_string());
1402 }
1403 } else {
1404 if split_tabs.len() <= 1 {
1406 self.handle_close_split(split_id);
1408 return true;
1409 }
1410
1411 let current_idx = split_tabs
1413 .iter()
1414 .position(|&id| id == buffer_id)
1415 .unwrap_or(0);
1416 let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
1417 let replacement_buffer = split_tabs[replacement_idx];
1418
1419 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
1421 view_state.remove_buffer(buffer_id);
1422 }
1423
1424 let _ = self
1426 .split_manager
1427 .set_split_buffer(split_id, replacement_buffer);
1428
1429 self.set_status_message(t!("buffer.tab_closed").to_string());
1430 }
1431 true
1432 }
1433
1434 pub fn close_other_tabs_in_split(&mut self, keep_buffer_id: BufferId, split_id: SplitId) {
1436 let split_tabs = self
1438 .split_view_states
1439 .get(&split_id)
1440 .map(|vs| vs.open_buffers.clone())
1441 .unwrap_or_default();
1442
1443 let tabs_to_close: Vec<_> = split_tabs
1445 .iter()
1446 .filter(|&&id| id != keep_buffer_id)
1447 .copied()
1448 .collect();
1449
1450 let mut closed = 0;
1451 let mut skipped_modified = 0;
1452 for buffer_id in tabs_to_close {
1453 if self.close_tab_in_split_silent(buffer_id, split_id) {
1454 closed += 1;
1455 } else {
1456 skipped_modified += 1;
1457 }
1458 }
1459
1460 let _ = self
1462 .split_manager
1463 .set_split_buffer(split_id, keep_buffer_id);
1464
1465 self.set_batch_close_status_message(closed, skipped_modified);
1466 }
1467
1468 pub fn close_tabs_to_right_in_split(&mut self, buffer_id: BufferId, split_id: SplitId) {
1470 let split_tabs = self
1472 .split_view_states
1473 .get(&split_id)
1474 .map(|vs| vs.open_buffers.clone())
1475 .unwrap_or_default();
1476
1477 let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
1479 return;
1480 };
1481
1482 let tabs_to_close: Vec<_> = split_tabs.iter().skip(target_idx + 1).copied().collect();
1484
1485 let mut closed = 0;
1486 let mut skipped_modified = 0;
1487 for buf_id in tabs_to_close {
1488 if self.close_tab_in_split_silent(buf_id, split_id) {
1489 closed += 1;
1490 } else {
1491 skipped_modified += 1;
1492 }
1493 }
1494
1495 self.set_batch_close_status_message(closed, skipped_modified);
1496 }
1497
1498 pub fn close_tabs_to_left_in_split(&mut self, buffer_id: BufferId, split_id: SplitId) {
1500 let split_tabs = self
1502 .split_view_states
1503 .get(&split_id)
1504 .map(|vs| vs.open_buffers.clone())
1505 .unwrap_or_default();
1506
1507 let Some(target_idx) = split_tabs.iter().position(|&id| id == buffer_id) else {
1509 return;
1510 };
1511
1512 let tabs_to_close: Vec<_> = split_tabs.iter().take(target_idx).copied().collect();
1514
1515 let mut closed = 0;
1516 let mut skipped_modified = 0;
1517 for buf_id in tabs_to_close {
1518 if self.close_tab_in_split_silent(buf_id, split_id) {
1519 closed += 1;
1520 } else {
1521 skipped_modified += 1;
1522 }
1523 }
1524
1525 self.set_batch_close_status_message(closed, skipped_modified);
1526 }
1527
1528 pub fn close_all_tabs_in_split(&mut self, split_id: SplitId) {
1530 let split_tabs = self
1532 .split_view_states
1533 .get(&split_id)
1534 .map(|vs| vs.open_buffers.clone())
1535 .unwrap_or_default();
1536
1537 let mut closed = 0;
1538 let mut skipped_modified = 0;
1539
1540 for buffer_id in split_tabs {
1542 if self.close_tab_in_split_silent(buffer_id, split_id) {
1543 closed += 1;
1544 } else {
1545 skipped_modified += 1;
1546 }
1547 }
1548
1549 self.set_batch_close_status_message(closed, skipped_modified);
1550 }
1551
1552 fn set_batch_close_status_message(&mut self, closed: usize, skipped_modified: usize) {
1554 let message = match (closed, skipped_modified) {
1555 (0, 0) => t!("buffer.no_tabs_to_close").to_string(),
1556 (0, n) => t!("buffer.skipped_modified", count = n).to_string(),
1557 (n, 0) => t!("buffer.closed_tabs", count = n).to_string(),
1558 (c, s) => t!("buffer.closed_tabs_skipped", closed = c, skipped = s).to_string(),
1559 };
1560 self.set_status_message(message);
1561 }
1562
1563 fn close_tab_in_split_silent(&mut self, buffer_id: BufferId, split_id: SplitId) -> bool {
1567 if self.terminal_mode && self.is_terminal_buffer(buffer_id) {
1569 self.terminal_mode = false;
1570 self.key_context = crate::input::keybindings::KeyContext::Normal;
1571 }
1572
1573 let buffer_in_other_splits = self
1575 .split_view_states
1576 .iter()
1577 .filter(|(&sid, view_state)| sid != split_id && view_state.has_buffer(buffer_id))
1578 .count();
1579
1580 let split_tabs = self
1582 .split_view_states
1583 .get(&split_id)
1584 .map(|vs| vs.open_buffers.clone())
1585 .unwrap_or_default();
1586
1587 let is_last_viewport = buffer_in_other_splits == 0;
1588
1589 if is_last_viewport {
1590 if let Some(state) = self.buffers.get(&buffer_id) {
1593 if state.buffer.is_modified() {
1594 return false;
1596 }
1597 }
1598 let _ = self.close_buffer(buffer_id);
1599 true
1600 } else {
1601 if split_tabs.len() <= 1 {
1603 self.handle_close_split(split_id);
1605 return true;
1606 }
1607
1608 let current_idx = split_tabs
1610 .iter()
1611 .position(|&id| id == buffer_id)
1612 .unwrap_or(0);
1613 let replacement_idx = if current_idx > 0 { current_idx - 1 } else { 1 };
1614 let replacement_buffer = split_tabs.get(replacement_idx).copied();
1615
1616 if let Some(view_state) = self.split_view_states.get_mut(&split_id) {
1618 view_state.remove_buffer(buffer_id);
1619 }
1620
1621 if let Some(replacement) = replacement_buffer {
1623 let _ = self.split_manager.set_split_buffer(split_id, replacement);
1624 }
1625 true
1626 }
1627 }
1628
1629 fn visible_buffers_for_active_split(&self) -> Vec<BufferId> {
1632 let active_split = self.split_manager.active_split();
1633 if let Some(view_state) = self.split_view_states.get(&active_split) {
1634 view_state
1635 .open_buffers
1636 .iter()
1637 .copied()
1638 .filter(|id| {
1639 !self
1640 .buffer_metadata
1641 .get(id)
1642 .map(|m| m.hidden_from_tabs)
1643 .unwrap_or(false)
1644 })
1645 .collect()
1646 } else {
1647 let mut all_ids: Vec<_> = self
1649 .buffers
1650 .keys()
1651 .copied()
1652 .filter(|id| {
1653 !self
1654 .buffer_metadata
1655 .get(id)
1656 .map(|m| m.hidden_from_tabs)
1657 .unwrap_or(false)
1658 })
1659 .collect();
1660 all_ids.sort_by_key(|id| id.0);
1661 all_ids
1662 }
1663 }
1664
1665 pub fn next_buffer(&mut self) {
1667 let ids = self.visible_buffers_for_active_split();
1668
1669 if ids.is_empty() {
1670 return;
1671 }
1672
1673 if let Some(idx) = ids.iter().position(|&id| id == self.active_buffer()) {
1674 let next_idx = (idx + 1) % ids.len();
1675 if ids[next_idx] != self.active_buffer() {
1676 self.position_history.commit_pending_movement();
1678
1679 let current_state = self.active_state();
1681 let position = current_state.cursors.primary().position;
1682 let anchor = current_state.cursors.primary().anchor;
1683 self.position_history
1684 .record_movement(self.active_buffer(), position, anchor);
1685 self.position_history.commit_pending_movement();
1686
1687 self.set_active_buffer(ids[next_idx]);
1688 }
1689 }
1690 }
1691
1692 pub fn prev_buffer(&mut self) {
1694 let ids = self.visible_buffers_for_active_split();
1695
1696 if ids.is_empty() {
1697 return;
1698 }
1699
1700 if let Some(idx) = ids.iter().position(|&id| id == self.active_buffer()) {
1701 let prev_idx = if idx == 0 { ids.len() - 1 } else { idx - 1 };
1702 if ids[prev_idx] != self.active_buffer() {
1703 self.position_history.commit_pending_movement();
1705
1706 let current_state = self.active_state();
1708 let position = current_state.cursors.primary().position;
1709 let anchor = current_state.cursors.primary().anchor;
1710 self.position_history
1711 .record_movement(self.active_buffer(), position, anchor);
1712 self.position_history.commit_pending_movement();
1713
1714 self.set_active_buffer(ids[prev_idx]);
1715 }
1716 }
1717 }
1718
1719 pub fn navigate_back(&mut self) {
1721 self.in_navigation = true;
1723
1724 self.position_history.commit_pending_movement();
1726
1727 if self.position_history.can_go_back() && !self.position_history.can_go_forward() {
1730 let current_state = self.active_state();
1731 let position = current_state.cursors.primary().position;
1732 let anchor = current_state.cursors.primary().anchor;
1733 self.position_history
1734 .record_movement(self.active_buffer(), position, anchor);
1735 self.position_history.commit_pending_movement();
1736 }
1737
1738 if let Some(entry) = self.position_history.back() {
1740 let target_buffer = entry.buffer_id;
1741 let target_position = entry.position;
1742 let target_anchor = entry.anchor;
1743
1744 if self.buffers.contains_key(&target_buffer) {
1746 self.set_active_buffer(target_buffer);
1747
1748 let state = self.active_state_mut();
1750 let cursor_id = state.cursors.primary_id();
1751 let old_position = state.cursors.primary().position;
1752 let old_anchor = state.cursors.primary().anchor;
1753 let old_sticky_column = state.cursors.primary().sticky_column;
1754 let event = Event::MoveCursor {
1755 cursor_id,
1756 old_position,
1757 new_position: target_position,
1758 old_anchor,
1759 new_anchor: target_anchor,
1760 old_sticky_column,
1761 new_sticky_column: 0, };
1763 state.apply(&event);
1764 }
1765 }
1766
1767 self.in_navigation = false;
1769 }
1770
1771 pub fn navigate_forward(&mut self) {
1773 self.in_navigation = true;
1775
1776 if let Some(entry) = self.position_history.forward() {
1777 let target_buffer = entry.buffer_id;
1778 let target_position = entry.position;
1779 let target_anchor = entry.anchor;
1780
1781 if self.buffers.contains_key(&target_buffer) {
1783 self.set_active_buffer(target_buffer);
1784
1785 let state = self.active_state_mut();
1787 let cursor_id = state.cursors.primary_id();
1788 let old_position = state.cursors.primary().position;
1789 let old_anchor = state.cursors.primary().anchor;
1790 let old_sticky_column = state.cursors.primary().sticky_column;
1791 let event = Event::MoveCursor {
1792 cursor_id,
1793 old_position,
1794 new_position: target_position,
1795 old_anchor,
1796 new_anchor: target_anchor,
1797 old_sticky_column,
1798 new_sticky_column: 0, };
1800 state.apply(&event);
1801 }
1802 }
1803
1804 self.in_navigation = false;
1806 }
1807
1808 pub fn get_mouse_hover_state(&self) -> Option<(usize, u16, u16)> {
1811 self.mouse_state
1812 .lsp_hover_state
1813 .map(|(pos, _, x, y)| (pos, x, y))
1814 }
1815
1816 pub fn has_transient_popup(&self) -> bool {
1818 self.active_state()
1819 .popups
1820 .top()
1821 .is_some_and(|p| p.transient)
1822 }
1823
1824 pub fn force_check_mouse_hover(&mut self) -> bool {
1827 if let Some((byte_pos, _, screen_x, screen_y)) = self.mouse_state.lsp_hover_state {
1829 if !self.mouse_state.lsp_hover_request_sent {
1830 self.mouse_state.lsp_hover_request_sent = true;
1831 self.mouse_hover_screen_position = Some((screen_x, screen_y));
1832 if let Err(e) = self.request_hover_at_position(byte_pos) {
1833 tracing::debug!("Failed to request hover: {}", e);
1834 return false;
1835 }
1836 return true;
1837 }
1838 }
1839 false
1840 }
1841}