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