1pub mod fs;
2mod ignore;
3mod worktree;
4
5use anyhow::Result;
6use buffer::LanguageRegistry;
7use client::Client;
8use futures::Future;
9use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
10use gpui::{executor, AppContext, Entity, ModelContext, ModelHandle, Task};
11use lsp::LanguageServer;
12use std::{
13 path::Path,
14 sync::{atomic::AtomicBool, Arc},
15};
16use util::TryFutureExt as _;
17
18pub use fs::*;
19pub use worktree::*;
20
21pub struct Project {
22 worktrees: Vec<ModelHandle<Worktree>>,
23 active_entry: Option<ProjectEntry>,
24 languages: Arc<LanguageRegistry>,
25 client: Arc<client::Client>,
26 fs: Arc<dyn Fs>,
27 language_server: Arc<LanguageServer>,
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 rpc: Arc<Client>,
51 fs: Arc<dyn Fs>,
52 background: &executor::Background,
53 ) -> Self {
54 Self {
55 worktrees: Default::default(),
56 active_entry: None,
57 languages,
58 client: rpc,
59 fs,
60 language_server: LanguageServer::new(
61 Path::new("/Users/as-cii/Downloads/rust-analyzer-x86_64-apple-darwin"),
62 background,
63 )
64 .unwrap(),
65 }
66 }
67
68 pub fn worktrees(&self) -> &[ModelHandle<Worktree>] {
69 &self.worktrees
70 }
71
72 pub fn worktree_for_id(&self, id: usize) -> Option<ModelHandle<Worktree>> {
73 self.worktrees
74 .iter()
75 .find(|worktree| worktree.id() == id)
76 .cloned()
77 }
78
79 pub fn add_local_worktree(
80 &mut self,
81 abs_path: &Path,
82 cx: &mut ModelContext<Self>,
83 ) -> Task<Result<ModelHandle<Worktree>>> {
84 let fs = self.fs.clone();
85 let rpc = self.client.clone();
86 let languages = self.languages.clone();
87 let path = Arc::from(abs_path);
88 cx.spawn(|this, mut cx| async move {
89 let worktree = Worktree::open_local(rpc, path, fs, languages, &mut cx).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 buffer::LanguageRegistry;
318 use fs::RealFs;
319 use gpui::TestAppContext;
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(|cx| Project::new(languages, rpc, fs, cx.background()))
424 }
425}