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 description_spans: None,
423 text: format!("{} ({})", enc.display_name(), enc.description()),
424 description: if is_default {
425 Some("default".to_string())
426 } else {
427 None
428 },
429 value: Some(enc.display_name().to_string()),
430 disabled: false,
431 keybinding: None,
432 source: None,
433 }
434 })
435 .collect();
436
437 self.active_window_mut().prompt = Some(crate::view::prompt::Prompt::with_suggestions(
438 "Select encoding: ".to_string(),
439 PromptType::OpenFileWithEncoding { path },
440 suggestions,
441 ));
442
443 if let Some(prompt) = self.active_window_mut().prompt.as_mut() {
445 if !prompt.suggestions.is_empty() {
446 prompt.selected_suggestion = Some(0); let enc = Encoding::Utf8;
448 prompt.input = format!("{} ({})", enc.display_name(), enc.description());
449 prompt.cursor_pos = prompt.input.len();
450 }
451 }
452 }
453
454 fn file_open_create_new_file(&mut self, path: std::path::PathBuf) {
456 self.active_window_mut().file_open_state = None;
458 self.active_window_mut().prompt = None;
459
460 self.active_window_mut().key_context = crate::input::keybindings::KeyContext::Normal;
463
464 if let Err(e) = self.open_file(&path) {
466 self.set_status_message(t!("file.error_opening", error = e.to_string()).to_string());
467 } else {
468 self.set_status_message(
469 t!("file.created_new", path = path.display().to_string()).to_string(),
470 );
471 }
472 }
473
474 fn file_open_save_file(&mut self, path: std::path::PathBuf) {
476 self.active_window_mut().file_open_state = None;
478 self.active_window_mut().prompt = None;
479
480 self.save_file_as_with_checks(path);
481 }
482
483 fn should_create_new_file(input: &str) -> bool {
486 let has_extension = input.rfind('.').is_some_and(|pos| {
489 let after_dot = &input[pos + 1..];
491 !after_dot.is_empty() && !after_dot.contains('/') && !after_dot.contains('\\')
492 });
493
494 let has_path_separator = input.contains('/') || input.contains('\\');
495
496 has_extension || has_path_separator
497 }
498
499 fn file_open_go_parent(&mut self) {
501 let parent = self
502 .active_window_mut()
503 .file_open_state
504 .as_ref()
505 .and_then(|s| s.current_dir.parent())
506 .map(|p| p.to_path_buf());
507
508 if let Some(parent_path) = parent {
509 self.file_open_navigate_to(parent_path);
510 }
511 }
512
513 pub fn update_file_open_filter(&mut self) {
515 if !self.is_file_open_active() {
516 return;
517 }
518
519 let filter = self
520 .active_window()
521 .prompt
522 .as_ref()
523 .map(|p| p.input.clone())
524 .unwrap_or_default();
525
526 if filter.contains('/') {
529 let current_dir = self
530 .active_window_mut()
531 .file_open_state
532 .as_ref()
533 .map(|s| s.current_dir.clone())
534 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
535
536 let tilde_expanded = expand_tilde(&filter);
539 let full_path = if tilde_expanded.is_absolute() {
540 tilde_expanded
541 } else {
542 current_dir.join(&filter)
543 };
544
545 let (target_dir, filename) = if filter.ends_with('/') {
547 (full_path.clone(), String::new())
549 } else {
550 let parent = full_path
552 .parent()
553 .map(|p| p.to_path_buf())
554 .unwrap_or(full_path.clone());
555 let name = full_path
556 .file_name()
557 .map(|n| n.to_string_lossy().to_string())
558 .unwrap_or_default();
559 (parent, name)
560 };
561
562 if target_dir.is_dir() && target_dir != current_dir {
564 if let Some(prompt) = &mut self.active_window_mut().prompt {
566 prompt.input = filename.clone();
567 prompt.cursor_pos = prompt.input.len();
568 }
569 self.load_file_open_directory(target_dir);
570
571 if let Some(state) = &mut self.active_window_mut().file_open_state {
573 state.apply_filter(&filename);
574 }
575 return;
576 }
577 }
578
579 if let Some(state) = &mut self.active_window_mut().file_open_state {
580 state.apply_filter(&filter);
581 }
582 }
583
584 pub fn file_open_toggle_sort(&mut self, mode: SortMode) {
586 if let Some(state) = &mut self.active_window_mut().file_open_state {
587 state.set_sort_mode(mode);
588 }
589 }
590
591 pub fn file_open_toggle_hidden(&mut self) {
593 if let Some(state) = &mut self.active_window_mut().file_open_state {
594 let show_hidden = state.show_hidden;
595 state.show_hidden = !show_hidden;
596 let new_state = state.show_hidden;
597
598 let current_dir = state.current_dir.clone();
600 self.load_file_open_directory(current_dir);
601
602 let msg = if new_state {
604 "Showing hidden files"
605 } else {
606 "Hiding hidden files"
607 };
608 self.set_status_message(msg.to_string());
609 }
610 }
611
612 pub fn file_open_toggle_detect_encoding(&mut self) {
614 if let Some(state) = &mut self.active_window_mut().file_open_state {
615 state.toggle_detect_encoding();
616 let new_state = state.detect_encoding;
617
618 let msg = if new_state {
620 "Encoding auto-detection enabled"
621 } else {
622 "Encoding auto-detection disabled - will prompt for encoding"
623 };
624 self.set_status_message(msg.to_string());
625 }
626 }
627
628 pub fn handle_file_open_scroll(&mut self, delta: i32) -> bool {
631 if !self.is_file_open_active() {
632 return false;
633 }
634
635 let visible_rows = self
636 .active_window_mut()
637 .file_browser_layout
638 .as_ref()
639 .map(|l| l.visible_rows)
640 .unwrap_or(10);
641
642 if let Some(state) = &mut self.active_window_mut().file_open_state {
643 let total_entries = state.entries.len();
644 if total_entries <= visible_rows {
645 return true;
647 }
648
649 let max_scroll = total_entries.saturating_sub(visible_rows);
650
651 if delta < 0 {
652 let scroll_amount = (-delta) as usize;
654 state.scroll_offset = state.scroll_offset.saturating_sub(scroll_amount);
655 } else {
656 let scroll_amount = delta as usize;
658 state.scroll_offset = (state.scroll_offset + scroll_amount).min(max_scroll);
659 }
660 return true;
661 }
662
663 false
664 }
665
666 pub fn handle_file_open_click(&mut self, x: u16, y: u16) -> bool {
668 if !self.is_file_open_active() {
669 return false;
670 }
671
672 let layout = match &self.active_window_mut().file_browser_layout {
673 Some(l) => l.clone(),
674 None => return false,
675 };
676
677 if layout.is_in_list(x, y) {
679 let scroll_offset = self
680 .active_window_mut()
681 .file_open_state
682 .as_ref()
683 .map(|s| s.scroll_offset)
684 .unwrap_or(0);
685
686 if let Some(index) = layout.click_to_index(y, scroll_offset) {
687 let entry_name = self
689 .active_window_mut()
690 .file_open_state
691 .as_ref()
692 .and_then(|s| s.entries.get(index))
693 .map(|e| e.fs_entry.name.clone());
694
695 if let Some(state) = &mut self.active_window_mut().file_open_state {
696 state.active_section = FileOpenSection::Files;
697 if index < state.entries.len() {
698 state.selected_index = Some(index);
699 }
700 }
701
702 if let Some(name) = entry_name {
704 if let Some(prompt) = &mut self.active_window_mut().prompt {
705 prompt.input = name;
706 prompt.cursor_pos = prompt.input.len();
707 }
708 }
709 }
710 return true;
711 }
712
713 if layout.is_on_show_hidden_checkbox(x, y) {
715 self.file_open_toggle_hidden();
716 return true;
717 }
718
719 if layout.is_on_detect_encoding_checkbox(x, y) {
721 self.file_open_toggle_detect_encoding();
722 return true;
723 }
724
725 if layout.is_in_nav(x, y) {
727 let shortcut_labels: Vec<&str> = self
729 .active_window_mut()
730 .file_open_state
731 .as_ref()
732 .map(|s| s.shortcuts.iter().map(|sc| sc.label.as_str()).collect())
733 .unwrap_or_default();
734
735 if let Some(shortcut_idx) = layout.nav_shortcut_at(x, y, &shortcut_labels) {
736 let target_path = self
738 .active_window_mut()
739 .file_open_state
740 .as_ref()
741 .and_then(|s| s.shortcuts.get(shortcut_idx))
742 .map(|sc| sc.path.clone());
743
744 if let Some(path) = target_path {
745 if let Some(state) = &mut self.active_window_mut().file_open_state {
746 state.active_section = FileOpenSection::Navigation;
747 state.selected_shortcut = shortcut_idx;
748 }
749 self.file_open_navigate_to(path);
750 }
751 } else {
752 if let Some(state) = &mut self.active_window_mut().file_open_state {
754 state.active_section = FileOpenSection::Navigation;
755 }
756 }
757 return true;
758 }
759
760 if layout.is_in_header(x, y) {
762 if let Some(mode) = layout.header_column_at(x) {
763 self.file_open_toggle_sort(mode);
764 }
765 return true;
766 }
767
768 if layout.is_in_scrollbar(x, y) {
770 let rel_y = y.saturating_sub(layout.scrollbar_area.y) as usize;
772 let track_height = layout.scrollbar_area.height as usize;
773
774 if let Some(state) = &mut self.active_window_mut().file_open_state {
775 let total_items = state.entries.len();
776 let visible_items = layout.visible_rows;
777
778 if total_items > visible_items && track_height > 0 {
779 let max_scroll = total_items.saturating_sub(visible_items);
780 let click_ratio = rel_y as f64 / track_height as f64;
781 let new_offset = (click_ratio * max_scroll as f64) as usize;
782 state.scroll_offset = new_offset.min(max_scroll);
783 }
784 }
785 return true;
786 }
787
788 false
789 }
790
791 pub fn handle_file_open_double_click(&mut self, x: u16, y: u16) -> bool {
793 if !self.is_file_open_active() {
794 return false;
795 }
796
797 let layout = match &self.active_window_mut().file_browser_layout {
798 Some(l) => l.clone(),
799 None => return false,
800 };
801
802 if layout.is_in_list(x, y) {
804 if self.is_folder_open_mode() {
810 let selected_dir = self.active_window().file_open_state.as_ref().and_then(|s| {
811 s.selected_index
812 .and_then(|idx| s.entries.get(idx))
813 .filter(|e| e.fs_entry.is_dir())
814 .map(|e| e.fs_entry.path.clone())
815 });
816 if let Some(path) = selected_dir {
817 self.file_open_navigate_to(path);
818 return true;
819 }
820 }
821 self.file_open_confirm();
822 return true;
823 }
824
825 false
826 }
827
828 pub fn compute_file_browser_hover(&self, x: u16, y: u16) -> Option<super::types::HoverTarget> {
830 use super::types::HoverTarget;
831
832 let layout = self.active_window().file_browser_layout.as_ref()?;
833
834 if layout.is_on_show_hidden_checkbox(x, y) {
836 return Some(HoverTarget::FileBrowserShowHiddenCheckbox);
837 }
838
839 if layout.is_on_detect_encoding_checkbox(x, y) {
841 return Some(HoverTarget::FileBrowserDetectEncodingCheckbox);
842 }
843
844 if layout.is_in_nav(x, y) {
846 let shortcut_labels: Vec<&str> = self
847 .active_window()
848 .file_open_state
849 .as_ref()
850 .map(|s| s.shortcuts.iter().map(|sc| sc.label.as_str()).collect())
851 .unwrap_or_default();
852
853 if let Some(idx) = layout.nav_shortcut_at(x, y, &shortcut_labels) {
854 return Some(HoverTarget::FileBrowserNavShortcut(idx));
855 }
856 }
857
858 if layout.is_in_header(x, y) {
860 if let Some(mode) = layout.header_column_at(x) {
861 return Some(HoverTarget::FileBrowserHeader(mode));
862 }
863 }
864
865 if layout.is_in_list(x, y) {
867 let scroll_offset = self
868 .active_window()
869 .file_open_state
870 .as_ref()
871 .map(|s| s.scroll_offset)
872 .unwrap_or(0);
873
874 if let Some(idx) = layout.click_to_index(y, scroll_offset) {
875 let total_entries = self
876 .active_window()
877 .file_open_state
878 .as_ref()
879 .map(|s| s.entries.len())
880 .unwrap_or(0);
881
882 if idx < total_entries {
883 return Some(HoverTarget::FileBrowserEntry(idx));
884 }
885 }
886 }
887
888 if layout.is_in_scrollbar(x, y) {
890 return Some(HoverTarget::FileBrowserScrollbar);
891 }
892
893 None
894 }
895}