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.active_window()
18 .prompt
19 .as_ref()
20 .map(|p| {
21 matches!(
22 p.prompt_type,
23 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
24 )
25 })
26 .unwrap_or(false)
27 && self.active_window().file_open_state.is_some()
28 }
29
30 fn is_folder_open_mode(&self) -> bool {
32 self.active_window()
33 .prompt
34 .as_ref()
35 .map(|p| p.prompt_type == PromptType::SwitchProject)
36 .unwrap_or(false)
37 }
38
39 fn is_save_mode(&self) -> bool {
41 self.active_window()
42 .prompt
43 .as_ref()
44 .map(|p| p.prompt_type == PromptType::SaveFileAs)
45 .unwrap_or(false)
46 }
47
48 pub fn handle_file_open_action(&mut self, action: &Action) -> bool {
51 if !self.is_file_open_active() {
52 return false;
53 }
54
55 match action {
56 Action::PromptSelectPrev => {
58 if let Some(state) = &mut self.active_window_mut().file_open_state {
59 state.select_prev();
60 }
61 true
62 }
63 Action::PromptSelectNext => {
64 if let Some(state) = &mut self.active_window_mut().file_open_state {
65 state.select_next();
66 }
67 true
68 }
69 Action::PromptPageUp => {
70 if let Some(state) = &mut self.active_window_mut().file_open_state {
71 state.page_up(10);
72 }
73 true
74 }
75 Action::PromptPageDown => {
76 if let Some(state) = &mut self.active_window_mut().file_open_state {
77 state.page_down(10);
78 }
79 true
80 }
81 Action::PromptConfirm => {
86 self.file_open_confirm();
87 true
88 }
89
90 Action::PromptAcceptSuggestion => {
92 let selected_info =
94 self.active_window_mut()
95 .file_open_state
96 .as_ref()
97 .and_then(|s| {
98 s.selected_index
99 .and_then(|idx| s.entries.get(idx))
100 .map(|e| {
101 (
102 e.fs_entry.name.clone(),
103 e.fs_entry.is_dir(),
104 e.fs_entry.path.clone(),
105 )
106 })
107 });
108
109 if let Some((name, is_dir, path)) = selected_info {
110 if is_dir {
111 self.file_open_navigate_to(path);
113 } else {
114 if let Some(prompt) = &mut self.active_window_mut().prompt {
116 prompt.input = name;
117 prompt.cursor_pos = prompt.input.len();
118 }
119 self.update_file_open_filter();
121 }
122 }
123 true
124 }
125
126 Action::PromptBackspace => {
128 let filter_empty = self
129 .active_window_mut()
130 .file_open_state
131 .as_ref()
132 .map(|s| s.filter.is_empty())
133 .unwrap_or(true);
134 let prompt_empty = self
135 .active_window()
136 .prompt
137 .as_ref()
138 .map(|p| p.input.is_empty())
139 .unwrap_or(true);
140
141 if filter_empty && prompt_empty {
142 self.file_open_go_parent();
143 true
144 } else {
145 false
147 }
148 }
149
150 Action::PromptCancel => {
152 self.cancel_prompt();
153 self.active_window_mut().file_open_state = None;
154 true
155 }
156
157 Action::FileBrowserToggleHidden => {
159 self.file_open_toggle_hidden();
160 true
161 }
162
163 Action::FileBrowserToggleDetectEncoding => {
165 self.file_open_toggle_detect_encoding();
166 true
167 }
168
169 _ => false,
171 }
172 }
173
174 fn file_open_confirm(&mut self) {
176 let is_folder_mode = self.is_folder_open_mode();
177 let is_save_mode = self.is_save_mode();
178 let prompt_input = self
179 .active_window()
180 .prompt
181 .as_ref()
182 .map(|p| p.input.clone())
183 .unwrap_or_default();
184 let (path_input, line, column) = parse_path_line_col(&prompt_input);
185
186 let current_dir = self
188 .active_window_mut()
189 .file_open_state
190 .as_ref()
191 .map(|s| s.current_dir.clone())
192 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
193
194 if !path_input.is_empty() {
196 let tilde_expanded = expand_tilde(&path_input);
198 let expanded_path = if tilde_expanded.is_absolute() {
199 tilde_expanded
200 } else {
201 current_dir.join(&path_input)
203 };
204
205 if expanded_path.is_dir() {
206 if is_folder_mode {
207 self.file_open_select_folder(expanded_path);
209 } else {
210 self.file_open_navigate_to(expanded_path);
211 }
212 return;
213 } else if is_save_mode {
214 self.file_open_save_file(expanded_path);
216 return;
217 } else if expanded_path.is_file() && !is_folder_mode {
218 self.file_open_open_file_at_location(expanded_path, line, column);
221 return;
222 } else if !is_folder_mode && Self::should_create_new_file(&path_input) {
223 self.file_open_create_new_file(expanded_path);
226 return;
227 }
228 }
232
233 let (path, is_dir) = {
235 let state = match &self.active_window_mut().file_open_state {
236 Some(s) => s,
237 None => {
238 if is_folder_mode {
240 self.file_open_select_folder(current_dir);
241 }
242 return;
243 }
244 };
245
246 let path = match state.get_selected_path() {
247 Some(p) => p,
248 None => {
249 if is_save_mode {
251 self.set_status_message(t!("file.save_as_no_filename").to_string());
252 return;
253 }
254 if is_folder_mode {
256 self.file_open_select_folder(current_dir);
257 }
258 return;
259 }
260 };
261
262 (path, state.selected_is_dir())
263 };
264
265 if is_dir {
266 if is_folder_mode {
267 self.file_open_select_folder(path);
269 } else {
270 self.file_open_navigate_to(path);
272 }
273 } else if is_save_mode {
274 self.file_open_save_file(path);
276 } else if !is_folder_mode {
277 self.file_open_open_file(path);
279 }
280 }
282
283 fn file_open_select_folder(&mut self, path: std::path::PathBuf) {
285 self.active_window_mut().file_open_state = None;
287 self.active_window_mut().prompt = None;
288
289 self.change_working_dir(path);
291 }
292
293 fn file_open_navigate_to(&mut self, path: std::path::PathBuf) {
295 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
297 prompt.input.clear();
298 prompt.cursor_pos = 0;
299 }
300
301 self.load_file_open_directory(path);
303 }
304
305 fn file_open_open_file(&mut self, path: std::path::PathBuf) {
307 self.file_open_open_file_at_location(path, None, None);
308 }
309
310 fn file_open_open_file_at_location(
312 &mut self,
313 path: std::path::PathBuf,
314 line: Option<usize>,
315 column: Option<usize>,
316 ) {
317 let detect_encoding = self
319 .active_window_mut()
320 .file_open_state
321 .as_ref()
322 .map(|s| s.detect_encoding)
323 .unwrap_or(true);
324
325 self.active_window_mut().file_open_state = None;
327 self.active_window_mut().prompt = None;
328
329 if !detect_encoding {
330 self.start_open_file_with_encoding_prompt(path);
332 return;
333 }
334
335 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Normal;
338
339 tracing::info!("[SYNTAX DEBUG] file_open_dialog opening file: {:?}", path);
341 if let Err(e) = self.open_file(&path) {
342 if let Some(confirmation) =
344 e.downcast_ref::<crate::model::buffer::LargeFileEncodingConfirmation>()
345 {
346 let size_mb = confirmation.file_size as f64 / (1024.0 * 1024.0);
348 let load_key = t!("file.large_encoding.key.load").to_string();
349 let encoding_key = t!("file.large_encoding.key.encoding").to_string();
350 let cancel_key = t!("file.large_encoding.key.cancel").to_string();
351 let prompt_msg = t!(
352 "file.large_encoding_prompt",
353 encoding = confirmation.encoding.display_name(),
354 size = format!("{:.0}", size_mb),
355 load_key = load_key,
356 encoding_key = encoding_key,
357 cancel_key = cancel_key
358 )
359 .to_string();
360 self.start_prompt(
361 prompt_msg,
362 PromptType::ConfirmLargeFileEncoding {
363 path: confirmation.path.clone(),
364 },
365 );
366 } else {
367 self.set_status_message(
368 t!("file.error_opening", error = e.to_string()).to_string(),
369 );
370 }
371 } else {
372 if let Some(line) = line {
373 self.goto_line_col(line, column);
374 }
375 self.set_status_message(
376 t!("file.opened", path = path.display().to_string()).to_string(),
377 );
378 }
379 }
380
381 pub fn start_large_file_encoding_confirmation(
387 &mut self,
388 confirmation: &crate::model::buffer::LargeFileEncodingConfirmation,
389 ) {
390 let size_mb = confirmation.file_size as f64 / (1024.0 * 1024.0);
391 let load_key = t!("file.large_encoding.key.load").to_string();
392 let encoding_key = t!("file.large_encoding.key.encoding").to_string();
393 let cancel_key = t!("file.large_encoding.key.cancel").to_string();
394 let prompt_msg = t!(
395 "file.large_encoding_prompt",
396 encoding = confirmation.encoding.display_name(),
397 size = format!("{:.0}", size_mb),
398 load_key = load_key,
399 encoding_key = encoding_key,
400 cancel_key = cancel_key
401 )
402 .to_string();
403 self.start_prompt(
404 prompt_msg,
405 PromptType::ConfirmLargeFileEncoding {
406 path: confirmation.path.clone(),
407 },
408 );
409 }
410
411 pub fn start_open_file_with_encoding_prompt(&mut self, path: std::path::PathBuf) {
413 use crate::model::buffer::Encoding;
414 use crate::view::prompt::PromptType;
415
416 let suggestions: Vec<crate::input::commands::Suggestion> = Encoding::all()
418 .iter()
419 .map(|enc| {
420 let is_default = *enc == Encoding::Utf8;
421 crate::input::commands::Suggestion {
422 text: format!("{} ({})", enc.display_name(), enc.description()),
423 description: if is_default {
424 Some("default".to_string())
425 } else {
426 None
427 },
428 value: Some(enc.display_name().to_string()),
429 disabled: false,
430 keybinding: None,
431 source: None,
432 }
433 })
434 .collect();
435
436 self.active_window_mut().prompt = Some(crate::view::prompt::Prompt::with_suggestions(
437 "Select encoding: ".to_string(),
438 PromptType::OpenFileWithEncoding { path },
439 suggestions,
440 ));
441
442 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
444 if !prompt.suggestions.is_empty() {
445 prompt.selected_suggestion = Some(0); let enc = Encoding::Utf8;
447 prompt.input = format!("{} ({})", enc.display_name(), enc.description());
448 prompt.cursor_pos = prompt.input.len();
449 }
450 }
451 }
452
453 fn file_open_create_new_file(&mut self, path: std::path::PathBuf) {
455 self.active_window_mut().file_open_state = None;
457 self.active_window_mut().prompt = None;
458
459 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Normal;
462
463 if let Err(e) = self.open_file(&path) {
465 self.set_status_message(t!("file.error_opening", error = e.to_string()).to_string());
466 } else {
467 self.set_status_message(
468 t!("file.created_new", path = path.display().to_string()).to_string(),
469 );
470 }
471 }
472
473 fn file_open_save_file(&mut self, path: std::path::PathBuf) {
475 self.active_window_mut().file_open_state = None;
477 self.active_window_mut().prompt = None;
478
479 self.save_file_as_with_checks(path);
480 }
481
482 fn should_create_new_file(input: &str) -> bool {
485 let has_extension = input.rfind('.').is_some_and(|pos| {
488 let after_dot = &input[pos + 1..];
490 !after_dot.is_empty() && !after_dot.contains('/') && !after_dot.contains('\\')
491 });
492
493 let has_path_separator = input.contains('/') || input.contains('\\');
494
495 has_extension || has_path_separator
496 }
497
498 fn file_open_go_parent(&mut self) {
500 let parent = self
501 .active_window_mut()
502 .file_open_state
503 .as_ref()
504 .and_then(|s| s.current_dir.parent())
505 .map(|p| p.to_path_buf());
506
507 if let Some(parent_path) = parent {
508 self.file_open_navigate_to(parent_path);
509 }
510 }
511
512 pub fn update_file_open_filter(&mut self) {
514 if !self.is_file_open_active() {
515 return;
516 }
517
518 let filter = self
519 .active_window()
520 .prompt
521 .as_ref()
522 .map(|p| p.input.clone())
523 .unwrap_or_default();
524
525 if filter.contains('/') {
528 let current_dir = self
529 .active_window_mut()
530 .file_open_state
531 .as_ref()
532 .map(|s| s.current_dir.clone())
533 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
534
535 let tilde_expanded = expand_tilde(&filter);
538 let full_path = if tilde_expanded.is_absolute() {
539 tilde_expanded
540 } else {
541 current_dir.join(&filter)
542 };
543
544 let (target_dir, filename) = if filter.ends_with('/') {
546 (full_path.clone(), String::new())
548 } else {
549 let parent = full_path
551 .parent()
552 .map(|p| p.to_path_buf())
553 .unwrap_or(full_path.clone());
554 let name = full_path
555 .file_name()
556 .map(|n| n.to_string_lossy().to_string())
557 .unwrap_or_default();
558 (parent, name)
559 };
560
561 if target_dir.is_dir() && target_dir != current_dir {
563 if let Some(prompt) = &mut self.active_window_mut().prompt {
565 prompt.input = filename.clone();
566 prompt.cursor_pos = prompt.input.len();
567 }
568 self.load_file_open_directory(target_dir);
569
570 if let Some(state) = &mut self.active_window_mut().file_open_state {
572 state.apply_filter(&filename);
573 }
574 return;
575 }
576 }
577
578 if let Some(state) = &mut self.active_window_mut().file_open_state {
579 state.apply_filter(&filter);
580 }
581 }
582
583 pub fn file_open_toggle_sort(&mut self, mode: SortMode) {
585 if let Some(state) = &mut self.active_window_mut().file_open_state {
586 state.set_sort_mode(mode);
587 }
588 }
589
590 pub fn file_open_toggle_hidden(&mut self) {
592 if let Some(state) = &mut self.active_window_mut().file_open_state {
593 let show_hidden = state.show_hidden;
594 state.show_hidden = !show_hidden;
595 let new_state = state.show_hidden;
596
597 let current_dir = state.current_dir.clone();
599 self.load_file_open_directory(current_dir);
600
601 let msg = if new_state {
603 "Showing hidden files"
604 } else {
605 "Hiding hidden files"
606 };
607 self.set_status_message(msg.to_string());
608 }
609 }
610
611 pub fn file_open_toggle_detect_encoding(&mut self) {
613 if let Some(state) = &mut self.active_window_mut().file_open_state {
614 state.toggle_detect_encoding();
615 let new_state = state.detect_encoding;
616
617 let msg = if new_state {
619 "Encoding auto-detection enabled"
620 } else {
621 "Encoding auto-detection disabled - will prompt for encoding"
622 };
623 self.set_status_message(msg.to_string());
624 }
625 }
626
627 pub fn handle_file_open_scroll(&mut self, delta: i32) -> bool {
630 if !self.is_file_open_active() {
631 return false;
632 }
633
634 let visible_rows = self
635 .active_window_mut()
636 .file_browser_layout
637 .as_ref()
638 .map(|l| l.visible_rows)
639 .unwrap_or(10);
640
641 if let Some(state) = &mut self.active_window_mut().file_open_state {
642 let total_entries = state.entries.len();
643 if total_entries <= visible_rows {
644 return true;
646 }
647
648 let max_scroll = total_entries.saturating_sub(visible_rows);
649
650 if delta < 0 {
651 let scroll_amount = (-delta) as usize;
653 state.scroll_offset = state.scroll_offset.saturating_sub(scroll_amount);
654 } else {
655 let scroll_amount = delta as usize;
657 state.scroll_offset = (state.scroll_offset + scroll_amount).min(max_scroll);
658 }
659 return true;
660 }
661
662 false
663 }
664
665 pub fn handle_file_open_click(&mut self, x: u16, y: u16) -> bool {
667 if !self.is_file_open_active() {
668 return false;
669 }
670
671 let layout = match &self.active_window_mut().file_browser_layout {
672 Some(l) => l.clone(),
673 None => return false,
674 };
675
676 if layout.is_in_list(x, y) {
678 let scroll_offset = self
679 .active_window_mut()
680 .file_open_state
681 .as_ref()
682 .map(|s| s.scroll_offset)
683 .unwrap_or(0);
684
685 if let Some(index) = layout.click_to_index(y, scroll_offset) {
686 let entry_name = self
688 .active_window_mut()
689 .file_open_state
690 .as_ref()
691 .and_then(|s| s.entries.get(index))
692 .map(|e| e.fs_entry.name.clone());
693
694 if let Some(state) = &mut self.active_window_mut().file_open_state {
695 state.active_section = FileOpenSection::Files;
696 if index < state.entries.len() {
697 state.selected_index = Some(index);
698 }
699 }
700
701 if let Some(name) = entry_name {
703 if let Some(prompt) = &mut self.active_window_mut().prompt {
704 prompt.input = name;
705 prompt.cursor_pos = prompt.input.len();
706 }
707 }
708 }
709 return true;
710 }
711
712 if layout.is_on_show_hidden_checkbox(x, y) {
714 self.file_open_toggle_hidden();
715 return true;
716 }
717
718 if layout.is_on_detect_encoding_checkbox(x, y) {
720 self.file_open_toggle_detect_encoding();
721 return true;
722 }
723
724 if layout.is_in_nav(x, y) {
726 let shortcut_labels: Vec<&str> = self
728 .active_window_mut()
729 .file_open_state
730 .as_ref()
731 .map(|s| s.shortcuts.iter().map(|sc| sc.label.as_str()).collect())
732 .unwrap_or_default();
733
734 if let Some(shortcut_idx) = layout.nav_shortcut_at(x, y, &shortcut_labels) {
735 let target_path = self
737 .active_window_mut()
738 .file_open_state
739 .as_ref()
740 .and_then(|s| s.shortcuts.get(shortcut_idx))
741 .map(|sc| sc.path.clone());
742
743 if let Some(path) = target_path {
744 if let Some(state) = &mut self.active_window_mut().file_open_state {
745 state.active_section = FileOpenSection::Navigation;
746 state.selected_shortcut = shortcut_idx;
747 }
748 self.file_open_navigate_to(path);
749 }
750 } else {
751 if let Some(state) = &mut self.active_window_mut().file_open_state {
753 state.active_section = FileOpenSection::Navigation;
754 }
755 }
756 return true;
757 }
758
759 if layout.is_in_header(x, y) {
761 if let Some(mode) = layout.header_column_at(x) {
762 self.file_open_toggle_sort(mode);
763 }
764 return true;
765 }
766
767 if layout.is_in_scrollbar(x, y) {
769 let rel_y = y.saturating_sub(layout.scrollbar_area.y) as usize;
771 let track_height = layout.scrollbar_area.height as usize;
772
773 if let Some(state) = &mut self.active_window_mut().file_open_state {
774 let total_items = state.entries.len();
775 let visible_items = layout.visible_rows;
776
777 if total_items > visible_items && track_height > 0 {
778 let max_scroll = total_items.saturating_sub(visible_items);
779 let click_ratio = rel_y as f64 / track_height as f64;
780 let new_offset = (click_ratio * max_scroll as f64) as usize;
781 state.scroll_offset = new_offset.min(max_scroll);
782 }
783 }
784 return true;
785 }
786
787 false
788 }
789
790 pub fn handle_file_open_double_click(&mut self, x: u16, y: u16) -> bool {
792 if !self.is_file_open_active() {
793 return false;
794 }
795
796 let layout = match &self.active_window_mut().file_browser_layout {
797 Some(l) => l.clone(),
798 None => return false,
799 };
800
801 if layout.is_in_list(x, y) {
803 if self.is_folder_open_mode() {
809 let selected_dir = self.active_window().file_open_state.as_ref().and_then(|s| {
810 s.selected_index
811 .and_then(|idx| s.entries.get(idx))
812 .filter(|e| e.fs_entry.is_dir())
813 .map(|e| e.fs_entry.path.clone())
814 });
815 if let Some(path) = selected_dir {
816 self.file_open_navigate_to(path);
817 return true;
818 }
819 }
820 self.file_open_confirm();
821 return true;
822 }
823
824 false
825 }
826
827 pub fn compute_file_browser_hover(&self, x: u16, y: u16) -> Option<super::types::HoverTarget> {
829 use super::types::HoverTarget;
830
831 let layout = self.active_window().file_browser_layout.as_ref()?;
832
833 if layout.is_on_show_hidden_checkbox(x, y) {
835 return Some(HoverTarget::FileBrowserShowHiddenCheckbox);
836 }
837
838 if layout.is_on_detect_encoding_checkbox(x, y) {
840 return Some(HoverTarget::FileBrowserDetectEncodingCheckbox);
841 }
842
843 if layout.is_in_nav(x, y) {
845 let shortcut_labels: Vec<&str> = self
846 .active_window()
847 .file_open_state
848 .as_ref()
849 .map(|s| s.shortcuts.iter().map(|sc| sc.label.as_str()).collect())
850 .unwrap_or_default();
851
852 if let Some(idx) = layout.nav_shortcut_at(x, y, &shortcut_labels) {
853 return Some(HoverTarget::FileBrowserNavShortcut(idx));
854 }
855 }
856
857 if layout.is_in_header(x, y) {
859 if let Some(mode) = layout.header_column_at(x) {
860 return Some(HoverTarget::FileBrowserHeader(mode));
861 }
862 }
863
864 if layout.is_in_list(x, y) {
866 let scroll_offset = self
867 .active_window()
868 .file_open_state
869 .as_ref()
870 .map(|s| s.scroll_offset)
871 .unwrap_or(0);
872
873 if let Some(idx) = layout.click_to_index(y, scroll_offset) {
874 let total_entries = self
875 .active_window()
876 .file_open_state
877 .as_ref()
878 .map(|s| s.entries.len())
879 .unwrap_or(0);
880
881 if idx < total_entries {
882 return Some(HoverTarget::FileBrowserEntry(idx));
883 }
884 }
885 }
886
887 if layout.is_in_scrollbar(x, y) {
889 return Some(HoverTarget::FileBrowserScrollbar);
890 }
891
892 None
893 }
894}