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: Vec<crate::model::event::BufferId> =
584 if let Some(state) = self.active_window_mut().overlay_preview_state.take() {
585 state.loaded_buffers.into_iter().collect()
586 } else {
587 Vec::new()
588 };
589 for buffer_id in to_close {
590 if let Err(e) = self.close_buffer(buffer_id) {
596 tracing::warn!("Failed to close overlay preview buffer: {}", e);
597 }
598 }
599 }
600
601 pub(crate) fn snapshot_prompt_results_for_grep(
607 &self,
608 prompt: &crate::view::prompt::Prompt,
609 ) -> Vec<crate::services::live_grep_state::GrepMatch> {
610 use crate::input::quick_open::parse_path_line_col;
611 prompt
617 .suggestions
618 .iter()
619 .filter(|s| !s.disabled)
620 .filter_map(|s| {
621 let from_text = parse_path_line_col(&s.text);
622 let (file, line, column) = if !from_text.0.is_empty() && from_text.1.is_some() {
623 from_text
624 } else if let Some(v) = s.value.as_deref() {
625 parse_path_line_col(v)
626 } else {
627 from_text
628 };
629 if file.is_empty() {
630 return None;
631 }
632 Some(crate::services::live_grep_state::GrepMatch {
633 file,
634 line: line.unwrap_or(1),
635 column: column.unwrap_or(1),
636 content: s.description.clone().unwrap_or_default(),
637 })
638 })
639 .collect()
640 }
641
642 pub fn cancel_prompt(&mut self) {
644 let theme_to_restore = if let Some(ref prompt) = self.active_window_mut().prompt {
646 if let PromptType::SelectTheme { original_theme } = &prompt.prompt_type {
647 Some(original_theme.clone())
648 } else {
649 None
650 }
651 } else {
652 None
653 };
654
655 let prompt_clone = self.active_window().prompt.clone();
660 if let Some(prompt) = prompt_clone {
661 let prompt = &prompt;
662 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
664 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
665 history.reset_navigation();
666 }
667 }
668 match &prompt.prompt_type {
669 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
670 self.active_window_mut().clear_search_highlights();
671 }
672 PromptType::Plugin { custom_type } => {
673 use crate::services::plugins::hooks::HookArgs;
675 self.plugin_manager.read().unwrap().run_hook(
676 "prompt_cancelled",
677 HookArgs::PromptCancelled {
678 prompt_type: custom_type.clone(),
679 input: prompt.input.clone(),
680 },
681 );
682 if custom_type == "live-grep" {
688 let cached = self.snapshot_prompt_results_for_grep(prompt);
689 if !prompt.input.is_empty() && !cached.is_empty() {
698 self.active_window_mut().live_grep_last_state =
699 Some(crate::services::live_grep_state::LiveGrepLastState {
700 query: prompt.input.clone(),
701 selected_index: prompt.selected_suggestion,
702 cached_results: Some(cached),
703 cached_at: Some(std::time::Instant::now()),
704 last_results_snapshot_id: None,
705 });
706 }
707 }
708 }
709 PromptType::LiveGrep => {
710 let cached = self.snapshot_prompt_results_for_grep(prompt);
711 if !prompt.input.is_empty() && !cached.is_empty() {
712 self.active_window_mut().live_grep_last_state =
713 Some(crate::services::live_grep_state::LiveGrepLastState {
714 query: prompt.input.clone(),
715 selected_index: prompt.selected_suggestion,
716 cached_results: Some(cached),
717 cached_at: Some(std::time::Instant::now()),
718 last_results_snapshot_id: None,
719 });
720 }
721 }
722 PromptType::LspRename { overlay_handle, .. } => {
723 let remove_overlay_event = crate::model::event::Event::RemoveOverlay {
725 handle: overlay_handle.clone(),
726 };
727 self.apply_event_to_active_buffer(&remove_overlay_event);
728 }
729 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
730 self.active_window_mut().file_open_state = None;
732 self.active_window_mut().file_browser_layout = None;
733
734 if matches!(prompt.prompt_type, PromptType::SaveFileAs)
739 && !self
740 .active_window_mut()
741 .pending_quit_unnamed_save
742 .is_empty()
743 {
744 self.active_window_mut().pending_quit_unnamed_save.clear();
745 self.set_status_message(t!("buffer.close_cancelled").to_string());
746 }
747 }
748 PromptType::AsyncPrompt => {
749 if let Some(callback_id) = self
751 .active_window_mut()
752 .pending_async_prompt_callback
753 .take()
754 {
755 self.plugin_manager
756 .read()
757 .unwrap()
758 .resolve_callback(callback_id, "null".to_string());
759 }
760 }
761 PromptType::QuickOpen => {
762 if let Some((provider, _)) = self.quick_open_registry.get_provider_for_input("")
764 {
765 if let Some(fp) = provider
766 .as_any()
767 .downcast_ref::<crate::input::quick_open::providers::FileProvider>(
768 ) {
769 fp.cancel_loading();
770 }
771 }
772 self.restore_goto_line_preview_snapshot();
775 }
776 PromptType::GotoLine => {
777 self.restore_goto_line_preview_snapshot();
780 }
781 _ => {}
782 }
783 }
784
785 let was_overlay = self
791 .active_window()
792 .prompt
793 .as_ref()
794 .is_some_and(|p| p.overlay);
795 if was_overlay {
796 self.cleanup_overlay_preview();
797 }
798
799 self.active_window_mut().prompt = None;
800 self.active_window_mut().pending_search_range = None;
801 self.active_window_mut().status_message = Some(t!("search.cancelled").to_string());
802
803 if let Some(original_theme) = theme_to_restore {
805 self.preview_theme(&original_theme);
806 }
807 }
808
809 pub fn handle_prompt_scroll(&mut self, delta: i32) -> bool {
812 if let Some(ref mut prompt) = self.active_window_mut().prompt {
813 if prompt.suggestions.is_empty() {
814 return false;
815 }
816
817 let current = prompt.selected_suggestion.unwrap_or(0);
818 let len = prompt.suggestions.len();
819
820 let new_selected = if delta < 0 {
823 current.saturating_sub((-delta) as usize)
825 } else {
826 (current + delta as usize).min(len.saturating_sub(1))
828 };
829
830 prompt.selected_suggestion = Some(new_selected);
831
832 if !matches!(prompt.prompt_type, PromptType::Plugin { .. }) {
834 if let Some(suggestion) = prompt.suggestions.get(new_selected) {
835 prompt.input = suggestion.get_value().to_string();
836 prompt.cursor_pos = prompt.input.len();
837 }
838 }
839
840 return true;
841 }
842 false
843 }
844
845 pub fn confirm_prompt(&mut self) -> Option<(String, PromptType, Option<usize>)> {
850 if let Some(prompt) = self.active_window_mut().prompt.take() {
851 let is_live_grep = match &prompt.prompt_type {
859 PromptType::LiveGrep => true,
860 PromptType::Plugin { custom_type } => custom_type == "live-grep",
861 _ => false,
862 };
863 if is_live_grep {
864 let cached = self.snapshot_prompt_results_for_grep(&prompt);
865 if !prompt.input.is_empty() && !cached.is_empty() {
866 self.active_window_mut().live_grep_last_state =
867 Some(crate::services::live_grep_state::LiveGrepLastState {
868 query: prompt.input.clone(),
869 selected_index: prompt.selected_suggestion,
870 cached_results: Some(cached),
871 cached_at: Some(std::time::Instant::now()),
872 last_results_snapshot_id: None,
873 });
874 }
875 }
876 if prompt.overlay {
881 self.cleanup_overlay_preview();
882 }
883 let selected_index = prompt.selected_suggestion;
884 let mut final_input = if prompt.sync_input_on_navigate {
886 prompt.input.clone()
889 } else if matches!(
890 prompt.prompt_type,
891 PromptType::OpenFile
892 | PromptType::SwitchProject
893 | PromptType::SaveFileAs
894 | PromptType::StopLspServer
895 | PromptType::RestartLspServer
896 | PromptType::SelectTheme { .. }
897 | PromptType::SelectLocale
898 | PromptType::SwitchToTab
899 | PromptType::SetLanguage
900 | PromptType::SetEncoding
901 | PromptType::SetLineEnding
902 | PromptType::Plugin { .. }
903 | PromptType::LiveGrep
910 ) {
911 if let Some(selected_idx) = prompt.selected_suggestion {
913 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
914 if suggestion.disabled {
916 self.set_status_message(
917 t!(
918 "error.command_not_available",
919 command = suggestion.text.clone()
920 )
921 .to_string(),
922 );
923 return None;
924 }
925 suggestion.get_value().to_string()
927 } else {
928 prompt.input.clone()
929 }
930 } else {
931 prompt.input.clone()
932 }
933 } else {
934 prompt.input.clone()
935 };
936
937 if matches!(
939 prompt.prompt_type,
940 PromptType::StopLspServer | PromptType::RestartLspServer
941 ) {
942 let is_valid = prompt
943 .suggestions
944 .iter()
945 .any(|s| s.text == final_input || s.get_value() == final_input);
946 if !is_valid {
947 self.active_window_mut().prompt = Some(prompt);
949 self.set_status_message(
950 t!("error.no_lsp_match", input = final_input.clone()).to_string(),
951 );
952 return None;
953 }
954 }
955
956 if matches!(prompt.prompt_type, PromptType::RemoveRuler) {
960 if prompt.input.is_empty() {
961 if let Some(selected_idx) = prompt.selected_suggestion {
963 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
964 final_input = suggestion.get_value().to_string();
965 }
966 } else {
967 self.active_window_mut().prompt = Some(prompt);
968 return None;
969 }
970 } else {
971 let typed = prompt.input.trim().to_string();
973 let matched = prompt.suggestions.iter().find(|s| s.get_value() == typed);
974 if let Some(suggestion) = matched {
975 final_input = suggestion.get_value().to_string();
976 } else {
977 self.active_window_mut().prompt = Some(prompt);
979 return None;
980 }
981 }
982 }
983
984 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
986 let history = self.get_or_create_prompt_history(&key);
987 history.push(final_input.clone());
988 history.reset_navigation();
989 }
990
991 Some((final_input, prompt.prompt_type, selected_index))
992 } else {
993 None
994 }
995 }
996
997 pub fn is_prompting(&self) -> bool {
999 self.active_window().prompt.is_some()
1000 }
1001
1002 pub(super) fn get_or_create_prompt_history(
1004 &mut self,
1005 key: &str,
1006 ) -> &mut crate::input::input_history::InputHistory {
1007 self.active_window_mut()
1008 .prompt_histories
1009 .entry(key.to_string())
1010 .or_default()
1011 }
1012
1013 pub(super) fn get_prompt_history(
1015 &self,
1016 key: &str,
1017 ) -> Option<&crate::input::input_history::InputHistory> {
1018 self.active_window().prompt_histories.get(key)
1019 }
1020
1021 pub(super) fn prompt_type_to_history_key(
1023 prompt_type: &crate::view::prompt::PromptType,
1024 ) -> Option<String> {
1025 use crate::view::prompt::PromptType;
1026 match prompt_type {
1027 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
1028 Some("search".to_string())
1029 }
1030 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
1031 Some("replace".to_string())
1032 }
1033 PromptType::GotoLine => Some("goto_line".to_string()),
1034 PromptType::Plugin { custom_type } => Some(format!("plugin:{}", custom_type)),
1035 _ => None,
1036 }
1037 }
1038
1039 pub fn editor_mode(&self) -> Option<String> {
1042 self.active_window().editor_mode.clone()
1043 }
1044
1045 pub fn command_registry(&self) -> &Arc<RwLock<CommandRegistry>> {
1047 &self.command_registry
1048 }
1049
1050 pub fn plugin_manager(&self) -> std::sync::RwLockReadGuard<'_, PluginManager> {
1056 self.plugin_manager.read().unwrap()
1057 }
1058
1059 pub fn plugin_manager_mut(&mut self) -> std::sync::RwLockWriteGuard<'_, PluginManager> {
1061 self.plugin_manager.write().unwrap()
1062 }
1063
1064 pub fn file_explorer_is_focused(&self) -> bool {
1066 self.active_window().key_context == KeyContext::FileExplorer
1067 }
1068
1069 pub fn prompt_input(&self) -> Option<&str> {
1071 self.active_window()
1072 .prompt
1073 .as_ref()
1074 .map(|p| p.input.as_str())
1075 }
1076
1077 pub fn has_active_selection(&self) -> bool {
1079 self.active_cursors().primary().selection_range().is_some()
1080 }
1081
1082 pub fn prompt_mut(&mut self) -> Option<&mut Prompt> {
1084 self.active_window_mut().prompt.as_mut()
1085 }
1086
1087 pub fn set_status_message(&mut self, message: String) {
1091 self.active_window_mut().set_status_message(message);
1092 }
1093
1094 pub fn get_status_message(&self) -> Option<&String> {
1096 self.active_window()
1097 .plugin_status_message
1098 .as_ref()
1099 .or(self.active_window().status_message.as_ref())
1100 }
1101
1102 pub fn get_plugin_errors(&self) -> &[String] {
1105 &self.active_window().plugin_errors
1106 }
1107
1108 pub fn clear_plugin_errors(&mut self) {
1110 self.active_window_mut().plugin_errors.clear();
1111 }
1112
1113 pub fn update_prompt_suggestions(&mut self) {
1115 let (prompt_type, input) = if let Some(prompt) = &self.active_window_mut().prompt {
1117 (prompt.prompt_type.clone(), prompt.input.clone())
1118 } else {
1119 return;
1120 };
1121
1122 match prompt_type {
1123 PromptType::QuickOpen => {
1124 self.update_quick_open_suggestions(&input);
1126 }
1127 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
1128 self.update_search_highlights(&input);
1130 if let Some(history) = self.active_window_mut().prompt_histories.get_mut("search") {
1132 history.reset_navigation();
1133 }
1134 }
1135 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
1136 if let Some(history) = self.active_window_mut().prompt_histories.get_mut("replace")
1138 {
1139 history.reset_navigation();
1140 }
1141 }
1142 PromptType::GotoLine => {
1143 if let Some(history) = self
1145 .active_window_mut()
1146 .prompt_histories
1147 .get_mut("goto_line")
1148 {
1149 history.reset_navigation();
1150 }
1151 let target = match crate::input::quick_open::parse_goto_line_input(input.trim()) {
1156 Some(crate::input::quick_open::GotoLineTarget::Absolute(n)) => Some(n),
1157 _ => None,
1158 };
1159 self.apply_goto_line_preview(target);
1160 }
1161 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
1162 self.update_file_open_filter();
1164 }
1165 PromptType::Plugin { custom_type } => {
1166 let key = format!("plugin:{}", custom_type);
1168 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
1169 history.reset_navigation();
1170 }
1171 use crate::services::plugins::hooks::HookArgs;
1173 self.plugin_manager.read().unwrap().run_hook(
1174 "prompt_changed",
1175 HookArgs::PromptChanged {
1176 prompt_type: custom_type,
1177 input,
1178 },
1179 );
1180 if let Some(prompt) = &mut self.active_window_mut().prompt {
1185 prompt.filter_suggestions(false);
1186 }
1187 }
1188 PromptType::SwitchToTab
1189 | PromptType::SelectTheme { .. }
1190 | PromptType::StopLspServer
1191 | PromptType::RestartLspServer
1192 | PromptType::SetLanguage
1193 | PromptType::SetEncoding
1194 | PromptType::SetLineEnding => {
1195 if let Some(prompt) = &mut self.active_window_mut().prompt {
1196 prompt.filter_suggestions(false);
1197 }
1198 }
1199 PromptType::SelectLocale => {
1200 if let Some(prompt) = &mut self.active_window_mut().prompt {
1202 prompt.filter_suggestions(true);
1203 }
1204 }
1205 _ => {}
1206 }
1207 }
1208}
1209
1210impl Window {
1211 pub(crate) fn cancel_search_prompt_if_active(&mut self) {
1214 if let Some(ref prompt) = self.prompt {
1215 if matches!(
1216 prompt.prompt_type,
1217 PromptType::Search
1218 | PromptType::ReplaceSearch
1219 | PromptType::Replace { .. }
1220 | PromptType::QueryReplaceSearch
1221 | PromptType::QueryReplace { .. }
1222 | PromptType::QueryReplaceConfirm
1223 ) {
1224 self.prompt = None;
1225 self.interactive_replace_state = None;
1227 let ns = self.search_namespace.clone();
1229 let state = self.active_state_mut();
1230 state.overlays.clear_namespace(&ns, &mut state.marker_list);
1231 }
1232 }
1233 }
1234}