project.rs

  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}