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