project.rs

  1pub mod fs;
  2mod ignore;
  3mod worktree;
  4
  5use 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::{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 add_local_worktree(
117        &mut self,
118        abs_path: &Path,
119        cx: &mut ModelContext<Self>,
120    ) -> Task<Result<ModelHandle<Worktree>>> {
121        let fs = self.fs.clone();
122        let client = self.client.clone();
123        let user_store = self.user_store.clone();
124        let languages = self.languages.clone();
125        let path = Arc::from(abs_path);
126        cx.spawn(|this, mut cx| async move {
127            let worktree =
128                Worktree::open_local(client, user_store, path, fs, languages, &mut cx).await?;
129            this.update(&mut cx, |this, cx| {
130                this.add_worktree(worktree.clone(), cx);
131            });
132            Ok(worktree)
133        })
134    }
135
136    pub fn add_remote_worktree(
137        &mut self,
138        remote_id: u64,
139        cx: &mut ModelContext<Self>,
140    ) -> Task<Result<ModelHandle<Worktree>>> {
141        let rpc = self.client.clone();
142        let languages = self.languages.clone();
143        let user_store = self.user_store.clone();
144        cx.spawn(|this, mut cx| async move {
145            rpc.authenticate_and_connect(&cx).await?;
146            let worktree =
147                Worktree::open_remote(rpc.clone(), remote_id, languages, user_store, &mut cx)
148                    .await?;
149            this.update(&mut cx, |this, cx| {
150                cx.subscribe(&worktree, move |this, _, event, cx| match event {
151                    worktree::Event::Closed => {
152                        this.close_remote_worktree(remote_id, cx);
153                        cx.notify();
154                    }
155                })
156                .detach();
157                this.add_worktree(worktree.clone(), cx);
158            });
159            Ok(worktree)
160        })
161    }
162
163    fn add_worktree(&mut self, worktree: ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
164        cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
165        if self.active_worktree.is_none() {
166            self.set_active_worktree(Some(worktree.id()), cx);
167        }
168        self.worktrees.push(worktree);
169        cx.notify();
170    }
171
172    fn set_active_worktree(&mut self, worktree_id: Option<usize>, cx: &mut ModelContext<Self>) {
173        if self.active_worktree != worktree_id {
174            self.active_worktree = worktree_id;
175            cx.notify();
176        }
177    }
178
179    pub fn active_worktree(&self) -> Option<ModelHandle<Worktree>> {
180        self.active_worktree
181            .and_then(|worktree_id| self.worktree_for_id(worktree_id))
182    }
183
184    pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
185        let new_active_entry = entry.and_then(|project_path| {
186            let worktree = self.worktree_for_id(project_path.worktree_id)?;
187            let entry = worktree.read(cx).entry_for_path(project_path.path)?;
188            Some(ProjectEntry {
189                worktree_id: project_path.worktree_id,
190                entry_id: entry.id,
191            })
192        });
193        if new_active_entry != self.active_entry {
194            if let Some(worktree_id) = new_active_entry.map(|e| e.worktree_id) {
195                self.set_active_worktree(Some(worktree_id), cx);
196            }
197            self.active_entry = new_active_entry;
198            cx.emit(Event::ActiveEntryChanged(new_active_entry));
199        }
200    }
201
202    pub fn diagnostic_summaries<'a>(
203        &'a self,
204        cx: &'a AppContext,
205    ) -> impl Iterator<Item = (ProjectPath, DiagnosticSummary)> + 'a {
206        self.worktrees.iter().flat_map(move |worktree| {
207            let worktree_id = worktree.id();
208            worktree
209                .read(cx)
210                .diagnostic_summaries()
211                .map(move |(path, summary)| (ProjectPath { worktree_id, path }, summary))
212        })
213    }
214
215    pub fn active_entry(&self) -> Option<ProjectEntry> {
216        self.active_entry
217    }
218
219    pub fn share_worktree(&self, remote_id: u64, cx: &mut ModelContext<Self>) {
220        let rpc = self.client.clone();
221        cx.spawn(|this, mut cx| {
222            async move {
223                rpc.authenticate_and_connect(&cx).await?;
224
225                let task = this.update(&mut cx, |this, cx| {
226                    for worktree in &this.worktrees {
227                        let task = worktree.update(cx, |worktree, cx| {
228                            worktree.as_local_mut().and_then(|worktree| {
229                                if worktree.remote_id() == Some(remote_id) {
230                                    Some(worktree.share(cx))
231                                } else {
232                                    None
233                                }
234                            })
235                        });
236                        if task.is_some() {
237                            return task;
238                        }
239                    }
240                    None
241                });
242
243                if let Some(task) = task {
244                    task.await?;
245                }
246
247                Ok(())
248            }
249            .log_err()
250        })
251        .detach();
252    }
253
254    pub fn unshare_worktree(&mut self, remote_id: u64, cx: &mut ModelContext<Self>) {
255        for worktree in &self.worktrees {
256            if worktree.update(cx, |worktree, cx| {
257                if let Some(worktree) = worktree.as_local_mut() {
258                    if worktree.remote_id() == Some(remote_id) {
259                        worktree.unshare(cx);
260                        return true;
261                    }
262                }
263                false
264            }) {
265                break;
266            }
267        }
268    }
269
270    pub fn close_remote_worktree(&mut self, id: u64, cx: &mut ModelContext<Self>) {
271        let mut reset_active = None;
272        self.worktrees.retain(|worktree| {
273            let keep = worktree.update(cx, |worktree, cx| {
274                if let Some(worktree) = worktree.as_remote_mut() {
275                    if worktree.remote_id() == id {
276                        worktree.close_all_buffers(cx);
277                        return false;
278                    }
279                }
280                true
281            });
282            if !keep {
283                cx.emit(Event::WorktreeRemoved(worktree.id()));
284                reset_active = Some(worktree.id());
285            }
286            keep
287        });
288
289        if self.active_worktree == reset_active {
290            self.active_worktree = self.worktrees.first().map(|w| w.id());
291            cx.notify();
292        }
293    }
294
295    pub fn match_paths<'a>(
296        &self,
297        query: &'a str,
298        include_ignored: bool,
299        smart_case: bool,
300        max_results: usize,
301        cancel_flag: &'a AtomicBool,
302        cx: &AppContext,
303    ) -> impl 'a + Future<Output = Vec<PathMatch>> {
304        let include_root_name = self.worktrees.len() > 1;
305        let candidate_sets = self
306            .worktrees
307            .iter()
308            .map(|worktree| CandidateSet {
309                snapshot: worktree.read(cx).snapshot(),
310                include_ignored,
311                include_root_name,
312            })
313            .collect::<Vec<_>>();
314
315        let background = cx.background().clone();
316        async move {
317            fuzzy::match_paths(
318                candidate_sets.as_slice(),
319                query,
320                smart_case,
321                max_results,
322                cancel_flag,
323                background,
324            )
325            .await
326        }
327    }
328}
329
330struct CandidateSet {
331    snapshot: Snapshot,
332    include_ignored: bool,
333    include_root_name: bool,
334}
335
336impl<'a> PathMatchCandidateSet<'a> for CandidateSet {
337    type Candidates = CandidateSetIter<'a>;
338
339    fn id(&self) -> usize {
340        self.snapshot.id()
341    }
342
343    fn len(&self) -> usize {
344        if self.include_ignored {
345            self.snapshot.file_count()
346        } else {
347            self.snapshot.visible_file_count()
348        }
349    }
350
351    fn prefix(&self) -> Arc<str> {
352        if self.snapshot.root_entry().map_or(false, |e| e.is_file()) {
353            self.snapshot.root_name().into()
354        } else if self.include_root_name {
355            format!("{}/", self.snapshot.root_name()).into()
356        } else {
357            "".into()
358        }
359    }
360
361    fn candidates(&'a self, start: usize) -> Self::Candidates {
362        CandidateSetIter {
363            traversal: self.snapshot.files(self.include_ignored, start),
364        }
365    }
366}
367
368struct CandidateSetIter<'a> {
369    traversal: Traversal<'a>,
370}
371
372impl<'a> Iterator for CandidateSetIter<'a> {
373    type Item = PathMatchCandidate<'a>;
374
375    fn next(&mut self) -> Option<Self::Item> {
376        self.traversal.next().map(|entry| {
377            if let EntryKind::File(char_bag) = entry.kind {
378                PathMatchCandidate {
379                    path: &entry.path,
380                    char_bag,
381                }
382            } else {
383                unreachable!()
384            }
385        })
386    }
387}
388
389impl Entity for Project {
390    type Event = Event;
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use client::{http::ServerResponse, test::FakeHttpClient};
397    use fs::RealFs;
398    use gpui::TestAppContext;
399    use language::LanguageRegistry;
400    use serde_json::json;
401    use std::{os::unix, path::PathBuf};
402    use util::test::temp_tree;
403
404    #[gpui::test]
405    async fn test_populate_and_search(mut cx: gpui::TestAppContext) {
406        let dir = temp_tree(json!({
407            "root": {
408                "apple": "",
409                "banana": {
410                    "carrot": {
411                        "date": "",
412                        "endive": "",
413                    }
414                },
415                "fennel": {
416                    "grape": "",
417                }
418            }
419        }));
420
421        let root_link_path = dir.path().join("root_link");
422        unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
423        unix::fs::symlink(
424            &dir.path().join("root/fennel"),
425            &dir.path().join("root/finnochio"),
426        )
427        .unwrap();
428
429        let project = build_project(&mut cx);
430
431        let tree = project
432            .update(&mut cx, |project, cx| {
433                project.add_local_worktree(&root_link_path, cx)
434            })
435            .await
436            .unwrap();
437
438        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
439            .await;
440        cx.read(|cx| {
441            let tree = tree.read(cx);
442            assert_eq!(tree.file_count(), 5);
443            assert_eq!(
444                tree.inode_for_path("fennel/grape"),
445                tree.inode_for_path("finnochio/grape")
446            );
447        });
448
449        let cancel_flag = Default::default();
450        let results = project
451            .read_with(&cx, |project, cx| {
452                project.match_paths("bna", false, false, 10, &cancel_flag, cx)
453            })
454            .await;
455        assert_eq!(
456            results
457                .into_iter()
458                .map(|result| result.path)
459                .collect::<Vec<Arc<Path>>>(),
460            vec![
461                PathBuf::from("banana/carrot/date").into(),
462                PathBuf::from("banana/carrot/endive").into(),
463            ]
464        );
465    }
466
467    #[gpui::test]
468    async fn test_search_worktree_without_files(mut cx: gpui::TestAppContext) {
469        let dir = temp_tree(json!({
470            "root": {
471                "dir1": {},
472                "dir2": {
473                    "dir3": {}
474                }
475            }
476        }));
477
478        let project = build_project(&mut cx);
479        let tree = project
480            .update(&mut cx, |project, cx| {
481                project.add_local_worktree(&dir.path(), cx)
482            })
483            .await
484            .unwrap();
485
486        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
487            .await;
488
489        let cancel_flag = Default::default();
490        let results = project
491            .read_with(&cx, |project, cx| {
492                project.match_paths("dir", false, false, 10, &cancel_flag, cx)
493            })
494            .await;
495
496        assert!(results.is_empty());
497    }
498
499    fn build_project(cx: &mut TestAppContext) -> ModelHandle<Project> {
500        let languages = Arc::new(LanguageRegistry::new());
501        let fs = Arc::new(RealFs);
502        let client = client::Client::new();
503        let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
504        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
505        cx.add_model(|_| Project::new(languages, client, user_store, fs))
506    }
507}