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}