project.rs

  1pub mod fs;
  2mod ignore;
  3mod worktree;
  4
  5use anyhow::{anyhow, Result};
  6use client::{Client, UserStore};
  7use clock::ReplicaId;
  8use futures::Future;
  9use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 10use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
 11use language::{Buffer, DiagnosticEntry, LanguageRegistry};
 12use lsp::DiagnosticSeverity;
 13use std::{
 14    path::Path,
 15    sync::{atomic::AtomicBool, Arc},
 16};
 17use util::TryFutureExt as _;
 18
 19pub use fs::*;
 20pub use worktree::*;
 21
 22pub struct Project {
 23    worktrees: Vec<ModelHandle<Worktree>>,
 24    active_worktree: Option<usize>,
 25    active_entry: Option<ProjectEntry>,
 26    languages: Arc<LanguageRegistry>,
 27    client: Arc<client::Client>,
 28    user_store: ModelHandle<UserStore>,
 29    fs: Arc<dyn Fs>,
 30}
 31
 32pub enum Event {
 33    ActiveEntryChanged(Option<ProjectEntry>),
 34    WorktreeRemoved(usize),
 35}
 36
 37#[derive(Clone, Debug, Eq, PartialEq, Hash)]
 38pub struct ProjectPath {
 39    pub worktree_id: usize,
 40    pub path: Arc<Path>,
 41}
 42
 43#[derive(Clone)]
 44pub struct DiagnosticSummary {
 45    pub error_count: usize,
 46    pub warning_count: usize,
 47    pub info_count: usize,
 48    pub hint_count: usize,
 49}
 50
 51impl DiagnosticSummary {
 52    fn new<T>(diagnostics: &[DiagnosticEntry<T>]) -> Self {
 53        let mut this = Self {
 54            error_count: 0,
 55            warning_count: 0,
 56            info_count: 0,
 57            hint_count: 0,
 58        };
 59
 60        for entry in diagnostics {
 61            if entry.diagnostic.is_primary {
 62                match entry.diagnostic.severity {
 63                    DiagnosticSeverity::ERROR => this.error_count += 1,
 64                    DiagnosticSeverity::WARNING => this.warning_count += 1,
 65                    DiagnosticSeverity::INFORMATION => this.info_count += 1,
 66                    DiagnosticSeverity::HINT => this.hint_count += 1,
 67                    _ => {}
 68                }
 69            }
 70        }
 71
 72        this
 73    }
 74}
 75
 76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 77pub struct ProjectEntry {
 78    pub worktree_id: usize,
 79    pub entry_id: usize,
 80}
 81
 82impl Project {
 83    pub fn new(
 84        languages: Arc<LanguageRegistry>,
 85        client: Arc<Client>,
 86        user_store: ModelHandle<UserStore>,
 87        fs: Arc<dyn Fs>,
 88    ) -> Self {
 89        Self {
 90            worktrees: Default::default(),
 91            active_worktree: None,
 92            active_entry: None,
 93            languages,
 94            client,
 95            user_store,
 96            fs,
 97        }
 98    }
 99
100    pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
101        // TODO
102        self.worktrees.first().unwrap().read(cx).replica_id()
103    }
104
105    pub fn worktrees(&self) -> &[ModelHandle<Worktree>] {
106        &self.worktrees
107    }
108
109    pub fn worktree_for_id(&self, id: usize) -> Option<ModelHandle<Worktree>> {
110        self.worktrees
111            .iter()
112            .find(|worktree| worktree.id() == id)
113            .cloned()
114    }
115
116    pub fn open_buffer(
117        &self,
118        path: ProjectPath,
119        cx: &mut ModelContext<Self>,
120    ) -> Task<Result<ModelHandle<Buffer>>> {
121        if let Some(worktree) = self.worktree_for_id(path.worktree_id) {
122            worktree.update(cx, |worktree, cx| worktree.open_buffer(path.path, cx))
123        } else {
124            cx.spawn(|_, _| async move { Err(anyhow!("no such worktree")) })
125        }
126    }
127
128    pub fn add_local_worktree(
129        &mut self,
130        abs_path: &Path,
131        cx: &mut ModelContext<Self>,
132    ) -> Task<Result<ModelHandle<Worktree>>> {
133        let fs = self.fs.clone();
134        let client = self.client.clone();
135        let user_store = self.user_store.clone();
136        let languages = self.languages.clone();
137        let path = Arc::from(abs_path);
138        cx.spawn(|this, mut cx| async move {
139            let worktree =
140                Worktree::open_local(client, user_store, path, fs, languages, &mut cx).await?;
141            this.update(&mut cx, |this, cx| {
142                this.add_worktree(worktree.clone(), cx);
143            });
144            Ok(worktree)
145        })
146    }
147
148    pub fn add_remote_worktree(
149        &mut self,
150        remote_id: u64,
151        cx: &mut ModelContext<Self>,
152    ) -> Task<Result<ModelHandle<Worktree>>> {
153        let rpc = self.client.clone();
154        let languages = self.languages.clone();
155        let user_store = self.user_store.clone();
156        cx.spawn(|this, mut cx| async move {
157            rpc.authenticate_and_connect(&cx).await?;
158            let worktree =
159                Worktree::open_remote(rpc.clone(), remote_id, languages, user_store, &mut cx)
160                    .await?;
161            this.update(&mut cx, |this, cx| {
162                cx.subscribe(&worktree, move |this, _, event, cx| match event {
163                    worktree::Event::Closed => {
164                        this.close_remote_worktree(remote_id, cx);
165                        cx.notify();
166                    }
167                })
168                .detach();
169                this.add_worktree(worktree.clone(), cx);
170            });
171            Ok(worktree)
172        })
173    }
174
175    fn add_worktree(&mut self, worktree: ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
176        cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
177        if self.active_worktree.is_none() {
178            self.set_active_worktree(Some(worktree.id()), cx);
179        }
180        self.worktrees.push(worktree);
181        cx.notify();
182    }
183
184    fn set_active_worktree(&mut self, worktree_id: Option<usize>, cx: &mut ModelContext<Self>) {
185        if self.active_worktree != worktree_id {
186            self.active_worktree = worktree_id;
187            cx.notify();
188        }
189    }
190
191    pub fn active_worktree(&self) -> Option<ModelHandle<Worktree>> {
192        self.active_worktree
193            .and_then(|worktree_id| self.worktree_for_id(worktree_id))
194    }
195
196    pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
197        let new_active_entry = entry.and_then(|project_path| {
198            let worktree = self.worktree_for_id(project_path.worktree_id)?;
199            let entry = worktree.read(cx).entry_for_path(project_path.path)?;
200            Some(ProjectEntry {
201                worktree_id: project_path.worktree_id,
202                entry_id: entry.id,
203            })
204        });
205        if new_active_entry != self.active_entry {
206            if let Some(worktree_id) = new_active_entry.map(|e| e.worktree_id) {
207                self.set_active_worktree(Some(worktree_id), cx);
208            }
209            self.active_entry = new_active_entry;
210            cx.emit(Event::ActiveEntryChanged(new_active_entry));
211        }
212    }
213
214    pub fn diagnostic_summaries<'a>(
215        &'a self,
216        cx: &'a AppContext,
217    ) -> impl Iterator<Item = (ProjectPath, DiagnosticSummary)> + 'a {
218        self.worktrees.iter().flat_map(move |worktree| {
219            let worktree_id = worktree.id();
220            worktree
221                .read(cx)
222                .diagnostic_summaries()
223                .map(move |(path, summary)| (ProjectPath { worktree_id, path }, summary))
224        })
225    }
226
227    pub fn active_entry(&self) -> Option<ProjectEntry> {
228        self.active_entry
229    }
230
231    pub fn share_worktree(&self, remote_id: u64, cx: &mut ModelContext<Self>) {
232        let rpc = self.client.clone();
233        cx.spawn(|this, mut cx| {
234            async move {
235                rpc.authenticate_and_connect(&cx).await?;
236
237                let task = this.update(&mut cx, |this, cx| {
238                    for worktree in &this.worktrees {
239                        let task = worktree.update(cx, |worktree, cx| {
240                            worktree.as_local_mut().and_then(|worktree| {
241                                if worktree.remote_id() == Some(remote_id) {
242                                    Some(worktree.share(cx))
243                                } else {
244                                    None
245                                }
246                            })
247                        });
248                        if task.is_some() {
249                            return task;
250                        }
251                    }
252                    None
253                });
254
255                if let Some(task) = task {
256                    task.await?;
257                }
258
259                Ok(())
260            }
261            .log_err()
262        })
263        .detach();
264    }
265
266    pub fn unshare_worktree(&mut self, remote_id: u64, cx: &mut ModelContext<Self>) {
267        for worktree in &self.worktrees {
268            if worktree.update(cx, |worktree, cx| {
269                if let Some(worktree) = worktree.as_local_mut() {
270                    if worktree.remote_id() == Some(remote_id) {
271                        worktree.unshare(cx);
272                        return true;
273                    }
274                }
275                false
276            }) {
277                break;
278            }
279        }
280    }
281
282    pub fn close_remote_worktree(&mut self, id: u64, cx: &mut ModelContext<Self>) {
283        let mut reset_active = None;
284        self.worktrees.retain(|worktree| {
285            let keep = worktree.update(cx, |worktree, cx| {
286                if let Some(worktree) = worktree.as_remote_mut() {
287                    if worktree.remote_id() == id {
288                        worktree.close_all_buffers(cx);
289                        return false;
290                    }
291                }
292                true
293            });
294            if !keep {
295                cx.emit(Event::WorktreeRemoved(worktree.id()));
296                reset_active = Some(worktree.id());
297            }
298            keep
299        });
300
301        if self.active_worktree == reset_active {
302            self.active_worktree = self.worktrees.first().map(|w| w.id());
303            cx.notify();
304        }
305    }
306
307    pub fn match_paths<'a>(
308        &self,
309        query: &'a str,
310        include_ignored: bool,
311        smart_case: bool,
312        max_results: usize,
313        cancel_flag: &'a AtomicBool,
314        cx: &AppContext,
315    ) -> impl 'a + Future<Output = Vec<PathMatch>> {
316        let include_root_name = self.worktrees.len() > 1;
317        let candidate_sets = self
318            .worktrees
319            .iter()
320            .map(|worktree| CandidateSet {
321                snapshot: worktree.read(cx).snapshot(),
322                include_ignored,
323                include_root_name,
324            })
325            .collect::<Vec<_>>();
326
327        let background = cx.background().clone();
328        async move {
329            fuzzy::match_paths(
330                candidate_sets.as_slice(),
331                query,
332                smart_case,
333                max_results,
334                cancel_flag,
335                background,
336            )
337            .await
338        }
339    }
340}
341
342struct CandidateSet {
343    snapshot: Snapshot,
344    include_ignored: bool,
345    include_root_name: bool,
346}
347
348impl<'a> PathMatchCandidateSet<'a> for CandidateSet {
349    type Candidates = CandidateSetIter<'a>;
350
351    fn id(&self) -> usize {
352        self.snapshot.id()
353    }
354
355    fn len(&self) -> usize {
356        if self.include_ignored {
357            self.snapshot.file_count()
358        } else {
359            self.snapshot.visible_file_count()
360        }
361    }
362
363    fn prefix(&self) -> Arc<str> {
364        if self.snapshot.root_entry().map_or(false, |e| e.is_file()) {
365            self.snapshot.root_name().into()
366        } else if self.include_root_name {
367            format!("{}/", self.snapshot.root_name()).into()
368        } else {
369            "".into()
370        }
371    }
372
373    fn candidates(&'a self, start: usize) -> Self::Candidates {
374        CandidateSetIter {
375            traversal: self.snapshot.files(self.include_ignored, start),
376        }
377    }
378}
379
380struct CandidateSetIter<'a> {
381    traversal: Traversal<'a>,
382}
383
384impl<'a> Iterator for CandidateSetIter<'a> {
385    type Item = PathMatchCandidate<'a>;
386
387    fn next(&mut self) -> Option<Self::Item> {
388        self.traversal.next().map(|entry| {
389            if let EntryKind::File(char_bag) = entry.kind {
390                PathMatchCandidate {
391                    path: &entry.path,
392                    char_bag,
393                }
394            } else {
395                unreachable!()
396            }
397        })
398    }
399}
400
401impl Entity for Project {
402    type Event = Event;
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use client::{http::ServerResponse, test::FakeHttpClient};
409    use fs::RealFs;
410    use gpui::TestAppContext;
411    use language::LanguageRegistry;
412    use serde_json::json;
413    use std::{os::unix, path::PathBuf};
414    use util::test::temp_tree;
415
416    #[gpui::test]
417    async fn test_populate_and_search(mut cx: gpui::TestAppContext) {
418        let dir = temp_tree(json!({
419            "root": {
420                "apple": "",
421                "banana": {
422                    "carrot": {
423                        "date": "",
424                        "endive": "",
425                    }
426                },
427                "fennel": {
428                    "grape": "",
429                }
430            }
431        }));
432
433        let root_link_path = dir.path().join("root_link");
434        unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
435        unix::fs::symlink(
436            &dir.path().join("root/fennel"),
437            &dir.path().join("root/finnochio"),
438        )
439        .unwrap();
440
441        let project = build_project(&mut cx);
442
443        let tree = project
444            .update(&mut cx, |project, cx| {
445                project.add_local_worktree(&root_link_path, cx)
446            })
447            .await
448            .unwrap();
449
450        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
451            .await;
452        cx.read(|cx| {
453            let tree = tree.read(cx);
454            assert_eq!(tree.file_count(), 5);
455            assert_eq!(
456                tree.inode_for_path("fennel/grape"),
457                tree.inode_for_path("finnochio/grape")
458            );
459        });
460
461        let cancel_flag = Default::default();
462        let results = project
463            .read_with(&cx, |project, cx| {
464                project.match_paths("bna", false, false, 10, &cancel_flag, cx)
465            })
466            .await;
467        assert_eq!(
468            results
469                .into_iter()
470                .map(|result| result.path)
471                .collect::<Vec<Arc<Path>>>(),
472            vec![
473                PathBuf::from("banana/carrot/date").into(),
474                PathBuf::from("banana/carrot/endive").into(),
475            ]
476        );
477    }
478
479    #[gpui::test]
480    async fn test_search_worktree_without_files(mut cx: gpui::TestAppContext) {
481        let dir = temp_tree(json!({
482            "root": {
483                "dir1": {},
484                "dir2": {
485                    "dir3": {}
486                }
487            }
488        }));
489
490        let project = build_project(&mut cx);
491        let tree = project
492            .update(&mut cx, |project, cx| {
493                project.add_local_worktree(&dir.path(), cx)
494            })
495            .await
496            .unwrap();
497
498        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
499            .await;
500
501        let cancel_flag = Default::default();
502        let results = project
503            .read_with(&cx, |project, cx| {
504                project.match_paths("dir", false, false, 10, &cancel_flag, cx)
505            })
506            .await;
507
508        assert!(results.is_empty());
509    }
510
511    fn build_project(cx: &mut TestAppContext) -> ModelHandle<Project> {
512        let languages = Arc::new(LanguageRegistry::new());
513        let fs = Arc::new(RealFs);
514        let client = client::Client::new();
515        let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
516        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
517        cx.add_model(|_| Project::new(languages, client, user_store, fs))
518    }
519}