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