lib.rs

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