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 self.file_open_state = None;
465 self.prompt = None;
466
467 self.save_file_as_with_checks(path);
468 }
469
470 fn should_create_new_file(input: &str) -> bool {
473 let has_extension = input.rfind('.').is_some_and(|pos| {
476 let after_dot = &input[pos + 1..];
478 !after_dot.is_empty() && !after_dot.contains('/') && !after_dot.contains('\\')
479 });
480
481 let has_path_separator = input.contains('/') || input.contains('\\');
482
483 has_extension || has_path_separator
484 }
485
486 fn file_open_go_parent(&mut self) {
488 let parent = self
489 .file_open_state
490 .as_ref()
491 .and_then(|s| s.current_dir.parent())
492 .map(|p| p.to_path_buf());
493
494 if let Some(parent_path) = parent {
495 self.file_open_navigate_to(parent_path);
496 }
497 }
498
499 pub fn update_file_open_filter(&mut self) {
501 if !self.is_file_open_active() {
502 return;
503 }
504
505 let filter = self
506 .prompt
507 .as_ref()
508 .map(|p| p.input.clone())
509 .unwrap_or_default();
510
511 if filter.contains('/') {
514 let current_dir = self
515 .file_open_state
516 .as_ref()
517 .map(|s| s.current_dir.clone())
518 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
519
520 let tilde_expanded = expand_tilde(&filter);
523 let full_path = if tilde_expanded.is_absolute() {
524 tilde_expanded
525 } else {
526 current_dir.join(&filter)
527 };
528
529 let (target_dir, filename) = if filter.ends_with('/') {
531 (full_path.clone(), String::new())
533 } else {
534 let parent = full_path
536 .parent()
537 .map(|p| p.to_path_buf())
538 .unwrap_or(full_path.clone());
539 let name = full_path
540 .file_name()
541 .map(|n| n.to_string_lossy().to_string())
542 .unwrap_or_default();
543 (parent, name)
544 };
545
546 if target_dir.is_dir() && target_dir != current_dir {
548 if let Some(prompt) = &mut self.prompt {
550 prompt.input = filename.clone();
551 prompt.cursor_pos = prompt.input.len();
552 }
553 self.load_file_open_directory(target_dir);
554
555 if let Some(state) = &mut self.file_open_state {
557 state.apply_filter(&filename);
558 }
559 return;
560 }
561 }
562
563 if let Some(state) = &mut self.file_open_state {
564 state.apply_filter(&filter);
565 }
566 }
567
568 pub fn file_open_toggle_sort(&mut self, mode: SortMode) {
570 if let Some(state) = &mut self.file_open_state {
571 state.set_sort_mode(mode);
572 }
573 }
574
575 pub fn file_open_toggle_hidden(&mut self) {
577 if let Some(state) = &mut self.file_open_state {
578 let show_hidden = state.show_hidden;
579 state.show_hidden = !show_hidden;
580 let new_state = state.show_hidden;
581
582 let current_dir = state.current_dir.clone();
584 self.load_file_open_directory(current_dir);
585
586 let msg = if new_state {
588 "Showing hidden files"
589 } else {
590 "Hiding hidden files"
591 };
592 self.set_status_message(msg.to_string());
593 }
594 }
595
596 pub fn file_open_toggle_detect_encoding(&mut self) {
598 if let Some(state) = &mut self.file_open_state {
599 state.toggle_detect_encoding();
600 let new_state = state.detect_encoding;
601
602 let msg = if new_state {
604 "Encoding auto-detection enabled"
605 } else {
606 "Encoding auto-detection disabled - will prompt for encoding"
607 };
608 self.set_status_message(msg.to_string());
609 }
610 }
611
612 pub fn handle_file_open_scroll(&mut self, delta: i32) -> bool {
615 if !self.is_file_open_active() {
616 return false;
617 }
618
619 let visible_rows = self
620 .file_browser_layout
621 .as_ref()
622 .map(|l| l.visible_rows)
623 .unwrap_or(10);
624
625 if let Some(state) = &mut self.file_open_state {
626 let total_entries = state.entries.len();
627 if total_entries <= visible_rows {
628 return true;
630 }
631
632 let max_scroll = total_entries.saturating_sub(visible_rows);
633
634 if delta < 0 {
635 let scroll_amount = (-delta) as usize;
637 state.scroll_offset = state.scroll_offset.saturating_sub(scroll_amount);
638 } else {
639 let scroll_amount = delta as usize;
641 state.scroll_offset = (state.scroll_offset + scroll_amount).min(max_scroll);
642 }
643 return true;
644 }
645
646 false
647 }
648
649 pub fn handle_file_open_click(&mut self, x: u16, y: u16) -> bool {
651 if !self.is_file_open_active() {
652 return false;
653 }
654
655 let layout = match &self.file_browser_layout {
656 Some(l) => l.clone(),
657 None => return false,
658 };
659
660 if layout.is_in_list(x, y) {
662 let scroll_offset = self
663 .file_open_state
664 .as_ref()
665 .map(|s| s.scroll_offset)
666 .unwrap_or(0);
667
668 if let Some(index) = layout.click_to_index(y, scroll_offset) {
669 let entry_name = self
671 .file_open_state
672 .as_ref()
673 .and_then(|s| s.entries.get(index))
674 .map(|e| e.fs_entry.name.clone());
675
676 if let Some(state) = &mut self.file_open_state {
677 state.active_section = FileOpenSection::Files;
678 if index < state.entries.len() {
679 state.selected_index = Some(index);
680 }
681 }
682
683 if let Some(name) = entry_name {
685 if let Some(prompt) = &mut self.prompt {
686 prompt.input = name;
687 prompt.cursor_pos = prompt.input.len();
688 }
689 }
690 }
691 return true;
692 }
693
694 if layout.is_on_show_hidden_checkbox(x, y) {
696 self.file_open_toggle_hidden();
697 return true;
698 }
699
700 if layout.is_on_detect_encoding_checkbox(x, y) {
702 self.file_open_toggle_detect_encoding();
703 return true;
704 }
705
706 if layout.is_in_nav(x, y) {
708 let shortcut_labels: Vec<&str> = self
710 .file_open_state
711 .as_ref()
712 .map(|s| s.shortcuts.iter().map(|sc| sc.label.as_str()).collect())
713 .unwrap_or_default();
714
715 if let Some(shortcut_idx) = layout.nav_shortcut_at(x, y, &shortcut_labels) {
716 let target_path = self
718 .file_open_state
719 .as_ref()
720 .and_then(|s| s.shortcuts.get(shortcut_idx))
721 .map(|sc| sc.path.clone());
722
723 if let Some(path) = target_path {
724 if let Some(state) = &mut self.file_open_state {
725 state.active_section = FileOpenSection::Navigation;
726 state.selected_shortcut = shortcut_idx;
727 }
728 self.file_open_navigate_to(path);
729 }
730 } else {
731 if let Some(state) = &mut self.file_open_state {
733 state.active_section = FileOpenSection::Navigation;
734 }
735 }
736 return true;
737 }
738
739 if layout.is_in_header(x, y) {
741 if let Some(mode) = layout.header_column_at(x) {
742 self.file_open_toggle_sort(mode);
743 }
744 return true;
745 }
746
747 if layout.is_in_scrollbar(x, y) {
749 let rel_y = y.saturating_sub(layout.scrollbar_area.y) as usize;
751 let track_height = layout.scrollbar_area.height as usize;
752
753 if let Some(state) = &mut self.file_open_state {
754 let total_items = state.entries.len();
755 let visible_items = layout.visible_rows;
756
757 if total_items > visible_items && track_height > 0 {
758 let max_scroll = total_items.saturating_sub(visible_items);
759 let click_ratio = rel_y as f64 / track_height as f64;
760 let new_offset = (click_ratio * max_scroll as f64) as usize;
761 state.scroll_offset = new_offset.min(max_scroll);
762 }
763 }
764 return true;
765 }
766
767 false
768 }
769
770 pub fn handle_file_open_double_click(&mut self, x: u16, y: u16) -> bool {
772 if !self.is_file_open_active() {
773 return false;
774 }
775
776 let layout = match &self.file_browser_layout {
777 Some(l) => l.clone(),
778 None => return false,
779 };
780
781 if layout.is_in_list(x, y) {
783 self.file_open_confirm();
784 return true;
785 }
786
787 false
788 }
789
790 pub fn compute_file_browser_hover(&self, x: u16, y: u16) -> Option<super::types::HoverTarget> {
792 use super::types::HoverTarget;
793
794 let layout = self.file_browser_layout.as_ref()?;
795
796 if layout.is_on_show_hidden_checkbox(x, y) {
798 return Some(HoverTarget::FileBrowserShowHiddenCheckbox);
799 }
800
801 if layout.is_on_detect_encoding_checkbox(x, y) {
803 return Some(HoverTarget::FileBrowserDetectEncodingCheckbox);
804 }
805
806 if layout.is_in_nav(x, y) {
808 let shortcut_labels: Vec<&str> = self
809 .file_open_state
810 .as_ref()
811 .map(|s| s.shortcuts.iter().map(|sc| sc.label.as_str()).collect())
812 .unwrap_or_default();
813
814 if let Some(idx) = layout.nav_shortcut_at(x, y, &shortcut_labels) {
815 return Some(HoverTarget::FileBrowserNavShortcut(idx));
816 }
817 }
818
819 if layout.is_in_header(x, y) {
821 if let Some(mode) = layout.header_column_at(x) {
822 return Some(HoverTarget::FileBrowserHeader(mode));
823 }
824 }
825
826 if layout.is_in_list(x, y) {
828 let scroll_offset = self
829 .file_open_state
830 .as_ref()
831 .map(|s| s.scroll_offset)
832 .unwrap_or(0);
833
834 if let Some(idx) = layout.click_to_index(y, scroll_offset) {
835 let total_entries = self
836 .file_open_state
837 .as_ref()
838 .map(|s| s.entries.len())
839 .unwrap_or(0);
840
841 if idx < total_entries {
842 return Some(HoverTarget::FileBrowserEntry(idx));
843 }
844 }
845 }
846
847 if layout.is_in_scrollbar(x, y) {
849 return Some(HoverTarget::FileBrowserScrollbar);
850 }
851
852 None
853 }
854}