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