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::LspRename {
423 original_text,
424 start_pos,
425 end_pos: _,
426 overlay_handle,
427 } => {
428 self.perform_lsp_rename(input, original_text, start_pos, overlay_handle);
429 }
430 PromptType::FileExplorerRename {
431 original_path,
432 original_name,
433 is_new_file,
434 } => {
435 self.perform_file_explorer_rename(original_path, original_name, input, is_new_file);
436 }
437 PromptType::ConfirmDeleteFile { path, is_dir } => {
438 let input_lower = input.trim().to_lowercase();
439 if input_lower == "y" || input_lower == "yes" {
440 self.perform_file_explorer_delete(path, is_dir);
441 } else {
442 self.set_status_message(t!("explorer.delete_cancelled").to_string());
443 }
444 }
445 PromptType::ConfirmPasteConflict { src, dst, is_cut } => {
446 match input.trim().to_lowercase().as_str() {
447 "o" | "overwrite" => {
448 self.perform_file_explorer_paste(src, dst, is_cut);
449 }
450 "r" | "rename" => {
451 let initial = dst
452 .file_name()
453 .map(|n| n.to_string_lossy().to_string())
454 .unwrap_or_default();
455 let dst_dir = dst
456 .parent()
457 .map(|p| p.to_path_buf())
458 .unwrap_or_else(|| dst.clone());
459 self.start_prompt_with_initial_text(
460 t!("explorer.paste_rename_prompt").to_string(),
461 PromptType::FileExplorerPasteRename {
462 src,
463 dst_dir,
464 is_cut,
465 },
466 initial,
467 );
468 }
469 "" | "c" | "cancel" => {
470 self.set_status_message(t!("explorer.paste_cancelled").to_string());
471 }
472 _ => {
473 let name = crate::app::file_explorer::truncate_name_for_prompt(
478 &dst.file_name().unwrap_or_default().to_string_lossy(),
479 40,
480 );
481 self.start_prompt(
482 t!("explorer.paste_conflict", name = &name).to_string(),
483 PromptType::ConfirmPasteConflict { src, dst, is_cut },
484 );
485 }
486 }
487 }
488 PromptType::FileExplorerPasteRename {
489 src,
490 dst_dir,
491 is_cut,
492 } => {
493 if input.trim().is_empty() {
494 self.set_status_message(t!("explorer.paste_cancelled").to_string());
495 return PromptResult::Done;
496 }
497 let new_dst = dst_dir.join(input.trim());
498 if self.authority.filesystem.exists(&new_dst) {
499 self.start_prompt(
500 t!("explorer.paste_conflict", name = input.trim()).to_string(),
501 PromptType::ConfirmPasteConflict {
502 src,
503 dst: new_dst,
504 is_cut,
505 },
506 );
507 } else {
508 self.perform_file_explorer_paste(src, new_dst, is_cut);
509 }
510 }
511 PromptType::ConfirmMultiDelete { paths } => {
512 let input_lower = input.trim().to_lowercase();
513 if input_lower == "y" || input_lower == "yes" {
514 for path in paths {
515 let is_dir = self.authority.filesystem.is_dir(&path).unwrap_or(false);
516 self.perform_file_explorer_delete(path, is_dir);
517 }
518 } else {
519 self.set_status_message(t!("explorer.delete_cancelled").to_string());
520 }
521 }
522 PromptType::ConfirmMultiPasteConflict {
523 safe,
524 confirmed,
525 mut pending,
526 is_cut,
527 } => {
528 let (cur_src, cur_dst) = pending.remove(0);
529 match input.trim() {
535 "o" | "overwrite" => {
536 let mut new_confirmed = confirmed;
537 new_confirmed.push((cur_src, cur_dst));
538 if pending.is_empty() {
539 self.execute_resolved_multi_paste(safe, new_confirmed, is_cut);
540 } else {
541 self.prompt_next_paste_conflict(safe, new_confirmed, pending, is_cut);
542 }
543 }
544 "O" => {
545 let mut new_confirmed = confirmed;
546 new_confirmed.push((cur_src, cur_dst));
547 new_confirmed.extend(pending);
548 self.execute_resolved_multi_paste(safe, new_confirmed, is_cut);
549 }
550 "s" | "skip" => {
551 if pending.is_empty() {
552 self.execute_resolved_multi_paste(safe, confirmed, is_cut);
553 } else {
554 self.prompt_next_paste_conflict(safe, confirmed, pending, is_cut);
555 }
556 }
557 "S" => {
558 self.execute_resolved_multi_paste(safe, confirmed, is_cut);
559 }
560 "" | "c" | "cancel" => {
561 self.set_status_message(t!("explorer.paste_cancelled").to_string());
562 }
563 _ => {
564 let mut pending_with_current = vec![(cur_src, cur_dst)];
567 pending_with_current.extend(pending);
568 self.prompt_next_paste_conflict(
569 safe,
570 confirmed,
571 pending_with_current,
572 is_cut,
573 );
574 }
575 }
576 }
577 PromptType::ConfirmLargeFileEncoding { path } => {
578 let input_lower = input.trim().to_lowercase();
579 let load_key = t!("file.large_encoding.key.load")
580 .to_string()
581 .to_lowercase();
582 let encoding_key = t!("file.large_encoding.key.encoding")
583 .to_string()
584 .to_lowercase();
585 let cancel_key = t!("file.large_encoding.key.cancel")
586 .to_string()
587 .to_lowercase();
588 if input_lower.is_empty() || input_lower == load_key {
590 if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
591 self.set_status_message(
592 t!("file.error_opening", error = e.to_string()).to_string(),
593 );
594 }
595 } else if input_lower == encoding_key {
596 self.start_open_file_with_encoding_prompt(path);
598 } else if input_lower == cancel_key {
599 self.set_status_message(t!("file.open_cancelled").to_string());
600 } else {
601 if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
603 self.set_status_message(
604 t!("file.error_opening", error = e.to_string()).to_string(),
605 );
606 }
607 }
608 }
609 PromptType::StopLspServer => {
610 self.handle_stop_lsp_server(&input);
611 }
612 PromptType::RestartLspServer => {
613 self.handle_restart_lsp_server(&input);
614 }
615 PromptType::SelectTheme { .. } => {
616 self.apply_theme(input.trim());
617 }
618 PromptType::SelectKeybindingMap => {
619 self.apply_keybinding_map(input.trim());
620 }
621 PromptType::SelectCursorStyle => {
622 self.apply_cursor_style(input.trim());
623 }
624 PromptType::SelectLocale => {
625 self.apply_locale(input.trim());
626 }
627 PromptType::CopyWithFormattingTheme => {
628 self.copy_selection_with_theme(input.trim());
629 }
630 PromptType::SwitchToTab => {
631 if let Ok(id) = input.trim().parse::<usize>() {
632 self.switch_to_tab(BufferId(id));
633 }
634 }
635 PromptType::QueryReplaceConfirm => {
636 if let Some(c) = input.chars().next() {
639 if let Err(e) = self.handle_interactive_replace_key(c) {
640 tracing::warn!("Interactive replace failed: {}", e);
641 }
642 }
643 }
644 PromptType::AddRuler => {
645 self.handle_add_ruler(&input);
646 }
647 PromptType::RemoveRuler => {
648 self.handle_remove_ruler(&input);
649 }
650 PromptType::SetTabSize => {
651 self.handle_set_tab_size(&input);
652 }
653 PromptType::SetLineEnding => {
654 self.handle_set_line_ending(&input);
655 }
656 PromptType::SetEncoding => {
657 self.handle_set_encoding(&input);
658 }
659 PromptType::SetLanguage => {
660 self.handle_set_language(&input);
661 }
662 PromptType::ShellCommand { replace } => {
663 self.handle_shell_command(&input, replace);
664 }
665 PromptType::AsyncPrompt => {
666 if let Some(callback_id) = self
668 .active_window_mut()
669 .pending_async_prompt_callback
670 .take()
671 {
672 let json = serde_json::to_string(&input).unwrap_or_else(|_| "null".to_string());
674 self.plugin_manager
675 .read()
676 .unwrap()
677 .resolve_callback(callback_id, json);
678 }
679 }
680 }
681 PromptResult::Done
682 }
683
684 fn handle_save_file_as(&mut self, input: &str) {
686 let expanded_path = expand_tilde(input);
688 let full_path = if expanded_path.is_absolute() {
689 normalize_path(&expanded_path)
690 } else {
691 normalize_path(&self.working_dir.join(&expanded_path))
692 };
693
694 self.save_file_as_with_checks(full_path);
695 }
696
697 pub(crate) fn save_file_as_with_checks(&mut self, full_path: std::path::PathBuf) {
699 let current_file_path = self
701 .active_state()
702 .buffer
703 .file_path()
704 .map(|p| p.to_path_buf());
705 let is_different_file = current_file_path.as_ref() != Some(&full_path);
706
707 if is_different_file && full_path.is_file() {
708 let filename = full_path
710 .file_name()
711 .map(|n| n.to_string_lossy().to_string())
712 .unwrap_or_else(|| full_path.display().to_string());
713 self.start_prompt(
714 t!("buffer.overwrite_confirm", name = &filename).to_string(),
715 PromptType::ConfirmOverwriteFile { path: full_path },
716 );
717 return;
718 }
719
720 if let Some(parent) = full_path.parent() {
722 if !parent.as_os_str().is_empty() && !self.authority.filesystem.exists(parent) {
723 let dir_name = parent
724 .strip_prefix(&self.working_dir)
725 .unwrap_or(parent)
726 .display()
727 .to_string();
728 self.start_prompt(
729 t!("buffer.create_directory_confirm", name = &dir_name).to_string(),
730 PromptType::ConfirmCreateDirectory { path: full_path },
731 );
732 return;
733 }
734 }
735
736 self.perform_save_file_as(full_path);
738 }
739
740 pub(crate) fn perform_save_file_as(&mut self, full_path: std::path::PathBuf) {
742 let before_idx = self.active_event_log().current_index();
743 let before_len = self.active_event_log().len();
744 tracing::debug!(
745 "SaveFileAs BEFORE: event_log index={}, len={}",
746 before_idx,
747 before_len
748 );
749
750 match self.active_state_mut().buffer.save_to_file(&full_path) {
751 Ok(()) => {
752 let after_save_idx = self.active_event_log().current_index();
753 let after_save_len = self.active_event_log().len();
754 tracing::debug!(
755 "SaveFileAs AFTER buffer.save_to_file: event_log index={}, len={}",
756 after_save_idx,
757 after_save_len
758 );
759
760 let metadata = BufferMetadata::with_file(
761 full_path.clone(),
762 &full_path,
763 &self.working_dir,
764 self.authority.path_translation.as_ref(),
765 );
766 let active_buffer = self.active_buffer();
767 self.active_window_mut()
768 .buffer_metadata
769 .insert(active_buffer, metadata);
770
771 let mut language_changed = false;
774 let mut new_language = String::new();
775 let __buffer_id = self.active_buffer();
776 if let Some(state) = self
777 .windows
778 .get_mut(&self.active_window)
779 .map(|w| &mut w.buffers)
780 .expect("active window present")
781 .get_mut(&__buffer_id)
782 {
783 if state.language == "text" {
784 let first_line = state.buffer.first_line_lossy();
785 let detected =
786 crate::primitives::detected_language::DetectedLanguage::from_path(
787 &full_path,
788 first_line.as_deref(),
789 &self.grammar_registry,
790 &self.config.languages,
791 );
792 new_language = detected.name.clone();
793 state.apply_language(detected);
794 language_changed = new_language != "text";
795 }
796 }
797 if language_changed {
798 #[cfg(feature = "plugins")]
799 self.update_plugin_state_snapshot();
800 self.plugin_manager.read().unwrap().run_hook(
801 "language_changed",
802 crate::services::plugins::hooks::HookArgs::LanguageChanged {
803 buffer_id: self.active_buffer(),
804 language: new_language,
805 },
806 );
807 }
808
809 self.active_event_log_mut().mark_saved();
810 tracing::debug!(
811 "SaveFileAs AFTER mark_saved: event_log index={}, len={}",
812 self.active_event_log().current_index(),
813 self.active_event_log().len()
814 );
815
816 if let Ok(metadata) = self.authority.filesystem.metadata(&full_path) {
817 if let Some(mtime) = metadata.modified {
818 self.file_mod_times_mut().insert(full_path.clone(), mtime);
819 }
820 }
821
822 self.active_window_mut().notify_lsp_save();
823
824 self.emit_event(
825 crate::model::control_event::events::FILE_SAVED.name,
826 serde_json::json!({"path": full_path.display().to_string()}),
827 );
828
829 self.plugin_manager.read().unwrap().run_hook(
830 "after_file_save",
831 crate::services::plugins::hooks::HookArgs::AfterFileSave {
832 buffer_id: self.active_buffer(),
833 path: full_path.clone(),
834 },
835 );
836
837 if let Some(buffer_to_close) = self.active_window_mut().pending_close_buffer.take()
838 {
839 if let Err(e) = self.force_close_buffer(buffer_to_close) {
840 self.set_status_message(
841 t!("file.saved_cannot_close", error = e.to_string()).to_string(),
842 );
843 } else {
844 self.set_status_message(t!("buffer.saved_and_closed").to_string());
845 }
846 } else if !self
847 .active_window_mut()
848 .pending_quit_unnamed_save
849 .is_empty()
850 {
851 let just_saved = self.active_buffer();
854 self.active_window_mut()
855 .pending_quit_unnamed_save
856 .retain(|id| *id != just_saved);
857 self.set_status_message(
858 t!("file.saved_as", path = full_path.display().to_string()).to_string(),
859 );
860 if !self.start_next_quit_save_as() {
861 self.should_quit = true;
862 }
863 } else {
864 self.set_status_message(
865 t!("file.saved_as", path = full_path.display().to_string()).to_string(),
866 );
867 }
868 }
869 Err(e) => {
870 self.active_window_mut().pending_close_buffer = None;
871 self.active_window_mut().pending_quit_unnamed_save.clear();
876 self.set_status_message(t!("file.error_saving", error = e.to_string()).to_string());
877 }
878 }
879 }
880
881 fn handle_set_page_width(&mut self, input: &str) {
883 let active_split = self
884 .windows
885 .get(&self.active_window)
886 .and_then(|w| w.buffers.splits())
887 .map(|(mgr, _)| mgr)
888 .expect("active window must have a populated split layout")
889 .active_split();
890 let trimmed = input.trim();
891
892 if trimmed.is_empty() {
893 if let Some(vs) = self
894 .windows
895 .get_mut(&self.active_window)
896 .and_then(|w| w.split_view_states_mut())
897 .expect("active window must have a populated split layout")
898 .get_mut(&active_split)
899 {
900 vs.compose_width = None;
901 }
902 self.set_status_message(t!("settings.page_width_cleared").to_string());
903 } else {
904 match trimmed.parse::<u16>() {
905 Ok(val) if val > 0 => {
906 if let Some(vs) = self
907 .windows
908 .get_mut(&self.active_window)
909 .and_then(|w| w.split_view_states_mut())
910 .expect("active window must have a populated split layout")
911 .get_mut(&active_split)
912 {
913 vs.compose_width = Some(val);
914 }
915 self.set_status_message(t!("settings.page_width_set", value = val).to_string());
916 }
917 _ => {
918 self.set_status_message(
919 t!("error.invalid_page_width", input = input).to_string(),
920 );
921 }
922 }
923 }
924 }
925
926 fn handle_add_ruler(&mut self, input: &str) {
928 let trimmed = input.trim();
929 match trimmed.parse::<usize>() {
930 Ok(col) if col > 0 => {
931 let active_split = self
932 .windows
933 .get(&self.active_window)
934 .and_then(|w| w.buffers.splits())
935 .map(|(mgr, _)| mgr)
936 .expect("active window must have a populated split layout")
937 .active_split();
938 if let Some(view_state) = self
939 .windows
940 .get_mut(&self.active_window)
941 .and_then(|w| w.split_view_states_mut())
942 .expect("active window must have a populated split layout")
943 .get_mut(&active_split)
944 {
945 if !view_state.rulers.contains(&col) {
946 view_state.rulers.push(col);
947 view_state.rulers.sort();
948 }
949 }
950 let new_rulers = self
952 .windows
953 .get(&self.active_window)
954 .and_then(|w| w.buffers.splits())
955 .map(|(_, vs)| vs)
956 .expect("active window must have a populated split layout")
957 .get(&active_split)
958 .map(|vs| vs.rulers.clone())
959 .unwrap_or_default();
960 self.config_mut().editor.rulers = new_rulers;
961 self.save_rulers_to_config();
962 self.set_status_message(t!("rulers.added", column = col).to_string());
963 }
964 Ok(_) => {
965 self.set_status_message(t!("rulers.must_be_positive").to_string());
966 }
967 Err(_) => {
968 self.set_status_message(t!("rulers.invalid_column", input = input).to_string());
969 }
970 }
971 }
972
973 fn handle_remove_ruler(&mut self, input: &str) {
975 let trimmed = input.trim();
976 if let Ok(col) = trimmed.parse::<usize>() {
977 let active_split = self
978 .windows
979 .get(&self.active_window)
980 .and_then(|w| w.buffers.splits())
981 .map(|(mgr, _)| mgr)
982 .expect("active window must have a populated split layout")
983 .active_split();
984 if let Some(view_state) = self
985 .windows
986 .get_mut(&self.active_window)
987 .and_then(|w| w.split_view_states_mut())
988 .expect("active window must have a populated split layout")
989 .get_mut(&active_split)
990 {
991 view_state.rulers.retain(|&r| r != col);
992 }
993 let new_rulers = self
995 .windows
996 .get(&self.active_window)
997 .and_then(|w| w.buffers.splits())
998 .map(|(_, vs)| vs)
999 .expect("active window must have a populated split layout")
1000 .get(&active_split)
1001 .map(|vs| vs.rulers.clone())
1002 .unwrap_or_default();
1003 self.config_mut().editor.rulers = new_rulers;
1004 self.save_rulers_to_config();
1005 self.set_status_message(t!("rulers.removed", column = col).to_string());
1006 }
1007 }
1008
1009 fn save_rulers_to_config(&mut self) {
1011 if let Err(e) = self
1012 .authority
1013 .filesystem
1014 .create_dir_all(&self.dir_context.config_dir)
1015 {
1016 tracing::warn!("Failed to create config directory: {}", e);
1017 return;
1018 }
1019 let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
1020 if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
1021 tracing::warn!("Failed to save rulers to config: {}", e);
1022 }
1023 }
1024
1025 fn handle_set_tab_size(&mut self, input: &str) {
1027 let buffer_id = self.active_buffer();
1028 let trimmed = input.trim();
1029
1030 match trimmed.parse::<usize>() {
1031 Ok(val) if val > 0 => {
1032 if let Some(state) = self
1033 .windows
1034 .get_mut(&self.active_window)
1035 .map(|w| &mut w.buffers)
1036 .expect("active window present")
1037 .get_mut(&buffer_id)
1038 {
1039 state.buffer_settings.tab_size = val;
1040 }
1041 self.set_status_message(t!("settings.tab_size_set", value = val).to_string());
1042 }
1043 Ok(_) => {
1044 self.set_status_message(t!("settings.tab_size_positive").to_string());
1045 }
1046 Err(_) => {
1047 self.set_status_message(t!("error.invalid_tab_size", input = input).to_string());
1048 }
1049 }
1050 }
1051
1052 fn handle_set_line_ending(&mut self, input: &str) {
1054 use crate::model::buffer::LineEnding;
1055
1056 let trimmed = input.trim();
1058 let code = trimmed.split_whitespace().next().unwrap_or(trimmed);
1059
1060 let line_ending = match code.to_uppercase().as_str() {
1061 "LF" => Some(LineEnding::LF),
1062 "CRLF" => Some(LineEnding::CRLF),
1063 "CR" => Some(LineEnding::CR),
1064 _ => None,
1065 };
1066
1067 match line_ending {
1068 Some(le) => {
1069 self.active_state_mut().buffer.set_line_ending(le);
1070 self.set_status_message(
1071 t!("settings.line_ending_set", value = le.display_name()).to_string(),
1072 );
1073 }
1074 None => {
1075 self.set_status_message(t!("error.unknown_line_ending", input = input).to_string());
1076 }
1077 }
1078 }
1079
1080 fn handle_set_encoding(&mut self, input: &str) {
1082 use crate::model::buffer::Encoding;
1083
1084 let trimmed = input.trim();
1085
1086 let encoding = Encoding::all()
1089 .iter()
1090 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
1091 .copied()
1092 .or_else(|| {
1093 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
1095 Encoding::all()
1096 .iter()
1097 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
1098 .copied()
1099 });
1100
1101 match encoding {
1102 Some(enc) => {
1103 self.active_state_mut().buffer.set_encoding(enc);
1104 self.set_status_message(format!("Encoding set to {}", enc.display_name()));
1105 }
1106 None => {
1107 self.set_status_message(format!("Unknown encoding: {}", input));
1108 }
1109 }
1110 }
1111
1112 fn handle_open_file_with_encoding(&mut self, path: &std::path::Path, input: &str) {
1118 use crate::model::buffer::Encoding;
1119 use crate::view::prompt::PromptType;
1120
1121 let trimmed = input.trim();
1122
1123 let encoding = Encoding::all()
1125 .iter()
1126 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
1127 .copied()
1128 .or_else(|| {
1129 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
1130 Encoding::all()
1131 .iter()
1132 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
1133 .copied()
1134 });
1135
1136 match encoding {
1137 Some(enc) => {
1138 let threshold = self.config.editor.large_file_threshold_bytes as usize;
1141 let file_size = self
1142 .authority
1143 .filesystem
1144 .metadata(path)
1145 .map(|m| m.size as usize)
1146 .unwrap_or(0);
1147
1148 if file_size >= threshold && enc.requires_full_file_load() {
1149 let size_mb = file_size as f64 / (1024.0 * 1024.0);
1151 let load_key = t!("file.large_encoding.key.load").to_string();
1152 let encoding_key = t!("file.large_encoding.key.encoding").to_string();
1153 let cancel_key = t!("file.large_encoding.key.cancel").to_string();
1154 let prompt_msg = t!(
1155 "file.large_encoding_prompt",
1156 encoding = enc.display_name(),
1157 size = format!("{:.0}", size_mb),
1158 load_key = load_key,
1159 encoding_key = encoding_key,
1160 cancel_key = cancel_key
1161 )
1162 .to_string();
1163 self.start_prompt(
1164 prompt_msg,
1165 PromptType::ConfirmLargeFileEncoding {
1166 path: path.to_path_buf(),
1167 },
1168 );
1169 return;
1170 }
1171
1172 self.active_window_mut().key_context =
1174 crate::input::keybindings::KeyContext::Normal;
1175
1176 if let Err(e) = self.open_file_with_encoding(path, enc) {
1178 self.set_status_message(
1179 t!("file.error_opening", error = e.to_string()).to_string(),
1180 );
1181 } else {
1182 self.set_status_message(format!(
1183 "Opened {} with {} encoding",
1184 path.display(),
1185 enc.display_name()
1186 ));
1187 }
1188 }
1189 None => {
1190 self.set_status_message(format!("Unknown encoding: {}", input));
1191 }
1192 }
1193 }
1194
1195 fn handle_reload_with_encoding(&mut self, input: &str) {
1198 use crate::model::buffer::Encoding;
1199
1200 let trimmed = input.trim();
1201
1202 let encoding = Encoding::all()
1204 .iter()
1205 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
1206 .copied()
1207 .or_else(|| {
1208 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
1209 Encoding::all()
1210 .iter()
1211 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
1212 .copied()
1213 });
1214
1215 match encoding {
1216 Some(enc) => {
1217 if let Err(e) = self.reload_with_encoding(enc) {
1219 self.set_status_message(format!("Failed to reload: {}", e));
1220 } else {
1221 self.set_status_message(format!(
1222 "Reloaded with {} encoding",
1223 enc.display_name()
1224 ));
1225 }
1226 }
1227 None => {
1228 self.set_status_message(format!("Unknown encoding: {}", input));
1229 }
1230 }
1231 }
1232
1233 fn handle_set_language(&mut self, input: &str) {
1235 use crate::primitives::detected_language::DetectedLanguage;
1236
1237 let trimmed = input.trim();
1238
1239 if trimmed == "Plain Text" || trimmed.to_lowercase() == "text" {
1241 let buffer_id = self.active_buffer();
1242 if let Some(state) = self
1243 .windows
1244 .get_mut(&self.active_window)
1245 .map(|w| &mut w.buffers)
1246 .expect("active window present")
1247 .get_mut(&buffer_id)
1248 {
1249 state.apply_language(DetectedLanguage::plain_text());
1250 self.set_status_message("Language set to Plain Text".to_string());
1251 }
1252 #[cfg(feature = "plugins")]
1253 self.update_plugin_state_snapshot();
1254 self.plugin_manager.read().unwrap().run_hook(
1255 "language_changed",
1256 crate::services::plugins::hooks::HookArgs::LanguageChanged {
1257 buffer_id: self.active_buffer(),
1258 language: "text".to_string(),
1259 },
1260 );
1261 return;
1262 }
1263
1264 if let Some(detected) = DetectedLanguage::from_syntax_name(
1266 trimmed,
1267 &self.grammar_registry,
1268 &self.config.languages,
1269 ) {
1270 let language = detected.name.clone();
1271 let buffer_id = self.active_buffer();
1272 if let Some(state) = self
1273 .windows
1274 .get_mut(&self.active_window)
1275 .map(|w| &mut w.buffers)
1276 .expect("active window present")
1277 .get_mut(&buffer_id)
1278 {
1279 state.apply_language(detected);
1280 self.set_status_message(format!("Language set to {}", trimmed));
1281 }
1282 #[cfg(feature = "plugins")]
1283 self.update_plugin_state_snapshot();
1284 self.plugin_manager.read().unwrap().run_hook(
1285 "language_changed",
1286 crate::services::plugins::hooks::HookArgs::LanguageChanged {
1287 buffer_id,
1288 language,
1289 },
1290 );
1291 } else {
1292 self.set_status_message(format!("Unknown language: {}", input));
1296 }
1297 }
1298
1299 fn handle_register_input<F>(&mut self, input: &str, action: F, register_type: &str)
1301 where
1302 F: FnOnce(&mut Self, char),
1303 {
1304 if let Some(c) = input.trim().chars().next() {
1305 if c.is_ascii_digit() {
1306 action(self, c);
1307 } else {
1308 self.set_status_message(
1309 t!("register.must_be_digit", "type" = register_type).to_string(),
1310 );
1311 }
1312 } else {
1313 self.set_status_message(t!("register.not_specified").to_string());
1314 }
1315 }
1316
1317 fn handle_confirm_close_buffer(&mut self, input: &str, buffer_id: BufferId) -> bool {
1319 let input_lower = input.trim().to_lowercase();
1320 let save_key = t!("prompt.key.save").to_string().to_lowercase();
1321 let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1322
1323 let first_char = input_lower.chars().next();
1324 let save_first = save_key.chars().next();
1325 let discard_first = discard_key.chars().next();
1326
1327 if first_char == save_first {
1328 let has_path = self
1330 .buffers()
1331 .get(&buffer_id)
1332 .map(|s| s.buffer.file_path().is_some())
1333 .unwrap_or(false);
1334
1335 if has_path {
1336 let old_active = self.active_buffer();
1337 self.set_active_buffer(buffer_id);
1338 if let Err(e) = self.save() {
1339 self.set_status_message(
1340 t!("file.save_failed", error = e.to_string()).to_string(),
1341 );
1342 self.set_active_buffer(old_active);
1343 return true; }
1345 self.set_active_buffer(old_active);
1346 if let Err(e) = self.force_close_buffer(buffer_id) {
1347 self.set_status_message(
1348 t!("file.cannot_close", error = e.to_string()).to_string(),
1349 );
1350 } else {
1351 self.set_status_message(t!("buffer.saved_and_closed").to_string());
1352 }
1353 } else {
1354 self.active_window_mut().pending_close_buffer = Some(buffer_id);
1355 self.start_prompt_with_initial_text(
1356 t!("file.save_as_prompt").to_string(),
1357 PromptType::SaveFileAs,
1358 String::new(),
1359 );
1360 }
1361 } else if first_char == discard_first {
1362 if let Err(e) = self.force_close_buffer(buffer_id) {
1364 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
1365 } else {
1366 self.set_status_message(t!("buffer.changes_discarded").to_string());
1367 }
1368 } else {
1369 self.set_status_message(t!("buffer.close_cancelled").to_string());
1370 }
1371 false
1372 }
1373
1374 fn handle_confirm_quit_modified(&mut self, input: &str) -> bool {
1376 let input_lower = input.trim().to_lowercase();
1377 let save_key = t!("prompt.key.save").to_string().to_lowercase();
1378 let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1379 let quit_key = t!("prompt.key.quit").to_string().to_lowercase();
1380
1381 let first_char = input_lower.chars().next();
1382 let save_first = save_key.chars().next();
1383 let discard_first = discard_key.chars().next();
1384 let quit_first = quit_key.chars().next();
1385
1386 if first_char == save_first {
1387 match self.save_all_on_exit() {
1389 Ok(count) => {
1390 tracing::info!("Saved {} buffer(s) on exit", count);
1391 }
1392 Err(e) => {
1393 self.set_status_message(
1394 t!("file.save_failed", error = e.to_string()).to_string(),
1395 );
1396 return true; }
1398 }
1399
1400 self.active_window_mut().pending_quit_unnamed_save =
1405 self.collect_unnamed_modified_buffers();
1406 if !self.start_next_quit_save_as() {
1407 self.should_quit = true;
1408 }
1409 } else if first_char == discard_first {
1410 for (_, state) in self
1415 .windows
1416 .get_mut(&self.active_window)
1417 .map(|w| &mut w.buffers)
1418 .expect("active window present")
1419 {
1420 state.buffer.clear_modified();
1421 state.buffer.set_recovery_pending(false);
1422 }
1423 self.should_quit = true;
1424 } else if first_char == quit_first && self.config.editor.hot_exit {
1425 self.should_quit = true;
1427 } else {
1428 self.set_status_message(t!("buffer.close_cancelled").to_string());
1430 }
1431 false
1432 }
1433
1434 pub fn handle_stop_lsp_server(&mut self, input: &str) {
1439 let input = input.trim();
1440 if input.is_empty() {
1441 return;
1442 }
1443
1444 let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1446 (lang, Some(name))
1447 } else {
1448 (input, None)
1449 };
1450
1451 let has_server = self
1452 .lsp()
1453 .as_ref()
1454 .is_some_and(|lsp| lsp.has_handles(language));
1455
1456 if !has_server {
1457 self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1458 return;
1459 }
1460
1461 let stopping_all = server_name.is_none()
1465 || self
1466 .lsp()
1467 .as_ref()
1468 .map(|lsp| lsp.handle_count(language) <= 1)
1469 .unwrap_or(true);
1470
1471 if stopping_all {
1472 let buffer_ids: Vec<_> = self
1479 .buffers()
1480 .iter()
1481 .filter(|(_, s)| s.language == language)
1482 .map(|(id, _)| *id)
1483 .collect();
1484 for buffer_id in buffer_ids {
1485 self.disable_lsp_for_buffer(buffer_id);
1486 }
1487 } else if let Some(name) = server_name {
1488 self.send_did_close_to_server(language, name);
1492 }
1493
1494 let stopped = self.stop_lsp_server_and_cleanup(language, server_name);
1498
1499 if !stopped {
1500 self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1501 return;
1502 }
1503
1504 if let Some(lsp_configs) = self.config_mut().lsp.get_mut(language) {
1506 for c in lsp_configs.as_mut_slice() {
1507 if let Some(name) = server_name {
1508 if c.display_name() == name {
1510 c.auto_start = false;
1511 }
1512 } else {
1513 c.auto_start = false;
1514 }
1515 }
1516 if let Err(e) = self.save_config() {
1517 tracing::warn!(
1518 "Failed to save config after disabling LSP auto-start: {}",
1519 e
1520 );
1521 } else {
1522 let config_path = self.dir_context.config_path();
1523 self.emit_event(
1524 "config_changed",
1525 serde_json::json!({
1526 "path": config_path.to_string_lossy(),
1527 }),
1528 );
1529 }
1530 }
1531
1532 let display = server_name.unwrap_or(language);
1533 self.set_status_message(t!("lsp.server_stopped", language = display).to_string());
1534 }
1535
1536 pub fn handle_restart_lsp_server(&mut self, input: &str) {
1541 let input = input.trim();
1542 if input.is_empty() {
1543 return;
1544 }
1545
1546 let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1548 (lang, Some(name))
1549 } else {
1550 (input, None)
1551 };
1552
1553 let buffer_id = self.active_buffer();
1555 let file_path = self
1556 .active_window()
1557 .buffer_metadata
1558 .get(&buffer_id)
1559 .and_then(|meta| meta.file_path().cloned());
1560
1561 let (success, message) = if let Some(name) = server_name {
1562 let __active_id = self.active_window;
1564 if let Some(lsp) = self
1565 .windows
1566 .get_mut(&__active_id)
1567 .and_then(|w| w.lsp.as_mut())
1568 {
1569 lsp.manual_restart_server(language, name, file_path.as_deref())
1570 } else {
1571 (false, t!("lsp.no_manager").to_string())
1572 }
1573 } else {
1574 let __active_id = self.active_window;
1576 if let Some(lsp) = self
1577 .windows
1578 .get_mut(&__active_id)
1579 .and_then(|w| w.lsp.as_mut())
1580 {
1581 lsp.manual_restart(language, file_path.as_deref())
1582 } else {
1583 (false, t!("lsp.no_manager").to_string())
1584 }
1585 };
1586
1587 self.active_window_mut().status_message = Some(message);
1588
1589 if success {
1590 self.reopen_buffers_for_language(language);
1591 }
1592 }
1593
1594 fn handle_quick_open_confirm(
1596 &mut self,
1597 input: &str,
1598 selected_index: Option<usize>,
1599 ) -> PromptResult {
1600 use crate::input::quick_open::QuickOpenResult;
1601
1602 let context = self.build_quick_open_context();
1603 let result = if let Some((provider, query)) =
1604 self.quick_open_registry.get_provider_for_input(input)
1605 {
1606 let suggestions = provider.suggestions(query, &context);
1608 let selected = selected_index.and_then(|i| suggestions.get(i));
1609 provider.on_select(selected, query, &context)
1610 } else {
1611 QuickOpenResult::None
1612 };
1613
1614 self.execute_quick_open_result(result)
1615 }
1616
1617 fn execute_quick_open_result(
1619 &mut self,
1620 result: crate::input::quick_open::QuickOpenResult,
1621 ) -> PromptResult {
1622 use crate::input::quick_open::QuickOpenResult;
1623
1624 match &result {
1628 QuickOpenResult::GotoLine(_) => {
1629 self.active_window_mut().goto_line_preview = None;
1632 }
1633 _ => {
1634 self.restore_goto_line_preview_snapshot();
1635 }
1636 }
1637
1638 match result {
1639 QuickOpenResult::ExecuteAction(action) => PromptResult::ExecuteAction(action),
1640 QuickOpenResult::OpenFile { path, line, column } => {
1641 let expanded_path = expand_tilde(&path);
1642 let full_path = if expanded_path.is_absolute() {
1643 expanded_path
1644 } else {
1645 self.working_dir.join(&expanded_path)
1646 };
1647 self.open_file_with_jump(full_path, line, column);
1648 PromptResult::Done
1649 }
1650 QuickOpenResult::ShowBuffer(buffer_id) => {
1651 let buffer_id = crate::model::event::BufferId(buffer_id);
1652 if self
1653 .windows
1654 .get(&self.active_window)
1655 .map(|w| &w.buffers)
1656 .expect("active window present")
1657 .contains_key(&buffer_id)
1658 {
1659 self.set_active_buffer(buffer_id);
1660 if let Some(name) = self.active_state().buffer.file_path() {
1661 self.set_status_message(
1662 t!("buffer.switched", name = name.display().to_string()).to_string(),
1663 );
1664 }
1665 }
1666 PromptResult::Done
1667 }
1668 QuickOpenResult::GotoLine(target) => {
1669 let buffer_id = self.active_buffer();
1670 if let Some(state) = self
1671 .windows
1672 .get(&self.active_window)
1673 .map(|w| &w.buffers)
1674 .expect("active window present")
1675 .get(&buffer_id)
1676 {
1677 let max_line = state.buffer.line_count().unwrap_or(1);
1678 let current_line = state.primary_cursor_line_number.value() + 1;
1679 let line = resolve_goto_line_target(target, current_line, max_line);
1680 self.goto_line_col(line, None);
1681 self.set_status_message(t!("goto.jumped", line = line).to_string());
1682 } else {
1683 self.set_status_message(t!("status.no_selection").to_string());
1684 }
1685 PromptResult::Done
1686 }
1687 QuickOpenResult::None => {
1688 self.set_status_message(t!("status.no_selection").to_string());
1689 PromptResult::Done
1690 }
1691 QuickOpenResult::Error(msg) => {
1692 self.set_status_message(msg);
1693 PromptResult::Done
1694 }
1695 }
1696 }
1697
1698 fn open_file_with_jump(
1699 &mut self,
1700 full_path: std::path::PathBuf,
1701 line: Option<usize>,
1702 column: Option<usize>,
1703 ) {
1704 match self.open_file(&full_path) {
1705 Ok(_) => {
1706 if let Some(line) = line {
1707 self.goto_line_col(line, column);
1708 }
1709 self.set_status_message(
1710 t!("buffer.opened", name = full_path.display().to_string()).to_string(),
1711 );
1712 }
1713 Err(e) => {
1714 if let Some(confirmation) =
1716 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
1717 {
1718 self.start_large_file_encoding_confirmation(confirmation);
1719 } else {
1720 self.set_status_message(
1721 t!("file.error_opening", error = e.to_string()).to_string(),
1722 );
1723 }
1724 }
1725 }
1726 }
1727
1728 fn prompt_next_paste_conflict(
1730 &mut self,
1731 safe: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1732 confirmed: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1733 pending: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1734 is_cut: bool,
1735 ) {
1736 let name = crate::app::file_explorer::truncate_name_for_prompt(
1737 &pending[0]
1738 .1
1739 .file_name()
1740 .unwrap_or_default()
1741 .to_string_lossy(),
1742 40,
1743 );
1744 self.start_prompt(
1745 t!("explorer.paste_conflict_multi", name = &name).to_string(),
1746 PromptType::ConfirmMultiPasteConflict {
1747 safe,
1748 confirmed,
1749 pending,
1750 is_cut,
1751 },
1752 );
1753 }
1754}
1755
1756#[cfg(test)]
1761mod tests {
1762 use super::parse_path_line_col;
1763
1764 #[test]
1765 fn test_parse_path_line_col_empty() {
1766 let (path, line, col) = parse_path_line_col("");
1767 assert_eq!(path, "");
1768 assert_eq!(line, None);
1769 assert_eq!(col, None);
1770 }
1771
1772 #[test]
1773 fn test_parse_path_line_col_plain_path() {
1774 let (path, line, col) = parse_path_line_col("src/main.rs");
1775 assert_eq!(path, "src/main.rs");
1776 assert_eq!(line, None);
1777 assert_eq!(col, None);
1778 }
1779
1780 #[test]
1781 fn test_parse_path_line_col_line_only() {
1782 let (path, line, col) = parse_path_line_col("src/main.rs:42");
1783 assert_eq!(path, "src/main.rs");
1784 assert_eq!(line, Some(42));
1785 assert_eq!(col, None);
1786 }
1787
1788 #[test]
1789 fn test_parse_path_line_col_line_and_col() {
1790 let (path, line, col) = parse_path_line_col("src/main.rs:42:10");
1791 assert_eq!(path, "src/main.rs");
1792 assert_eq!(line, Some(42));
1793 assert_eq!(col, Some(10));
1794 }
1795
1796 #[test]
1797 fn test_parse_path_line_col_trimmed() {
1798 let (path, line, col) = parse_path_line_col(" src/main.rs:5:2 ");
1799 assert_eq!(path, "src/main.rs");
1800 assert_eq!(line, Some(5));
1801 assert_eq!(col, Some(2));
1802 }
1803
1804 #[test]
1805 fn test_parse_path_line_col_zero_line_rejected() {
1806 let (path, line, col) = parse_path_line_col("src/main.rs:0");
1807 assert_eq!(path, "src/main.rs:0");
1808 assert_eq!(line, None);
1809 assert_eq!(col, None);
1810 }
1811
1812 #[test]
1813 fn test_parse_path_line_col_zero_col_rejected() {
1814 let (path, line, col) = parse_path_line_col("src/main.rs:1:0");
1815 assert_eq!(path, "src/main.rs:1:0");
1816 assert_eq!(line, None);
1817 assert_eq!(col, None);
1818 }
1819
1820 #[cfg(windows)]
1821 #[test]
1822 fn test_parse_path_line_col_windows_drive() {
1823 let (path, line, col) = parse_path_line_col(r"C:\src\main.rs:12:3");
1824 assert_eq!(path, r"C:\src\main.rs");
1825 assert_eq!(line, Some(12));
1826 assert_eq!(col, Some(3));
1827 }
1828}