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.clone())
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.clone())
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.clone();
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 ) {
904 if let Some(selected_idx) = prompt.selected_suggestion {
906 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
907 if suggestion.disabled {
909 self.set_status_message(
910 t!(
911 "error.command_not_available",
912 command = suggestion.text.clone()
913 )
914 .to_string(),
915 );
916 return None;
917 }
918 suggestion.get_value().to_string()
920 } else {
921 prompt.input.clone()
922 }
923 } else {
924 prompt.input.clone()
925 }
926 } else {
927 prompt.input.clone()
928 };
929
930 if matches!(
932 prompt.prompt_type,
933 PromptType::StopLspServer | PromptType::RestartLspServer
934 ) {
935 let is_valid = prompt
936 .suggestions
937 .iter()
938 .any(|s| s.text == final_input || s.get_value() == final_input);
939 if !is_valid {
940 self.active_window_mut().prompt = Some(prompt);
942 self.set_status_message(
943 t!("error.no_lsp_match", input = final_input.clone()).to_string(),
944 );
945 return None;
946 }
947 }
948
949 if matches!(prompt.prompt_type, PromptType::RemoveRuler) {
953 if prompt.input.is_empty() {
954 if let Some(selected_idx) = prompt.selected_suggestion {
956 if let Some(suggestion) = prompt.suggestions.get(selected_idx) {
957 final_input = suggestion.get_value().to_string();
958 }
959 } else {
960 self.active_window_mut().prompt = Some(prompt);
961 return None;
962 }
963 } else {
964 let typed = prompt.input.trim().to_string();
966 let matched = prompt.suggestions.iter().find(|s| s.get_value() == typed);
967 if let Some(suggestion) = matched {
968 final_input = suggestion.get_value().to_string();
969 } else {
970 self.active_window_mut().prompt = Some(prompt);
972 return None;
973 }
974 }
975 }
976
977 if let Some(key) = Self::prompt_type_to_history_key(&prompt.prompt_type) {
979 let history = self.get_or_create_prompt_history(&key);
980 history.push(final_input.clone());
981 history.reset_navigation();
982 }
983
984 Some((final_input, prompt.prompt_type, selected_index))
985 } else {
986 None
987 }
988 }
989
990 pub fn is_prompting(&self) -> bool {
992 self.active_window().prompt.is_some()
993 }
994
995 pub(super) fn get_or_create_prompt_history(
997 &mut self,
998 key: &str,
999 ) -> &mut crate::input::input_history::InputHistory {
1000 self.active_window_mut()
1001 .prompt_histories
1002 .entry(key.to_string())
1003 .or_default()
1004 }
1005
1006 pub(super) fn get_prompt_history(
1008 &self,
1009 key: &str,
1010 ) -> Option<&crate::input::input_history::InputHistory> {
1011 self.active_window().prompt_histories.get(key)
1012 }
1013
1014 pub(super) fn prompt_type_to_history_key(
1016 prompt_type: &crate::view::prompt::PromptType,
1017 ) -> Option<String> {
1018 use crate::view::prompt::PromptType;
1019 match prompt_type {
1020 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
1021 Some("search".to_string())
1022 }
1023 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
1024 Some("replace".to_string())
1025 }
1026 PromptType::GotoLine => Some("goto_line".to_string()),
1027 PromptType::Plugin { custom_type } => Some(format!("plugin:{}", custom_type)),
1028 _ => None,
1029 }
1030 }
1031
1032 pub fn editor_mode(&self) -> Option<String> {
1035 self.active_window().editor_mode.clone()
1036 }
1037
1038 pub fn command_registry(&self) -> &Arc<RwLock<CommandRegistry>> {
1040 &self.command_registry
1041 }
1042
1043 pub fn plugin_manager(&self) -> std::sync::RwLockReadGuard<'_, PluginManager> {
1049 self.plugin_manager.read().unwrap()
1050 }
1051
1052 pub fn plugin_manager_mut(&mut self) -> std::sync::RwLockWriteGuard<'_, PluginManager> {
1054 self.plugin_manager.write().unwrap()
1055 }
1056
1057 pub fn file_explorer_is_focused(&self) -> bool {
1059 self.active_window().key_context == KeyContext::FileExplorer
1060 }
1061
1062 pub fn prompt_input(&self) -> Option<&str> {
1064 self.active_window()
1065 .prompt
1066 .as_ref()
1067 .map(|p| p.input.as_str())
1068 }
1069
1070 pub fn has_active_selection(&self) -> bool {
1072 self.active_cursors().primary().selection_range().is_some()
1073 }
1074
1075 pub fn prompt_mut(&mut self) -> Option<&mut Prompt> {
1077 self.active_window_mut().prompt.as_mut()
1078 }
1079
1080 pub fn set_status_message(&mut self, message: String) {
1084 self.active_window_mut().set_status_message(message);
1085 }
1086
1087 pub fn get_status_message(&self) -> Option<&String> {
1089 self.active_window()
1090 .plugin_status_message
1091 .as_ref()
1092 .or(self.active_window().status_message.as_ref())
1093 }
1094
1095 pub fn get_plugin_errors(&self) -> &[String] {
1098 &self.active_window().plugin_errors
1099 }
1100
1101 pub fn clear_plugin_errors(&mut self) {
1103 self.active_window_mut().plugin_errors.clear();
1104 }
1105
1106 pub fn update_prompt_suggestions(&mut self) {
1108 let (prompt_type, input) = if let Some(prompt) = &self.active_window_mut().prompt {
1110 (prompt.prompt_type.clone(), prompt.input.clone())
1111 } else {
1112 return;
1113 };
1114
1115 match prompt_type {
1116 PromptType::QuickOpen => {
1117 self.update_quick_open_suggestions(&input);
1119 }
1120 PromptType::Search | PromptType::ReplaceSearch | PromptType::QueryReplaceSearch => {
1121 self.update_search_highlights(&input);
1123 if let Some(history) = self.active_window_mut().prompt_histories.get_mut("search") {
1125 history.reset_navigation();
1126 }
1127 }
1128 PromptType::Replace { .. } | PromptType::QueryReplace { .. } => {
1129 if let Some(history) = self.active_window_mut().prompt_histories.get_mut("replace")
1131 {
1132 history.reset_navigation();
1133 }
1134 }
1135 PromptType::GotoLine => {
1136 if let Some(history) = self
1138 .active_window_mut()
1139 .prompt_histories
1140 .get_mut("goto_line")
1141 {
1142 history.reset_navigation();
1143 }
1144 let target = match crate::input::quick_open::parse_goto_line_input(input.trim()) {
1149 Some(crate::input::quick_open::GotoLineTarget::Absolute(n)) => Some(n),
1150 _ => None,
1151 };
1152 self.apply_goto_line_preview(target);
1153 }
1154 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs => {
1155 self.update_file_open_filter();
1157 }
1158 PromptType::Plugin { custom_type } => {
1159 let key = format!("plugin:{}", custom_type);
1161 if let Some(history) = self.active_window_mut().prompt_histories.get_mut(&key) {
1162 history.reset_navigation();
1163 }
1164 use crate::services::plugins::hooks::HookArgs;
1166 self.plugin_manager.read().unwrap().run_hook(
1167 "prompt_changed",
1168 HookArgs::PromptChanged {
1169 prompt_type: custom_type,
1170 input,
1171 },
1172 );
1173 if let Some(prompt) = &mut self.active_window_mut().prompt {
1178 prompt.filter_suggestions(false);
1179 }
1180 }
1181 PromptType::SwitchToTab
1182 | PromptType::SelectTheme { .. }
1183 | PromptType::StopLspServer
1184 | PromptType::RestartLspServer
1185 | PromptType::SetLanguage
1186 | PromptType::SetEncoding
1187 | PromptType::SetLineEnding => {
1188 if let Some(prompt) = &mut self.active_window_mut().prompt {
1189 prompt.filter_suggestions(false);
1190 }
1191 }
1192 PromptType::SelectLocale => {
1193 if let Some(prompt) = &mut self.active_window_mut().prompt {
1195 prompt.filter_suggestions(true);
1196 }
1197 }
1198 _ => {}
1199 }
1200 }
1201}
1202
1203impl Window {
1204 pub(crate) fn cancel_search_prompt_if_active(&mut self) {
1207 if let Some(ref prompt) = self.prompt {
1208 if matches!(
1209 prompt.prompt_type,
1210 PromptType::Search
1211 | PromptType::ReplaceSearch
1212 | PromptType::Replace { .. }
1213 | PromptType::QueryReplaceSearch
1214 | PromptType::QueryReplace { .. }
1215 | PromptType::QueryReplaceConfirm
1216 ) {
1217 self.prompt = None;
1218 self.interactive_replace_state = None;
1220 let ns = self.search_namespace.clone();
1222 let state = self.active_state_mut();
1223 state.overlays.clear_namespace(&ns, &mut state.marker_list);
1224 }
1225 }
1226 }
1227}