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