lib.rs

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