open_path_prompt.rs

  1use futures::channel::oneshot;
  2use fuzzy::StringMatchCandidate;
  3use picker::{Picker, PickerDelegate};
  4use project::DirectoryLister;
  5use std::{
  6    path::{Path, PathBuf},
  7    sync::{
  8        atomic::{self, AtomicBool},
  9        Arc,
 10    },
 11};
 12use ui::{prelude::*, LabelLike, ListItemSpacing};
 13use ui::{ListItem, ViewContext};
 14use util::{maybe, paths::compare_paths};
 15use workspace::Workspace;
 16
 17pub(crate) struct OpenPathPrompt;
 18
 19pub struct OpenPathDelegate {
 20    tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
 21    lister: DirectoryLister,
 22    selected_index: usize,
 23    directory_state: Option<DirectoryState>,
 24    matches: Vec<usize>,
 25    cancel_flag: Arc<AtomicBool>,
 26    should_dismiss: bool,
 27}
 28
 29impl OpenPathDelegate {
 30    pub fn new(tx: oneshot::Sender<Option<Vec<PathBuf>>>, lister: DirectoryLister) -> Self {
 31        Self {
 32            tx: Some(tx),
 33            lister,
 34            selected_index: 0,
 35            directory_state: None,
 36            matches: Vec::new(),
 37            cancel_flag: Arc::new(AtomicBool::new(false)),
 38            should_dismiss: true,
 39        }
 40    }
 41}
 42
 43struct DirectoryState {
 44    path: String,
 45    match_candidates: Vec<StringMatchCandidate>,
 46    error: Option<SharedString>,
 47}
 48
 49impl OpenPathPrompt {
 50    pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 51        workspace.set_prompt_for_open_path(Box::new(|workspace, lister, cx| {
 52            let (tx, rx) = futures::channel::oneshot::channel();
 53            Self::prompt_for_open_path(workspace, lister, tx, cx);
 54            rx
 55        }));
 56    }
 57
 58    fn prompt_for_open_path(
 59        workspace: &mut Workspace,
 60        lister: DirectoryLister,
 61        tx: oneshot::Sender<Option<Vec<PathBuf>>>,
 62        cx: &mut ViewContext<Workspace>,
 63    ) {
 64        workspace.toggle_modal(cx, |cx| {
 65            let delegate = OpenPathDelegate::new(tx, lister.clone());
 66
 67            let picker = Picker::uniform_list(delegate, cx).width(rems(34.));
 68            let query = lister.default_query(cx);
 69            picker.set_query(query, cx);
 70            picker
 71        });
 72    }
 73}
 74
 75impl PickerDelegate for OpenPathDelegate {
 76    type ListItem = ui::ListItem;
 77
 78    fn match_count(&self) -> usize {
 79        self.matches.len()
 80    }
 81
 82    fn selected_index(&self) -> usize {
 83        self.selected_index
 84    }
 85
 86    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
 87        self.selected_index = ix;
 88        cx.notify();
 89    }
 90
 91    fn update_matches(
 92        &mut self,
 93        query: String,
 94        cx: &mut ViewContext<Picker<Self>>,
 95    ) -> gpui::Task<()> {
 96        let lister = self.lister.clone();
 97        let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
 98            (query[..index].to_string(), query[index + 1..].to_string())
 99        } else {
100            (query, String::new())
101        };
102        if dir == "" {
103            dir = "/".to_string();
104        }
105
106        let query = if self
107            .directory_state
108            .as_ref()
109            .map_or(false, |s| s.path == dir)
110        {
111            None
112        } else {
113            Some(lister.list_directory(dir.clone(), cx))
114        };
115        self.cancel_flag.store(true, atomic::Ordering::Relaxed);
116        self.cancel_flag = Arc::new(AtomicBool::new(false));
117        let cancel_flag = self.cancel_flag.clone();
118
119        cx.spawn(|this, mut cx| async move {
120            if let Some(query) = query {
121                let paths = query.await;
122                if cancel_flag.load(atomic::Ordering::Relaxed) {
123                    return;
124                }
125
126                this.update(&mut cx, |this, _| {
127                    this.delegate.directory_state = Some(match paths {
128                        Ok(mut paths) => {
129                            paths.sort_by(|a, b| compare_paths((a, true), (b, true)));
130                            let match_candidates = paths
131                                .iter()
132                                .enumerate()
133                                .map(|(ix, path)| {
134                                    StringMatchCandidate::new(ix, path.to_string_lossy().into())
135                                })
136                                .collect::<Vec<_>>();
137
138                            DirectoryState {
139                                match_candidates,
140                                path: dir,
141                                error: None,
142                            }
143                        }
144                        Err(err) => DirectoryState {
145                            match_candidates: vec![],
146                            path: dir,
147                            error: Some(err.to_string().into()),
148                        },
149                    });
150                })
151                .ok();
152            }
153
154            let match_candidates = this
155                .update(&mut cx, |this, cx| {
156                    let directory_state = this.delegate.directory_state.as_ref()?;
157                    if directory_state.error.is_some() {
158                        this.delegate.matches.clear();
159                        this.delegate.selected_index = 0;
160                        cx.notify();
161                        return None;
162                    }
163
164                    Some(directory_state.match_candidates.clone())
165                })
166                .unwrap_or(None);
167
168            let Some(mut match_candidates) = match_candidates else {
169                return;
170            };
171
172            if !suffix.starts_with('.') {
173                match_candidates.retain(|m| !m.string.starts_with('.'));
174            }
175
176            if suffix == "" {
177                this.update(&mut cx, |this, cx| {
178                    this.delegate.matches.clear();
179                    this.delegate
180                        .matches
181                        .extend(match_candidates.iter().map(|m| m.id));
182
183                    cx.notify();
184                })
185                .ok();
186                return;
187            }
188
189            let matches = fuzzy::match_strings(
190                match_candidates.as_slice(),
191                &suffix,
192                false,
193                100,
194                &cancel_flag,
195                cx.background_executor().clone(),
196            )
197            .await;
198            if cancel_flag.load(atomic::Ordering::Relaxed) {
199                return;
200            }
201
202            this.update(&mut cx, |this, cx| {
203                this.delegate.matches.clear();
204                this.delegate
205                    .matches
206                    .extend(matches.into_iter().map(|m| m.candidate_id));
207                this.delegate.matches.sort_by_key(|m| {
208                    (
209                        this.delegate.directory_state.as_ref().and_then(|d| {
210                            d.match_candidates
211                                .get(*m)
212                                .map(|c| !c.string.starts_with(&suffix))
213                        }),
214                        *m,
215                    )
216                });
217                cx.notify();
218            })
219            .ok();
220        })
221    }
222
223    fn confirm_completion(
224        &mut self,
225        query: String,
226        _: &mut ViewContext<Picker<Self>>,
227    ) -> Option<String> {
228        Some(
229            maybe!({
230                let m = self.matches.get(self.selected_index)?;
231                let directory_state = self.directory_state.as_ref()?;
232                let candidate = directory_state.match_candidates.get(*m)?;
233                Some(format!("{}/{}", directory_state.path, candidate.string))
234            })
235            .unwrap_or(query),
236        )
237    }
238
239    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
240        let Some(m) = self.matches.get(self.selected_index) else {
241            return;
242        };
243        let Some(directory_state) = self.directory_state.as_ref() else {
244            return;
245        };
246        let Some(candidate) = directory_state.match_candidates.get(*m) else {
247            return;
248        };
249        let result = Path::new(
250            self.lister
251                .resolve_tilde(&directory_state.path, cx)
252                .as_ref(),
253        )
254        .join(&candidate.string);
255        if let Some(tx) = self.tx.take() {
256            tx.send(Some(vec![result])).ok();
257        }
258        cx.emit(gpui::DismissEvent);
259    }
260
261    fn should_dismiss(&self) -> bool {
262        self.should_dismiss
263    }
264
265    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
266        if let Some(tx) = self.tx.take() {
267            tx.send(None).ok();
268        }
269        cx.emit(gpui::DismissEvent)
270    }
271
272    fn render_match(
273        &self,
274        ix: usize,
275        selected: bool,
276        _: &mut ViewContext<Picker<Self>>,
277    ) -> Option<Self::ListItem> {
278        let m = self.matches.get(ix)?;
279        let directory_state = self.directory_state.as_ref()?;
280        let candidate = directory_state.match_candidates.get(*m)?;
281
282        Some(
283            ListItem::new(ix)
284                .spacing(ListItemSpacing::Sparse)
285                .inset(true)
286                .selected(selected)
287                .child(LabelLike::new().child(candidate.string.clone())),
288        )
289    }
290
291    fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
292        if let Some(error) = self.directory_state.as_ref().and_then(|s| s.error.clone()) {
293            error
294        } else {
295            "No such file or directory".into()
296        }
297    }
298
299    fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
300        Arc::from("[directory/]filename.ext")
301    }
302}