1use crate::{
2 fs::Fs,
3 fuzzy::{self, PathMatch},
4 rpc::Client,
5 util::TryFutureExt as _,
6 worktree::{self, Worktree},
7 AppState,
8};
9use anyhow::Result;
10use buffer::LanguageRegistry;
11use futures::Future;
12use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
13use std::{
14 path::Path,
15 sync::{atomic::AtomicBool, Arc},
16};
17
18pub struct Project {
19 worktrees: Vec<ModelHandle<Worktree>>,
20 active_entry: Option<ProjectEntry>,
21 languages: Arc<LanguageRegistry>,
22 rpc: Arc<Client>,
23 fs: Arc<dyn Fs>,
24}
25
26pub enum Event {
27 ActiveEntryChanged(Option<ProjectEntry>),
28 WorktreeRemoved(usize),
29}
30
31#[derive(Clone, Debug, Eq, PartialEq, Hash)]
32pub struct ProjectPath {
33 pub worktree_id: usize,
34 pub path: Arc<Path>,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub struct ProjectEntry {
39 pub worktree_id: usize,
40 pub entry_id: usize,
41}
42
43impl Project {
44 pub fn new(app_state: &AppState) -> Self {
45 Self {
46 worktrees: Default::default(),
47 active_entry: None,
48 languages: app_state.languages.clone(),
49 rpc: app_state.rpc.clone(),
50 fs: app_state.fs.clone(),
51 }
52 }
53
54 pub fn worktrees(&self) -> &[ModelHandle<Worktree>] {
55 &self.worktrees
56 }
57
58 pub fn worktree_for_id(&self, id: usize) -> Option<ModelHandle<Worktree>> {
59 self.worktrees
60 .iter()
61 .find(|worktree| worktree.id() == id)
62 .cloned()
63 }
64
65 pub fn add_local_worktree(
66 &mut self,
67 abs_path: &Path,
68 cx: &mut ModelContext<Self>,
69 ) -> Task<Result<ModelHandle<Worktree>>> {
70 let fs = self.fs.clone();
71 let rpc = self.rpc.clone();
72 let languages = self.languages.clone();
73 let path = Arc::from(abs_path);
74 cx.spawn(|this, mut cx| async move {
75 let worktree = Worktree::open_local(rpc, path, fs, languages, &mut cx).await?;
76 this.update(&mut cx, |this, cx| {
77 this.add_worktree(worktree.clone(), cx);
78 });
79 Ok(worktree)
80 })
81 }
82
83 pub fn add_remote_worktree(
84 &mut self,
85 remote_id: u64,
86 cx: &mut ModelContext<Self>,
87 ) -> Task<Result<ModelHandle<Worktree>>> {
88 let rpc = self.rpc.clone();
89 let languages = self.languages.clone();
90 cx.spawn(|this, mut cx| async move {
91 rpc.authenticate_and_connect(&cx).await?;
92 let worktree =
93 Worktree::open_remote(rpc.clone(), remote_id, languages, &mut cx).await?;
94 this.update(&mut cx, |this, cx| {
95 cx.subscribe(&worktree, move |this, _, event, cx| match event {
96 worktree::Event::Closed => {
97 this.close_remote_worktree(remote_id, cx);
98 cx.notify();
99 }
100 })
101 .detach();
102 this.add_worktree(worktree.clone(), cx);
103 });
104 Ok(worktree)
105 })
106 }
107
108 fn add_worktree(&mut self, worktree: ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
109 cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
110 self.worktrees.push(worktree);
111 cx.notify();
112 }
113
114 pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
115 let new_active_entry = entry.and_then(|project_path| {
116 let worktree = self.worktree_for_id(project_path.worktree_id)?;
117 let entry = worktree.read(cx).entry_for_path(project_path.path)?;
118 Some(ProjectEntry {
119 worktree_id: project_path.worktree_id,
120 entry_id: entry.id,
121 })
122 });
123 if new_active_entry != self.active_entry {
124 self.active_entry = new_active_entry;
125 cx.emit(Event::ActiveEntryChanged(new_active_entry));
126 }
127 }
128
129 pub fn active_entry(&self) -> Option<ProjectEntry> {
130 self.active_entry
131 }
132
133 pub fn share_worktree(&self, remote_id: u64, cx: &mut ModelContext<Self>) {
134 let rpc = self.rpc.clone();
135 cx.spawn(|this, mut cx| {
136 async move {
137 rpc.authenticate_and_connect(&cx).await?;
138
139 let task = this.update(&mut cx, |this, cx| {
140 for worktree in &this.worktrees {
141 let task = worktree.update(cx, |worktree, cx| {
142 worktree.as_local_mut().and_then(|worktree| {
143 if worktree.remote_id() == Some(remote_id) {
144 Some(worktree.share(cx))
145 } else {
146 None
147 }
148 })
149 });
150 if task.is_some() {
151 return task;
152 }
153 }
154 None
155 });
156
157 if let Some(task) = task {
158 task.await?;
159 }
160
161 Ok(())
162 }
163 .log_err()
164 })
165 .detach();
166 }
167
168 pub fn unshare_worktree(&mut self, remote_id: u64, cx: &mut ModelContext<Self>) {
169 for worktree in &self.worktrees {
170 if worktree.update(cx, |worktree, cx| {
171 if let Some(worktree) = worktree.as_local_mut() {
172 if worktree.remote_id() == Some(remote_id) {
173 worktree.unshare(cx);
174 return true;
175 }
176 }
177 false
178 }) {
179 break;
180 }
181 }
182 }
183
184 pub fn close_remote_worktree(&mut self, id: u64, cx: &mut ModelContext<Self>) {
185 self.worktrees.retain(|worktree| {
186 let keep = worktree.update(cx, |worktree, cx| {
187 if let Some(worktree) = worktree.as_remote_mut() {
188 if worktree.remote_id() == id {
189 worktree.close_all_buffers(cx);
190 return false;
191 }
192 }
193 true
194 });
195 if !keep {
196 cx.emit(Event::WorktreeRemoved(worktree.id()));
197 }
198 keep
199 });
200 }
201
202 pub fn match_paths<'a>(
203 &self,
204 query: &'a str,
205 include_ignored: bool,
206 smart_case: bool,
207 max_results: usize,
208 cancel_flag: &'a AtomicBool,
209 cx: &AppContext,
210 ) -> impl 'a + Future<Output = Vec<PathMatch>> {
211 let snapshots = self
212 .worktrees
213 .iter()
214 .map(|worktree| worktree.read(cx).snapshot())
215 .collect::<Vec<_>>();
216 let background = cx.background().clone();
217
218 async move {
219 fuzzy::match_paths(
220 snapshots.as_slice(),
221 query,
222 include_ignored,
223 smart_case,
224 max_results,
225 cancel_flag,
226 background,
227 )
228 .await
229 }
230 }
231}
232
233impl Entity for Project {
234 type Event = Event;
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::{
241 fs::RealFs,
242 test::{temp_tree, test_app_state},
243 };
244 use serde_json::json;
245 use std::{os::unix, path::PathBuf};
246
247 #[gpui::test]
248 async fn test_populate_and_search(mut cx: gpui::TestAppContext) {
249 let mut app_state = cx.update(test_app_state);
250 Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(RealFs);
251 let dir = temp_tree(json!({
252 "root": {
253 "apple": "",
254 "banana": {
255 "carrot": {
256 "date": "",
257 "endive": "",
258 }
259 },
260 "fennel": {
261 "grape": "",
262 }
263 }
264 }));
265
266 let root_link_path = dir.path().join("root_link");
267 unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
268 unix::fs::symlink(
269 &dir.path().join("root/fennel"),
270 &dir.path().join("root/finnochio"),
271 )
272 .unwrap();
273
274 let project = cx.add_model(|_| Project::new(app_state.as_ref()));
275 let tree = project
276 .update(&mut cx, |project, cx| {
277 project.add_local_worktree(&root_link_path, cx)
278 })
279 .await
280 .unwrap();
281
282 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
283 .await;
284 cx.read(|cx| {
285 let tree = tree.read(cx);
286 assert_eq!(tree.file_count(), 5);
287 assert_eq!(
288 tree.inode_for_path("fennel/grape"),
289 tree.inode_for_path("finnochio/grape")
290 );
291 });
292
293 let cancel_flag = Default::default();
294 let results = project
295 .read_with(&cx, |project, cx| {
296 project.match_paths("bna", false, false, 10, &cancel_flag, cx)
297 })
298 .await;
299 assert_eq!(
300 results
301 .into_iter()
302 .map(|result| result.path)
303 .collect::<Vec<Arc<Path>>>(),
304 vec![
305 PathBuf::from("banana/carrot/date").into(),
306 PathBuf::from("banana/carrot/endive").into(),
307 ]
308 );
309 }
310
311 #[gpui::test]
312 async fn test_search_worktree_without_files(mut cx: gpui::TestAppContext) {
313 let mut app_state = cx.update(test_app_state);
314 Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(RealFs);
315 let dir = temp_tree(json!({
316 "root": {
317 "dir1": {},
318 "dir2": {
319 "dir3": {}
320 }
321 }
322 }));
323
324 let project = cx.add_model(|_| Project::new(app_state.as_ref()));
325 let tree = project
326 .update(&mut cx, |project, cx| {
327 project.add_local_worktree(&dir.path(), cx)
328 })
329 .await
330 .unwrap();
331
332 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
333 .await;
334
335 let cancel_flag = Default::default();
336 let results = project
337 .read_with(&cx, |project, cx| {
338 project.match_paths("dir", false, false, 10, &cancel_flag, cx)
339 })
340 .await;
341
342 assert!(results.is_empty());
343 }
344}