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 detected =
563                            crate::primitives::detected_language::DetectedLanguage::from_path(
564                                &full_path,
565                                &self.grammar_registry,
566                                &self.config.languages,
567                            );
568                        new_language = detected.name.clone();
569                        state.apply_language(detected);
570                        language_changed = new_language != "text";
571                    }
572                }
573                if language_changed {
574                    #[cfg(feature = "plugins")]
575                    self.update_plugin_state_snapshot();
576                    self.plugin_manager.run_hook(
577                        "language_changed",
578                        crate::services::plugins::hooks::HookArgs::LanguageChanged {
579                            buffer_id: self.active_buffer(),
580                            language: new_language,
581                        },
582                    );
583                }
584
585                self.active_event_log_mut().mark_saved();
586                tracing::debug!(
587                    "SaveFileAs AFTER mark_saved: event_log index={}, len={}",
588                    self.active_event_log().current_index(),
589                    self.active_event_log().len()
590                );
591
592                if let Ok(metadata) = self.filesystem.metadata(&full_path) {
593                    if let Some(mtime) = metadata.modified {
594                        self.file_mod_times.insert(full_path.clone(), mtime);
595                    }
596                }
597
598                self.notify_lsp_save();
599
600                self.emit_event(
601                    crate::model::control_event::events::FILE_SAVED.name,
602                    serde_json::json!({"path": full_path.display().to_string()}),
603                );
604
605                self.plugin_manager.run_hook(
606                    "after_file_save",
607                    crate::services::plugins::hooks::HookArgs::AfterFileSave {
608                        buffer_id: self.active_buffer(),
609                        path: full_path.clone(),
610                    },
611                );
612
613                if let Some(buffer_to_close) = self.pending_close_buffer.take() {
614                    if let Err(e) = self.force_close_buffer(buffer_to_close) {
615                        self.set_status_message(
616                            t!("file.saved_cannot_close", error = e.to_string()).to_string(),
617                        );
618                    } else {
619                        self.set_status_message(t!("buffer.saved_and_closed").to_string());
620                    }
621                } else {
622                    self.set_status_message(
623                        t!("file.saved_as", path = full_path.display().to_string()).to_string(),
624                    );
625                }
626            }
627            Err(e) => {
628                self.pending_close_buffer = None;
629                self.set_status_message(t!("file.error_saving", error = e.to_string()).to_string());
630            }
631        }
632    }
633
634    /// Handle SetPageWidth prompt confirmation.
635    fn handle_set_page_width(&mut self, input: &str) {
636        let active_split = self.split_manager.active_split();
637        let trimmed = input.trim();
638
639        if trimmed.is_empty() {
640            if let Some(vs) = self.split_view_states.get_mut(&active_split) {
641                vs.compose_width = None;
642            }
643            self.set_status_message(t!("settings.page_width_cleared").to_string());
644        } else {
645            match trimmed.parse::<u16>() {
646                Ok(val) if val > 0 => {
647                    if let Some(vs) = self.split_view_states.get_mut(&active_split) {
648                        vs.compose_width = Some(val);
649                    }
650                    self.set_status_message(t!("settings.page_width_set", value = val).to_string());
651                }
652                _ => {
653                    self.set_status_message(
654                        t!("error.invalid_page_width", input = input).to_string(),
655                    );
656                }
657            }
658        }
659    }
660
661    /// Handle AddRuler prompt confirmation.
662    fn handle_add_ruler(&mut self, input: &str) {
663        let trimmed = input.trim();
664        match trimmed.parse::<usize>() {
665            Ok(col) if col > 0 => {
666                let active_split = self.split_manager.active_split();
667                if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
668                    if !view_state.rulers.contains(&col) {
669                        view_state.rulers.push(col);
670                        view_state.rulers.sort();
671                    }
672                }
673                // Persist to user config
674                self.config.editor.rulers = self
675                    .split_view_states
676                    .get(&active_split)
677                    .map(|vs| vs.rulers.clone())
678                    .unwrap_or_default();
679                self.save_rulers_to_config();
680                self.set_status_message(t!("rulers.added", column = col).to_string());
681            }
682            Ok(_) => {
683                self.set_status_message(t!("rulers.must_be_positive").to_string());
684            }
685            Err(_) => {
686                self.set_status_message(t!("rulers.invalid_column", input = input).to_string());
687            }
688        }
689    }
690
691    /// Handle RemoveRuler prompt confirmation.
692    fn handle_remove_ruler(&mut self, input: &str) {
693        let trimmed = input.trim();
694        if let Ok(col) = trimmed.parse::<usize>() {
695            let active_split = self.split_manager.active_split();
696            if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
697                view_state.rulers.retain(|&r| r != col);
698            }
699            // Persist to user config
700            self.config.editor.rulers = self
701                .split_view_states
702                .get(&active_split)
703                .map(|vs| vs.rulers.clone())
704                .unwrap_or_default();
705            self.save_rulers_to_config();
706            self.set_status_message(t!("rulers.removed", column = col).to_string());
707        }
708    }
709
710    /// Save the current rulers setting to the user's config file
711    fn save_rulers_to_config(&mut self) {
712        if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.config_dir) {
713            tracing::warn!("Failed to create config directory: {}", e);
714            return;
715        }
716        let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
717        if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
718            tracing::warn!("Failed to save rulers to config: {}", e);
719        }
720    }
721
722    /// Handle SetTabSize prompt confirmation.
723    fn handle_set_tab_size(&mut self, input: &str) {
724        let buffer_id = self.active_buffer();
725        let trimmed = input.trim();
726
727        match trimmed.parse::<usize>() {
728            Ok(val) if val > 0 => {
729                if let Some(state) = self.buffers.get_mut(&buffer_id) {
730                    state.buffer_settings.tab_size = val;
731                }
732                self.set_status_message(t!("settings.tab_size_set", value = val).to_string());
733            }
734            Ok(_) => {
735                self.set_status_message(t!("settings.tab_size_positive").to_string());
736            }
737            Err(_) => {
738                self.set_status_message(t!("error.invalid_tab_size", input = input).to_string());
739            }
740        }
741    }
742
743    /// Handle SetLineEnding prompt confirmation.
744    fn handle_set_line_ending(&mut self, input: &str) {
745        use crate::model::buffer::LineEnding;
746
747        // Extract the line ending code from the input (e.g., "LF" from "LF (Unix/Linux/Mac)")
748        let trimmed = input.trim();
749        let code = trimmed.split_whitespace().next().unwrap_or(trimmed);
750
751        let line_ending = match code.to_uppercase().as_str() {
752            "LF" => Some(LineEnding::LF),
753            "CRLF" => Some(LineEnding::CRLF),
754            "CR" => Some(LineEnding::CR),
755            _ => None,
756        };
757
758        match line_ending {
759            Some(le) => {
760                self.active_state_mut().buffer.set_line_ending(le);
761                self.set_status_message(
762                    t!("settings.line_ending_set", value = le.display_name()).to_string(),
763                );
764            }
765            None => {
766                self.set_status_message(t!("error.unknown_line_ending", input = input).to_string());
767            }
768        }
769    }
770
771    /// Handle SetEncoding prompt confirmation.
772    fn handle_set_encoding(&mut self, input: &str) {
773        use crate::model::buffer::Encoding;
774
775        let trimmed = input.trim();
776
777        // First try to match the full input against encoding display names
778        // This handles multi-word names like "UTF-16 LE" and "UTF-8 BOM"
779        let encoding = Encoding::all()
780            .iter()
781            .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
782            .copied()
783            .or_else(|| {
784                // If no match, try extracting before the parenthesis (e.g., "UTF-8" from "UTF-8 (Unicode)")
785                let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
786                Encoding::all()
787                    .iter()
788                    .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
789                    .copied()
790            });
791
792        match encoding {
793            Some(enc) => {
794                self.active_state_mut().buffer.set_encoding(enc);
795                self.set_status_message(format!("Encoding set to {}", enc.display_name()));
796            }
797            None => {
798                self.set_status_message(format!("Unknown encoding: {}", input));
799            }
800        }
801    }
802
803    /// Handle OpenFileWithEncoding prompt confirmation.
804    /// Opens a file with a specific encoding (no auto-detection).
805    ///
806    /// For large files with non-resynchronizable encodings, shows a confirmation prompt
807    /// before loading the entire file into memory.
808    fn handle_open_file_with_encoding(&mut self, path: &std::path::Path, input: &str) {
809        use crate::model::buffer::Encoding;
810        use crate::view::prompt::PromptType;
811
812        let trimmed = input.trim();
813
814        // Parse the encoding from input
815        let encoding = Encoding::all()
816            .iter()
817            .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
818            .copied()
819            .or_else(|| {
820                let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
821                Encoding::all()
822                    .iter()
823                    .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
824                    .copied()
825            });
826
827        match encoding {
828            Some(enc) => {
829                // Check if this is a large file with non-resynchronizable encoding
830                // If so, show confirmation prompt before loading
831                let threshold = self.config.editor.large_file_threshold_bytes as usize;
832                let file_size = self
833                    .filesystem
834                    .metadata(path)
835                    .map(|m| m.size as usize)
836                    .unwrap_or(0);
837
838                if file_size >= threshold && enc.requires_full_file_load() {
839                    // Show confirmation prompt for large file with non-resynchronizable encoding
840                    let size_mb = file_size as f64 / (1024.0 * 1024.0);
841                    let load_key = t!("file.large_encoding.key.load").to_string();
842                    let encoding_key = t!("file.large_encoding.key.encoding").to_string();
843                    let cancel_key = t!("file.large_encoding.key.cancel").to_string();
844                    let prompt_msg = t!(
845                        "file.large_encoding_prompt",
846                        encoding = enc.display_name(),
847                        size = format!("{:.0}", size_mb),
848                        load_key = load_key,
849                        encoding_key = encoding_key,
850                        cancel_key = cancel_key
851                    )
852                    .to_string();
853                    self.start_prompt(
854                        prompt_msg,
855                        PromptType::ConfirmLargeFileEncoding {
856                            path: path.to_path_buf(),
857                        },
858                    );
859                    return;
860                }
861
862                // Reset key context to Normal so editor gets focus
863                self.key_context = crate::input::keybindings::KeyContext::Normal;
864
865                // Open the file with the specified encoding
866                if let Err(e) = self.open_file_with_encoding(path, enc) {
867                    self.set_status_message(
868                        t!("file.error_opening", error = e.to_string()).to_string(),
869                    );
870                } else {
871                    self.set_status_message(format!(
872                        "Opened {} with {} encoding",
873                        path.display(),
874                        enc.display_name()
875                    ));
876                }
877            }
878            None => {
879                self.set_status_message(format!("Unknown encoding: {}", input));
880            }
881        }
882    }
883
884    /// Handle ReloadWithEncoding prompt confirmation.
885    /// Reloads the current file with a specific encoding.
886    fn handle_reload_with_encoding(&mut self, input: &str) {
887        use crate::model::buffer::Encoding;
888
889        let trimmed = input.trim();
890
891        // Parse the encoding from input
892        let encoding = Encoding::all()
893            .iter()
894            .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
895            .copied()
896            .or_else(|| {
897                let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
898                Encoding::all()
899                    .iter()
900                    .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
901                    .copied()
902            });
903
904        match encoding {
905            Some(enc) => {
906                // Reload the file with the specified encoding
907                if let Err(e) = self.reload_with_encoding(enc) {
908                    self.set_status_message(format!("Failed to reload: {}", e));
909                } else {
910                    self.set_status_message(format!(
911                        "Reloaded with {} encoding",
912                        enc.display_name()
913                    ));
914                }
915            }
916            None => {
917                self.set_status_message(format!("Unknown encoding: {}", input));
918            }
919        }
920    }
921
922    /// Handle SetLanguage prompt confirmation.
923    fn handle_set_language(&mut self, input: &str) {
924        use crate::primitives::detected_language::DetectedLanguage;
925
926        let trimmed = input.trim();
927
928        // Check for "Plain Text" (no highlighting)
929        if trimmed == "Plain Text" || trimmed.to_lowercase() == "text" {
930            let buffer_id = self.active_buffer();
931            if let Some(state) = self.buffers.get_mut(&buffer_id) {
932                state.apply_language(DetectedLanguage::plain_text());
933                self.set_status_message("Language set to Plain Text".to_string());
934            }
935            #[cfg(feature = "plugins")]
936            self.update_plugin_state_snapshot();
937            self.plugin_manager.run_hook(
938                "language_changed",
939                crate::services::plugins::hooks::HookArgs::LanguageChanged {
940                    buffer_id: self.active_buffer(),
941                    language: "text".to_string(),
942                },
943            );
944            return;
945        }
946
947        // Try to find the syntax by name and resolve canonical language ID from config
948        if let Some(detected) = DetectedLanguage::from_syntax_name(
949            trimmed,
950            &self.grammar_registry,
951            &self.config.languages,
952        ) {
953            let language = detected.name.clone();
954            let buffer_id = self.active_buffer();
955            if let Some(state) = self.buffers.get_mut(&buffer_id) {
956                state.apply_language(detected);
957                self.set_status_message(format!("Language set to {}", trimmed));
958            }
959            #[cfg(feature = "plugins")]
960            self.update_plugin_state_snapshot();
961            self.plugin_manager.run_hook(
962                "language_changed",
963                crate::services::plugins::hooks::HookArgs::LanguageChanged {
964                    buffer_id,
965                    language,
966                },
967            );
968        } else if self.config.languages.contains_key(trimmed) {
969            // Handle user-configured languages without a matching syntect grammar
970            // (e.g. "fish" with grammar "fish" that isn't available in syntect).
971            // These languages won't have syntax highlighting but should still be
972            // selectable so the correct language ID is set for config/LSP purposes.
973            let detected = DetectedLanguage::from_config_language(trimmed);
974            let language = detected.name.clone();
975            let buffer_id = self.active_buffer();
976            if let Some(state) = self.buffers.get_mut(&buffer_id) {
977                state.apply_language(detected);
978                self.set_status_message(format!("Language set to {}", trimmed));
979            }
980            #[cfg(feature = "plugins")]
981            self.update_plugin_state_snapshot();
982            self.plugin_manager.run_hook(
983                "language_changed",
984                crate::services::plugins::hooks::HookArgs::LanguageChanged {
985                    buffer_id,
986                    language,
987                },
988            );
989        } else {
990            self.set_status_message(format!("Unknown language: {}", input));
991        }
992    }
993
994    /// Handle register-based input (macros, bookmarks).
995    fn handle_register_input<F>(&mut self, input: &str, action: F, register_type: &str)
996    where
997        F: FnOnce(&mut Self, char),
998    {
999        if let Some(c) = input.trim().chars().next() {
1000            if c.is_ascii_digit() {
1001                action(self, c);
1002            } else {
1003                self.set_status_message(
1004                    t!("register.must_be_digit", "type" = register_type).to_string(),
1005                );
1006            }
1007        } else {
1008            self.set_status_message(t!("register.not_specified").to_string());
1009        }
1010    }
1011
1012    /// Handle ConfirmCloseBuffer prompt. Returns true if early return is needed.
1013    fn handle_confirm_close_buffer(&mut self, input: &str, buffer_id: BufferId) -> bool {
1014        let input_lower = input.trim().to_lowercase();
1015        let save_key = t!("prompt.key.save").to_string().to_lowercase();
1016        let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1017
1018        let first_char = input_lower.chars().next();
1019        let save_first = save_key.chars().next();
1020        let discard_first = discard_key.chars().next();
1021
1022        if first_char == save_first {
1023            // Save and close
1024            let has_path = self
1025                .buffers
1026                .get(&buffer_id)
1027                .map(|s| s.buffer.file_path().is_some())
1028                .unwrap_or(false);
1029
1030            if has_path {
1031                let old_active = self.active_buffer();
1032                self.set_active_buffer(buffer_id);
1033                if let Err(e) = self.save() {
1034                    self.set_status_message(
1035                        t!("file.save_failed", error = e.to_string()).to_string(),
1036                    );
1037                    self.set_active_buffer(old_active);
1038                    return true; // Early return
1039                }
1040                self.set_active_buffer(old_active);
1041                if let Err(e) = self.force_close_buffer(buffer_id) {
1042                    self.set_status_message(
1043                        t!("file.cannot_close", error = e.to_string()).to_string(),
1044                    );
1045                } else {
1046                    self.set_status_message(t!("buffer.saved_and_closed").to_string());
1047                }
1048            } else {
1049                self.pending_close_buffer = Some(buffer_id);
1050                self.start_prompt_with_initial_text(
1051                    t!("file.save_as_prompt").to_string(),
1052                    PromptType::SaveFileAs,
1053                    String::new(),
1054                );
1055            }
1056        } else if first_char == discard_first {
1057            // Discard and close
1058            if let Err(e) = self.force_close_buffer(buffer_id) {
1059                self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
1060            } else {
1061                self.set_status_message(t!("buffer.changes_discarded").to_string());
1062            }
1063        } else {
1064            self.set_status_message(t!("buffer.close_cancelled").to_string());
1065        }
1066        false
1067    }
1068
1069    /// Handle ConfirmQuitWithModified prompt. Returns true if early return is needed.
1070    fn handle_confirm_quit_modified(&mut self, input: &str) -> bool {
1071        let input_lower = input.trim().to_lowercase();
1072        let save_key = t!("prompt.key.save").to_string().to_lowercase();
1073        let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1074        let quit_key = t!("prompt.key.quit").to_string().to_lowercase();
1075
1076        let first_char = input_lower.chars().next();
1077        let save_first = save_key.chars().next();
1078        let discard_first = discard_key.chars().next();
1079        let quit_first = quit_key.chars().next();
1080
1081        if first_char == save_first {
1082            // Save all modified file-backed buffers to disk, then quit
1083            match self.save_all_on_exit() {
1084                Ok(count) => {
1085                    tracing::info!("Saved {} buffer(s) on exit", count);
1086                    self.should_quit = true;
1087                }
1088                Err(e) => {
1089                    self.set_status_message(
1090                        t!("file.save_failed", error = e.to_string()).to_string(),
1091                    );
1092                    return true; // Early return, stay in editor
1093                }
1094            }
1095        } else if first_char == discard_first {
1096            // Discard changes and quit (no recovery)
1097            self.should_quit = true;
1098        } else if first_char == quit_first && self.config.editor.hot_exit {
1099            // Quit without saving — changes will be preserved via hot exit recovery
1100            self.should_quit = true;
1101        } else {
1102            // Cancel (default)
1103            self.set_status_message(t!("buffer.close_cancelled").to_string());
1104        }
1105        false
1106    }
1107
1108    /// Handle StopLspServer prompt confirmation.
1109    ///
1110    /// Input format: `"language"` (stops all servers) or `"language/server_name"`
1111    /// (stops a specific server).
1112    pub fn handle_stop_lsp_server(&mut self, input: &str) {
1113        let input = input.trim();
1114        if input.is_empty() {
1115            return;
1116        }
1117
1118        // Parse "language/server_name" or just "language"
1119        let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1120            (lang, Some(name))
1121        } else {
1122            (input, None)
1123        };
1124
1125        let has_server = self
1126            .lsp
1127            .as_ref()
1128            .is_some_and(|lsp| lsp.has_handles(language));
1129
1130        if !has_server {
1131            self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1132            return;
1133        }
1134
1135        // Check how many servers remain for this language after the stop.
1136        // If we're stopping a specific server and others remain, we should
1137        // only send didClose to that server, not disable LSP for the buffers.
1138        let stopping_all = server_name.is_none()
1139            || self
1140                .lsp
1141                .as_ref()
1142                .map(|lsp| lsp.handle_count(language) <= 1)
1143                .unwrap_or(true);
1144
1145        if stopping_all {
1146            // Send didClose for all buffers of this language BEFORE shutting
1147            // down the server, so the notifications reach the still-running
1148            // server and its handles are still present.
1149            let buffer_ids: Vec<_> = self
1150                .buffers
1151                .iter()
1152                .filter(|(_, s)| s.language == language)
1153                .map(|(id, _)| *id)
1154                .collect();
1155            for buffer_id in buffer_ids {
1156                self.disable_lsp_for_buffer(buffer_id);
1157            }
1158        } else if let Some(name) = server_name {
1159            // Send didClose only to the specific server being stopped
1160            self.send_did_close_to_server(language, name);
1161            // Clear diagnostics published by this server and update overlays
1162            self.clear_diagnostics_for_server(name);
1163        }
1164
1165        // Now shut down the server (removes handles).
1166        let stopped = if let Some(lsp) = &mut self.lsp {
1167            if let Some(name) = server_name {
1168                lsp.shutdown_server_by_name(language, name)
1169            } else {
1170                lsp.shutdown_server(language)
1171            }
1172        } else {
1173            false
1174        };
1175
1176        if !stopped {
1177            self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1178            return;
1179        }
1180
1181        // Update config: disable auto_start for the stopped server(s)
1182        if let Some(lsp_configs) = self.config.lsp.get_mut(language) {
1183            for c in lsp_configs.as_mut_slice() {
1184                if let Some(name) = server_name {
1185                    // Only disable auto_start for the specific server
1186                    if c.display_name() == name {
1187                        c.auto_start = false;
1188                    }
1189                } else {
1190                    c.auto_start = false;
1191                }
1192            }
1193            if let Err(e) = self.save_config() {
1194                tracing::warn!(
1195                    "Failed to save config after disabling LSP auto-start: {}",
1196                    e
1197                );
1198            } else {
1199                let config_path = self.dir_context.config_path();
1200                self.emit_event(
1201                    "config_changed",
1202                    serde_json::json!({
1203                        "path": config_path.to_string_lossy(),
1204                    }),
1205                );
1206            }
1207        }
1208
1209        let display = server_name.unwrap_or(language);
1210        self.set_status_message(t!("lsp.server_stopped", language = display).to_string());
1211    }
1212
1213    /// Handle RestartLspServer prompt confirmation.
1214    ///
1215    /// Input format: `"language"` (restarts all enabled servers) or
1216    /// `"language/server_name"` (restarts a specific server).
1217    pub fn handle_restart_lsp_server(&mut self, input: &str) {
1218        let input = input.trim();
1219        if input.is_empty() {
1220            return;
1221        }
1222
1223        // Parse "language/server_name" or just "language"
1224        let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1225            (lang, Some(name))
1226        } else {
1227            (input, None)
1228        };
1229
1230        // Get file_path from active buffer for workspace root detection
1231        let buffer_id = self.active_buffer();
1232        let file_path = self
1233            .buffer_metadata
1234            .get(&buffer_id)
1235            .and_then(|meta| meta.file_path().cloned());
1236
1237        let (success, message) = if let Some(name) = server_name {
1238            // Restart a specific server
1239            if let Some(lsp) = self.lsp.as_mut() {
1240                lsp.manual_restart_server(language, name, file_path.as_deref())
1241            } else {
1242                (false, t!("lsp.no_manager").to_string())
1243            }
1244        } else {
1245            // Restart all enabled servers for the language
1246            if let Some(lsp) = self.lsp.as_mut() {
1247                lsp.manual_restart(language, file_path.as_deref())
1248            } else {
1249                (false, t!("lsp.no_manager").to_string())
1250            }
1251        };
1252
1253        self.status_message = Some(message);
1254
1255        if success {
1256            self.reopen_buffers_for_language(language);
1257        }
1258    }
1259
1260    /// Handle Quick Open prompt confirmation by dispatching through the provider registry
1261    fn handle_quick_open_confirm(
1262        &mut self,
1263        input: &str,
1264        selected_index: Option<usize>,
1265    ) -> PromptResult {
1266        use crate::input::quick_open::QuickOpenResult;
1267
1268        let context = self.build_quick_open_context();
1269        let result = if let Some((provider, query)) =
1270            self.quick_open_registry.get_provider_for_input(input)
1271        {
1272            // Resolve the selected suggestion once, so providers don't recompute
1273            let suggestions = provider.suggestions(query, &context);
1274            let selected = selected_index.and_then(|i| suggestions.get(i));
1275            provider.on_select(selected, query, &context)
1276        } else {
1277            QuickOpenResult::None
1278        };
1279
1280        self.execute_quick_open_result(result)
1281    }
1282
1283    /// Map a QuickOpenResult to a PromptResult, executing any necessary side effects
1284    fn execute_quick_open_result(
1285        &mut self,
1286        result: crate::input::quick_open::QuickOpenResult,
1287    ) -> PromptResult {
1288        use crate::input::quick_open::QuickOpenResult;
1289
1290        match result {
1291            QuickOpenResult::ExecuteAction(action) => PromptResult::ExecuteAction(action),
1292            QuickOpenResult::OpenFile { path, line, column } => {
1293                let expanded_path = expand_tilde(&path);
1294                let full_path = if expanded_path.is_absolute() {
1295                    expanded_path
1296                } else {
1297                    self.working_dir.join(&expanded_path)
1298                };
1299                self.open_file_with_jump(full_path, line, column);
1300                PromptResult::Done
1301            }
1302            QuickOpenResult::ShowBuffer(buffer_id) => {
1303                let buffer_id = crate::model::event::BufferId(buffer_id);
1304                if self.buffers.contains_key(&buffer_id) {
1305                    self.set_active_buffer(buffer_id);
1306                    if let Some(name) = self.active_state().buffer.file_path() {
1307                        self.set_status_message(
1308                            t!("buffer.switched", name = name.display().to_string()).to_string(),
1309                        );
1310                    }
1311                }
1312                PromptResult::Done
1313            }
1314            QuickOpenResult::GotoLine(line) => {
1315                self.goto_line_col(line, None);
1316                self.set_status_message(t!("goto.jumped", line = line).to_string());
1317                PromptResult::Done
1318            }
1319            QuickOpenResult::None => {
1320                self.set_status_message(t!("status.no_selection").to_string());
1321                PromptResult::Done
1322            }
1323            QuickOpenResult::Error(msg) => {
1324                self.set_status_message(msg);
1325                PromptResult::Done
1326            }
1327        }
1328    }
1329
1330    fn open_file_with_jump(
1331        &mut self,
1332        full_path: std::path::PathBuf,
1333        line: Option<usize>,
1334        column: Option<usize>,
1335    ) {
1336        match self.open_file(&full_path) {
1337            Ok(_) => {
1338                if let Some(line) = line {
1339                    self.goto_line_col(line, column);
1340                }
1341                self.set_status_message(
1342                    t!("buffer.opened", name = full_path.display().to_string()).to_string(),
1343                );
1344            }
1345            Err(e) => {
1346                // Check if this is a large file encoding confirmation error
1347                if let Some(confirmation) =
1348                    e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
1349                {
1350                    self.start_large_file_encoding_confirmation(confirmation);
1351                } else {
1352                    self.set_status_message(
1353                        t!("file.error_opening", error = e.to_string()).to_string(),
1354                    );
1355                }
1356            }
1357        }
1358    }
1359}
1360
1361// ---------------------------------------------------------------------------
1362// Tests
1363// ---------------------------------------------------------------------------
1364
1365#[cfg(test)]
1366mod tests {
1367    use super::parse_path_line_col;
1368
1369    #[test]
1370    fn test_parse_path_line_col_empty() {
1371        let (path, line, col) = parse_path_line_col("");
1372        assert_eq!(path, "");
1373        assert_eq!(line, None);
1374        assert_eq!(col, None);
1375    }
1376
1377    #[test]
1378    fn test_parse_path_line_col_plain_path() {
1379        let (path, line, col) = parse_path_line_col("src/main.rs");
1380        assert_eq!(path, "src/main.rs");
1381        assert_eq!(line, None);
1382        assert_eq!(col, None);
1383    }
1384
1385    #[test]
1386    fn test_parse_path_line_col_line_only() {
1387        let (path, line, col) = parse_path_line_col("src/main.rs:42");
1388        assert_eq!(path, "src/main.rs");
1389        assert_eq!(line, Some(42));
1390        assert_eq!(col, None);
1391    }
1392
1393    #[test]
1394    fn test_parse_path_line_col_line_and_col() {
1395        let (path, line, col) = parse_path_line_col("src/main.rs:42:10");
1396        assert_eq!(path, "src/main.rs");
1397        assert_eq!(line, Some(42));
1398        assert_eq!(col, Some(10));
1399    }
1400
1401    #[test]
1402    fn test_parse_path_line_col_trimmed() {
1403        let (path, line, col) = parse_path_line_col("  src/main.rs:5:2  ");
1404        assert_eq!(path, "src/main.rs");
1405        assert_eq!(line, Some(5));
1406        assert_eq!(col, Some(2));
1407    }
1408
1409    #[test]
1410    fn test_parse_path_line_col_zero_line_rejected() {
1411        let (path, line, col) = parse_path_line_col("src/main.rs:0");
1412        assert_eq!(path, "src/main.rs:0");
1413        assert_eq!(line, None);
1414        assert_eq!(col, None);
1415    }
1416
1417    #[test]
1418    fn test_parse_path_line_col_zero_col_rejected() {
1419        let (path, line, col) = parse_path_line_col("src/main.rs:1:0");
1420        assert_eq!(path, "src/main.rs:1:0");
1421        assert_eq!(line, None);
1422        assert_eq!(col, None);
1423    }
1424
1425    #[cfg(windows)]
1426    #[test]
1427    fn test_parse_path_line_col_windows_drive() {
1428        let (path, line, col) = parse_path_line_col(r"C:\src\main.rs:12:3");
1429        assert_eq!(path, r"C:\src\main.rs");
1430        assert_eq!(line, Some(12));
1431        assert_eq!(col, Some(3));
1432    }
1433}