fresh/app/
file_open_input.rs1use super::file_open::{FileOpenSection, SortMode};
7use super::Editor;
8use crate::input::keybindings::Action;
9use crate::primitives::path_utils::expand_tilde;
10use crate::view::prompt::PromptType;
11use rust_i18n::t;
12
13impl Editor {
14 pub fn is_file_open_active(&self) -> bool {
16 self.prompt
17 .as_ref()
18 .map(|p| {
19 matches!(
20 p.prompt_type,
21 PromptType::OpenFile | PromptType::SwitchProject | PromptType::SaveFileAs
22 )
23 })
24 .unwrap_or(false)
25 && self.file_open_state.is_some()
26 }
27
28 fn is_folder_open_mode(&self) -> bool {
30 self.prompt
31 .as_ref()
32 .map(|p| p.prompt_type == PromptType::SwitchProject)
33 .unwrap_or(false)
34 }
35
36 fn is_save_mode(&self) -> bool {
38 self.prompt
39 .as_ref()
40 .map(|p| p.prompt_type == PromptType::SaveFileAs)
41 .unwrap_or(false)
42 }
43
44 pub fn handle_file_open_action(&mut self, action: &Action) -> bool {
47 if !self.is_file_open_active() {
48 return false;
49 }
50
51 match action {
52 Action::PromptSelectPrev => {
54 if let Some(state) = &mut self.file_open_state {
55 state.select_prev();
56 }
57 true
58 }
59 Action::PromptSelectNext => {
60 if let Some(state) = &mut self.file_open_state {
61 state.select_next();
62 }
63 true
64 }
65 Action::PromptPageUp => {
66 if let Some(state) = &mut self.file_open_state {
67 state.page_up(10);
68 }
69 true
70 }
71 Action::PromptPageDown => {
72 if let Some(state) = &mut self.file_open_state {
73 state.page_down(10);
74 }
75 true
76 }
77 Action::PromptConfirm => {
82 self.file_open_confirm();
83 true
84 }
85
86 Action::PromptAcceptSuggestion => {
88 let selected_info = self.file_open_state.as_ref().and_then(|s| {
90 s.selected_index
91 .and_then(|idx| s.entries.get(idx))
92 .map(|e| {
93 (
94 e.fs_entry.name.clone(),
95 e.fs_entry.is_dir(),
96 e.fs_entry.path.clone(),
97 )
98 })
99 });
100
101 if let Some((name, is_dir, path)) = selected_info {
102 if is_dir {
103 self.file_open_navigate_to(path);
105 } else {
106 if let Some(prompt) = &mut self.prompt {
108 prompt.input = name;
109 prompt.cursor_pos = prompt.input.len();
110 }
111 self.update_file_open_filter();
113 }
114 }
115 true
116 }
117
118 Action::PromptBackspace => {
120 let filter_empty = self
121 .file_open_state
122 .as_ref()
123 .map(|s| s.filter.is_empty())
124 .unwrap_or(true);
125 let prompt_empty = self
126 .prompt
127 .as_ref()
128 .map(|p| p.input.is_empty())
129 .unwrap_or(true);
130
131 if filter_empty && prompt_empty {
132 self.file_open_go_parent();
133 true
134 } else {
135 false
137 }
138 }
139
140 Action::PromptCancel => {
142 self.cancel_prompt();
143 self.file_open_state = None;
144 true
145 }
146
147 Action::FileBrowserToggleHidden => {
149 self.file_open_toggle_hidden();
150 true
151 }
152
153 _ => false,
155 }
156 }
157
158 fn file_open_confirm(&mut self) {
160 let is_folder_mode = self.is_folder_open_mode();
161 let is_save_mode = self.is_save_mode();
162 let prompt_input = self
163 .prompt
164 .as_ref()
165 .map(|p| p.input.clone())
166 .unwrap_or_default();
167
168 let current_dir = self
170 .file_open_state
171 .as_ref()
172 .map(|s| s.current_dir.clone())
173 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
174
175 if !prompt_input.is_empty() {
177 let tilde_expanded = expand_tilde(&prompt_input);
179 let expanded_path = if tilde_expanded.is_absolute() {
180 tilde_expanded
181 } else {
182 current_dir.join(&prompt_input)
184 };
185
186 if expanded_path.is_dir() {
187 if is_folder_mode {
188 self.file_open_select_folder(expanded_path);
190 } else {
191 self.file_open_navigate_to(expanded_path);
192 }
193 return;
194 } else if is_save_mode {
195 self.file_open_save_file(expanded_path);
197 return;
198 } else if expanded_path.is_file() && !is_folder_mode {
199 self.file_open_open_file(expanded_path);
202 return;
203 } else if !is_folder_mode && Self::should_create_new_file(&prompt_input) {
204 self.file_open_create_new_file(expanded_path);
207 return;
208 }
209 }
213
214 let (path, is_dir) = {
216 let state = match &self.file_open_state {
217 Some(s) => s,
218 None => {
219 if is_folder_mode {
221 self.file_open_select_folder(current_dir);
222 }
223 return;
224 }
225 };
226
227 let path = match state.get_selected_path() {
228 Some(p) => p,
229 None => {
230 if is_save_mode {
232 self.set_status_message(t!("file.save_as_no_filename").to_string());
233 return;
234 }
235 if is_folder_mode {
237 self.file_open_select_folder(current_dir);
238 }
239 return;
240 }
241 };
242
243 (path, state.selected_is_dir())
244 };
245
246 if is_dir {
247 if is_folder_mode {
248 self.file_open_select_folder(path);
250 } else {
251 self.file_open_navigate_to(path);
253 }
254 } else if is_save_mode {
255 self.file_open_save_file(path);
257 } else if !is_folder_mode {
258 self.file_open_open_file(path);
260 }
261 }
263
264 fn file_open_select_folder(&mut self, path: std::path::PathBuf) {
266 self.file_open_state = None;
268 self.prompt = None;
269
270 self.change_working_dir(path);
272 }
273
274 fn file_open_navigate_to(&mut self, path: std::path::PathBuf) {
276 if let Some(prompt) = self.prompt.as_mut() {
278 prompt.input.clear();
279 prompt.cursor_pos = 0;
280 }
281
282 self.load_file_open_directory(path);
284 }
285
286 fn file_open_open_file(&mut self, path: std::path::PathBuf) {
288 self.file_open_state = None;
290 self.prompt = None;
291
292 self.key_context = crate::input::keybindings::KeyContext::Normal;
295
296 tracing::info!("[SYNTAX DEBUG] file_open_dialog opening file: {:?}", path);
298 if let Err(e) = self.open_file(&path) {
299 self.set_status_message(t!("file.error_opening", error = e.to_string()).to_string());
300 } else {
301 self.set_status_message(
302 t!("file.opened", path = path.display().to_string()).to_string(),
303 );
304 }
305 }
306
307 fn file_open_create_new_file(&mut self, path: std::path::PathBuf) {
309 self.file_open_state = None;
311 self.prompt = None;
312
313 self.key_context = crate::input::keybindings::KeyContext::Normal;
316
317 if let Err(e) = self.open_file(&path) {
319 self.set_status_message(t!("file.error_opening", error = e.to_string()).to_string());
320 } else {
321 self.set_status_message(
322 t!("file.created_new", path = path.display().to_string()).to_string(),
323 );
324 }
325 }
326
327 fn file_open_save_file(&mut self, path: std::path::PathBuf) {
329 use crate::view::prompt::PromptType as PT;
330
331 self.file_open_state = None;
333 self.prompt = None;
334
335 let current_file_path = self
337 .active_state()
338 .buffer
339 .file_path()
340 .map(|p| p.to_path_buf());
341 let is_different_file = current_file_path.as_ref() != Some(&path);
342
343 if is_different_file && path.is_file() {
344 let filename = path
346 .file_name()
347 .map(|n| n.to_string_lossy().to_string())
348 .unwrap_or_else(|| path.display().to_string());
349 self.start_prompt(
350 t!("buffer.overwrite_confirm", name = &filename).to_string(),
351 PT::ConfirmOverwriteFile { path },
352 );
353 return;
354 }
355
356 self.perform_save_file_as(path);
358 }
359
360 fn should_create_new_file(input: &str) -> bool {
363 let has_extension = input.rfind('.').is_some_and(|pos| {
366 let after_dot = &input[pos + 1..];
368 !after_dot.is_empty() && !after_dot.contains('/') && !after_dot.contains('\\')
369 });
370
371 let has_path_separator = input.contains('/') || input.contains('\\');
372
373 has_extension || has_path_separator
374 }
375
376 fn file_open_go_parent(&mut self) {
378 let parent = self
379 .file_open_state
380 .as_ref()
381 .and_then(|s| s.current_dir.parent())
382 .map(|p| p.to_path_buf());
383
384 if let Some(parent_path) = parent {
385 self.file_open_navigate_to(parent_path);
386 }
387 }
388
389 pub fn update_file_open_filter(&mut self) {
391 if !self.is_file_open_active() {
392 return;
393 }
394
395 let filter = self
396 .prompt
397 .as_ref()
398 .map(|p| p.input.clone())
399 .unwrap_or_default();
400
401 if filter.contains('/') {
404 let current_dir = self
405 .file_open_state
406 .as_ref()
407 .map(|s| s.current_dir.clone())
408 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
409
410 let tilde_expanded = expand_tilde(&filter);
413 let full_path = if tilde_expanded.is_absolute() {
414 tilde_expanded
415 } else {
416 current_dir.join(&filter)
417 };
418
419 let (target_dir, filename) = if filter.ends_with('/') {
421 (full_path.clone(), String::new())
423 } else {
424 let parent = full_path
426 .parent()
427 .map(|p| p.to_path_buf())
428 .unwrap_or(full_path.clone());
429 let name = full_path
430 .file_name()
431 .map(|n| n.to_string_lossy().to_string())
432 .unwrap_or_default();
433 (parent, name)
434 };
435
436 if target_dir.is_dir() && target_dir != current_dir {
438 if let Some(prompt) = &mut self.prompt {
440 prompt.input = filename.clone();
441 prompt.cursor_pos = prompt.input.len();
442 }
443 self.load_file_open_directory(target_dir);
444
445 if let Some(state) = &mut self.file_open_state {
447 state.apply_filter(&filename);
448 }
449 return;
450 }
451 }
452
453 if let Some(state) = &mut self.file_open_state {
454 state.apply_filter(&filter);
455 }
456 }
457
458 pub fn file_open_toggle_sort(&mut self, mode: SortMode) {
460 if let Some(state) = &mut self.file_open_state {
461 state.set_sort_mode(mode);
462 }
463 }
464
465 pub fn file_open_toggle_hidden(&mut self) {
467 if let Some(state) = &mut self.file_open_state {
468 let show_hidden = state.show_hidden;
469 state.show_hidden = !show_hidden;
470 let new_state = state.show_hidden;
471
472 let current_dir = state.current_dir.clone();
474 self.load_file_open_directory(current_dir);
475
476 let msg = if new_state {
478 "Showing hidden files"
479 } else {
480 "Hiding hidden files"
481 };
482 self.set_status_message(msg.to_string());
483 }
484 }
485
486 pub fn handle_file_open_scroll(&mut self, delta: i32) -> bool {
489 if !self.is_file_open_active() {
490 return false;
491 }
492
493 let visible_rows = self
494 .file_browser_layout
495 .as_ref()
496 .map(|l| l.visible_rows)
497 .unwrap_or(10);
498
499 if let Some(state) = &mut self.file_open_state {
500 let total_entries = state.entries.len();
501 if total_entries <= visible_rows {
502 return true;
504 }
505
506 let max_scroll = total_entries.saturating_sub(visible_rows);
507
508 if delta < 0 {
509 let scroll_amount = (-delta) as usize;
511 state.scroll_offset = state.scroll_offset.saturating_sub(scroll_amount);
512 } else {
513 let scroll_amount = delta as usize;
515 state.scroll_offset = (state.scroll_offset + scroll_amount).min(max_scroll);
516 }
517 return true;
518 }
519
520 false
521 }
522
523 pub fn handle_file_open_click(&mut self, x: u16, y: u16) -> bool {
525 if !self.is_file_open_active() {
526 return false;
527 }
528
529 let layout = match &self.file_browser_layout {
530 Some(l) => l.clone(),
531 None => return false,
532 };
533
534 if layout.is_in_list(x, y) {
536 let scroll_offset = self
537 .file_open_state
538 .as_ref()
539 .map(|s| s.scroll_offset)
540 .unwrap_or(0);
541
542 if let Some(index) = layout.click_to_index(y, scroll_offset) {
543 let entry_name = self
545 .file_open_state
546 .as_ref()
547 .and_then(|s| s.entries.get(index))
548 .map(|e| e.fs_entry.name.clone());
549
550 if let Some(state) = &mut self.file_open_state {
551 state.active_section = FileOpenSection::Files;
552 if index < state.entries.len() {
553 state.selected_index = Some(index);
554 }
555 }
556
557 if let Some(name) = entry_name {
559 if let Some(prompt) = &mut self.prompt {
560 prompt.input = name;
561 prompt.cursor_pos = prompt.input.len();
562 }
563 }
564 }
565 return true;
566 }
567
568 if layout.is_on_show_hidden_checkbox(x, y) {
570 self.file_open_toggle_hidden();
571 return true;
572 }
573
574 if layout.is_in_nav(x, y) {
576 let shortcut_labels: Vec<&str> = self
578 .file_open_state
579 .as_ref()
580 .map(|s| s.shortcuts.iter().map(|sc| sc.label.as_str()).collect())
581 .unwrap_or_default();
582
583 if let Some(shortcut_idx) = layout.nav_shortcut_at(x, y, &shortcut_labels) {
584 let target_path = self
586 .file_open_state
587 .as_ref()
588 .and_then(|s| s.shortcuts.get(shortcut_idx))
589 .map(|sc| sc.path.clone());
590
591 if let Some(path) = target_path {
592 if let Some(state) = &mut self.file_open_state {
593 state.active_section = FileOpenSection::Navigation;
594 state.selected_shortcut = shortcut_idx;
595 }
596 self.file_open_navigate_to(path);
597 }
598 } else {
599 if let Some(state) = &mut self.file_open_state {
601 state.active_section = FileOpenSection::Navigation;
602 }
603 }
604 return true;
605 }
606
607 if layout.is_in_header(x, y) {
609 if let Some(mode) = layout.header_column_at(x) {
610 self.file_open_toggle_sort(mode);
611 }
612 return true;
613 }
614
615 if layout.is_in_scrollbar(x, y) {
617 let rel_y = y.saturating_sub(layout.scrollbar_area.y) as usize;
619 let track_height = layout.scrollbar_area.height as usize;
620
621 if let Some(state) = &mut self.file_open_state {
622 let total_items = state.entries.len();
623 let visible_items = layout.visible_rows;
624
625 if total_items > visible_items && track_height > 0 {
626 let max_scroll = total_items.saturating_sub(visible_items);
627 let click_ratio = rel_y as f64 / track_height as f64;
628 let new_offset = (click_ratio * max_scroll as f64) as usize;
629 state.scroll_offset = new_offset.min(max_scroll);
630 }
631 }
632 return true;
633 }
634
635 false
636 }
637
638 pub fn handle_file_open_double_click(&mut self, x: u16, y: u16) -> bool {
640 if !self.is_file_open_active() {
641 return false;
642 }
643
644 let layout = match &self.file_browser_layout {
645 Some(l) => l.clone(),
646 None => return false,
647 };
648
649 if layout.is_in_list(x, y) {
651 self.file_open_confirm();
652 return true;
653 }
654
655 false
656 }
657
658 pub fn compute_file_browser_hover(&self, x: u16, y: u16) -> Option<super::types::HoverTarget> {
660 use super::types::HoverTarget;
661
662 let layout = self.file_browser_layout.as_ref()?;
663
664 if layout.is_on_show_hidden_checkbox(x, y) {
666 return Some(HoverTarget::FileBrowserShowHiddenCheckbox);
667 }
668
669 if layout.is_in_nav(x, y) {
671 let shortcut_labels: Vec<&str> = self
672 .file_open_state
673 .as_ref()
674 .map(|s| s.shortcuts.iter().map(|sc| sc.label.as_str()).collect())
675 .unwrap_or_default();
676
677 if let Some(idx) = layout.nav_shortcut_at(x, y, &shortcut_labels) {
678 return Some(HoverTarget::FileBrowserNavShortcut(idx));
679 }
680 }
681
682 if layout.is_in_header(x, y) {
684 if let Some(mode) = layout.header_column_at(x) {
685 return Some(HoverTarget::FileBrowserHeader(mode));
686 }
687 }
688
689 if layout.is_in_list(x, y) {
691 let scroll_offset = self
692 .file_open_state
693 .as_ref()
694 .map(|s| s.scroll_offset)
695 .unwrap_or(0);
696
697 if let Some(idx) = layout.click_to_index(y, scroll_offset) {
698 let total_entries = self
699 .file_open_state
700 .as_ref()
701 .map(|s| s.entries.len())
702 .unwrap_or(0);
703
704 if idx < total_entries {
705 return Some(HoverTarget::FileBrowserEntry(idx));
706 }
707 }
708 }
709
710 if layout.is_in_scrollbar(x, y) {
712 return Some(HoverTarget::FileBrowserScrollbar);
713 }
714
715 None
716 }
717}