Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:37:38.189Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

open_path_prompt.rs

1106 lines · 41.5 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.