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}