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