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