1use 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
17pub enum PromptResult {
19 Done,
21 ExecuteAction(Action),
23 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
31pub(super) fn resolve_goto_line_target(
35 target: crate::input::quick_open::GotoLineTarget,
36 current_line: usize,
37 max_line: usize,
38) -> usize {
39 use crate::input::quick_open::GotoLineTarget;
40 let raw = match target {
41 GotoLineTarget::Absolute(n) => n,
42 GotoLineTarget::Relative(delta) => {
43 if delta >= 0 {
44 current_line.saturating_add(delta as usize)
45 } else {
46 current_line.saturating_sub(delta.unsigned_abs())
47 }
48 }
49 };
50 raw.clamp(1, max_line.max(1))
51}
52
53impl Editor {
54 pub fn handle_prompt_confirm_input(
58 &mut self,
59 input: String,
60 prompt_type: PromptType,
61 selected_index: Option<usize>,
62 ) -> PromptResult {
63 match prompt_type {
64 PromptType::OpenFile => {
65 let (path_str, line, column) = parse_path_line_col(&input);
66 let expanded_path = expand_tilde(&path_str);
68 let resolved_path = if expanded_path.is_absolute() {
69 normalize_path(&expanded_path)
70 } else {
71 normalize_path(&self.working_dir.join(&expanded_path))
72 };
73
74 self.open_file_with_jump(resolved_path, line, column);
75 }
76 PromptType::OpenFileWithEncoding { path } => {
77 self.handle_open_file_with_encoding(&path, &input);
78 }
79 PromptType::ReloadWithEncoding => {
80 self.handle_reload_with_encoding(&input);
81 }
82 PromptType::SwitchProject => {
83 let expanded_path = expand_tilde(&input);
85 let resolved_path = if expanded_path.is_absolute() {
86 normalize_path(&expanded_path)
87 } else {
88 normalize_path(&self.working_dir.join(&expanded_path))
89 };
90
91 if resolved_path.is_dir() {
92 self.change_working_dir(resolved_path);
93 } else {
94 self.set_status_message(
95 t!(
96 "file.not_directory",
97 path = resolved_path.display().to_string()
98 )
99 .to_string(),
100 );
101 }
102 }
103 PromptType::SaveFileAs => {
104 self.handle_save_file_as(&input);
105 }
106 PromptType::Search => {
107 self.perform_search(&input);
108 }
109 PromptType::ReplaceSearch => {
110 self.perform_search(&input);
111 self.start_prompt(
112 t!("replace.prompt", search = &input).to_string(),
113 PromptType::Replace {
114 search: input.clone(),
115 },
116 );
117 }
118 PromptType::Replace { search } => {
119 if self.search_confirm_each {
120 self.start_interactive_replace(&search, &input);
121 } else {
122 self.perform_replace(&search, &input);
123 }
124 }
125 PromptType::QueryReplaceSearch => {
126 self.perform_search(&input);
127 self.start_prompt(
128 t!("replace.query_prompt", search = &input).to_string(),
129 PromptType::QueryReplace {
130 search: input.clone(),
131 },
132 );
133 }
134 PromptType::QueryReplace { search } => {
135 if self.search_confirm_each {
136 self.start_interactive_replace(&search, &input);
137 } else {
138 self.perform_replace(&search, &input);
139 }
140 }
141 PromptType::GotoLine => {
142 let buffer_id = self.active_buffer();
143 if let Some(state) = self.buffers.get(&buffer_id) {
144 let max_line = state.buffer.line_count().unwrap_or(1);
145 let current_line = state.primary_cursor_line_number.value() + 1;
146 match crate::input::quick_open::parse_goto_line_input(&input) {
147 Some(target) => {
148 let line = resolve_goto_line_target(target, current_line, max_line);
149 self.goto_line_col(line, None);
150 self.set_status_message(t!("goto.jumped", line = line).to_string());
151 }
152 None => {
153 self.set_status_message(
154 t!("error.invalid_line", input = input.trim()).to_string(),
155 );
156 }
157 }
158 } else {
159 self.set_status_message(t!("status.no_selection").to_string());
160 }
161 }
162 PromptType::GotoByteOffset => {
163 let trimmed = input.trim();
165 let num_str = trimmed
166 .strip_suffix('B')
167 .or_else(|| trimmed.strip_suffix('b'))
168 .unwrap_or(trimmed);
169 match num_str.parse::<usize>() {
170 Ok(offset) => {
171 self.goto_byte_offset(offset);
172 self.set_status_message(
173 t!("goto.jumped_byte", offset = offset).to_string(),
174 );
175 }
176 Err(_) => {
177 self.set_status_message(
178 t!("goto.invalid_byte_offset", input = &input).to_string(),
179 );
180 }
181 }
182 }
183 PromptType::GotoLineScanConfirm => {
184 let answer = input.trim().to_lowercase();
185 if answer == "y" || answer == "yes" {
186 self.start_incremental_line_scan(true);
188 } else {
191 self.start_prompt(
193 t!("goto.byte_offset_prompt").to_string(),
194 PromptType::GotoByteOffset,
195 );
196 }
197 }
198 PromptType::QuickOpen => {
199 return self.handle_quick_open_confirm(&input, selected_index);
201 }
202 PromptType::SetBackgroundFile => {
203 if let Err(e) = self.load_ansi_background(&input) {
204 self.set_status_message(
205 t!("error.background_load_failed", error = e.to_string()).to_string(),
206 );
207 }
208 }
209 PromptType::SetBackgroundBlend => match input.trim().parse::<f32>() {
210 Ok(val) => {
211 let clamped = val.clamp(0.0, 1.0);
212 self.background_fade = clamped;
213 self.set_status_message(
214 t!(
215 "error.background_blend_set",
216 value = format!("{:.2}", clamped)
217 )
218 .to_string(),
219 );
220 }
221 Err(_) => {
222 self.set_status_message(t!("error.invalid_blend", input = &input).to_string());
223 }
224 },
225 PromptType::SetPageWidth => {
226 self.handle_set_page_width(&input);
227 }
228 PromptType::RecordMacro => {
229 self.handle_register_input(
230 &input,
231 |editor, c| editor.toggle_macro_recording(c),
232 "Macro",
233 );
234 }
235 PromptType::PlayMacro => {
236 self.handle_register_input(&input, |editor, c| editor.play_macro(c), "Macro");
237 }
238 PromptType::SetBookmark => {
239 self.handle_register_input(&input, |editor, c| editor.set_bookmark(c), "Bookmark");
240 }
241 PromptType::JumpToBookmark => {
242 self.handle_register_input(
243 &input,
244 |editor, c| editor.jump_to_bookmark(c),
245 "Bookmark",
246 );
247 }
248 PromptType::Plugin { custom_type } => {
249 tracing::info!(
250 "prompt_confirmed: dispatching hook for prompt_type='{}', input='{}', selected_index={:?}",
251 custom_type, input, selected_index
252 );
253 self.plugin_manager.run_hook(
254 "prompt_confirmed",
255 HookArgs::PromptConfirmed {
256 prompt_type: custom_type.clone(),
257 input,
258 selected_index,
259 },
260 );
261 tracing::info!(
262 "prompt_confirmed: hook dispatched for prompt_type='{}'",
263 custom_type
264 );
265 }
266 PromptType::ConfirmRevert => {
267 let input_lower = input.trim().to_lowercase();
268 let revert_key = t!("prompt.key.revert").to_string().to_lowercase();
269 if input_lower == revert_key || input_lower == "revert" {
270 if let Err(e) = self.revert_file() {
271 self.set_status_message(
272 t!("file.revert_failed", error = e.to_string()).to_string(),
273 );
274 }
275 } else {
276 self.set_status_message(t!("buffer.revert_cancelled").to_string());
277 }
278 }
279 PromptType::ConfirmSaveConflict => {
280 let input_lower = input.trim().to_lowercase();
281 if input_lower == "o" || input_lower == "overwrite" {
282 if let Err(e) = self.save() {
283 self.set_status_message(
284 t!("file.save_failed", error = e.to_string()).to_string(),
285 );
286 }
287 } else {
288 self.set_status_message(t!("buffer.save_cancelled").to_string());
289 }
290 }
291 PromptType::ConfirmSudoSave { info } => {
292 let input_lower = input.trim().to_lowercase();
293 if input_lower == "y" || input_lower == "yes" {
294 self.cancel_prompt();
296
297 let result = (|| -> anyhow::Result<()> {
299 let data = self.authority.filesystem.read_file(&info.temp_path)?;
300 self.authority.filesystem.sudo_write(
301 &info.dest_path,
302 &data,
303 info.mode,
304 info.uid,
305 info.gid,
306 )?;
307 #[allow(clippy::let_underscore_must_use)]
309 let _ = self.authority.filesystem.remove_file(&info.temp_path);
310 Ok(())
311 })();
312
313 match result {
314 Ok(_) => {
315 if let Err(e) = self
316 .active_state_mut()
317 .buffer
318 .finalize_external_save(info.dest_path.clone())
319 {
320 tracing::warn!("Failed to finalize sudo save: {}", e);
321 self.set_status_message(
322 t!("prompt.sudo_save_failed", error = e.to_string())
323 .to_string(),
324 );
325 } else if let Err(e) = self.finalize_save(Some(info.dest_path)) {
326 tracing::warn!("Failed to finalize save after sudo: {}", e);
327 self.set_status_message(
328 t!("prompt.sudo_save_failed", error = e.to_string())
329 .to_string(),
330 );
331 }
332 }
333 Err(e) => {
334 tracing::warn!("Sudo save failed: {}", e);
335 self.set_status_message(
336 t!("prompt.sudo_save_failed", error = e.to_string()).to_string(),
337 );
338 #[allow(clippy::let_underscore_must_use)]
340 let _ = self.authority.filesystem.remove_file(&info.temp_path);
341 }
342 }
343 } else {
344 self.set_status_message(t!("buffer.save_cancelled").to_string());
345 #[allow(clippy::let_underscore_must_use)]
347 let _ = self.authority.filesystem.remove_file(&info.temp_path);
348 }
349 }
350 PromptType::ConfirmOverwriteFile { path } => {
351 let input_lower = input.trim().to_lowercase();
352 if input_lower == "o" || input_lower == "overwrite" {
353 self.perform_save_file_as(path);
354 } else {
355 self.set_status_message(t!("buffer.save_cancelled").to_string());
356 }
357 }
358 PromptType::ConfirmCreateDirectory { path } => {
359 let input_lower = input.trim().to_lowercase();
360 if input_lower == "c" || input_lower == "create" {
361 if let Some(parent) = path.parent() {
362 if let Err(e) = self.authority.filesystem.create_dir_all(parent) {
363 self.set_status_message(
364 t!("file.error_saving", error = e.to_string()).to_string(),
365 );
366 return PromptResult::Done;
367 }
368 }
369 self.perform_save_file_as(path);
370 } else {
371 self.set_status_message(t!("buffer.save_cancelled").to_string());
372 }
373 }
374 PromptType::ConfirmCloseBuffer { buffer_id } => {
375 if self.handle_confirm_close_buffer(&input, buffer_id) {
376 return PromptResult::EarlyReturn;
377 }
378 }
379 PromptType::ConfirmQuitWithModified => {
380 if self.handle_confirm_quit_modified(&input) {
381 return PromptResult::EarlyReturn;
382 }
383 }
384 PromptType::LspRename {
385 original_text,
386 start_pos,
387 end_pos: _,
388 overlay_handle,
389 } => {
390 self.perform_lsp_rename(input, original_text, start_pos, overlay_handle);
391 }
392 PromptType::FileExplorerRename {
393 original_path,
394 original_name,
395 is_new_file,
396 } => {
397 self.perform_file_explorer_rename(original_path, original_name, input, is_new_file);
398 }
399 PromptType::ConfirmDeleteFile { path, is_dir } => {
400 let input_lower = input.trim().to_lowercase();
401 if input_lower == "y" || input_lower == "yes" {
402 self.perform_file_explorer_delete(path, is_dir);
403 } else {
404 self.set_status_message(t!("explorer.delete_cancelled").to_string());
405 }
406 }
407 PromptType::ConfirmPasteConflict { src, dst, is_cut } => {
408 match input.trim().to_lowercase().as_str() {
409 "o" | "overwrite" => {
410 self.perform_file_explorer_paste(src, dst, is_cut);
411 }
412 "r" | "rename" => {
413 let initial = dst
414 .file_name()
415 .map(|n| n.to_string_lossy().to_string())
416 .unwrap_or_default();
417 let dst_dir = dst
418 .parent()
419 .map(|p| p.to_path_buf())
420 .unwrap_or_else(|| dst.clone());
421 self.start_prompt_with_initial_text(
422 t!("explorer.paste_rename_prompt").to_string(),
423 PromptType::FileExplorerPasteRename {
424 src,
425 dst_dir,
426 is_cut,
427 },
428 initial,
429 );
430 }
431 "" | "c" | "cancel" => {
432 self.set_status_message(t!("explorer.paste_cancelled").to_string());
433 }
434 _ => {
435 let name = crate::app::file_explorer::truncate_name_for_prompt(
440 &dst.file_name().unwrap_or_default().to_string_lossy(),
441 40,
442 );
443 self.start_prompt(
444 t!("explorer.paste_conflict", name = &name).to_string(),
445 PromptType::ConfirmPasteConflict { src, dst, is_cut },
446 );
447 }
448 }
449 }
450 PromptType::FileExplorerPasteRename {
451 src,
452 dst_dir,
453 is_cut,
454 } => {
455 if input.trim().is_empty() {
456 self.set_status_message(t!("explorer.paste_cancelled").to_string());
457 return PromptResult::Done;
458 }
459 let new_dst = dst_dir.join(input.trim());
460 if self.authority.filesystem.exists(&new_dst) {
461 self.start_prompt(
462 t!("explorer.paste_conflict", name = input.trim()).to_string(),
463 PromptType::ConfirmPasteConflict {
464 src,
465 dst: new_dst,
466 is_cut,
467 },
468 );
469 } else {
470 self.perform_file_explorer_paste(src, new_dst, is_cut);
471 }
472 }
473 PromptType::ConfirmMultiDelete { paths } => {
474 let input_lower = input.trim().to_lowercase();
475 if input_lower == "y" || input_lower == "yes" {
476 for path in paths {
477 let is_dir = self.authority.filesystem.is_dir(&path).unwrap_or(false);
478 self.perform_file_explorer_delete(path, is_dir);
479 }
480 } else {
481 self.set_status_message(t!("explorer.delete_cancelled").to_string());
482 }
483 }
484 PromptType::ConfirmMultiPasteConflict {
485 safe,
486 confirmed,
487 mut pending,
488 is_cut,
489 } => {
490 let (cur_src, cur_dst) = pending.remove(0);
491 match input.trim() {
497 "o" | "overwrite" => {
498 let mut new_confirmed = confirmed;
499 new_confirmed.push((cur_src, cur_dst));
500 if pending.is_empty() {
501 self.execute_resolved_multi_paste(safe, new_confirmed, is_cut);
502 } else {
503 self.prompt_next_paste_conflict(safe, new_confirmed, pending, is_cut);
504 }
505 }
506 "O" => {
507 let mut new_confirmed = confirmed;
508 new_confirmed.push((cur_src, cur_dst));
509 new_confirmed.extend(pending);
510 self.execute_resolved_multi_paste(safe, new_confirmed, is_cut);
511 }
512 "s" | "skip" => {
513 if pending.is_empty() {
514 self.execute_resolved_multi_paste(safe, confirmed, is_cut);
515 } else {
516 self.prompt_next_paste_conflict(safe, confirmed, pending, is_cut);
517 }
518 }
519 "S" => {
520 self.execute_resolved_multi_paste(safe, confirmed, is_cut);
521 }
522 "" | "c" | "cancel" => {
523 self.set_status_message(t!("explorer.paste_cancelled").to_string());
524 }
525 _ => {
526 let mut pending_with_current = vec![(cur_src, cur_dst)];
529 pending_with_current.extend(pending);
530 self.prompt_next_paste_conflict(
531 safe,
532 confirmed,
533 pending_with_current,
534 is_cut,
535 );
536 }
537 }
538 }
539 PromptType::ConfirmLargeFileEncoding { path } => {
540 let input_lower = input.trim().to_lowercase();
541 let load_key = t!("file.large_encoding.key.load")
542 .to_string()
543 .to_lowercase();
544 let encoding_key = t!("file.large_encoding.key.encoding")
545 .to_string()
546 .to_lowercase();
547 let cancel_key = t!("file.large_encoding.key.cancel")
548 .to_string()
549 .to_lowercase();
550 if input_lower.is_empty() || input_lower == load_key {
552 if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
553 self.set_status_message(
554 t!("file.error_opening", error = e.to_string()).to_string(),
555 );
556 }
557 } else if input_lower == encoding_key {
558 self.start_open_file_with_encoding_prompt(path);
560 } else if input_lower == cancel_key {
561 self.set_status_message(t!("file.open_cancelled").to_string());
562 } else {
563 if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
565 self.set_status_message(
566 t!("file.error_opening", error = e.to_string()).to_string(),
567 );
568 }
569 }
570 }
571 PromptType::StopLspServer => {
572 self.handle_stop_lsp_server(&input);
573 }
574 PromptType::RestartLspServer => {
575 self.handle_restart_lsp_server(&input);
576 }
577 PromptType::SelectTheme { .. } => {
578 self.apply_theme(input.trim());
579 }
580 PromptType::SelectKeybindingMap => {
581 self.apply_keybinding_map(input.trim());
582 }
583 PromptType::SelectCursorStyle => {
584 self.apply_cursor_style(input.trim());
585 }
586 PromptType::SelectLocale => {
587 self.apply_locale(input.trim());
588 }
589 PromptType::CopyWithFormattingTheme => {
590 self.copy_selection_with_theme(input.trim());
591 }
592 PromptType::SwitchToTab => {
593 if let Ok(id) = input.trim().parse::<usize>() {
594 self.switch_to_tab(BufferId(id));
595 }
596 }
597 PromptType::QueryReplaceConfirm => {
598 if let Some(c) = input.chars().next() {
601 if let Err(e) = self.handle_interactive_replace_key(c) {
602 tracing::warn!("Interactive replace failed: {}", e);
603 }
604 }
605 }
606 PromptType::AddRuler => {
607 self.handle_add_ruler(&input);
608 }
609 PromptType::RemoveRuler => {
610 self.handle_remove_ruler(&input);
611 }
612 PromptType::SetTabSize => {
613 self.handle_set_tab_size(&input);
614 }
615 PromptType::SetLineEnding => {
616 self.handle_set_line_ending(&input);
617 }
618 PromptType::SetEncoding => {
619 self.handle_set_encoding(&input);
620 }
621 PromptType::SetLanguage => {
622 self.handle_set_language(&input);
623 }
624 PromptType::ShellCommand { replace } => {
625 self.handle_shell_command(&input, replace);
626 }
627 PromptType::AsyncPrompt => {
628 if let Some(callback_id) = self.pending_async_prompt_callback.take() {
630 let json = serde_json::to_string(&input).unwrap_or_else(|_| "null".to_string());
632 self.plugin_manager.resolve_callback(callback_id, json);
633 }
634 }
635 }
636 PromptResult::Done
637 }
638
639 fn handle_save_file_as(&mut self, input: &str) {
641 let expanded_path = expand_tilde(input);
643 let full_path = if expanded_path.is_absolute() {
644 normalize_path(&expanded_path)
645 } else {
646 normalize_path(&self.working_dir.join(&expanded_path))
647 };
648
649 self.save_file_as_with_checks(full_path);
650 }
651
652 pub(crate) fn save_file_as_with_checks(&mut self, full_path: std::path::PathBuf) {
654 let current_file_path = self
656 .active_state()
657 .buffer
658 .file_path()
659 .map(|p| p.to_path_buf());
660 let is_different_file = current_file_path.as_ref() != Some(&full_path);
661
662 if is_different_file && full_path.is_file() {
663 let filename = full_path
665 .file_name()
666 .map(|n| n.to_string_lossy().to_string())
667 .unwrap_or_else(|| full_path.display().to_string());
668 self.start_prompt(
669 t!("buffer.overwrite_confirm", name = &filename).to_string(),
670 PromptType::ConfirmOverwriteFile { path: full_path },
671 );
672 return;
673 }
674
675 if let Some(parent) = full_path.parent() {
677 if !parent.as_os_str().is_empty() && !self.authority.filesystem.exists(parent) {
678 let dir_name = parent
679 .strip_prefix(&self.working_dir)
680 .unwrap_or(parent)
681 .display()
682 .to_string();
683 self.start_prompt(
684 t!("buffer.create_directory_confirm", name = &dir_name).to_string(),
685 PromptType::ConfirmCreateDirectory { path: full_path },
686 );
687 return;
688 }
689 }
690
691 self.perform_save_file_as(full_path);
693 }
694
695 pub(crate) fn perform_save_file_as(&mut self, full_path: std::path::PathBuf) {
697 let before_idx = self.active_event_log().current_index();
698 let before_len = self.active_event_log().len();
699 tracing::debug!(
700 "SaveFileAs BEFORE: event_log index={}, len={}",
701 before_idx,
702 before_len
703 );
704
705 match self.active_state_mut().buffer.save_to_file(&full_path) {
706 Ok(()) => {
707 let after_save_idx = self.active_event_log().current_index();
708 let after_save_len = self.active_event_log().len();
709 tracing::debug!(
710 "SaveFileAs AFTER buffer.save_to_file: event_log index={}, len={}",
711 after_save_idx,
712 after_save_len
713 );
714
715 let metadata = BufferMetadata::with_file(
716 full_path.clone(),
717 &full_path,
718 &self.working_dir,
719 self.authority.path_translation.as_ref(),
720 );
721 self.buffer_metadata.insert(self.active_buffer(), metadata);
722
723 let mut language_changed = false;
726 let mut new_language = String::new();
727 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
728 if state.language == "text" {
729 let first_line = state.buffer.first_line_lossy();
730 let detected =
731 crate::primitives::detected_language::DetectedLanguage::from_path(
732 &full_path,
733 first_line.as_deref(),
734 &self.grammar_registry,
735 &self.config.languages,
736 );
737 new_language = detected.name.clone();
738 state.apply_language(detected);
739 language_changed = new_language != "text";
740 }
741 }
742 if language_changed {
743 #[cfg(feature = "plugins")]
744 self.update_plugin_state_snapshot();
745 self.plugin_manager.run_hook(
746 "language_changed",
747 crate::services::plugins::hooks::HookArgs::LanguageChanged {
748 buffer_id: self.active_buffer(),
749 language: new_language,
750 },
751 );
752 }
753
754 self.active_event_log_mut().mark_saved();
755 tracing::debug!(
756 "SaveFileAs AFTER mark_saved: event_log index={}, len={}",
757 self.active_event_log().current_index(),
758 self.active_event_log().len()
759 );
760
761 if let Ok(metadata) = self.authority.filesystem.metadata(&full_path) {
762 if let Some(mtime) = metadata.modified {
763 self.file_mod_times.insert(full_path.clone(), mtime);
764 }
765 }
766
767 self.notify_lsp_save();
768
769 self.emit_event(
770 crate::model::control_event::events::FILE_SAVED.name,
771 serde_json::json!({"path": full_path.display().to_string()}),
772 );
773
774 self.plugin_manager.run_hook(
775 "after_file_save",
776 crate::services::plugins::hooks::HookArgs::AfterFileSave {
777 buffer_id: self.active_buffer(),
778 path: full_path.clone(),
779 },
780 );
781
782 if let Some(buffer_to_close) = self.pending_close_buffer.take() {
783 if let Err(e) = self.force_close_buffer(buffer_to_close) {
784 self.set_status_message(
785 t!("file.saved_cannot_close", error = e.to_string()).to_string(),
786 );
787 } else {
788 self.set_status_message(t!("buffer.saved_and_closed").to_string());
789 }
790 } else {
791 self.set_status_message(
792 t!("file.saved_as", path = full_path.display().to_string()).to_string(),
793 );
794 }
795 }
796 Err(e) => {
797 self.pending_close_buffer = None;
798 self.set_status_message(t!("file.error_saving", error = e.to_string()).to_string());
799 }
800 }
801 }
802
803 fn handle_set_page_width(&mut self, input: &str) {
805 let active_split = self.split_manager.active_split();
806 let trimmed = input.trim();
807
808 if trimmed.is_empty() {
809 if let Some(vs) = self.split_view_states.get_mut(&active_split) {
810 vs.compose_width = None;
811 }
812 self.set_status_message(t!("settings.page_width_cleared").to_string());
813 } else {
814 match trimmed.parse::<u16>() {
815 Ok(val) if val > 0 => {
816 if let Some(vs) = self.split_view_states.get_mut(&active_split) {
817 vs.compose_width = Some(val);
818 }
819 self.set_status_message(t!("settings.page_width_set", value = val).to_string());
820 }
821 _ => {
822 self.set_status_message(
823 t!("error.invalid_page_width", input = input).to_string(),
824 );
825 }
826 }
827 }
828 }
829
830 fn handle_add_ruler(&mut self, input: &str) {
832 let trimmed = input.trim();
833 match trimmed.parse::<usize>() {
834 Ok(col) if col > 0 => {
835 let active_split = self.split_manager.active_split();
836 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
837 if !view_state.rulers.contains(&col) {
838 view_state.rulers.push(col);
839 view_state.rulers.sort();
840 }
841 }
842 let new_rulers = self
844 .split_view_states
845 .get(&active_split)
846 .map(|vs| vs.rulers.clone())
847 .unwrap_or_default();
848 self.config_mut().editor.rulers = new_rulers;
849 self.save_rulers_to_config();
850 self.set_status_message(t!("rulers.added", column = col).to_string());
851 }
852 Ok(_) => {
853 self.set_status_message(t!("rulers.must_be_positive").to_string());
854 }
855 Err(_) => {
856 self.set_status_message(t!("rulers.invalid_column", input = input).to_string());
857 }
858 }
859 }
860
861 fn handle_remove_ruler(&mut self, input: &str) {
863 let trimmed = input.trim();
864 if let Ok(col) = trimmed.parse::<usize>() {
865 let active_split = self.split_manager.active_split();
866 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
867 view_state.rulers.retain(|&r| r != col);
868 }
869 let new_rulers = self
871 .split_view_states
872 .get(&active_split)
873 .map(|vs| vs.rulers.clone())
874 .unwrap_or_default();
875 self.config_mut().editor.rulers = new_rulers;
876 self.save_rulers_to_config();
877 self.set_status_message(t!("rulers.removed", column = col).to_string());
878 }
879 }
880
881 fn save_rulers_to_config(&mut self) {
883 if let Err(e) = self
884 .authority
885 .filesystem
886 .create_dir_all(&self.dir_context.config_dir)
887 {
888 tracing::warn!("Failed to create config directory: {}", e);
889 return;
890 }
891 let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
892 if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
893 tracing::warn!("Failed to save rulers to config: {}", e);
894 }
895 }
896
897 fn handle_set_tab_size(&mut self, input: &str) {
899 let buffer_id = self.active_buffer();
900 let trimmed = input.trim();
901
902 match trimmed.parse::<usize>() {
903 Ok(val) if val > 0 => {
904 if let Some(state) = self.buffers.get_mut(&buffer_id) {
905 state.buffer_settings.tab_size = val;
906 }
907 self.set_status_message(t!("settings.tab_size_set", value = val).to_string());
908 }
909 Ok(_) => {
910 self.set_status_message(t!("settings.tab_size_positive").to_string());
911 }
912 Err(_) => {
913 self.set_status_message(t!("error.invalid_tab_size", input = input).to_string());
914 }
915 }
916 }
917
918 fn handle_set_line_ending(&mut self, input: &str) {
920 use crate::model::buffer::LineEnding;
921
922 let trimmed = input.trim();
924 let code = trimmed.split_whitespace().next().unwrap_or(trimmed);
925
926 let line_ending = match code.to_uppercase().as_str() {
927 "LF" => Some(LineEnding::LF),
928 "CRLF" => Some(LineEnding::CRLF),
929 "CR" => Some(LineEnding::CR),
930 _ => None,
931 };
932
933 match line_ending {
934 Some(le) => {
935 self.active_state_mut().buffer.set_line_ending(le);
936 self.set_status_message(
937 t!("settings.line_ending_set", value = le.display_name()).to_string(),
938 );
939 }
940 None => {
941 self.set_status_message(t!("error.unknown_line_ending", input = input).to_string());
942 }
943 }
944 }
945
946 fn handle_set_encoding(&mut self, input: &str) {
948 use crate::model::buffer::Encoding;
949
950 let trimmed = input.trim();
951
952 let encoding = Encoding::all()
955 .iter()
956 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
957 .copied()
958 .or_else(|| {
959 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
961 Encoding::all()
962 .iter()
963 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
964 .copied()
965 });
966
967 match encoding {
968 Some(enc) => {
969 self.active_state_mut().buffer.set_encoding(enc);
970 self.set_status_message(format!("Encoding set to {}", enc.display_name()));
971 }
972 None => {
973 self.set_status_message(format!("Unknown encoding: {}", input));
974 }
975 }
976 }
977
978 fn handle_open_file_with_encoding(&mut self, path: &std::path::Path, input: &str) {
984 use crate::model::buffer::Encoding;
985 use crate::view::prompt::PromptType;
986
987 let trimmed = input.trim();
988
989 let encoding = Encoding::all()
991 .iter()
992 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
993 .copied()
994 .or_else(|| {
995 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
996 Encoding::all()
997 .iter()
998 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
999 .copied()
1000 });
1001
1002 match encoding {
1003 Some(enc) => {
1004 let threshold = self.config.editor.large_file_threshold_bytes as usize;
1007 let file_size = self
1008 .authority
1009 .filesystem
1010 .metadata(path)
1011 .map(|m| m.size as usize)
1012 .unwrap_or(0);
1013
1014 if file_size >= threshold && enc.requires_full_file_load() {
1015 let size_mb = file_size as f64 / (1024.0 * 1024.0);
1017 let load_key = t!("file.large_encoding.key.load").to_string();
1018 let encoding_key = t!("file.large_encoding.key.encoding").to_string();
1019 let cancel_key = t!("file.large_encoding.key.cancel").to_string();
1020 let prompt_msg = t!(
1021 "file.large_encoding_prompt",
1022 encoding = enc.display_name(),
1023 size = format!("{:.0}", size_mb),
1024 load_key = load_key,
1025 encoding_key = encoding_key,
1026 cancel_key = cancel_key
1027 )
1028 .to_string();
1029 self.start_prompt(
1030 prompt_msg,
1031 PromptType::ConfirmLargeFileEncoding {
1032 path: path.to_path_buf(),
1033 },
1034 );
1035 return;
1036 }
1037
1038 self.key_context = crate::input::keybindings::KeyContext::Normal;
1040
1041 if let Err(e) = self.open_file_with_encoding(path, enc) {
1043 self.set_status_message(
1044 t!("file.error_opening", error = e.to_string()).to_string(),
1045 );
1046 } else {
1047 self.set_status_message(format!(
1048 "Opened {} with {} encoding",
1049 path.display(),
1050 enc.display_name()
1051 ));
1052 }
1053 }
1054 None => {
1055 self.set_status_message(format!("Unknown encoding: {}", input));
1056 }
1057 }
1058 }
1059
1060 fn handle_reload_with_encoding(&mut self, input: &str) {
1063 use crate::model::buffer::Encoding;
1064
1065 let trimmed = input.trim();
1066
1067 let encoding = Encoding::all()
1069 .iter()
1070 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
1071 .copied()
1072 .or_else(|| {
1073 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
1074 Encoding::all()
1075 .iter()
1076 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
1077 .copied()
1078 });
1079
1080 match encoding {
1081 Some(enc) => {
1082 if let Err(e) = self.reload_with_encoding(enc) {
1084 self.set_status_message(format!("Failed to reload: {}", e));
1085 } else {
1086 self.set_status_message(format!(
1087 "Reloaded with {} encoding",
1088 enc.display_name()
1089 ));
1090 }
1091 }
1092 None => {
1093 self.set_status_message(format!("Unknown encoding: {}", input));
1094 }
1095 }
1096 }
1097
1098 fn handle_set_language(&mut self, input: &str) {
1100 use crate::primitives::detected_language::DetectedLanguage;
1101
1102 let trimmed = input.trim();
1103
1104 if trimmed == "Plain Text" || trimmed.to_lowercase() == "text" {
1106 let buffer_id = self.active_buffer();
1107 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1108 state.apply_language(DetectedLanguage::plain_text());
1109 self.set_status_message("Language set to Plain Text".to_string());
1110 }
1111 #[cfg(feature = "plugins")]
1112 self.update_plugin_state_snapshot();
1113 self.plugin_manager.run_hook(
1114 "language_changed",
1115 crate::services::plugins::hooks::HookArgs::LanguageChanged {
1116 buffer_id: self.active_buffer(),
1117 language: "text".to_string(),
1118 },
1119 );
1120 return;
1121 }
1122
1123 if let Some(detected) = DetectedLanguage::from_syntax_name(
1125 trimmed,
1126 &self.grammar_registry,
1127 &self.config.languages,
1128 ) {
1129 let language = detected.name.clone();
1130 let buffer_id = self.active_buffer();
1131 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1132 state.apply_language(detected);
1133 self.set_status_message(format!("Language set to {}", trimmed));
1134 }
1135 #[cfg(feature = "plugins")]
1136 self.update_plugin_state_snapshot();
1137 self.plugin_manager.run_hook(
1138 "language_changed",
1139 crate::services::plugins::hooks::HookArgs::LanguageChanged {
1140 buffer_id,
1141 language,
1142 },
1143 );
1144 } else {
1145 self.set_status_message(format!("Unknown language: {}", input));
1149 }
1150 }
1151
1152 fn handle_register_input<F>(&mut self, input: &str, action: F, register_type: &str)
1154 where
1155 F: FnOnce(&mut Self, char),
1156 {
1157 if let Some(c) = input.trim().chars().next() {
1158 if c.is_ascii_digit() {
1159 action(self, c);
1160 } else {
1161 self.set_status_message(
1162 t!("register.must_be_digit", "type" = register_type).to_string(),
1163 );
1164 }
1165 } else {
1166 self.set_status_message(t!("register.not_specified").to_string());
1167 }
1168 }
1169
1170 fn handle_confirm_close_buffer(&mut self, input: &str, buffer_id: BufferId) -> bool {
1172 let input_lower = input.trim().to_lowercase();
1173 let save_key = t!("prompt.key.save").to_string().to_lowercase();
1174 let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1175
1176 let first_char = input_lower.chars().next();
1177 let save_first = save_key.chars().next();
1178 let discard_first = discard_key.chars().next();
1179
1180 if first_char == save_first {
1181 let has_path = self
1183 .buffers
1184 .get(&buffer_id)
1185 .map(|s| s.buffer.file_path().is_some())
1186 .unwrap_or(false);
1187
1188 if has_path {
1189 let old_active = self.active_buffer();
1190 self.set_active_buffer(buffer_id);
1191 if let Err(e) = self.save() {
1192 self.set_status_message(
1193 t!("file.save_failed", error = e.to_string()).to_string(),
1194 );
1195 self.set_active_buffer(old_active);
1196 return true; }
1198 self.set_active_buffer(old_active);
1199 if let Err(e) = self.force_close_buffer(buffer_id) {
1200 self.set_status_message(
1201 t!("file.cannot_close", error = e.to_string()).to_string(),
1202 );
1203 } else {
1204 self.set_status_message(t!("buffer.saved_and_closed").to_string());
1205 }
1206 } else {
1207 self.pending_close_buffer = Some(buffer_id);
1208 self.start_prompt_with_initial_text(
1209 t!("file.save_as_prompt").to_string(),
1210 PromptType::SaveFileAs,
1211 String::new(),
1212 );
1213 }
1214 } else if first_char == discard_first {
1215 if let Err(e) = self.force_close_buffer(buffer_id) {
1217 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
1218 } else {
1219 self.set_status_message(t!("buffer.changes_discarded").to_string());
1220 }
1221 } else {
1222 self.set_status_message(t!("buffer.close_cancelled").to_string());
1223 }
1224 false
1225 }
1226
1227 fn handle_confirm_quit_modified(&mut self, input: &str) -> bool {
1229 let input_lower = input.trim().to_lowercase();
1230 let save_key = t!("prompt.key.save").to_string().to_lowercase();
1231 let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1232 let quit_key = t!("prompt.key.quit").to_string().to_lowercase();
1233
1234 let first_char = input_lower.chars().next();
1235 let save_first = save_key.chars().next();
1236 let discard_first = discard_key.chars().next();
1237 let quit_first = quit_key.chars().next();
1238
1239 if first_char == save_first {
1240 match self.save_all_on_exit() {
1242 Ok(count) => {
1243 tracing::info!("Saved {} buffer(s) on exit", count);
1244 self.should_quit = true;
1245 }
1246 Err(e) => {
1247 self.set_status_message(
1248 t!("file.save_failed", error = e.to_string()).to_string(),
1249 );
1250 return true; }
1252 }
1253 } else if first_char == discard_first {
1254 self.should_quit = true;
1256 } else if first_char == quit_first && self.config.editor.hot_exit {
1257 self.should_quit = true;
1259 } else {
1260 self.set_status_message(t!("buffer.close_cancelled").to_string());
1262 }
1263 false
1264 }
1265
1266 pub fn handle_stop_lsp_server(&mut self, input: &str) {
1271 let input = input.trim();
1272 if input.is_empty() {
1273 return;
1274 }
1275
1276 let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1278 (lang, Some(name))
1279 } else {
1280 (input, None)
1281 };
1282
1283 let has_server = self
1284 .lsp
1285 .as_ref()
1286 .is_some_and(|lsp| lsp.has_handles(language));
1287
1288 if !has_server {
1289 self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1290 return;
1291 }
1292
1293 let stopping_all = server_name.is_none()
1297 || self
1298 .lsp
1299 .as_ref()
1300 .map(|lsp| lsp.handle_count(language) <= 1)
1301 .unwrap_or(true);
1302
1303 if stopping_all {
1304 let buffer_ids: Vec<_> = self
1311 .buffers
1312 .iter()
1313 .filter(|(_, s)| s.language == language)
1314 .map(|(id, _)| *id)
1315 .collect();
1316 for buffer_id in buffer_ids {
1317 self.disable_lsp_for_buffer(buffer_id);
1318 }
1319 } else if let Some(name) = server_name {
1320 self.send_did_close_to_server(language, name);
1324 }
1325
1326 let stopped = self.stop_lsp_server_and_cleanup(language, server_name);
1330
1331 if !stopped {
1332 self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1333 return;
1334 }
1335
1336 if let Some(lsp_configs) = self.config_mut().lsp.get_mut(language) {
1338 for c in lsp_configs.as_mut_slice() {
1339 if let Some(name) = server_name {
1340 if c.display_name() == name {
1342 c.auto_start = false;
1343 }
1344 } else {
1345 c.auto_start = false;
1346 }
1347 }
1348 if let Err(e) = self.save_config() {
1349 tracing::warn!(
1350 "Failed to save config after disabling LSP auto-start: {}",
1351 e
1352 );
1353 } else {
1354 let config_path = self.dir_context.config_path();
1355 self.emit_event(
1356 "config_changed",
1357 serde_json::json!({
1358 "path": config_path.to_string_lossy(),
1359 }),
1360 );
1361 }
1362 }
1363
1364 let display = server_name.unwrap_or(language);
1365 self.set_status_message(t!("lsp.server_stopped", language = display).to_string());
1366 }
1367
1368 pub fn handle_restart_lsp_server(&mut self, input: &str) {
1373 let input = input.trim();
1374 if input.is_empty() {
1375 return;
1376 }
1377
1378 let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1380 (lang, Some(name))
1381 } else {
1382 (input, None)
1383 };
1384
1385 let buffer_id = self.active_buffer();
1387 let file_path = self
1388 .buffer_metadata
1389 .get(&buffer_id)
1390 .and_then(|meta| meta.file_path().cloned());
1391
1392 let (success, message) = if let Some(name) = server_name {
1393 if let Some(lsp) = self.lsp.as_mut() {
1395 lsp.manual_restart_server(language, name, file_path.as_deref())
1396 } else {
1397 (false, t!("lsp.no_manager").to_string())
1398 }
1399 } else {
1400 if let Some(lsp) = self.lsp.as_mut() {
1402 lsp.manual_restart(language, file_path.as_deref())
1403 } else {
1404 (false, t!("lsp.no_manager").to_string())
1405 }
1406 };
1407
1408 self.status_message = Some(message);
1409
1410 if success {
1411 self.reopen_buffers_for_language(language);
1412 }
1413 }
1414
1415 fn handle_quick_open_confirm(
1417 &mut self,
1418 input: &str,
1419 selected_index: Option<usize>,
1420 ) -> PromptResult {
1421 use crate::input::quick_open::QuickOpenResult;
1422
1423 let context = self.build_quick_open_context();
1424 let result = if let Some((provider, query)) =
1425 self.quick_open_registry.get_provider_for_input(input)
1426 {
1427 let suggestions = provider.suggestions(query, &context);
1429 let selected = selected_index.and_then(|i| suggestions.get(i));
1430 provider.on_select(selected, query, &context)
1431 } else {
1432 QuickOpenResult::None
1433 };
1434
1435 self.execute_quick_open_result(result)
1436 }
1437
1438 fn execute_quick_open_result(
1440 &mut self,
1441 result: crate::input::quick_open::QuickOpenResult,
1442 ) -> PromptResult {
1443 use crate::input::quick_open::QuickOpenResult;
1444
1445 match &result {
1449 QuickOpenResult::GotoLine(_) => {
1450 self.goto_line_preview = None;
1453 }
1454 _ => {
1455 self.restore_goto_line_preview_snapshot();
1456 }
1457 }
1458
1459 match result {
1460 QuickOpenResult::ExecuteAction(action) => PromptResult::ExecuteAction(action),
1461 QuickOpenResult::OpenFile { path, line, column } => {
1462 let expanded_path = expand_tilde(&path);
1463 let full_path = if expanded_path.is_absolute() {
1464 expanded_path
1465 } else {
1466 self.working_dir.join(&expanded_path)
1467 };
1468 self.open_file_with_jump(full_path, line, column);
1469 PromptResult::Done
1470 }
1471 QuickOpenResult::ShowBuffer(buffer_id) => {
1472 let buffer_id = crate::model::event::BufferId(buffer_id);
1473 if self.buffers.contains_key(&buffer_id) {
1474 self.set_active_buffer(buffer_id);
1475 if let Some(name) = self.active_state().buffer.file_path() {
1476 self.set_status_message(
1477 t!("buffer.switched", name = name.display().to_string()).to_string(),
1478 );
1479 }
1480 }
1481 PromptResult::Done
1482 }
1483 QuickOpenResult::GotoLine(target) => {
1484 let buffer_id = self.active_buffer();
1485 if let Some(state) = self.buffers.get(&buffer_id) {
1486 let max_line = state.buffer.line_count().unwrap_or(1);
1487 let current_line = state.primary_cursor_line_number.value() + 1;
1488 let line = resolve_goto_line_target(target, current_line, max_line);
1489 self.goto_line_col(line, None);
1490 self.set_status_message(t!("goto.jumped", line = line).to_string());
1491 } else {
1492 self.set_status_message(t!("status.no_selection").to_string());
1493 }
1494 PromptResult::Done
1495 }
1496 QuickOpenResult::None => {
1497 self.set_status_message(t!("status.no_selection").to_string());
1498 PromptResult::Done
1499 }
1500 QuickOpenResult::Error(msg) => {
1501 self.set_status_message(msg);
1502 PromptResult::Done
1503 }
1504 }
1505 }
1506
1507 fn open_file_with_jump(
1508 &mut self,
1509 full_path: std::path::PathBuf,
1510 line: Option<usize>,
1511 column: Option<usize>,
1512 ) {
1513 match self.open_file(&full_path) {
1514 Ok(_) => {
1515 if let Some(line) = line {
1516 self.goto_line_col(line, column);
1517 }
1518 self.set_status_message(
1519 t!("buffer.opened", name = full_path.display().to_string()).to_string(),
1520 );
1521 }
1522 Err(e) => {
1523 if let Some(confirmation) =
1525 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
1526 {
1527 self.start_large_file_encoding_confirmation(confirmation);
1528 } else {
1529 self.set_status_message(
1530 t!("file.error_opening", error = e.to_string()).to_string(),
1531 );
1532 }
1533 }
1534 }
1535 }
1536
1537 fn prompt_next_paste_conflict(
1539 &mut self,
1540 safe: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1541 confirmed: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1542 pending: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1543 is_cut: bool,
1544 ) {
1545 let name = crate::app::file_explorer::truncate_name_for_prompt(
1546 &pending[0]
1547 .1
1548 .file_name()
1549 .unwrap_or_default()
1550 .to_string_lossy(),
1551 40,
1552 );
1553 self.start_prompt(
1554 t!("explorer.paste_conflict_multi", name = &name).to_string(),
1555 PromptType::ConfirmMultiPasteConflict {
1556 safe,
1557 confirmed,
1558 pending,
1559 is_cut,
1560 },
1561 );
1562 }
1563}
1564
1565#[cfg(test)]
1570mod tests {
1571 use super::parse_path_line_col;
1572
1573 #[test]
1574 fn test_parse_path_line_col_empty() {
1575 let (path, line, col) = parse_path_line_col("");
1576 assert_eq!(path, "");
1577 assert_eq!(line, None);
1578 assert_eq!(col, None);
1579 }
1580
1581 #[test]
1582 fn test_parse_path_line_col_plain_path() {
1583 let (path, line, col) = parse_path_line_col("src/main.rs");
1584 assert_eq!(path, "src/main.rs");
1585 assert_eq!(line, None);
1586 assert_eq!(col, None);
1587 }
1588
1589 #[test]
1590 fn test_parse_path_line_col_line_only() {
1591 let (path, line, col) = parse_path_line_col("src/main.rs:42");
1592 assert_eq!(path, "src/main.rs");
1593 assert_eq!(line, Some(42));
1594 assert_eq!(col, None);
1595 }
1596
1597 #[test]
1598 fn test_parse_path_line_col_line_and_col() {
1599 let (path, line, col) = parse_path_line_col("src/main.rs:42:10");
1600 assert_eq!(path, "src/main.rs");
1601 assert_eq!(line, Some(42));
1602 assert_eq!(col, Some(10));
1603 }
1604
1605 #[test]
1606 fn test_parse_path_line_col_trimmed() {
1607 let (path, line, col) = parse_path_line_col(" src/main.rs:5:2 ");
1608 assert_eq!(path, "src/main.rs");
1609 assert_eq!(line, Some(5));
1610 assert_eq!(col, Some(2));
1611 }
1612
1613 #[test]
1614 fn test_parse_path_line_col_zero_line_rejected() {
1615 let (path, line, col) = parse_path_line_col("src/main.rs:0");
1616 assert_eq!(path, "src/main.rs:0");
1617 assert_eq!(line, None);
1618 assert_eq!(col, None);
1619 }
1620
1621 #[test]
1622 fn test_parse_path_line_col_zero_col_rejected() {
1623 let (path, line, col) = parse_path_line_col("src/main.rs:1:0");
1624 assert_eq!(path, "src/main.rs:1:0");
1625 assert_eq!(line, None);
1626 assert_eq!(col, None);
1627 }
1628
1629 #[cfg(windows)]
1630 #[test]
1631 fn test_parse_path_line_col_windows_drive() {
1632 let (path, line, col) = parse_path_line_col(r"C:\src\main.rs:12:3");
1633 assert_eq!(path, r"C:\src\main.rs");
1634 assert_eq!(line, Some(12));
1635 assert_eq!(col, Some(3));
1636 }
1637}