lib.rs

  1pub mod fs;
  2mod ignore;
  3mod worktree;
  4
  5use anyhow::Result;
  6use client::Client;
  7use futures::Future;
  8use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
  9use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
 10use language::LanguageRegistry;
 11use std::{
 12    path::Path,
 13    sync::{atomic::AtomicBool, Arc},
 14};
 15use util::TryFutureExt as _;
 16
 17pub use fs::*;
 18pub use worktree::*;
 19
 20pub struct Project {
 21    worktrees: Vec<ModelHandle<Worktree>>,
 22    active_entry: Option<ProjectEntry>,
 23    languages: Arc<LanguageRegistry>,
 24    client: Arc<client::Client>,
 25    fs: Arc<dyn Fs>,
 26}
 27
 28pub enum Event {
 29    ActiveEntryChanged(Option<ProjectEntry>),
 30    WorktreeRemoved(usize),
 31}
 32
 33#[derive(Clone, Debug, Eq, PartialEq, Hash)]
 34pub struct ProjectPath {
 35    pub worktree_id: usize,
 36    pub path: Arc<Path>,
 37}
 38
 39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 40pub struct ProjectEntry {
 41    pub worktree_id: usize,
 42    pub entry_id: usize,
 43}
 44
 45impl Project {
 46    pub fn new(languages: Arc<LanguageRegistry>, rpc: Arc<Client>, fs: Arc<dyn Fs>) -> Self {
 47        Self {
 48            worktrees: Default::default(),
 49            active_entry: None,
 50            languages,
 51            client: rpc,
 52            fs,
 53        }
 54    }
 55
 56    pub fn worktrees(&self) -> &[ModelHandle<Worktree>] {
 57        &self.worktrees
 58    }
 59
 60    pub fn worktree_for_id(&self, id: usize) -> Option<ModelHandle<Worktree>> {
 61        self.worktrees
 62            .iter()
 63            .find(|worktree| worktree.id() == id)
 64            .cloned()
 65    }
 66
 67    pub fn add_local_worktree(
 68        &mut self,
 69        abs_path: &Path,
 70        cx: &mut ModelContext<Self>,
 71    ) -> Task<Result<ModelHandle<Worktree>>> {
 72        let fs = self.fs.clone();
 73        let rpc = self.client.clone();
 74        let languages = self.languages.clone();
 75        let path = Arc::from(abs_path);
 76        cx.spawn(|this, mut cx| async move {
 77            let worktree = Worktree::open_local(rpc, path, fs, languages, &mut cx).await?;
 78            this.update(&mut cx, |this, cx| {
 79                this.add_worktree(worktree.clone(), cx);
 80            });
 81            Ok(worktree)
 82        })
 83    }
 84
 85    pub fn add_remote_worktree(
 86        &mut self,
 87        remote_id: u64,
 88        cx: &mut ModelContext<Self>,
 89    ) -> Task<Result<ModelHandle<Worktree>>> {
 90        let rpc = self.client.clone();
 91        let languages = self.languages.clone();
 92        cx.spawn(|this, mut cx| async move {
 93            rpc.authenticate_and_connect(&cx).await?;
 94            let worktree =
 95                Worktree::open_remote(rpc.clone(), remote_id, languages, &mut cx).await?;
 96            this.update(&mut cx, |this, cx| {
 97                cx.subscribe(&worktree, move |this, _, event, cx| match event {
 98                    worktree::Event::Closed => {
 99                        this.close_remote_worktree(remote_id, cx);
100                        cx.notify();
101                    }
102                })
103                .detach();
104                this.add_worktree(worktree.clone(), cx);
105            });
106            Ok(worktree)
107        })
108    }
109
110    fn add_worktree(&mut self, worktree: ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
111        cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
112        self.worktrees.push(worktree);
113        cx.notify();
114    }
115
116    pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
117        let new_active_entry = entry.and_then(|project_path| {
118            let worktree = self.worktree_for_id(project_path.worktree_id)?;
119            let entry = worktree.read(cx).entry_for_path(project_path.path)?;
120            Some(ProjectEntry {
121                worktree_id: project_path.worktree_id,
122                entry_id: entry.id,
123            })
124        });
125        if new_active_entry != self.active_entry {
126            self.active_entry = new_active_entry;
127            cx.emit(Event::ActiveEntryChanged(new_active_entry));
128        }
129    }
130
131    pub fn active_entry(&self) -> Option<ProjectEntry> {
132        self.active_entry
133    }
134
135    pub fn share_worktree(&self, remote_id: u64, cx: &mut ModelContext<Self>) {
136        let rpc = self.client.clone();
137        cx.spawn(|this, mut cx| {
138            async move {
139                rpc.authenticate_and_connect(&cx).await?;
140
141                let task = this.update(&mut cx, |this, cx| {
142                    for worktree in &this.worktrees {
143                        let task = worktree.update(cx, |worktree, cx| {
144                            worktree.as_local_mut().and_then(|worktree| {
145                                if worktree.remote_id() == Some(remote_id) {
146                                    Some(worktree.share(cx))
147                                } else {
148                                    None
149                                }
150                            })
151                        });
152                        if task.is_some() {
153                            return task;
154                        }
155                    }
156                    None
157                });
158
159                if let Some(task) = task {
160                    task.await?;
161                }
162
163                Ok(())
164            }
165            .log_err()
166        })
167        .detach();
168    }
169
170    pub fn unshare_worktree(&mut self, remote_id: u64, cx: &mut ModelContext<Self>) {
171        for worktree in &self.worktrees {
172            if worktree.update(cx, |worktree, cx| {
173                if let Some(worktree) = worktree.as_local_mut() {
174                    if worktree.remote_id() == Some(remote_id) {
175                        worktree.unshare(cx);
176                        return true;
177                    }
178                }
179                false
180            }) {
181                break;
182            }
183        }
184    }
185
186    pub fn close_remote_worktree(&mut self, id: u64, cx: &mut ModelContext<Self>) {
187        self.worktrees.retain(|worktree| {
188            let keep = worktree.update(cx, |worktree, cx| {
189                if let Some(worktree) = worktree.as_remote_mut() {
190                    if worktree.remote_id() == id {
191                        worktree.close_all_buffers(cx);
192                        return false;
193                    }
194                }
195                true
196            });
197            if !keep {
198                cx.emit(Event::WorktreeRemoved(worktree.id()));
199            }
200            keep
201        });
202    }
203
204    pub fn match_paths<'a>(
205        &self,
206        query: &'a str,
207        include_ignored: bool,
208        smart_case: bool,
209        max_results: usize,
210        cancel_flag: &'a AtomicBool,
211        cx: &AppContext,
212    ) -> impl 'a + Future<Output = Vec<PathMatch>> {
213        let include_root_name = self.worktrees.len() > 1;
214        let candidate_sets = self
215            .worktrees
216            .iter()
217            .map(|worktree| CandidateSet {
218                snapshot: worktree.read(cx).snapshot(),
219                include_ignored,
220                include_root_name,
221            })
222            .collect::<Vec<_>>();
223
224        let background = cx.background().clone();
225        async move {
226            fuzzy::match_paths(
227                candidate_sets.as_slice(),
228                query,
229                smart_case,
230                max_results,
231                cancel_flag,
232                background,
233            )
234            .await
235        }
236    }
237}
238
239struct CandidateSet {
240    snapshot: Snapshot,
241    include_ignored: bool,
242    include_root_name: bool,
243}
244
245impl<'a> PathMatchCandidateSet<'a> for CandidateSet {
246    type Candidates = CandidateSetIter<'a>;
247
248    fn id(&self) -> usize {
249        self.snapshot.id()
250    }
251
252    fn len(&self) -> usize {
253        if self.include_ignored {
254            self.snapshot.file_count()
255        } else {
256            self.snapshot.visible_file_count()
257        }
258    }
259
260    fn prefix(&self) -> Arc<str> {
261        if self.snapshot.root_entry().map_or(false, |e| e.is_file()) {
262            self.snapshot.root_name().into()
263        } else if self.include_root_name {
264            format!("{}/", self.snapshot.root_name()).into()
265        } else {
266            "".into()
267        }
268    }
269
270    fn candidates(&'a self, start: usize) -> Self::Candidates {
271        CandidateSetIter {
272            traversal: self.snapshot.files(self.include_ignored, start),
273        }
274    }
275}
276
277struct CandidateSetIter<'a> {
278    traversal: Traversal<'a>,
279}
280
281impl<'a> Iterator for CandidateSetIter<'a> {
282    type Item = PathMatchCandidate<'a>;
283
284    fn next(&mut self) -> Option<Self::Item> {
285        self.traversal.next().map(|entry| {
286            if let EntryKind::File(char_bag) = entry.kind {
287                PathMatchCandidate {
288                    path: &entry.path,
289                    char_bag,
290                }
291            } else {
292                unreachable!()
293            }
294        })
295    }
296}
297
298impl Entity for Project {
299    type Event = Event;
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use fs::RealFs;
306    use gpui::TestAppContext;
307    use language::LanguageRegistry;
308    use serde_json::json;
309    use std::{os::unix, path::PathBuf};
310    use util::test::temp_tree;
311
312    #[gpui::test]
313    async fn test_populate_and_search(mut cx: gpui::TestAppContext) {
314        let dir = temp_tree(json!({
315            "root": {
316                "apple": "",
317                "banana": {
318                    "carrot": {
319                        "date": "",
320                        "endive": "",
321                    }
322                },
323                "fennel": {
324                    "grape": "",
325                }
326            }
327        }));
328
329        let root_link_path = dir.path().join("root_link");
330        unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
331        unix::fs::symlink(
332            &dir.path().join("root/fennel"),
333            &dir.path().join("root/finnochio"),
334        )
335        .unwrap();
336
337        let project = build_project(&mut cx);
338
339        let tree = project
340            .update(&mut cx, |project, cx| {
341                project.add_local_worktree(&root_link_path, cx)
342            })
343            .await
344            .unwrap();
345
346        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
347            .await;
348        cx.read(|cx| {
349            let tree = tree.read(cx);
350            assert_eq!(tree.file_count(), 5);
351            assert_eq!(
352                tree.inode_for_path("fennel/grape"),
353                tree.inode_for_path("finnochio/grape")
354            );
355        });
356
357        let cancel_flag = Default::default();
358        let results = project
359            .read_with(&cx, |project, cx| {
360                project.match_paths("bna", false, false, 10, &cancel_flag, cx)
361            })
362            .await;
363        assert_eq!(
364            results
365                .into_iter()
366                .map(|result| result.path)
367                .collect::<Vec<Arc<Path>>>(),
368            vec![
369                PathBuf::from("banana/carrot/date").into(),
370                PathBuf::from("banana/carrot/endive").into(),
371            ]
372        );
373    }
374
375    #[gpui::test]
376    async fn test_search_worktree_without_files(mut cx: gpui::TestAppContext) {
377        let dir = temp_tree(json!({
378            "root": {
379                "dir1": {},
380                "dir2": {
381                    "dir3": {}
382                }
383            }
384        }));
385
386        let project = build_project(&mut cx);
387        let tree = project
388            .update(&mut cx, |project, cx| {
389                project.add_local_worktree(&dir.path(), cx)
390            })
391            .await
392            .unwrap();
393
394        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
395            .await;
396
397        let cancel_flag = Default::default();
398        let results = project
399            .read_with(&cx, |project, cx| {
400                project.match_paths("dir", false, false, 10, &cancel_flag, cx)
401            })
402            .await;
403
404        assert!(results.is_empty());
405    }
406
407    fn build_project(cx: &mut TestAppContext) -> ModelHandle<Project> {
408        let languages = Arc::new(LanguageRegistry::new());
409        let fs = Arc::new(RealFs);
410        let rpc = client::Client::new();
411        cx.add_model(|_| Project::new(languages, rpc, fs))
412    }
413}