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