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