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::config_io::{ConfigLayer, ConfigResolver};
12use crate::input::keybindings::Action;
13use crate::primitives::path_utils::expand_tilde;
14use crate::services::plugins::hooks::HookArgs;
15use crate::view::prompt::PromptType;
16
17/// Result of handling a prompt confirmation.
18pub enum PromptResult {
19    /// Prompt handled, continue normally
20    Done,
21    /// Prompt handled, should execute this action next
22    ExecuteAction(Action),
23    /// Prompt handled, should return early from handle_action
24    EarlyReturn,
25}
26
27pub(super) fn parse_path_line_col(input: &str) -> (String, Option<usize>, Option<usize>) {
28    use std::path::{Component, Path};
29
30    let trimmed = input.trim();
31    if trimmed.is_empty() {
32        return (String::new(), None, None);
33    }
34
35    // Check if the path has a Windows drive prefix using std::path
36    let has_prefix = Path::new(trimmed)
37        .components()
38        .next()
39        .map(|c| matches!(c, Component::Prefix(_)))
40        .unwrap_or(false);
41
42    // Calculate where to start looking for :line:col
43    let search_start = if has_prefix {
44        trimmed.find(':').map(|i| i + 1).unwrap_or(0)
45    } else {
46        0
47    };
48
49    let suffix = &trimmed[search_start..];
50    let parts: Vec<&str> = suffix.rsplitn(3, ':').collect();
51
52    match parts.as_slice() {
53        [maybe_col, maybe_line, rest] => {
54            if !rest.is_empty() {
55                if let (Ok(line), Ok(col)) =
56                    (maybe_line.parse::<usize>(), maybe_col.parse::<usize>())
57                {
58                    if line > 0 && col > 0 {
59                        let path_str = if has_prefix {
60                            format!("{}{}", &trimmed[..search_start], rest)
61                        } else {
62                            rest.to_string()
63                        };
64                        return (path_str, Some(line), Some(col));
65                    }
66                }
67            }
68        }
69        [maybe_line, rest] => {
70            if !rest.is_empty() {
71                if let Ok(line) = maybe_line.parse::<usize>() {
72                    if line > 0 {
73                        let path_str = if has_prefix {
74                            format!("{}{}", &trimmed[..search_start], rest)
75                        } else {
76                            rest.to_string()
77                        };
78                        return (path_str, Some(line), None);
79                    }
80                }
81            }
82        }
83        _ => {}
84    }
85
86    (trimmed.to_string(), None, None)
87}
88
89impl Editor {
90    /// Handle prompt confirmation based on the prompt type.
91    ///
92    /// Returns a `PromptResult` indicating what the caller should do next.
93    pub fn handle_prompt_confirm_input(
94        &mut self,
95        input: String,
96        prompt_type: PromptType,
97        selected_index: Option<usize>,
98    ) -> PromptResult {
99        match prompt_type {
100            PromptType::OpenFile => {
101                let (path_str, line, column) = parse_path_line_col(&input);
102                // Expand tilde to home directory first
103                let expanded_path = expand_tilde(&path_str);
104                let resolved_path = if expanded_path.is_absolute() {
105                    normalize_path(&expanded_path)
106                } else {
107                    normalize_path(&self.working_dir.join(&expanded_path))
108                };
109
110                self.open_file_with_jump(resolved_path, line, column);
111            }
112            PromptType::OpenFileWithEncoding { path } => {
113                self.handle_open_file_with_encoding(&path, &input);
114            }
115            PromptType::ReloadWithEncoding => {
116                self.handle_reload_with_encoding(&input);
117            }
118            PromptType::SwitchProject => {
119                // Expand tilde to home directory first
120                let expanded_path = expand_tilde(&input);
121                let resolved_path = if expanded_path.is_absolute() {
122                    normalize_path(&expanded_path)
123                } else {
124                    normalize_path(&self.working_dir.join(&expanded_path))
125                };
126
127                if resolved_path.is_dir() {
128                    self.change_working_dir(resolved_path);
129                } else {
130                    self.set_status_message(
131                        t!(
132                            "file.not_directory",
133                            path = resolved_path.display().to_string()
134                        )
135                        .to_string(),
136                    );
137                }
138            }
139            PromptType::SaveFileAs => {
140                self.handle_save_file_as(&input);
141            }
142            PromptType::Search => {
143                self.perform_search(&input);
144            }
145            PromptType::ReplaceSearch => {
146                self.perform_search(&input);
147                self.start_prompt(
148                    t!("replace.prompt", search = &input).to_string(),
149                    PromptType::Replace {
150                        search: input.clone(),
151                    },
152                );
153            }
154            PromptType::Replace { search } => {
155                if self.search_confirm_each {
156                    self.start_interactive_replace(&search, &input);
157                } else {
158                    self.perform_replace(&search, &input);
159                }
160            }
161            PromptType::QueryReplaceSearch => {
162                self.perform_search(&input);
163                self.start_prompt(
164                    t!("replace.query_prompt", search = &input).to_string(),
165                    PromptType::QueryReplace {
166                        search: input.clone(),
167                    },
168                );
169            }
170            PromptType::QueryReplace { search } => {
171                if self.search_confirm_each {
172                    self.start_interactive_replace(&search, &input);
173                } else {
174                    self.perform_replace(&search, &input);
175                }
176            }
177            PromptType::Command => {
178                let commands = self.command_registry.read().unwrap().get_all();
179                if let Some(cmd) = commands.iter().find(|c| c.get_localized_name() == input) {
180                    let action = cmd.action.clone();
181                    let cmd_name = cmd.get_localized_name();
182                    self.command_registry
183                        .write()
184                        .unwrap()
185                        .record_usage(&cmd_name);
186                    return PromptResult::ExecuteAction(action);
187                } else {
188                    self.set_status_message(
189                        t!("error.unknown_command", input = &input).to_string(),
190                    );
191                }
192            }
193            PromptType::GotoLine => match input.trim().parse::<usize>() {
194                Ok(line_num) if line_num > 0 => {
195                    self.goto_line_col(line_num, None);
196                    self.set_status_message(t!("goto.jumped", line = line_num).to_string());
197                }
198                Ok(_) => {
199                    self.set_status_message(t!("goto.line_must_be_positive").to_string());
200                }
201                Err(_) => {
202                    self.set_status_message(t!("error.invalid_line", input = &input).to_string());
203                }
204            },
205            PromptType::GotoByteOffset => {
206                // Parse byte offset — strip optional trailing 'B' or 'b' suffix
207                let trimmed = input.trim();
208                let num_str = trimmed
209                    .strip_suffix('B')
210                    .or_else(|| trimmed.strip_suffix('b'))
211                    .unwrap_or(trimmed);
212                match num_str.parse::<usize>() {
213                    Ok(offset) => {
214                        self.goto_byte_offset(offset);
215                        self.set_status_message(
216                            t!("goto.jumped_byte", offset = offset).to_string(),
217                        );
218                    }
219                    Err(_) => {
220                        self.set_status_message(
221                            t!("goto.invalid_byte_offset", input = &input).to_string(),
222                        );
223                    }
224                }
225            }
226            PromptType::GotoLineScanConfirm => {
227                let answer = input.trim().to_lowercase();
228                if answer == "y" || answer == "yes" {
229                    // Start incremental scan (non-blocking, updates progress in status bar)
230                    self.start_incremental_line_scan(true);
231                    // The GotoLine prompt will be opened when the scan completes
232                    // (in process_line_scan)
233                } else {
234                    // No scan — open byte offset prompt (exact byte navigation)
235                    self.start_prompt(
236                        t!("goto.byte_offset_prompt").to_string(),
237                        PromptType::GotoByteOffset,
238                    );
239                }
240            }
241            PromptType::QuickOpen => {
242                // Handle Quick Open confirmation based on prefix
243                return self.handle_quick_open_confirm(&input, selected_index);
244            }
245            PromptType::SetBackgroundFile => {
246                if let Err(e) = self.load_ansi_background(&input) {
247                    self.set_status_message(
248                        t!("error.background_load_failed", error = e.to_string()).to_string(),
249                    );
250                }
251            }
252            PromptType::SetBackgroundBlend => match input.trim().parse::<f32>() {
253                Ok(val) => {
254                    let clamped = val.clamp(0.0, 1.0);
255                    self.background_fade = clamped;
256                    self.set_status_message(
257                        t!(
258                            "error.background_blend_set",
259                            value = format!("{:.2}", clamped)
260                        )
261                        .to_string(),
262                    );
263                }
264                Err(_) => {
265                    self.set_status_message(t!("error.invalid_blend", input = &input).to_string());
266                }
267            },
268            PromptType::SetPageWidth => {
269                self.handle_set_page_width(&input);
270            }
271            PromptType::RecordMacro => {
272                self.handle_register_input(
273                    &input,
274                    |editor, c| editor.toggle_macro_recording(c),
275                    "Macro",
276                );
277            }
278            PromptType::PlayMacro => {
279                self.handle_register_input(&input, |editor, c| editor.play_macro(c), "Macro");
280            }
281            PromptType::SetBookmark => {
282                self.handle_register_input(&input, |editor, c| editor.set_bookmark(c), "Bookmark");
283            }
284            PromptType::JumpToBookmark => {
285                self.handle_register_input(
286                    &input,
287                    |editor, c| editor.jump_to_bookmark(c),
288                    "Bookmark",
289                );
290            }
291            PromptType::Plugin { custom_type } => {
292                tracing::info!(
293                    "prompt_confirmed: dispatching hook for prompt_type='{}', input='{}', selected_index={:?}",
294                    custom_type, input, selected_index
295                );
296                self.plugin_manager.run_hook(
297                    "prompt_confirmed",
298                    HookArgs::PromptConfirmed {
299                        prompt_type: custom_type.clone(),
300                        input,
301                        selected_index,
302                    },
303                );
304                tracing::info!(
305                    "prompt_confirmed: hook dispatched for prompt_type='{}'",
306                    custom_type
307                );
308            }
309            PromptType::ConfirmRevert => {
310                let input_lower = input.trim().to_lowercase();
311                let revert_key = t!("prompt.key.revert").to_string().to_lowercase();
312                if input_lower == revert_key || input_lower == "revert" {
313                    if let Err(e) = self.revert_file() {
314                        self.set_status_message(
315                            t!("file.revert_failed", error = e.to_string()).to_string(),
316                        );
317                    }
318                } else {
319                    self.set_status_message(t!("buffer.revert_cancelled").to_string());
320                }
321            }
322            PromptType::ConfirmSaveConflict => {
323                let input_lower = input.trim().to_lowercase();
324                if input_lower == "o" || input_lower == "overwrite" {
325                    if let Err(e) = self.save() {
326                        self.set_status_message(
327                            t!("file.save_failed", error = e.to_string()).to_string(),
328                        );
329                    }
330                } else {
331                    self.set_status_message(t!("buffer.save_cancelled").to_string());
332                }
333            }
334            PromptType::ConfirmSudoSave { info } => {
335                let input_lower = input.trim().to_lowercase();
336                if input_lower == "y" || input_lower == "yes" {
337                    // Hide prompt before starting blocking command to clear the line
338                    self.cancel_prompt();
339
340                    // Read temp file and write via sudo (works for both local and remote)
341                    let result = (|| -> anyhow::Result<()> {
342                        let data = self.filesystem.read_file(&info.temp_path)?;
343                        self.filesystem.sudo_write(
344                            &info.dest_path,
345                            &data,
346                            info.mode,
347                            info.uid,
348                            info.gid,
349                        )?;
350                        // Best-effort cleanup of temp file.
351                        #[allow(clippy::let_underscore_must_use)]
352                        let _ = self.filesystem.remove_file(&info.temp_path);
353                        Ok(())
354                    })();
355
356                    match result {
357                        Ok(_) => {
358                            if let Err(e) = self
359                                .active_state_mut()
360                                .buffer
361                                .finalize_external_save(info.dest_path.clone())
362                            {
363                                tracing::warn!("Failed to finalize sudo save: {}", e);
364                                self.set_status_message(
365                                    t!("prompt.sudo_save_failed", error = e.to_string())
366                                        .to_string(),
367                                );
368                            } else if let Err(e) = self.finalize_save(Some(info.dest_path)) {
369                                tracing::warn!("Failed to finalize save after sudo: {}", e);
370                                self.set_status_message(
371                                    t!("prompt.sudo_save_failed", error = e.to_string())
372                                        .to_string(),
373                                );
374                            }
375                        }
376                        Err(e) => {
377                            tracing::warn!("Sudo save failed: {}", e);
378                            self.set_status_message(
379                                t!("prompt.sudo_save_failed", error = e.to_string()).to_string(),
380                            );
381                            // Best-effort cleanup of temp file.
382                            #[allow(clippy::let_underscore_must_use)]
383                            let _ = self.filesystem.remove_file(&info.temp_path);
384                        }
385                    }
386                } else {
387                    self.set_status_message(t!("buffer.save_cancelled").to_string());
388                    // Best-effort cleanup of temp file.
389                    #[allow(clippy::let_underscore_must_use)]
390                    let _ = self.filesystem.remove_file(&info.temp_path);
391                }
392            }
393            PromptType::ConfirmOverwriteFile { path } => {
394                let input_lower = input.trim().to_lowercase();
395                if input_lower == "o" || input_lower == "overwrite" {
396                    self.perform_save_file_as(path);
397                } else {
398                    self.set_status_message(t!("buffer.save_cancelled").to_string());
399                }
400            }
401            PromptType::ConfirmCloseBuffer { buffer_id } => {
402                if self.handle_confirm_close_buffer(&input, buffer_id) {
403                    return PromptResult::EarlyReturn;
404                }
405            }
406            PromptType::ConfirmQuitWithModified => {
407                if self.handle_confirm_quit_modified(&input) {
408                    return PromptResult::EarlyReturn;
409                }
410            }
411            PromptType::LspRename {
412                original_text,
413                start_pos,
414                end_pos: _,
415                overlay_handle,
416            } => {
417                self.perform_lsp_rename(input, original_text, start_pos, overlay_handle);
418            }
419            PromptType::FileExplorerRename {
420                original_path,
421                original_name,
422                is_new_file,
423            } => {
424                self.perform_file_explorer_rename(original_path, original_name, input, is_new_file);
425            }
426            PromptType::ConfirmDeleteFile { path, is_dir } => {
427                let input_lower = input.trim().to_lowercase();
428                if input_lower == "y" || input_lower == "yes" {
429                    self.perform_file_explorer_delete(path, is_dir);
430                } else {
431                    self.set_status_message(t!("explorer.delete_cancelled").to_string());
432                }
433            }
434            PromptType::ConfirmLargeFileEncoding { path } => {
435                let input_lower = input.trim().to_lowercase();
436                let load_key = t!("file.large_encoding.key.load")
437                    .to_string()
438                    .to_lowercase();
439                let encoding_key = t!("file.large_encoding.key.encoding")
440                    .to_string()
441                    .to_lowercase();
442                let cancel_key = t!("file.large_encoding.key.cancel")
443                    .to_string()
444                    .to_lowercase();
445                // Default (empty input or load key) loads the file
446                if input_lower.is_empty() || input_lower == load_key {
447                    if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
448                        self.set_status_message(
449                            t!("file.error_opening", error = e.to_string()).to_string(),
450                        );
451                    }
452                } else if input_lower == encoding_key {
453                    // Let user pick a different encoding
454                    self.start_open_file_with_encoding_prompt(path);
455                } else if input_lower == cancel_key {
456                    self.set_status_message(t!("file.open_cancelled").to_string());
457                } else {
458                    // Unknown input - default to load
459                    if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
460                        self.set_status_message(
461                            t!("file.error_opening", error = e.to_string()).to_string(),
462                        );
463                    }
464                }
465            }
466            PromptType::StopLspServer => {
467                self.handle_stop_lsp_server(&input);
468            }
469            PromptType::RestartLspServer => {
470                self.handle_restart_lsp_server(&input);
471            }
472            PromptType::SelectTheme { .. } => {
473                self.apply_theme(input.trim());
474            }
475            PromptType::SelectKeybindingMap => {
476                self.apply_keybinding_map(input.trim());
477            }
478            PromptType::SelectCursorStyle => {
479                self.apply_cursor_style(input.trim());
480            }
481            PromptType::SelectLocale => {
482                self.apply_locale(input.trim());
483            }
484            PromptType::CopyWithFormattingTheme => {
485                self.copy_selection_with_theme(input.trim());
486            }
487            PromptType::SwitchToTab => {
488                if let Ok(id) = input.trim().parse::<usize>() {
489                    self.switch_to_tab(BufferId(id));
490                }
491            }
492            PromptType::QueryReplaceConfirm => {
493                // This is handled by InsertChar, not PromptConfirm
494                // But if somehow Enter is pressed, treat it as skip (n)
495                if let Some(c) = input.chars().next() {
496                    if let Err(e) = self.handle_interactive_replace_key(c) {
497                        tracing::warn!("Interactive replace failed: {}", e);
498                    }
499                }
500            }
501            PromptType::AddRuler => {
502                self.handle_add_ruler(&input);
503            }
504            PromptType::RemoveRuler => {
505                self.handle_remove_ruler(&input);
506            }
507            PromptType::SetTabSize => {
508                self.handle_set_tab_size(&input);
509            }
510            PromptType::SetLineEnding => {
511                self.handle_set_line_ending(&input);
512            }
513            PromptType::SetEncoding => {
514                self.handle_set_encoding(&input);
515            }
516            PromptType::SetLanguage => {
517                self.handle_set_language(&input);
518            }
519            PromptType::ShellCommand { replace } => {
520                self.handle_shell_command(&input, replace);
521            }
522            PromptType::AsyncPrompt => {
523                // Resolve the pending async prompt callback with the input text
524                if let Some(callback_id) = self.pending_async_prompt_callback.take() {
525                    // Serialize the input as a JSON string
526                    let json = serde_json::to_string(&input).unwrap_or_else(|_| "null".to_string());
527                    self.plugin_manager.resolve_callback(callback_id, json);
528                }
529            }
530        }
531        PromptResult::Done
532    }
533
534    /// Handle SaveFileAs prompt confirmation.
535    fn handle_save_file_as(&mut self, input: &str) {
536        // Expand tilde to home directory first
537        let expanded_path = expand_tilde(input);
538        let full_path = if expanded_path.is_absolute() {
539            normalize_path(&expanded_path)
540        } else {
541            normalize_path(&self.working_dir.join(&expanded_path))
542        };
543
544        // Check if we're saving to a different file that already exists
545        let current_file_path = self
546            .active_state()
547            .buffer
548            .file_path()
549            .map(|p| p.to_path_buf());
550        let is_different_file = current_file_path.as_ref() != Some(&full_path);
551
552        if is_different_file && full_path.is_file() {
553            // File exists and is different from current - ask for confirmation
554            let filename = full_path
555                .file_name()
556                .map(|n| n.to_string_lossy().to_string())
557                .unwrap_or_else(|| full_path.display().to_string());
558            self.start_prompt(
559                t!("buffer.overwrite_confirm", name = &filename).to_string(),
560                PromptType::ConfirmOverwriteFile { path: full_path },
561            );
562            return;
563        }
564
565        // Proceed with save
566        self.perform_save_file_as(full_path);
567    }
568
569    /// Perform the actual SaveFileAs operation (called after confirmation if needed).
570    pub(crate) fn perform_save_file_as(&mut self, full_path: std::path::PathBuf) {
571        let before_idx = self.active_event_log().current_index();
572        let before_len = self.active_event_log().len();
573        tracing::debug!(
574            "SaveFileAs BEFORE: event_log index={}, len={}",
575            before_idx,
576            before_len
577        );
578
579        match self.active_state_mut().buffer.save_to_file(&full_path) {
580            Ok(()) => {
581                let after_save_idx = self.active_event_log().current_index();
582                let after_save_len = self.active_event_log().len();
583                tracing::debug!(
584                    "SaveFileAs AFTER buffer.save_to_file: event_log index={}, len={}",
585                    after_save_idx,
586                    after_save_len
587                );
588
589                let metadata = BufferMetadata::with_file(full_path.clone(), &self.working_dir);
590                self.buffer_metadata.insert(self.active_buffer(), metadata);
591
592                // Auto-detect language if it's currently "text"
593                // This ensures syntax highlighting works immediately after "Save As"
594                let mut language_changed = false;
595                let mut new_language = String::new();
596                if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
597                    if state.language == "text" {
598                        let detected =
599                            crate::primitives::detected_language::DetectedLanguage::from_path(
600                                &full_path,
601                                &self.grammar_registry,
602                                &self.config.languages,
603                            );
604                        new_language = detected.name.clone();
605                        state.apply_language(detected);
606                        language_changed = new_language != "text";
607                    }
608                }
609                if language_changed {
610                    #[cfg(feature = "plugins")]
611                    self.update_plugin_state_snapshot();
612                    self.plugin_manager.run_hook(
613                        "language_changed",
614                        crate::services::plugins::hooks::HookArgs::LanguageChanged {
615                            buffer_id: self.active_buffer(),
616                            language: new_language,
617                        },
618                    );
619                }
620
621                self.active_event_log_mut().mark_saved();
622                tracing::debug!(
623                    "SaveFileAs AFTER mark_saved: event_log index={}, len={}",
624                    self.active_event_log().current_index(),
625                    self.active_event_log().len()
626                );
627
628                if let Ok(metadata) = self.filesystem.metadata(&full_path) {
629                    if let Some(mtime) = metadata.modified {
630                        self.file_mod_times.insert(full_path.clone(), mtime);
631                    }
632                }
633
634                self.notify_lsp_save();
635
636                self.emit_event(
637                    crate::model::control_event::events::FILE_SAVED.name,
638                    serde_json::json!({"path": full_path.display().to_string()}),
639                );
640
641                self.plugin_manager.run_hook(
642                    "after_file_save",
643                    crate::services::plugins::hooks::HookArgs::AfterFileSave {
644                        buffer_id: self.active_buffer(),
645                        path: full_path.clone(),
646                    },
647                );
648
649                if let Some(buffer_to_close) = self.pending_close_buffer.take() {
650                    if let Err(e) = self.force_close_buffer(buffer_to_close) {
651                        self.set_status_message(
652                            t!("file.saved_cannot_close", error = e.to_string()).to_string(),
653                        );
654                    } else {
655                        self.set_status_message(t!("buffer.saved_and_closed").to_string());
656                    }
657                } else {
658                    self.set_status_message(
659                        t!("file.saved_as", path = full_path.display().to_string()).to_string(),
660                    );
661                }
662            }
663            Err(e) => {
664                self.pending_close_buffer = None;
665                self.set_status_message(t!("file.error_saving", error = e.to_string()).to_string());
666            }
667        }
668    }
669
670    /// Handle SetPageWidth prompt confirmation.
671    fn handle_set_page_width(&mut self, input: &str) {
672        let active_split = self.split_manager.active_split();
673        let trimmed = input.trim();
674
675        if trimmed.is_empty() {
676            if let Some(vs) = self.split_view_states.get_mut(&active_split) {
677                vs.compose_width = None;
678            }
679            self.set_status_message(t!("settings.page_width_cleared").to_string());
680        } else {
681            match trimmed.parse::<u16>() {
682                Ok(val) if val > 0 => {
683                    if let Some(vs) = self.split_view_states.get_mut(&active_split) {
684                        vs.compose_width = Some(val);
685                    }
686                    self.set_status_message(t!("settings.page_width_set", value = val).to_string());
687                }
688                _ => {
689                    self.set_status_message(
690                        t!("error.invalid_page_width", input = input).to_string(),
691                    );
692                }
693            }
694        }
695    }
696
697    /// Handle AddRuler prompt confirmation.
698    fn handle_add_ruler(&mut self, input: &str) {
699        let trimmed = input.trim();
700        match trimmed.parse::<usize>() {
701            Ok(col) if col > 0 => {
702                let active_split = self.split_manager.active_split();
703                if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
704                    if !view_state.rulers.contains(&col) {
705                        view_state.rulers.push(col);
706                        view_state.rulers.sort();
707                    }
708                }
709                // Persist to user config
710                self.config.editor.rulers = self
711                    .split_view_states
712                    .get(&active_split)
713                    .map(|vs| vs.rulers.clone())
714                    .unwrap_or_default();
715                self.save_rulers_to_config();
716                self.set_status_message(t!("rulers.added", column = col).to_string());
717            }
718            Ok(_) => {
719                self.set_status_message(t!("rulers.must_be_positive").to_string());
720            }
721            Err(_) => {
722                self.set_status_message(t!("rulers.invalid_column", input = input).to_string());
723            }
724        }
725    }
726
727    /// Handle RemoveRuler prompt confirmation.
728    fn handle_remove_ruler(&mut self, input: &str) {
729        let trimmed = input.trim();
730        if let Ok(col) = trimmed.parse::<usize>() {
731            let active_split = self.split_manager.active_split();
732            if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
733                view_state.rulers.retain(|&r| r != col);
734            }
735            // Persist to user config
736            self.config.editor.rulers = self
737                .split_view_states
738                .get(&active_split)
739                .map(|vs| vs.rulers.clone())
740                .unwrap_or_default();
741            self.save_rulers_to_config();
742            self.set_status_message(t!("rulers.removed", column = col).to_string());
743        }
744    }
745
746    /// Save the current rulers setting to the user's config file
747    fn save_rulers_to_config(&mut self) {
748        if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.config_dir) {
749            tracing::warn!("Failed to create config directory: {}", e);
750            return;
751        }
752        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
753        if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
754            tracing::warn!("Failed to save rulers to config: {}", e);
755        }
756    }
757
758    /// Handle SetTabSize prompt confirmation.
759    fn handle_set_tab_size(&mut self, input: &str) {
760        let buffer_id = self.active_buffer();
761        let trimmed = input.trim();
762
763        match trimmed.parse::<usize>() {
764            Ok(val) if val > 0 => {
765                if let Some(state) = self.buffers.get_mut(&buffer_id) {
766                    state.buffer_settings.tab_size = val;
767                }
768                self.set_status_message(t!("settings.tab_size_set", value = val).to_string());
769            }
770            Ok(_) => {
771                self.set_status_message(t!("settings.tab_size_positive").to_string());
772            }
773            Err(_) => {
774                self.set_status_message(t!("error.invalid_tab_size", input = input).to_string());
775            }
776        }
777    }
778
779    /// Handle SetLineEnding prompt confirmation.
780    fn handle_set_line_ending(&mut self, input: &str) {
781        use crate::model::buffer::LineEnding;
782
783        // Extract the line ending code from the input (e.g., "LF" from "LF (Unix/Linux/Mac)")
784        let trimmed = input.trim();
785        let code = trimmed.split_whitespace().next().unwrap_or(trimmed);
786
787        let line_ending = match code.to_uppercase().as_str() {
788            "LF" => Some(LineEnding::LF),
789            "CRLF" => Some(LineEnding::CRLF),
790            "CR" => Some(LineEnding::CR),
791            _ => None,
792        };
793
794        match line_ending {
795            Some(le) => {
796                self.active_state_mut().buffer.set_line_ending(le);
797                self.set_status_message(
798                    t!("settings.line_ending_set", value = le.display_name()).to_string(),
799                );
800            }
801            None => {
802                self.set_status_message(t!("error.unknown_line_ending", input = input).to_string());
803            }
804        }
805    }
806
807    /// Handle SetEncoding prompt confirmation.
808    fn handle_set_encoding(&mut self, input: &str) {
809        use crate::model::buffer::Encoding;
810
811        let trimmed = input.trim();
812
813        // First try to match the full input against encoding display names
814        // This handles multi-word names like "UTF-16 LE" and "UTF-8 BOM"
815        let encoding = Encoding::all()
816            .iter()
817            .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
818            .copied()
819            .or_else(|| {
820                // If no match, try extracting before the parenthesis (e.g., "UTF-8" from "UTF-8 (Unicode)")
821                let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
822                Encoding::all()
823                    .iter()
824                    .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
825                    .copied()
826            });
827
828        match encoding {
829            Some(enc) => {
830                self.active_state_mut().buffer.set_encoding(enc);
831                self.set_status_message(format!("Encoding set to {}", enc.display_name()));
832            }
833            None => {
834                self.set_status_message(format!("Unknown encoding: {}", input));
835            }
836        }
837    }
838
839    /// Handle OpenFileWithEncoding prompt confirmation.
840    /// Opens a file with a specific encoding (no auto-detection).
841    ///
842    /// For large files with non-resynchronizable encodings, shows a confirmation prompt
843    /// before loading the entire file into memory.
844    fn handle_open_file_with_encoding(&mut self, path: &std::path::Path, input: &str) {
845        use crate::model::buffer::Encoding;
846        use crate::view::prompt::PromptType;
847
848        let trimmed = input.trim();
849
850        // Parse the encoding from input
851        let encoding = Encoding::all()
852            .iter()
853            .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
854            .copied()
855            .or_else(|| {
856                let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
857                Encoding::all()
858                    .iter()
859                    .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
860                    .copied()
861            });
862
863        match encoding {
864            Some(enc) => {
865                // Check if this is a large file with non-resynchronizable encoding
866                // If so, show confirmation prompt before loading
867                let threshold = self.config.editor.large_file_threshold_bytes as usize;
868                let file_size = self
869                    .filesystem
870                    .metadata(path)
871                    .map(|m| m.size as usize)
872                    .unwrap_or(0);
873
874                if file_size >= threshold && enc.requires_full_file_load() {
875                    // Show confirmation prompt for large file with non-resynchronizable encoding
876                    let size_mb = file_size as f64 / (1024.0 * 1024.0);
877                    let load_key = t!("file.large_encoding.key.load").to_string();
878                    let encoding_key = t!("file.large_encoding.key.encoding").to_string();
879                    let cancel_key = t!("file.large_encoding.key.cancel").to_string();
880                    let prompt_msg = t!(
881                        "file.large_encoding_prompt",
882                        encoding = enc.display_name(),
883                        size = format!("{:.0}", size_mb),
884                        load_key = load_key,
885                        encoding_key = encoding_key,
886                        cancel_key = cancel_key
887                    )
888                    .to_string();
889                    self.start_prompt(
890                        prompt_msg,
891                        PromptType::ConfirmLargeFileEncoding {
892                            path: path.to_path_buf(),
893                        },
894                    );
895                    return;
896                }
897
898                // Reset key context to Normal so editor gets focus
899                self.key_context = crate::input::keybindings::KeyContext::Normal;
900
901                // Open the file with the specified encoding
902                if let Err(e) = self.open_file_with_encoding(path, enc) {
903                    self.set_status_message(
904                        t!("file.error_opening", error = e.to_string()).to_string(),
905                    );
906                } else {
907                    self.set_status_message(format!(
908                        "Opened {} with {} encoding",
909                        path.display(),
910                        enc.display_name()
911                    ));
912                }
913            }
914            None => {
915                self.set_status_message(format!("Unknown encoding: {}", input));
916            }
917        }
918    }
919
920    /// Handle ReloadWithEncoding prompt confirmation.
921    /// Reloads the current file with a specific encoding.
922    fn handle_reload_with_encoding(&mut self, input: &str) {
923        use crate::model::buffer::Encoding;
924
925        let trimmed = input.trim();
926
927        // Parse the encoding from input
928        let encoding = Encoding::all()
929            .iter()
930            .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
931            .copied()
932            .or_else(|| {
933                let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
934                Encoding::all()
935                    .iter()
936                    .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
937                    .copied()
938            });
939
940        match encoding {
941            Some(enc) => {
942                // Reload the file with the specified encoding
943                if let Err(e) = self.reload_with_encoding(enc) {
944                    self.set_status_message(format!("Failed to reload: {}", e));
945                } else {
946                    self.set_status_message(format!(
947                        "Reloaded with {} encoding",
948                        enc.display_name()
949                    ));
950                }
951            }
952            None => {
953                self.set_status_message(format!("Unknown encoding: {}", input));
954            }
955        }
956    }
957
958    /// Handle SetLanguage prompt confirmation.
959    fn handle_set_language(&mut self, input: &str) {
960        use crate::primitives::detected_language::DetectedLanguage;
961
962        let trimmed = input.trim();
963
964        // Check for "Plain Text" (no highlighting)
965        if trimmed == "Plain Text" || trimmed.to_lowercase() == "text" {
966            let buffer_id = self.active_buffer();
967            if let Some(state) = self.buffers.get_mut(&buffer_id) {
968                state.apply_language(DetectedLanguage::plain_text());
969                self.set_status_message("Language set to Plain Text".to_string());
970            }
971            #[cfg(feature = "plugins")]
972            self.update_plugin_state_snapshot();
973            self.plugin_manager.run_hook(
974                "language_changed",
975                crate::services::plugins::hooks::HookArgs::LanguageChanged {
976                    buffer_id: self.active_buffer(),
977                    language: "text".to_string(),
978                },
979            );
980            return;
981        }
982
983        // Try to find the syntax by name and resolve canonical language ID from config
984        if let Some(detected) = DetectedLanguage::from_syntax_name(
985            trimmed,
986            &self.grammar_registry,
987            &self.config.languages,
988        ) {
989            let language = detected.name.clone();
990            let buffer_id = self.active_buffer();
991            if let Some(state) = self.buffers.get_mut(&buffer_id) {
992                state.apply_language(detected);
993                self.set_status_message(format!("Language set to {}", trimmed));
994            }
995            #[cfg(feature = "plugins")]
996            self.update_plugin_state_snapshot();
997            self.plugin_manager.run_hook(
998                "language_changed",
999                crate::services::plugins::hooks::HookArgs::LanguageChanged {
1000                    buffer_id,
1001                    language,
1002                },
1003            );
1004        } else if self.config.languages.contains_key(trimmed) {
1005            // Handle user-configured languages without a matching syntect grammar
1006            // (e.g. "fish" with grammar "fish" that isn't available in syntect).
1007            // These languages won't have syntax highlighting but should still be
1008            // selectable so the correct language ID is set for config/LSP purposes.
1009            let detected = DetectedLanguage::from_config_language(trimmed);
1010            let language = detected.name.clone();
1011            let buffer_id = self.active_buffer();
1012            if let Some(state) = self.buffers.get_mut(&buffer_id) {
1013                state.apply_language(detected);
1014                self.set_status_message(format!("Language set to {}", trimmed));
1015            }
1016            #[cfg(feature = "plugins")]
1017            self.update_plugin_state_snapshot();
1018            self.plugin_manager.run_hook(
1019                "language_changed",
1020                crate::services::plugins::hooks::HookArgs::LanguageChanged {
1021                    buffer_id,
1022                    language,
1023                },
1024            );
1025        } else {
1026            self.set_status_message(format!("Unknown language: {}", input));
1027        }
1028    }
1029
1030    /// Handle register-based input (macros, bookmarks).
1031    fn handle_register_input<F>(&mut self, input: &str, action: F, register_type: &str)
1032    where
1033        F: FnOnce(&mut Self, char),
1034    {
1035        if let Some(c) = input.trim().chars().next() {
1036            if c.is_ascii_digit() {
1037                action(self, c);
1038            } else {
1039                self.set_status_message(
1040                    t!("register.must_be_digit", "type" = register_type).to_string(),
1041                );
1042            }
1043        } else {
1044            self.set_status_message(t!("register.not_specified").to_string());
1045        }
1046    }
1047
1048    /// Handle ConfirmCloseBuffer prompt. Returns true if early return is needed.
1049    fn handle_confirm_close_buffer(&mut self, input: &str, buffer_id: BufferId) -> bool {
1050        let input_lower = input.trim().to_lowercase();
1051        let save_key = t!("prompt.key.save").to_string().to_lowercase();
1052        let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1053
1054        let first_char = input_lower.chars().next();
1055        let save_first = save_key.chars().next();
1056        let discard_first = discard_key.chars().next();
1057
1058        if first_char == save_first {
1059            // Save and close
1060            let has_path = self
1061                .buffers
1062                .get(&buffer_id)
1063                .map(|s| s.buffer.file_path().is_some())
1064                .unwrap_or(false);
1065
1066            if has_path {
1067                let old_active = self.active_buffer();
1068                self.set_active_buffer(buffer_id);
1069                if let Err(e) = self.save() {
1070                    self.set_status_message(
1071                        t!("file.save_failed", error = e.to_string()).to_string(),
1072                    );
1073                    self.set_active_buffer(old_active);
1074                    return true; // Early return
1075                }
1076                self.set_active_buffer(old_active);
1077                if let Err(e) = self.force_close_buffer(buffer_id) {
1078                    self.set_status_message(
1079                        t!("file.cannot_close", error = e.to_string()).to_string(),
1080                    );
1081                } else {
1082                    self.set_status_message(t!("buffer.saved_and_closed").to_string());
1083                }
1084            } else {
1085                self.pending_close_buffer = Some(buffer_id);
1086                self.start_prompt_with_initial_text(
1087                    t!("file.save_as_prompt").to_string(),
1088                    PromptType::SaveFileAs,
1089                    String::new(),
1090                );
1091            }
1092        } else if first_char == discard_first {
1093            // Discard and close
1094            if let Err(e) = self.force_close_buffer(buffer_id) {
1095                self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
1096            } else {
1097                self.set_status_message(t!("buffer.changes_discarded").to_string());
1098            }
1099        } else {
1100            self.set_status_message(t!("buffer.close_cancelled").to_string());
1101        }
1102        false
1103    }
1104
1105    /// Handle ConfirmQuitWithModified prompt. Returns true if early return is needed.
1106    fn handle_confirm_quit_modified(&mut self, input: &str) -> bool {
1107        let input_lower = input.trim().to_lowercase();
1108        let save_key = t!("prompt.key.save").to_string().to_lowercase();
1109        let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1110        let quit_key = t!("prompt.key.quit").to_string().to_lowercase();
1111
1112        let first_char = input_lower.chars().next();
1113        let save_first = save_key.chars().next();
1114        let discard_first = discard_key.chars().next();
1115        let quit_first = quit_key.chars().next();
1116
1117        if first_char == save_first {
1118            // Save all modified file-backed buffers to disk, then quit
1119            match self.save_all_on_exit() {
1120                Ok(count) => {
1121                    tracing::info!("Saved {} buffer(s) on exit", count);
1122                    self.should_quit = true;
1123                }
1124                Err(e) => {
1125                    self.set_status_message(
1126                        t!("file.save_failed", error = e.to_string()).to_string(),
1127                    );
1128                    return true; // Early return, stay in editor
1129                }
1130            }
1131        } else if first_char == discard_first {
1132            // Discard changes and quit (no recovery)
1133            self.should_quit = true;
1134        } else if first_char == quit_first && self.config.editor.hot_exit {
1135            // Quit without saving — changes will be preserved via hot exit recovery
1136            self.should_quit = true;
1137        } else {
1138            // Cancel (default)
1139            self.set_status_message(t!("buffer.close_cancelled").to_string());
1140        }
1141        false
1142    }
1143
1144    /// Handle StopLspServer prompt confirmation.
1145    ///
1146    /// Input format: `"language"` (stops all servers) or `"language/server_name"`
1147    /// (stops a specific server).
1148    pub fn handle_stop_lsp_server(&mut self, input: &str) {
1149        let input = input.trim();
1150        if input.is_empty() {
1151            return;
1152        }
1153
1154        // Parse "language/server_name" or just "language"
1155        let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1156            (lang, Some(name))
1157        } else {
1158            (input, None)
1159        };
1160
1161        let has_server = self
1162            .lsp
1163            .as_ref()
1164            .is_some_and(|lsp| !lsp.get_handles(language).is_empty());
1165
1166        if !has_server {
1167            self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1168            return;
1169        }
1170
1171        // Check how many servers remain for this language after the stop.
1172        // If we're stopping a specific server and others remain, we should
1173        // only send didClose to that server, not disable LSP for the buffers.
1174        let stopping_all = server_name.is_none()
1175            || self
1176                .lsp
1177                .as_ref()
1178                .map(|lsp| lsp.get_handles(language).len() <= 1)
1179                .unwrap_or(true);
1180
1181        if stopping_all {
1182            // Send didClose for all buffers of this language BEFORE shutting
1183            // down the server, so the notifications reach the still-running
1184            // server and its handles are still present.
1185            let buffer_ids: Vec<_> = self
1186                .buffers
1187                .iter()
1188                .filter(|(_, s)| s.language == language)
1189                .map(|(id, _)| *id)
1190                .collect();
1191            for buffer_id in buffer_ids {
1192                self.disable_lsp_for_buffer(buffer_id);
1193            }
1194        } else if let Some(name) = server_name {
1195            // Send didClose only to the specific server being stopped
1196            self.send_did_close_to_server(language, name);
1197            // Clear diagnostics published by this server and update overlays
1198            self.clear_diagnostics_for_server(name);
1199        }
1200
1201        // Now shut down the server (removes handles).
1202        let stopped = if let Some(lsp) = &mut self.lsp {
1203            if let Some(name) = server_name {
1204                lsp.shutdown_server_by_name(language, name)
1205            } else {
1206                lsp.shutdown_server(language)
1207            }
1208        } else {
1209            false
1210        };
1211
1212        if !stopped {
1213            self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1214            return;
1215        }
1216
1217        // Update config: disable auto_start for the stopped server(s)
1218        if let Some(lsp_configs) = self.config.lsp.get_mut(language) {
1219            for c in lsp_configs.as_mut_slice() {
1220                if let Some(name) = server_name {
1221                    // Only disable auto_start for the specific server
1222                    if c.display_name() == name {
1223                        c.auto_start = false;
1224                    }
1225                } else {
1226                    c.auto_start = false;
1227                }
1228            }
1229            if let Err(e) = self.save_config() {
1230                tracing::warn!(
1231                    "Failed to save config after disabling LSP auto-start: {}",
1232                    e
1233                );
1234            } else {
1235                let config_path = self.dir_context.config_path();
1236                self.emit_event(
1237                    "config_changed",
1238                    serde_json::json!({
1239                        "path": config_path.to_string_lossy(),
1240                    }),
1241                );
1242            }
1243        }
1244
1245        let display = server_name.unwrap_or(language);
1246        self.set_status_message(t!("lsp.server_stopped", language = display).to_string());
1247    }
1248
1249    /// Handle RestartLspServer prompt confirmation.
1250    ///
1251    /// Input format: `"language"` (restarts all enabled servers) or
1252    /// `"language/server_name"` (restarts a specific server).
1253    pub fn handle_restart_lsp_server(&mut self, input: &str) {
1254        let input = input.trim();
1255        if input.is_empty() {
1256            return;
1257        }
1258
1259        // Parse "language/server_name" or just "language"
1260        let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1261            (lang, Some(name))
1262        } else {
1263            (input, None)
1264        };
1265
1266        // Get file_path from active buffer for workspace root detection
1267        let buffer_id = self.active_buffer();
1268        let file_path = self
1269            .buffer_metadata
1270            .get(&buffer_id)
1271            .and_then(|meta| meta.file_path().cloned());
1272
1273        let (success, message) = if let Some(name) = server_name {
1274            // Restart a specific server
1275            if let Some(lsp) = self.lsp.as_mut() {
1276                lsp.manual_restart_server(language, name, file_path.as_deref())
1277            } else {
1278                (false, t!("lsp.no_manager").to_string())
1279            }
1280        } else {
1281            // Restart all enabled servers for the language
1282            if let Some(lsp) = self.lsp.as_mut() {
1283                lsp.manual_restart(language, file_path.as_deref())
1284            } else {
1285                (false, t!("lsp.no_manager").to_string())
1286            }
1287        };
1288
1289        self.status_message = Some(message);
1290
1291        if success {
1292            self.reopen_buffers_for_language(language);
1293        }
1294    }
1295
1296    /// Handle Quick Open prompt confirmation based on prefix routing
1297    fn handle_quick_open_confirm(
1298        &mut self,
1299        input: &str,
1300        selected_index: Option<usize>,
1301    ) -> PromptResult {
1302        // Determine the mode based on prefix
1303        if let Some(query) = input.strip_prefix('>') {
1304            // Command mode - find and execute the selected command
1305            return self.handle_quick_open_command(query, selected_index);
1306        }
1307
1308        if let Some(query) = input.strip_prefix('#') {
1309            // Buffer mode - switch to selected buffer
1310            return self.handle_quick_open_buffer(query, selected_index);
1311        }
1312
1313        if let Some(line_str) = input.strip_prefix(':') {
1314            // Go to line mode
1315            if let Ok(line_num) = line_str.parse::<usize>() {
1316                if line_num > 0 {
1317                    self.goto_line_col(line_num, None);
1318                    self.set_status_message(t!("goto.jumped", line = line_num).to_string());
1319                } else {
1320                    self.set_status_message(t!("goto.line_must_be_positive").to_string());
1321                }
1322            } else {
1323                self.set_status_message(t!("error.invalid_line", input = line_str).to_string());
1324            }
1325            return PromptResult::Done;
1326        }
1327
1328        // Default: file mode - open the selected file
1329        self.handle_quick_open_file(input, selected_index)
1330    }
1331
1332    /// Handle Quick Open command selection
1333    fn handle_quick_open_command(
1334        &mut self,
1335        query: &str,
1336        selected_index: Option<usize>,
1337    ) -> PromptResult {
1338        let suggestions = {
1339            let registry = self.command_registry.read().unwrap();
1340            let selection_active = self.has_active_selection();
1341            let active_buffer_mode = self
1342                .buffer_metadata
1343                .get(&self.active_buffer())
1344                .and_then(|m| m.virtual_mode());
1345            let has_lsp_config = {
1346                let language = self
1347                    .buffers
1348                    .get(&self.active_buffer())
1349                    .map(|s| s.language.as_str());
1350                language
1351                    .and_then(|lang| self.lsp.as_ref().and_then(|lsp| lsp.get_config(lang)))
1352                    .is_some()
1353            };
1354
1355            registry.filter(
1356                query,
1357                self.key_context.clone(),
1358                &self.keybindings,
1359                selection_active,
1360                &self.active_custom_contexts,
1361                active_buffer_mode,
1362                has_lsp_config,
1363            )
1364        };
1365
1366        if let Some(idx) = selected_index {
1367            if let Some(suggestion) = suggestions.get(idx) {
1368                if suggestion.disabled {
1369                    self.set_status_message(t!("status.command_not_available").to_string());
1370                    return PromptResult::Done;
1371                }
1372
1373                // Find and execute the command
1374                let commands = self.command_registry.read().unwrap().get_all();
1375                if let Some(cmd) = commands
1376                    .iter()
1377                    .find(|c| c.get_localized_name() == suggestion.text)
1378                {
1379                    let action = cmd.action.clone();
1380                    let cmd_name = cmd.get_localized_name();
1381                    self.command_registry
1382                        .write()
1383                        .unwrap()
1384                        .record_usage(&cmd_name);
1385                    return PromptResult::ExecuteAction(action);
1386                }
1387            }
1388        }
1389
1390        self.set_status_message(t!("status.no_selection").to_string());
1391        PromptResult::Done
1392    }
1393
1394    /// Handle Quick Open buffer selection
1395    fn handle_quick_open_buffer(
1396        &mut self,
1397        query: &str,
1398        selected_index: Option<usize>,
1399    ) -> PromptResult {
1400        // Regenerate buffer suggestions since prompt was already taken by confirm_prompt
1401        let suggestions = self.get_buffer_suggestions(query);
1402
1403        if let Some(idx) = selected_index {
1404            if let Some(suggestion) = suggestions.get(idx) {
1405                if let Some(value) = &suggestion.value {
1406                    if let Ok(buffer_id) = value.parse::<usize>() {
1407                        let buffer_id = crate::model::event::BufferId(buffer_id);
1408                        if self.buffers.contains_key(&buffer_id) {
1409                            self.set_active_buffer(buffer_id);
1410                            if let Some(name) = self.active_state().buffer.file_path() {
1411                                self.set_status_message(
1412                                    t!("buffer.switched", name = name.display().to_string())
1413                                        .to_string(),
1414                                );
1415                            }
1416                            return PromptResult::Done;
1417                        }
1418                    }
1419                }
1420            }
1421        }
1422
1423        self.set_status_message(t!("status.no_selection").to_string());
1424        PromptResult::Done
1425    }
1426
1427    fn open_file_with_jump(
1428        &mut self,
1429        full_path: std::path::PathBuf,
1430        line: Option<usize>,
1431        column: Option<usize>,
1432    ) {
1433        match self.open_file(&full_path) {
1434            Ok(_) => {
1435                if let Some(line) = line {
1436                    self.goto_line_col(line, column);
1437                }
1438                self.set_status_message(
1439                    t!("buffer.opened", name = full_path.display().to_string()).to_string(),
1440                );
1441            }
1442            Err(e) => {
1443                // Check if this is a large file encoding confirmation error
1444                if let Some(confirmation) =
1445                    e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
1446                {
1447                    self.start_large_file_encoding_confirmation(confirmation);
1448                } else {
1449                    self.set_status_message(
1450                        t!("file.error_opening", error = e.to_string()).to_string(),
1451                    );
1452                }
1453            }
1454        }
1455    }
1456
1457    /// Handle Quick Open file selection
1458    fn handle_quick_open_file(
1459        &mut self,
1460        input: &str,
1461        selected_index: Option<usize>,
1462    ) -> PromptResult {
1463        let (path_from_input, line, column) = parse_path_line_col(input);
1464        // Regenerate file suggestions using the parsed path (without :line:col suffix)
1465        // so that fuzzy matching still works when the user types a jump suffix.
1466        let suggestion_input = if path_from_input.is_empty() {
1467            input
1468        } else {
1469            &path_from_input
1470        };
1471        let suggestions = self.get_file_suggestions(suggestion_input);
1472
1473        if let Some(idx) = selected_index {
1474            if let Some(suggestion) = suggestions.get(idx) {
1475                if let Some(path_str) = &suggestion.value {
1476                    let path = std::path::PathBuf::from(path_str);
1477                    let full_path = if path.is_absolute() {
1478                        path
1479                    } else {
1480                        self.working_dir.join(&path)
1481                    };
1482
1483                    // Record file access for frecency
1484                    self.file_provider.record_access(path_str);
1485
1486                    self.open_file_with_jump(full_path, line, column);
1487                    return PromptResult::Done;
1488                }
1489            }
1490        }
1491
1492        if line.is_some() && !path_from_input.is_empty() {
1493            let expanded_path = expand_tilde(&path_from_input);
1494            let full_path = if expanded_path.is_absolute() {
1495                expanded_path
1496            } else {
1497                self.working_dir.join(&expanded_path)
1498            };
1499
1500            // Record file access for frecency
1501            self.file_provider.record_access(&path_from_input);
1502
1503            self.open_file_with_jump(full_path, line, column);
1504            return PromptResult::Done;
1505        }
1506
1507        self.set_status_message(t!("status.no_selection").to_string());
1508        PromptResult::Done
1509    }
1510}
1511
1512// ---------------------------------------------------------------------------
1513// Tests
1514// ---------------------------------------------------------------------------
1515
1516#[cfg(test)]
1517mod tests {
1518    use super::parse_path_line_col;
1519
1520    #[test]
1521    fn test_parse_path_line_col_empty() {
1522        let (path, line, col) = parse_path_line_col("");
1523        assert_eq!(path, "");
1524        assert_eq!(line, None);
1525        assert_eq!(col, None);
1526    }
1527
1528    #[test]
1529    fn test_parse_path_line_col_plain_path() {
1530        let (path, line, col) = parse_path_line_col("src/main.rs");
1531        assert_eq!(path, "src/main.rs");
1532        assert_eq!(line, None);
1533        assert_eq!(col, None);
1534    }
1535
1536    #[test]
1537    fn test_parse_path_line_col_line_only() {
1538        let (path, line, col) = parse_path_line_col("src/main.rs:42");
1539        assert_eq!(path, "src/main.rs");
1540        assert_eq!(line, Some(42));
1541        assert_eq!(col, None);
1542    }
1543
1544    #[test]
1545    fn test_parse_path_line_col_line_and_col() {
1546        let (path, line, col) = parse_path_line_col("src/main.rs:42:10");
1547        assert_eq!(path, "src/main.rs");
1548        assert_eq!(line, Some(42));
1549        assert_eq!(col, Some(10));
1550    }
1551
1552    #[test]
1553    fn test_parse_path_line_col_trimmed() {
1554        let (path, line, col) = parse_path_line_col("  src/main.rs:5:2  ");
1555        assert_eq!(path, "src/main.rs");
1556        assert_eq!(line, Some(5));
1557        assert_eq!(col, Some(2));
1558    }
1559
1560    #[test]
1561    fn test_parse_path_line_col_zero_line_rejected() {
1562        let (path, line, col) = parse_path_line_col("src/main.rs:0");
1563        assert_eq!(path, "src/main.rs:0");
1564        assert_eq!(line, None);
1565        assert_eq!(col, None);
1566    }
1567
1568    #[test]
1569    fn test_parse_path_line_col_zero_col_rejected() {
1570        let (path, line, col) = parse_path_line_col("src/main.rs:1:0");
1571        assert_eq!(path, "src/main.rs:1:0");
1572        assert_eq!(line, None);
1573        assert_eq!(col, None);
1574    }
1575
1576    #[cfg(windows)]
1577    #[test]
1578    fn test_parse_path_line_col_windows_drive() {
1579        let (path, line, col) = parse_path_line_col(r"C:\src\main.rs:12:3");
1580        assert_eq!(path, r"C:\src\main.rs");
1581        assert_eq!(line, Some(12));
1582        assert_eq!(col, Some(3));
1583    }
1584}