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