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