Remove weak worktrees from project when nobody references them

Antonio Scandurra created

Also, avoid showing them in the project panel as well as in the
contacts panel.

Change summary

crates/diagnostics/src/diagnostics.rs     |   4 
crates/file_finder/src/file_finder.rs     |  25 +++-
crates/project/src/project.rs             |  97 ++++++++++++++------
crates/project/src/worktree.rs            |  32 ++++++
crates/project_panel/src/project_panel.rs | 113 +++++++++++++-----------
crates/rpc/proto/zed.proto                |   1 
crates/server/src/rpc.rs                  |  82 +++++++++++------
crates/server/src/rpc/store.rs            |   2 
crates/workspace/src/workspace.rs         |  22 +---
crates/zed/src/zed.rs                     |  55 +++++------
10 files changed, 262 insertions(+), 171 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -764,9 +764,9 @@ mod tests {
             )
             .await;
 
-        let worktree = project
+        let (worktree, _) = project
             .update(&mut cx, |project, cx| {
-                project.add_local_worktree("/test", cx)
+                project.find_or_create_worktree_for_abs_path("/test", false, cx)
             })
             .await
             .unwrap();

crates/file_finder/src/file_finder.rs 🔗

@@ -454,9 +454,10 @@ mod tests {
             .await;
 
         let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        workspace
-            .update(&mut cx, |workspace, cx| {
-                workspace.add_worktree(Path::new("/root"), cx)
+        params
+            .project
+            .update(&mut cx, |project, cx| {
+                project.find_or_create_worktree_for_abs_path(Path::new("/root"), false, cx)
             })
             .await
             .unwrap();
@@ -514,9 +515,10 @@ mod tests {
         .await;
 
         let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        workspace
-            .update(&mut cx, |workspace, cx| {
-                workspace.add_worktree("/dir".as_ref(), cx)
+        params
+            .project
+            .update(&mut cx, |project, cx| {
+                project.find_or_create_worktree_for_abs_path(Path::new("/dir"), false, cx)
             })
             .await
             .unwrap();
@@ -579,9 +581,14 @@ mod tests {
             .await;
 
         let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        workspace
-            .update(&mut cx, |workspace, cx| {
-                workspace.add_worktree(Path::new("/root/the-parent-dir/the-file"), cx)
+        params
+            .project
+            .update(&mut cx, |project, cx| {
+                project.find_or_create_worktree_for_abs_path(
+                    Path::new("/root/the-parent-dir/the-file"),
+                    false,
+                    cx,
+                )
             })
             .await
             .unwrap();

crates/project/src/project.rs 🔗

@@ -10,6 +10,7 @@ use futures::Future;
 use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 use gpui::{
     AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
+    WeakModelHandle,
 };
 use language::{
     Bias, Buffer, DiagnosticEntry, File as _, Language, LanguageRegistry, ToOffset, ToPointUtf16,
@@ -28,7 +29,7 @@ pub use fs::*;
 pub use worktree::*;
 
 pub struct Project {
-    worktrees: Vec<ModelHandle<Worktree>>,
+    worktrees: Vec<WorktreeHandle>,
     active_entry: Option<ProjectEntry>,
     languages: Arc<LanguageRegistry>,
     language_servers: HashMap<(WorktreeId, String), Arc<LanguageServer>>,
@@ -41,6 +42,11 @@ pub struct Project {
     language_servers_with_diagnostics_running: isize,
 }
 
+enum WorktreeHandle {
+    Strong(ModelHandle<Worktree>),
+    Weak(WeakModelHandle<Worktree>),
+}
+
 enum ProjectClientState {
     Local {
         is_shared: bool,
@@ -161,7 +167,7 @@ impl Project {
                                 if let Some(project_id) = remote_id {
                                     let mut registrations = Vec::new();
                                     this.update(&mut cx, |this, cx| {
-                                        for worktree in &this.worktrees {
+                                        for worktree in this.worktrees(cx).collect::<Vec<_>>() {
                                             registrations.push(worktree.update(
                                                 cx,
                                                 |worktree, cx| {
@@ -295,7 +301,7 @@ impl Project {
                 language_servers: Default::default(),
             };
             for worktree in worktrees {
-                this.add_worktree(worktree, cx);
+                this.add_worktree(&worktree, false, cx);
             }
             this
         }))
@@ -364,8 +370,13 @@ impl Project {
         &self.collaborators
     }
 
-    pub fn worktrees(&self) -> &[ModelHandle<Worktree>] {
-        &self.worktrees
+    pub fn worktrees<'a>(
+        &'a self,
+        cx: &'a AppContext,
+    ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
+        self.worktrees
+            .iter()
+            .filter_map(move |worktree| worktree.upgrade(cx))
     }
 
     pub fn worktree_for_id(
@@ -373,10 +384,8 @@ impl Project {
         id: WorktreeId,
         cx: &AppContext,
     ) -> Option<ModelHandle<Worktree>> {
-        self.worktrees
-            .iter()
+        self.worktrees(cx)
             .find(|worktree| worktree.read(cx).id() == id)
-            .cloned()
     }
 
     pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<anyhow::Result<()>> {
@@ -401,7 +410,7 @@ impl Project {
             rpc.request(proto::ShareProject { project_id }).await?;
             let mut tasks = Vec::new();
             this.update(&mut cx, |this, cx| {
-                for worktree in &this.worktrees {
+                for worktree in this.worktrees(cx).collect::<Vec<_>>() {
                     worktree.update(cx, |worktree, cx| {
                         let worktree = worktree.as_local_mut().unwrap();
                         tasks.push(worktree.share(cx));
@@ -438,7 +447,7 @@ impl Project {
             rpc.send(proto::UnshareProject { project_id }).await?;
             this.update(&mut cx, |this, cx| {
                 this.collaborators.clear();
-                for worktree in &this.worktrees {
+                for worktree in this.worktrees(cx).collect::<Vec<_>>() {
                     worktree.update(cx, |worktree, _| {
                         worktree.as_local_mut().unwrap().unshare();
                     });
@@ -494,7 +503,7 @@ impl Project {
         abs_path: PathBuf,
         cx: &mut ModelContext<Project>,
     ) -> Task<Result<()>> {
-        let worktree_task = self.find_or_create_worktree_for_abs_path(&abs_path, cx);
+        let worktree_task = self.find_or_create_worktree_for_abs_path(&abs_path, false, cx);
         cx.spawn(|this, mut cx| async move {
             let (worktree, path) = worktree_task.await?;
             worktree
@@ -777,7 +786,7 @@ impl Project {
                         } else {
                             let (worktree, relative_path) = this
                                 .update(&mut cx, |this, cx| {
-                                    this.create_worktree_for_abs_path(&abs_path, cx)
+                                    this.create_worktree_for_abs_path(&abs_path, true, cx)
                                 })
                                 .await?;
                             this.update(&mut cx, |this, cx| {
@@ -829,22 +838,25 @@ impl Project {
 
     pub fn find_or_create_worktree_for_abs_path(
         &self,
-        abs_path: &Path,
+        abs_path: impl AsRef<Path>,
+        weak: bool,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
+        let abs_path = abs_path.as_ref();
         if let Some((tree, relative_path)) = self.find_worktree_for_abs_path(abs_path, cx) {
             Task::ready(Ok((tree.clone(), relative_path.into())))
         } else {
-            self.create_worktree_for_abs_path(abs_path, cx)
+            self.create_worktree_for_abs_path(abs_path, weak, cx)
         }
     }
 
     fn create_worktree_for_abs_path(
         &self,
         abs_path: &Path,
+        weak: bool,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
-        let worktree = self.add_local_worktree(abs_path, cx);
+        let worktree = self.add_local_worktree(abs_path, weak, cx);
         cx.background().spawn(async move {
             let worktree = worktree.await?;
             Ok((worktree, PathBuf::new()))
@@ -856,7 +868,7 @@ impl Project {
         abs_path: &Path,
         cx: &AppContext,
     ) -> Option<(ModelHandle<Worktree>, PathBuf)> {
-        for tree in &self.worktrees {
+        for tree in self.worktrees(cx) {
             if let Some(relative_path) = tree
                 .read(cx)
                 .as_local()
@@ -875,9 +887,10 @@ impl Project {
         }
     }
 
-    pub fn add_local_worktree(
+    fn add_local_worktree(
         &self,
         abs_path: impl AsRef<Path>,
+        weak: bool,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ModelHandle<Worktree>>> {
         let fs = self.fs.clone();
@@ -886,10 +899,10 @@ impl Project {
         let path = Arc::from(abs_path.as_ref());
         cx.spawn(|project, mut cx| async move {
             let worktree =
-                Worktree::open_local(client.clone(), user_store, path, fs, &mut cx).await?;
+                Worktree::open_local(client.clone(), user_store, path, weak, fs, &mut cx).await?;
 
             let (remote_project_id, is_shared) = project.update(&mut cx, |project, cx| {
-                project.add_worktree(worktree.clone(), cx);
+                project.add_worktree(&worktree, weak, cx);
                 (project.remote_id(), project.is_shared())
             });
 
@@ -913,14 +926,28 @@ impl Project {
     }
 
     pub fn remove_worktree(&mut self, id: WorktreeId, cx: &mut ModelContext<Self>) {
-        self.worktrees
-            .retain(|worktree| worktree.read(cx).id() != id);
+        self.worktrees.retain(|worktree| {
+            worktree
+                .upgrade(cx)
+                .map_or(false, |w| w.read(cx).id() != id)
+        });
         cx.notify();
     }
 
-    fn add_worktree(&mut self, worktree: ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
+    fn add_worktree(
+        &mut self,
+        worktree: &ModelHandle<Worktree>,
+        weak: bool,
+        cx: &mut ModelContext<Self>,
+    ) {
         cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
-        self.worktrees.push(worktree);
+        if weak {
+            self.worktrees
+                .push(WorktreeHandle::Weak(worktree.downgrade()));
+        } else {
+            self.worktrees
+                .push(WorktreeHandle::Strong(worktree.clone()));
+        }
         cx.notify();
     }
 
@@ -966,7 +993,7 @@ impl Project {
         &'a self,
         cx: &'a AppContext,
     ) -> impl Iterator<Item = (ProjectPath, DiagnosticSummary)> + 'a {
-        self.worktrees.iter().flat_map(move |worktree| {
+        self.worktrees(cx).flat_map(move |worktree| {
             let worktree = worktree.read(cx);
             let worktree_id = worktree.id();
             worktree
@@ -1059,7 +1086,7 @@ impl Project {
             .remove(&peer_id)
             .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))?
             .replica_id;
-        for worktree in &self.worktrees {
+        for worktree in self.worktrees(cx).collect::<Vec<_>>() {
             worktree.update(cx, |worktree, cx| {
                 worktree.remove_collaborator(peer_id, replica_id, cx);
             })
@@ -1085,7 +1112,7 @@ impl Project {
                 let worktree =
                     Worktree::remote(remote_id, replica_id, worktree, client, user_store, &mut cx)
                         .await?;
-                this.update(&mut cx, |this, cx| this.add_worktree(worktree, cx));
+                this.update(&mut cx, |this, cx| this.add_worktree(&worktree, false, cx));
                 Ok(())
             }
             .log_err()
@@ -1293,8 +1320,7 @@ impl Project {
     ) -> impl 'a + Future<Output = Vec<PathMatch>> {
         let include_root_name = self.worktrees.len() > 1;
         let candidate_sets = self
-            .worktrees
-            .iter()
+            .worktrees(cx)
             .map(|worktree| CandidateSet {
                 snapshot: worktree.read(cx).snapshot(),
                 include_ignored,
@@ -1317,6 +1343,15 @@ impl Project {
     }
 }
 
+impl WorktreeHandle {
+    pub fn upgrade(&self, cx: &AppContext) -> Option<ModelHandle<Worktree>> {
+        match self {
+            WorktreeHandle::Strong(handle) => Some(handle.clone()),
+            WorktreeHandle::Weak(handle) => handle.upgrade(cx),
+        }
+    }
+}
+
 struct CandidateSet {
     snapshot: Snapshot,
     include_ignored: bool,
@@ -1489,7 +1524,7 @@ mod tests {
 
         let tree = project
             .update(&mut cx, |project, cx| {
-                project.add_local_worktree(&root_link_path, cx)
+                project.add_local_worktree(&root_link_path, false, cx)
             })
             .await
             .unwrap();
@@ -1564,7 +1599,7 @@ mod tests {
 
         let tree = project
             .update(&mut cx, |project, cx| {
-                project.add_local_worktree(dir.path(), cx)
+                project.add_local_worktree(dir.path(), false, cx)
             })
             .await
             .unwrap();
@@ -1670,7 +1705,7 @@ mod tests {
         let project = build_project(&mut cx);
         let tree = project
             .update(&mut cx, |project, cx| {
-                project.add_local_worktree(&dir.path(), cx)
+                project.add_local_worktree(&dir.path(), false, cx)
             })
             .await
             .unwrap();

crates/project/src/worktree.rs 🔗

@@ -91,11 +91,12 @@ impl Worktree {
         client: Arc<Client>,
         user_store: ModelHandle<UserStore>,
         path: impl Into<Arc<Path>>,
+        weak: bool,
         fs: Arc<dyn Fs>,
         cx: &mut AsyncAppContext,
     ) -> Result<ModelHandle<Self>> {
         let (tree, scan_states_tx) =
-            LocalWorktree::new(client, user_store, path, fs.clone(), cx).await?;
+            LocalWorktree::new(client, user_store, path, weak, fs.clone(), cx).await?;
         tree.update(cx, |tree, cx| {
             let tree = tree.as_local_mut().unwrap();
             let abs_path = tree.snapshot.abs_path.clone();
@@ -126,6 +127,7 @@ impl Worktree {
             .map(|c| c.to_ascii_lowercase())
             .collect();
         let root_name = worktree.root_name.clone();
+        let weak = worktree.weak;
         let (entries_by_path, entries_by_id, diagnostic_summaries) = cx
             .background()
             .spawn(async move {
@@ -225,6 +227,7 @@ impl Worktree {
                     queued_operations: Default::default(),
                     user_store,
                     diagnostic_summaries,
+                    weak,
                 })
             })
         });
@@ -271,6 +274,13 @@ impl Worktree {
         }
     }
 
+    pub fn is_weak(&self) -> bool {
+        match self {
+            Worktree::Local(worktree) => worktree.weak,
+            Worktree::Remote(worktree) => worktree.weak,
+        }
+    }
+
     pub fn replica_id(&self) -> ReplicaId {
         match self {
             Worktree::Local(_) => 0,
@@ -776,6 +786,7 @@ pub struct LocalWorktree {
     client: Arc<Client>,
     user_store: ModelHandle<UserStore>,
     fs: Arc<dyn Fs>,
+    weak: bool,
 }
 
 #[derive(Debug, Eq, PartialEq)]
@@ -803,6 +814,7 @@ pub struct RemoteWorktree {
     user_store: ModelHandle<UserStore>,
     queued_operations: Vec<(u64, Operation)>,
     diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
+    weak: bool,
 }
 
 type LoadingBuffers = HashMap<
@@ -822,6 +834,7 @@ impl LocalWorktree {
         client: Arc<Client>,
         user_store: ModelHandle<UserStore>,
         path: impl Into<Arc<Path>>,
+        weak: bool,
         fs: Arc<dyn Fs>,
         cx: &mut AsyncAppContext,
     ) -> Result<(ModelHandle<Worktree>, Sender<ScanState>)> {
@@ -889,6 +902,7 @@ impl LocalWorktree {
                 client,
                 user_store,
                 fs,
+                weak,
             };
 
             cx.spawn_weak(|this, mut cx| async move {
@@ -1415,10 +1429,11 @@ impl LocalWorktree {
         });
 
         let diagnostic_summaries = self.diagnostic_summaries.clone();
+        let weak = self.weak;
         let share_message = cx.background().spawn(async move {
             proto::ShareWorktree {
                 project_id,
-                worktree: Some(snapshot.to_proto(&diagnostic_summaries)),
+                worktree: Some(snapshot.to_proto(&diagnostic_summaries, weak)),
             }
         });
 
@@ -1636,6 +1651,7 @@ impl Snapshot {
     pub fn to_proto(
         &self,
         diagnostic_summaries: &TreeMap<PathKey, DiagnosticSummary>,
+        weak: bool,
     ) -> proto::Worktree {
         let root_name = self.root_name.clone();
         proto::Worktree {
@@ -1651,6 +1667,7 @@ impl Snapshot {
                 .iter()
                 .map(|(path, summary)| summary.to_proto(path.0.clone()))
                 .collect(),
+            weak,
         }
     }
 
@@ -3110,6 +3127,7 @@ mod tests {
             client,
             user_store,
             Arc::from(Path::new("/root")),
+            false,
             Arc::new(fs),
             &mut cx.to_async(),
         )
@@ -3147,6 +3165,7 @@ mod tests {
             client,
             user_store,
             dir.path(),
+            false,
             Arc::new(RealFs),
             &mut cx.to_async(),
         )
@@ -3181,6 +3200,7 @@ mod tests {
             client,
             user_store,
             file_path.clone(),
+            false,
             Arc::new(RealFs),
             &mut cx.to_async(),
         )
@@ -3229,6 +3249,7 @@ mod tests {
             client,
             user_store.clone(),
             dir.path(),
+            false,
             Arc::new(RealFs),
             &mut cx.to_async(),
         )
@@ -3265,7 +3286,7 @@ mod tests {
         let remote = Worktree::remote(
             1,
             1,
-            initial_snapshot.to_proto(&Default::default()),
+            initial_snapshot.to_proto(&Default::default(), Default::default()),
             Client::new(http_client.clone()),
             user_store,
             &mut cx.to_async(),
@@ -3379,6 +3400,7 @@ mod tests {
             client,
             user_store,
             dir.path(),
+            false,
             Arc::new(RealFs),
             &mut cx.to_async(),
         )
@@ -3431,6 +3453,7 @@ mod tests {
             client.clone(),
             user_store,
             "/the-dir".as_ref(),
+            false,
             fs,
             &mut cx.to_async(),
         )
@@ -3485,6 +3508,7 @@ mod tests {
             client,
             user_store,
             dir.path(),
+            false,
             Arc::new(RealFs),
             &mut cx.to_async(),
         )
@@ -3621,6 +3645,7 @@ mod tests {
             client,
             user_store,
             dir.path(),
+            false,
             Arc::new(RealFs),
             &mut cx.to_async(),
         )
@@ -3735,6 +3760,7 @@ mod tests {
             client.clone(),
             user_store,
             "/the-dir".as_ref(),
+            false,
             fs,
             &mut cx.to_async(),
         )

crates/project_panel/src/project_panel.rs 🔗

@@ -6,8 +6,8 @@ use gpui::{
     },
     keymap::{self, Binding},
     platform::CursorStyle,
-    AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext,
+    ViewHandle, WeakViewHandle,
 };
 use postage::watch;
 use project::{Project, ProjectEntry, ProjectPath, Worktree, WorktreeId};
@@ -24,7 +24,7 @@ use workspace::{
 pub struct ProjectPanel {
     project: ModelHandle<Project>,
     list: UniformListState,
-    visible_entries: Vec<Vec<usize>>,
+    visible_entries: Vec<(WorktreeId, Vec<usize>)>,
     expanded_dir_ids: HashMap<WorktreeId, Vec<usize>>,
     selection: Option<Selection>,
     settings: watch::Receiver<Settings>,
@@ -260,7 +260,11 @@ impl ProjectPanel {
     }
 
     fn select_first(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(worktree) = self.project.read(cx).worktrees().first() {
+        let worktree = self
+            .visible_entries
+            .first()
+            .and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx));
+        if let Some(worktree) = worktree {
             let worktree = worktree.read(cx);
             let worktree_id = worktree.id();
             if let Some(root_entry) = worktree.root_entry() {
@@ -289,10 +293,11 @@ impl ProjectPanel {
         let project = self.project.read(cx);
         let mut offset = None;
         let mut ix = 0;
-        for (worktree_ix, visible_entries) in self.visible_entries.iter().enumerate() {
+        for (worktree_id, visible_entries) in &self.visible_entries {
             if target_ix < ix + visible_entries.len() {
-                let worktree = project.worktrees()[worktree_ix].read(cx);
-                offset = Some((worktree, visible_entries[target_ix - ix]));
+                offset = project
+                    .worktree_for_id(*worktree_id, cx)
+                    .map(|w| (w.read(cx), visible_entries[target_ix - ix]));
                 break;
             } else {
                 ix += visible_entries.len();
@@ -318,7 +323,11 @@ impl ProjectPanel {
         new_selected_entry: Option<(WorktreeId, usize)>,
         cx: &mut ViewContext<Self>,
     ) {
-        let worktrees = self.project.read(cx).worktrees();
+        let worktrees = self
+            .project
+            .read(cx)
+            .worktrees(cx)
+            .filter(|worktree| !worktree.read(cx).is_weak());
         self.visible_entries.clear();
 
         let mut entry_ix = 0;
@@ -369,7 +378,8 @@ impl ProjectPanel {
                 }
                 entry_iter.advance();
             }
-            self.visible_entries.push(visible_worktree_entries);
+            self.visible_entries
+                .push((worktree_id, visible_worktree_entries));
         }
     }
 
@@ -404,16 +414,14 @@ impl ProjectPanel {
         }
     }
 
-    fn for_each_visible_entry<C: ReadModel>(
+    fn for_each_visible_entry(
         &self,
         range: Range<usize>,
-        cx: &mut C,
-        mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut C),
+        cx: &mut ViewContext<ProjectPanel>,
+        mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut ViewContext<ProjectPanel>),
     ) {
-        let project = self.project.read(cx);
-        let worktrees = project.worktrees().to_vec();
         let mut ix = 0;
-        for (worktree_ix, visible_worktree_entries) in self.visible_entries.iter().enumerate() {
+        for (worktree_id, visible_worktree_entries) in &self.visible_entries {
             if ix >= range.end {
                 return;
             }
@@ -423,37 +431,38 @@ impl ProjectPanel {
             }
 
             let end_ix = range.end.min(ix + visible_worktree_entries.len());
-            let worktree = &worktrees[worktree_ix];
-            let snapshot = worktree.read(cx).snapshot();
-            let expanded_entry_ids = self
-                .expanded_dir_ids
-                .get(&snapshot.id())
-                .map(Vec::as_slice)
-                .unwrap_or(&[]);
-            let root_name = OsStr::new(snapshot.root_name());
-            let mut cursor = snapshot.entries(false);
-
-            for ix in visible_worktree_entries[range.start.saturating_sub(ix)..end_ix - ix]
-                .iter()
-                .copied()
-            {
-                cursor.advance_to_offset(ix);
-                if let Some(entry) = cursor.entry() {
-                    let filename = entry.path.file_name().unwrap_or(root_name);
-                    let details = EntryDetails {
-                        filename: filename.to_string_lossy().to_string(),
-                        depth: entry.path.components().count(),
-                        is_dir: entry.is_dir(),
-                        is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(),
-                        is_selected: self.selection.map_or(false, |e| {
-                            e.worktree_id == snapshot.id() && e.entry_id == entry.id
-                        }),
-                    };
-                    let entry = ProjectEntry {
-                        worktree_id: snapshot.id(),
-                        entry_id: entry.id,
-                    };
-                    callback(entry, details, cx);
+            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
+                let snapshot = worktree.read(cx).snapshot();
+                let expanded_entry_ids = self
+                    .expanded_dir_ids
+                    .get(&snapshot.id())
+                    .map(Vec::as_slice)
+                    .unwrap_or(&[]);
+                let root_name = OsStr::new(snapshot.root_name());
+                let mut cursor = snapshot.entries(false);
+
+                for ix in visible_worktree_entries[range.start.saturating_sub(ix)..end_ix - ix]
+                    .iter()
+                    .copied()
+                {
+                    cursor.advance_to_offset(ix);
+                    if let Some(entry) = cursor.entry() {
+                        let filename = entry.path.file_name().unwrap_or(root_name);
+                        let details = EntryDetails {
+                            filename: filename.to_string_lossy().to_string(),
+                            depth: entry.path.components().count(),
+                            is_dir: entry.is_dir(),
+                            is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(),
+                            is_selected: self.selection.map_or(false, |e| {
+                                e.worktree_id == snapshot.id() && e.entry_id == entry.id
+                            }),
+                        };
+                        let entry = ProjectEntry {
+                            worktree_id: snapshot.id(),
+                            entry_id: entry.id,
+                        };
+                        callback(entry, details, cx);
+                    }
                 }
             }
             ix = end_ix;
@@ -545,7 +554,7 @@ impl View for ProjectPanel {
             self.list.clone(),
             self.visible_entries
                 .iter()
-                .map(|worktree_entries| worktree_entries.len())
+                .map(|(_, worktree_entries)| worktree_entries.len())
                 .sum(),
             move |range, items, cx| {
                 let theme = &settings.borrow().theme.project_panel;
@@ -633,18 +642,18 @@ mod tests {
                 cx,
             )
         });
-        let root1 = project
+        let (root1, _) = project
             .update(&mut cx, |project, cx| {
-                project.add_local_worktree("/root1", cx)
+                project.find_or_create_worktree_for_abs_path("/root1", false, cx)
             })
             .await
             .unwrap();
         root1
             .read_with(&cx, |t, _| t.as_local().unwrap().scan_complete())
             .await;
-        let root2 = project
+        let (root2, _) = project
             .update(&mut cx, |project, cx| {
-                project.add_local_worktree("/root2", cx)
+                project.find_or_create_worktree_for_abs_path("/root2", false, cx)
             })
             .await
             .unwrap();
@@ -827,7 +836,7 @@ mod tests {
         ) {
             let path = path.as_ref();
             panel.update(cx, |panel, cx| {
-                for worktree in panel.project.read(cx).worktrees() {
+                for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
                     let worktree = worktree.read(cx);
                     if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
                         let entry_id = worktree.entry_for_path(relative_path).unwrap().id;

crates/rpc/proto/zed.proto 🔗

@@ -272,6 +272,7 @@ message Worktree {
     string root_name = 2;
     repeated Entry entries = 3;
     repeated DiagnosticSummary diagnostic_summaries = 4;
+    bool weak = 5;
 }
 
 message Entry {

crates/server/src/rpc.rs 🔗

@@ -309,6 +309,7 @@ impl Server {
                                 .values()
                                 .cloned()
                                 .collect(),
+                            weak: worktree.weak,
                         })
                     })
                     .collect();
@@ -421,6 +422,7 @@ impl Server {
                 authorized_user_ids: contact_user_ids.clone(),
                 root_name: request.payload.root_name,
                 share: None,
+                weak: false,
             },
         );
 
@@ -1158,8 +1160,10 @@ mod tests {
                 cx,
             )
         });
-        let worktree_a = project_a
-            .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx))
+        let (worktree_a, _) = project_a
+            .update(&mut cx_a, |p, cx| {
+                p.find_or_create_worktree_for_abs_path("/a", false, cx)
+            })
             .await
             .unwrap();
         worktree_a
@@ -1184,7 +1188,7 @@ mod tests {
         )
         .await
         .unwrap();
-        let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone());
+        let worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap());
 
         let replica_id_b = project_b.read_with(&cx_b, |project, _| {
             assert_eq!(
@@ -1293,8 +1297,10 @@ mod tests {
                 cx,
             )
         });
-        let worktree_a = project_a
-            .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx))
+        let (worktree_a, _) = project_a
+            .update(&mut cx_a, |p, cx| {
+                p.find_or_create_worktree_for_abs_path("/a", false, cx)
+            })
             .await
             .unwrap();
         worktree_a
@@ -1321,7 +1327,7 @@ mod tests {
         .await
         .unwrap();
 
-        let worktree_b = project_b.read_with(&cx_b, |p, _| p.worktrees()[0].clone());
+        let worktree_b = project_b.read_with(&cx_b, |p, cx| p.worktrees(cx).next().unwrap());
         worktree_b
             .update(&mut cx_b, |tree, cx| tree.open_buffer("a.txt", cx))
             .await
@@ -1353,7 +1359,7 @@ mod tests {
         )
         .await
         .unwrap();
-        let worktree_c = project_c.read_with(&cx_b, |p, _| p.worktrees()[0].clone());
+        let worktree_c = project_c.read_with(&cx_b, |p, cx| p.worktrees(cx).next().unwrap());
         worktree_c
             .update(&mut cx_b, |tree, cx| tree.open_buffer("a.txt", cx))
             .await
@@ -1395,8 +1401,10 @@ mod tests {
                 cx,
             )
         });
-        let worktree_a = project_a
-            .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx))
+        let (worktree_a, _) = project_a
+            .update(&mut cx_a, |p, cx| {
+                p.find_or_create_worktree_for_abs_path("/a", false, cx)
+            })
             .await
             .unwrap();
         worktree_a
@@ -1433,8 +1441,8 @@ mod tests {
         .unwrap();
 
         // Open and edit a buffer as both guests B and C.
-        let worktree_b = project_b.read_with(&cx_b, |p, _| p.worktrees()[0].clone());
-        let worktree_c = project_c.read_with(&cx_c, |p, _| p.worktrees()[0].clone());
+        let worktree_b = project_b.read_with(&cx_b, |p, cx| p.worktrees(cx).next().unwrap());
+        let worktree_c = project_c.read_with(&cx_c, |p, cx| p.worktrees(cx).next().unwrap());
         let buffer_b = worktree_b
             .update(&mut cx_b, |tree, cx| tree.open_buffer("file1", cx))
             .await
@@ -1547,8 +1555,10 @@ mod tests {
                 cx,
             )
         });
-        let worktree_a = project_a
-            .update(&mut cx_a, |p, cx| p.add_local_worktree("/dir", cx))
+        let (worktree_a, _) = project_a
+            .update(&mut cx_a, |p, cx| {
+                p.find_or_create_worktree_for_abs_path("/dir", false, cx)
+            })
             .await
             .unwrap();
         worktree_a
@@ -1573,7 +1583,7 @@ mod tests {
         )
         .await
         .unwrap();
-        let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone());
+        let worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap());
 
         // Open a buffer as client B
         let buffer_b = worktree_b
@@ -1642,8 +1652,10 @@ mod tests {
                 cx,
             )
         });
-        let worktree_a = project_a
-            .update(&mut cx_a, |p, cx| p.add_local_worktree("/dir", cx))
+        let (worktree_a, _) = project_a
+            .update(&mut cx_a, |p, cx| {
+                p.find_or_create_worktree_for_abs_path("/dir", false, cx)
+            })
             .await
             .unwrap();
         worktree_a
@@ -1668,7 +1680,7 @@ mod tests {
         )
         .await
         .unwrap();
-        let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone());
+        let worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap());
 
         // Open a buffer as client A
         let buffer_a = worktree_a
@@ -1723,8 +1735,10 @@ mod tests {
                 cx,
             )
         });
-        let worktree_a = project_a
-            .update(&mut cx_a, |p, cx| p.add_local_worktree("/dir", cx))
+        let (worktree_a, _) = project_a
+            .update(&mut cx_a, |p, cx| {
+                p.find_or_create_worktree_for_abs_path("/dir", false, cx)
+            })
             .await
             .unwrap();
         worktree_a
@@ -1749,7 +1763,7 @@ mod tests {
         )
         .await
         .unwrap();
-        let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone());
+        let worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap());
 
         // See that a guest has joined as client A.
         project_a
@@ -1799,8 +1813,10 @@ mod tests {
                 cx,
             )
         });
-        let worktree_a = project_a
-            .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx))
+        let (worktree_a, _) = project_a
+            .update(&mut cx_a, |p, cx| {
+                p.find_or_create_worktree_for_abs_path("/a", false, cx)
+            })
             .await
             .unwrap();
         worktree_a
@@ -1886,8 +1902,10 @@ mod tests {
                 cx,
             )
         });
-        let worktree_a = project_a
-            .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx))
+        let (worktree_a, _) = project_a
+            .update(&mut cx_a, |p, cx| {
+                p.find_or_create_worktree_for_abs_path("/a", false, cx)
+            })
             .await
             .unwrap();
         worktree_a
@@ -2023,7 +2041,7 @@ mod tests {
             .await;
 
         // Open the file with the errors on client B. They should be present.
-        let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone());
+        let worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap());
         let buffer_b = cx_b
             .background()
             .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.rs", cx)))
@@ -2108,8 +2126,10 @@ mod tests {
                 cx,
             )
         });
-        let worktree_a = project_a
-            .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx))
+        let (worktree_a, _) = project_a
+            .update(&mut cx_a, |p, cx| {
+                p.find_or_create_worktree_for_abs_path("/a", false, cx)
+            })
             .await
             .unwrap();
         worktree_a
@@ -2136,7 +2156,7 @@ mod tests {
         .unwrap();
 
         // Open the file to be formatted on client B.
-        let worktree_b = project_b.update(&mut cx_b, |p, _| p.worktrees()[0].clone());
+        let worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap());
         let buffer_b = cx_b
             .background()
             .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.rs", cx)))
@@ -2616,8 +2636,10 @@ mod tests {
                 cx,
             )
         });
-        let worktree_a = project_a
-            .update(&mut cx_a, |p, cx| p.add_local_worktree("/a", cx))
+        let (worktree_a, _) = project_a
+            .update(&mut cx_a, |p, cx| {
+                p.find_or_create_worktree_for_abs_path("/a", false, cx)
+            })
             .await
             .unwrap();
         worktree_a

crates/server/src/rpc/store.rs 🔗

@@ -31,6 +31,7 @@ pub struct Worktree {
     pub authorized_user_ids: Vec<UserId>,
     pub root_name: String,
     pub share: Option<WorktreeShare>,
+    pub weak: bool,
 }
 
 #[derive(Default)]
@@ -202,6 +203,7 @@ impl Store {
                 let mut worktree_root_names = project
                     .worktrees
                     .values()
+                    .filter(|worktree| !worktree.weak)
                     .map(|worktree| worktree.root_name.clone())
                     .collect::<Vec<_>>();
                 worktree_root_names.sort_unstable();

crates/workspace/src/workspace.rs 🔗

@@ -639,8 +639,11 @@ impl Workspace {
         &self.project
     }
 
-    pub fn worktrees<'a>(&self, cx: &'a AppContext) -> &'a [ModelHandle<Worktree>] {
-        &self.project.read(cx).worktrees()
+    pub fn worktrees<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
+        self.project.read(cx).worktrees(cx)
     }
 
     pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool {
@@ -660,7 +663,6 @@ impl Workspace {
     pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
         let futures = self
             .worktrees(cx)
-            .iter()
             .filter_map(|worktree| worktree.read(cx).as_local())
             .map(|worktree| worktree.scan_complete())
             .collect::<Vec<_>>();
@@ -720,7 +722,7 @@ impl Workspace {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<ProjectPath>> {
         let entry = self.project().update(cx, |project, cx| {
-            project.find_or_create_worktree_for_abs_path(abs_path, cx)
+            project.find_or_create_worktree_for_abs_path(abs_path, false, cx)
         });
         cx.spawn(|_, cx| async move {
             let (worktree, path) = entry.await?;
@@ -731,15 +733,6 @@ impl Workspace {
         })
     }
 
-    pub fn add_worktree(
-        &self,
-        path: &Path,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Result<ModelHandle<Worktree>>> {
-        self.project
-            .update(cx, |project, cx| project.add_local_worktree(path, cx))
-    }
-
     pub fn toggle_modal<V, F>(&mut self, cx: &mut ViewContext<Self>, add_view: F)
     where
         V: 'static + View,
@@ -866,7 +859,7 @@ impl Workspace {
                     item.save(cx)
                 }
             } else if item.can_save_as(cx) {
-                let worktree = self.worktrees(cx).first();
+                let worktree = self.worktrees(cx).next();
                 let start_abs_path = worktree
                     .and_then(|w| w.read(cx).as_local())
                     .map_or(Path::new(""), |w| w.abs_path())
@@ -1343,7 +1336,6 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
     fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
         self.read(cx)
             .worktrees(cx)
-            .iter()
             .flat_map(|worktree| {
                 let worktree_id = worktree.read(cx).id();
                 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {

crates/zed/src/zed.rs 🔗

@@ -175,7 +175,7 @@ mod tests {
         assert_eq!(cx.window_ids().len(), 1);
         let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
         workspace_1.read_with(&cx, |workspace, cx| {
-            assert_eq!(workspace.worktrees(cx).len(), 2)
+            assert_eq!(workspace.worktrees(cx).count(), 2)
         });
 
         cx.update(|cx| {
@@ -242,9 +242,10 @@ mod tests {
             .await;
         let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
         let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        workspace
-            .update(&mut cx, |workspace, cx| {
-                workspace.add_worktree(Path::new("/root"), cx)
+        params
+            .project
+            .update(&mut cx, |project, cx| {
+                project.find_or_create_worktree_for_abs_path(Path::new("/root"), false, cx)
             })
             .await
             .unwrap();
@@ -366,9 +367,10 @@ mod tests {
 
         let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
         let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        workspace
-            .update(&mut cx, |workspace, cx| {
-                workspace.add_worktree("/dir1".as_ref(), cx)
+        params
+            .project
+            .update(&mut cx, |project, cx| {
+                project.find_or_create_worktree_for_abs_path(Path::new("/dir1"), false, cx)
             })
             .await
             .unwrap();
@@ -402,7 +404,6 @@ mod tests {
             let worktree_roots = workspace
                 .read(cx)
                 .worktrees(cx)
-                .iter()
                 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
                 .collect::<HashSet<_>>();
             assert_eq!(
@@ -433,9 +434,10 @@ mod tests {
 
         let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
         let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        workspace
-            .update(&mut cx, |workspace, cx| {
-                workspace.add_worktree(Path::new("/root"), cx)
+        params
+            .project
+            .update(&mut cx, |project, cx| {
+                project.find_or_create_worktree_for_abs_path(Path::new("/root"), false, cx)
             })
             .await
             .unwrap();
@@ -481,21 +483,14 @@ mod tests {
         app_state.fs.as_fake().insert_dir("/root").await.unwrap();
         let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
         let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        workspace
-            .update(&mut cx, |workspace, cx| {
-                workspace.add_worktree(Path::new("/root"), cx)
+        params
+            .project
+            .update(&mut cx, |project, cx| {
+                project.find_or_create_worktree_for_abs_path(Path::new("/root"), false, cx)
             })
             .await
             .unwrap();
-        let worktree = cx.read(|cx| {
-            workspace
-                .read(cx)
-                .worktrees(cx)
-                .iter()
-                .next()
-                .unwrap()
-                .clone()
-        });
+        let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
 
         // Create a new untitled buffer
         cx.dispatch_action(window_id, vec![workspace.id()], OpenNew(app_state.clone()));
@@ -640,9 +635,10 @@ mod tests {
 
         let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
         let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        workspace
-            .update(&mut cx, |workspace, cx| {
-                workspace.add_worktree(Path::new("/root"), cx)
+        params
+            .project
+            .update(&mut cx, |project, cx| {
+                project.find_or_create_worktree_for_abs_path(Path::new("/root"), false, cx)
             })
             .await
             .unwrap();
@@ -717,9 +713,10 @@ mod tests {
             .await;
         let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
         let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        workspace
-            .update(&mut cx, |workspace, cx| {
-                workspace.add_worktree(Path::new("/root"), cx)
+        params
+            .project
+            .update(&mut cx, |project, cx| {
+                project.find_or_create_worktree_for_abs_path(Path::new("/root"), false, cx)
             })
             .await
             .unwrap();