Skip to main content

fresh/app/
prompt_actions.rs

1//! Prompt confirmation action handlers.
2//!
3//! This module contains handlers for different prompt types when the user confirms input.
4
5use rust_i18n::t;
6
7use super::normalize_path;
8use super::BufferId;
9use super::BufferMetadata;
10use super::Editor;
11use crate::input::keybindings::Action;
12use crate::primitives::path_utils::expand_tilde;
13use crate::services::plugins::hooks::HookArgs;
14use crate::view::prompt::PromptType;
15
16/// Result of handling a prompt confirmation.
17pub enum PromptResult {
18    /// Prompt handled, continue normally
19    Done,
20    /// Prompt handled, should execute this action next
21    ExecuteAction(Action),
22    /// Prompt handled, should return early from handle_action
23    EarlyReturn,
24}
25
26impl Editor {
27    /// Handle prompt confirmation based on the prompt type.
28    ///
29    /// Returns a `PromptResult` indicating what the caller should do next.
30    pub fn handle_prompt_confirm_input(
31        &mut self,
32        input: String,
33        prompt_type: PromptType,
34        selected_index: Option<usize>,
35    ) -> PromptResult {
36        match prompt_type {
37            PromptType::OpenFile => {
38                // Expand tilde to home directory first
39                let expanded_path = expand_tilde(&input);
40                let resolved_path = if expanded_path.is_absolute() {
41                    normalize_path(&expanded_path)
42                } else {
43                    normalize_path(&self.working_dir.join(&expanded_path))
44                };
45
46                if let Err(e) = self.open_file(&resolved_path) {
47                    self.set_status_message(
48                        t!("file.error_opening", error = e.to_string()).to_string(),
49                    );
50                } else {
51                    self.set_status_message(
52                        t!("buffer.opened", name = resolved_path.display().to_string()).to_string(),
53                    );
54                }
55            }
56            PromptType::SwitchProject => {
57                // Expand tilde to home directory first
58                let expanded_path = expand_tilde(&input);
59                let resolved_path = if expanded_path.is_absolute() {
60                    normalize_path(&expanded_path)
61                } else {
62                    normalize_path(&self.working_dir.join(&expanded_path))
63                };
64
65                if resolved_path.is_dir() {
66                    self.change_working_dir(resolved_path);
67                } else {
68                    self.set_status_message(
69                        t!(
70                            "file.not_directory",
71                            path = resolved_path.display().to_string()
72                        )
73                        .to_string(),
74                    );
75                }
76            }
77            PromptType::SaveFileAs => {
78                self.handle_save_file_as(&input);
79            }
80            PromptType::Search => {
81                self.perform_search(&input);
82            }
83            PromptType::ReplaceSearch => {
84                self.perform_search(&input);
85                self.start_prompt(
86                    t!("replace.prompt", search = &input).to_string(),
87                    PromptType::Replace {
88                        search: input.clone(),
89                    },
90                );
91            }
92            PromptType::Replace { search } => {
93                if self.search_confirm_each {
94                    self.start_interactive_replace(&search, &input);
95                } else {
96                    self.perform_replace(&search, &input);
97                }
98            }
99            PromptType::QueryReplaceSearch => {
100                self.perform_search(&input);
101                self.start_prompt(
102                    t!("replace.query_prompt", search = &input).to_string(),
103                    PromptType::QueryReplace {
104                        search: input.clone(),
105                    },
106                );
107            }
108            PromptType::QueryReplace { search } => {
109                if self.search_confirm_each {
110                    self.start_interactive_replace(&search, &input);
111                } else {
112                    self.perform_replace(&search, &input);
113                }
114            }
115            PromptType::Command => {
116                let commands = self.command_registry.read().unwrap().get_all();
117                if let Some(cmd) = commands.iter().find(|c| c.get_localized_name() == input) {
118                    let action = cmd.action.clone();
119                    let cmd_name = cmd.get_localized_name();
120                    self.command_registry
121                        .write()
122                        .unwrap()
123                        .record_usage(&cmd_name);
124                    return PromptResult::ExecuteAction(action);
125                } else {
126                    self.set_status_message(
127                        t!("error.unknown_command", input = &input).to_string(),
128                    );
129                }
130            }
131            PromptType::GotoLine => match input.trim().parse::<usize>() {
132                Ok(line_num) if line_num > 0 => {
133                    self.goto_line_col(line_num, None);
134                    self.set_status_message(t!("goto.jumped", line = line_num).to_string());
135                }
136                Ok(_) => {
137                    self.set_status_message(t!("goto.line_must_be_positive").to_string());
138                }
139                Err(_) => {
140                    self.set_status_message(t!("error.invalid_line", input = &input).to_string());
141                }
142            },
143            PromptType::QuickOpen => {
144                // Handle Quick Open confirmation based on prefix
145                return self.handle_quick_open_confirm(&input, selected_index);
146            }
147            PromptType::SetBackgroundFile => {
148                if let Err(e) = self.load_ansi_background(&input) {
149                    self.set_status_message(
150                        t!("error.background_load_failed", error = e.to_string()).to_string(),
151                    );
152                }
153            }
154            PromptType::SetBackgroundBlend => match input.trim().parse::<f32>() {
155                Ok(val) => {
156                    let clamped = val.clamp(0.0, 1.0);
157                    self.background_fade = clamped;
158                    self.set_status_message(
159                        t!(
160                            "error.background_blend_set",
161                            value = format!("{:.2}", clamped)
162                        )
163                        .to_string(),
164                    );
165                }
166                Err(_) => {
167                    self.set_status_message(t!("error.invalid_blend", input = &input).to_string());
168                }
169            },
170            PromptType::SetComposeWidth => {
171                self.handle_set_compose_width(&input);
172            }
173            PromptType::RecordMacro => {
174                self.handle_register_input(
175                    &input,
176                    |editor, c| editor.toggle_macro_recording(c),
177                    "Macro",
178                );
179            }
180            PromptType::PlayMacro => {
181                self.handle_register_input(&input, |editor, c| editor.play_macro(c), "Macro");
182            }
183            PromptType::SetBookmark => {
184                self.handle_register_input(&input, |editor, c| editor.set_bookmark(c), "Bookmark");
185            }
186            PromptType::JumpToBookmark => {
187                self.handle_register_input(
188                    &input,
189                    |editor, c| editor.jump_to_bookmark(c),
190                    "Bookmark",
191                );
192            }
193            PromptType::Plugin { custom_type } => {
194                tracing::info!(
195                    "prompt_confirmed: dispatching hook for prompt_type='{}', input='{}', selected_index={:?}",
196                    custom_type, input, selected_index
197                );
198                self.plugin_manager.run_hook(
199                    "prompt_confirmed",
200                    HookArgs::PromptConfirmed {
201                        prompt_type: custom_type.clone(),
202                        input,
203                        selected_index,
204                    },
205                );
206                tracing::info!(
207                    "prompt_confirmed: hook dispatched for prompt_type='{}'",
208                    custom_type
209                );
210            }
211            PromptType::ConfirmRevert => {
212                let input_lower = input.trim().to_lowercase();
213                let revert_key = t!("prompt.key.revert").to_string().to_lowercase();
214                if input_lower == revert_key || input_lower == "revert" {
215                    if let Err(e) = self.revert_file() {
216                        self.set_status_message(
217                            t!("file.revert_failed", error = e.to_string()).to_string(),
218                        );
219                    }
220                } else {
221                    self.set_status_message(t!("buffer.revert_cancelled").to_string());
222                }
223            }
224            PromptType::ConfirmSaveConflict => {
225                let input_lower = input.trim().to_lowercase();
226                if input_lower == "o" || input_lower == "overwrite" {
227                    if let Err(e) = self.save() {
228                        self.set_status_message(
229                            t!("file.save_failed", error = e.to_string()).to_string(),
230                        );
231                    }
232                } else {
233                    self.set_status_message(t!("buffer.save_cancelled").to_string());
234                }
235            }
236            PromptType::ConfirmSudoSave { info } => {
237                let input_lower = input.trim().to_lowercase();
238                if input_lower == "y" || input_lower == "yes" {
239                    // Hide prompt before starting blocking command to clear the line
240                    self.cancel_prompt();
241
242                    // Read temp file and write via sudo (works for both local and remote)
243                    let result = (|| -> anyhow::Result<()> {
244                        let data = self.filesystem.read_file(&info.temp_path)?;
245                        self.filesystem.sudo_write(
246                            &info.dest_path,
247                            &data,
248                            info.mode,
249                            info.uid,
250                            info.gid,
251                        )?;
252                        // Clean up temp file on success
253                        let _ = self.filesystem.remove_file(&info.temp_path);
254                        Ok(())
255                    })();
256
257                    match result {
258                        Ok(_) => {
259                            if let Err(e) = self
260                                .active_state_mut()
261                                .buffer
262                                .finalize_external_save(info.dest_path.clone())
263                            {
264                                tracing::warn!("Failed to finalize sudo save: {}", e);
265                                self.set_status_message(
266                                    t!("prompt.sudo_save_failed", error = e.to_string())
267                                        .to_string(),
268                                );
269                            } else if let Err(e) = self.finalize_save(Some(info.dest_path)) {
270                                tracing::warn!("Failed to finalize save after sudo: {}", e);
271                                self.set_status_message(
272                                    t!("prompt.sudo_save_failed", error = e.to_string())
273                                        .to_string(),
274                                );
275                            }
276                        }
277                        Err(e) => {
278                            tracing::warn!("Sudo save failed: {}", e);
279                            self.set_status_message(
280                                t!("prompt.sudo_save_failed", error = e.to_string()).to_string(),
281                            );
282                            // Clean up temp file on failure
283                            let _ = self.filesystem.remove_file(&info.temp_path);
284                        }
285                    }
286                } else {
287                    self.set_status_message(t!("buffer.save_cancelled").to_string());
288                    // Clean up temp file
289                    let _ = self.filesystem.remove_file(&info.temp_path);
290                }
291            }
292            PromptType::ConfirmOverwriteFile { path } => {
293                let input_lower = input.trim().to_lowercase();
294                if input_lower == "o" || input_lower == "overwrite" {
295                    self.perform_save_file_as(path);
296                } else {
297                    self.set_status_message(t!("buffer.save_cancelled").to_string());
298                }
299            }
300            PromptType::ConfirmCloseBuffer { buffer_id } => {
301                if self.handle_confirm_close_buffer(&input, buffer_id) {
302                    return PromptResult::EarlyReturn;
303                }
304            }
305            PromptType::ConfirmQuitWithModified => {
306                let input_lower = input.trim().to_lowercase();
307                let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
308                if input_lower == discard_key || input_lower == "discard" {
309                    self.should_quit = true;
310                } else {
311                    self.set_status_message(t!("buffer.close_cancelled").to_string());
312                }
313            }
314            PromptType::LspRename {
315                original_text,
316                start_pos,
317                end_pos: _,
318                overlay_handle,
319            } => {
320                self.perform_lsp_rename(input, original_text, start_pos, overlay_handle);
321            }
322            PromptType::FileExplorerRename {
323                original_path,
324                original_name,
325                is_new_file,
326            } => {
327                self.perform_file_explorer_rename(original_path, original_name, input, is_new_file);
328            }
329            PromptType::ConfirmDeleteFile { path, is_dir } => {
330                let input_lower = input.trim().to_lowercase();
331                if input_lower == "y" || input_lower == "yes" {
332                    self.perform_file_explorer_delete(path, is_dir);
333                } else {
334                    self.set_status_message(t!("explorer.delete_cancelled").to_string());
335                }
336            }
337            PromptType::StopLspServer => {
338                self.handle_stop_lsp_server(&input);
339            }
340            PromptType::SelectTheme { .. } => {
341                self.apply_theme(input.trim());
342            }
343            PromptType::SelectKeybindingMap => {
344                self.apply_keybinding_map(input.trim());
345            }
346            PromptType::SelectCursorStyle => {
347                self.apply_cursor_style(input.trim());
348            }
349            PromptType::SelectLocale => {
350                self.apply_locale(input.trim());
351            }
352            PromptType::CopyWithFormattingTheme => {
353                self.copy_selection_with_theme(input.trim());
354            }
355            PromptType::SwitchToTab => {
356                if let Ok(id) = input.trim().parse::<usize>() {
357                    self.switch_to_tab(BufferId(id));
358                }
359            }
360            PromptType::QueryReplaceConfirm => {
361                // This is handled by InsertChar, not PromptConfirm
362                // But if somehow Enter is pressed, treat it as skip (n)
363                if let Some(c) = input.chars().next() {
364                    let _ = self.handle_interactive_replace_key(c);
365                }
366            }
367            PromptType::SetTabSize => {
368                self.handle_set_tab_size(&input);
369            }
370            PromptType::SetLineEnding => {
371                self.handle_set_line_ending(&input);
372            }
373            PromptType::SetLanguage => {
374                self.handle_set_language(&input);
375            }
376            PromptType::ShellCommand { replace } => {
377                self.handle_shell_command(&input, replace);
378            }
379            PromptType::AsyncPrompt => {
380                // Resolve the pending async prompt callback with the input text
381                if let Some(callback_id) = self.pending_async_prompt_callback.take() {
382                    // Serialize the input as a JSON string
383                    let json = serde_json::to_string(&input).unwrap_or_else(|_| "null".to_string());
384                    self.plugin_manager.resolve_callback(callback_id, json);
385                }
386            }
387        }
388        PromptResult::Done
389    }
390
391    /// Handle SaveFileAs prompt confirmation.
392    fn handle_save_file_as(&mut self, input: &str) {
393        // Expand tilde to home directory first
394        let expanded_path = expand_tilde(input);
395        let full_path = if expanded_path.is_absolute() {
396            normalize_path(&expanded_path)
397        } else {
398            normalize_path(&self.working_dir.join(&expanded_path))
399        };
400
401        // Check if we're saving to a different file that already exists
402        let current_file_path = self
403            .active_state()
404            .buffer
405            .file_path()
406            .map(|p| p.to_path_buf());
407        let is_different_file = current_file_path.as_ref() != Some(&full_path);
408
409        if is_different_file && full_path.is_file() {
410            // File exists and is different from current - ask for confirmation
411            let filename = full_path
412                .file_name()
413                .map(|n| n.to_string_lossy().to_string())
414                .unwrap_or_else(|| full_path.display().to_string());
415            self.start_prompt(
416                t!("buffer.overwrite_confirm", name = &filename).to_string(),
417                PromptType::ConfirmOverwriteFile { path: full_path },
418            );
419            return;
420        }
421
422        // Proceed with save
423        self.perform_save_file_as(full_path);
424    }
425
426    /// Perform the actual SaveFileAs operation (called after confirmation if needed).
427    pub(crate) fn perform_save_file_as(&mut self, full_path: std::path::PathBuf) {
428        let before_idx = self.active_event_log().current_index();
429        let before_len = self.active_event_log().len();
430        tracing::debug!(
431            "SaveFileAs BEFORE: event_log index={}, len={}",
432            before_idx,
433            before_len
434        );
435
436        match self.active_state_mut().buffer.save_to_file(&full_path) {
437            Ok(()) => {
438                let after_save_idx = self.active_event_log().current_index();
439                let after_save_len = self.active_event_log().len();
440                tracing::debug!(
441                    "SaveFileAs AFTER buffer.save_to_file: event_log index={}, len={}",
442                    after_save_idx,
443                    after_save_len
444                );
445
446                let metadata = BufferMetadata::with_file(full_path.clone(), &self.working_dir);
447                self.buffer_metadata.insert(self.active_buffer(), metadata);
448
449                // Auto-detect language if it's currently "text"
450                // This ensures syntax highlighting works immediately after "Save As"
451                if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
452                    if state.language == "text" {
453                        if let Some(filename) = full_path.file_name().and_then(|n| n.to_str()) {
454                            state.set_language_from_name(filename, &self.grammar_registry);
455                        }
456                    }
457                }
458
459                self.active_event_log_mut().mark_saved();
460                tracing::debug!(
461                    "SaveFileAs AFTER mark_saved: event_log index={}, len={}",
462                    self.active_event_log().current_index(),
463                    self.active_event_log().len()
464                );
465
466                if let Ok(metadata) = self.filesystem.metadata(&full_path) {
467                    if let Some(mtime) = metadata.modified {
468                        self.file_mod_times.insert(full_path.clone(), mtime);
469                    }
470                }
471
472                self.notify_lsp_save();
473
474                self.emit_event(
475                    crate::model::control_event::events::FILE_SAVED.name,
476                    serde_json::json!({"path": full_path.display().to_string()}),
477                );
478
479                self.plugin_manager.run_hook(
480                    "after_file_save",
481                    crate::services::plugins::hooks::HookArgs::AfterFileSave {
482                        buffer_id: self.active_buffer(),
483                        path: full_path.clone(),
484                    },
485                );
486
487                if let Some(buffer_to_close) = self.pending_close_buffer.take() {
488                    if let Err(e) = self.force_close_buffer(buffer_to_close) {
489                        self.set_status_message(
490                            t!("file.saved_cannot_close", error = e.to_string()).to_string(),
491                        );
492                    } else {
493                        self.set_status_message(t!("buffer.saved_and_closed").to_string());
494                    }
495                } else {
496                    self.set_status_message(
497                        t!("file.saved_as", path = full_path.display().to_string()).to_string(),
498                    );
499                }
500            }
501            Err(e) => {
502                self.pending_close_buffer = None;
503                self.set_status_message(t!("file.error_saving", error = e.to_string()).to_string());
504            }
505        }
506    }
507
508    /// Handle SetComposeWidth prompt confirmation.
509    fn handle_set_compose_width(&mut self, input: &str) {
510        let buffer_id = self.active_buffer();
511        let active_split = self.split_manager.active_split();
512        let trimmed = input.trim();
513
514        if trimmed.is_empty() {
515            if let Some(state) = self.buffers.get_mut(&buffer_id) {
516                state.compose_width = None;
517            }
518            if let Some(vs) = self.split_view_states.get_mut(&active_split) {
519                vs.compose_width = None;
520            }
521            self.set_status_message(t!("settings.compose_width_cleared").to_string());
522        } else {
523            match trimmed.parse::<u16>() {
524                Ok(val) if val > 0 => {
525                    if let Some(state) = self.buffers.get_mut(&buffer_id) {
526                        state.compose_width = Some(val);
527                    }
528                    if let Some(vs) = self.split_view_states.get_mut(&active_split) {
529                        vs.compose_width = Some(val);
530                    }
531                    self.set_status_message(
532                        t!("settings.compose_width_set", value = val).to_string(),
533                    );
534                }
535                _ => {
536                    self.set_status_message(
537                        t!("error.invalid_compose_width", input = input).to_string(),
538                    );
539                }
540            }
541        }
542    }
543
544    /// Handle SetTabSize prompt confirmation.
545    fn handle_set_tab_size(&mut self, input: &str) {
546        let buffer_id = self.active_buffer();
547        let trimmed = input.trim();
548
549        match trimmed.parse::<usize>() {
550            Ok(val) if val > 0 => {
551                if let Some(state) = self.buffers.get_mut(&buffer_id) {
552                    state.tab_size = val;
553                }
554                self.set_status_message(t!("settings.tab_size_set", value = val).to_string());
555            }
556            Ok(_) => {
557                self.set_status_message(t!("settings.tab_size_positive").to_string());
558            }
559            Err(_) => {
560                self.set_status_message(t!("error.invalid_tab_size", input = input).to_string());
561            }
562        }
563    }
564
565    /// Handle SetLineEnding prompt confirmation.
566    fn handle_set_line_ending(&mut self, input: &str) {
567        use crate::model::buffer::LineEnding;
568
569        // Extract the line ending code from the input (e.g., "LF" from "LF (Unix/Linux/Mac)")
570        let trimmed = input.trim();
571        let code = trimmed.split_whitespace().next().unwrap_or(trimmed);
572
573        let line_ending = match code.to_uppercase().as_str() {
574            "LF" => Some(LineEnding::LF),
575            "CRLF" => Some(LineEnding::CRLF),
576            "CR" => Some(LineEnding::CR),
577            _ => None,
578        };
579
580        match line_ending {
581            Some(le) => {
582                self.active_state_mut().buffer.set_line_ending(le);
583                self.set_status_message(
584                    t!("settings.line_ending_set", value = le.display_name()).to_string(),
585                );
586            }
587            None => {
588                self.set_status_message(t!("error.unknown_line_ending", input = input).to_string());
589            }
590        }
591    }
592
593    /// Handle SetLanguage prompt confirmation.
594    fn handle_set_language(&mut self, input: &str) {
595        use crate::primitives::highlight_engine::HighlightEngine;
596        use crate::primitives::highlighter::Language;
597
598        let trimmed = input.trim();
599
600        // Check for "Plain Text" (no highlighting)
601        if trimmed == "Plain Text" || trimmed.to_lowercase() == "text" {
602            let buffer_id = self.active_buffer();
603            if let Some(state) = self.buffers.get_mut(&buffer_id) {
604                state.language = "Plain Text".to_string();
605                state.highlighter = HighlightEngine::None;
606                self.set_status_message("Language set to Plain Text".to_string());
607            }
608            return;
609        }
610
611        // Try to find the syntax by name in the grammar registry
612        // This supports all syntect syntaxes (100+) plus user-configured grammars
613        if self.grammar_registry.find_syntax_by_name(trimmed).is_some() {
614            // Try to detect a tree-sitter language for non-highlighting features
615            // (indentation, semantic highlighting). This is best-effort since
616            // tree-sitter only supports ~18 languages while syntect supports 100+.
617            let ts_language = Language::from_name(trimmed);
618
619            let buffer_id = self.active_buffer();
620            if let Some(state) = self.buffers.get_mut(&buffer_id) {
621                state.language = trimmed.to_string();
622                state.highlighter =
623                    HighlightEngine::for_syntax_name(trimmed, &self.grammar_registry, ts_language);
624                // Update reference highlighter if tree-sitter language is available
625                if let Some(lang) = ts_language {
626                    state.reference_highlighter.set_language(&lang);
627                }
628                self.set_status_message(format!("Language set to {}", trimmed));
629            }
630        } else {
631            self.set_status_message(format!("Unknown language: {}", input));
632        }
633    }
634
635    /// Handle register-based input (macros, bookmarks).
636    fn handle_register_input<F>(&mut self, input: &str, action: F, register_type: &str)
637    where
638        F: FnOnce(&mut Self, char),
639    {
640        if let Some(c) = input.trim().chars().next() {
641            if c.is_ascii_digit() {
642                action(self, c);
643            } else {
644                self.set_status_message(
645                    t!("register.must_be_digit", "type" = register_type).to_string(),
646                );
647            }
648        } else {
649            self.set_status_message(t!("register.not_specified").to_string());
650        }
651    }
652
653    /// Handle ConfirmCloseBuffer prompt. Returns true if early return is needed.
654    fn handle_confirm_close_buffer(&mut self, input: &str, buffer_id: BufferId) -> bool {
655        let input_lower = input.trim().to_lowercase();
656        let save_key = t!("prompt.key.save").to_string().to_lowercase();
657        let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
658
659        let first_char = input_lower.chars().next();
660        let save_first = save_key.chars().next();
661        let discard_first = discard_key.chars().next();
662
663        if first_char == save_first {
664            // Save and close
665            let has_path = self
666                .buffers
667                .get(&buffer_id)
668                .map(|s| s.buffer.file_path().is_some())
669                .unwrap_or(false);
670
671            if has_path {
672                let old_active = self.active_buffer();
673                self.set_active_buffer(buffer_id);
674                if let Err(e) = self.save() {
675                    self.set_status_message(
676                        t!("file.save_failed", error = e.to_string()).to_string(),
677                    );
678                    self.set_active_buffer(old_active);
679                    return true; // Early return
680                }
681                self.set_active_buffer(old_active);
682                if let Err(e) = self.force_close_buffer(buffer_id) {
683                    self.set_status_message(
684                        t!("file.cannot_close", error = e.to_string()).to_string(),
685                    );
686                } else {
687                    self.set_status_message(t!("buffer.saved_and_closed").to_string());
688                }
689            } else {
690                self.pending_close_buffer = Some(buffer_id);
691                self.start_prompt_with_initial_text(
692                    t!("file.save_as_prompt").to_string(),
693                    PromptType::SaveFileAs,
694                    String::new(),
695                );
696            }
697        } else if first_char == discard_first {
698            // Discard and close
699            if let Err(e) = self.force_close_buffer(buffer_id) {
700                self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
701            } else {
702                self.set_status_message(t!("buffer.changes_discarded").to_string());
703            }
704        } else {
705            self.set_status_message(t!("buffer.close_cancelled").to_string());
706        }
707        false
708    }
709
710    /// Handle StopLspServer prompt confirmation.
711    fn handle_stop_lsp_server(&mut self, input: &str) {
712        let language = input.trim();
713        if language.is_empty() {
714            return;
715        }
716
717        if let Some(lsp) = &mut self.lsp {
718            if lsp.shutdown_server(language) {
719                if let Some(lsp_config) = self.config.lsp.get_mut(language) {
720                    lsp_config.auto_start = false;
721                    if let Err(e) = self.save_config() {
722                        tracing::warn!(
723                            "Failed to save config after disabling LSP auto-start: {}",
724                            e
725                        );
726                    } else {
727                        let config_path = self.dir_context.config_path();
728                        self.emit_event(
729                            "config_changed",
730                            serde_json::json!({
731                                "path": config_path.to_string_lossy(),
732                            }),
733                        );
734                    }
735                }
736                self.set_status_message(t!("lsp.server_stopped", language = language).to_string());
737            } else {
738                self.set_status_message(
739                    t!("lsp.server_not_found", language = language).to_string(),
740                );
741            }
742        }
743    }
744
745    /// Handle Quick Open prompt confirmation based on prefix routing
746    fn handle_quick_open_confirm(
747        &mut self,
748        input: &str,
749        selected_index: Option<usize>,
750    ) -> PromptResult {
751        // Determine the mode based on prefix
752        if input.starts_with('>') {
753            // Command mode - find and execute the selected command
754            let query = &input[1..];
755            return self.handle_quick_open_command(query, selected_index);
756        }
757
758        if input.starts_with('#') {
759            // Buffer mode - switch to selected buffer
760            let query = &input[1..];
761            return self.handle_quick_open_buffer(query, selected_index);
762        }
763
764        if input.starts_with(':') {
765            // Go to line mode
766            let line_str = &input[1..];
767            if let Ok(line_num) = line_str.parse::<usize>() {
768                if line_num > 0 {
769                    self.goto_line_col(line_num, None);
770                    self.set_status_message(t!("goto.jumped", line = line_num).to_string());
771                } else {
772                    self.set_status_message(t!("goto.line_must_be_positive").to_string());
773                }
774            } else {
775                self.set_status_message(t!("error.invalid_line", input = line_str).to_string());
776            }
777            return PromptResult::Done;
778        }
779
780        // Default: file mode - open the selected file
781        self.handle_quick_open_file(input, selected_index)
782    }
783
784    /// Handle Quick Open command selection
785    fn handle_quick_open_command(
786        &mut self,
787        query: &str,
788        selected_index: Option<usize>,
789    ) -> PromptResult {
790        let suggestions = {
791            let registry = self.command_registry.read().unwrap();
792            let selection_active = self.has_active_selection();
793            let active_buffer_mode = self
794                .buffer_metadata
795                .get(&self.active_buffer())
796                .and_then(|m| m.virtual_mode());
797
798            registry.filter(
799                query,
800                self.key_context,
801                &self.keybindings,
802                selection_active,
803                &self.active_custom_contexts,
804                active_buffer_mode,
805            )
806        };
807
808        if let Some(idx) = selected_index {
809            if let Some(suggestion) = suggestions.get(idx) {
810                if suggestion.disabled {
811                    self.set_status_message(t!("status.command_not_available").to_string());
812                    return PromptResult::Done;
813                }
814
815                // Find and execute the command
816                let commands = self.command_registry.read().unwrap().get_all();
817                if let Some(cmd) = commands
818                    .iter()
819                    .find(|c| c.get_localized_name() == suggestion.text)
820                {
821                    let action = cmd.action.clone();
822                    let cmd_name = cmd.get_localized_name();
823                    self.command_registry
824                        .write()
825                        .unwrap()
826                        .record_usage(&cmd_name);
827                    return PromptResult::ExecuteAction(action);
828                }
829            }
830        }
831
832        self.set_status_message(t!("status.no_selection").to_string());
833        PromptResult::Done
834    }
835
836    /// Handle Quick Open buffer selection
837    fn handle_quick_open_buffer(
838        &mut self,
839        query: &str,
840        selected_index: Option<usize>,
841    ) -> PromptResult {
842        // Regenerate buffer suggestions since prompt was already taken by confirm_prompt
843        let suggestions = self.get_buffer_suggestions(query);
844
845        if let Some(idx) = selected_index {
846            if let Some(suggestion) = suggestions.get(idx) {
847                if let Some(value) = &suggestion.value {
848                    if let Ok(buffer_id) = value.parse::<usize>() {
849                        let buffer_id = crate::model::event::BufferId(buffer_id);
850                        if self.buffers.contains_key(&buffer_id) {
851                            self.set_active_buffer(buffer_id);
852                            if let Some(name) = self.active_state().buffer.file_path() {
853                                self.set_status_message(
854                                    t!("buffer.switched", name = name.display().to_string())
855                                        .to_string(),
856                                );
857                            }
858                            return PromptResult::Done;
859                        }
860                    }
861                }
862            }
863        }
864
865        self.set_status_message(t!("status.no_selection").to_string());
866        PromptResult::Done
867    }
868
869    /// Handle Quick Open file selection
870    fn handle_quick_open_file(
871        &mut self,
872        input: &str,
873        selected_index: Option<usize>,
874    ) -> PromptResult {
875        // Regenerate file suggestions since prompt was already taken by confirm_prompt
876        let suggestions = self.get_file_suggestions(input);
877
878        if let Some(idx) = selected_index {
879            if let Some(suggestion) = suggestions.get(idx) {
880                if let Some(path_str) = &suggestion.value {
881                    let path = std::path::PathBuf::from(path_str);
882                    let full_path = if path.is_absolute() {
883                        path
884                    } else {
885                        self.working_dir.join(&path)
886                    };
887
888                    // Record file access for frecency
889                    self.file_provider.record_access(path_str);
890
891                    if let Err(e) = self.open_file(&full_path) {
892                        self.set_status_message(
893                            t!("file.error_opening", error = e.to_string()).to_string(),
894                        );
895                    } else {
896                        self.set_status_message(
897                            t!("buffer.opened", name = full_path.display().to_string()).to_string(),
898                        );
899                    }
900                    return PromptResult::Done;
901                }
902            }
903        }
904
905        self.set_status_message(t!("status.no_selection").to_string());
906        PromptResult::Done
907    }
908}