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
31impl Editor {
32 pub fn handle_prompt_confirm_input(
36 &mut self,
37 input: String,
38 prompt_type: PromptType,
39 selected_index: Option<usize>,
40 ) -> PromptResult {
41 match prompt_type {
42 PromptType::OpenFile => {
43 let (path_str, line, column) = parse_path_line_col(&input);
44 let expanded_path = expand_tilde(&path_str);
46 let resolved_path = if expanded_path.is_absolute() {
47 normalize_path(&expanded_path)
48 } else {
49 normalize_path(&self.working_dir.join(&expanded_path))
50 };
51
52 self.open_file_with_jump(resolved_path, line, column);
53 }
54 PromptType::OpenFileWithEncoding { path } => {
55 self.handle_open_file_with_encoding(&path, &input);
56 }
57 PromptType::ReloadWithEncoding => {
58 self.handle_reload_with_encoding(&input);
59 }
60 PromptType::SwitchProject => {
61 let expanded_path = expand_tilde(&input);
63 let resolved_path = if expanded_path.is_absolute() {
64 normalize_path(&expanded_path)
65 } else {
66 normalize_path(&self.working_dir.join(&expanded_path))
67 };
68
69 if resolved_path.is_dir() {
70 self.change_working_dir(resolved_path);
71 } else {
72 self.set_status_message(
73 t!(
74 "file.not_directory",
75 path = resolved_path.display().to_string()
76 )
77 .to_string(),
78 );
79 }
80 }
81 PromptType::SaveFileAs => {
82 self.handle_save_file_as(&input);
83 }
84 PromptType::Search => {
85 self.perform_search(&input);
86 }
87 PromptType::ReplaceSearch => {
88 self.perform_search(&input);
89 self.start_prompt(
90 t!("replace.prompt", search = &input).to_string(),
91 PromptType::Replace {
92 search: input.clone(),
93 },
94 );
95 }
96 PromptType::Replace { search } => {
97 if self.search_confirm_each {
98 self.start_interactive_replace(&search, &input);
99 } else {
100 self.perform_replace(&search, &input);
101 }
102 }
103 PromptType::QueryReplaceSearch => {
104 self.perform_search(&input);
105 self.start_prompt(
106 t!("replace.query_prompt", search = &input).to_string(),
107 PromptType::QueryReplace {
108 search: input.clone(),
109 },
110 );
111 }
112 PromptType::QueryReplace { search } => {
113 if self.search_confirm_each {
114 self.start_interactive_replace(&search, &input);
115 } else {
116 self.perform_replace(&search, &input);
117 }
118 }
119 PromptType::GotoLine => match input.trim().parse::<usize>() {
120 Ok(line_num) if line_num > 0 => {
121 self.goto_line_preview = None;
126 self.goto_line_col(line_num, None);
127 self.set_status_message(t!("goto.jumped", line = line_num).to_string());
128 }
129 Ok(_) => {
130 self.set_status_message(t!("goto.line_must_be_positive").to_string());
134 }
135 Err(_) => {
136 self.set_status_message(t!("error.invalid_line", input = &input).to_string());
137 }
138 },
139 PromptType::GotoByteOffset => {
140 let trimmed = input.trim();
142 let num_str = trimmed
143 .strip_suffix('B')
144 .or_else(|| trimmed.strip_suffix('b'))
145 .unwrap_or(trimmed);
146 match num_str.parse::<usize>() {
147 Ok(offset) => {
148 self.goto_byte_offset(offset);
149 self.set_status_message(
150 t!("goto.jumped_byte", offset = offset).to_string(),
151 );
152 }
153 Err(_) => {
154 self.set_status_message(
155 t!("goto.invalid_byte_offset", input = &input).to_string(),
156 );
157 }
158 }
159 }
160 PromptType::GotoLineScanConfirm => {
161 let answer = input.trim().to_lowercase();
162 if answer == "y" || answer == "yes" {
163 self.start_incremental_line_scan(true);
165 } else {
168 self.start_prompt(
170 t!("goto.byte_offset_prompt").to_string(),
171 PromptType::GotoByteOffset,
172 );
173 }
174 }
175 PromptType::QuickOpen => {
176 return self.handle_quick_open_confirm(&input, selected_index);
178 }
179 PromptType::SetBackgroundFile => {
180 if let Err(e) = self.load_ansi_background(&input) {
181 self.set_status_message(
182 t!("error.background_load_failed", error = e.to_string()).to_string(),
183 );
184 }
185 }
186 PromptType::SetBackgroundBlend => match input.trim().parse::<f32>() {
187 Ok(val) => {
188 let clamped = val.clamp(0.0, 1.0);
189 self.background_fade = clamped;
190 self.set_status_message(
191 t!(
192 "error.background_blend_set",
193 value = format!("{:.2}", clamped)
194 )
195 .to_string(),
196 );
197 }
198 Err(_) => {
199 self.set_status_message(t!("error.invalid_blend", input = &input).to_string());
200 }
201 },
202 PromptType::SetPageWidth => {
203 self.handle_set_page_width(&input);
204 }
205 PromptType::RecordMacro => {
206 self.handle_register_input(
207 &input,
208 |editor, c| editor.toggle_macro_recording(c),
209 "Macro",
210 );
211 }
212 PromptType::PlayMacro => {
213 self.handle_register_input(&input, |editor, c| editor.play_macro(c), "Macro");
214 }
215 PromptType::SetBookmark => {
216 self.handle_register_input(&input, |editor, c| editor.set_bookmark(c), "Bookmark");
217 }
218 PromptType::JumpToBookmark => {
219 self.handle_register_input(
220 &input,
221 |editor, c| editor.jump_to_bookmark(c),
222 "Bookmark",
223 );
224 }
225 PromptType::Plugin { custom_type } => {
226 tracing::info!(
227 "prompt_confirmed: dispatching hook for prompt_type='{}', input='{}', selected_index={:?}",
228 custom_type, input, selected_index
229 );
230 self.plugin_manager.run_hook(
231 "prompt_confirmed",
232 HookArgs::PromptConfirmed {
233 prompt_type: custom_type.clone(),
234 input,
235 selected_index,
236 },
237 );
238 tracing::info!(
239 "prompt_confirmed: hook dispatched for prompt_type='{}'",
240 custom_type
241 );
242 }
243 PromptType::ConfirmRevert => {
244 let input_lower = input.trim().to_lowercase();
245 let revert_key = t!("prompt.key.revert").to_string().to_lowercase();
246 if input_lower == revert_key || input_lower == "revert" {
247 if let Err(e) = self.revert_file() {
248 self.set_status_message(
249 t!("file.revert_failed", error = e.to_string()).to_string(),
250 );
251 }
252 } else {
253 self.set_status_message(t!("buffer.revert_cancelled").to_string());
254 }
255 }
256 PromptType::ConfirmSaveConflict => {
257 let input_lower = input.trim().to_lowercase();
258 if input_lower == "o" || input_lower == "overwrite" {
259 if let Err(e) = self.save() {
260 self.set_status_message(
261 t!("file.save_failed", error = e.to_string()).to_string(),
262 );
263 }
264 } else {
265 self.set_status_message(t!("buffer.save_cancelled").to_string());
266 }
267 }
268 PromptType::ConfirmSudoSave { info } => {
269 let input_lower = input.trim().to_lowercase();
270 if input_lower == "y" || input_lower == "yes" {
271 self.cancel_prompt();
273
274 let result = (|| -> anyhow::Result<()> {
276 let data = self.authority.filesystem.read_file(&info.temp_path)?;
277 self.authority.filesystem.sudo_write(
278 &info.dest_path,
279 &data,
280 info.mode,
281 info.uid,
282 info.gid,
283 )?;
284 #[allow(clippy::let_underscore_must_use)]
286 let _ = self.authority.filesystem.remove_file(&info.temp_path);
287 Ok(())
288 })();
289
290 match result {
291 Ok(_) => {
292 if let Err(e) = self
293 .active_state_mut()
294 .buffer
295 .finalize_external_save(info.dest_path.clone())
296 {
297 tracing::warn!("Failed to finalize sudo save: {}", e);
298 self.set_status_message(
299 t!("prompt.sudo_save_failed", error = e.to_string())
300 .to_string(),
301 );
302 } else if let Err(e) = self.finalize_save(Some(info.dest_path)) {
303 tracing::warn!("Failed to finalize save after sudo: {}", e);
304 self.set_status_message(
305 t!("prompt.sudo_save_failed", error = e.to_string())
306 .to_string(),
307 );
308 }
309 }
310 Err(e) => {
311 tracing::warn!("Sudo save failed: {}", e);
312 self.set_status_message(
313 t!("prompt.sudo_save_failed", error = e.to_string()).to_string(),
314 );
315 #[allow(clippy::let_underscore_must_use)]
317 let _ = self.authority.filesystem.remove_file(&info.temp_path);
318 }
319 }
320 } else {
321 self.set_status_message(t!("buffer.save_cancelled").to_string());
322 #[allow(clippy::let_underscore_must_use)]
324 let _ = self.authority.filesystem.remove_file(&info.temp_path);
325 }
326 }
327 PromptType::ConfirmOverwriteFile { path } => {
328 let input_lower = input.trim().to_lowercase();
329 if input_lower == "o" || input_lower == "overwrite" {
330 self.perform_save_file_as(path);
331 } else {
332 self.set_status_message(t!("buffer.save_cancelled").to_string());
333 }
334 }
335 PromptType::ConfirmCreateDirectory { path } => {
336 let input_lower = input.trim().to_lowercase();
337 if input_lower == "c" || input_lower == "create" {
338 if let Some(parent) = path.parent() {
339 if let Err(e) = self.authority.filesystem.create_dir_all(parent) {
340 self.set_status_message(
341 t!("file.error_saving", error = e.to_string()).to_string(),
342 );
343 return PromptResult::Done;
344 }
345 }
346 self.perform_save_file_as(path);
347 } else {
348 self.set_status_message(t!("buffer.save_cancelled").to_string());
349 }
350 }
351 PromptType::ConfirmCloseBuffer { buffer_id } => {
352 if self.handle_confirm_close_buffer(&input, buffer_id) {
353 return PromptResult::EarlyReturn;
354 }
355 }
356 PromptType::ConfirmQuitWithModified => {
357 if self.handle_confirm_quit_modified(&input) {
358 return PromptResult::EarlyReturn;
359 }
360 }
361 PromptType::LspRename {
362 original_text,
363 start_pos,
364 end_pos: _,
365 overlay_handle,
366 } => {
367 self.perform_lsp_rename(input, original_text, start_pos, overlay_handle);
368 }
369 PromptType::FileExplorerRename {
370 original_path,
371 original_name,
372 is_new_file,
373 } => {
374 self.perform_file_explorer_rename(original_path, original_name, input, is_new_file);
375 }
376 PromptType::ConfirmDeleteFile { path, is_dir } => {
377 let input_lower = input.trim().to_lowercase();
378 if input_lower == "y" || input_lower == "yes" {
379 self.perform_file_explorer_delete(path, is_dir);
380 } else {
381 self.set_status_message(t!("explorer.delete_cancelled").to_string());
382 }
383 }
384 PromptType::ConfirmPasteConflict { src, dst, is_cut } => {
385 match input.trim().to_lowercase().as_str() {
386 "o" | "overwrite" => {
387 self.perform_file_explorer_paste(src, dst, is_cut);
388 }
389 "r" | "rename" => {
390 let initial = dst
391 .file_name()
392 .map(|n| n.to_string_lossy().to_string())
393 .unwrap_or_default();
394 let dst_dir = dst
395 .parent()
396 .map(|p| p.to_path_buf())
397 .unwrap_or_else(|| dst.clone());
398 self.start_prompt_with_initial_text(
399 t!("explorer.paste_rename_prompt").to_string(),
400 PromptType::FileExplorerPasteRename {
401 src,
402 dst_dir,
403 is_cut,
404 },
405 initial,
406 );
407 }
408 "" | "c" | "cancel" => {
409 self.set_status_message(t!("explorer.paste_cancelled").to_string());
410 }
411 _ => {
412 let name = crate::app::file_explorer::truncate_name_for_prompt(
417 &dst.file_name().unwrap_or_default().to_string_lossy(),
418 40,
419 );
420 self.start_prompt(
421 t!("explorer.paste_conflict", name = &name).to_string(),
422 PromptType::ConfirmPasteConflict { src, dst, is_cut },
423 );
424 }
425 }
426 }
427 PromptType::FileExplorerPasteRename {
428 src,
429 dst_dir,
430 is_cut,
431 } => {
432 if input.trim().is_empty() {
433 self.set_status_message(t!("explorer.paste_cancelled").to_string());
434 return PromptResult::Done;
435 }
436 let new_dst = dst_dir.join(input.trim());
437 if self.authority.filesystem.exists(&new_dst) {
438 self.start_prompt(
439 t!("explorer.paste_conflict", name = input.trim()).to_string(),
440 PromptType::ConfirmPasteConflict {
441 src,
442 dst: new_dst,
443 is_cut,
444 },
445 );
446 } else {
447 self.perform_file_explorer_paste(src, new_dst, is_cut);
448 }
449 }
450 PromptType::ConfirmMultiDelete { paths } => {
451 let input_lower = input.trim().to_lowercase();
452 if input_lower == "y" || input_lower == "yes" {
453 for path in paths {
454 let is_dir = self.authority.filesystem.is_dir(&path).unwrap_or(false);
455 self.perform_file_explorer_delete(path, is_dir);
456 }
457 } else {
458 self.set_status_message(t!("explorer.delete_cancelled").to_string());
459 }
460 }
461 PromptType::ConfirmMultiPasteConflict {
462 safe,
463 confirmed,
464 mut pending,
465 is_cut,
466 } => {
467 let (cur_src, cur_dst) = pending.remove(0);
468 match input.trim() {
474 "o" | "overwrite" => {
475 let mut new_confirmed = confirmed;
476 new_confirmed.push((cur_src, cur_dst));
477 if pending.is_empty() {
478 self.execute_resolved_multi_paste(safe, new_confirmed, is_cut);
479 } else {
480 self.prompt_next_paste_conflict(safe, new_confirmed, pending, is_cut);
481 }
482 }
483 "O" => {
484 let mut new_confirmed = confirmed;
485 new_confirmed.push((cur_src, cur_dst));
486 new_confirmed.extend(pending);
487 self.execute_resolved_multi_paste(safe, new_confirmed, is_cut);
488 }
489 "s" | "skip" => {
490 if pending.is_empty() {
491 self.execute_resolved_multi_paste(safe, confirmed, is_cut);
492 } else {
493 self.prompt_next_paste_conflict(safe, confirmed, pending, is_cut);
494 }
495 }
496 "S" => {
497 self.execute_resolved_multi_paste(safe, confirmed, is_cut);
498 }
499 "" | "c" | "cancel" => {
500 self.set_status_message(t!("explorer.paste_cancelled").to_string());
501 }
502 _ => {
503 let mut pending_with_current = vec![(cur_src, cur_dst)];
506 pending_with_current.extend(pending);
507 self.prompt_next_paste_conflict(
508 safe,
509 confirmed,
510 pending_with_current,
511 is_cut,
512 );
513 }
514 }
515 }
516 PromptType::ConfirmLargeFileEncoding { path } => {
517 let input_lower = input.trim().to_lowercase();
518 let load_key = t!("file.large_encoding.key.load")
519 .to_string()
520 .to_lowercase();
521 let encoding_key = t!("file.large_encoding.key.encoding")
522 .to_string()
523 .to_lowercase();
524 let cancel_key = t!("file.large_encoding.key.cancel")
525 .to_string()
526 .to_lowercase();
527 if input_lower.is_empty() || input_lower == load_key {
529 if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
530 self.set_status_message(
531 t!("file.error_opening", error = e.to_string()).to_string(),
532 );
533 }
534 } else if input_lower == encoding_key {
535 self.start_open_file_with_encoding_prompt(path);
537 } else if input_lower == cancel_key {
538 self.set_status_message(t!("file.open_cancelled").to_string());
539 } else {
540 if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
542 self.set_status_message(
543 t!("file.error_opening", error = e.to_string()).to_string(),
544 );
545 }
546 }
547 }
548 PromptType::StopLspServer => {
549 self.handle_stop_lsp_server(&input);
550 }
551 PromptType::RestartLspServer => {
552 self.handle_restart_lsp_server(&input);
553 }
554 PromptType::SelectTheme { .. } => {
555 self.apply_theme(input.trim());
556 }
557 PromptType::SelectKeybindingMap => {
558 self.apply_keybinding_map(input.trim());
559 }
560 PromptType::SelectCursorStyle => {
561 self.apply_cursor_style(input.trim());
562 }
563 PromptType::SelectLocale => {
564 self.apply_locale(input.trim());
565 }
566 PromptType::CopyWithFormattingTheme => {
567 self.copy_selection_with_theme(input.trim());
568 }
569 PromptType::SwitchToTab => {
570 if let Ok(id) = input.trim().parse::<usize>() {
571 self.switch_to_tab(BufferId(id));
572 }
573 }
574 PromptType::QueryReplaceConfirm => {
575 if let Some(c) = input.chars().next() {
578 if let Err(e) = self.handle_interactive_replace_key(c) {
579 tracing::warn!("Interactive replace failed: {}", e);
580 }
581 }
582 }
583 PromptType::AddRuler => {
584 self.handle_add_ruler(&input);
585 }
586 PromptType::RemoveRuler => {
587 self.handle_remove_ruler(&input);
588 }
589 PromptType::SetTabSize => {
590 self.handle_set_tab_size(&input);
591 }
592 PromptType::SetLineEnding => {
593 self.handle_set_line_ending(&input);
594 }
595 PromptType::SetEncoding => {
596 self.handle_set_encoding(&input);
597 }
598 PromptType::SetLanguage => {
599 self.handle_set_language(&input);
600 }
601 PromptType::ShellCommand { replace } => {
602 self.handle_shell_command(&input, replace);
603 }
604 PromptType::AsyncPrompt => {
605 if let Some(callback_id) = self.pending_async_prompt_callback.take() {
607 let json = serde_json::to_string(&input).unwrap_or_else(|_| "null".to_string());
609 self.plugin_manager.resolve_callback(callback_id, json);
610 }
611 }
612 }
613 PromptResult::Done
614 }
615
616 fn handle_save_file_as(&mut self, input: &str) {
618 let expanded_path = expand_tilde(input);
620 let full_path = if expanded_path.is_absolute() {
621 normalize_path(&expanded_path)
622 } else {
623 normalize_path(&self.working_dir.join(&expanded_path))
624 };
625
626 self.save_file_as_with_checks(full_path);
627 }
628
629 pub(crate) fn save_file_as_with_checks(&mut self, full_path: std::path::PathBuf) {
631 let current_file_path = self
633 .active_state()
634 .buffer
635 .file_path()
636 .map(|p| p.to_path_buf());
637 let is_different_file = current_file_path.as_ref() != Some(&full_path);
638
639 if is_different_file && full_path.is_file() {
640 let filename = full_path
642 .file_name()
643 .map(|n| n.to_string_lossy().to_string())
644 .unwrap_or_else(|| full_path.display().to_string());
645 self.start_prompt(
646 t!("buffer.overwrite_confirm", name = &filename).to_string(),
647 PromptType::ConfirmOverwriteFile { path: full_path },
648 );
649 return;
650 }
651
652 if let Some(parent) = full_path.parent() {
654 if !parent.as_os_str().is_empty() && !self.authority.filesystem.exists(parent) {
655 let dir_name = parent
656 .strip_prefix(&self.working_dir)
657 .unwrap_or(parent)
658 .display()
659 .to_string();
660 self.start_prompt(
661 t!("buffer.create_directory_confirm", name = &dir_name).to_string(),
662 PromptType::ConfirmCreateDirectory { path: full_path },
663 );
664 return;
665 }
666 }
667
668 self.perform_save_file_as(full_path);
670 }
671
672 pub(crate) fn perform_save_file_as(&mut self, full_path: std::path::PathBuf) {
674 let before_idx = self.active_event_log().current_index();
675 let before_len = self.active_event_log().len();
676 tracing::debug!(
677 "SaveFileAs BEFORE: event_log index={}, len={}",
678 before_idx,
679 before_len
680 );
681
682 match self.active_state_mut().buffer.save_to_file(&full_path) {
683 Ok(()) => {
684 let after_save_idx = self.active_event_log().current_index();
685 let after_save_len = self.active_event_log().len();
686 tracing::debug!(
687 "SaveFileAs AFTER buffer.save_to_file: event_log index={}, len={}",
688 after_save_idx,
689 after_save_len
690 );
691
692 let metadata =
693 BufferMetadata::with_file(full_path.clone(), &full_path, &self.working_dir);
694 self.buffer_metadata.insert(self.active_buffer(), metadata);
695
696 let mut language_changed = false;
699 let mut new_language = String::new();
700 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
701 if state.language == "text" {
702 let first_line = state.buffer.first_line_lossy();
703 let detected =
704 crate::primitives::detected_language::DetectedLanguage::from_path(
705 &full_path,
706 first_line.as_deref(),
707 &self.grammar_registry,
708 &self.config.languages,
709 );
710 new_language = detected.name.clone();
711 state.apply_language(detected);
712 language_changed = new_language != "text";
713 }
714 }
715 if language_changed {
716 #[cfg(feature = "plugins")]
717 self.update_plugin_state_snapshot();
718 self.plugin_manager.run_hook(
719 "language_changed",
720 crate::services::plugins::hooks::HookArgs::LanguageChanged {
721 buffer_id: self.active_buffer(),
722 language: new_language,
723 },
724 );
725 }
726
727 self.active_event_log_mut().mark_saved();
728 tracing::debug!(
729 "SaveFileAs AFTER mark_saved: event_log index={}, len={}",
730 self.active_event_log().current_index(),
731 self.active_event_log().len()
732 );
733
734 if let Ok(metadata) = self.authority.filesystem.metadata(&full_path) {
735 if let Some(mtime) = metadata.modified {
736 self.file_mod_times.insert(full_path.clone(), mtime);
737 }
738 }
739
740 self.notify_lsp_save();
741
742 self.emit_event(
743 crate::model::control_event::events::FILE_SAVED.name,
744 serde_json::json!({"path": full_path.display().to_string()}),
745 );
746
747 self.plugin_manager.run_hook(
748 "after_file_save",
749 crate::services::plugins::hooks::HookArgs::AfterFileSave {
750 buffer_id: self.active_buffer(),
751 path: full_path.clone(),
752 },
753 );
754
755 if let Some(buffer_to_close) = self.pending_close_buffer.take() {
756 if let Err(e) = self.force_close_buffer(buffer_to_close) {
757 self.set_status_message(
758 t!("file.saved_cannot_close", error = e.to_string()).to_string(),
759 );
760 } else {
761 self.set_status_message(t!("buffer.saved_and_closed").to_string());
762 }
763 } else {
764 self.set_status_message(
765 t!("file.saved_as", path = full_path.display().to_string()).to_string(),
766 );
767 }
768 }
769 Err(e) => {
770 self.pending_close_buffer = None;
771 self.set_status_message(t!("file.error_saving", error = e.to_string()).to_string());
772 }
773 }
774 }
775
776 fn handle_set_page_width(&mut self, input: &str) {
778 let active_split = self.split_manager.active_split();
779 let trimmed = input.trim();
780
781 if trimmed.is_empty() {
782 if let Some(vs) = self.split_view_states.get_mut(&active_split) {
783 vs.compose_width = None;
784 }
785 self.set_status_message(t!("settings.page_width_cleared").to_string());
786 } else {
787 match trimmed.parse::<u16>() {
788 Ok(val) if val > 0 => {
789 if let Some(vs) = self.split_view_states.get_mut(&active_split) {
790 vs.compose_width = Some(val);
791 }
792 self.set_status_message(t!("settings.page_width_set", value = val).to_string());
793 }
794 _ => {
795 self.set_status_message(
796 t!("error.invalid_page_width", input = input).to_string(),
797 );
798 }
799 }
800 }
801 }
802
803 fn handle_add_ruler(&mut self, input: &str) {
805 let trimmed = input.trim();
806 match trimmed.parse::<usize>() {
807 Ok(col) if col > 0 => {
808 let active_split = self.split_manager.active_split();
809 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
810 if !view_state.rulers.contains(&col) {
811 view_state.rulers.push(col);
812 view_state.rulers.sort();
813 }
814 }
815 let new_rulers = self
817 .split_view_states
818 .get(&active_split)
819 .map(|vs| vs.rulers.clone())
820 .unwrap_or_default();
821 self.config_mut().editor.rulers = new_rulers;
822 self.save_rulers_to_config();
823 self.set_status_message(t!("rulers.added", column = col).to_string());
824 }
825 Ok(_) => {
826 self.set_status_message(t!("rulers.must_be_positive").to_string());
827 }
828 Err(_) => {
829 self.set_status_message(t!("rulers.invalid_column", input = input).to_string());
830 }
831 }
832 }
833
834 fn handle_remove_ruler(&mut self, input: &str) {
836 let trimmed = input.trim();
837 if let Ok(col) = trimmed.parse::<usize>() {
838 let active_split = self.split_manager.active_split();
839 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
840 view_state.rulers.retain(|&r| r != col);
841 }
842 let new_rulers = self
844 .split_view_states
845 .get(&active_split)
846 .map(|vs| vs.rulers.clone())
847 .unwrap_or_default();
848 self.config_mut().editor.rulers = new_rulers;
849 self.save_rulers_to_config();
850 self.set_status_message(t!("rulers.removed", column = col).to_string());
851 }
852 }
853
854 fn save_rulers_to_config(&mut self) {
856 if let Err(e) = self
857 .authority
858 .filesystem
859 .create_dir_all(&self.dir_context.config_dir)
860 {
861 tracing::warn!("Failed to create config directory: {}", e);
862 return;
863 }
864 let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
865 if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
866 tracing::warn!("Failed to save rulers to config: {}", e);
867 }
868 }
869
870 fn handle_set_tab_size(&mut self, input: &str) {
872 let buffer_id = self.active_buffer();
873 let trimmed = input.trim();
874
875 match trimmed.parse::<usize>() {
876 Ok(val) if val > 0 => {
877 if let Some(state) = self.buffers.get_mut(&buffer_id) {
878 state.buffer_settings.tab_size = val;
879 }
880 self.set_status_message(t!("settings.tab_size_set", value = val).to_string());
881 }
882 Ok(_) => {
883 self.set_status_message(t!("settings.tab_size_positive").to_string());
884 }
885 Err(_) => {
886 self.set_status_message(t!("error.invalid_tab_size", input = input).to_string());
887 }
888 }
889 }
890
891 fn handle_set_line_ending(&mut self, input: &str) {
893 use crate::model::buffer::LineEnding;
894
895 let trimmed = input.trim();
897 let code = trimmed.split_whitespace().next().unwrap_or(trimmed);
898
899 let line_ending = match code.to_uppercase().as_str() {
900 "LF" => Some(LineEnding::LF),
901 "CRLF" => Some(LineEnding::CRLF),
902 "CR" => Some(LineEnding::CR),
903 _ => None,
904 };
905
906 match line_ending {
907 Some(le) => {
908 self.active_state_mut().buffer.set_line_ending(le);
909 self.set_status_message(
910 t!("settings.line_ending_set", value = le.display_name()).to_string(),
911 );
912 }
913 None => {
914 self.set_status_message(t!("error.unknown_line_ending", input = input).to_string());
915 }
916 }
917 }
918
919 fn handle_set_encoding(&mut self, input: &str) {
921 use crate::model::buffer::Encoding;
922
923 let trimmed = input.trim();
924
925 let encoding = Encoding::all()
928 .iter()
929 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
930 .copied()
931 .or_else(|| {
932 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
934 Encoding::all()
935 .iter()
936 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
937 .copied()
938 });
939
940 match encoding {
941 Some(enc) => {
942 self.active_state_mut().buffer.set_encoding(enc);
943 self.set_status_message(format!("Encoding set to {}", enc.display_name()));
944 }
945 None => {
946 self.set_status_message(format!("Unknown encoding: {}", input));
947 }
948 }
949 }
950
951 fn handle_open_file_with_encoding(&mut self, path: &std::path::Path, input: &str) {
957 use crate::model::buffer::Encoding;
958 use crate::view::prompt::PromptType;
959
960 let trimmed = input.trim();
961
962 let encoding = Encoding::all()
964 .iter()
965 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
966 .copied()
967 .or_else(|| {
968 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
969 Encoding::all()
970 .iter()
971 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
972 .copied()
973 });
974
975 match encoding {
976 Some(enc) => {
977 let threshold = self.config.editor.large_file_threshold_bytes as usize;
980 let file_size = self
981 .authority
982 .filesystem
983 .metadata(path)
984 .map(|m| m.size as usize)
985 .unwrap_or(0);
986
987 if file_size >= threshold && enc.requires_full_file_load() {
988 let size_mb = file_size as f64 / (1024.0 * 1024.0);
990 let load_key = t!("file.large_encoding.key.load").to_string();
991 let encoding_key = t!("file.large_encoding.key.encoding").to_string();
992 let cancel_key = t!("file.large_encoding.key.cancel").to_string();
993 let prompt_msg = t!(
994 "file.large_encoding_prompt",
995 encoding = enc.display_name(),
996 size = format!("{:.0}", size_mb),
997 load_key = load_key,
998 encoding_key = encoding_key,
999 cancel_key = cancel_key
1000 )
1001 .to_string();
1002 self.start_prompt(
1003 prompt_msg,
1004 PromptType::ConfirmLargeFileEncoding {
1005 path: path.to_path_buf(),
1006 },
1007 );
1008 return;
1009 }
1010
1011 self.key_context = crate::input::keybindings::KeyContext::Normal;
1013
1014 if let Err(e) = self.open_file_with_encoding(path, enc) {
1016 self.set_status_message(
1017 t!("file.error_opening", error = e.to_string()).to_string(),
1018 );
1019 } else {
1020 self.set_status_message(format!(
1021 "Opened {} with {} encoding",
1022 path.display(),
1023 enc.display_name()
1024 ));
1025 }
1026 }
1027 None => {
1028 self.set_status_message(format!("Unknown encoding: {}", input));
1029 }
1030 }
1031 }
1032
1033 fn handle_reload_with_encoding(&mut self, input: &str) {
1036 use crate::model::buffer::Encoding;
1037
1038 let trimmed = input.trim();
1039
1040 let encoding = Encoding::all()
1042 .iter()
1043 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
1044 .copied()
1045 .or_else(|| {
1046 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
1047 Encoding::all()
1048 .iter()
1049 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
1050 .copied()
1051 });
1052
1053 match encoding {
1054 Some(enc) => {
1055 if let Err(e) = self.reload_with_encoding(enc) {
1057 self.set_status_message(format!("Failed to reload: {}", e));
1058 } else {
1059 self.set_status_message(format!(
1060 "Reloaded with {} encoding",
1061 enc.display_name()
1062 ));
1063 }
1064 }
1065 None => {
1066 self.set_status_message(format!("Unknown encoding: {}", input));
1067 }
1068 }
1069 }
1070
1071 fn handle_set_language(&mut self, input: &str) {
1073 use crate::primitives::detected_language::DetectedLanguage;
1074
1075 let trimmed = input.trim();
1076
1077 if trimmed == "Plain Text" || trimmed.to_lowercase() == "text" {
1079 let buffer_id = self.active_buffer();
1080 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1081 state.apply_language(DetectedLanguage::plain_text());
1082 self.set_status_message("Language set to Plain Text".to_string());
1083 }
1084 #[cfg(feature = "plugins")]
1085 self.update_plugin_state_snapshot();
1086 self.plugin_manager.run_hook(
1087 "language_changed",
1088 crate::services::plugins::hooks::HookArgs::LanguageChanged {
1089 buffer_id: self.active_buffer(),
1090 language: "text".to_string(),
1091 },
1092 );
1093 return;
1094 }
1095
1096 if let Some(detected) = DetectedLanguage::from_syntax_name(
1098 trimmed,
1099 &self.grammar_registry,
1100 &self.config.languages,
1101 ) {
1102 let language = detected.name.clone();
1103 let buffer_id = self.active_buffer();
1104 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1105 state.apply_language(detected);
1106 self.set_status_message(format!("Language set to {}", trimmed));
1107 }
1108 #[cfg(feature = "plugins")]
1109 self.update_plugin_state_snapshot();
1110 self.plugin_manager.run_hook(
1111 "language_changed",
1112 crate::services::plugins::hooks::HookArgs::LanguageChanged {
1113 buffer_id,
1114 language,
1115 },
1116 );
1117 } else {
1118 self.set_status_message(format!("Unknown language: {}", input));
1122 }
1123 }
1124
1125 fn handle_register_input<F>(&mut self, input: &str, action: F, register_type: &str)
1127 where
1128 F: FnOnce(&mut Self, char),
1129 {
1130 if let Some(c) = input.trim().chars().next() {
1131 if c.is_ascii_digit() {
1132 action(self, c);
1133 } else {
1134 self.set_status_message(
1135 t!("register.must_be_digit", "type" = register_type).to_string(),
1136 );
1137 }
1138 } else {
1139 self.set_status_message(t!("register.not_specified").to_string());
1140 }
1141 }
1142
1143 fn handle_confirm_close_buffer(&mut self, input: &str, buffer_id: BufferId) -> bool {
1145 let input_lower = input.trim().to_lowercase();
1146 let save_key = t!("prompt.key.save").to_string().to_lowercase();
1147 let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1148
1149 let first_char = input_lower.chars().next();
1150 let save_first = save_key.chars().next();
1151 let discard_first = discard_key.chars().next();
1152
1153 if first_char == save_first {
1154 let has_path = self
1156 .buffers
1157 .get(&buffer_id)
1158 .map(|s| s.buffer.file_path().is_some())
1159 .unwrap_or(false);
1160
1161 if has_path {
1162 let old_active = self.active_buffer();
1163 self.set_active_buffer(buffer_id);
1164 if let Err(e) = self.save() {
1165 self.set_status_message(
1166 t!("file.save_failed", error = e.to_string()).to_string(),
1167 );
1168 self.set_active_buffer(old_active);
1169 return true; }
1171 self.set_active_buffer(old_active);
1172 if let Err(e) = self.force_close_buffer(buffer_id) {
1173 self.set_status_message(
1174 t!("file.cannot_close", error = e.to_string()).to_string(),
1175 );
1176 } else {
1177 self.set_status_message(t!("buffer.saved_and_closed").to_string());
1178 }
1179 } else {
1180 self.pending_close_buffer = Some(buffer_id);
1181 self.start_prompt_with_initial_text(
1182 t!("file.save_as_prompt").to_string(),
1183 PromptType::SaveFileAs,
1184 String::new(),
1185 );
1186 }
1187 } else if first_char == discard_first {
1188 if let Err(e) = self.force_close_buffer(buffer_id) {
1190 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
1191 } else {
1192 self.set_status_message(t!("buffer.changes_discarded").to_string());
1193 }
1194 } else {
1195 self.set_status_message(t!("buffer.close_cancelled").to_string());
1196 }
1197 false
1198 }
1199
1200 fn handle_confirm_quit_modified(&mut self, input: &str) -> bool {
1202 let input_lower = input.trim().to_lowercase();
1203 let save_key = t!("prompt.key.save").to_string().to_lowercase();
1204 let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1205 let quit_key = t!("prompt.key.quit").to_string().to_lowercase();
1206
1207 let first_char = input_lower.chars().next();
1208 let save_first = save_key.chars().next();
1209 let discard_first = discard_key.chars().next();
1210 let quit_first = quit_key.chars().next();
1211
1212 if first_char == save_first {
1213 match self.save_all_on_exit() {
1215 Ok(count) => {
1216 tracing::info!("Saved {} buffer(s) on exit", count);
1217 self.should_quit = true;
1218 }
1219 Err(e) => {
1220 self.set_status_message(
1221 t!("file.save_failed", error = e.to_string()).to_string(),
1222 );
1223 return true; }
1225 }
1226 } else if first_char == discard_first {
1227 self.should_quit = true;
1229 } else if first_char == quit_first && self.config.editor.hot_exit {
1230 self.should_quit = true;
1232 } else {
1233 self.set_status_message(t!("buffer.close_cancelled").to_string());
1235 }
1236 false
1237 }
1238
1239 pub fn handle_stop_lsp_server(&mut self, input: &str) {
1244 let input = input.trim();
1245 if input.is_empty() {
1246 return;
1247 }
1248
1249 let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1251 (lang, Some(name))
1252 } else {
1253 (input, None)
1254 };
1255
1256 let has_server = self
1257 .lsp
1258 .as_ref()
1259 .is_some_and(|lsp| lsp.has_handles(language));
1260
1261 if !has_server {
1262 self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1263 return;
1264 }
1265
1266 let stopping_all = server_name.is_none()
1270 || self
1271 .lsp
1272 .as_ref()
1273 .map(|lsp| lsp.handle_count(language) <= 1)
1274 .unwrap_or(true);
1275
1276 if stopping_all {
1277 let buffer_ids: Vec<_> = self
1284 .buffers
1285 .iter()
1286 .filter(|(_, s)| s.language == language)
1287 .map(|(id, _)| *id)
1288 .collect();
1289 for buffer_id in buffer_ids {
1290 self.disable_lsp_for_buffer(buffer_id);
1291 }
1292 } else if let Some(name) = server_name {
1293 self.send_did_close_to_server(language, name);
1297 }
1298
1299 let stopped = self.stop_lsp_server_and_cleanup(language, server_name);
1303
1304 if !stopped {
1305 self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1306 return;
1307 }
1308
1309 if let Some(lsp_configs) = self.config_mut().lsp.get_mut(language) {
1311 for c in lsp_configs.as_mut_slice() {
1312 if let Some(name) = server_name {
1313 if c.display_name() == name {
1315 c.auto_start = false;
1316 }
1317 } else {
1318 c.auto_start = false;
1319 }
1320 }
1321 if let Err(e) = self.save_config() {
1322 tracing::warn!(
1323 "Failed to save config after disabling LSP auto-start: {}",
1324 e
1325 );
1326 } else {
1327 let config_path = self.dir_context.config_path();
1328 self.emit_event(
1329 "config_changed",
1330 serde_json::json!({
1331 "path": config_path.to_string_lossy(),
1332 }),
1333 );
1334 }
1335 }
1336
1337 let display = server_name.unwrap_or(language);
1338 self.set_status_message(t!("lsp.server_stopped", language = display).to_string());
1339 }
1340
1341 pub fn handle_restart_lsp_server(&mut self, input: &str) {
1346 let input = input.trim();
1347 if input.is_empty() {
1348 return;
1349 }
1350
1351 let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1353 (lang, Some(name))
1354 } else {
1355 (input, None)
1356 };
1357
1358 let buffer_id = self.active_buffer();
1360 let file_path = self
1361 .buffer_metadata
1362 .get(&buffer_id)
1363 .and_then(|meta| meta.file_path().cloned());
1364
1365 let (success, message) = if let Some(name) = server_name {
1366 if let Some(lsp) = self.lsp.as_mut() {
1368 lsp.manual_restart_server(language, name, file_path.as_deref())
1369 } else {
1370 (false, t!("lsp.no_manager").to_string())
1371 }
1372 } else {
1373 if let Some(lsp) = self.lsp.as_mut() {
1375 lsp.manual_restart(language, file_path.as_deref())
1376 } else {
1377 (false, t!("lsp.no_manager").to_string())
1378 }
1379 };
1380
1381 self.status_message = Some(message);
1382
1383 if success {
1384 self.reopen_buffers_for_language(language);
1385 }
1386 }
1387
1388 fn handle_quick_open_confirm(
1390 &mut self,
1391 input: &str,
1392 selected_index: Option<usize>,
1393 ) -> PromptResult {
1394 use crate::input::quick_open::QuickOpenResult;
1395
1396 let context = self.build_quick_open_context();
1397 let result = if let Some((provider, query)) =
1398 self.quick_open_registry.get_provider_for_input(input)
1399 {
1400 let suggestions = provider.suggestions(query, &context);
1402 let selected = selected_index.and_then(|i| suggestions.get(i));
1403 provider.on_select(selected, query, &context)
1404 } else {
1405 QuickOpenResult::None
1406 };
1407
1408 self.execute_quick_open_result(result)
1409 }
1410
1411 fn execute_quick_open_result(
1413 &mut self,
1414 result: crate::input::quick_open::QuickOpenResult,
1415 ) -> PromptResult {
1416 use crate::input::quick_open::QuickOpenResult;
1417
1418 match &result {
1422 QuickOpenResult::GotoLine(_) => {
1423 self.goto_line_preview = None;
1426 }
1427 _ => {
1428 self.restore_goto_line_preview_snapshot();
1429 }
1430 }
1431
1432 match result {
1433 QuickOpenResult::ExecuteAction(action) => PromptResult::ExecuteAction(action),
1434 QuickOpenResult::OpenFile { path, line, column } => {
1435 let expanded_path = expand_tilde(&path);
1436 let full_path = if expanded_path.is_absolute() {
1437 expanded_path
1438 } else {
1439 self.working_dir.join(&expanded_path)
1440 };
1441 self.open_file_with_jump(full_path, line, column);
1442 PromptResult::Done
1443 }
1444 QuickOpenResult::ShowBuffer(buffer_id) => {
1445 let buffer_id = crate::model::event::BufferId(buffer_id);
1446 if self.buffers.contains_key(&buffer_id) {
1447 self.set_active_buffer(buffer_id);
1448 if let Some(name) = self.active_state().buffer.file_path() {
1449 self.set_status_message(
1450 t!("buffer.switched", name = name.display().to_string()).to_string(),
1451 );
1452 }
1453 }
1454 PromptResult::Done
1455 }
1456 QuickOpenResult::GotoLine(line) => {
1457 self.goto_line_col(line, None);
1458 self.set_status_message(t!("goto.jumped", line = line).to_string());
1459 PromptResult::Done
1460 }
1461 QuickOpenResult::None => {
1462 self.set_status_message(t!("status.no_selection").to_string());
1463 PromptResult::Done
1464 }
1465 QuickOpenResult::Error(msg) => {
1466 self.set_status_message(msg);
1467 PromptResult::Done
1468 }
1469 }
1470 }
1471
1472 fn open_file_with_jump(
1473 &mut self,
1474 full_path: std::path::PathBuf,
1475 line: Option<usize>,
1476 column: Option<usize>,
1477 ) {
1478 match self.open_file(&full_path) {
1479 Ok(_) => {
1480 if let Some(line) = line {
1481 self.goto_line_col(line, column);
1482 }
1483 self.set_status_message(
1484 t!("buffer.opened", name = full_path.display().to_string()).to_string(),
1485 );
1486 }
1487 Err(e) => {
1488 if let Some(confirmation) =
1490 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
1491 {
1492 self.start_large_file_encoding_confirmation(confirmation);
1493 } else {
1494 self.set_status_message(
1495 t!("file.error_opening", error = e.to_string()).to_string(),
1496 );
1497 }
1498 }
1499 }
1500 }
1501
1502 fn prompt_next_paste_conflict(
1504 &mut self,
1505 safe: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1506 confirmed: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1507 pending: Vec<(std::path::PathBuf, std::path::PathBuf)>,
1508 is_cut: bool,
1509 ) {
1510 let name = crate::app::file_explorer::truncate_name_for_prompt(
1511 &pending[0]
1512 .1
1513 .file_name()
1514 .unwrap_or_default()
1515 .to_string_lossy(),
1516 40,
1517 );
1518 self.start_prompt(
1519 t!("explorer.paste_conflict_multi", name = &name).to_string(),
1520 PromptType::ConfirmMultiPasteConflict {
1521 safe,
1522 confirmed,
1523 pending,
1524 is_cut,
1525 },
1526 );
1527 }
1528}
1529
1530#[cfg(test)]
1535mod tests {
1536 use super::parse_path_line_col;
1537
1538 #[test]
1539 fn test_parse_path_line_col_empty() {
1540 let (path, line, col) = parse_path_line_col("");
1541 assert_eq!(path, "");
1542 assert_eq!(line, None);
1543 assert_eq!(col, None);
1544 }
1545
1546 #[test]
1547 fn test_parse_path_line_col_plain_path() {
1548 let (path, line, col) = parse_path_line_col("src/main.rs");
1549 assert_eq!(path, "src/main.rs");
1550 assert_eq!(line, None);
1551 assert_eq!(col, None);
1552 }
1553
1554 #[test]
1555 fn test_parse_path_line_col_line_only() {
1556 let (path, line, col) = parse_path_line_col("src/main.rs:42");
1557 assert_eq!(path, "src/main.rs");
1558 assert_eq!(line, Some(42));
1559 assert_eq!(col, None);
1560 }
1561
1562 #[test]
1563 fn test_parse_path_line_col_line_and_col() {
1564 let (path, line, col) = parse_path_line_col("src/main.rs:42:10");
1565 assert_eq!(path, "src/main.rs");
1566 assert_eq!(line, Some(42));
1567 assert_eq!(col, Some(10));
1568 }
1569
1570 #[test]
1571 fn test_parse_path_line_col_trimmed() {
1572 let (path, line, col) = parse_path_line_col(" src/main.rs:5:2 ");
1573 assert_eq!(path, "src/main.rs");
1574 assert_eq!(line, Some(5));
1575 assert_eq!(col, Some(2));
1576 }
1577
1578 #[test]
1579 fn test_parse_path_line_col_zero_line_rejected() {
1580 let (path, line, col) = parse_path_line_col("src/main.rs:0");
1581 assert_eq!(path, "src/main.rs:0");
1582 assert_eq!(line, None);
1583 assert_eq!(col, None);
1584 }
1585
1586 #[test]
1587 fn test_parse_path_line_col_zero_col_rejected() {
1588 let (path, line, col) = parse_path_line_col("src/main.rs:1:0");
1589 assert_eq!(path, "src/main.rs:1:0");
1590 assert_eq!(line, None);
1591 assert_eq!(col, None);
1592 }
1593
1594 #[cfg(windows)]
1595 #[test]
1596 fn test_parse_path_line_col_windows_drive() {
1597 let (path, line, col) = parse_path_line_col(r"C:\src\main.rs:12:3");
1598 assert_eq!(path, r"C:\src\main.rs");
1599 assert_eq!(line, Some(12));
1600 assert_eq!(col, Some(3));
1601 }
1602}