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