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::SetComposeWidth => {
269                self.handle_set_compose_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                let input_lower = input.trim().to_lowercase();
408                let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
409                if input_lower == discard_key || input_lower == "discard" {
410                    self.should_quit = true;
411                } else {
412                    self.set_status_message(t!("buffer.close_cancelled").to_string());
413                }
414            }
415            PromptType::LspRename {
416                original_text,
417                start_pos,
418                end_pos: _,
419                overlay_handle,
420            } => {
421                self.perform_lsp_rename(input, original_text, start_pos, overlay_handle);
422            }
423            PromptType::FileExplorerRename {
424                original_path,
425                original_name,
426                is_new_file,
427            } => {
428                self.perform_file_explorer_rename(original_path, original_name, input, is_new_file);
429            }
430            PromptType::ConfirmDeleteFile { path, is_dir } => {
431                let input_lower = input.trim().to_lowercase();
432                if input_lower == "y" || input_lower == "yes" {
433                    self.perform_file_explorer_delete(path, is_dir);
434                } else {
435                    self.set_status_message(t!("explorer.delete_cancelled").to_string());
436                }
437            }
438            PromptType::ConfirmLargeFileEncoding { path } => {
439                let input_lower = input.trim().to_lowercase();
440                let load_key = t!("file.large_encoding.key.load")
441                    .to_string()
442                    .to_lowercase();
443                let encoding_key = t!("file.large_encoding.key.encoding")
444                    .to_string()
445                    .to_lowercase();
446                let cancel_key = t!("file.large_encoding.key.cancel")
447                    .to_string()
448                    .to_lowercase();
449                // Default (empty input or load key) loads the file
450                if input_lower.is_empty() || input_lower == load_key {
451                    if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
452                        self.set_status_message(
453                            t!("file.error_opening", error = e.to_string()).to_string(),
454                        );
455                    }
456                } else if input_lower == encoding_key {
457                    // Let user pick a different encoding
458                    self.start_open_file_with_encoding_prompt(path);
459                } else if input_lower == cancel_key {
460                    self.set_status_message(t!("file.open_cancelled").to_string());
461                } else {
462                    // Unknown input - default to load
463                    if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
464                        self.set_status_message(
465                            t!("file.error_opening", error = e.to_string()).to_string(),
466                        );
467                    }
468                }
469            }
470            PromptType::StopLspServer => {
471                self.handle_stop_lsp_server(&input);
472            }
473            PromptType::SelectTheme { .. } => {
474                self.apply_theme(input.trim());
475            }
476            PromptType::SelectKeybindingMap => {
477                self.apply_keybinding_map(input.trim());
478            }
479            PromptType::SelectCursorStyle => {
480                self.apply_cursor_style(input.trim());
481            }
482            PromptType::SelectLocale => {
483                self.apply_locale(input.trim());
484            }
485            PromptType::CopyWithFormattingTheme => {
486                self.copy_selection_with_theme(input.trim());
487            }
488            PromptType::SwitchToTab => {
489                if let Ok(id) = input.trim().parse::<usize>() {
490                    self.switch_to_tab(BufferId(id));
491                }
492            }
493            PromptType::QueryReplaceConfirm => {
494                // This is handled by InsertChar, not PromptConfirm
495                // But if somehow Enter is pressed, treat it as skip (n)
496                if let Some(c) = input.chars().next() {
497                    if let Err(e) = self.handle_interactive_replace_key(c) {
498                        tracing::warn!("Interactive replace failed: {}", e);
499                    }
500                }
501            }
502            PromptType::AddRuler => {
503                self.handle_add_ruler(&input);
504            }
505            PromptType::RemoveRuler => {
506                self.handle_remove_ruler(&input);
507            }
508            PromptType::SetTabSize => {
509                self.handle_set_tab_size(&input);
510            }
511            PromptType::SetLineEnding => {
512                self.handle_set_line_ending(&input);
513            }
514            PromptType::SetEncoding => {
515                self.handle_set_encoding(&input);
516            }
517            PromptType::SetLanguage => {
518                self.handle_set_language(&input);
519            }
520            PromptType::ShellCommand { replace } => {
521                self.handle_shell_command(&input, replace);
522            }
523            PromptType::AsyncPrompt => {
524                // Resolve the pending async prompt callback with the input text
525                if let Some(callback_id) = self.pending_async_prompt_callback.take() {
526                    // Serialize the input as a JSON string
527                    let json = serde_json::to_string(&input).unwrap_or_else(|_| "null".to_string());
528                    self.plugin_manager.resolve_callback(callback_id, json);
529                }
530            }
531        }
532        PromptResult::Done
533    }
534
535    /// Handle SaveFileAs prompt confirmation.
536    fn handle_save_file_as(&mut self, input: &str) {
537        // Expand tilde to home directory first
538        let expanded_path = expand_tilde(input);
539        let full_path = if expanded_path.is_absolute() {
540            normalize_path(&expanded_path)
541        } else {
542            normalize_path(&self.working_dir.join(&expanded_path))
543        };
544
545        // Check if we're saving to a different file that already exists
546        let current_file_path = self
547            .active_state()
548            .buffer
549            .file_path()
550            .map(|p| p.to_path_buf());
551        let is_different_file = current_file_path.as_ref() != Some(&full_path);
552
553        if is_different_file && full_path.is_file() {
554            // File exists and is different from current - ask for confirmation
555            let filename = full_path
556                .file_name()
557                .map(|n| n.to_string_lossy().to_string())
558                .unwrap_or_else(|| full_path.display().to_string());
559            self.start_prompt(
560                t!("buffer.overwrite_confirm", name = &filename).to_string(),
561                PromptType::ConfirmOverwriteFile { path: full_path },
562            );
563            return;
564        }
565
566        // Proceed with save
567        self.perform_save_file_as(full_path);
568    }
569
570    /// Perform the actual SaveFileAs operation (called after confirmation if needed).
571    pub(crate) fn perform_save_file_as(&mut self, full_path: std::path::PathBuf) {
572        let before_idx = self.active_event_log().current_index();
573        let before_len = self.active_event_log().len();
574        tracing::debug!(
575            "SaveFileAs BEFORE: event_log index={}, len={}",
576            before_idx,
577            before_len
578        );
579
580        match self.active_state_mut().buffer.save_to_file(&full_path) {
581            Ok(()) => {
582                let after_save_idx = self.active_event_log().current_index();
583                let after_save_len = self.active_event_log().len();
584                tracing::debug!(
585                    "SaveFileAs AFTER buffer.save_to_file: event_log index={}, len={}",
586                    after_save_idx,
587                    after_save_len
588                );
589
590                let metadata = BufferMetadata::with_file(full_path.clone(), &self.working_dir);
591                self.buffer_metadata.insert(self.active_buffer(), metadata);
592
593                // Auto-detect language if it's currently "text"
594                // This ensures syntax highlighting works immediately after "Save As"
595                let mut language_changed = false;
596                let mut new_language = String::new();
597                if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
598                    if state.language == "text" {
599                        let detected =
600                            crate::primitives::detected_language::DetectedLanguage::from_path(
601                                &full_path,
602                                &self.grammar_registry,
603                                &self.config.languages,
604                            );
605                        new_language = detected.name.clone();
606                        state.apply_language(detected);
607                        language_changed = new_language != "text";
608                    }
609                }
610                if language_changed {
611                    #[cfg(feature = "plugins")]
612                    self.update_plugin_state_snapshot();
613                    self.plugin_manager.run_hook(
614                        "language_changed",
615                        crate::services::plugins::hooks::HookArgs::LanguageChanged {
616                            buffer_id: self.active_buffer(),
617                            language: new_language,
618                        },
619                    );
620                }
621
622                self.active_event_log_mut().mark_saved();
623                tracing::debug!(
624                    "SaveFileAs AFTER mark_saved: event_log index={}, len={}",
625                    self.active_event_log().current_index(),
626                    self.active_event_log().len()
627                );
628
629                if let Ok(metadata) = self.filesystem.metadata(&full_path) {
630                    if let Some(mtime) = metadata.modified {
631                        self.file_mod_times.insert(full_path.clone(), mtime);
632                    }
633                }
634
635                self.notify_lsp_save();
636
637                self.emit_event(
638                    crate::model::control_event::events::FILE_SAVED.name,
639                    serde_json::json!({"path": full_path.display().to_string()}),
640                );
641
642                self.plugin_manager.run_hook(
643                    "after_file_save",
644                    crate::services::plugins::hooks::HookArgs::AfterFileSave {
645                        buffer_id: self.active_buffer(),
646                        path: full_path.clone(),
647                    },
648                );
649
650                if let Some(buffer_to_close) = self.pending_close_buffer.take() {
651                    if let Err(e) = self.force_close_buffer(buffer_to_close) {
652                        self.set_status_message(
653                            t!("file.saved_cannot_close", error = e.to_string()).to_string(),
654                        );
655                    } else {
656                        self.set_status_message(t!("buffer.saved_and_closed").to_string());
657                    }
658                } else {
659                    self.set_status_message(
660                        t!("file.saved_as", path = full_path.display().to_string()).to_string(),
661                    );
662                }
663            }
664            Err(e) => {
665                self.pending_close_buffer = None;
666                self.set_status_message(t!("file.error_saving", error = e.to_string()).to_string());
667            }
668        }
669    }
670
671    /// Handle SetComposeWidth prompt confirmation.
672    fn handle_set_compose_width(&mut self, input: &str) {
673        let active_split = self.split_manager.active_split();
674        let trimmed = input.trim();
675
676        if trimmed.is_empty() {
677            if let Some(vs) = self.split_view_states.get_mut(&active_split) {
678                vs.compose_width = None;
679            }
680            self.set_status_message(t!("settings.compose_width_cleared").to_string());
681        } else {
682            match trimmed.parse::<u16>() {
683                Ok(val) if val > 0 => {
684                    if let Some(vs) = self.split_view_states.get_mut(&active_split) {
685                        vs.compose_width = Some(val);
686                    }
687                    self.set_status_message(
688                        t!("settings.compose_width_set", value = val).to_string(),
689                    );
690                }
691                _ => {
692                    self.set_status_message(
693                        t!("error.invalid_compose_width", input = input).to_string(),
694                    );
695                }
696            }
697        }
698    }
699
700    /// Handle AddRuler prompt confirmation.
701    fn handle_add_ruler(&mut self, input: &str) {
702        let trimmed = input.trim();
703        match trimmed.parse::<usize>() {
704            Ok(col) if col > 0 => {
705                let active_split = self.split_manager.active_split();
706                if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
707                    if !view_state.rulers.contains(&col) {
708                        view_state.rulers.push(col);
709                        view_state.rulers.sort();
710                    }
711                }
712                // Persist to user config
713                self.config.editor.rulers = self
714                    .split_view_states
715                    .get(&active_split)
716                    .map(|vs| vs.rulers.clone())
717                    .unwrap_or_default();
718                self.save_rulers_to_config();
719                self.set_status_message(t!("rulers.added", column = col).to_string());
720            }
721            Ok(_) => {
722                self.set_status_message(t!("rulers.must_be_positive").to_string());
723            }
724            Err(_) => {
725                self.set_status_message(t!("rulers.invalid_column", input = input).to_string());
726            }
727        }
728    }
729
730    /// Handle RemoveRuler prompt confirmation.
731    fn handle_remove_ruler(&mut self, input: &str) {
732        let trimmed = input.trim();
733        if let Ok(col) = trimmed.parse::<usize>() {
734            let active_split = self.split_manager.active_split();
735            if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
736                view_state.rulers.retain(|&r| r != col);
737            }
738            // Persist to user config
739            self.config.editor.rulers = self
740                .split_view_states
741                .get(&active_split)
742                .map(|vs| vs.rulers.clone())
743                .unwrap_or_default();
744            self.save_rulers_to_config();
745            self.set_status_message(t!("rulers.removed", column = col).to_string());
746        }
747    }
748
749    /// Save the current rulers setting to the user's config file
750    fn save_rulers_to_config(&mut self) {
751        if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.config_dir) {
752            tracing::warn!("Failed to create config directory: {}", e);
753            return;
754        }
755        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
756        if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
757            tracing::warn!("Failed to save rulers to config: {}", e);
758        }
759    }
760
761    /// Handle SetTabSize prompt confirmation.
762    fn handle_set_tab_size(&mut self, input: &str) {
763        let buffer_id = self.active_buffer();
764        let trimmed = input.trim();
765
766        match trimmed.parse::<usize>() {
767            Ok(val) if val > 0 => {
768                if let Some(state) = self.buffers.get_mut(&buffer_id) {
769                    state.buffer_settings.tab_size = val;
770                }
771                self.set_status_message(t!("settings.tab_size_set", value = val).to_string());
772            }
773            Ok(_) => {
774                self.set_status_message(t!("settings.tab_size_positive").to_string());
775            }
776            Err(_) => {
777                self.set_status_message(t!("error.invalid_tab_size", input = input).to_string());
778            }
779        }
780    }
781
782    /// Handle SetLineEnding prompt confirmation.
783    fn handle_set_line_ending(&mut self, input: &str) {
784        use crate::model::buffer::LineEnding;
785
786        // Extract the line ending code from the input (e.g., "LF" from "LF (Unix/Linux/Mac)")
787        let trimmed = input.trim();
788        let code = trimmed.split_whitespace().next().unwrap_or(trimmed);
789
790        let line_ending = match code.to_uppercase().as_str() {
791            "LF" => Some(LineEnding::LF),
792            "CRLF" => Some(LineEnding::CRLF),
793            "CR" => Some(LineEnding::CR),
794            _ => None,
795        };
796
797        match line_ending {
798            Some(le) => {
799                self.active_state_mut().buffer.set_line_ending(le);
800                self.set_status_message(
801                    t!("settings.line_ending_set", value = le.display_name()).to_string(),
802                );
803            }
804            None => {
805                self.set_status_message(t!("error.unknown_line_ending", input = input).to_string());
806            }
807        }
808    }
809
810    /// Handle SetEncoding prompt confirmation.
811    fn handle_set_encoding(&mut self, input: &str) {
812        use crate::model::buffer::Encoding;
813
814        let trimmed = input.trim();
815
816        // First try to match the full input against encoding display names
817        // This handles multi-word names like "UTF-16 LE" and "UTF-8 BOM"
818        let encoding = Encoding::all()
819            .iter()
820            .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
821            .copied()
822            .or_else(|| {
823                // If no match, try extracting before the parenthesis (e.g., "UTF-8" from "UTF-8 (Unicode)")
824                let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
825                Encoding::all()
826                    .iter()
827                    .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
828                    .copied()
829            });
830
831        match encoding {
832            Some(enc) => {
833                self.active_state_mut().buffer.set_encoding(enc);
834                self.set_status_message(format!("Encoding set to {}", enc.display_name()));
835            }
836            None => {
837                self.set_status_message(format!("Unknown encoding: {}", input));
838            }
839        }
840    }
841
842    /// Handle OpenFileWithEncoding prompt confirmation.
843    /// Opens a file with a specific encoding (no auto-detection).
844    ///
845    /// For large files with non-resynchronizable encodings, shows a confirmation prompt
846    /// before loading the entire file into memory.
847    fn handle_open_file_with_encoding(&mut self, path: &std::path::Path, input: &str) {
848        use crate::model::buffer::Encoding;
849        use crate::view::prompt::PromptType;
850
851        let trimmed = input.trim();
852
853        // Parse the encoding from input
854        let encoding = Encoding::all()
855            .iter()
856            .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
857            .copied()
858            .or_else(|| {
859                let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
860                Encoding::all()
861                    .iter()
862                    .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
863                    .copied()
864            });
865
866        match encoding {
867            Some(enc) => {
868                // Check if this is a large file with non-resynchronizable encoding
869                // If so, show confirmation prompt before loading
870                let threshold = self.config.editor.large_file_threshold_bytes as usize;
871                let file_size = self
872                    .filesystem
873                    .metadata(path)
874                    .map(|m| m.size as usize)
875                    .unwrap_or(0);
876
877                if file_size >= threshold && enc.requires_full_file_load() {
878                    // Show confirmation prompt for large file with non-resynchronizable encoding
879                    let size_mb = file_size as f64 / (1024.0 * 1024.0);
880                    let load_key = t!("file.large_encoding.key.load").to_string();
881                    let encoding_key = t!("file.large_encoding.key.encoding").to_string();
882                    let cancel_key = t!("file.large_encoding.key.cancel").to_string();
883                    let prompt_msg = t!(
884                        "file.large_encoding_prompt",
885                        encoding = enc.display_name(),
886                        size = format!("{:.0}", size_mb),
887                        load_key = load_key,
888                        encoding_key = encoding_key,
889                        cancel_key = cancel_key
890                    )
891                    .to_string();
892                    self.start_prompt(
893                        prompt_msg,
894                        PromptType::ConfirmLargeFileEncoding {
895                            path: path.to_path_buf(),
896                        },
897                    );
898                    return;
899                }
900
901                // Reset key context to Normal so editor gets focus
902                self.key_context = crate::input::keybindings::KeyContext::Normal;
903
904                // Open the file with the specified encoding
905                if let Err(e) = self.open_file_with_encoding(path, enc) {
906                    self.set_status_message(
907                        t!("file.error_opening", error = e.to_string()).to_string(),
908                    );
909                } else {
910                    self.set_status_message(format!(
911                        "Opened {} with {} encoding",
912                        path.display(),
913                        enc.display_name()
914                    ));
915                }
916            }
917            None => {
918                self.set_status_message(format!("Unknown encoding: {}", input));
919            }
920        }
921    }
922
923    /// Handle ReloadWithEncoding prompt confirmation.
924    /// Reloads the current file with a specific encoding.
925    fn handle_reload_with_encoding(&mut self, input: &str) {
926        use crate::model::buffer::Encoding;
927
928        let trimmed = input.trim();
929
930        // Parse the encoding from input
931        let encoding = Encoding::all()
932            .iter()
933            .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
934            .copied()
935            .or_else(|| {
936                let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
937                Encoding::all()
938                    .iter()
939                    .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
940                    .copied()
941            });
942
943        match encoding {
944            Some(enc) => {
945                // Reload the file with the specified encoding
946                if let Err(e) = self.reload_with_encoding(enc) {
947                    self.set_status_message(format!("Failed to reload: {}", e));
948                } else {
949                    self.set_status_message(format!(
950                        "Reloaded with {} encoding",
951                        enc.display_name()
952                    ));
953                }
954            }
955            None => {
956                self.set_status_message(format!("Unknown encoding: {}", input));
957            }
958        }
959    }
960
961    /// Resolve a syntect syntax name to the canonical config language ID.
962    ///
963    /// Resolve a syntect syntax display name to its canonical config language ID.
964    /// Delegates to the free function in `detected_language`.
965    pub(crate) fn resolve_language_id(&self, syntax_name: &str) -> Option<String> {
966        crate::primitives::detected_language::resolve_language_id(
967            syntax_name,
968            &self.grammar_registry,
969            &self.config.languages,
970        )
971    }
972
973    /// Handle SetLanguage prompt confirmation.
974    fn handle_set_language(&mut self, input: &str) {
975        use crate::primitives::detected_language::DetectedLanguage;
976
977        let trimmed = input.trim();
978
979        // Check for "Plain Text" (no highlighting)
980        if trimmed == "Plain Text" || trimmed.to_lowercase() == "text" {
981            let buffer_id = self.active_buffer();
982            if let Some(state) = self.buffers.get_mut(&buffer_id) {
983                state.apply_language(DetectedLanguage::plain_text());
984                self.set_status_message("Language set to Plain Text".to_string());
985            }
986            #[cfg(feature = "plugins")]
987            self.update_plugin_state_snapshot();
988            self.plugin_manager.run_hook(
989                "language_changed",
990                crate::services::plugins::hooks::HookArgs::LanguageChanged {
991                    buffer_id: self.active_buffer(),
992                    language: "text".to_string(),
993                },
994            );
995            return;
996        }
997
998        // Try to find the syntax by name and resolve canonical language ID from config
999        if let Some(detected) = DetectedLanguage::from_syntax_name(
1000            trimmed,
1001            &self.grammar_registry,
1002            &self.config.languages,
1003        ) {
1004            let language = detected.name.clone();
1005            let buffer_id = self.active_buffer();
1006            if let Some(state) = self.buffers.get_mut(&buffer_id) {
1007                state.apply_language(detected);
1008                self.set_status_message(format!("Language set to {}", trimmed));
1009            }
1010            #[cfg(feature = "plugins")]
1011            self.update_plugin_state_snapshot();
1012            self.plugin_manager.run_hook(
1013                "language_changed",
1014                crate::services::plugins::hooks::HookArgs::LanguageChanged {
1015                    buffer_id,
1016                    language,
1017                },
1018            );
1019        } else {
1020            self.set_status_message(format!("Unknown language: {}", input));
1021        }
1022    }
1023
1024    /// Handle register-based input (macros, bookmarks).
1025    fn handle_register_input<F>(&mut self, input: &str, action: F, register_type: &str)
1026    where
1027        F: FnOnce(&mut Self, char),
1028    {
1029        if let Some(c) = input.trim().chars().next() {
1030            if c.is_ascii_digit() {
1031                action(self, c);
1032            } else {
1033                self.set_status_message(
1034                    t!("register.must_be_digit", "type" = register_type).to_string(),
1035                );
1036            }
1037        } else {
1038            self.set_status_message(t!("register.not_specified").to_string());
1039        }
1040    }
1041
1042    /// Handle ConfirmCloseBuffer prompt. Returns true if early return is needed.
1043    fn handle_confirm_close_buffer(&mut self, input: &str, buffer_id: BufferId) -> bool {
1044        let input_lower = input.trim().to_lowercase();
1045        let save_key = t!("prompt.key.save").to_string().to_lowercase();
1046        let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1047
1048        let first_char = input_lower.chars().next();
1049        let save_first = save_key.chars().next();
1050        let discard_first = discard_key.chars().next();
1051
1052        if first_char == save_first {
1053            // Save and close
1054            let has_path = self
1055                .buffers
1056                .get(&buffer_id)
1057                .map(|s| s.buffer.file_path().is_some())
1058                .unwrap_or(false);
1059
1060            if has_path {
1061                let old_active = self.active_buffer();
1062                self.set_active_buffer(buffer_id);
1063                if let Err(e) = self.save() {
1064                    self.set_status_message(
1065                        t!("file.save_failed", error = e.to_string()).to_string(),
1066                    );
1067                    self.set_active_buffer(old_active);
1068                    return true; // Early return
1069                }
1070                self.set_active_buffer(old_active);
1071                if let Err(e) = self.force_close_buffer(buffer_id) {
1072                    self.set_status_message(
1073                        t!("file.cannot_close", error = e.to_string()).to_string(),
1074                    );
1075                } else {
1076                    self.set_status_message(t!("buffer.saved_and_closed").to_string());
1077                }
1078            } else {
1079                self.pending_close_buffer = Some(buffer_id);
1080                self.start_prompt_with_initial_text(
1081                    t!("file.save_as_prompt").to_string(),
1082                    PromptType::SaveFileAs,
1083                    String::new(),
1084                );
1085            }
1086        } else if first_char == discard_first {
1087            // Discard and close
1088            if let Err(e) = self.force_close_buffer(buffer_id) {
1089                self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
1090            } else {
1091                self.set_status_message(t!("buffer.changes_discarded").to_string());
1092            }
1093        } else {
1094            self.set_status_message(t!("buffer.close_cancelled").to_string());
1095        }
1096        false
1097    }
1098
1099    /// Handle StopLspServer prompt confirmation.
1100    fn handle_stop_lsp_server(&mut self, input: &str) {
1101        let language = input.trim();
1102        if language.is_empty() {
1103            return;
1104        }
1105
1106        if let Some(lsp) = &mut self.lsp {
1107            if lsp.shutdown_server(language) {
1108                if let Some(lsp_config) = self.config.lsp.get_mut(language) {
1109                    lsp_config.auto_start = false;
1110                    if let Err(e) = self.save_config() {
1111                        tracing::warn!(
1112                            "Failed to save config after disabling LSP auto-start: {}",
1113                            e
1114                        );
1115                    } else {
1116                        let config_path = self.dir_context.config_path();
1117                        self.emit_event(
1118                            "config_changed",
1119                            serde_json::json!({
1120                                "path": config_path.to_string_lossy(),
1121                            }),
1122                        );
1123                    }
1124                }
1125
1126                // Clear diagnostics and overlays for all buffers of this language
1127                let buffer_ids: Vec<_> = self
1128                    .buffers
1129                    .iter()
1130                    .filter(|(_, s)| s.language == language)
1131                    .map(|(id, _)| *id)
1132                    .collect();
1133                for buffer_id in buffer_ids {
1134                    self.disable_lsp_for_buffer(buffer_id);
1135                }
1136
1137                self.set_status_message(t!("lsp.server_stopped", language = language).to_string());
1138            } else {
1139                self.set_status_message(
1140                    t!("lsp.server_not_found", language = language).to_string(),
1141                );
1142            }
1143        }
1144    }
1145
1146    /// Handle Quick Open prompt confirmation based on prefix routing
1147    fn handle_quick_open_confirm(
1148        &mut self,
1149        input: &str,
1150        selected_index: Option<usize>,
1151    ) -> PromptResult {
1152        // Determine the mode based on prefix
1153        if let Some(query) = input.strip_prefix('>') {
1154            // Command mode - find and execute the selected command
1155            return self.handle_quick_open_command(query, selected_index);
1156        }
1157
1158        if let Some(query) = input.strip_prefix('#') {
1159            // Buffer mode - switch to selected buffer
1160            return self.handle_quick_open_buffer(query, selected_index);
1161        }
1162
1163        if let Some(line_str) = input.strip_prefix(':') {
1164            // Go to line mode
1165            if let Ok(line_num) = line_str.parse::<usize>() {
1166                if line_num > 0 {
1167                    self.goto_line_col(line_num, None);
1168                    self.set_status_message(t!("goto.jumped", line = line_num).to_string());
1169                } else {
1170                    self.set_status_message(t!("goto.line_must_be_positive").to_string());
1171                }
1172            } else {
1173                self.set_status_message(t!("error.invalid_line", input = line_str).to_string());
1174            }
1175            return PromptResult::Done;
1176        }
1177
1178        // Default: file mode - open the selected file
1179        self.handle_quick_open_file(input, selected_index)
1180    }
1181
1182    /// Handle Quick Open command selection
1183    fn handle_quick_open_command(
1184        &mut self,
1185        query: &str,
1186        selected_index: Option<usize>,
1187    ) -> PromptResult {
1188        let suggestions = {
1189            let registry = self.command_registry.read().unwrap();
1190            let selection_active = self.has_active_selection();
1191            let active_buffer_mode = self
1192                .buffer_metadata
1193                .get(&self.active_buffer())
1194                .and_then(|m| m.virtual_mode());
1195            let has_lsp_config = {
1196                let language = self
1197                    .buffers
1198                    .get(&self.active_buffer())
1199                    .map(|s| s.language.as_str());
1200                language
1201                    .and_then(|lang| self.lsp.as_ref().and_then(|lsp| lsp.get_config(lang)))
1202                    .is_some()
1203            };
1204
1205            registry.filter(
1206                query,
1207                self.key_context,
1208                &self.keybindings,
1209                selection_active,
1210                &self.active_custom_contexts,
1211                active_buffer_mode,
1212                has_lsp_config,
1213            )
1214        };
1215
1216        if let Some(idx) = selected_index {
1217            if let Some(suggestion) = suggestions.get(idx) {
1218                if suggestion.disabled {
1219                    self.set_status_message(t!("status.command_not_available").to_string());
1220                    return PromptResult::Done;
1221                }
1222
1223                // Find and execute the command
1224                let commands = self.command_registry.read().unwrap().get_all();
1225                if let Some(cmd) = commands
1226                    .iter()
1227                    .find(|c| c.get_localized_name() == suggestion.text)
1228                {
1229                    let action = cmd.action.clone();
1230                    let cmd_name = cmd.get_localized_name();
1231                    self.command_registry
1232                        .write()
1233                        .unwrap()
1234                        .record_usage(&cmd_name);
1235                    return PromptResult::ExecuteAction(action);
1236                }
1237            }
1238        }
1239
1240        self.set_status_message(t!("status.no_selection").to_string());
1241        PromptResult::Done
1242    }
1243
1244    /// Handle Quick Open buffer selection
1245    fn handle_quick_open_buffer(
1246        &mut self,
1247        query: &str,
1248        selected_index: Option<usize>,
1249    ) -> PromptResult {
1250        // Regenerate buffer suggestions since prompt was already taken by confirm_prompt
1251        let suggestions = self.get_buffer_suggestions(query);
1252
1253        if let Some(idx) = selected_index {
1254            if let Some(suggestion) = suggestions.get(idx) {
1255                if let Some(value) = &suggestion.value {
1256                    if let Ok(buffer_id) = value.parse::<usize>() {
1257                        let buffer_id = crate::model::event::BufferId(buffer_id);
1258                        if self.buffers.contains_key(&buffer_id) {
1259                            self.set_active_buffer(buffer_id);
1260                            if let Some(name) = self.active_state().buffer.file_path() {
1261                                self.set_status_message(
1262                                    t!("buffer.switched", name = name.display().to_string())
1263                                        .to_string(),
1264                                );
1265                            }
1266                            return PromptResult::Done;
1267                        }
1268                    }
1269                }
1270            }
1271        }
1272
1273        self.set_status_message(t!("status.no_selection").to_string());
1274        PromptResult::Done
1275    }
1276
1277    fn open_file_with_jump(
1278        &mut self,
1279        full_path: std::path::PathBuf,
1280        line: Option<usize>,
1281        column: Option<usize>,
1282    ) {
1283        match self.open_file(&full_path) {
1284            Ok(_) => {
1285                if let Some(line) = line {
1286                    self.goto_line_col(line, column);
1287                }
1288                self.set_status_message(
1289                    t!("buffer.opened", name = full_path.display().to_string()).to_string(),
1290                );
1291            }
1292            Err(e) => {
1293                // Check if this is a large file encoding confirmation error
1294                if let Some(confirmation) =
1295                    e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
1296                {
1297                    self.start_large_file_encoding_confirmation(confirmation);
1298                } else {
1299                    self.set_status_message(
1300                        t!("file.error_opening", error = e.to_string()).to_string(),
1301                    );
1302                }
1303            }
1304        }
1305    }
1306
1307    /// Handle Quick Open file selection
1308    fn handle_quick_open_file(
1309        &mut self,
1310        input: &str,
1311        selected_index: Option<usize>,
1312    ) -> PromptResult {
1313        let (path_from_input, line, column) = parse_path_line_col(input);
1314        // Regenerate file suggestions using the parsed path (without :line:col suffix)
1315        // so that fuzzy matching still works when the user types a jump suffix.
1316        let suggestion_input = if path_from_input.is_empty() {
1317            input
1318        } else {
1319            &path_from_input
1320        };
1321        let suggestions = self.get_file_suggestions(suggestion_input);
1322
1323        if let Some(idx) = selected_index {
1324            if let Some(suggestion) = suggestions.get(idx) {
1325                if let Some(path_str) = &suggestion.value {
1326                    let path = std::path::PathBuf::from(path_str);
1327                    let full_path = if path.is_absolute() {
1328                        path
1329                    } else {
1330                        self.working_dir.join(&path)
1331                    };
1332
1333                    // Record file access for frecency
1334                    self.file_provider.record_access(path_str);
1335
1336                    self.open_file_with_jump(full_path, line, column);
1337                    return PromptResult::Done;
1338                }
1339            }
1340        }
1341
1342        if line.is_some() && !path_from_input.is_empty() {
1343            let expanded_path = expand_tilde(&path_from_input);
1344            let full_path = if expanded_path.is_absolute() {
1345                expanded_path
1346            } else {
1347                self.working_dir.join(&expanded_path)
1348            };
1349
1350            // Record file access for frecency
1351            self.file_provider.record_access(&path_from_input);
1352
1353            self.open_file_with_jump(full_path, line, column);
1354            return PromptResult::Done;
1355        }
1356
1357        self.set_status_message(t!("status.no_selection").to_string());
1358        PromptResult::Done
1359    }
1360}
1361
1362// ---------------------------------------------------------------------------
1363// Tests
1364// ---------------------------------------------------------------------------
1365
1366#[cfg(test)]
1367mod tests {
1368    use super::parse_path_line_col;
1369
1370    #[test]
1371    fn test_parse_path_line_col_empty() {
1372        let (path, line, col) = parse_path_line_col("");
1373        assert_eq!(path, "");
1374        assert_eq!(line, None);
1375        assert_eq!(col, None);
1376    }
1377
1378    #[test]
1379    fn test_parse_path_line_col_plain_path() {
1380        let (path, line, col) = parse_path_line_col("src/main.rs");
1381        assert_eq!(path, "src/main.rs");
1382        assert_eq!(line, None);
1383        assert_eq!(col, None);
1384    }
1385
1386    #[test]
1387    fn test_parse_path_line_col_line_only() {
1388        let (path, line, col) = parse_path_line_col("src/main.rs:42");
1389        assert_eq!(path, "src/main.rs");
1390        assert_eq!(line, Some(42));
1391        assert_eq!(col, None);
1392    }
1393
1394    #[test]
1395    fn test_parse_path_line_col_line_and_col() {
1396        let (path, line, col) = parse_path_line_col("src/main.rs:42:10");
1397        assert_eq!(path, "src/main.rs");
1398        assert_eq!(line, Some(42));
1399        assert_eq!(col, Some(10));
1400    }
1401
1402    #[test]
1403    fn test_parse_path_line_col_trimmed() {
1404        let (path, line, col) = parse_path_line_col("  src/main.rs:5:2  ");
1405        assert_eq!(path, "src/main.rs");
1406        assert_eq!(line, Some(5));
1407        assert_eq!(col, Some(2));
1408    }
1409
1410    #[test]
1411    fn test_parse_path_line_col_zero_line_rejected() {
1412        let (path, line, col) = parse_path_line_col("src/main.rs:0");
1413        assert_eq!(path, "src/main.rs:0");
1414        assert_eq!(line, None);
1415        assert_eq!(col, None);
1416    }
1417
1418    #[test]
1419    fn test_parse_path_line_col_zero_col_rejected() {
1420        let (path, line, col) = parse_path_line_col("src/main.rs:1:0");
1421        assert_eq!(path, "src/main.rs:1:0");
1422        assert_eq!(line, None);
1423        assert_eq!(col, None);
1424    }
1425
1426    #[cfg(windows)]
1427    #[test]
1428    fn test_parse_path_line_col_windows_drive() {
1429        let (path, line, col) = parse_path_line_col(r"C:\src\main.rs:12:3");
1430        assert_eq!(path, r"C:\src\main.rs");
1431        assert_eq!(line, Some(12));
1432        assert_eq!(col, Some(3));
1433    }
1434}