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