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_col(line_num, None);
122 self.set_status_message(t!("goto.jumped", line = line_num).to_string());
123 }
124 Ok(_) => {
125 self.set_status_message(t!("goto.line_must_be_positive").to_string());
126 }
127 Err(_) => {
128 self.set_status_message(t!("error.invalid_line", input = &input).to_string());
129 }
130 },
131 PromptType::GotoByteOffset => {
132 let trimmed = input.trim();
134 let num_str = trimmed
135 .strip_suffix('B')
136 .or_else(|| trimmed.strip_suffix('b'))
137 .unwrap_or(trimmed);
138 match num_str.parse::<usize>() {
139 Ok(offset) => {
140 self.goto_byte_offset(offset);
141 self.set_status_message(
142 t!("goto.jumped_byte", offset = offset).to_string(),
143 );
144 }
145 Err(_) => {
146 self.set_status_message(
147 t!("goto.invalid_byte_offset", input = &input).to_string(),
148 );
149 }
150 }
151 }
152 PromptType::GotoLineScanConfirm => {
153 let answer = input.trim().to_lowercase();
154 if answer == "y" || answer == "yes" {
155 self.start_incremental_line_scan(true);
157 } else {
160 self.start_prompt(
162 t!("goto.byte_offset_prompt").to_string(),
163 PromptType::GotoByteOffset,
164 );
165 }
166 }
167 PromptType::QuickOpen => {
168 return self.handle_quick_open_confirm(&input, selected_index);
170 }
171 PromptType::SetBackgroundFile => {
172 if let Err(e) = self.load_ansi_background(&input) {
173 self.set_status_message(
174 t!("error.background_load_failed", error = e.to_string()).to_string(),
175 );
176 }
177 }
178 PromptType::SetBackgroundBlend => match input.trim().parse::<f32>() {
179 Ok(val) => {
180 let clamped = val.clamp(0.0, 1.0);
181 self.background_fade = clamped;
182 self.set_status_message(
183 t!(
184 "error.background_blend_set",
185 value = format!("{:.2}", clamped)
186 )
187 .to_string(),
188 );
189 }
190 Err(_) => {
191 self.set_status_message(t!("error.invalid_blend", input = &input).to_string());
192 }
193 },
194 PromptType::SetPageWidth => {
195 self.handle_set_page_width(&input);
196 }
197 PromptType::RecordMacro => {
198 self.handle_register_input(
199 &input,
200 |editor, c| editor.toggle_macro_recording(c),
201 "Macro",
202 );
203 }
204 PromptType::PlayMacro => {
205 self.handle_register_input(&input, |editor, c| editor.play_macro(c), "Macro");
206 }
207 PromptType::SetBookmark => {
208 self.handle_register_input(&input, |editor, c| editor.set_bookmark(c), "Bookmark");
209 }
210 PromptType::JumpToBookmark => {
211 self.handle_register_input(
212 &input,
213 |editor, c| editor.jump_to_bookmark(c),
214 "Bookmark",
215 );
216 }
217 PromptType::Plugin { custom_type } => {
218 tracing::info!(
219 "prompt_confirmed: dispatching hook for prompt_type='{}', input='{}', selected_index={:?}",
220 custom_type, input, selected_index
221 );
222 self.plugin_manager.run_hook(
223 "prompt_confirmed",
224 HookArgs::PromptConfirmed {
225 prompt_type: custom_type.clone(),
226 input,
227 selected_index,
228 },
229 );
230 tracing::info!(
231 "prompt_confirmed: hook dispatched for prompt_type='{}'",
232 custom_type
233 );
234 }
235 PromptType::ConfirmRevert => {
236 let input_lower = input.trim().to_lowercase();
237 let revert_key = t!("prompt.key.revert").to_string().to_lowercase();
238 if input_lower == revert_key || input_lower == "revert" {
239 if let Err(e) = self.revert_file() {
240 self.set_status_message(
241 t!("file.revert_failed", error = e.to_string()).to_string(),
242 );
243 }
244 } else {
245 self.set_status_message(t!("buffer.revert_cancelled").to_string());
246 }
247 }
248 PromptType::ConfirmSaveConflict => {
249 let input_lower = input.trim().to_lowercase();
250 if input_lower == "o" || input_lower == "overwrite" {
251 if let Err(e) = self.save() {
252 self.set_status_message(
253 t!("file.save_failed", error = e.to_string()).to_string(),
254 );
255 }
256 } else {
257 self.set_status_message(t!("buffer.save_cancelled").to_string());
258 }
259 }
260 PromptType::ConfirmSudoSave { info } => {
261 let input_lower = input.trim().to_lowercase();
262 if input_lower == "y" || input_lower == "yes" {
263 self.cancel_prompt();
265
266 let result = (|| -> anyhow::Result<()> {
268 let data = self.filesystem.read_file(&info.temp_path)?;
269 self.filesystem.sudo_write(
270 &info.dest_path,
271 &data,
272 info.mode,
273 info.uid,
274 info.gid,
275 )?;
276 #[allow(clippy::let_underscore_must_use)]
278 let _ = self.filesystem.remove_file(&info.temp_path);
279 Ok(())
280 })();
281
282 match result {
283 Ok(_) => {
284 if let Err(e) = self
285 .active_state_mut()
286 .buffer
287 .finalize_external_save(info.dest_path.clone())
288 {
289 tracing::warn!("Failed to finalize sudo save: {}", e);
290 self.set_status_message(
291 t!("prompt.sudo_save_failed", error = e.to_string())
292 .to_string(),
293 );
294 } else if let Err(e) = self.finalize_save(Some(info.dest_path)) {
295 tracing::warn!("Failed to finalize save after sudo: {}", e);
296 self.set_status_message(
297 t!("prompt.sudo_save_failed", error = e.to_string())
298 .to_string(),
299 );
300 }
301 }
302 Err(e) => {
303 tracing::warn!("Sudo save failed: {}", e);
304 self.set_status_message(
305 t!("prompt.sudo_save_failed", error = e.to_string()).to_string(),
306 );
307 #[allow(clippy::let_underscore_must_use)]
309 let _ = self.filesystem.remove_file(&info.temp_path);
310 }
311 }
312 } else {
313 self.set_status_message(t!("buffer.save_cancelled").to_string());
314 #[allow(clippy::let_underscore_must_use)]
316 let _ = self.filesystem.remove_file(&info.temp_path);
317 }
318 }
319 PromptType::ConfirmOverwriteFile { path } => {
320 let input_lower = input.trim().to_lowercase();
321 if input_lower == "o" || input_lower == "overwrite" {
322 self.perform_save_file_as(path);
323 } else {
324 self.set_status_message(t!("buffer.save_cancelled").to_string());
325 }
326 }
327 PromptType::ConfirmCreateDirectory { path } => {
328 let input_lower = input.trim().to_lowercase();
329 if input_lower == "c" || input_lower == "create" {
330 if let Some(parent) = path.parent() {
331 if let Err(e) = self.filesystem.create_dir_all(parent) {
332 self.set_status_message(
333 t!("file.error_saving", error = e.to_string()).to_string(),
334 );
335 return PromptResult::Done;
336 }
337 }
338 self.perform_save_file_as(path);
339 } else {
340 self.set_status_message(t!("buffer.save_cancelled").to_string());
341 }
342 }
343 PromptType::ConfirmCloseBuffer { buffer_id } => {
344 if self.handle_confirm_close_buffer(&input, buffer_id) {
345 return PromptResult::EarlyReturn;
346 }
347 }
348 PromptType::ConfirmQuitWithModified => {
349 if self.handle_confirm_quit_modified(&input) {
350 return PromptResult::EarlyReturn;
351 }
352 }
353 PromptType::LspRename {
354 original_text,
355 start_pos,
356 end_pos: _,
357 overlay_handle,
358 } => {
359 self.perform_lsp_rename(input, original_text, start_pos, overlay_handle);
360 }
361 PromptType::FileExplorerRename {
362 original_path,
363 original_name,
364 is_new_file,
365 } => {
366 self.perform_file_explorer_rename(original_path, original_name, input, is_new_file);
367 }
368 PromptType::ConfirmDeleteFile { path, is_dir } => {
369 let input_lower = input.trim().to_lowercase();
370 if input_lower == "y" || input_lower == "yes" {
371 self.perform_file_explorer_delete(path, is_dir);
372 } else {
373 self.set_status_message(t!("explorer.delete_cancelled").to_string());
374 }
375 }
376 PromptType::ConfirmLargeFileEncoding { path } => {
377 let input_lower = input.trim().to_lowercase();
378 let load_key = t!("file.large_encoding.key.load")
379 .to_string()
380 .to_lowercase();
381 let encoding_key = t!("file.large_encoding.key.encoding")
382 .to_string()
383 .to_lowercase();
384 let cancel_key = t!("file.large_encoding.key.cancel")
385 .to_string()
386 .to_lowercase();
387 if input_lower.is_empty() || input_lower == load_key {
389 if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
390 self.set_status_message(
391 t!("file.error_opening", error = e.to_string()).to_string(),
392 );
393 }
394 } else if input_lower == encoding_key {
395 self.start_open_file_with_encoding_prompt(path);
397 } else if input_lower == cancel_key {
398 self.set_status_message(t!("file.open_cancelled").to_string());
399 } else {
400 if let Err(e) = self.open_file_large_encoding_confirmed(&path) {
402 self.set_status_message(
403 t!("file.error_opening", error = e.to_string()).to_string(),
404 );
405 }
406 }
407 }
408 PromptType::StopLspServer => {
409 self.handle_stop_lsp_server(&input);
410 }
411 PromptType::RestartLspServer => {
412 self.handle_restart_lsp_server(&input);
413 }
414 PromptType::SelectTheme { .. } => {
415 self.apply_theme(input.trim());
416 }
417 PromptType::SelectKeybindingMap => {
418 self.apply_keybinding_map(input.trim());
419 }
420 PromptType::SelectCursorStyle => {
421 self.apply_cursor_style(input.trim());
422 }
423 PromptType::SelectLocale => {
424 self.apply_locale(input.trim());
425 }
426 PromptType::CopyWithFormattingTheme => {
427 self.copy_selection_with_theme(input.trim());
428 }
429 PromptType::SwitchToTab => {
430 if let Ok(id) = input.trim().parse::<usize>() {
431 self.switch_to_tab(BufferId(id));
432 }
433 }
434 PromptType::QueryReplaceConfirm => {
435 if let Some(c) = input.chars().next() {
438 if let Err(e) = self.handle_interactive_replace_key(c) {
439 tracing::warn!("Interactive replace failed: {}", e);
440 }
441 }
442 }
443 PromptType::AddRuler => {
444 self.handle_add_ruler(&input);
445 }
446 PromptType::RemoveRuler => {
447 self.handle_remove_ruler(&input);
448 }
449 PromptType::SetTabSize => {
450 self.handle_set_tab_size(&input);
451 }
452 PromptType::SetLineEnding => {
453 self.handle_set_line_ending(&input);
454 }
455 PromptType::SetEncoding => {
456 self.handle_set_encoding(&input);
457 }
458 PromptType::SetLanguage => {
459 self.handle_set_language(&input);
460 }
461 PromptType::ShellCommand { replace } => {
462 self.handle_shell_command(&input, replace);
463 }
464 PromptType::AsyncPrompt => {
465 if let Some(callback_id) = self.pending_async_prompt_callback.take() {
467 let json = serde_json::to_string(&input).unwrap_or_else(|_| "null".to_string());
469 self.plugin_manager.resolve_callback(callback_id, json);
470 }
471 }
472 }
473 PromptResult::Done
474 }
475
476 fn handle_save_file_as(&mut self, input: &str) {
478 let expanded_path = expand_tilde(input);
480 let full_path = if expanded_path.is_absolute() {
481 normalize_path(&expanded_path)
482 } else {
483 normalize_path(&self.working_dir.join(&expanded_path))
484 };
485
486 self.save_file_as_with_checks(full_path);
487 }
488
489 pub(crate) fn save_file_as_with_checks(&mut self, full_path: std::path::PathBuf) {
491 let current_file_path = self
493 .active_state()
494 .buffer
495 .file_path()
496 .map(|p| p.to_path_buf());
497 let is_different_file = current_file_path.as_ref() != Some(&full_path);
498
499 if is_different_file && full_path.is_file() {
500 let filename = full_path
502 .file_name()
503 .map(|n| n.to_string_lossy().to_string())
504 .unwrap_or_else(|| full_path.display().to_string());
505 self.start_prompt(
506 t!("buffer.overwrite_confirm", name = &filename).to_string(),
507 PromptType::ConfirmOverwriteFile { path: full_path },
508 );
509 return;
510 }
511
512 if let Some(parent) = full_path.parent() {
514 if !parent.as_os_str().is_empty() && !self.filesystem.exists(parent) {
515 let dir_name = parent
516 .strip_prefix(&self.working_dir)
517 .unwrap_or(parent)
518 .display()
519 .to_string();
520 self.start_prompt(
521 t!("buffer.create_directory_confirm", name = &dir_name).to_string(),
522 PromptType::ConfirmCreateDirectory { path: full_path },
523 );
524 return;
525 }
526 }
527
528 self.perform_save_file_as(full_path);
530 }
531
532 pub(crate) fn perform_save_file_as(&mut self, full_path: std::path::PathBuf) {
534 let before_idx = self.active_event_log().current_index();
535 let before_len = self.active_event_log().len();
536 tracing::debug!(
537 "SaveFileAs BEFORE: event_log index={}, len={}",
538 before_idx,
539 before_len
540 );
541
542 match self.active_state_mut().buffer.save_to_file(&full_path) {
543 Ok(()) => {
544 let after_save_idx = self.active_event_log().current_index();
545 let after_save_len = self.active_event_log().len();
546 tracing::debug!(
547 "SaveFileAs AFTER buffer.save_to_file: event_log index={}, len={}",
548 after_save_idx,
549 after_save_len
550 );
551
552 let metadata =
553 BufferMetadata::with_file(full_path.clone(), &full_path, &self.working_dir);
554 self.buffer_metadata.insert(self.active_buffer(), metadata);
555
556 let mut language_changed = false;
559 let mut new_language = String::new();
560 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
561 if state.language == "text" {
562 let detected =
563 crate::primitives::detected_language::DetectedLanguage::from_path(
564 &full_path,
565 &self.grammar_registry,
566 &self.config.languages,
567 );
568 new_language = detected.name.clone();
569 state.apply_language(detected);
570 language_changed = new_language != "text";
571 }
572 }
573 if language_changed {
574 #[cfg(feature = "plugins")]
575 self.update_plugin_state_snapshot();
576 self.plugin_manager.run_hook(
577 "language_changed",
578 crate::services::plugins::hooks::HookArgs::LanguageChanged {
579 buffer_id: self.active_buffer(),
580 language: new_language,
581 },
582 );
583 }
584
585 self.active_event_log_mut().mark_saved();
586 tracing::debug!(
587 "SaveFileAs AFTER mark_saved: event_log index={}, len={}",
588 self.active_event_log().current_index(),
589 self.active_event_log().len()
590 );
591
592 if let Ok(metadata) = self.filesystem.metadata(&full_path) {
593 if let Some(mtime) = metadata.modified {
594 self.file_mod_times.insert(full_path.clone(), mtime);
595 }
596 }
597
598 self.notify_lsp_save();
599
600 self.emit_event(
601 crate::model::control_event::events::FILE_SAVED.name,
602 serde_json::json!({"path": full_path.display().to_string()}),
603 );
604
605 self.plugin_manager.run_hook(
606 "after_file_save",
607 crate::services::plugins::hooks::HookArgs::AfterFileSave {
608 buffer_id: self.active_buffer(),
609 path: full_path.clone(),
610 },
611 );
612
613 if let Some(buffer_to_close) = self.pending_close_buffer.take() {
614 if let Err(e) = self.force_close_buffer(buffer_to_close) {
615 self.set_status_message(
616 t!("file.saved_cannot_close", error = e.to_string()).to_string(),
617 );
618 } else {
619 self.set_status_message(t!("buffer.saved_and_closed").to_string());
620 }
621 } else {
622 self.set_status_message(
623 t!("file.saved_as", path = full_path.display().to_string()).to_string(),
624 );
625 }
626 }
627 Err(e) => {
628 self.pending_close_buffer = None;
629 self.set_status_message(t!("file.error_saving", error = e.to_string()).to_string());
630 }
631 }
632 }
633
634 fn handle_set_page_width(&mut self, input: &str) {
636 let active_split = self.split_manager.active_split();
637 let trimmed = input.trim();
638
639 if trimmed.is_empty() {
640 if let Some(vs) = self.split_view_states.get_mut(&active_split) {
641 vs.compose_width = None;
642 }
643 self.set_status_message(t!("settings.page_width_cleared").to_string());
644 } else {
645 match trimmed.parse::<u16>() {
646 Ok(val) if val > 0 => {
647 if let Some(vs) = self.split_view_states.get_mut(&active_split) {
648 vs.compose_width = Some(val);
649 }
650 self.set_status_message(t!("settings.page_width_set", value = val).to_string());
651 }
652 _ => {
653 self.set_status_message(
654 t!("error.invalid_page_width", input = input).to_string(),
655 );
656 }
657 }
658 }
659 }
660
661 fn handle_add_ruler(&mut self, input: &str) {
663 let trimmed = input.trim();
664 match trimmed.parse::<usize>() {
665 Ok(col) if col > 0 => {
666 let active_split = self.split_manager.active_split();
667 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
668 if !view_state.rulers.contains(&col) {
669 view_state.rulers.push(col);
670 view_state.rulers.sort();
671 }
672 }
673 self.config.editor.rulers = self
675 .split_view_states
676 .get(&active_split)
677 .map(|vs| vs.rulers.clone())
678 .unwrap_or_default();
679 self.save_rulers_to_config();
680 self.set_status_message(t!("rulers.added", column = col).to_string());
681 }
682 Ok(_) => {
683 self.set_status_message(t!("rulers.must_be_positive").to_string());
684 }
685 Err(_) => {
686 self.set_status_message(t!("rulers.invalid_column", input = input).to_string());
687 }
688 }
689 }
690
691 fn handle_remove_ruler(&mut self, input: &str) {
693 let trimmed = input.trim();
694 if let Ok(col) = trimmed.parse::<usize>() {
695 let active_split = self.split_manager.active_split();
696 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
697 view_state.rulers.retain(|&r| r != col);
698 }
699 self.config.editor.rulers = self
701 .split_view_states
702 .get(&active_split)
703 .map(|vs| vs.rulers.clone())
704 .unwrap_or_default();
705 self.save_rulers_to_config();
706 self.set_status_message(t!("rulers.removed", column = col).to_string());
707 }
708 }
709
710 fn save_rulers_to_config(&mut self) {
712 if let Err(e) = self.filesystem.create_dir_all(&self.dir_context.config_dir) {
713 tracing::warn!("Failed to create config directory: {}", e);
714 return;
715 }
716 let resolver = ConfigResolver::new(self.dir_context.clone(), self.working_dir.clone());
717 if let Err(e) = resolver.save_to_layer(&self.config, ConfigLayer::User) {
718 tracing::warn!("Failed to save rulers to config: {}", e);
719 }
720 }
721
722 fn handle_set_tab_size(&mut self, input: &str) {
724 let buffer_id = self.active_buffer();
725 let trimmed = input.trim();
726
727 match trimmed.parse::<usize>() {
728 Ok(val) if val > 0 => {
729 if let Some(state) = self.buffers.get_mut(&buffer_id) {
730 state.buffer_settings.tab_size = val;
731 }
732 self.set_status_message(t!("settings.tab_size_set", value = val).to_string());
733 }
734 Ok(_) => {
735 self.set_status_message(t!("settings.tab_size_positive").to_string());
736 }
737 Err(_) => {
738 self.set_status_message(t!("error.invalid_tab_size", input = input).to_string());
739 }
740 }
741 }
742
743 fn handle_set_line_ending(&mut self, input: &str) {
745 use crate::model::buffer::LineEnding;
746
747 let trimmed = input.trim();
749 let code = trimmed.split_whitespace().next().unwrap_or(trimmed);
750
751 let line_ending = match code.to_uppercase().as_str() {
752 "LF" => Some(LineEnding::LF),
753 "CRLF" => Some(LineEnding::CRLF),
754 "CR" => Some(LineEnding::CR),
755 _ => None,
756 };
757
758 match line_ending {
759 Some(le) => {
760 self.active_state_mut().buffer.set_line_ending(le);
761 self.set_status_message(
762 t!("settings.line_ending_set", value = le.display_name()).to_string(),
763 );
764 }
765 None => {
766 self.set_status_message(t!("error.unknown_line_ending", input = input).to_string());
767 }
768 }
769 }
770
771 fn handle_set_encoding(&mut self, input: &str) {
773 use crate::model::buffer::Encoding;
774
775 let trimmed = input.trim();
776
777 let encoding = Encoding::all()
780 .iter()
781 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
782 .copied()
783 .or_else(|| {
784 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
786 Encoding::all()
787 .iter()
788 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
789 .copied()
790 });
791
792 match encoding {
793 Some(enc) => {
794 self.active_state_mut().buffer.set_encoding(enc);
795 self.set_status_message(format!("Encoding set to {}", enc.display_name()));
796 }
797 None => {
798 self.set_status_message(format!("Unknown encoding: {}", input));
799 }
800 }
801 }
802
803 fn handle_open_file_with_encoding(&mut self, path: &std::path::Path, input: &str) {
809 use crate::model::buffer::Encoding;
810 use crate::view::prompt::PromptType;
811
812 let trimmed = input.trim();
813
814 let encoding = Encoding::all()
816 .iter()
817 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
818 .copied()
819 .or_else(|| {
820 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
821 Encoding::all()
822 .iter()
823 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
824 .copied()
825 });
826
827 match encoding {
828 Some(enc) => {
829 let threshold = self.config.editor.large_file_threshold_bytes as usize;
832 let file_size = self
833 .filesystem
834 .metadata(path)
835 .map(|m| m.size as usize)
836 .unwrap_or(0);
837
838 if file_size >= threshold && enc.requires_full_file_load() {
839 let size_mb = file_size as f64 / (1024.0 * 1024.0);
841 let load_key = t!("file.large_encoding.key.load").to_string();
842 let encoding_key = t!("file.large_encoding.key.encoding").to_string();
843 let cancel_key = t!("file.large_encoding.key.cancel").to_string();
844 let prompt_msg = t!(
845 "file.large_encoding_prompt",
846 encoding = enc.display_name(),
847 size = format!("{:.0}", size_mb),
848 load_key = load_key,
849 encoding_key = encoding_key,
850 cancel_key = cancel_key
851 )
852 .to_string();
853 self.start_prompt(
854 prompt_msg,
855 PromptType::ConfirmLargeFileEncoding {
856 path: path.to_path_buf(),
857 },
858 );
859 return;
860 }
861
862 self.key_context = crate::input::keybindings::KeyContext::Normal;
864
865 if let Err(e) = self.open_file_with_encoding(path, enc) {
867 self.set_status_message(
868 t!("file.error_opening", error = e.to_string()).to_string(),
869 );
870 } else {
871 self.set_status_message(format!(
872 "Opened {} with {} encoding",
873 path.display(),
874 enc.display_name()
875 ));
876 }
877 }
878 None => {
879 self.set_status_message(format!("Unknown encoding: {}", input));
880 }
881 }
882 }
883
884 fn handle_reload_with_encoding(&mut self, input: &str) {
887 use crate::model::buffer::Encoding;
888
889 let trimmed = input.trim();
890
891 let encoding = Encoding::all()
893 .iter()
894 .find(|enc| enc.display_name().eq_ignore_ascii_case(trimmed))
895 .copied()
896 .or_else(|| {
897 let before_paren = trimmed.split('(').next().unwrap_or(trimmed).trim();
898 Encoding::all()
899 .iter()
900 .find(|enc| enc.display_name().eq_ignore_ascii_case(before_paren))
901 .copied()
902 });
903
904 match encoding {
905 Some(enc) => {
906 if let Err(e) = self.reload_with_encoding(enc) {
908 self.set_status_message(format!("Failed to reload: {}", e));
909 } else {
910 self.set_status_message(format!(
911 "Reloaded with {} encoding",
912 enc.display_name()
913 ));
914 }
915 }
916 None => {
917 self.set_status_message(format!("Unknown encoding: {}", input));
918 }
919 }
920 }
921
922 fn handle_set_language(&mut self, input: &str) {
924 use crate::primitives::detected_language::DetectedLanguage;
925
926 let trimmed = input.trim();
927
928 if trimmed == "Plain Text" || trimmed.to_lowercase() == "text" {
930 let buffer_id = self.active_buffer();
931 if let Some(state) = self.buffers.get_mut(&buffer_id) {
932 state.apply_language(DetectedLanguage::plain_text());
933 self.set_status_message("Language set to Plain Text".to_string());
934 }
935 #[cfg(feature = "plugins")]
936 self.update_plugin_state_snapshot();
937 self.plugin_manager.run_hook(
938 "language_changed",
939 crate::services::plugins::hooks::HookArgs::LanguageChanged {
940 buffer_id: self.active_buffer(),
941 language: "text".to_string(),
942 },
943 );
944 return;
945 }
946
947 if let Some(detected) = DetectedLanguage::from_syntax_name(
949 trimmed,
950 &self.grammar_registry,
951 &self.config.languages,
952 ) {
953 let language = detected.name.clone();
954 let buffer_id = self.active_buffer();
955 if let Some(state) = self.buffers.get_mut(&buffer_id) {
956 state.apply_language(detected);
957 self.set_status_message(format!("Language set to {}", trimmed));
958 }
959 #[cfg(feature = "plugins")]
960 self.update_plugin_state_snapshot();
961 self.plugin_manager.run_hook(
962 "language_changed",
963 crate::services::plugins::hooks::HookArgs::LanguageChanged {
964 buffer_id,
965 language,
966 },
967 );
968 } else if self.config.languages.contains_key(trimmed) {
969 let detected = DetectedLanguage::from_config_language(trimmed);
974 let language = detected.name.clone();
975 let buffer_id = self.active_buffer();
976 if let Some(state) = self.buffers.get_mut(&buffer_id) {
977 state.apply_language(detected);
978 self.set_status_message(format!("Language set to {}", trimmed));
979 }
980 #[cfg(feature = "plugins")]
981 self.update_plugin_state_snapshot();
982 self.plugin_manager.run_hook(
983 "language_changed",
984 crate::services::plugins::hooks::HookArgs::LanguageChanged {
985 buffer_id,
986 language,
987 },
988 );
989 } else {
990 self.set_status_message(format!("Unknown language: {}", input));
991 }
992 }
993
994 fn handle_register_input<F>(&mut self, input: &str, action: F, register_type: &str)
996 where
997 F: FnOnce(&mut Self, char),
998 {
999 if let Some(c) = input.trim().chars().next() {
1000 if c.is_ascii_digit() {
1001 action(self, c);
1002 } else {
1003 self.set_status_message(
1004 t!("register.must_be_digit", "type" = register_type).to_string(),
1005 );
1006 }
1007 } else {
1008 self.set_status_message(t!("register.not_specified").to_string());
1009 }
1010 }
1011
1012 fn handle_confirm_close_buffer(&mut self, input: &str, buffer_id: BufferId) -> bool {
1014 let input_lower = input.trim().to_lowercase();
1015 let save_key = t!("prompt.key.save").to_string().to_lowercase();
1016 let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1017
1018 let first_char = input_lower.chars().next();
1019 let save_first = save_key.chars().next();
1020 let discard_first = discard_key.chars().next();
1021
1022 if first_char == save_first {
1023 let has_path = self
1025 .buffers
1026 .get(&buffer_id)
1027 .map(|s| s.buffer.file_path().is_some())
1028 .unwrap_or(false);
1029
1030 if has_path {
1031 let old_active = self.active_buffer();
1032 self.set_active_buffer(buffer_id);
1033 if let Err(e) = self.save() {
1034 self.set_status_message(
1035 t!("file.save_failed", error = e.to_string()).to_string(),
1036 );
1037 self.set_active_buffer(old_active);
1038 return true; }
1040 self.set_active_buffer(old_active);
1041 if let Err(e) = self.force_close_buffer(buffer_id) {
1042 self.set_status_message(
1043 t!("file.cannot_close", error = e.to_string()).to_string(),
1044 );
1045 } else {
1046 self.set_status_message(t!("buffer.saved_and_closed").to_string());
1047 }
1048 } else {
1049 self.pending_close_buffer = Some(buffer_id);
1050 self.start_prompt_with_initial_text(
1051 t!("file.save_as_prompt").to_string(),
1052 PromptType::SaveFileAs,
1053 String::new(),
1054 );
1055 }
1056 } else if first_char == discard_first {
1057 if let Err(e) = self.force_close_buffer(buffer_id) {
1059 self.set_status_message(t!("file.cannot_close", error = e.to_string()).to_string());
1060 } else {
1061 self.set_status_message(t!("buffer.changes_discarded").to_string());
1062 }
1063 } else {
1064 self.set_status_message(t!("buffer.close_cancelled").to_string());
1065 }
1066 false
1067 }
1068
1069 fn handle_confirm_quit_modified(&mut self, input: &str) -> bool {
1071 let input_lower = input.trim().to_lowercase();
1072 let save_key = t!("prompt.key.save").to_string().to_lowercase();
1073 let discard_key = t!("prompt.key.discard").to_string().to_lowercase();
1074 let quit_key = t!("prompt.key.quit").to_string().to_lowercase();
1075
1076 let first_char = input_lower.chars().next();
1077 let save_first = save_key.chars().next();
1078 let discard_first = discard_key.chars().next();
1079 let quit_first = quit_key.chars().next();
1080
1081 if first_char == save_first {
1082 match self.save_all_on_exit() {
1084 Ok(count) => {
1085 tracing::info!("Saved {} buffer(s) on exit", count);
1086 self.should_quit = true;
1087 }
1088 Err(e) => {
1089 self.set_status_message(
1090 t!("file.save_failed", error = e.to_string()).to_string(),
1091 );
1092 return true; }
1094 }
1095 } else if first_char == discard_first {
1096 self.should_quit = true;
1098 } else if first_char == quit_first && self.config.editor.hot_exit {
1099 self.should_quit = true;
1101 } else {
1102 self.set_status_message(t!("buffer.close_cancelled").to_string());
1104 }
1105 false
1106 }
1107
1108 pub fn handle_stop_lsp_server(&mut self, input: &str) {
1113 let input = input.trim();
1114 if input.is_empty() {
1115 return;
1116 }
1117
1118 let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1120 (lang, Some(name))
1121 } else {
1122 (input, None)
1123 };
1124
1125 let has_server = self
1126 .lsp
1127 .as_ref()
1128 .is_some_and(|lsp| lsp.has_handles(language));
1129
1130 if !has_server {
1131 self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1132 return;
1133 }
1134
1135 let stopping_all = server_name.is_none()
1139 || self
1140 .lsp
1141 .as_ref()
1142 .map(|lsp| lsp.handle_count(language) <= 1)
1143 .unwrap_or(true);
1144
1145 if stopping_all {
1146 let buffer_ids: Vec<_> = self
1150 .buffers
1151 .iter()
1152 .filter(|(_, s)| s.language == language)
1153 .map(|(id, _)| *id)
1154 .collect();
1155 for buffer_id in buffer_ids {
1156 self.disable_lsp_for_buffer(buffer_id);
1157 }
1158 } else if let Some(name) = server_name {
1159 self.send_did_close_to_server(language, name);
1161 self.clear_diagnostics_for_server(name);
1163 }
1164
1165 let stopped = if let Some(lsp) = &mut self.lsp {
1167 if let Some(name) = server_name {
1168 lsp.shutdown_server_by_name(language, name)
1169 } else {
1170 lsp.shutdown_server(language)
1171 }
1172 } else {
1173 false
1174 };
1175
1176 if !stopped {
1177 self.set_status_message(t!("lsp.server_not_found", language = language).to_string());
1178 return;
1179 }
1180
1181 if let Some(lsp_configs) = self.config.lsp.get_mut(language) {
1183 for c in lsp_configs.as_mut_slice() {
1184 if let Some(name) = server_name {
1185 if c.display_name() == name {
1187 c.auto_start = false;
1188 }
1189 } else {
1190 c.auto_start = false;
1191 }
1192 }
1193 if let Err(e) = self.save_config() {
1194 tracing::warn!(
1195 "Failed to save config after disabling LSP auto-start: {}",
1196 e
1197 );
1198 } else {
1199 let config_path = self.dir_context.config_path();
1200 self.emit_event(
1201 "config_changed",
1202 serde_json::json!({
1203 "path": config_path.to_string_lossy(),
1204 }),
1205 );
1206 }
1207 }
1208
1209 let display = server_name.unwrap_or(language);
1210 self.set_status_message(t!("lsp.server_stopped", language = display).to_string());
1211 }
1212
1213 pub fn handle_restart_lsp_server(&mut self, input: &str) {
1218 let input = input.trim();
1219 if input.is_empty() {
1220 return;
1221 }
1222
1223 let (language, server_name) = if let Some((lang, name)) = input.split_once('/') {
1225 (lang, Some(name))
1226 } else {
1227 (input, None)
1228 };
1229
1230 let buffer_id = self.active_buffer();
1232 let file_path = self
1233 .buffer_metadata
1234 .get(&buffer_id)
1235 .and_then(|meta| meta.file_path().cloned());
1236
1237 let (success, message) = if let Some(name) = server_name {
1238 if let Some(lsp) = self.lsp.as_mut() {
1240 lsp.manual_restart_server(language, name, file_path.as_deref())
1241 } else {
1242 (false, t!("lsp.no_manager").to_string())
1243 }
1244 } else {
1245 if let Some(lsp) = self.lsp.as_mut() {
1247 lsp.manual_restart(language, file_path.as_deref())
1248 } else {
1249 (false, t!("lsp.no_manager").to_string())
1250 }
1251 };
1252
1253 self.status_message = Some(message);
1254
1255 if success {
1256 self.reopen_buffers_for_language(language);
1257 }
1258 }
1259
1260 fn handle_quick_open_confirm(
1262 &mut self,
1263 input: &str,
1264 selected_index: Option<usize>,
1265 ) -> PromptResult {
1266 use crate::input::quick_open::QuickOpenResult;
1267
1268 let context = self.build_quick_open_context();
1269 let result = if let Some((provider, query)) =
1270 self.quick_open_registry.get_provider_for_input(input)
1271 {
1272 let suggestions = provider.suggestions(query, &context);
1274 let selected = selected_index.and_then(|i| suggestions.get(i));
1275 provider.on_select(selected, query, &context)
1276 } else {
1277 QuickOpenResult::None
1278 };
1279
1280 self.execute_quick_open_result(result)
1281 }
1282
1283 fn execute_quick_open_result(
1285 &mut self,
1286 result: crate::input::quick_open::QuickOpenResult,
1287 ) -> PromptResult {
1288 use crate::input::quick_open::QuickOpenResult;
1289
1290 match result {
1291 QuickOpenResult::ExecuteAction(action) => PromptResult::ExecuteAction(action),
1292 QuickOpenResult::OpenFile { path, line, column } => {
1293 let expanded_path = expand_tilde(&path);
1294 let full_path = if expanded_path.is_absolute() {
1295 expanded_path
1296 } else {
1297 self.working_dir.join(&expanded_path)
1298 };
1299 self.open_file_with_jump(full_path, line, column);
1300 PromptResult::Done
1301 }
1302 QuickOpenResult::ShowBuffer(buffer_id) => {
1303 let buffer_id = crate::model::event::BufferId(buffer_id);
1304 if self.buffers.contains_key(&buffer_id) {
1305 self.set_active_buffer(buffer_id);
1306 if let Some(name) = self.active_state().buffer.file_path() {
1307 self.set_status_message(
1308 t!("buffer.switched", name = name.display().to_string()).to_string(),
1309 );
1310 }
1311 }
1312 PromptResult::Done
1313 }
1314 QuickOpenResult::GotoLine(line) => {
1315 self.goto_line_col(line, None);
1316 self.set_status_message(t!("goto.jumped", line = line).to_string());
1317 PromptResult::Done
1318 }
1319 QuickOpenResult::None => {
1320 self.set_status_message(t!("status.no_selection").to_string());
1321 PromptResult::Done
1322 }
1323 QuickOpenResult::Error(msg) => {
1324 self.set_status_message(msg);
1325 PromptResult::Done
1326 }
1327 }
1328 }
1329
1330 fn open_file_with_jump(
1331 &mut self,
1332 full_path: std::path::PathBuf,
1333 line: Option<usize>,
1334 column: Option<usize>,
1335 ) {
1336 match self.open_file(&full_path) {
1337 Ok(_) => {
1338 if let Some(line) = line {
1339 self.goto_line_col(line, column);
1340 }
1341 self.set_status_message(
1342 t!("buffer.opened", name = full_path.display().to_string()).to_string(),
1343 );
1344 }
1345 Err(e) => {
1346 if let Some(confirmation) =
1348 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
1349 {
1350 self.start_large_file_encoding_confirmation(confirmation);
1351 } else {
1352 self.set_status_message(
1353 t!("file.error_opening", error = e.to_string()).to_string(),
1354 );
1355 }
1356 }
1357 }
1358 }
1359}
1360
1361#[cfg(test)]
1366mod tests {
1367 use super::parse_path_line_col;
1368
1369 #[test]
1370 fn test_parse_path_line_col_empty() {
1371 let (path, line, col) = parse_path_line_col("");
1372 assert_eq!(path, "");
1373 assert_eq!(line, None);
1374 assert_eq!(col, None);
1375 }
1376
1377 #[test]
1378 fn test_parse_path_line_col_plain_path() {
1379 let (path, line, col) = parse_path_line_col("src/main.rs");
1380 assert_eq!(path, "src/main.rs");
1381 assert_eq!(line, None);
1382 assert_eq!(col, None);
1383 }
1384
1385 #[test]
1386 fn test_parse_path_line_col_line_only() {
1387 let (path, line, col) = parse_path_line_col("src/main.rs:42");
1388 assert_eq!(path, "src/main.rs");
1389 assert_eq!(line, Some(42));
1390 assert_eq!(col, None);
1391 }
1392
1393 #[test]
1394 fn test_parse_path_line_col_line_and_col() {
1395 let (path, line, col) = parse_path_line_col("src/main.rs:42:10");
1396 assert_eq!(path, "src/main.rs");
1397 assert_eq!(line, Some(42));
1398 assert_eq!(col, Some(10));
1399 }
1400
1401 #[test]
1402 fn test_parse_path_line_col_trimmed() {
1403 let (path, line, col) = parse_path_line_col(" src/main.rs:5:2 ");
1404 assert_eq!(path, "src/main.rs");
1405 assert_eq!(line, Some(5));
1406 assert_eq!(col, Some(2));
1407 }
1408
1409 #[test]
1410 fn test_parse_path_line_col_zero_line_rejected() {
1411 let (path, line, col) = parse_path_line_col("src/main.rs:0");
1412 assert_eq!(path, "src/main.rs:0");
1413 assert_eq!(line, None);
1414 assert_eq!(col, None);
1415 }
1416
1417 #[test]
1418 fn test_parse_path_line_col_zero_col_rejected() {
1419 let (path, line, col) = parse_path_line_col("src/main.rs:1:0");
1420 assert_eq!(path, "src/main.rs:1:0");
1421 assert_eq!(line, None);
1422 assert_eq!(col, None);
1423 }
1424
1425 #[cfg(windows)]
1426 #[test]
1427 fn test_parse_path_line_col_windows_drive() {
1428 let (path, line, col) = parse_path_line_col(r"C:\src\main.rs:12:3");
1429 assert_eq!(path, r"C:\src\main.rs");
1430 assert_eq!(line, Some(12));
1431 assert_eq!(col, Some(3));
1432 }
1433}