1pub mod file_finder_settings;
2
3#[cfg(test)]
4mod open_path_prompt_tests;
5
6use file_finder_settings::FileFinderSettings;
7use file_icons::FileIcons;
8use futures::channel::oneshot;
9use fuzzy::{CharBag, StringMatch, StringMatchCandidate};
10use gpui::{HighlightStyle, StyledText, Task};
11use picker::{Picker, PickerDelegate};
12use project::{DirectoryItem, DirectoryLister};
13use settings::Settings;
14use std::{
15 path::{self, Path, PathBuf},
16 sync::{
17 Arc,
18 atomic::{self, AtomicBool},
19 },
20};
21use ui::{Context, LabelLike, ListItem, Window};
22use ui::{HighlightedLabel, ListItemSpacing, prelude::*};
23use util::{
24 maybe,
25 paths::{PathStyle, compare_paths},
26};
27use workspace::Workspace;
28
29pub struct OpenPathPrompt;
30
31pub struct OpenPathDelegate {
32 tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
33 lister: DirectoryLister,
34 selected_index: usize,
35 directory_state: DirectoryState,
36 string_matches: Vec<StringMatch>,
37 cancel_flag: Arc<AtomicBool>,
38 should_dismiss: bool,
39 prompt_root: String,
40 path_style: PathStyle,
41 replace_prompt: Task<()>,
42 render_footer:
43 Arc<dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static>,
44 hidden_entries: bool,
45}
46
47impl OpenPathDelegate {
48 pub fn new(
49 tx: oneshot::Sender<Option<Vec<PathBuf>>>,
50 lister: DirectoryLister,
51 creating_path: bool,
52 cx: &App,
53 ) -> Self {
54 let path_style = lister.path_style(cx);
55 Self {
56 tx: Some(tx),
57 lister,
58 selected_index: 0,
59 directory_state: DirectoryState::None {
60 create: creating_path,
61 },
62 string_matches: Vec::new(),
63 cancel_flag: Arc::new(AtomicBool::new(false)),
64 should_dismiss: true,
65 prompt_root: match path_style {
66 PathStyle::Posix => "/".to_string(),
67 PathStyle::Windows => "C:\\".to_string(),
68 },
69 path_style,
70 replace_prompt: Task::ready(()),
71 render_footer: Arc::new(|_, _| None),
72 hidden_entries: false,
73 }
74 }
75
76 pub fn with_footer(
77 mut self,
78 footer: Arc<
79 dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static,
80 >,
81 ) -> Self {
82 self.render_footer = footer;
83 self
84 }
85
86 pub fn show_hidden(mut self) -> Self {
87 self.hidden_entries = true;
88 self
89 }
90 fn get_entry(&self, selected_match_index: usize) -> Option<CandidateInfo> {
91 match &self.directory_state {
92 DirectoryState::List { entries, .. } => {
93 let id = self.string_matches.get(selected_match_index)?.candidate_id;
94 entries.iter().find(|entry| entry.path.id == id).cloned()
95 }
96 DirectoryState::Create {
97 user_input,
98 entries,
99 ..
100 } => {
101 let mut i = selected_match_index;
102 if let Some(user_input) = user_input
103 && (!user_input.exists || !user_input.is_dir)
104 {
105 if i == 0 {
106 return Some(CandidateInfo {
107 path: user_input.file.clone(),
108 is_dir: false,
109 });
110 } else {
111 i -= 1;
112 }
113 }
114 let id = self.string_matches.get(i)?.candidate_id;
115 entries.iter().find(|entry| entry.path.id == id).cloned()
116 }
117 DirectoryState::None { .. } => None,
118 }
119 }
120
121 #[cfg(any(test, feature = "test-support"))]
122 pub fn collect_match_candidates(&self) -> Vec<String> {
123 match &self.directory_state {
124 DirectoryState::List { entries, .. } => self
125 .string_matches
126 .iter()
127 .filter_map(|string_match| {
128 entries
129 .iter()
130 .find(|entry| entry.path.id == string_match.candidate_id)
131 .map(|candidate| candidate.path.string.clone())
132 })
133 .collect(),
134 DirectoryState::Create {
135 user_input,
136 entries,
137 ..
138 } => user_input
139 .iter()
140 .filter(|user_input| !user_input.exists || !user_input.is_dir)
141 .map(|user_input| user_input.file.string.clone())
142 .chain(self.string_matches.iter().filter_map(|string_match| {
143 entries
144 .iter()
145 .find(|entry| entry.path.id == string_match.candidate_id)
146 .map(|candidate| candidate.path.string.clone())
147 }))
148 .collect(),
149 DirectoryState::None { .. } => Vec::new(),
150 }
151 }
152
153 fn current_dir(&self) -> &'static str {
154 match self.path_style {
155 PathStyle::Posix => "./",
156 PathStyle::Windows => ".\\",
157 }
158 }
159}
160
161#[derive(Debug)]
162enum DirectoryState {
163 List {
164 parent_path: String,
165 entries: Vec<CandidateInfo>,
166 error: Option<SharedString>,
167 },
168 Create {
169 parent_path: String,
170 user_input: Option<UserInput>,
171 entries: Vec<CandidateInfo>,
172 },
173 None {
174 create: bool,
175 },
176}
177
178#[derive(Debug, Clone)]
179struct UserInput {
180 file: StringMatchCandidate,
181 exists: bool,
182 is_dir: bool,
183}
184
185#[derive(Debug, Clone)]
186struct CandidateInfo {
187 path: StringMatchCandidate,
188 is_dir: bool,
189}
190
191impl OpenPathPrompt {
192 pub fn register(
193 workspace: &mut Workspace,
194 _window: Option<&mut Window>,
195 _: &mut Context<Workspace>,
196 ) {
197 workspace.set_prompt_for_open_path(Box::new(|workspace, lister, window, cx| {
198 let (tx, rx) = futures::channel::oneshot::channel();
199 Self::prompt_for_open_path(workspace, lister, false, None, tx, window, cx);
200 rx
201 }));
202 }
203
204 pub fn register_new_path(
205 workspace: &mut Workspace,
206 _window: Option<&mut Window>,
207 _: &mut Context<Workspace>,
208 ) {
209 workspace.set_prompt_for_new_path(Box::new(
210 |workspace, lister, suggested_name, window, cx| {
211 let (tx, rx) = futures::channel::oneshot::channel();
212 Self::prompt_for_new_path(workspace, lister, suggested_name, tx, window, cx);
213 rx
214 },
215 ));
216 }
217
218 fn prompt_for_open_path(
219 workspace: &mut Workspace,
220 lister: DirectoryLister,
221 creating_path: bool,
222 suggested_name: Option<String>,
223 tx: oneshot::Sender<Option<Vec<PathBuf>>>,
224 window: &mut Window,
225 cx: &mut Context<Workspace>,
226 ) {
227 workspace.toggle_modal(window, cx, |window, cx| {
228 let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, cx);
229 let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
230 let mut query = lister.default_query(cx);
231 if let Some(suggested_name) = suggested_name {
232 query.push_str(&suggested_name);
233 }
234 picker.set_query(&query, window, cx);
235 picker
236 });
237 }
238
239 fn prompt_for_new_path(
240 workspace: &mut Workspace,
241 lister: DirectoryLister,
242 suggested_name: Option<String>,
243 tx: oneshot::Sender<Option<Vec<PathBuf>>>,
244 window: &mut Window,
245 cx: &mut Context<Workspace>,
246 ) {
247 Self::prompt_for_open_path(workspace, lister, true, suggested_name, tx, window, cx);
248 }
249}
250
251impl PickerDelegate for OpenPathDelegate {
252 type ListItem = ui::ListItem;
253
254 fn match_count(&self) -> usize {
255 let user_input = if let DirectoryState::Create { user_input, .. } = &self.directory_state {
256 user_input
257 .as_ref()
258 .filter(|input| !input.exists || !input.is_dir)
259 .into_iter()
260 .count()
261 } else {
262 0
263 };
264 self.string_matches.len() + user_input
265 }
266
267 fn selected_index(&self) -> usize {
268 self.selected_index
269 }
270
271 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
272 self.selected_index = ix;
273 cx.notify();
274 }
275
276 fn update_matches(
277 &mut self,
278 query: String,
279 window: &mut Window,
280 cx: &mut Context<Picker<Self>>,
281 ) -> Task<()> {
282 let lister = &self.lister;
283 let input_is_empty = query.is_empty();
284 let (dir, suffix) = get_dir_and_suffix(query, self.path_style);
285
286 let query = match &self.directory_state {
287 DirectoryState::List { parent_path, .. } => {
288 if parent_path == &dir {
289 None
290 } else {
291 Some(lister.list_directory(dir.clone(), cx))
292 }
293 }
294 DirectoryState::Create {
295 parent_path,
296 user_input,
297 ..
298 } => {
299 if parent_path == &dir
300 && user_input.as_ref().map(|input| &input.file.string) == Some(&suffix)
301 {
302 None
303 } else {
304 Some(lister.list_directory(dir.clone(), cx))
305 }
306 }
307 DirectoryState::None { .. } => Some(lister.list_directory(dir.clone(), cx)),
308 };
309 self.cancel_flag.store(true, atomic::Ordering::Release);
310 self.cancel_flag = Arc::new(AtomicBool::new(false));
311 let cancel_flag = self.cancel_flag.clone();
312 let hidden_entries = self.hidden_entries;
313 let parent_path_is_root = self.prompt_root == dir;
314 let current_dir = self.current_dir();
315 cx.spawn_in(window, async move |this, cx| {
316 if let Some(query) = query {
317 let paths = query.await;
318 if cancel_flag.load(atomic::Ordering::Acquire) {
319 return;
320 }
321
322 if this
323 .update(cx, |this, _| {
324 let new_state = match &this.delegate.directory_state {
325 DirectoryState::None { create: false }
326 | DirectoryState::List { .. } => match paths {
327 Ok(paths) => DirectoryState::List {
328 entries: path_candidates(parent_path_is_root, paths),
329 parent_path: dir.clone(),
330 error: None,
331 },
332 Err(e) => DirectoryState::List {
333 entries: Vec::new(),
334 parent_path: dir.clone(),
335 error: Some(SharedString::from(e.to_string())),
336 },
337 },
338 DirectoryState::None { create: true }
339 | DirectoryState::Create { .. } => match paths {
340 Ok(paths) => {
341 let mut entries = path_candidates(parent_path_is_root, paths);
342 let mut exists = false;
343 let mut is_dir = false;
344 let mut new_id = None;
345 entries.retain(|entry| {
346 new_id = new_id.max(Some(entry.path.id));
347 if entry.path.string == suffix {
348 exists = true;
349 is_dir = entry.is_dir;
350 }
351 !exists || is_dir
352 });
353
354 let new_id = new_id.map(|id| id + 1).unwrap_or(0);
355 let user_input = if suffix.is_empty() {
356 None
357 } else {
358 Some(UserInput {
359 file: StringMatchCandidate::new(new_id, &suffix),
360 exists,
361 is_dir,
362 })
363 };
364 DirectoryState::Create {
365 entries,
366 parent_path: dir.clone(),
367 user_input,
368 }
369 }
370 Err(_) => DirectoryState::Create {
371 entries: Vec::new(),
372 parent_path: dir.clone(),
373 user_input: Some(UserInput {
374 exists: false,
375 is_dir: false,
376 file: StringMatchCandidate::new(0, &suffix),
377 }),
378 },
379 },
380 };
381 this.delegate.directory_state = new_state;
382 })
383 .is_err()
384 {
385 return;
386 }
387 }
388
389 let Ok(mut new_entries) =
390 this.update(cx, |this, _| match &this.delegate.directory_state {
391 DirectoryState::List {
392 entries,
393 error: None,
394 ..
395 }
396 | DirectoryState::Create { entries, .. } => entries.clone(),
397 DirectoryState::List { error: Some(_), .. } | DirectoryState::None { .. } => {
398 Vec::new()
399 }
400 })
401 else {
402 return;
403 };
404
405 let mut max_id = 0;
406 if !suffix.starts_with('.') && !hidden_entries {
407 new_entries.retain(|entry| {
408 max_id = max_id.max(entry.path.id);
409 !entry.path.string.starts_with('.')
410 });
411 }
412
413 if suffix.is_empty() {
414 let should_prepend_with_current_dir = this
415 .read_with(cx, |picker, _| {
416 !input_is_empty
417 && match &picker.delegate.directory_state {
418 DirectoryState::List { error, .. } => error.is_none(),
419 DirectoryState::Create { .. } => false,
420 DirectoryState::None { .. } => false,
421 }
422 })
423 .unwrap_or(false);
424
425 let current_dir_in_new_entries = new_entries
426 .iter()
427 .any(|entry| &entry.path.string == current_dir);
428
429 if should_prepend_with_current_dir && !current_dir_in_new_entries {
430 new_entries.insert(
431 0,
432 CandidateInfo {
433 path: StringMatchCandidate {
434 id: max_id + 1,
435 string: current_dir.to_string(),
436 char_bag: CharBag::from(current_dir),
437 },
438 is_dir: true,
439 },
440 );
441 }
442
443 this.update(cx, |this, cx| {
444 this.delegate.selected_index = 0;
445 this.delegate.string_matches = new_entries
446 .iter()
447 .map(|m| StringMatch {
448 candidate_id: m.path.id,
449 score: 0.0,
450 positions: Vec::new(),
451 string: m.path.string.clone(),
452 })
453 .collect();
454 this.delegate.directory_state =
455 match &this.delegate.directory_state {
456 DirectoryState::None { create: false }
457 | DirectoryState::List { .. } => DirectoryState::List {
458 parent_path: dir.clone(),
459 entries: new_entries,
460 error: None,
461 },
462 DirectoryState::None { create: true }
463 | DirectoryState::Create { .. } => DirectoryState::Create {
464 parent_path: dir.clone(),
465 user_input: None,
466 entries: new_entries,
467 },
468 };
469 cx.notify();
470 })
471 .ok();
472 return;
473 }
474
475 let Ok(is_create_state) =
476 this.update(cx, |this, _| match &this.delegate.directory_state {
477 DirectoryState::Create { .. } => true,
478 DirectoryState::List { .. } => false,
479 DirectoryState::None { create } => *create,
480 })
481 else {
482 return;
483 };
484
485 let candidates = new_entries
486 .iter()
487 .filter_map(|entry| {
488 if is_create_state && !entry.is_dir && Some(&suffix) == Some(&entry.path.string)
489 {
490 None
491 } else {
492 Some(&entry.path)
493 }
494 })
495 .collect::<Vec<_>>();
496
497 let matches = fuzzy::match_strings(
498 candidates.as_slice(),
499 &suffix,
500 false,
501 true,
502 100,
503 &cancel_flag,
504 cx.background_executor().clone(),
505 )
506 .await;
507 if cancel_flag.load(atomic::Ordering::Acquire) {
508 return;
509 }
510
511 this.update(cx, |this, cx| {
512 this.delegate.selected_index = 0;
513 this.delegate.string_matches = matches.clone();
514 this.delegate.string_matches.sort_by_key(|m| {
515 (
516 new_entries
517 .iter()
518 .find(|entry| entry.path.id == m.candidate_id)
519 .map(|entry| &entry.path)
520 .map(|candidate| !candidate.string.starts_with(&suffix)),
521 m.candidate_id,
522 )
523 });
524 this.delegate.directory_state = match &this.delegate.directory_state {
525 DirectoryState::None { create: false } | DirectoryState::List { .. } => {
526 DirectoryState::List {
527 entries: new_entries,
528 parent_path: dir.clone(),
529 error: None,
530 }
531 }
532 DirectoryState::None { create: true } => DirectoryState::Create {
533 entries: new_entries,
534 parent_path: dir.clone(),
535 user_input: Some(UserInput {
536 file: StringMatchCandidate::new(0, &suffix),
537 exists: false,
538 is_dir: false,
539 }),
540 },
541 DirectoryState::Create { user_input, .. } => {
542 let (new_id, exists, is_dir) = user_input
543 .as_ref()
544 .map(|input| (input.file.id, input.exists, input.is_dir))
545 .unwrap_or_else(|| (0, false, false));
546 DirectoryState::Create {
547 entries: new_entries,
548 parent_path: dir.clone(),
549 user_input: Some(UserInput {
550 file: StringMatchCandidate::new(new_id, &suffix),
551 exists,
552 is_dir,
553 }),
554 }
555 }
556 };
557
558 cx.notify();
559 })
560 .ok();
561 })
562 }
563
564 fn confirm_completion(
565 &mut self,
566 query: String,
567 _window: &mut Window,
568 _: &mut Context<Picker<Self>>,
569 ) -> Option<String> {
570 let candidate = self.get_entry(self.selected_index)?;
571 if candidate.path.string.is_empty() || candidate.path.string == self.current_dir() {
572 return None;
573 }
574
575 let path_style = self.path_style;
576 Some(
577 maybe!({
578 match &self.directory_state {
579 DirectoryState::Create { parent_path, .. } => Some(format!(
580 "{}{}{}",
581 parent_path,
582 candidate.path.string,
583 if candidate.is_dir {
584 path_style.primary_separator()
585 } else {
586 ""
587 }
588 )),
589 DirectoryState::List { parent_path, .. } => Some(format!(
590 "{}{}{}",
591 parent_path,
592 candidate.path.string,
593 if candidate.is_dir {
594 path_style.primary_separator()
595 } else {
596 ""
597 }
598 )),
599 DirectoryState::None { .. } => return None,
600 }
601 })
602 .unwrap_or(query),
603 )
604 }
605
606 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
607 let Some(candidate) = self.get_entry(self.selected_index) else {
608 return;
609 };
610
611 match &self.directory_state {
612 DirectoryState::None { .. } => return,
613 DirectoryState::List { parent_path, .. } => {
614 let confirmed_path =
615 if parent_path == &self.prompt_root && candidate.path.string.is_empty() {
616 PathBuf::from(&self.prompt_root)
617 } else {
618 Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
619 .join(&candidate.path.string)
620 };
621 if let Some(tx) = self.tx.take() {
622 tx.send(Some(vec![confirmed_path])).ok();
623 }
624 }
625 DirectoryState::Create {
626 parent_path,
627 user_input,
628 ..
629 } => match user_input {
630 None => return,
631 Some(user_input) => {
632 if user_input.is_dir {
633 return;
634 }
635 let prompted_path =
636 if parent_path == &self.prompt_root && user_input.file.string.is_empty() {
637 PathBuf::from(&self.prompt_root)
638 } else {
639 Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
640 .join(&user_input.file.string)
641 };
642 if user_input.exists {
643 self.should_dismiss = false;
644 let answer = window.prompt(
645 gpui::PromptLevel::Critical,
646 &format!("{prompted_path:?} already exists. Do you want to replace it?"),
647 Some(
648 "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
649 ),
650 &["Replace", "Cancel"],
651 cx
652 );
653 self.replace_prompt = cx.spawn_in(window, async move |picker, cx| {
654 let answer = answer.await.ok();
655 picker
656 .update(cx, |picker, cx| {
657 picker.delegate.should_dismiss = true;
658 if answer != Some(0) {
659 return;
660 }
661 if let Some(tx) = picker.delegate.tx.take() {
662 tx.send(Some(vec![prompted_path])).ok();
663 }
664 cx.emit(gpui::DismissEvent);
665 })
666 .ok();
667 });
668 return;
669 } else if let Some(tx) = self.tx.take() {
670 tx.send(Some(vec![prompted_path])).ok();
671 }
672 }
673 },
674 }
675
676 cx.emit(gpui::DismissEvent);
677 }
678
679 fn should_dismiss(&self) -> bool {
680 self.should_dismiss
681 }
682
683 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
684 if let Some(tx) = self.tx.take() {
685 tx.send(None).ok();
686 }
687 cx.emit(gpui::DismissEvent)
688 }
689
690 fn render_match(
691 &self,
692 ix: usize,
693 selected: bool,
694 window: &mut Window,
695 cx: &mut Context<Picker<Self>>,
696 ) -> Option<Self::ListItem> {
697 let settings = FileFinderSettings::get_global(cx);
698 let candidate = self.get_entry(ix)?;
699 let mut match_positions = match &self.directory_state {
700 DirectoryState::List { .. } => self.string_matches.get(ix)?.positions.clone(),
701 DirectoryState::Create { user_input, .. } => {
702 if let Some(user_input) = user_input {
703 if !user_input.exists || !user_input.is_dir {
704 if ix == 0 {
705 Vec::new()
706 } else {
707 self.string_matches.get(ix - 1)?.positions.clone()
708 }
709 } else {
710 self.string_matches.get(ix)?.positions.clone()
711 }
712 } else {
713 self.string_matches.get(ix)?.positions.clone()
714 }
715 }
716 DirectoryState::None { .. } => Vec::new(),
717 };
718
719 let is_current_dir_candidate = candidate.path.string == self.current_dir();
720
721 let file_icon = maybe!({
722 if !settings.file_icons {
723 return None;
724 }
725
726 let path = path::Path::new(&candidate.path.string);
727 let icon = if candidate.is_dir {
728 if is_current_dir_candidate {
729 return Some(Icon::new(IconName::ReplyArrowRight).color(Color::Muted));
730 } else {
731 FileIcons::get_folder_icon(false, path, cx)?
732 }
733 } else {
734 FileIcons::get_icon(path, cx)?
735 };
736 Some(Icon::from_path(icon).color(Color::Muted))
737 });
738
739 match &self.directory_state {
740 DirectoryState::List { parent_path, .. } => {
741 let (label, indices) = if is_current_dir_candidate {
742 ("open this directory".to_string(), vec![])
743 } else if *parent_path == self.prompt_root {
744 match_positions.iter_mut().for_each(|position| {
745 *position += self.prompt_root.len();
746 });
747 (
748 format!("{}{}", self.prompt_root, candidate.path.string),
749 match_positions,
750 )
751 } else {
752 (candidate.path.string, match_positions)
753 };
754 Some(
755 ListItem::new(ix)
756 .spacing(ListItemSpacing::Sparse)
757 .start_slot::<Icon>(file_icon)
758 .inset(true)
759 .toggle_state(selected)
760 .child(HighlightedLabel::new(label, indices)),
761 )
762 }
763 DirectoryState::Create {
764 parent_path,
765 user_input,
766 ..
767 } => {
768 let (label, delta) = if *parent_path == self.prompt_root {
769 match_positions.iter_mut().for_each(|position| {
770 *position += self.prompt_root.len();
771 });
772 (
773 format!("{}{}", self.prompt_root, candidate.path.string),
774 self.prompt_root.len(),
775 )
776 } else {
777 (candidate.path.string.clone(), 0)
778 };
779
780 let label_with_highlights = match user_input {
781 Some(user_input) => {
782 let label_len = label.len();
783 if user_input.file.string == candidate.path.string {
784 if user_input.exists {
785 let label = if user_input.is_dir {
786 label
787 } else {
788 format!("{label} (replace)")
789 };
790 StyledText::new(label)
791 .with_default_highlights(
792 &window.text_style(),
793 vec![(
794 delta..label_len,
795 HighlightStyle::color(Color::Conflict.color(cx)),
796 )],
797 )
798 .into_any_element()
799 } else {
800 StyledText::new(format!("{label} (create)"))
801 .with_default_highlights(
802 &window.text_style(),
803 vec![(
804 delta..label_len,
805 HighlightStyle::color(Color::Created.color(cx)),
806 )],
807 )
808 .into_any_element()
809 }
810 } else {
811 HighlightedLabel::new(label, match_positions).into_any_element()
812 }
813 }
814 None => HighlightedLabel::new(label, match_positions).into_any_element(),
815 };
816
817 Some(
818 ListItem::new(ix)
819 .spacing(ListItemSpacing::Sparse)
820 .start_slot::<Icon>(file_icon)
821 .inset(true)
822 .toggle_state(selected)
823 .child(LabelLike::new().child(label_with_highlights)),
824 )
825 }
826 DirectoryState::None { .. } => None,
827 }
828 }
829
830 fn render_footer(
831 &self,
832 window: &mut Window,
833 cx: &mut Context<Picker<Self>>,
834 ) -> Option<AnyElement> {
835 (self.render_footer)(window, cx)
836 }
837
838 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
839 Some(match &self.directory_state {
840 DirectoryState::Create { .. } => SharedString::from("Type a path…"),
841 DirectoryState::List {
842 error: Some(error), ..
843 } => error.clone(),
844 DirectoryState::List { .. } | DirectoryState::None { .. } => {
845 SharedString::from("No such file or directory")
846 }
847 })
848 }
849
850 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
851 Arc::from(
852 format!(
853 "[directory{}]filename.ext",
854 self.path_style.primary_separator()
855 )
856 .as_str(),
857 )
858 }
859
860 fn separators_after_indices(&self) -> Vec<usize> {
861 let Some(m) = self.string_matches.first() else {
862 return Vec::new();
863 };
864 if m.string == self.current_dir() {
865 vec![0]
866 } else {
867 Vec::new()
868 }
869 }
870}
871
872fn path_candidates(
873 parent_path_is_root: bool,
874 mut children: Vec<DirectoryItem>,
875) -> Vec<CandidateInfo> {
876 if parent_path_is_root {
877 children.push(DirectoryItem {
878 is_dir: true,
879 path: PathBuf::default(),
880 });
881 }
882
883 children.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
884 children
885 .iter()
886 .enumerate()
887 .map(|(ix, item)| CandidateInfo {
888 path: StringMatchCandidate::new(ix, &item.path.to_string_lossy()),
889 is_dir: item.is_dir,
890 })
891 .collect()
892}
893
894#[cfg(target_os = "windows")]
895fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
896 let last_item = Path::new(&query)
897 .file_name()
898 .unwrap_or_default()
899 .to_string_lossy();
900 let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
901 (dir.to_string(), last_item.into_owned())
902 } else {
903 (query.to_string(), String::new())
904 };
905 match path_style {
906 PathStyle::Posix => {
907 if dir.is_empty() {
908 dir = "/".to_string();
909 }
910 }
911 PathStyle::Windows => {
912 if dir.len() < 3 {
913 dir = "C:\\".to_string();
914 }
915 }
916 }
917 (dir, suffix)
918}
919
920#[cfg(not(target_os = "windows"))]
921fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
922 match path_style {
923 PathStyle::Posix => {
924 let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
925 (query[..index].to_string(), query[index + 1..].to_string())
926 } else {
927 (query, String::new())
928 };
929 if !dir.ends_with('/') {
930 dir.push('/');
931 }
932 (dir, suffix)
933 }
934 PathStyle::Windows => {
935 let (mut dir, suffix) = if let Some(index) = query.rfind('\\') {
936 (query[..index].to_string(), query[index + 1..].to_string())
937 } else {
938 (query, String::new())
939 };
940 if dir.len() < 3 {
941 dir = "C:\\".to_string();
942 }
943 if !dir.ends_with('\\') {
944 dir.push('\\');
945 }
946 (dir, suffix)
947 }
948 }
949}
950
951#[cfg(test)]
952mod tests {
953 use util::paths::PathStyle;
954
955 use super::get_dir_and_suffix;
956
957 #[test]
958 fn test_get_dir_and_suffix_with_windows_style() {
959 let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
960 assert_eq!(dir, "C:\\");
961 assert_eq!(suffix, "");
962
963 let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows);
964 assert_eq!(dir, "C:\\");
965 assert_eq!(suffix, "");
966
967 let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows);
968 assert_eq!(dir, "C:\\");
969 assert_eq!(suffix, "");
970
971 let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows);
972 assert_eq!(dir, "C:\\");
973 assert_eq!(suffix, "Use");
974
975 let (dir, suffix) =
976 get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
977 assert_eq!(dir, "C:\\Users\\Junkui\\");
978 assert_eq!(suffix, "Docum");
979
980 let (dir, suffix) =
981 get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
982 assert_eq!(dir, "C:\\Users\\Junkui\\");
983 assert_eq!(suffix, "Documents");
984
985 let (dir, suffix) =
986 get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
987 assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
988 assert_eq!(suffix, "");
989 }
990
991 #[test]
992 fn test_get_dir_and_suffix_with_posix_style() {
993 let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
994 assert_eq!(dir, "/");
995 assert_eq!(suffix, "");
996
997 let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
998 assert_eq!(dir, "/");
999 assert_eq!(suffix, "");
1000
1001 let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
1002 assert_eq!(dir, "/");
1003 assert_eq!(suffix, "Use");
1004
1005 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
1006 assert_eq!(dir, "/Users/Junkui/");
1007 assert_eq!(suffix, "Docum");
1008
1009 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
1010 assert_eq!(dir, "/Users/Junkui/");
1011 assert_eq!(suffix, "Documents");
1012
1013 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
1014 assert_eq!(dir, "/Users/Junkui/Documents/");
1015 assert_eq!(suffix, "");
1016 }
1017}