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