1use super::file_open::{FileOpenSection, SortMode};
7use super::prompt_actions::parse_path_line_col;
8use super::Editor;
9use crate::input::keybindings::Action;
10use crate::primitives::path_utils::expand_tilde;
11use crate::view::prompt::PromptType;
12use rust_i18n::t;
13
14impl Editor {
15 pub fn is_file_open_active(&self) -> bool {
17 self.prompt
18 .as_ref()
19 .map(|p| {
20 matches!(
21 p.prompt_type,
22 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
23 )
24 })
25 .unwrap_or(false)
26 && self.file_open_state.is_some()
27 }
28
29 fn is_folder_open_mode(&self) -> bool {
31 self.prompt
32 .as_ref()
33 .map(|p| p.prompt_type == PromptType::SwitchProject)
34 .unwrap_or(false)
35 }
36
37 fn is_save_mode(&self) -> bool {
39 self.prompt
40 .as_ref()
41 .map(|p| p.prompt_type == PromptType::SaveFileAs)
42 .unwrap_or(false)
43 }
44
45 pub fn handle_file_open_action(&mut self, action: &Action) -> bool {
48 if !self.is_file_open_active() {
49 return false;
50 }
51
52 match action {
53 Action::PromptSelectPrev => {
55 if let Some(state) = &mut self.file_open_state {
56 state.select_prev();
57 }
58 true
59 }
60 Action::PromptSelectNext => {
61 if let Some(state) = &mut self.file_open_state {
62 state.select_next();
63 }
64 true
65 }
66 Action::PromptPageUp => {
67 if let Some(state) = &mut self.file_open_state {
68 state.page_up(10);
69 }
70 true
71 }
72 Action::PromptPageDown => {
73 if let Some(state) = &mut self.file_open_state {
74 state.page_down(10);
75 }
76 true
77 }
78 Action::PromptConfirm => {
83 self.file_open_confirm();
84 true
85 }
86
87 Action::PromptAcceptSuggestion => {
89 let selected_info = self.file_open_state.as_ref().and_then(|s| {
91 s.selected_index
92 .and_then(|idx| s.entries.get(idx))
93 .map(|e| {
94 (
95 e.fs_entry.name.clone(),
96 e.fs_entry.is_dir(),
97 e.fs_entry.path.clone(),
98 )
99 })
100 });
101
102 if let Some((name, is_dir, path)) = selected_info {
103 if is_dir {
104 self.file_open_navigate_to(path);
106 } else {
107 if let Some(prompt) = &mut self.prompt {
109 prompt.input = name;
110 prompt.cursor_pos = prompt.input.len();
111 }
112 self.update_file_open_filter();
114 }
115 }
116 true
117 }
118
119 Action::PromptBackspace => {
121 let filter_empty = self
122 .file_open_state
123 .as_ref()
124 .map(|s| s.filter.is_empty())
125 .unwrap_or(true);
126 let prompt_empty = self
127 .prompt
128 .as_ref()
129 .map(|p| p.input.is_empty())
130 .unwrap_or(true);
131
132 if filter_empty && prompt_empty {
133 self.file_open_go_parent();
134 true
135 } else {
136 false
138 }
139 }
140
141 Action::PromptCancel => {
143 self.cancel_prompt();
144 self.file_open_state = None;
145 true
146 }
147
148 Action::FileBrowserToggleHidden => {
150 self.file_open_toggle_hidden();
151 true
152 }
153
154 Action::FileBrowserToggleDetectEncoding => {
156 self.file_open_toggle_detect_encoding();
157 true
158 }
159
160 _ => false,
162 }
163 }
164
165 fn file_open_confirm(&mut self) {
167 let is_folder_mode = self.is_folder_open_mode();
168 let is_save_mode = self.is_save_mode();
169 let prompt_input = self
170 .prompt
171 .as_ref()
172 .map(|p| p.input.clone())
173 .unwrap_or_default();
174 let (path_input, line, column) = parse_path_line_col(&prompt_input);
175
176 let current_dir = self
178 .file_open_state
179 .as_ref()
180 .map(|s| s.current_dir.clone())
181 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
182
183 if !path_input.is_empty() {
185 let tilde_expanded = expand_tilde(&path_input);
187 let expanded_path = if tilde_expanded.is_absolute() {
188 tilde_expanded
189 } else {
190 current_dir.join(&path_input)
192 };
193
194 if expanded_path.is_dir() {
195 if is_folder_mode {
196 self.file_open_select_folder(expanded_path);
198 } else {
199 self.file_open_navigate_to(expanded_path);
200 }
201 return;
202 } else if is_save_mode {
203 self.file_open_save_file(expanded_path);
205 return;
206 } else if expanded_path.is_file() && !is_folder_mode {
207 self.file_open_open_file_at_location(expanded_path, line, column);
210 return;
211 } else if !is_folder_mode && Self::should_create_new_file(&path_input) {
212 self.file_open_create_new_file(expanded_path);
215 return;
216 }
217 }
221
222 let (path, is_dir) = {
224 let state = match &self.file_open_state {
225 Some(s) => s,
226 None => {
227 if is_folder_mode {
229 self.file_open_select_folder(current_dir);
230 }
231 return;
232 }
233 };
234
235 let path = match state.get_selected_path() {
236 Some(p) => p,
237 None => {
238 if is_save_mode {
240 self.set_status_message(t!("file.save_as_no_filename").to_string());
241 return;
242 }
243 if is_folder_mode {
245 self.file_open_select_folder(current_dir);
246 }
247 return;
248 }
249 };
250
251 (path, state.selected_is_dir())
252 };
253
254 if is_dir {
255 if is_folder_mode {
256 self.file_open_select_folder(path);
258 } else {
259 self.file_open_navigate_to(path);
261 }
262 } else if is_save_mode {
263 self.file_open_save_file(path);
265 } else if !is_folder_mode {
266 self.file_open_open_file(path);
268 }
269 }
271
272 fn file_open_select_folder(&mut self, path: std::path::PathBuf) {
274 self.file_open_state = None;
276 self.prompt = None;
277
278 self.change_working_dir(path);
280 }
281
282 fn file_open_navigate_to(&mut self, path: std::path::PathBuf) {
284 if let Some(prompt) = self.prompt.as_mut() {
286 prompt.input.clear();
287 prompt.cursor_pos = 0;
288 }
289
290 self.load_file_open_directory(path);
292 }
293
294 fn file_open_open_file(&mut self, path: std::path::PathBuf) {
296 self.file_open_open_file_at_location(path, None, None);
297 }
298
299 fn file_open_open_file_at_location(
301 &mut self,
302 path: std::path::PathBuf,
303 line: Option<usize>,
304 column: Option<usize>,
305 ) {
306 let detect_encoding = self
308 .file_open_state
309 .as_ref()
310 .map(|s| s.detect_encoding)
311 .unwrap_or(true);
312
313 self.file_open_state = None;
315 self.prompt = None;
316
317 if !detect_encoding {
318 self.start_open_file_with_encoding_prompt(path);
320 return;
321 }
322
323 self.key_context = crate::input::keybindings::KeyContext::Normal;
326
327 tracing::info!("[SYNTAX DEBUG] file_open_dialog opening file: {:?}", path);
329 if let Err(e) = self.open_file(&path) {
330 if let Some(confirmation) =
332 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
333 {
334 let size_mb = confirmation.file_size as f64 / (1024.0 * 1024.0);
336 let load_key = t!("file.large_encoding.key.load").to_string();
337 let encoding_key = t!("file.large_encoding.key.encoding").to_string();
338 let cancel_key = t!("file.large_encoding.key.cancel").to_string();
339 let prompt_msg = t!(
340 "file.large_encoding_prompt",
341 encoding = confirmation.encoding.display_name(),
342 size = format!("{:.0}", size_mb),
343 load_key = load_key,
344 encoding_key = encoding_key,
345 cancel_key = cancel_key
346 )
347 .to_string();
348 self.start_prompt(
349 prompt_msg,
350 PromptType::ConfirmLargeFileEncoding {
351 path: confirmation.path.clone(),
352 },
353 );
354 } else {
355 self.set_status_message(
356 t!("file.error_opening", error = e.to_string()).to_string(),
357 );
358 }
359 } else {
360 if let Some(line) = line {
361 self.goto_line_col(line, column);
362 }
363 self.set_status_message(
364 t!("file.opened", path = path.display().to_string()).to_string(),
365 );
366 }
367 }
368
369 pub fn start_large_file_encoding_confirmation(
375 &mut self,
376 confirmation: &crate::model::buffer::LargeFileEncodingConfirmation,
377 ) {
378 let size_mb = confirmation.file_size as f64 / (1024.0 * 1024.0);
379 let load_key = t!("file.large_encoding.key.load").to_string();
380 let encoding_key = t!("file.large_encoding.key.encoding").to_string();
381 let cancel_key = t!("file.large_encoding.key.cancel").to_string();
382 let prompt_msg = t!(
383 "file.large_encoding_prompt",
384 encoding = confirmation.encoding.display_name(),
385 size = format!("{:.0}", size_mb),
386 load_key = load_key,
387 encoding_key = encoding_key,
388 cancel_key = cancel_key
389 )
390 .to_string();
391 self.start_prompt(
392 prompt_msg,
393 PromptType::ConfirmLargeFileEncoding {
394 path: confirmation.path.clone(),
395 },
396 );
397 }
398
399 pub fn start_open_file_with_encoding_prompt(&mut self, path: std::path::PathBuf) {
401 use crate::model::buffer::Encoding;
402 use crate::view::prompt::PromptType;
403
404 let suggestions: Vec<crate::input::commands::Suggestion> = Encoding::all()
406 .iter()
407 .map(|enc| {
408 let is_default = *enc == Encoding::Utf8;
409 crate::input::commands::Suggestion {
410 text: format!("{} ({})", enc.display_name(), enc.description()),
411 description: if is_default {
412 Some("default".to_string())
413 } else {
414 None
415 },
416 value: Some(enc.display_name().to_string()),
417 disabled: false,
418 keybinding: None,
419 source: None,
420 }
421 })
422 .collect();
423
424 self.prompt = Some(crate::view::prompt::Prompt::with_suggestions(
425 "Select encoding: ".to_string(),
426 PromptType::OpenFileWithEncoding { path },
427 suggestions,
428 ));
429
430 if let Some(prompt) = self.prompt.as_mut() {
432 if !prompt.suggestions.is_empty() {
433 prompt.selected_suggestion = Some(0); let enc = Encoding::Utf8;
435 prompt.input = format!("{} ({})", enc.display_name(), enc.description());
436 prompt.cursor_pos = prompt.input.len();
437 }
438 }
439 }
440
441 fn file_open_create_new_file(&mut self, path: std::path::PathBuf) {
443 self.file_open_state = None;
445 self.prompt = None;
446
447 self.key_context = crate::input::keybindings::KeyContext::Normal;
450
451 if let Err(e) = self.open_file(&path) {
453 self.set_status_message(t!("file.error_opening", error = e.to_string()).to_string());
454 } else {
455 self.set_status_message(
456 t!("file.created_new", path = path.display().to_string()).to_string(),
457 );
458 }
459 }
460
461 fn file_open_save_file(&mut self, path: std::path::PathBuf) {
463 use crate::view::prompt::PromptType as PT;
464
465 self.file_open_state = None;
467 self.prompt = None;
468
469 let current_file_path = self
471 .active_state()
472 .buffer
473 .file_path()
474 .map(|p| p.to_path_buf());
475 let is_different_file = current_file_path.as_ref() != Some(&path);
476
477 if is_different_file && path.is_file() {
478 let filename = path
480 .file_name()
481 .map(|n| n.to_string_lossy().to_string())
482 .unwrap_or_else(|| path.display().to_string());
483 self.start_prompt(
484 t!("buffer.overwrite_confirm", name = &filename).to_string(),
485 PT::ConfirmOverwriteFile { path },
486 );
487 return;
488 }
489
490 self.perform_save_file_as(path);
492 }
493
494 fn should_create_new_file(input: &str) -> bool {
497 let has_extension = input.rfind('.').is_some_and(|pos| {
500 let after_dot = &input[pos + 1..];
502 !after_dot.is_empty() && !after_dot.contains('/') && !after_dot.contains('\\')
503 });
504
505 let has_path_separator = input.contains('/') || input.contains('\\');
506
507 has_extension || has_path_separator
508 }
509
510 fn file_open_go_parent(&mut self) {
512 let parent = self
513 .file_open_state
514 .as_ref()
515 .and_then(|s| s.current_dir.parent())
516 .map(|p| p.to_path_buf());
517
518 if let Some(parent_path) = parent {
519 self.file_open_navigate_to(parent_path);
520 }
521 }
522
523 pub fn update_file_open_filter(&mut self) {
525 if !self.is_file_open_active() {
526 return;
527 }
528
529 let filter = self
530 .prompt
531 .as_ref()
532 .map(|p| p.input.clone())
533 .unwrap_or_default();
534
535 if filter.contains('/') {
538 let current_dir = self
539 .file_open_state
540 .as_ref()
541 .map(|s| s.current_dir.clone())
542 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
543
544 let tilde_expanded = expand_tilde(&filter);
547 let full_path = if tilde_expanded.is_absolute() {
548 tilde_expanded
549 } else {
550 current_dir.join(&filter)
551 };
552
553 let (target_dir, filename) = if filter.ends_with('/') {
555 (full_path.clone(), String::new())
557 } else {
558 let parent = full_path
560 .parent()
561 .map(|p| p.to_path_buf())
562 .unwrap_or(full_path.clone());
563 let name = full_path
564 .file_name()
565 .map(|n| n.to_string_lossy().to_string())
566 .unwrap_or_default();
567 (parent, name)
568 };
569
570 if target_dir.is_dir() && target_dir != current_dir {
572 if let Some(prompt) = &mut self.prompt {
574 prompt.input = filename.clone();
575 prompt.cursor_pos = prompt.input.len();
576 }
577 self.load_file_open_directory(target_dir);
578
579 if let Some(state) = &mut self.file_open_state {
581 state.apply_filter(&filename);
582 }
583 return;
584 }
585 }
586
587 if let Some(state) = &mut self.file_open_state {
588 state.apply_filter(&filter);
589 }
590 }
591
592 pub fn file_open_toggle_sort(&mut self, mode: SortMode) {
594 if let Some(state) = &mut self.file_open_state {
595 state.set_sort_mode(mode);
596 }
597 }
598
599 pub fn file_open_toggle_hidden(&mut self) {
601 if let Some(state) = &mut self.file_open_state {
602 let show_hidden = state.show_hidden;
603 state.show_hidden = !show_hidden;
604 let new_state = state.show_hidden;
605
606 let current_dir = state.current_dir.clone();
608 self.load_file_open_directory(current_dir);
609
610 let msg = if new_state {
612 "Showing hidden files"
613 } else {
614 "Hiding hidden files"
615 };
616 self.set_status_message(msg.to_string());
617 }
618 }
619
620 pub fn file_open_toggle_detect_encoding(&mut self) {
622 if let Some(state) = &mut self.file_open_state {
623 state.toggle_detect_encoding();
624 let new_state = state.detect_encoding;
625
626 let msg = if new_state {
628 "Encoding auto-detection enabled"
629 } else {
630 "Encoding auto-detection disabled - will prompt for encoding"
631 };
632 self.set_status_message(msg.to_string());
633 }
634 }
635
636 pub fn handle_file_open_scroll(&mut self, delta: i32) -> bool {
639 if !self.is_file_open_active() {
640 return false;
641 }
642
643 let visible_rows = self
644 .file_browser_layout
645 .as_ref()
646 .map(|l| l.visible_rows)
647 .unwrap_or(10);
648
649 if let Some(state) = &mut self.file_open_state {
650 let total_entries = state.entries.len();
651 if total_entries <= visible_rows {
652 return true;
654 }
655
656 let max_scroll = total_entries.saturating_sub(visible_rows);
657
658 if delta < 0 {
659 let scroll_amount = (-delta) as usize;
661 state.scroll_offset = state.scroll_offset.saturating_sub(scroll_amount);
662 } else {
663 let scroll_amount = delta as usize;
665 state.scroll_offset = (state.scroll_offset + scroll_amount).min(max_scroll);
666 }
667 return true;
668 }
669
670 false
671 }
672
673 pub fn handle_file_open_click(&mut self, x: u16, y: u16) -> bool {
675 if !self.is_file_open_active() {
676 return false;
677 }
678
679 let layout = match &self.file_browser_layout {
680 Some(l) => l.clone(),
681 None => return false,
682 };
683
684 if layout.is_in_list(x, y) {
686 let scroll_offset = self
687 .file_open_state
688 .as_ref()
689 .map(|s| s.scroll_offset)
690 .unwrap_or(0);
691
692 if let Some(index) = layout.click_to_index(y, scroll_offset) {
693 let entry_name = self
695 .file_open_state
696 .as_ref()
697 .and_then(|s| s.entries.get(index))
698 .map(|e| e.fs_entry.name.clone());
699
700 if let Some(state) = &mut self.file_open_state {
701 state.active_section = FileOpenSection::Files;
702 if index < state.entries.len() {
703 state.selected_index = Some(index);
704 }
705 }
706
707 if let Some(name) = entry_name {
709 if let Some(prompt) = &mut self.prompt {
710 prompt.input = name;
711 prompt.cursor_pos = prompt.input.len();
712 }
713 }
714 }
715 return true;
716 }
717
718 if layout.is_on_show_hidden_checkbox(x, y) {
720 self.file_open_toggle_hidden();
721 return true;
722 }
723
724 if layout.is_on_detect_encoding_checkbox(x, y) {
726 self.file_open_toggle_detect_encoding();
727 return true;
728 }
729
730 if layout.is_in_nav(x, y) {
732 let shortcut_labels: Vec<&str> = self
734 .file_open_state
735 .as_ref()
736 .map(|s| s.shortcuts.iter().map(|sc| sc.label.as_str()).collect())
737 .unwrap_or_default();
738
739 if let Some(shortcut_idx) = layout.nav_shortcut_at(x, y, &shortcut_labels) {
740 let target_path = self
742 .file_open_state
743 .as_ref()
744 .and_then(|s| s.shortcuts.get(shortcut_idx))
745 .map(|sc| sc.path.clone());
746
747 if let Some(path) = target_path {
748 if let Some(state) = &mut self.file_open_state {
749 state.active_section = FileOpenSection::Navigation;
750 state.selected_shortcut = shortcut_idx;
751 }
752 self.file_open_navigate_to(path);
753 }
754 } else {
755 if let Some(state) = &mut self.file_open_state {
757 state.active_section = FileOpenSection::Navigation;
758 }
759 }
760 return true;
761 }
762
763 if layout.is_in_header(x, y) {
765 if let Some(mode) = layout.header_column_at(x) {
766 self.file_open_toggle_sort(mode);
767 }
768 return true;
769 }
770
771 if layout.is_in_scrollbar(x, y) {
773 let rel_y = y.saturating_sub(layout.scrollbar_area.y) as usize;
775 let track_height = layout.scrollbar_area.height as usize;
776
777 if let Some(state) = &mut self.file_open_state {
778 let total_items = state.entries.len();
779 let visible_items = layout.visible_rows;
780
781 if total_items > visible_items && track_height > 0 {
782 let max_scroll = total_items.saturating_sub(visible_items);
783 let click_ratio = rel_y as f64 / track_height as f64;
784 let new_offset = (click_ratio * max_scroll as f64) as usize;
785 state.scroll_offset = new_offset.min(max_scroll);
786 }
787 }
788 return true;
789 }
790
791 false
792 }
793
794 pub fn handle_file_open_double_click(&mut self, x: u16, y: u16) -> bool {
796 if !self.is_file_open_active() {
797 return false;
798 }
799
800 let layout = match &self.file_browser_layout {
801 Some(l) => l.clone(),
802 None => return false,
803 };
804
805 if layout.is_in_list(x, y) {
807 self.file_open_confirm();
808 return true;
809 }
810
811 false
812 }
813
814 pub fn compute_file_browser_hover(&self, x: u16, y: u16) -> Option<super::types::HoverTarget> {
816 use super::types::HoverTarget;
817
818 let layout = self.file_browser_layout.as_ref()?;
819
820 if layout.is_on_show_hidden_checkbox(x, y) {
822 return Some(HoverTarget::FileBrowserShowHiddenCheckbox);
823 }
824
825 if layout.is_on_detect_encoding_checkbox(x, y) {
827 return Some(HoverTarget::FileBrowserDetectEncodingCheckbox);
828 }
829
830 if layout.is_in_nav(x, y) {
832 let shortcut_labels: Vec<&str> = self
833 .file_open_state
834 .as_ref()
835 .map(|s| s.shortcuts.iter().map(|sc| sc.label.as_str()).collect())
836 .unwrap_or_default();
837
838 if let Some(idx) = layout.nav_shortcut_at(x, y, &shortcut_labels) {
839 return Some(HoverTarget::FileBrowserNavShortcut(idx));
840 }
841 }
842
843 if layout.is_in_header(x, y) {
845 if let Some(mode) = layout.header_column_at(x) {
846 return Some(HoverTarget::FileBrowserHeader(mode));
847 }
848 }
849
850 if layout.is_in_list(x, y) {
852 let scroll_offset = self
853 .file_open_state
854 .as_ref()
855 .map(|s| s.scroll_offset)
856 .unwrap_or(0);
857
858 if let Some(idx) = layout.click_to_index(y, scroll_offset) {
859 let total_entries = self
860 .file_open_state
861 .as_ref()
862 .map(|s| s.entries.len())
863 .unwrap_or(0);
864
865 if idx < total_entries {
866 return Some(HoverTarget::FileBrowserEntry(idx));
867 }
868 }
869 }
870
871 if layout.is_in_scrollbar(x, y) {
873 return Some(HoverTarget::FileBrowserScrollbar);
874 }
875
876 None
877 }
878}