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