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 {
495 let fs_manager = self.active_window().resources.fs_manager.clone();
496 let sender = self.async_bridge.as_ref().map(|b| b.sender());
497
498 runtime.spawn(async move {
499 let result = fs_manager.list_dir_with_metadata(path).await;
500 if let Some(sender) = sender {
501 #[allow(clippy::let_underscore_must_use)]
503 let _ = sender.send(AsyncMessage::FileOpenDirectoryLoaded(result));
504 }
505 });
506 } else {
507 if let Some(state) = &mut self.active_window_mut().file_open_state {
509 state.set_error("Async runtime not available".to_string());
510 }
511 }
512 }
513
514 pub(super) fn handle_file_open_directory_loaded(
516 &mut self,
517 result: std::io::Result<Vec<crate::services::fs::DirEntry>>,
518 ) {
519 match result {
520 Ok(entries) => {
521 if let Some(state) = &mut self.active_window_mut().file_open_state {
522 state.set_entries(entries);
523 }
524 let filter = self
526 .active_window()
527 .prompt
528 .as_ref()
529 .map(|p| p.input.clone())
530 .unwrap_or_default();
531 if !filter.is_empty() {
532 if let Some(state) = &mut self.active_window_mut().file_open_state {
533 state.apply_filter(&filter);
534 }
535 }
536 }
537 Err(e) => {
538 if let Some(state) = &mut self.active_window_mut().file_open_state {
539 state.set_error(e.to_string());
540 }
541 }
542 }
543 }
544
545 pub(super) fn load_file_open_shortcuts_async(&mut self) {
549 if let Some(ref runtime) = self.tokio_runtime {
550 let filesystem = self.authority().filesystem.clone();
551 let sender = self.async_bridge.as_ref().map(|b| b.sender());
552
553 runtime.spawn(async move {
554 let shortcuts = tokio::task::spawn_blocking(move || {
556 file_open::FileOpenState::build_shortcuts_async(&*filesystem)
557 })
558 .await
559 .unwrap_or_default();
560
561 if let Some(sender) = sender {
562 #[allow(clippy::let_underscore_must_use)]
564 let _ = sender.send(AsyncMessage::FileOpenShortcutsLoaded(shortcuts));
565 }
566 });
567 }
568 }
569
570 pub(super) fn handle_file_open_shortcuts_loaded(
572 &mut self,
573 shortcuts: Vec<file_open::NavigationShortcut>,
574 ) {
575 if let Some(state) = &mut self.active_window_mut().file_open_state {
576 state.merge_async_shortcuts(shortcuts);
577 }
578 }
579
580 pub(crate) fn cleanup_overlay_preview(&mut self) {
585 let (to_close, last_buffer): (Vec<crate::model::event::BufferId>, _) =
586 if let Some(state) = self.active_window_mut().overlay_preview_state.take() {
587 let last = state.buffer_id;
588 (state.loaded_buffers.into_iter().collect(), Some(last))
589 } else {
590 (Vec::new(), None)
591 };
592 if let Some(last) = last_buffer {
597 let ns = crate::view::overlay::OverlayNamespace::from_string(
598 "overlay-preview-search".to_string(),
599 );
600 if let Some(state) = self.active_window_mut().buffers.get_mut(&last) {
601 state.overlays.clear_namespace(&ns, &mut state.marker_list);
602 }
603 }
604 for buffer_id in to_close {
605 if let Err(e) = self.close_buffer(buffer_id) {
611 tracing::warn!("Failed to close overlay preview buffer: {}", e);
612 }
613 }
614 }
615
616 pub(crate) fn snapshot_prompt_results_for_grep(
622 &self,
623 prompt: &crate::view::prompt::Prompt,
624 ) -> Vec<crate::services::live_grep_state::GrepMatch> {
625 use crate::input::quick_open::parse_path_line_col;
626 prompt
632 .suggestions
633 .iter()
634 .filter(|s| !s.disabled)
635 .filter_map(|s| {
636 let from_text = parse_path_line_col(&s.text);
637 let (file, line, column) = if !from_text.0.is_empty() && from_text.1.is_some() {
638 from_text
639 } else if let Some(v) = s.value.as_deref() {
640 parse_path_line_col(v)
641 } else {
642 from_text
643 };
644 if file.is_empty() {
645 return None;
646 }
647 Some(crate::services::live_grep_state::GrepMatch {
648 file,
649 line: line.unwrap_or(1),
650 column: column.unwrap_or(1),
651 content: s.description.clone().unwrap_or_default(),
652 })
653 })
654 .collect()
655 }
656
657 pub fn cancel_prompt(&mut self) {
659 let theme_to_restore = if let Some(ref prompt) = self.active_window_mut().prompt {
661 if let PromptType::SelectTheme { original_theme } = &prompt.prompt_type {
662 Some(original_theme.clone())
663 } else {
664 None
665 }
666 } else {
667 None
668 };
669
670 let prompt_clone = self.active_window().prompt.clone();
675 if let Some(prompt) = prompt_clone {
676 let prompt = &prompt;
677 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
679 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
680 history.reset_navigation();
681 }
682 }
683 match &prompt.prompt_type {
684 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
685 self.active_window_mut().clear_search_highlights();
686 }
687 PromptType::Plugin { custom_type } => {
688 use crate::services::plugins::hooks::HookArgs;
690 self.plugin_manager.read().unwrap().run_hook(
691 "prompt_cancelled",
692 HookArgs::PromptCancelled {
693 prompt_type: custom_type.clone(),
694 input: prompt.input.clone(),
695 },
696 );
697 if custom_type == "live-grep" {
703 let cached = self.snapshot_prompt_results_for_grep(prompt);
704 if !prompt.input.is_empty() && !cached.is_empty() {
713 self.active_window_mut().live_grep_last_state =
714 Some(crate::services::live_grep_state::LiveGrepLastState {
715 query: prompt.input.clone(),
716 selected_index: prompt.selected_suggestion,
717 cached_results: Some(cached),
718 cached_at: Some(std::time::Instant::now()),
719 last_results_snapshot_id: None,
720 });
721 }
722 }
723 }
724 PromptType::LiveGrep => {
725 let cached = self.snapshot_prompt_results_for_grep(prompt);
726 if !prompt.input.is_empty() && !cached.is_empty() {
727 self.active_window_mut().live_grep_last_state =
728 Some(crate::services::live_grep_state::LiveGrepLastState {
729 query: prompt.input.clone(),
730 selected_index: prompt.selected_suggestion,
731 cached_results: Some(cached),
732 cached_at: Some(std::time::Instant::now()),
733 last_results_snapshot_id: None,
734 });
735 }
736 }
737 PromptType::LspRename { overlay_handle, .. } => {
738 let remove_overlay_event = crate::model::event::Event::RemoveOverlay {
740 handle: overlay_handle.clone(),
741 };
742 self.apply_event_to_active_buffer(&remove_overlay_event);
743 }
744 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
745 self.active_window_mut().file_open_state = None;
747 self.active_window_mut().file_browser_layout = None;
748
749 if matches!(prompt.prompt_type, PromptType::SaveFileAs)
754 && !self
755 .active_window_mut()
756 .pending_quit_unnamed_save
757 .is_empty()
758 {
759 self.active_window_mut().pending_quit_unnamed_save.clear();
760 self.set_status_message(t!("buffer.close_cancelled").to_string());
761 }
762 }
763 PromptType::AsyncPrompt => {
764 if let Some(callback_id) = self
766 .active_window_mut()
767 .pending_async_prompt_callback
768 .take()
769 {
770 self.plugin_manager
771 .read()
772 .unwrap()
773 .resolve_callback(callback_id, "null".to_string());
774 }
775 }
776 PromptType::QuickOpen => {
777 if let Some((provider, _)) = self.quick_open_registry.get_provider_for_input("")
779 {
780 if let Some(fp) = provider
781 .as_any()
782 .downcast_ref::<crate::input::quick_open::providers::FileProvider>(
783 ) {
784 fp.cancel_loading();
785 }
786 }
787 self.restore_goto_line_preview_snapshot();
790 }
791 PromptType::GotoLine => {
792 self.restore_goto_line_preview_snapshot();
795 }
796 _ => {}
797 }
798 }
799
800 let was_overlay = self
806 .active_window()
807 .prompt
808 .as_ref()
809 .is_some_and(|p| p.overlay);
810 if was_overlay {
811 self.cleanup_overlay_preview();
812 }
813
814 self.active_window_mut().prompt = None;
815 self.active_window_mut().pending_search_range = None;
816 self.active_window_mut().status_message = Some(t!("search.cancelled").to_string());
817
818 if let Some(original_theme) = theme_to_restore {
820 self.preview_theme(&original_theme);
821 }
822 }
823
824 pub fn handle_prompt_scroll(&mut self, delta: i32) -> bool {
827 if let Some(ref mut prompt) = self.active_window_mut().prompt {
828 if prompt.suggestions.is_empty() {
829 return false;
830 }
831
832 let current = prompt.selected_suggestion.unwrap_or(0);
833 let len = prompt.suggestions.len();
834
835 let new_selected = if delta < 0 {
838 current.saturating_sub((-delta) as usize)
840 } else {
841 (current + delta as usize).min(len.saturating_sub(1))
843 };
844
845 prompt.selected_suggestion = Some(new_selected);
846
847 if !matches!(prompt.prompt_type, PromptType::Plugin { .. }) {
849 if let Some(suggestion) = prompt.suggestions.get(new_selected) {
850 prompt.input = suggestion.get_value().to_string();
851 prompt.cursor_pos = prompt.input.len();
852 }
853 }
854
855 return true;
856 }
857 false
858 }
859
860 pub fn confirm_prompt(&mut self) -> Option<(String, PromptType, Option<usize>)> {
865 if let Some(prompt) = self.active_window_mut().prompt.take() {
866 let is_live_grep = match &prompt.prompt_type {
874 PromptType::LiveGrep => true,
875 PromptType::Plugin { custom_type } => custom_type == "live-grep",
876 _ => false,
877 };
878 if is_live_grep {
879 let cached = self.snapshot_prompt_results_for_grep(&prompt);
880 if !prompt.input.is_empty() && !cached.is_empty() {
881 self.active_window_mut().live_grep_last_state =
882 Some(crate::services::live_grep_state::LiveGrepLastState {
883 query: prompt.input.clone(),
884 selected_index: prompt.selected_suggestion,
885 cached_results: Some(cached),
886 cached_at: Some(std::time::Instant::now()),
887 last_results_snapshot_id: None,
888 });
889 }
890 }
891 if prompt.overlay {
896 self.cleanup_overlay_preview();
897 }
898 let selected_index = prompt.selected_suggestion;
899 let mut final_input = if prompt.sync_input_on_navigate {
901 prompt.input.clone()
904 } else if matches!(
905 prompt.prompt_type,
906 PromptType::OpenFile
907 | PromptType::SwitchProject
908 | PromptType::SaveFileAs
909 | PromptType::StopLspServer
910 | PromptType::RestartLspServer
911 | PromptType::SelectTheme { .. }
912 | PromptType::SelectLocale
913 | PromptType::SwitchToTab
914 | PromptType::SetLanguage
915 | PromptType::SetEncoding
916 | PromptType::SetLineEnding
917 | PromptType::Plugin { .. }
918 | PromptType::LiveGrep
925 ) {
926 if let Some(selected_idx) = prompt.selected_suggestion {
928 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
929 if suggestion.disabled {
931 self.set_status_message(
932 t!(
933 "error.command_not_available",
934 command = suggestion.text.clone()
935 )
936 .to_string(),
937 );
938 return None;
939 }
940 suggestion.get_value().to_string()
942 } else {
943 prompt.input.clone()
944 }
945 } else {
946 prompt.input.clone()
947 }
948 } else {
949 prompt.input.clone()
950 };
951
952 if matches!(
954 prompt.prompt_type,
955 PromptType::StopLspServer | PromptType::RestartLspServer
956 ) {
957 let is_valid = prompt
958 .suggestions
959 .iter()
960 .any(|s| s.text == final_input || s.get_value() == final_input);
961 if !is_valid {
962 self.active_window_mut().prompt = Some(prompt);
964 self.set_status_message(
965 t!("error.no_lsp_match", input = final_input.clone()).to_string(),
966 );
967 return None;
968 }
969 }
970
971 if matches!(prompt.prompt_type, PromptType::RemoveRuler) {
975 if prompt.input.is_empty() {
976 if let Some(selected_idx) = prompt.selected_suggestion {
978 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
979 final_input = suggestion.get_value().to_string();
980 }
981 } else {
982 self.active_window_mut().prompt = Some(prompt);
983 return None;
984 }
985 } else {
986 let typed = prompt.input.trim().to_string();
988 let matched = prompt.suggestions.iter().find(|s| s.get_value() == typed);
989 if let Some(suggestion) = matched {
990 final_input = suggestion.get_value().to_string();
991 } else {
992 self.active_window_mut().prompt = Some(prompt);
994 return None;
995 }
996 }
997 }
998
999 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
1001 let history = self.get_or_create_prompt_history(&key);
1002 history.push(final_input.clone());
1003 history.reset_navigation();
1004 }
1005
1006 Some((final_input, prompt.prompt_type, selected_index))
1007 } else {
1008 None
1009 }
1010 }
1011
1012 pub fn is_prompting(&self) -> bool {
1014 self.active_window().prompt.is_some()
1015 }
1016
1017 pub(super) fn get_or_create_prompt_history(
1019 &mut self,
1020 key: &str,
1021 ) -> &mut crate::input::input_history::InputHistory {
1022 self.active_window_mut()
1023 .prompt_histories
1024 .entry(key.to_string())
1025 .or_default()
1026 }
1027
1028 pub(super) fn get_prompt_history(
1030 &self,
1031 key: &str,
1032 ) -> Option<&crate::input::input_history::InputHistory> {
1033 self.active_window().prompt_histories.get(key)
1034 }
1035
1036 pub(super) fn prompt_type_to_history_key(
1038 prompt_type: &crate::view::prompt::PromptType,
1039 ) -> Option<String> {
1040 use crate::view::prompt::PromptType;
1041 match prompt_type {
1042 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
1043 Some("search".to_string())
1044 }
1045 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
1046 Some("replace".to_string())
1047 }
1048 PromptType::GotoLine => Some("goto_line".to_string()),
1049 PromptType::Plugin { custom_type } => Some(format!("plugin:{}", custom_type)),
1050 _ => None,
1051 }
1052 }
1053
1054 pub fn editor_mode(&self) -> Option<String> {
1057 self.active_window().editor_mode.clone()
1058 }
1059
1060 pub fn command_registry(&self) -> &Arc<RwLock<CommandRegistry>> {
1062 &self.command_registry
1063 }
1064
1065 pub fn plugin_manager(&self) -> std::sync::RwLockReadGuard<'_, PluginManager> {
1071 self.plugin_manager.read().unwrap()
1072 }
1073
1074 pub fn plugin_manager_mut(&mut self) -> std::sync::RwLockWriteGuard<'_, PluginManager> {
1076 self.plugin_manager.write().unwrap()
1077 }
1078
1079 pub fn file_explorer_is_focused(&self) -> bool {
1081 self.active_window().key_context == KeyContext::FileExplorer
1082 }
1083
1084 pub fn prompt_input(&self) -> Option<&str> {
1086 self.active_window()
1087 .prompt
1088 .as_ref()
1089 .map(|p| p.input.as_str())
1090 }
1091
1092 pub fn has_active_selection(&self) -> bool {
1094 self.active_cursors().primary().selection_range().is_some()
1095 }
1096
1097 pub fn prompt_mut(&mut self) -> Option<&mut Prompt> {
1099 self.active_window_mut().prompt.as_mut()
1100 }
1101
1102 pub fn set_status_message(&mut self, message: String) {
1106 self.active_window_mut().set_status_message(message);
1107 }
1108
1109 pub fn get_status_message(&self) -> Option<&String> {
1111 self.active_window()
1112 .plugin_status_message
1113 .as_ref()
1114 .or(self.active_window().status_message.as_ref())
1115 }
1116
1117 pub fn get_plugin_errors(&self) -> &[String] {
1120 &self.active_window().plugin_errors
1121 }
1122
1123 pub fn clear_plugin_errors(&mut self) {
1125 self.active_window_mut().plugin_errors.clear();
1126 }
1127
1128 pub fn update_prompt_suggestions(&mut self) {
1130 let (prompt_type, input) = if let Some(prompt) = &self.active_window_mut().prompt {
1132 (prompt.prompt_type.clone(), prompt.input.clone())
1133 } else {
1134 return;
1135 };
1136
1137 match prompt_type {
1138 PromptType::QuickOpen => {
1139 self.update_quick_open_suggestions(&input);
1141 }
1142 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
1143 self.update_search_highlights(&input);
1145 if let Some(history) = self.active_window_mut().prompt_histories.get_mut("search") {
1147 history.reset_navigation();
1148 }
1149 }
1150 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
1151 if let Some(history) = self.active_window_mut().prompt_histories.get_mut("replace")
1153 {
1154 history.reset_navigation();
1155 }
1156 }
1157 PromptType::GotoLine => {
1158 if let Some(history) = self
1160 .active_window_mut()
1161 .prompt_histories
1162 .get_mut("goto_line")
1163 {
1164 history.reset_navigation();
1165 }
1166 let target = match crate::input::quick_open::parse_goto_line_input(input.trim()) {
1171 Some(crate::input::quick_open::GotoLineTarget::Absolute(n)) => Some(n),
1172 _ => None,
1173 };
1174 self.apply_goto_line_preview(target);
1175 }
1176 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
1177 self.update_file_open_filter();
1179 }
1180 PromptType::Plugin { custom_type } => {
1181 let key = format!("plugin:{}", custom_type);
1183 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
1184 history.reset_navigation();
1185 }
1186 use crate::services::plugins::hooks::HookArgs;
1188 self.plugin_manager.read().unwrap().run_hook(
1189 "prompt_changed",
1190 HookArgs::PromptChanged {
1191 prompt_type: custom_type,
1192 input,
1193 },
1194 );
1195 if let Some(prompt) = &mut self.active_window_mut().prompt {
1200 prompt.filter_suggestions(false);
1201 }
1202 }
1203 PromptType::SwitchToTab
1204 | PromptType::SelectTheme { .. }
1205 | PromptType::StopLspServer
1206 | PromptType::RestartLspServer
1207 | PromptType::SetLanguage
1208 | PromptType::SetEncoding
1209 | PromptType::SetLineEnding => {
1210 if let Some(prompt) = &mut self.active_window_mut().prompt {
1211 prompt.filter_suggestions(false);
1212 }
1213 }
1214 PromptType::SelectLocale => {
1215 if let Some(prompt) = &mut self.active_window_mut().prompt {
1217 prompt.filter_suggestions(true);
1218 }
1219 }
1220 _ => {}
1221 }
1222 }
1223}
1224
1225impl Window {
1226 pub(crate) fn cancel_search_prompt_if_active(&mut self) {
1229 if let Some(ref prompt) = self.prompt {
1230 if matches!(
1231 prompt.prompt_type,
1232 PromptType::Search
1233 | PromptType::ReplaceSearch
1234 | PromptType::Replace { .. }
1235 | PromptType::QueryReplaceSearch
1236 | PromptType::QueryReplace { .. }
1237 | PromptType::QueryReplaceConfirm
1238 ) {
1239 self.prompt = None;
1240 self.interactive_replace_state = None;
1242 let ns = self.search_namespace.clone();
1244 let state = self.active_state_mut();
1245 state.overlays.clear_namespace(&ns, &mut state.marker_list);
1246 }
1247 }
1248 }
1249}