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.active_window().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.active_window().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
144 .windows
145 .get(&self.active_window)
146 .map(|w| &w.buffers)
147 .expect("active window present")
148 .get(&buffer_id)
149 {
150 let max_line = state.buffer.line_count().unwrap_or(1);
151 let current_line = state.primary_cursor_line_number.value() + 1;
152 match crate::input::quick_open::parse_goto_line_input(&input) {
153 Some(target) => {
154 let line = resolve_goto_line_target(target, current_line, max_line);
155 self.goto_line_col(line, None);
156 self.set_status_message(t!("goto.jumped", line = line).to_string());
157 }
158 None => {
159 self.set_status_message(
160 t!("error.invalid_line", input = input.trim()).to_string(),
161 );
162 }
163 }
164 } else {
165 self.set_status_message(t!("status.no_selection").to_string());
166 }
167 }
168 PromptType::GotoByteOffset => {
169 let trimmed = input.trim();
171 let num_str = trimmed
172 .strip_suffix('B')
173 .or_else(|| trimmed.strip_suffix('b'))
174 .unwrap_or(trimmed);
175 match num_str.parse::<usize>() {
176 Ok(offset) => {
177 self.goto_byte_offset(offset);
178 self.set_status_message(
179 t!("goto.jumped_byte", offset = offset).to_string(),
180 );
181 }
182 Err(_) => {
183 self.set_status_message(
184 t!("goto.invalid_byte_offset", input = &input).to_string(),
185 );
186 }
187 }
188 }
189 PromptType::GotoLineScanConfirm => {
190 let answer = input.trim().to_lowercase();
191 if answer == "y" || answer == "yes" {
192 self.start_incremental_line_scan(true);
194 } else {
197 self.start_prompt(
199 t!("goto.byte_offset_prompt").to_string(),
200 PromptType::GotoByteOffset,
201 );
202 }
203 }
204 PromptType::QuickOpen => {
205 return self.handle_quick_open_confirm(&input, selected_index);
207 }
208 PromptType::LiveGrep => {
209 use crate::input::quick_open::parse_path_line_col;
218 let (path_str, line, column) = parse_path_line_col(&input);
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::SaveMacroToInit => {
266 self.handle_register_input(
267 &input,
268 |editor, c| editor.save_macro_to_init(c),
269 "Macro",
270 );
271 }
272 PromptType::PromoteMacro => {
273 self.handle_register_input(
274 &input,
275 |editor, c| editor.promote_macro_to_command(c),
276 "Macro",
277 );
278 }
279 PromptType::SetBookmark => {
280 self.handle_register_input(
281 &input,
282 |editor, c| editor.active_window_mut().set_bookmark(c),
283 "Bookmark",
284 );
285 }
286 PromptType::JumpToBookmark => {
287 self.handle_register_input(
288 &input,
289 |editor, c| editor.jump_to_bookmark(c),
290 "Bookmark",
291 );
292 }
293 PromptType::Plugin { custom_type } => {
294 tracing::info!(
295 "prompt_confirmed: dispatching hook for prompt_type='{}', input='{}', selected_index={:?}",
296 custom_type, input, selected_index
297 );
298 self.plugin_manager.read().unwrap().run_hook(
299 "prompt_confirmed",
300 HookArgs::PromptConfirmed {
301 prompt_type: custom_type.clone(),
302 input,
303 selected_index,
304 },
305 );
306 tracing::info!(
307 "prompt_confirmed: hook dispatched for prompt_type='{}'",
308 custom_type
309 );
310 }
311 PromptType::ConfirmRevert => {
312 let input_lower = input.trim().to_lowercase();
313 let revert_key = t!("prompt.key.revert").to_string().to_lowercase();
314 if input_lower == revert_key || input_lower == "revert" {
315 if let Err(e) = self.revert_file() {
316 self.set_status_message(
317 t!("file.revert_failed", error = e.to_string()).to_string(),
318 );
319 }
320 } else {
321 self.set_status_message(t!("buffer.revert_cancelled").to_string());
322 }
323 }
324 PromptType::ConfirmSaveConflict => {
325 let input_lower = input.trim().to_lowercase();
326 if input_lower == "o" || input_lower == "overwrite" {
327 if let Err(e) = self.save() {
328 self.set_status_message(
329 t!("file.save_failed", error = e.to_string()).to_string(),
330 );
331 }
332 } else {
333 self.set_status_message(t!("buffer.save_cancelled").to_string());
334 }
335 }
336 PromptType::ConfirmSudoSave { info } => {
337 let input_lower = input.trim().to_lowercase();
338 if input_lower == "y" || input_lower == "yes" {
339 self.cancel_prompt();
341
342 let result = (|| -> anyhow::Result<()> {
344 let data = self.authority().filesystem.read_file(&info.temp_path)?;
345 self.authority().filesystem.sudo_write(
346 &info.dest_path,
347 &data,
348 info.mode,
349 info.uid,
350 info.gid,
351 )?;
352 #[allow(clippy::let_underscore_must_use)]
354 let _ = self.authority().filesystem.remove_file(&info.temp_path);
355 Ok(())
356 })();
357
358 match result {
359 Ok(_) => {
360 if let Err(e) = self
361 .active_state_mut()
362 .buffer
363 .finalize_external_save(info.dest_path.clone())
364 {
365 tracing::warn!("Failed to finalize sudo save: {}", e);
366 self.set_status_message(
367 t!("prompt.sudo_save_failed", error = e.to_string())
368 .to_string(),
369 );
370 } else if let Err(e) = self.finalize_save(Some(info.dest_path)) {
371 tracing::warn!("Failed to finalize save after sudo: {}", e);
372 self.set_status_message(
373 t!("prompt.sudo_save_failed", error = e.to_string())
374 .to_string(),
375 );
376 }
377 }
378 Err(e) => {
379 tracing::warn!("Sudo save failed: {}", e);
380 self.set_status_message(
381 t!("prompt.sudo_save_failed", error = e.to_string()).to_string(),
382 );
383 #[allow(clippy::let_underscore_must_use)]
385 let _ = self.authority().filesystem.remove_file(&info.temp_path);
386 }
387 }
388 } else {
389 self.set_status_message(t!("buffer.save_cancelled").to_string());
390 #[allow(clippy::let_underscore_must_use)]
392 let _ = self.authority().filesystem.remove_file(&info.temp_path);
393 }
394 }
395 PromptType::ConfirmOverwriteFile { path } => {
396 let input_lower = input.trim().to_lowercase();
397 if input_lower == "o" || input_lower == "overwrite" {
398 self.perform_save_file_as(path);
399 } else {
400 self.set_status_message(t!("buffer.save_cancelled").to_string());
401 }
402 }
403 PromptType::ConfirmCreateDirectory { path } => {
404 let input_lower = input.trim().to_lowercase();
405 if input_lower == "c" || input_lower == "create" {
406 if let Some(parent) = path.parent() {
407 if let Err(e) = self.authority().filesystem.create_dir_all(parent) {
408 self.set_status_message(
409 t!("file.error_saving", error = e.to_string()).to_string(),
410 );
411 return PromptResult::Done;
412 }
413 }
414 self.perform_save_file_as(path);
415 } else {
416 self.set_status_message(t!("buffer.save_cancelled").to_string());
417 }
418 }
419 PromptType::ConfirmCloseBuffer { buffer_id } => {
420 if self.handle_confirm_close_buffer(&input, buffer_id) {
421 return PromptResult::EarlyReturn;
422 }
423 }
424 PromptType::ConfirmQuitWithModified => {
425 if self.handle_confirm_quit_modified(&input) {
426 return PromptResult::EarlyReturn;
427 }
428 }
429 PromptType::ConfirmQuit => {
430 self.handle_confirm_quit(&input);
431 }
432 PromptType::LspRename {
433 original_text,
434 start_pos,
435 end_pos: _,
436 overlay_handle,
437 } => {
438 self.perform_lsp_rename(input, original_text, start_pos, overlay_handle);
439 }
440 PromptType::FileExplorerRename {
441 original_path,
442 original_name,
443 is_new_file,
444 } => {
445 self.perform_file_explorer_rename(original_path, original_name, input, is_new_file);
446 }
447 PromptType::ConfirmDeleteFile { path, is_dir } => {
448 let input_lower = input.trim().to_lowercase();
449 if input_lower == "y" || input_lower == "yes" {
450 self.perform_file_explorer_delete(path, is_dir);
451 } else {
452 self.set_status_message(t!("explorer.delete_cancelled").to_string());
453 }
454 }
455 PromptType::ConfirmPasteConflict { src, dst, is_cut } => {
456 match input.trim().to_lowercase().as_str() {
457 "o" | "overwrite" => {
458 self.perform_file_explorer_paste(src, dst, is_cut);
459 }
460 "r" | "rename" => {
461 let initial = dst
462 .file_name()
463 .map(|n| n.to_string_lossy().to_string())
464 .unwrap_or_default();
465 let dst_dir = dst
466 .parent()
467 .map(|p| p.to_path_buf())
468 .unwrap_or_else(|| dst.clone());
469 self.start_prompt_with_initial_text(
470 t!("explorer.paste_rename_prompt").to_string(),
471 PromptType::FileExplorerPasteRename {
472 src,
473 dst_dir,
474 is_cut,
475 },
476 initial,
477 );
478 }
479 "" | "c" | "cancel" => {
480 self.set_status_message(t!("explorer.paste_cancelled").to_string());
481 }
482 _ => {
483 let name = crate::app::file_explorer::truncate_name_for_prompt(
488 &dst.file_name().unwrap_or_default().to_string_lossy(),
489 40,
490 );
491 self.start_prompt(
492 t!("explorer.paste_conflict", name = &name).to_string(),
493 PromptType::ConfirmPasteConflict { src, dst, is_cut },
494 );
495 }
496 }
497 }
498 PromptType::FileExplorerPasteRename {
499 src,
500 dst_dir,
501 is_cut,
502 } => {
503 if input.trim().is_empty() {
504 self.set_status_message(t!("explorer.paste_cancelled").to_string());
505 return PromptResult::Done;
506 }
507 let new_dst = dst_dir.join(input.trim());
508 if self.authority().filesystem.exists(&new_dst) {
509 self.start_prompt(
510 t!("explorer.paste_conflict", name = input.trim()).to_string(),
511 PromptType::ConfirmPasteConflict {
512 src,
513 dst: new_dst,
514 is_cut,
515 },
516 );
517 } else {
518 self.perform_file_explorer_paste(src, new_dst, is_cut);
519 }
520 }
521 PromptType::ConfirmMultiDelete { paths } => {
522 let input_lower = input.trim().to_lowercase();
523 if input_lower == "y" || input_lower == "yes" {
524 for path in paths {
525 let is_dir = self.authority().filesystem.is_dir(&path).unwrap_or(false);
526 self.perform_file_explorer_delete(path, is_dir);
527 }
528 } else {
529 self.set_status_message(t!("explorer.delete_cancelled").to_string());
530 }
531 }
532 PromptType::ConfirmMultiPasteConflict {
533 safe,
534 confirmed,
535 mut pending,
536 is_cut,
537 } => {
538 let (cur_src, cur_dst) = pending.remove(0);
539 match input.trim() {
545 "o" | "overwrite" => {
546 let mut new_confirmed = confirmed;
547 new_confirmed.push((cur_src, cur_dst));
548 if pending.is_empty() {
549 self.execute_resolved_multi_paste(safe, new_confirmed, is_cut);
550 } else {
551 self.prompt_next_paste_conflict(safe, new_confirmed, pending, is_cut);
552 }
553 }
554 "O" => {
555 let mut new_confirmed = confirmed;
556 new_confirmed.push((cur_src, cur_dst));
557 new_confirmed.extend(pending);
558 self.execute_resolved_multi_paste(safe, new_confirmed, is_cut);
559 }
560 "s" | "skip" => {
561 if pending.is_empty() {
562 self.execute_resolved_multi_paste(safe, confirmed, is_cut);
563 } else {
564 self.prompt_next_paste_conflict(safe, confirmed, pending, is_cut);
565 }
566 }
567 "S" => {
568 self.execute_resolved_multi_paste(safe, confirmed, is_cut);
569 }
570 "" | "c" | "cancel" => {
571 self.set_status_message(t!("explorer.paste_cancelled").to_string());
572 }
573 _ => {
574 let mut pending_with_current = vec![(cur_src, cur_dst)];
577 pending_with_current.extend(pending);
578 self.prompt_next_paste_conflict(
579 safe,
580 confirmed,
581 pending_with_current,
582 is_cut,
583 );
584 }
585 }
586 }
587 PromptType::ConfirmLargeFileEncoding { path } => {
588 let input_lower = input.trim().to_lowercase();
589 let load_key = t!("file.large_encoding.key.load")
590 .to_string()
591 .to_lowercase();
592 let encoding_key = t!("file.large_encoding.key.encoding")
593 .to_string()
594 .to_lowercase();
595 let cancel_key = t!("file.large_encoding.key.cancel")
596 .to_string()
597 .to_lowercase();
598 if input_lower.is_empty() || input_lower == load_key {
600 if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
601 self.set_status_message(
602 t!("file.error_opening", error = e.to_string()).to_string(),
603 );
604 }
605 } else if input_lower == encoding_key {
606 self.start_open_file_with_encoding_prompt(path);
608 } else if input_lower == cancel_key {
609 self.set_status_message(t!("file.open_cancelled").to_string());
610 } else {
611 if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
613 self.set_status_message(
614 t!("file.error_opening", error = e.to_string()).to_string(),
615 );
616 }
617 }
618 }
619 PromptType::StopLspServer => {
620 self.handle_stop_lsp_server(&input);
621 }
622 PromptType::RestartLspServer => {
623 self.handle_restart_lsp_server(&input);
624 }
625 PromptType::SelectTheme { .. } => {
626 self.apply_theme(input.trim());
627 }
628 PromptType::SelectKeybindingMap => {
629 self.apply_keybinding_map(input.trim());
630 }
631 PromptType::SelectCursorStyle => {
632 self.apply_cursor_style(input.trim());
633 }
634 PromptType::SelectLocale => {
635 self.apply_locale(input.trim());
636 }
637 PromptType::CopyWithFormattingTheme => {
638 self.copy_selection_with_theme(input.trim());
639 }
640 PromptType::SwitchToTab => {
641 if let Ok(id) = input.trim().parse::<usize>() {
642 self.switch_to_tab(BufferId(id));
643 }
644 }
645 PromptType::QueryReplaceConfirm => {
646 if let Some(c) = input.chars().next() {
649 if let Err(e) = self.handle_interactive_replace_key(c) {
650 tracing::warn!("Interactive replace failed: {}", e);
651 }
652 }
653 }
654 PromptType::AddRuler => {
655 self.handle_add_ruler(&input);
656 }
657 PromptType::RemoveRuler => {
658 self.handle_remove_ruler(&input);
659 }
660 PromptType::SetTabSize => {
661 self.handle_set_tab_size(&input);
662 }
663 PromptType::SetLineEnding => {
664 self.handle_set_line_ending(&input);
665 }
666 PromptType::SetEncoding => {
667 self.handle_set_encoding(&input);
668 }
669 PromptType::SetLanguage => {
670 self.handle_set_language(&input);
671 }
672 PromptType::ShellCommand { replace } => {
673 self.handle_shell_command(&input, replace);
674 }
675 PromptType::AsyncPrompt => {
676 if let Some(callback_id) = self
678 .active_window_mut()
679 .pending_async_prompt_callback
680 .take()
681 {
682 let json = serde_json::to_string(&input).unwrap_or_else(|_| "null".to_string());
684 self.plugin_manager
685 .read()
686 .unwrap()
687 .resolve_callback(callback_id, json);
688 }
689 }
690 }
691 PromptResult::Done
692 }
693
694 fn handle_save_file_as(&mut self, input: &str) {
696 let expanded_path = expand_tilde(input);
698 let full_path = if expanded_path.is_absolute() {
699 normalize_path(&expanded_path)
700 } else {
701 normalize_path(&self.working_dir().join(&expanded_path))
702 };
703
704 self.save_file_as_with_checks(full_path);
705 }
706
707 pub(crate) fn save_file_as_with_checks(&mut self, full_path: std::path::PathBuf) {
709 let current_file_path = self
711 .active_state()
712 .buffer
713 .file_path()
714 .map(|p| p.to_path_buf());
715 let is_different_file = current_file_path.as_ref() != Some(&full_path);
716
717 if is_different_file && full_path.is_file() {
718 let filename = full_path
720 .file_name()
721 .map(|n| n.to_string_lossy().to_string())
722 .unwrap_or_else(|| full_path.display().to_string());
723 self.start_prompt(
724 t!("buffer.overwrite_confirm", name = &filename).to_string(),
725 PromptType::ConfirmOverwriteFile { path: full_path },
726 );
727 return;
728 }
729
730 if let Some(parent) = full_path.parent() {
732 if !parent.as_os_str().is_empty() && !self.authority().filesystem.exists(parent) {
733 let dir_name = parent
734 .strip_prefix(self.working_dir())
735 .unwrap_or(parent)
736 .display()
737 .to_string();
738 self.start_prompt(
739 t!("buffer.create_directory_confirm", name = &dir_name).to_string(),
740 PromptType::ConfirmCreateDirectory { path: full_path },
741 );
742 return;
743 }
744 }
745
746 self.perform_save_file_as(full_path);
748 }
749
750 pub(crate) fn perform_save_file_as(&mut self, full_path: std::path::PathBuf) {
752 let before_idx = self.active_event_log().current_index();
753 let before_len = self.active_event_log().len();
754 tracing::debug!(
755 "SaveFileAs BEFORE: event_log index={}, len={}",
756 before_idx,
757 before_len
758 );
759
760 match self.active_state_mut().buffer.save_to_file(&full_path) {
761 Ok(()) => {
762 let after_save_idx = self.active_event_log().current_index();
763 let after_save_len = self.active_event_log().len();
764 tracing::debug!(
765 "SaveFileAs AFTER buffer.save_to_file: event_log index={}, len={}",
766 after_save_idx,
767 after_save_len
768 );
769
770 let metadata = BufferMetadata::with_file(
771 full_path.clone(),
772 &full_path,
773 self.working_dir(),
774 self.authority().path_translation.as_ref(),
775 self.config.editor.auto_read_only,
776 );
777 let active_buffer = self.active_buffer();
778 self.active_window_mut()
779 .buffer_metadata
780 .insert(active_buffer, metadata);
781
782 let mut language_changed = false;
785 let mut new_language = String::new();
786 let __buffer_id = self.active_buffer();
787 if let Some(state) = self
788 .windows
789 .get_mut(&self.active_window)
790 .map(|w| &mut w.buffers)
791 .expect("active window present")
792 .get_mut(&__buffer_id)
793 {
794 if state.language == "text" {
795 let first_line = state.buffer.first_line_lossy();
796 let detected =
797 crate::primitives::detected_language::DetectedLanguage::from_path(
798 &full_path,
799 first_line.as_deref(),
800 &self.grammar_registry,
801 &self.config.languages,
802 );
803 new_language = detected.name.clone();
804 state.apply_language(detected);
805 language_changed = new_language != "text";
806 }
807 }
808 if language_changed {
809 #[cfg(feature = "plugins")]
810 self.update_plugin_state_snapshot();
811 self.plugin_manager.read().unwrap().run_hook(
812 "language_changed",
813 crate::services::plugins::hooks::HookArgs::LanguageChanged {
814 buffer_id: self.active_buffer(),
815 language: new_language,
816 },
817 );
818 }
819
820 self.active_event_log_mut().mark_saved();
821 tracing::debug!(
822 "SaveFileAs AFTER mark_saved: event_log index={}, len={}",
823 self.active_event_log().current_index(),
824 self.active_event_log().len()
825 );
826
827 if let Ok(metadata) = self.authority().filesystem.metadata(&full_path) {
828 if let Some(mtime) = metadata.modified {
829 self.file_mod_times_mut().insert(full_path.clone(), mtime);
830 }
831 }
832
833 self.active_window_mut().notify_lsp_save();
834
835 self.emit_event(
836 crate::model::control_event::events::FILE_SAVED.name,
837 serde_json::json!({"path": full_path.display().to_string()}),
838 );
839
840 self.plugin_manager.read().unwrap().run_hook(
841 "after_file_save",
842 crate::services::plugins::hooks::HookArgs::AfterFileSave {
843 buffer_id: self.active_buffer(),
844 path: full_path.clone(),
845 },
846 );
847
848 if let Some(buffer_to_close) = self.active_window_mut().pending_close_buffer.take()
849 {
850 if let Err(e) = self.force_close_buffer(buffer_to_close) {
851 self.set_status_message(
852 t!("file.saved_cannot_close", error = e.to_string()).to_string(),
853 );
854 } else {
855 self.set_status_message(t!("buffer.saved_and_closed").to_string());
856 }
857 } else if !self
858 .active_window_mut()
859 .pending_quit_unnamed_save
860 .is_empty()
861 {
862 let just_saved = self.active_buffer();
865 self.active_window_mut()
866 .pending_quit_unnamed_save
867 .retain(|id| *id != just_saved);
868 self.set_status_message(
869 t!("file.saved_as", path = full_path.display().to_string()).to_string(),
870 );
871 if !self.start_next_quit_save_as() {
872 self.should_quit = true;
873 }
874 } else {
875 self.set_status_message(
876 t!("file.saved_as", path = full_path.display().to_string()).to_string(),
877 );
878 }
879 }
880 Err(e) => {
881 self.active_window_mut().pending_close_buffer = None;
882 self.active_window_mut().pending_quit_unnamed_save.clear();
887 self.set_status_message(t!("file.error_saving", error = e.to_string()).to_string());
888 }
889 }
890 }
891
892 fn handle_set_page_width(&mut self, input: &str) {
894 let active_split = self
895 .windows
896 .get(&self.active_window)
897 .and_then(|w| w.buffers.splits())
898 .map(|(mgr, _)| mgr)
899 .expect("active window must have a populated split layout")
900 .active_split();
901 let trimmed = input.trim();
902
903 if trimmed.is_empty() {
904 if let Some(vs) = self
905 .windows
906 .get_mut(&self.active_window)
907 .and_then(|w| w.split_view_states_mut())
908 .expect("active window must have a populated split layout")
909 .get_mut(&active_split)
910 {
911 vs.compose_width = None;
912 }
913 self.set_status_message(t!("settings.page_width_cleared").to_string());
914 } else {
915 match trimmed.parse::<u16>() {
916 Ok(val) if val > 0 => {
917 if let Some(vs) = self
918 .windows
919 .get_mut(&self.active_window)
920 .and_then(|w| w.split_view_states_mut())
921 .expect("active window must have a populated split layout")
922 .get_mut(&active_split)
923 {
924 vs.compose_width = Some(val);
925 }
926 self.set_status_message(t!("settings.page_width_set", value = val).to_string());
927 }
928 _ => {
929 self.set_status_message(
930 t!("error.invalid_page_width", input = input).to_string(),
931 );
932 }
933 }
934 }
935 }
936
937 fn handle_add_ruler(&mut self, input: &str) {
939 let trimmed = input.trim();
940 match trimmed.parse::<usize>() {
941 Ok(col) if col > 0 => {
942 let active_split = self
943 .windows
944 .get(&self.active_window)
945 .and_then(|w| w.buffers.splits())
946 .map(|(mgr, _)| mgr)
947 .expect("active window must have a populated split layout")
948 .active_split();
949 if let Some(view_state) = self
950 .windows
951 .get_mut(&self.active_window)
952 .and_then(|w| w.split_view_states_mut())
953 .expect("active window must have a populated split layout")
954 .get_mut(&active_split)
955 {
956 if !view_state.rulers.contains(&col) {
957 view_state.rulers.push(col);
958 view_state.rulers.sort();
959 }
960 }
961 let new_rulers = self
963 .windows
964 .get(&self.active_window)
965 .and_then(|w| w.buffers.splits())
966 .map(|(_, vs)| vs)
967 .expect("active window must have a populated split layout")
968 .get(&active_split)
969 .map(|vs| vs.rulers.clone())
970 .unwrap_or_default();
971 self.config_mut().editor.rulers = new_rulers;
972 self.save_rulers_to_config();
973 self.set_status_message(t!("rulers.added", column = col).to_string());
974 }
975 Ok(_) => {
976 self.set_status_message(t!("rulers.must_be_positive").to_string());
977 }
978 Err(_) => {
979 self.set_status_message(t!("rulers.invalid_column", input = input).to_string());
980 }
981 }
982 }
983
984 fn handle_remove_ruler(&mut self, input: &str) {
986 let trimmed = input.trim();
987 if let Ok(col) = trimmed.parse::<usize>() {
988 let active_split = self
989 .windows
990 .get(&self.active_window)
991 .and_then(|w| w.buffers.splits())
992 .map(|(mgr, _)| mgr)
993 .expect("active window must have a populated split layout")
994 .active_split();
995 if let Some(view_state) = self
996 .windows
997 .get_mut(&self.active_window)
998 .and_then(|w| w.split_view_states_mut())
999 .expect("active window must have a populated split layout")
1000 .get_mut(&active_split)
1001 {
1002 view_state.rulers.retain(|&r| r != col);
1003 }
1004 let new_rulers = self
1006 .windows
1007 .get(&self.active_window)
1008 .and_then(|w| w.buffers.splits())
1009 .map(|(_, vs)| vs)
1010 .expect("active window must have a populated split layout")
1011 .get(&active_split)
1012 .map(|vs| vs.rulers.clone())
1013 .unwrap_or_default();
1014 self.config_mut().editor.rulers = new_rulers;
1015 self.save_rulers_to_config();
1016 self.set_status_message(t!("rulers.removed", column = col).to_string());
1017 }
1018 }
1019
1020 fn save_rulers_to_config(&mut self) {
1022 if let Err(e) = self
1023 .authority()
1024 .filesystem
1025 .create_dir_all(&self.dir_context.config_dir)
1026 {
1027 tracing::warn!("Failed to create config directory: {}", e);
1028 return;
1029 }
1030 let resolver =
1031 ConfigResolver::new(self.dir_context.clone(), self.working_dir().to_path_buf());
1032 if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
1033 tracing::warn!("Failed to save rulers to config: {}", e);
1034 }
1035 }
1036
1037 fn handle_set_tab_size(&mut self, input: &str) {
1039 let buffer_id = self.active_buffer();
1040 let trimmed = input.trim();
1041
1042 match trimmed.parse::<usize>() {
1043 Ok(val) if val > 0 => {
1044 if let Some(state) = self
1045 .windows
1046 .get_mut(&self.active_window)
1047 .map(|w| &mut w.buffers)
1048 .expect("active window present")
1049 .get_mut(&buffer_id)
1050 {
1051 state.buffer_settings.tab_size = val;
1052 }
1053 self.set_status_message(t!("settings.tab_size_set", value = val).to_string());
1054 }
1055 Ok(_) => {
1056 self.set_status_message(t!("settings.tab_size_positive").to_string());
1057 }
1058 Err(_) => {
1059 self.set_status_message(t!("error.invalid_tab_size", input = input).to_string());
1060 }
1061 }
1062 }
1063
1064 fn handle_set_line_ending(&mut self, input: &str) {
1066 use crate::model::buffer::LineEnding;
1067
1068 let trimmed = input.trim();
1070 let code = trimmed.split_whitespace().next().unwrap_or(trimmed);
1071
1072 let line_ending = match code.to_uppercase().as_str() {
1073 "LF" => Some(LineEnding::LF),
1074 "CRLF" => Some(LineEnding::CRLF),
1075 "CR" => Some(LineEnding::CR),
1076 _ => None,
1077 };
1078
1079 match line_ending {
1080 Some(le) => {
1081 self.active_state_mut().buffer.set_line_ending(le);
1082 self.set_status_message(
1083 t!("settings.line_ending_set", value = le.display_name()).to_string(),
1084 );
1085 }
1086 None => {
1087 self.set_status_message(t!("error.unknown_line_ending", input = input).to_string());
1088 }
1089 }
1090 }
1091
1092 fn handle_set_encoding(&mut self, input: &str) {
1094 use crate::model::buffer::Encoding;
1095
1096 let trimmed = input.trim();
1097
1098 let encoding = Encoding::all()
1101 .iter()
1102 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
1103 .copied()
1104 .or_else(|| {
1105 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
1107 Encoding::all()
1108 .iter()
1109 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
1110 .copied()
1111 });
1112
1113 match encoding {
1114 Some(enc) => {
1115 self.active_state_mut().buffer.set_encoding(enc);
1116 self.set_status_message(format!("Encoding set to {}", enc.display_name()));
1117 }
1118 None => {
1119 self.set_status_message(format!("Unknown encoding: {}", input));
1120 }
1121 }
1122 }
1123
1124 fn handle_open_file_with_encoding(&mut self, path: &std::path::Path, input: &str) {
1130 use crate::model::buffer::Encoding;
1131 use crate::view::prompt::PromptType;
1132
1133 let trimmed = input.trim();
1134
1135 let encoding = Encoding::all()
1137 .iter()
1138 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
1139 .copied()
1140 .or_else(|| {
1141 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
1142 Encoding::all()
1143 .iter()
1144 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
1145 .copied()
1146 });
1147
1148 match encoding {
1149 Some(enc) => {
1150 let threshold = self.config.editor.large_file_threshold_bytes as usize;
1153 let file_size = self
1154 .authority()
1155 .filesystem
1156 .metadata(path)
1157 .map(|m| m.size as usize)
1158 .unwrap_or(0);
1159
1160 if file_size >= threshold && enc.requires_full_file_load() {
1161 let size_mb = file_size as f64 / (1024.0 * 1024.0);
1163 let load_key = t!("file.large_encoding.key.load").to_string();
1164 let encoding_key = t!("file.large_encoding.key.encoding").to_string();
1165 let cancel_key = t!("file.large_encoding.key.cancel").to_string();
1166 let prompt_msg = t!(
1167 "file.large_encoding_prompt",
1168 encoding = enc.display_name(),
1169 size = format!("{:.0}", size_mb),
1170 load_key = load_key,
1171 encoding_key = encoding_key,
1172 cancel_key = cancel_key
1173 )
1174 .to_string();
1175 self.start_prompt(
1176 prompt_msg,
1177 PromptType::ConfirmLargeFileEncoding {
1178 path: path.to_path_buf(),
1179 },
1180 );
1181 return;
1182 }
1183
1184 self.active_window_mut().key_context =
1186 crate::input::keybindings::KeyContext::Normal;
1187
1188 if let Err(e) = self.open_file_with_encoding(path, enc) {
1190 self.set_status_message(
1191 t!("file.error_opening", error = e.to_string()).to_string(),
1192 );
1193 } else {
1194 self.set_status_message(format!(
1195 "Opened {} with {} encoding",
1196 path.display(),
1197 enc.display_name()
1198 ));
1199 }
1200 }
1201 None => {
1202 self.set_status_message(format!("Unknown encoding: {}", input));
1203 }
1204 }
1205 }
1206
1207 fn handle_reload_with_encoding(&mut self, input: &str) {
1210 use crate::model::buffer::Encoding;
1211
1212 let trimmed = input.trim();
1213
1214 let encoding = Encoding::all()
1216 .iter()
1217 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
1218 .copied()
1219 .or_else(|| {
1220 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
1221 Encoding::all()
1222 .iter()
1223 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
1224 .copied()
1225 });
1226
1227 match encoding {
1228 Some(enc) => {
1229 if let Err(e) = self.reload_with_encoding(enc) {
1231 self.set_status_message(format!("Failed to reload: {}", e));
1232 } else {
1233 self.set_status_message(format!(
1234 "Reloaded with {} encoding",
1235 enc.display_name()
1236 ));
1237 }
1238 }
1239 None => {
1240 self.set_status_message(format!("Unknown encoding: {}", input));
1241 }
1242 }
1243 }
1244
1245 fn handle_set_language(&mut self, input: &str) {
1247 use crate::primitives::detected_language::DetectedLanguage;
1248
1249 let trimmed = input.trim();
1250
1251 if trimmed == "Plain Text" || trimmed.to_lowercase() == "text" {
1253 let buffer_id = self.active_buffer();
1254 if let Some(state) = self
1255 .windows
1256 .get_mut(&self.active_window)
1257 .map(|w| &mut w.buffers)
1258 .expect("active window present")
1259 .get_mut(&buffer_id)
1260 {
1261 state.apply_language(DetectedLanguage::plain_text());
1262 self.set_status_message("Language set to Plain Text".to_string());
1263 }
1264 #[cfg(feature = "plugins")]
1265 self.update_plugin_state_snapshot();
1266 self.plugin_manager.read().unwrap().run_hook(
1267 "language_changed",
1268 crate::services::plugins::hooks::HookArgs::LanguageChanged {
1269 buffer_id: self.active_buffer(),
1270 language: "text".to_string(),
1271 },
1272 );
1273 return;
1274 }
1275
1276 if let Some(detected) = DetectedLanguage::from_syntax_name(
1278 trimmed,
1279 &self.grammar_registry,
1280 &self.config.languages,
1281 ) {
1282 let language = detected.name.clone();
1283 let buffer_id = self.active_buffer();
1284 if let Some(state) = self
1285 .windows
1286 .get_mut(&self.active_window)
1287 .map(|w| &mut w.buffers)
1288 .expect("active window present")
1289 .get_mut(&buffer_id)
1290 {
1291 state.apply_language(detected);
1292 self.set_status_message(format!("Language set to {}", trimmed));
1293 }
1294 #[cfg(feature = "plugins")]
1295 self.update_plugin_state_snapshot();
1296 self.plugin_manager.read().unwrap().run_hook(
1297 "language_changed",
1298 crate::services::plugins::hooks::HookArgs::LanguageChanged {
1299 buffer_id,
1300 language,
1301 },
1302 );
1303 } else {
1304 self.set_status_message(format!("Unknown language: {}", input));
1308 }
1309 }
1310
1311 fn handle_register_input<F>(&mut self, input: &str, action: F, register_type: &str)
1313 where
1314 F: FnOnce(&mut Self, char),
1315 {
1316 if let Some(c) = input.trim().chars().next() {
1317 if c.is_ascii_digit() {
1318 action(self, c);
1319 } else {
1320 self.set_status_message(
1321 t!("register.must_be_digit", "type" = register_type).to_string(),
1322 );
1323 }
1324 } else {
1325 self.set_status_message(t!("register.not_specified").to_string());
1326 }
1327 }
1328
1329 fn handle_confirm_close_buffer(&mut self, input: &str, buffer_id: BufferId) -> bool {
1331 let input_lower = input.trim().to_lowercase();
1332 let save_key = t!("prompt.key.save").to_string().to_lowercase();
1333 let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1334
1335 let first_char = input_lower.chars().next();
1336 let save_first = save_key.chars().next();
1337 let discard_first = discard_key.chars().next();
1338
1339 if first_char == save_first {
1340 let has_path = self
1342 .buffers()
1343 .get(&buffer_id)
1344 .map(|s| s.buffer.file_path().is_some())
1345 .unwrap_or(false);
1346
1347 if has_path {
1348 let old_active = self.active_buffer();
1349 self.set_active_buffer(buffer_id);
1350 if let Err(e) = self.save() {
1351 self.set_status_message(
1352 t!("file.save_failed", error = e.to_string()).to_string(),
1353 );
1354 self.set_active_buffer(old_active);
1355 return true; }
1357 self.set_active_buffer(old_active);
1358 if let Err(e) = self.force_close_buffer(buffer_id) {
1359 self.set_status_message(
1360 t!("file.cannot_close", error = e.to_string()).to_string(),
1361 );
1362 } else {
1363 self.set_status_message(t!("buffer.saved_and_closed").to_string());
1364 }
1365 } else {
1366 self.active_window_mut().pending_close_buffer = Some(buffer_id);
1367 self.start_prompt_with_initial_text(
1368 t!("file.save_as_prompt").to_string(),
1369 PromptType::SaveFileAs,
1370 String::new(),
1371 );
1372 }
1373 } else if first_char == discard_first {
1374 if let Err(e) = self.force_close_buffer(buffer_id) {
1376 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
1377 } else {
1378 self.set_status_message(t!("buffer.changes_discarded").to_string());
1379 }
1380 } else {
1381 self.set_status_message(t!("buffer.close_cancelled").to_string());
1382 }
1383 false
1384 }
1385
1386 fn handle_confirm_quit(&mut self, input: &str) {
1391 let input_trim = input.trim().to_lowercase();
1392 let quit_first = t!("prompt.key.quit")
1393 .to_string()
1394 .to_lowercase()
1395 .chars()
1396 .next();
1397 let first_char = input_trim.chars().next();
1398 let confirms = first_char == quit_first || first_char == Some('y') || input_trim == "yes";
1399 if confirms {
1400 self.should_quit = true;
1401 } else {
1402 self.set_status_message(t!("buffer.close_cancelled").to_string());
1403 }
1404 }
1405
1406 fn handle_confirm_quit_modified(&mut self, input: &str) -> bool {
1408 let input_lower = input.trim().to_lowercase();
1409 let save_key = t!("prompt.key.save").to_string().to_lowercase();
1410 let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1411 let quit_key = t!("prompt.key.quit").to_string().to_lowercase();
1412
1413 let first_char = input_lower.chars().next();
1414 let save_first = save_key.chars().next();
1415 let discard_first = discard_key.chars().next();
1416 let quit_first = quit_key.chars().next();
1417
1418 if first_char == save_first {
1419 match self.save_all_on_exit() {
1421 Ok(count) => {
1422 tracing::info!("Saved {} buffer(s) on exit", count);
1423 }
1424 Err(e) => {
1425 self.set_status_message(
1426 t!("file.save_failed", error = e.to_string()).to_string(),
1427 );
1428 return true; }
1430 }
1431
1432 self.active_window_mut().pending_quit_unnamed_save =
1437 self.collect_unnamed_modified_buffers();
1438 if !self.start_next_quit_save_as() {
1439 self.should_quit = true;
1440 }
1441 } else if first_char == discard_first {
1442 for (_, state) in self
1447 .windows
1448 .get_mut(&self.active_window)
1449 .map(|w| &mut w.buffers)
1450 .expect("active window present")
1451 {
1452 state.buffer.clear_modified();
1453 state.buffer.set_recovery_pending(false);
1454 }
1455 self.should_quit = true;
1456 } else if first_char == quit_first && self.config.editor.hot_exit {
1457 self.should_quit = true;
1459 } else {
1460 self.set_status_message(t!("buffer.close_cancelled").to_string());
1462 }
1463 false
1464 }
1465
1466 pub fn handle_stop_lsp_server(&mut self, input: &str) {
1471 let input = input.trim();
1472 if input.is_empty() {
1473 return;
1474 }
1475
1476 let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1478 (lang, Some(name))
1479 } else {
1480 (input, None)
1481 };
1482
1483 let has_server = self
1484 .lsp()
1485 .as_ref()
1486 .is_some_and(|lsp| lsp.has_handles(language));
1487
1488 if !has_server {
1489 self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1490 return;
1491 }
1492
1493 let stopping_all = server_name.is_none()
1497 || self
1498 .lsp()
1499 .as_ref()
1500 .map(|lsp| lsp.handle_count(language) <= 1)
1501 .unwrap_or(true);
1502
1503 if stopping_all {
1504 let buffer_ids: Vec<_> = self
1511 .buffers()
1512 .iter()
1513 .filter(|(_, s)| s.language == language)
1514 .map(|(id, _)| *id)
1515 .collect();
1516 for buffer_id in buffer_ids {
1517 self.disable_lsp_for_buffer(buffer_id);
1518 }
1519 } else if let Some(name) = server_name {
1520 self.send_did_close_to_server(language, name);
1524 }
1525
1526 let stopped = self.stop_lsp_server_and_cleanup(language, server_name);
1530
1531 if !stopped {
1532 self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1533 return;
1534 }
1535
1536 if let Some(lsp_configs) = self.config_mut().lsp.get_mut(language) {
1538 for c in lsp_configs.as_mut_slice() {
1539 if let Some(name) = server_name {
1540 if c.display_name() == name {
1542 c.auto_start = false;
1543 }
1544 } else {
1545 c.auto_start = false;
1546 }
1547 }
1548 if let Err(e) = self.save_config() {
1549 tracing::warn!(
1550 "Failed to save config after disabling LSP auto-start: {}",
1551 e
1552 );
1553 } else {
1554 let config_path = self.dir_context.config_path();
1555 self.emit_event(
1556 "config_changed",
1557 serde_json::json!({
1558 "path": config_path.to_string_lossy(),
1559 }),
1560 );
1561 }
1562 }
1563
1564 let display = server_name.unwrap_or(language);
1565 self.set_status_message(t!("lsp.server_stopped", language = display).to_string());
1566 }
1567
1568 pub fn handle_restart_lsp_server(&mut self, input: &str) {
1573 let input = input.trim();
1574 if input.is_empty() {
1575 return;
1576 }
1577
1578 let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1580 (lang, Some(name))
1581 } else {
1582 (input, None)
1583 };
1584
1585 let buffer_id = self.active_buffer();
1587 let file_path = self
1588 .active_window()
1589 .buffer_metadata
1590 .get(&buffer_id)
1591 .and_then(|meta| meta.file_path().cloned());
1592
1593 let (success, message) = if let Some(name) = server_name {
1594 let __active_id = self.active_window;
1596 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
1597 lsp.manual_restart_server(language, name, file_path.as_deref())
1598 } else {
1599 (false, t!("lsp.no_manager").to_string())
1600 }
1601 } else {
1602 let __active_id = self.active_window;
1604 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
1605 lsp.manual_restart(language, file_path.as_deref())
1606 } else {
1607 (false, t!("lsp.no_manager").to_string())
1608 }
1609 };
1610
1611 self.active_window_mut().status_message = Some(message);
1612
1613 if success {
1614 self.reopen_buffers_for_language(language);
1615 }
1616 }
1617
1618 fn handle_quick_open_confirm(
1620 &mut self,
1621 input: &str,
1622 selected_index: Option<usize>,
1623 ) -> PromptResult {
1624 use crate::input::quick_open::QuickOpenResult;
1625
1626 let context = self.build_quick_open_context();
1627 let result = if let Some((provider, query)) =
1628 self.quick_open_registry.get_provider_for_input(input)
1629 {
1630 let suggestions = provider.suggestions(query, &context);
1632 let selected = selected_index.and_then(|i| suggestions.get(i));
1633 provider.on_select(selected, query, &context)
1634 } else {
1635 QuickOpenResult::None
1636 };
1637
1638 self.execute_quick_open_result(result)
1639 }
1640
1641 fn execute_quick_open_result(
1643 &mut self,
1644 result: crate::input::quick_open::QuickOpenResult,
1645 ) -> PromptResult {
1646 use crate::input::quick_open::QuickOpenResult;
1647
1648 match &result {
1652 QuickOpenResult::GotoLine(_) => {
1653 self.active_window_mut().goto_line_preview = None;
1656 }
1657 _ => {
1658 self.restore_goto_line_preview_snapshot();
1659 }
1660 }
1661
1662 match result {
1663 QuickOpenResult::ExecuteAction(action) => PromptResult::ExecuteAction(action),
1664 QuickOpenResult::OpenFile { path, line, column } => {
1665 let expanded_path = expand_tilde(&path);
1666 let full_path = if expanded_path.is_absolute() {
1667 expanded_path
1668 } else {
1669 self.working_dir().join(&expanded_path)
1670 };
1671 self.open_file_with_jump(full_path, line, column);
1672 PromptResult::Done
1673 }
1674 QuickOpenResult::ShowBuffer(buffer_id) => {
1675 let buffer_id = crate::model::event::BufferId(buffer_id);
1676 if self
1677 .windows
1678 .get(&self.active_window)
1679 .map(|w| &w.buffers)
1680 .expect("active window present")
1681 .contains_key(&buffer_id)
1682 {
1683 self.set_active_buffer(buffer_id);
1684 if let Some(name) = self.active_state().buffer.file_path() {
1685 self.set_status_message(
1686 t!("buffer.switched", name = name.display().to_string()).to_string(),
1687 );
1688 }
1689 }
1690 PromptResult::Done
1691 }
1692 QuickOpenResult::GotoLine(target) => {
1693 let buffer_id = self.active_buffer();
1694 if let Some(state) = self
1695 .windows
1696 .get(&self.active_window)
1697 .map(|w| &w.buffers)
1698 .expect("active window present")
1699 .get(&buffer_id)
1700 {
1701 let max_line = state.buffer.line_count().unwrap_or(1);
1702 let current_line = state.primary_cursor_line_number.value() + 1;
1703 let line = resolve_goto_line_target(target, current_line, max_line);
1704 self.goto_line_col(line, None);
1705 self.set_status_message(t!("goto.jumped", line = line).to_string());
1706 } else {
1707 self.set_status_message(t!("status.no_selection").to_string());
1708 }
1709 PromptResult::Done
1710 }
1711 QuickOpenResult::None => {
1712 self.set_status_message(t!("status.no_selection").to_string());
1713 PromptResult::Done
1714 }
1715 QuickOpenResult::Error(msg) => {
1716 self.set_status_message(msg);
1717 PromptResult::Done
1718 }
1719 }
1720 }
1721
1722 fn open_file_with_jump(
1723 &mut self,
1724 full_path: std::path::PathBuf,
1725 line: Option<usize>,
1726 column: Option<usize>,
1727 ) {
1728 match self.open_file(&full_path) {
1729 Ok(_) => {
1730 if let Some(line) = line {
1731 self.goto_line_col(line, column);
1732 }
1733 self.active_window_mut().key_context =
1741 crate::input::keybindings::KeyContext::Normal;
1742 self.set_status_message(
1743 t!("buffer.opened", name = full_path.display().to_string()).to_string(),
1744 );
1745 }
1746 Err(e) => {
1747 if let Some(confirmation) =
1749 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
1750 {
1751 self.start_large_file_encoding_confirmation(confirmation);
1752 } else {
1753 self.set_status_message(
1754 t!("file.error_opening", error = e.to_string()).to_string(),
1755 );
1756 }
1757 }
1758 }
1759 }
1760
1761 fn prompt_next_paste_conflict(
1763 &mut self,
1764 safe: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1765 confirmed: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1766 pending: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1767 is_cut: bool,
1768 ) {
1769 let name = crate::app::file_explorer::truncate_name_for_prompt(
1770 &pending[0]
1771 .1
1772 .file_name()
1773 .unwrap_or_default()
1774 .to_string_lossy(),
1775 40,
1776 );
1777 self.start_prompt(
1778 t!("explorer.paste_conflict_multi", name = &name).to_string(),
1779 PromptType::ConfirmMultiPasteConflict {
1780 safe,
1781 confirmed,
1782 pending,
1783 is_cut,
1784 },
1785 );
1786 }
1787}
1788
1789#[cfg(test)]
1794mod tests {
1795 use super::parse_path_line_col;
1796
1797 #[test]
1798 fn test_parse_path_line_col_empty() {
1799 let (path, line, col) = parse_path_line_col("");
1800 assert_eq!(path, "");
1801 assert_eq!(line, None);
1802 assert_eq!(col, None);
1803 }
1804
1805 #[test]
1806 fn test_parse_path_line_col_plain_path() {
1807 let (path, line, col) = parse_path_line_col("src/main.rs");
1808 assert_eq!(path, "src/main.rs");
1809 assert_eq!(line, None);
1810 assert_eq!(col, None);
1811 }
1812
1813 #[test]
1814 fn test_parse_path_line_col_line_only() {
1815 let (path, line, col) = parse_path_line_col("src/main.rs:42");
1816 assert_eq!(path, "src/main.rs");
1817 assert_eq!(line, Some(42));
1818 assert_eq!(col, None);
1819 }
1820
1821 #[test]
1822 fn test_parse_path_line_col_line_and_col() {
1823 let (path, line, col) = parse_path_line_col("src/main.rs:42:10");
1824 assert_eq!(path, "src/main.rs");
1825 assert_eq!(line, Some(42));
1826 assert_eq!(col, Some(10));
1827 }
1828
1829 #[test]
1830 fn test_parse_path_line_col_trimmed() {
1831 let (path, line, col) = parse_path_line_col(" src/main.rs:5:2 ");
1832 assert_eq!(path, "src/main.rs");
1833 assert_eq!(line, Some(5));
1834 assert_eq!(col, Some(2));
1835 }
1836
1837 #[test]
1838 fn test_parse_path_line_col_zero_line_rejected() {
1839 let (path, line, col) = parse_path_line_col("src/main.rs:0");
1840 assert_eq!(path, "src/main.rs:0");
1841 assert_eq!(line, None);
1842 assert_eq!(col, None);
1843 }
1844
1845 #[test]
1846 fn test_parse_path_line_col_zero_col_rejected() {
1847 let (path, line, col) = parse_path_line_col("src/main.rs:1:0");
1848 assert_eq!(path, "src/main.rs:1:0");
1849 assert_eq!(line, None);
1850 assert_eq!(col, None);
1851 }
1852
1853 #[cfg(windows)]
1854 #[test]
1855 fn test_parse_path_line_col_windows_drive() {
1856 let (path, line, col) = parse_path_line_col(r"C:\src\main.rs:12:3");
1857 assert_eq!(path, r"C:\src\main.rs");
1858 assert_eq!(line, Some(12));
1859 assert_eq!(col, Some(3));
1860 }
1861}