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