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