1use std::path::PathBuf;
8use std::sync::{Arc, RwLock};
9
10use rust_i18n::t;
11
12use crate::input::command_registry::CommandRegistry;
13use crate::input::commands::Suggestion;
14use crate::input::keybindings::KeyContext;
15use crate::input::quick_open::{BufferInfo, QuickOpenContext};
16use crate::services::async_bridge::AsyncMessage;
17use crate::services::plugins::PluginManager;
18use crate::view::prompt::{Prompt, PromptType};
19
20use super::file_open;
21use super::window::Window;
22use super::Editor;
23
24impl Editor {
25 pub fn start_prompt(&mut self, message: String, prompt_type: PromptType) {
29 self.start_prompt_with_suggestions(message, prompt_type, Vec::new());
30 }
31
32 pub(super) fn start_search_prompt(
37 &mut self,
38 message: String,
39 prompt_type: PromptType,
40 use_selection_range: bool,
41 ) {
42 self.active_window_mut().pending_search_range = None;
44
45 let selection_range = self.active_cursors().primary().selection_range();
46
47 let selected_text = if let Some(range) = selection_range.clone() {
48 let state = self.active_state_mut();
49 let text = state.get_text_range(range.start, range.end);
50 if !text.contains('\n') && !text.is_empty() {
51 Some(text)
52 } else {
53 None
54 }
55 } else {
56 None
57 };
58
59 if use_selection_range {
60 self.active_window_mut().pending_search_range = selection_range;
61 }
62
63 let from_history = selected_text.is_none();
65 let default_text = selected_text.or_else(|| {
66 self.get_prompt_history("search")
67 .and_then(|h| h.last().map(|s| s.to_string()))
68 });
69
70 self.start_prompt(message, prompt_type);
72
73 if let Some(text) = default_text {
75 if let Some(ref mut prompt) = self.active_window_mut().prompt {
76 prompt.set_input(text.clone());
77 prompt.selection_anchor = Some(0);
78 prompt.cursor_pos = text.len();
79 }
80 if from_history {
81 self.get_or_create_prompt_history("search").init_at_last();
82 }
83 self.update_search_highlights(&text);
84 }
85 }
86
87 pub fn start_prompt_with_suggestions(
89 &mut self,
90 message: String,
91 prompt_type: PromptType,
92 suggestions: Vec<Suggestion>,
93 ) {
94 self.active_window_mut().on_editor_focus_lost();
96
97 match prompt_type {
100 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
101 self.active_window_mut().clear_search_highlights();
102 }
103 _ => {}
104 }
105
106 let needs_suggestions = matches!(
108 prompt_type,
109 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
110 );
111
112 self.active_window_mut().prompt =
113 Some(Prompt::with_suggestions(message, prompt_type, suggestions));
114
115 if needs_suggestions {
117 self.update_prompt_suggestions();
118 }
119 }
120
121 pub fn start_prompt_with_initial_text(
123 &mut self,
124 message: String,
125 prompt_type: PromptType,
126 initial_text: String,
127 ) {
128 self.active_window_mut().on_editor_focus_lost();
130
131 self.active_window_mut().prompt = Some(Prompt::with_initial_text(
132 message,
133 prompt_type,
134 initial_text,
135 ));
136 }
137
138 pub fn start_quick_open(&mut self) {
140 self.start_quick_open_with_prefix(">");
141 }
142
143 pub fn start_quick_open_with_prefix(&mut self, prefix: &str) {
145 self.active_window_mut().on_editor_focus_lost();
146 self.active_window_mut().status_message = None;
147 self.active_window_mut().goto_line_preview = None;
148
149 let mut prompt = Prompt::with_suggestions(String::new(), PromptType::QuickOpen, vec![]);
150 prompt.input = prefix.to_string();
151 prompt.cursor_pos = prefix.len();
152 self.active_window_mut().prompt = Some(prompt);
153
154 self.update_quick_open_suggestions(prefix);
155 }
156
157 pub(super) fn build_quick_open_context(&self) -> QuickOpenContext {
159 let open_buffers = self
160 .buffers()
161 .iter()
162 .filter_map(|(buffer_id, state)| {
163 let path = state.buffer.file_path()?;
164 let name = path
165 .file_name()
166 .map(|n| n.to_string_lossy().to_string())
167 .unwrap_or_else(|| format!("Buffer {}", buffer_id.0));
168 Some(BufferInfo {
169 id: buffer_id.0,
170 path: path.display().to_string(),
171 name,
172 modified: state.buffer.is_modified(),
173 })
174 })
175 .collect();
176
177 let has_lsp_config = {
178 let language = self
179 .buffers()
180 .get(&self.active_buffer())
181 .map(|s| s.language.as_str());
182 language
183 .and_then(|lang| self.lsp().and_then(|lsp| lsp.get_config(lang)))
184 .is_some()
185 };
186
187 QuickOpenContext {
188 cwd: self.working_dir().display().to_string(),
189 open_buffers,
190 active_buffer_id: self.active_buffer().0,
191 active_buffer_path: self
192 .active_state()
193 .buffer
194 .file_path()
195 .map(|p| p.display().to_string()),
196 has_selection: self.has_active_selection(),
197 key_context: self.active_window().key_context.clone(),
198 custom_contexts: self.active_window().active_custom_contexts.clone(),
199 buffer_mode: self
200 .active_window()
201 .buffer_metadata
202 .get(&self.active_buffer())
203 .and_then(|m| m.virtual_mode())
204 .map(|s| s.to_string()),
205 has_lsp_config,
206 relative_line_numbers: self.config.editor.relative_line_numbers,
207 }
208 }
209
210 pub(super) fn update_quick_open_suggestions(&mut self, input: &str) {
212 let context = self.build_quick_open_context();
213 let suggestions = if let Some((provider, query)) =
214 self.quick_open_registry.get_provider_for_input(input)
215 {
216 provider.suggestions(query, &context)
217 } else {
218 vec![]
219 };
220
221 if let Some(prompt) = &mut self.active_window_mut().prompt {
222 prompt.suggestions = suggestions;
223 prompt.selected_suggestion = if prompt.suggestions.is_empty() {
224 None
225 } else {
226 Some(0)
227 };
228 }
229
230 let input = input.trim();
238 let target = Self::parse_quick_open_goto_line_target(input);
239 self.apply_goto_line_preview(target);
240 }
241
242 pub(super) fn parse_quick_open_goto_line_target(input: &str) -> Option<usize> {
245 let rest = input.strip_prefix(':')?;
246 match crate::input::quick_open::parse_goto_line_input(rest) {
247 Some(crate::input::quick_open::GotoLineTarget::Absolute(n)) => Some(n),
248 _ => None,
249 }
250 }
251
252 pub(super) fn apply_goto_line_preview(&mut self, target_line: Option<usize>) {
259 if let Some(line) = target_line {
260 self.save_goto_line_preview_snapshot();
261 self.goto_line_col(line, None);
262 let new_position = self.active_cursors().primary().position;
265 if let Some(snap) = self.active_window_mut().goto_line_preview.as_mut() {
266 snap.last_jump_position = new_position;
267 }
268 } else {
269 self.restore_goto_line_preview_snapshot();
270 }
271 }
272
273 pub(super) fn save_goto_line_preview_snapshot(&mut self) {
277 if self.active_window_mut().goto_line_preview.is_some() {
278 return;
279 }
280
281 let buffer_id = self.active_buffer();
282 let split_id = self
283 .windows
284 .get(&self.active_window)
285 .and_then(|w| w.buffers.splits())
286 .map(|(mgr, _)| mgr)
287 .expect("active window must have a populated split layout")
288 .active_split();
289 let (cursor_id, position, anchor, sticky_column) = {
290 let cursors = self.active_cursors();
291 let primary = cursors.primary();
292 (
293 cursors.primary_id(),
294 primary.position,
295 primary.anchor,
296 primary.sticky_column,
297 )
298 };
299 let (viewport_top_byte, viewport_top_view_line_offset, viewport_left_column) = {
300 let vp = self.active_viewport();
301 (vp.top_byte, vp.top_view_line_offset, vp.left_column)
302 };
303
304 self.active_window_mut().goto_line_preview = Some(super::GotoLinePreviewSnapshot {
305 buffer_id,
306 split_id,
307 cursor_id,
308 position,
309 anchor,
310 sticky_column,
311 viewport_top_byte,
312 viewport_top_view_line_offset,
313 viewport_left_column,
314 last_jump_position: position,
318 });
319 }
320
321 pub(super) fn restore_goto_line_preview_snapshot(&mut self) {
330 let Some(snap) = self.active_window_mut().goto_line_preview.take() else {
331 return;
332 };
333
334 if self.active_buffer() != snap.buffer_id
337 || self
338 .windows
339 .get(&self.active_window)
340 .and_then(|w| w.buffers.splits())
341 .map(|(mgr, _)| mgr)
342 .expect("active window must have a populated split layout")
343 .active_split()
344 != snap.split_id
345 {
346 return;
347 }
348
349 let cursors = self.active_cursors();
350 let current = cursors.primary();
351
352 if current.position != snap.last_jump_position {
356 return;
357 }
358 let event = crate::model::event::Event::MoveCursor {
359 cursor_id: snap.cursor_id,
360 old_position: current.position,
361 new_position: snap.position,
362 old_anchor: current.anchor,
363 new_anchor: snap.anchor,
364 old_sticky_column: current.sticky_column,
365 new_sticky_column: snap.sticky_column,
366 };
367
368 self.active_window_mut()
369 .apply_event_to_buffer(snap.buffer_id, snap.split_id, &event);
370
371 if let Some(view_state) = self
372 .active_window_mut()
373 .buffers
374 .splits_mut()
375 .expect("active window must have a populated split layout")
376 .1
377 .get_mut(&snap.split_id)
378 {
379 let vp = &mut view_state.viewport;
380 vp.top_byte = snap.viewport_top_byte;
381 vp.top_view_line_offset = snap.viewport_top_view_line_offset;
382 vp.left_column = snap.viewport_left_column;
383 vp.set_skip_ensure_visible();
386 }
387 }
388
389 pub(super) fn prefill_open_file_prompt(&mut self) {
391 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
395 if prompt.prompt_type == PromptType::OpenFile {
396 prompt.input.clear();
397 prompt.cursor_pos = 0;
398 prompt.selection_anchor = None;
399 }
400 }
401 }
402
403 pub(super) fn init_file_open_state(&mut self) {
409 let buffer_id = self.active_buffer();
411
412 let initial_dir = if self.active_window().is_terminal_buffer(buffer_id) {
415 self.active_window()
416 .get_terminal_id(buffer_id)
417 .and_then(|tid| self.active_window().terminal_manager.get(tid))
418 .and_then(|handle| handle.cwd())
419 .unwrap_or_else(|| self.working_dir().to_path_buf())
420 } else {
421 self.active_state()
422 .buffer
423 .file_path()
424 .and_then(|path| path.parent())
425 .map(|p| p.to_path_buf())
426 .unwrap_or_else(|| self.working_dir().to_path_buf())
427 };
428
429 let show_hidden = self.config.file_browser.show_hidden;
431 self.active_window_mut().file_open_state = Some(file_open::FileOpenState::new(
432 initial_dir.clone(),
433 show_hidden,
434 self.authority.filesystem.clone(),
435 ));
436
437 self.load_file_open_directory(initial_dir);
439 self.load_file_open_shortcuts_async();
440 }
441
442 pub(super) fn init_folder_open_state(&mut self) {
447 let initial_dir = self.working_dir().to_path_buf();
449
450 let show_hidden = self.config.file_browser.show_hidden;
452 self.active_window_mut().file_open_state = Some(file_open::FileOpenState::new(
453 initial_dir.clone(),
454 show_hidden,
455 self.authority.filesystem.clone(),
456 ));
457
458 self.load_file_open_directory(initial_dir);
460 self.load_file_open_shortcuts_async();
461 }
462
463 pub fn change_working_dir(&mut self, new_path: PathBuf) {
473 let new_path = new_path.canonicalize().unwrap_or(new_path);
475
476 self.request_restart(new_path);
479 }
480
481 pub(super) fn load_file_open_directory(&mut self, path: PathBuf) {
483 if let Some(state) = &mut self.active_window_mut().file_open_state {
485 state.current_dir = path.clone();
486 state.loading = true;
487 state.error = None;
488 state.update_shortcuts();
489 }
490
491 if let Some(ref runtime) = self.tokio_runtime {
493 let fs_manager = self.fs_manager.clone();
494 let sender = self.async_bridge.as_ref().map(|b| b.sender());
495
496 runtime.spawn(async move {
497 let result = fs_manager.list_dir_with_metadata(path).await;
498 if let Some(sender) = sender {
499 #[allow(clippy::let_underscore_must_use)]
501 let _ = sender.send(AsyncMessage::FileOpenDirectoryLoaded(result));
502 }
503 });
504 } else {
505 if let Some(state) = &mut self.active_window_mut().file_open_state {
507 state.set_error("Async runtime not available".to_string());
508 }
509 }
510 }
511
512 pub(super) fn handle_file_open_directory_loaded(
514 &mut self,
515 result: std::io::Result<Vec<crate::services::fs::DirEntry>>,
516 ) {
517 match result {
518 Ok(entries) => {
519 if let Some(state) = &mut self.active_window_mut().file_open_state {
520 state.set_entries(entries);
521 }
522 let filter = self
524 .active_window()
525 .prompt
526 .as_ref()
527 .map(|p| p.input.clone())
528 .unwrap_or_default();
529 if !filter.is_empty() {
530 if let Some(state) = &mut self.active_window_mut().file_open_state {
531 state.apply_filter(&filter);
532 }
533 }
534 }
535 Err(e) => {
536 if let Some(state) = &mut self.active_window_mut().file_open_state {
537 state.set_error(e.to_string());
538 }
539 }
540 }
541 }
542
543 pub(super) fn load_file_open_shortcuts_async(&mut self) {
547 if let Some(ref runtime) = self.tokio_runtime {
548 let filesystem = self.authority.filesystem.clone();
549 let sender = self.async_bridge.as_ref().map(|b| b.sender());
550
551 runtime.spawn(async move {
552 let shortcuts = tokio::task::spawn_blocking(move || {
554 file_open::FileOpenState::build_shortcuts_async(&*filesystem)
555 })
556 .await
557 .unwrap_or_default();
558
559 if let Some(sender) = sender {
560 #[allow(clippy::let_underscore_must_use)]
562 let _ = sender.send(AsyncMessage::FileOpenShortcutsLoaded(shortcuts));
563 }
564 });
565 }
566 }
567
568 pub(super) fn handle_file_open_shortcuts_loaded(
570 &mut self,
571 shortcuts: Vec<file_open::NavigationShortcut>,
572 ) {
573 if let Some(state) = &mut self.active_window_mut().file_open_state {
574 state.merge_async_shortcuts(shortcuts);
575 }
576 }
577
578 pub(crate) fn cleanup_overlay_preview(&mut self) {
583 let (to_close, last_buffer): (Vec<crate::model::event::BufferId>, _) =
584 if let Some(state) = self.active_window_mut().overlay_preview_state.take() {
585 let last = state.buffer_id;
586 (state.loaded_buffers.into_iter().collect(), Some(last))
587 } else {
588 (Vec::new(), None)
589 };
590 if let Some(last) = last_buffer {
595 let ns = crate::view::overlay::OverlayNamespace::from_string(
596 "overlay-preview-search".to_string(),
597 );
598 if let Some(state) = self.active_window_mut().buffers.get_mut(&last) {
599 state.overlays.clear_namespace(&ns, &mut state.marker_list);
600 }
601 }
602 for buffer_id in to_close {
603 if let Err(e) = self.close_buffer(buffer_id) {
609 tracing::warn!("Failed to close overlay preview buffer: {}", e);
610 }
611 }
612 }
613
614 pub(crate) fn snapshot_prompt_results_for_grep(
620 &self,
621 prompt: &crate::view::prompt::Prompt,
622 ) -> Vec<crate::services::live_grep_state::GrepMatch> {
623 use crate::input::quick_open::parse_path_line_col;
624 prompt
630 .suggestions
631 .iter()
632 .filter(|s| !s.disabled)
633 .filter_map(|s| {
634 let from_text = parse_path_line_col(&s.text);
635 let (file, line, column) = if !from_text.0.is_empty() && from_text.1.is_some() {
636 from_text
637 } else if let Some(v) = s.value.as_deref() {
638 parse_path_line_col(v)
639 } else {
640 from_text
641 };
642 if file.is_empty() {
643 return None;
644 }
645 Some(crate::services::live_grep_state::GrepMatch {
646 file,
647 line: line.unwrap_or(1),
648 column: column.unwrap_or(1),
649 content: s.description.clone().unwrap_or_default(),
650 })
651 })
652 .collect()
653 }
654
655 pub fn cancel_prompt(&mut self) {
657 let theme_to_restore = if let Some(ref prompt) = self.active_window_mut().prompt {
659 if let PromptType::SelectTheme { original_theme } = &prompt.prompt_type {
660 Some(original_theme.clone())
661 } else {
662 None
663 }
664 } else {
665 None
666 };
667
668 let prompt_clone = self.active_window().prompt.clone();
673 if let Some(prompt) = prompt_clone {
674 let prompt = &prompt;
675 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
677 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
678 history.reset_navigation();
679 }
680 }
681 match &prompt.prompt_type {
682 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
683 self.active_window_mut().clear_search_highlights();
684 }
685 PromptType::Plugin { custom_type } => {
686 use crate::services::plugins::hooks::HookArgs;
688 self.plugin_manager.read().unwrap().run_hook(
689 "prompt_cancelled",
690 HookArgs::PromptCancelled {
691 prompt_type: custom_type.clone(),
692 input: prompt.input.clone(),
693 },
694 );
695 if custom_type == "live-grep" {
701 let cached = self.snapshot_prompt_results_for_grep(prompt);
702 if !prompt.input.is_empty() && !cached.is_empty() {
711 self.active_window_mut().live_grep_last_state =
712 Some(crate::services::live_grep_state::LiveGrepLastState {
713 query: prompt.input.clone(),
714 selected_index: prompt.selected_suggestion,
715 cached_results: Some(cached),
716 cached_at: Some(std::time::Instant::now()),
717 last_results_snapshot_id: None,
718 });
719 }
720 }
721 }
722 PromptType::LiveGrep => {
723 let cached = self.snapshot_prompt_results_for_grep(prompt);
724 if !prompt.input.is_empty() && !cached.is_empty() {
725 self.active_window_mut().live_grep_last_state =
726 Some(crate::services::live_grep_state::LiveGrepLastState {
727 query: prompt.input.clone(),
728 selected_index: prompt.selected_suggestion,
729 cached_results: Some(cached),
730 cached_at: Some(std::time::Instant::now()),
731 last_results_snapshot_id: None,
732 });
733 }
734 }
735 PromptType::LspRename { overlay_handle, .. } => {
736 let remove_overlay_event = crate::model::event::Event::RemoveOverlay {
738 handle: overlay_handle.clone(),
739 };
740 self.apply_event_to_active_buffer(&remove_overlay_event);
741 }
742 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
743 self.active_window_mut().file_open_state = None;
745 self.active_window_mut().file_browser_layout = None;
746
747 if matches!(prompt.prompt_type, PromptType::SaveFileAs)
752 && !self
753 .active_window_mut()
754 .pending_quit_unnamed_save
755 .is_empty()
756 {
757 self.active_window_mut().pending_quit_unnamed_save.clear();
758 self.set_status_message(t!("buffer.close_cancelled").to_string());
759 }
760 }
761 PromptType::AsyncPrompt => {
762 if let Some(callback_id) = self
764 .active_window_mut()
765 .pending_async_prompt_callback
766 .take()
767 {
768 self.plugin_manager
769 .read()
770 .unwrap()
771 .resolve_callback(callback_id, "null".to_string());
772 }
773 }
774 PromptType::QuickOpen => {
775 if let Some((provider, _)) = self.quick_open_registry.get_provider_for_input("")
777 {
778 if let Some(fp) = provider
779 .as_any()
780 .downcast_ref::<crate::input::quick_open::providers::FileProvider>(
781 ) {
782 fp.cancel_loading();
783 }
784 }
785 self.restore_goto_line_preview_snapshot();
788 }
789 PromptType::GotoLine => {
790 self.restore_goto_line_preview_snapshot();
793 }
794 _ => {}
795 }
796 }
797
798 let was_overlay = self
804 .active_window()
805 .prompt
806 .as_ref()
807 .is_some_and(|p| p.overlay);
808 if was_overlay {
809 self.cleanup_overlay_preview();
810 }
811
812 self.active_window_mut().prompt = None;
813 self.active_window_mut().pending_search_range = None;
814 self.active_window_mut().status_message = Some(t!("search.cancelled").to_string());
815
816 if let Some(original_theme) = theme_to_restore {
818 self.preview_theme(&original_theme);
819 }
820 }
821
822 pub fn handle_prompt_scroll(&mut self, delta: i32) -> bool {
825 if let Some(ref mut prompt) = self.active_window_mut().prompt {
826 if prompt.suggestions.is_empty() {
827 return false;
828 }
829
830 let current = prompt.selected_suggestion.unwrap_or(0);
831 let len = prompt.suggestions.len();
832
833 let new_selected = if delta < 0 {
836 current.saturating_sub((-delta) as usize)
838 } else {
839 (current + delta as usize).min(len.saturating_sub(1))
841 };
842
843 prompt.selected_suggestion = Some(new_selected);
844
845 if !matches!(prompt.prompt_type, PromptType::Plugin { .. }) {
847 if let Some(suggestion) = prompt.suggestions.get(new_selected) {
848 prompt.input = suggestion.get_value().to_string();
849 prompt.cursor_pos = prompt.input.len();
850 }
851 }
852
853 return true;
854 }
855 false
856 }
857
858 pub fn confirm_prompt(&mut self) -> Option<(String, PromptType, Option<usize>)> {
863 if let Some(prompt) = self.active_window_mut().prompt.take() {
864 let is_live_grep = match &prompt.prompt_type {
872 PromptType::LiveGrep => true,
873 PromptType::Plugin { custom_type } => custom_type == "live-grep",
874 _ => false,
875 };
876 if is_live_grep {
877 let cached = self.snapshot_prompt_results_for_grep(&prompt);
878 if !prompt.input.is_empty() && !cached.is_empty() {
879 self.active_window_mut().live_grep_last_state =
880 Some(crate::services::live_grep_state::LiveGrepLastState {
881 query: prompt.input.clone(),
882 selected_index: prompt.selected_suggestion,
883 cached_results: Some(cached),
884 cached_at: Some(std::time::Instant::now()),
885 last_results_snapshot_id: None,
886 });
887 }
888 }
889 if prompt.overlay {
894 self.cleanup_overlay_preview();
895 }
896 let selected_index = prompt.selected_suggestion;
897 let mut final_input = if prompt.sync_input_on_navigate {
899 prompt.input.clone()
902 } else if matches!(
903 prompt.prompt_type,
904 PromptType::OpenFile
905 | PromptType::SwitchProject
906 | PromptType::SaveFileAs
907 | PromptType::StopLspServer
908 | PromptType::RestartLspServer
909 | PromptType::SelectTheme { .. }
910 | PromptType::SelectLocale
911 | PromptType::SwitchToTab
912 | PromptType::SetLanguage
913 | PromptType::SetEncoding
914 | PromptType::SetLineEnding
915 | PromptType::Plugin { .. }
916 | PromptType::LiveGrep
923 ) {
924 if let Some(selected_idx) = prompt.selected_suggestion {
926 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
927 if suggestion.disabled {
929 self.set_status_message(
930 t!(
931 "error.command_not_available",
932 command = suggestion.text.clone()
933 )
934 .to_string(),
935 );
936 return None;
937 }
938 suggestion.get_value().to_string()
940 } else {
941 prompt.input.clone()
942 }
943 } else {
944 prompt.input.clone()
945 }
946 } else {
947 prompt.input.clone()
948 };
949
950 if matches!(
952 prompt.prompt_type,
953 PromptType::StopLspServer | PromptType::RestartLspServer
954 ) {
955 let is_valid = prompt
956 .suggestions
957 .iter()
958 .any(|s| s.text == final_input || s.get_value() == final_input);
959 if !is_valid {
960 self.active_window_mut().prompt = Some(prompt);
962 self.set_status_message(
963 t!("error.no_lsp_match", input = final_input.clone()).to_string(),
964 );
965 return None;
966 }
967 }
968
969 if matches!(prompt.prompt_type, PromptType::RemoveRuler) {
973 if prompt.input.is_empty() {
974 if let Some(selected_idx) = prompt.selected_suggestion {
976 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
977 final_input = suggestion.get_value().to_string();
978 }
979 } else {
980 self.active_window_mut().prompt = Some(prompt);
981 return None;
982 }
983 } else {
984 let typed = prompt.input.trim().to_string();
986 let matched = prompt.suggestions.iter().find(|s| s.get_value() == typed);
987 if let Some(suggestion) = matched {
988 final_input = suggestion.get_value().to_string();
989 } else {
990 self.active_window_mut().prompt = Some(prompt);
992 return None;
993 }
994 }
995 }
996
997 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
999 let history = self.get_or_create_prompt_history(&key);
1000 history.push(final_input.clone());
1001 history.reset_navigation();
1002 }
1003
1004 Some((final_input, prompt.prompt_type, selected_index))
1005 } else {
1006 None
1007 }
1008 }
1009
1010 pub fn is_prompting(&self) -> bool {
1012 self.active_window().prompt.is_some()
1013 }
1014
1015 pub(super) fn get_or_create_prompt_history(
1017 &mut self,
1018 key: &str,
1019 ) -> &mut crate::input::input_history::InputHistory {
1020 self.active_window_mut()
1021 .prompt_histories
1022 .entry(key.to_string())
1023 .or_default()
1024 }
1025
1026 pub(super) fn get_prompt_history(
1028 &self,
1029 key: &str,
1030 ) -> Option<&crate::input::input_history::InputHistory> {
1031 self.active_window().prompt_histories.get(key)
1032 }
1033
1034 pub(super) fn prompt_type_to_history_key(
1036 prompt_type: &crate::view::prompt::PromptType,
1037 ) -> Option<String> {
1038 use crate::view::prompt::PromptType;
1039 match prompt_type {
1040 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
1041 Some("search".to_string())
1042 }
1043 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
1044 Some("replace".to_string())
1045 }
1046 PromptType::GotoLine => Some("goto_line".to_string()),
1047 PromptType::Plugin { custom_type } => Some(format!("plugin:{}", custom_type)),
1048 _ => None,
1049 }
1050 }
1051
1052 pub fn editor_mode(&self) -> Option<String> {
1055 self.active_window().editor_mode.clone()
1056 }
1057
1058 pub fn command_registry(&self) -> &Arc<RwLock<CommandRegistry>> {
1060 &self.command_registry
1061 }
1062
1063 pub fn plugin_manager(&self) -> std::sync::RwLockReadGuard<'_, PluginManager> {
1069 self.plugin_manager.read().unwrap()
1070 }
1071
1072 pub fn plugin_manager_mut(&mut self) -> std::sync::RwLockWriteGuard<'_, PluginManager> {
1074 self.plugin_manager.write().unwrap()
1075 }
1076
1077 pub fn file_explorer_is_focused(&self) -> bool {
1079 self.active_window().key_context == KeyContext::FileExplorer
1080 }
1081
1082 pub fn prompt_input(&self) -> Option<&str> {
1084 self.active_window()
1085 .prompt
1086 .as_ref()
1087 .map(|p| p.input.as_str())
1088 }
1089
1090 pub fn has_active_selection(&self) -> bool {
1092 self.active_cursors().primary().selection_range().is_some()
1093 }
1094
1095 pub fn prompt_mut(&mut self) -> Option<&mut Prompt> {
1097 self.active_window_mut().prompt.as_mut()
1098 }
1099
1100 pub fn set_status_message(&mut self, message: String) {
1104 self.active_window_mut().set_status_message(message);
1105 }
1106
1107 pub fn get_status_message(&self) -> Option<&String> {
1109 self.active_window()
1110 .plugin_status_message
1111 .as_ref()
1112 .or(self.active_window().status_message.as_ref())
1113 }
1114
1115 pub fn get_plugin_errors(&self) -> &[String] {
1118 &self.active_window().plugin_errors
1119 }
1120
1121 pub fn clear_plugin_errors(&mut self) {
1123 self.active_window_mut().plugin_errors.clear();
1124 }
1125
1126 pub fn update_prompt_suggestions(&mut self) {
1128 let (prompt_type, input) = if let Some(prompt) = &self.active_window_mut().prompt {
1130 (prompt.prompt_type.clone(), prompt.input.clone())
1131 } else {
1132 return;
1133 };
1134
1135 match prompt_type {
1136 PromptType::QuickOpen => {
1137 self.update_quick_open_suggestions(&input);
1139 }
1140 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
1141 self.update_search_highlights(&input);
1143 if let Some(history) = self.active_window_mut().prompt_histories.get_mut("search") {
1145 history.reset_navigation();
1146 }
1147 }
1148 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
1149 if let Some(history) = self.active_window_mut().prompt_histories.get_mut("replace")
1151 {
1152 history.reset_navigation();
1153 }
1154 }
1155 PromptType::GotoLine => {
1156 if let Some(history) = self
1158 .active_window_mut()
1159 .prompt_histories
1160 .get_mut("goto_line")
1161 {
1162 history.reset_navigation();
1163 }
1164 let target = match crate::input::quick_open::parse_goto_line_input(input.trim()) {
1169 Some(crate::input::quick_open::GotoLineTarget::Absolute(n)) => Some(n),
1170 _ => None,
1171 };
1172 self.apply_goto_line_preview(target);
1173 }
1174 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
1175 self.update_file_open_filter();
1177 }
1178 PromptType::Plugin { custom_type } => {
1179 let key = format!("plugin:{}", custom_type);
1181 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
1182 history.reset_navigation();
1183 }
1184 use crate::services::plugins::hooks::HookArgs;
1186 self.plugin_manager.read().unwrap().run_hook(
1187 "prompt_changed",
1188 HookArgs::PromptChanged {
1189 prompt_type: custom_type,
1190 input,
1191 },
1192 );
1193 if let Some(prompt) = &mut self.active_window_mut().prompt {
1198 prompt.filter_suggestions(false);
1199 }
1200 }
1201 PromptType::SwitchToTab
1202 | PromptType::SelectTheme { .. }
1203 | PromptType::StopLspServer
1204 | PromptType::RestartLspServer
1205 | PromptType::SetLanguage
1206 | PromptType::SetEncoding
1207 | PromptType::SetLineEnding => {
1208 if let Some(prompt) = &mut self.active_window_mut().prompt {
1209 prompt.filter_suggestions(false);
1210 }
1211 }
1212 PromptType::SelectLocale => {
1213 if let Some(prompt) = &mut self.active_window_mut().prompt {
1215 prompt.filter_suggestions(true);
1216 }
1217 }
1218 _ => {}
1219 }
1220 }
1221}
1222
1223impl Window {
1224 pub(crate) fn cancel_search_prompt_if_active(&mut self) {
1227 if let Some(ref prompt) = self.prompt {
1228 if matches!(
1229 prompt.prompt_type,
1230 PromptType::Search
1231 | PromptType::ReplaceSearch
1232 | PromptType::Replace { .. }
1233 | PromptType::QueryReplaceSearch
1234 | PromptType::QueryReplace { .. }
1235 | PromptType::QueryReplaceConfirm
1236 ) {
1237 self.prompt = None;
1238 self.interactive_replace_state = None;
1240 let ns = self.search_namespace.clone();
1242 let state = self.active_state_mut();
1243 state.overlays.clear_namespace(&ns, &mut state.marker_list);
1244 }
1245 }
1246 }
1247}