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 );
762 let active_buffer = self.active_buffer();
763 self.active_window_mut()
764 .buffer_metadata
765 .insert(active_buffer, metadata);
766
767 let mut language_changed = false;
770 let mut new_language = String::new();
771 let __buffer_id = self.active_buffer();
772 if let Some(state) = self
773 .windows
774 .get_mut(&self.active_window)
775 .map(|w| &mut w.buffers)
776 .expect("active window present")
777 .get_mut(&__buffer_id)
778 {
779 if state.language == "text" {
780 let first_line = state.buffer.first_line_lossy();
781 let detected =
782 crate::primitives::detected_language::DetectedLanguage::from_path(
783 &full_path,
784 first_line.as_deref(),
785 &self.grammar_registry,
786 &self.config.languages,
787 );
788 new_language = detected.name.clone();
789 state.apply_language(detected);
790 language_changed = new_language != "text";
791 }
792 }
793 if language_changed {
794 #[cfg(feature = "plugins")]
795 self.update_plugin_state_snapshot();
796 self.plugin_manager.read().unwrap().run_hook(
797 "language_changed",
798 crate::services::plugins::hooks::HookArgs::LanguageChanged {
799 buffer_id: self.active_buffer(),
800 language: new_language,
801 },
802 );
803 }
804
805 self.active_event_log_mut().mark_saved();
806 tracing::debug!(
807 "SaveFileAs AFTER mark_saved: event_log index={}, len={}",
808 self.active_event_log().current_index(),
809 self.active_event_log().len()
810 );
811
812 if let Ok(metadata) = self.authority.filesystem.metadata(&full_path) {
813 if let Some(mtime) = metadata.modified {
814 self.file_mod_times_mut().insert(full_path.clone(), mtime);
815 }
816 }
817
818 self.active_window_mut().notify_lsp_save();
819
820 self.emit_event(
821 crate::model::control_event::events::FILE_SAVED.name,
822 serde_json::json!({"path": full_path.display().to_string()}),
823 );
824
825 self.plugin_manager.read().unwrap().run_hook(
826 "after_file_save",
827 crate::services::plugins::hooks::HookArgs::AfterFileSave {
828 buffer_id: self.active_buffer(),
829 path: full_path.clone(),
830 },
831 );
832
833 if let Some(buffer_to_close) = self.active_window_mut().pending_close_buffer.take()
834 {
835 if let Err(e) = self.force_close_buffer(buffer_to_close) {
836 self.set_status_message(
837 t!("file.saved_cannot_close", error = e.to_string()).to_string(),
838 );
839 } else {
840 self.set_status_message(t!("buffer.saved_and_closed").to_string());
841 }
842 } else if !self
843 .active_window_mut()
844 .pending_quit_unnamed_save
845 .is_empty()
846 {
847 let just_saved = self.active_buffer();
850 self.active_window_mut()
851 .pending_quit_unnamed_save
852 .retain(|id| *id != just_saved);
853 self.set_status_message(
854 t!("file.saved_as", path = full_path.display().to_string()).to_string(),
855 );
856 if !self.start_next_quit_save_as() {
857 self.should_quit = true;
858 }
859 } else {
860 self.set_status_message(
861 t!("file.saved_as", path = full_path.display().to_string()).to_string(),
862 );
863 }
864 }
865 Err(e) => {
866 self.active_window_mut().pending_close_buffer = None;
867 self.active_window_mut().pending_quit_unnamed_save.clear();
872 self.set_status_message(t!("file.error_saving", error = e.to_string()).to_string());
873 }
874 }
875 }
876
877 fn handle_set_page_width(&mut self, input: &str) {
879 let active_split = self
880 .windows
881 .get(&self.active_window)
882 .and_then(|w| w.buffers.splits())
883 .map(|(mgr, _)| mgr)
884 .expect("active window must have a populated split layout")
885 .active_split();
886 let trimmed = input.trim();
887
888 if trimmed.is_empty() {
889 if let Some(vs) = self
890 .windows
891 .get_mut(&self.active_window)
892 .and_then(|w| w.split_view_states_mut())
893 .expect("active window must have a populated split layout")
894 .get_mut(&active_split)
895 {
896 vs.compose_width = None;
897 }
898 self.set_status_message(t!("settings.page_width_cleared").to_string());
899 } else {
900 match trimmed.parse::<u16>() {
901 Ok(val) if val > 0 => {
902 if let Some(vs) = self
903 .windows
904 .get_mut(&self.active_window)
905 .and_then(|w| w.split_view_states_mut())
906 .expect("active window must have a populated split layout")
907 .get_mut(&active_split)
908 {
909 vs.compose_width = Some(val);
910 }
911 self.set_status_message(t!("settings.page_width_set", value = val).to_string());
912 }
913 _ => {
914 self.set_status_message(
915 t!("error.invalid_page_width", input = input).to_string(),
916 );
917 }
918 }
919 }
920 }
921
922 fn handle_add_ruler(&mut self, input: &str) {
924 let trimmed = input.trim();
925 match trimmed.parse::<usize>() {
926 Ok(col) if col > 0 => {
927 let active_split = self
928 .windows
929 .get(&self.active_window)
930 .and_then(|w| w.buffers.splits())
931 .map(|(mgr, _)| mgr)
932 .expect("active window must have a populated split layout")
933 .active_split();
934 if let Some(view_state) = self
935 .windows
936 .get_mut(&self.active_window)
937 .and_then(|w| w.split_view_states_mut())
938 .expect("active window must have a populated split layout")
939 .get_mut(&active_split)
940 {
941 if !view_state.rulers.contains(&col) {
942 view_state.rulers.push(col);
943 view_state.rulers.sort();
944 }
945 }
946 let new_rulers = self
948 .windows
949 .get(&self.active_window)
950 .and_then(|w| w.buffers.splits())
951 .map(|(_, vs)| vs)
952 .expect("active window must have a populated split layout")
953 .get(&active_split)
954 .map(|vs| vs.rulers.clone())
955 .unwrap_or_default();
956 self.config_mut().editor.rulers = new_rulers;
957 self.save_rulers_to_config();
958 self.set_status_message(t!("rulers.added", column = col).to_string());
959 }
960 Ok(_) => {
961 self.set_status_message(t!("rulers.must_be_positive").to_string());
962 }
963 Err(_) => {
964 self.set_status_message(t!("rulers.invalid_column", input = input).to_string());
965 }
966 }
967 }
968
969 fn handle_remove_ruler(&mut self, input: &str) {
971 let trimmed = input.trim();
972 if let Ok(col) = trimmed.parse::<usize>() {
973 let active_split = self
974 .windows
975 .get(&self.active_window)
976 .and_then(|w| w.buffers.splits())
977 .map(|(mgr, _)| mgr)
978 .expect("active window must have a populated split layout")
979 .active_split();
980 if let Some(view_state) = self
981 .windows
982 .get_mut(&self.active_window)
983 .and_then(|w| w.split_view_states_mut())
984 .expect("active window must have a populated split layout")
985 .get_mut(&active_split)
986 {
987 view_state.rulers.retain(|&r| r != col);
988 }
989 let new_rulers = self
991 .windows
992 .get(&self.active_window)
993 .and_then(|w| w.buffers.splits())
994 .map(|(_, vs)| vs)
995 .expect("active window must have a populated split layout")
996 .get(&active_split)
997 .map(|vs| vs.rulers.clone())
998 .unwrap_or_default();
999 self.config_mut().editor.rulers = new_rulers;
1000 self.save_rulers_to_config();
1001 self.set_status_message(t!("rulers.removed", column = col).to_string());
1002 }
1003 }
1004
1005 fn save_rulers_to_config(&mut self) {
1007 if let Err(e) = self
1008 .authority
1009 .filesystem
1010 .create_dir_all(&self.dir_context.config_dir)
1011 {
1012 tracing::warn!("Failed to create config directory: {}", e);
1013 return;
1014 }
1015 let resolver =
1016 ConfigResolver::new(self.dir_context.clone(), self.working_dir().to_path_buf());
1017 if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
1018 tracing::warn!("Failed to save rulers to config: {}", e);
1019 }
1020 }
1021
1022 fn handle_set_tab_size(&mut self, input: &str) {
1024 let buffer_id = self.active_buffer();
1025 let trimmed = input.trim();
1026
1027 match trimmed.parse::<usize>() {
1028 Ok(val) if val > 0 => {
1029 if let Some(state) = self
1030 .windows
1031 .get_mut(&self.active_window)
1032 .map(|w| &mut w.buffers)
1033 .expect("active window present")
1034 .get_mut(&buffer_id)
1035 {
1036 state.buffer_settings.tab_size = val;
1037 }
1038 self.set_status_message(t!("settings.tab_size_set", value = val).to_string());
1039 }
1040 Ok(_) => {
1041 self.set_status_message(t!("settings.tab_size_positive").to_string());
1042 }
1043 Err(_) => {
1044 self.set_status_message(t!("error.invalid_tab_size", input = input).to_string());
1045 }
1046 }
1047 }
1048
1049 fn handle_set_line_ending(&mut self, input: &str) {
1051 use crate::model::buffer::LineEnding;
1052
1053 let trimmed = input.trim();
1055 let code = trimmed.split_whitespace().next().unwrap_or(trimmed);
1056
1057 let line_ending = match code.to_uppercase().as_str() {
1058 "LF" => Some(LineEnding::LF),
1059 "CRLF" => Some(LineEnding::CRLF),
1060 "CR" => Some(LineEnding::CR),
1061 _ => None,
1062 };
1063
1064 match line_ending {
1065 Some(le) => {
1066 self.active_state_mut().buffer.set_line_ending(le);
1067 self.set_status_message(
1068 t!("settings.line_ending_set", value = le.display_name()).to_string(),
1069 );
1070 }
1071 None => {
1072 self.set_status_message(t!("error.unknown_line_ending", input = input).to_string());
1073 }
1074 }
1075 }
1076
1077 fn handle_set_encoding(&mut self, input: &str) {
1079 use crate::model::buffer::Encoding;
1080
1081 let trimmed = input.trim();
1082
1083 let encoding = Encoding::all()
1086 .iter()
1087 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
1088 .copied()
1089 .or_else(|| {
1090 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
1092 Encoding::all()
1093 .iter()
1094 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
1095 .copied()
1096 });
1097
1098 match encoding {
1099 Some(enc) => {
1100 self.active_state_mut().buffer.set_encoding(enc);
1101 self.set_status_message(format!("Encoding set to {}", enc.display_name()));
1102 }
1103 None => {
1104 self.set_status_message(format!("Unknown encoding: {}", input));
1105 }
1106 }
1107 }
1108
1109 fn handle_open_file_with_encoding(&mut self, path: &std::path::Path, input: &str) {
1115 use crate::model::buffer::Encoding;
1116 use crate::view::prompt::PromptType;
1117
1118 let trimmed = input.trim();
1119
1120 let encoding = Encoding::all()
1122 .iter()
1123 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
1124 .copied()
1125 .or_else(|| {
1126 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
1127 Encoding::all()
1128 .iter()
1129 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
1130 .copied()
1131 });
1132
1133 match encoding {
1134 Some(enc) => {
1135 let threshold = self.config.editor.large_file_threshold_bytes as usize;
1138 let file_size = self
1139 .authority
1140 .filesystem
1141 .metadata(path)
1142 .map(|m| m.size as usize)
1143 .unwrap_or(0);
1144
1145 if file_size >= threshold && enc.requires_full_file_load() {
1146 let size_mb = file_size as f64 / (1024.0 * 1024.0);
1148 let load_key = t!("file.large_encoding.key.load").to_string();
1149 let encoding_key = t!("file.large_encoding.key.encoding").to_string();
1150 let cancel_key = t!("file.large_encoding.key.cancel").to_string();
1151 let prompt_msg = t!(
1152 "file.large_encoding_prompt",
1153 encoding = enc.display_name(),
1154 size = format!("{:.0}", size_mb),
1155 load_key = load_key,
1156 encoding_key = encoding_key,
1157 cancel_key = cancel_key
1158 )
1159 .to_string();
1160 self.start_prompt(
1161 prompt_msg,
1162 PromptType::ConfirmLargeFileEncoding {
1163 path: path.to_path_buf(),
1164 },
1165 );
1166 return;
1167 }
1168
1169 self.active_window_mut().key_context =
1171 crate::input::keybindings::KeyContext::Normal;
1172
1173 if let Err(e) = self.open_file_with_encoding(path, enc) {
1175 self.set_status_message(
1176 t!("file.error_opening", error = e.to_string()).to_string(),
1177 );
1178 } else {
1179 self.set_status_message(format!(
1180 "Opened {} with {} encoding",
1181 path.display(),
1182 enc.display_name()
1183 ));
1184 }
1185 }
1186 None => {
1187 self.set_status_message(format!("Unknown encoding: {}", input));
1188 }
1189 }
1190 }
1191
1192 fn handle_reload_with_encoding(&mut self, input: &str) {
1195 use crate::model::buffer::Encoding;
1196
1197 let trimmed = input.trim();
1198
1199 let encoding = Encoding::all()
1201 .iter()
1202 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
1203 .copied()
1204 .or_else(|| {
1205 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
1206 Encoding::all()
1207 .iter()
1208 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
1209 .copied()
1210 });
1211
1212 match encoding {
1213 Some(enc) => {
1214 if let Err(e) = self.reload_with_encoding(enc) {
1216 self.set_status_message(format!("Failed to reload: {}", e));
1217 } else {
1218 self.set_status_message(format!(
1219 "Reloaded with {} encoding",
1220 enc.display_name()
1221 ));
1222 }
1223 }
1224 None => {
1225 self.set_status_message(format!("Unknown encoding: {}", input));
1226 }
1227 }
1228 }
1229
1230 fn handle_set_language(&mut self, input: &str) {
1232 use crate::primitives::detected_language::DetectedLanguage;
1233
1234 let trimmed = input.trim();
1235
1236 if trimmed == "Plain Text" || trimmed.to_lowercase() == "text" {
1238 let buffer_id = self.active_buffer();
1239 if let Some(state) = self
1240 .windows
1241 .get_mut(&self.active_window)
1242 .map(|w| &mut w.buffers)
1243 .expect("active window present")
1244 .get_mut(&buffer_id)
1245 {
1246 state.apply_language(DetectedLanguage::plain_text());
1247 self.set_status_message("Language set to Plain Text".to_string());
1248 }
1249 #[cfg(feature = "plugins")]
1250 self.update_plugin_state_snapshot();
1251 self.plugin_manager.read().unwrap().run_hook(
1252 "language_changed",
1253 crate::services::plugins::hooks::HookArgs::LanguageChanged {
1254 buffer_id: self.active_buffer(),
1255 language: "text".to_string(),
1256 },
1257 );
1258 return;
1259 }
1260
1261 if let Some(detected) = DetectedLanguage::from_syntax_name(
1263 trimmed,
1264 &self.grammar_registry,
1265 &self.config.languages,
1266 ) {
1267 let language = detected.name.clone();
1268 let buffer_id = self.active_buffer();
1269 if let Some(state) = self
1270 .windows
1271 .get_mut(&self.active_window)
1272 .map(|w| &mut w.buffers)
1273 .expect("active window present")
1274 .get_mut(&buffer_id)
1275 {
1276 state.apply_language(detected);
1277 self.set_status_message(format!("Language set to {}", trimmed));
1278 }
1279 #[cfg(feature = "plugins")]
1280 self.update_plugin_state_snapshot();
1281 self.plugin_manager.read().unwrap().run_hook(
1282 "language_changed",
1283 crate::services::plugins::hooks::HookArgs::LanguageChanged {
1284 buffer_id,
1285 language,
1286 },
1287 );
1288 } else {
1289 self.set_status_message(format!("Unknown language: {}", input));
1293 }
1294 }
1295
1296 fn handle_register_input<F>(&mut self, input: &str, action: F, register_type: &str)
1298 where
1299 F: FnOnce(&mut Self, char),
1300 {
1301 if let Some(c) = input.trim().chars().next() {
1302 if c.is_ascii_digit() {
1303 action(self, c);
1304 } else {
1305 self.set_status_message(
1306 t!("register.must_be_digit", "type" = register_type).to_string(),
1307 );
1308 }
1309 } else {
1310 self.set_status_message(t!("register.not_specified").to_string());
1311 }
1312 }
1313
1314 fn handle_confirm_close_buffer(&mut self, input: &str, buffer_id: BufferId) -> bool {
1316 let input_lower = input.trim().to_lowercase();
1317 let save_key = t!("prompt.key.save").to_string().to_lowercase();
1318 let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1319
1320 let first_char = input_lower.chars().next();
1321 let save_first = save_key.chars().next();
1322 let discard_first = discard_key.chars().next();
1323
1324 if first_char == save_first {
1325 let has_path = self
1327 .buffers()
1328 .get(&buffer_id)
1329 .map(|s| s.buffer.file_path().is_some())
1330 .unwrap_or(false);
1331
1332 if has_path {
1333 let old_active = self.active_buffer();
1334 self.set_active_buffer(buffer_id);
1335 if let Err(e) = self.save() {
1336 self.set_status_message(
1337 t!("file.save_failed", error = e.to_string()).to_string(),
1338 );
1339 self.set_active_buffer(old_active);
1340 return true; }
1342 self.set_active_buffer(old_active);
1343 if let Err(e) = self.force_close_buffer(buffer_id) {
1344 self.set_status_message(
1345 t!("file.cannot_close", error = e.to_string()).to_string(),
1346 );
1347 } else {
1348 self.set_status_message(t!("buffer.saved_and_closed").to_string());
1349 }
1350 } else {
1351 self.active_window_mut().pending_close_buffer = Some(buffer_id);
1352 self.start_prompt_with_initial_text(
1353 t!("file.save_as_prompt").to_string(),
1354 PromptType::SaveFileAs,
1355 String::new(),
1356 );
1357 }
1358 } else if first_char == discard_first {
1359 if let Err(e) = self.force_close_buffer(buffer_id) {
1361 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
1362 } else {
1363 self.set_status_message(t!("buffer.changes_discarded").to_string());
1364 }
1365 } else {
1366 self.set_status_message(t!("buffer.close_cancelled").to_string());
1367 }
1368 false
1369 }
1370
1371 fn handle_confirm_quit(&mut self, input: &str) {
1376 let input_trim = input.trim().to_lowercase();
1377 let quit_first = t!("prompt.key.quit")
1378 .to_string()
1379 .to_lowercase()
1380 .chars()
1381 .next();
1382 let first_char = input_trim.chars().next();
1383 let confirms = first_char == quit_first || first_char == Some('y') || input_trim == "yes";
1384 if confirms {
1385 self.should_quit = true;
1386 } else {
1387 self.set_status_message(t!("buffer.close_cancelled").to_string());
1388 }
1389 }
1390
1391 fn handle_confirm_quit_modified(&mut self, input: &str) -> bool {
1393 let input_lower = input.trim().to_lowercase();
1394 let save_key = t!("prompt.key.save").to_string().to_lowercase();
1395 let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1396 let quit_key = t!("prompt.key.quit").to_string().to_lowercase();
1397
1398 let first_char = input_lower.chars().next();
1399 let save_first = save_key.chars().next();
1400 let discard_first = discard_key.chars().next();
1401 let quit_first = quit_key.chars().next();
1402
1403 if first_char == save_first {
1404 match self.save_all_on_exit() {
1406 Ok(count) => {
1407 tracing::info!("Saved {} buffer(s) on exit", count);
1408 }
1409 Err(e) => {
1410 self.set_status_message(
1411 t!("file.save_failed", error = e.to_string()).to_string(),
1412 );
1413 return true; }
1415 }
1416
1417 self.active_window_mut().pending_quit_unnamed_save =
1422 self.collect_unnamed_modified_buffers();
1423 if !self.start_next_quit_save_as() {
1424 self.should_quit = true;
1425 }
1426 } else if first_char == discard_first {
1427 for (_, state) in self
1432 .windows
1433 .get_mut(&self.active_window)
1434 .map(|w| &mut w.buffers)
1435 .expect("active window present")
1436 {
1437 state.buffer.clear_modified();
1438 state.buffer.set_recovery_pending(false);
1439 }
1440 self.should_quit = true;
1441 } else if first_char == quit_first && self.config.editor.hot_exit {
1442 self.should_quit = true;
1444 } else {
1445 self.set_status_message(t!("buffer.close_cancelled").to_string());
1447 }
1448 false
1449 }
1450
1451 pub fn handle_stop_lsp_server(&mut self, input: &str) {
1456 let input = input.trim();
1457 if input.is_empty() {
1458 return;
1459 }
1460
1461 let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1463 (lang, Some(name))
1464 } else {
1465 (input, None)
1466 };
1467
1468 let has_server = self
1469 .lsp()
1470 .as_ref()
1471 .is_some_and(|lsp| lsp.has_handles(language));
1472
1473 if !has_server {
1474 self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1475 return;
1476 }
1477
1478 let stopping_all = server_name.is_none()
1482 || self
1483 .lsp()
1484 .as_ref()
1485 .map(|lsp| lsp.handle_count(language) <= 1)
1486 .unwrap_or(true);
1487
1488 if stopping_all {
1489 let buffer_ids: Vec<_> = self
1496 .buffers()
1497 .iter()
1498 .filter(|(_, s)| s.language == language)
1499 .map(|(id, _)| *id)
1500 .collect();
1501 for buffer_id in buffer_ids {
1502 self.disable_lsp_for_buffer(buffer_id);
1503 }
1504 } else if let Some(name) = server_name {
1505 self.send_did_close_to_server(language, name);
1509 }
1510
1511 let stopped = self.stop_lsp_server_and_cleanup(language, server_name);
1515
1516 if !stopped {
1517 self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1518 return;
1519 }
1520
1521 if let Some(lsp_configs) = self.config_mut().lsp.get_mut(language) {
1523 for c in lsp_configs.as_mut_slice() {
1524 if let Some(name) = server_name {
1525 if c.display_name() == name {
1527 c.auto_start = false;
1528 }
1529 } else {
1530 c.auto_start = false;
1531 }
1532 }
1533 if let Err(e) = self.save_config() {
1534 tracing::warn!(
1535 "Failed to save config after disabling LSP auto-start: {}",
1536 e
1537 );
1538 } else {
1539 let config_path = self.dir_context.config_path();
1540 self.emit_event(
1541 "config_changed",
1542 serde_json::json!({
1543 "path": config_path.to_string_lossy(),
1544 }),
1545 );
1546 }
1547 }
1548
1549 let display = server_name.unwrap_or(language);
1550 self.set_status_message(t!("lsp.server_stopped", language = display).to_string());
1551 }
1552
1553 pub fn handle_restart_lsp_server(&mut self, input: &str) {
1558 let input = input.trim();
1559 if input.is_empty() {
1560 return;
1561 }
1562
1563 let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1565 (lang, Some(name))
1566 } else {
1567 (input, None)
1568 };
1569
1570 let buffer_id = self.active_buffer();
1572 let file_path = self
1573 .active_window()
1574 .buffer_metadata
1575 .get(&buffer_id)
1576 .and_then(|meta| meta.file_path().cloned());
1577
1578 let (success, message) = if let Some(name) = server_name {
1579 let __active_id = self.active_window;
1581 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
1582 lsp.manual_restart_server(language, name, file_path.as_deref())
1583 } else {
1584 (false, t!("lsp.no_manager").to_string())
1585 }
1586 } else {
1587 let __active_id = self.active_window;
1589 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
1590 lsp.manual_restart(language, file_path.as_deref())
1591 } else {
1592 (false, t!("lsp.no_manager").to_string())
1593 }
1594 };
1595
1596 self.active_window_mut().status_message = Some(message);
1597
1598 if success {
1599 self.reopen_buffers_for_language(language);
1600 }
1601 }
1602
1603 fn handle_quick_open_confirm(
1605 &mut self,
1606 input: &str,
1607 selected_index: Option<usize>,
1608 ) -> PromptResult {
1609 use crate::input::quick_open::QuickOpenResult;
1610
1611 let context = self.build_quick_open_context();
1612 let result = if let Some((provider, query)) =
1613 self.quick_open_registry.get_provider_for_input(input)
1614 {
1615 let suggestions = provider.suggestions(query, &context);
1617 let selected = selected_index.and_then(|i| suggestions.get(i));
1618 provider.on_select(selected, query, &context)
1619 } else {
1620 QuickOpenResult::None
1621 };
1622
1623 self.execute_quick_open_result(result)
1624 }
1625
1626 fn execute_quick_open_result(
1628 &mut self,
1629 result: crate::input::quick_open::QuickOpenResult,
1630 ) -> PromptResult {
1631 use crate::input::quick_open::QuickOpenResult;
1632
1633 match &result {
1637 QuickOpenResult::GotoLine(_) => {
1638 self.active_window_mut().goto_line_preview = None;
1641 }
1642 _ => {
1643 self.restore_goto_line_preview_snapshot();
1644 }
1645 }
1646
1647 match result {
1648 QuickOpenResult::ExecuteAction(action) => PromptResult::ExecuteAction(action),
1649 QuickOpenResult::OpenFile { path, line, column } => {
1650 let expanded_path = expand_tilde(&path);
1651 let full_path = if expanded_path.is_absolute() {
1652 expanded_path
1653 } else {
1654 self.working_dir().join(&expanded_path)
1655 };
1656 self.open_file_with_jump(full_path, line, column);
1657 PromptResult::Done
1658 }
1659 QuickOpenResult::ShowBuffer(buffer_id) => {
1660 let buffer_id = crate::model::event::BufferId(buffer_id);
1661 if self
1662 .windows
1663 .get(&self.active_window)
1664 .map(|w| &w.buffers)
1665 .expect("active window present")
1666 .contains_key(&buffer_id)
1667 {
1668 self.set_active_buffer(buffer_id);
1669 if let Some(name) = self.active_state().buffer.file_path() {
1670 self.set_status_message(
1671 t!("buffer.switched", name = name.display().to_string()).to_string(),
1672 );
1673 }
1674 }
1675 PromptResult::Done
1676 }
1677 QuickOpenResult::GotoLine(target) => {
1678 let buffer_id = self.active_buffer();
1679 if let Some(state) = self
1680 .windows
1681 .get(&self.active_window)
1682 .map(|w| &w.buffers)
1683 .expect("active window present")
1684 .get(&buffer_id)
1685 {
1686 let max_line = state.buffer.line_count().unwrap_or(1);
1687 let current_line = state.primary_cursor_line_number.value() + 1;
1688 let line = resolve_goto_line_target(target, current_line, max_line);
1689 self.goto_line_col(line, None);
1690 self.set_status_message(t!("goto.jumped", line = line).to_string());
1691 } else {
1692 self.set_status_message(t!("status.no_selection").to_string());
1693 }
1694 PromptResult::Done
1695 }
1696 QuickOpenResult::None => {
1697 self.set_status_message(t!("status.no_selection").to_string());
1698 PromptResult::Done
1699 }
1700 QuickOpenResult::Error(msg) => {
1701 self.set_status_message(msg);
1702 PromptResult::Done
1703 }
1704 }
1705 }
1706
1707 fn open_file_with_jump(
1708 &mut self,
1709 full_path: std::path::PathBuf,
1710 line: Option<usize>,
1711 column: Option<usize>,
1712 ) {
1713 match self.open_file(&full_path) {
1714 Ok(_) => {
1715 if let Some(line) = line {
1716 self.goto_line_col(line, column);
1717 }
1718 self.active_window_mut().key_context =
1726 crate::input::keybindings::KeyContext::Normal;
1727 self.set_status_message(
1728 t!("buffer.opened", name = full_path.display().to_string()).to_string(),
1729 );
1730 }
1731 Err(e) => {
1732 if let Some(confirmation) =
1734 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
1735 {
1736 self.start_large_file_encoding_confirmation(confirmation);
1737 } else {
1738 self.set_status_message(
1739 t!("file.error_opening", error = e.to_string()).to_string(),
1740 );
1741 }
1742 }
1743 }
1744 }
1745
1746 fn prompt_next_paste_conflict(
1748 &mut self,
1749 safe: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1750 confirmed: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1751 pending: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1752 is_cut: bool,
1753 ) {
1754 let name = crate::app::file_explorer::truncate_name_for_prompt(
1755 &pending[0]
1756 .1
1757 .file_name()
1758 .unwrap_or_default()
1759 .to_string_lossy(),
1760 40,
1761 );
1762 self.start_prompt(
1763 t!("explorer.paste_conflict_multi", name = &name).to_string(),
1764 PromptType::ConfirmMultiPasteConflict {
1765 safe,
1766 confirmed,
1767 pending,
1768 is_cut,
1769 },
1770 );
1771 }
1772}
1773
1774#[cfg(test)]
1779mod tests {
1780 use super::parse_path_line_col;
1781
1782 #[test]
1783 fn test_parse_path_line_col_empty() {
1784 let (path, line, col) = parse_path_line_col("");
1785 assert_eq!(path, "");
1786 assert_eq!(line, None);
1787 assert_eq!(col, None);
1788 }
1789
1790 #[test]
1791 fn test_parse_path_line_col_plain_path() {
1792 let (path, line, col) = parse_path_line_col("src/main.rs");
1793 assert_eq!(path, "src/main.rs");
1794 assert_eq!(line, None);
1795 assert_eq!(col, None);
1796 }
1797
1798 #[test]
1799 fn test_parse_path_line_col_line_only() {
1800 let (path, line, col) = parse_path_line_col("src/main.rs:42");
1801 assert_eq!(path, "src/main.rs");
1802 assert_eq!(line, Some(42));
1803 assert_eq!(col, None);
1804 }
1805
1806 #[test]
1807 fn test_parse_path_line_col_line_and_col() {
1808 let (path, line, col) = parse_path_line_col("src/main.rs:42:10");
1809 assert_eq!(path, "src/main.rs");
1810 assert_eq!(line, Some(42));
1811 assert_eq!(col, Some(10));
1812 }
1813
1814 #[test]
1815 fn test_parse_path_line_col_trimmed() {
1816 let (path, line, col) = parse_path_line_col(" src/main.rs:5:2 ");
1817 assert_eq!(path, "src/main.rs");
1818 assert_eq!(line, Some(5));
1819 assert_eq!(col, Some(2));
1820 }
1821
1822 #[test]
1823 fn test_parse_path_line_col_zero_line_rejected() {
1824 let (path, line, col) = parse_path_line_col("src/main.rs:0");
1825 assert_eq!(path, "src/main.rs:0");
1826 assert_eq!(line, None);
1827 assert_eq!(col, None);
1828 }
1829
1830 #[test]
1831 fn test_parse_path_line_col_zero_col_rejected() {
1832 let (path, line, col) = parse_path_line_col("src/main.rs:1:0");
1833 assert_eq!(path, "src/main.rs:1:0");
1834 assert_eq!(line, None);
1835 assert_eq!(col, None);
1836 }
1837
1838 #[cfg(windows)]
1839 #[test]
1840 fn test_parse_path_line_col_windows_drive() {
1841 let (path, line, col) = parse_path_line_col(r"C:\src\main.rs:12:3");
1842 assert_eq!(path, r"C:\src\main.rs");
1843 assert_eq!(line, Some(12));
1844 assert_eq!(col, Some(3));
1845 }
1846}