Merge pull request #354 from zed-industries/go-to-definition

Nathan Sobo created

Go to definition

Change summary

crates/diagnostics/src/diagnostics.rs     | 116 ++
crates/diagnostics/src/items.rs           |   2 
crates/editor/src/display_map.rs          |  14 
crates/editor/src/editor.rs               | 150 +++-
crates/editor/src/items.rs                |  78 -
crates/editor/src/multi_buffer.rs         |  21 
crates/file_finder/src/file_finder.rs     |  25 
crates/go_to_line/src/go_to_line.rs       |  18 
crates/gpui/src/app.rs                    | 239 ++++--
crates/gpui/src/platform.rs               |  18 
crates/gpui/src/platform/mac/platform.rs  |  26 
crates/gpui/src/platform/mac/window.rs    |  12 
crates/gpui/src/platform/test.rs          |  30 
crates/journal/src/journal.rs             |   2 
crates/language/src/buffer.rs             |  37 
crates/language/src/tests.rs              |  28 
crates/lsp/src/lsp.rs                     |  80 +-
crates/outline/src/outline.rs             |   2 
crates/project/src/project.rs             | 730 +++++++++++++++++----
crates/project/src/worktree.rs            | 839 +++++++++---------------
crates/project_panel/src/project_panel.rs | 113 +-
crates/rpc/proto/zed.proto                |   3 
crates/server/src/rpc.rs                  | 124 ++-
crates/server/src/rpc/store.rs            |   2 
crates/workspace/src/pane.rs              | 109 +-
crates/workspace/src/workspace.rs         | 238 +++---
crates/zed/src/zed.rs                     | 134 +--
27 files changed, 1,828 insertions(+), 1,362 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -9,15 +9,22 @@ use editor::{
     Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset,
 };
 use gpui::{
-    action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
-    RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    action, elements::*, keymap::Binding, AnyViewHandle, AppContext, Entity, ModelHandle,
+    MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point, Selection, SelectionGoal};
 use postage::watch;
-use project::{Project, ProjectPath, WorktreeId};
-use std::{cmp::Ordering, mem, ops::Range, rc::Rc, sync::Arc};
+use project::{Project, ProjectPath};
+use std::{
+    any::{Any, TypeId},
+    cmp::Ordering,
+    mem,
+    ops::Range,
+    path::PathBuf,
+    sync::Arc,
+};
 use util::TryFutureExt;
-use workspace::{Navigation, Workspace};
+use workspace::{ItemNavHistory, Workspace};
 
 action!(Deploy);
 action!(OpenExcerpts);
@@ -49,7 +56,7 @@ struct ProjectDiagnosticsEditor {
     editor: ViewHandle<Editor>,
     excerpts: ModelHandle<MultiBuffer>,
     path_states: Vec<PathState>,
-    paths_to_update: HashMap<WorktreeId, BTreeSet<ProjectPath>>,
+    paths_to_update: BTreeSet<ProjectPath>,
     build_settings: BuildSettings,
     settings: watch::Receiver<workspace::Settings>,
 }
@@ -119,16 +126,12 @@ impl ProjectDiagnosticsEditor {
     ) -> Self {
         let project = model.read(cx).project.clone();
         cx.subscribe(&project, |this, _, event, cx| match event {
-            project::Event::DiskBasedDiagnosticsUpdated { worktree_id } => {
-                if let Some(paths) = this.paths_to_update.remove(&worktree_id) {
-                    this.update_excerpts(paths, cx);
-                }
+            project::Event::DiskBasedDiagnosticsFinished => {
+                let paths = mem::take(&mut this.paths_to_update);
+                this.update_excerpts(paths, cx);
             }
             project::Event::DiagnosticsUpdated(path) => {
-                this.paths_to_update
-                    .entry(path.worktree_id)
-                    .or_default()
-                    .insert(path.clone());
+                this.paths_to_update.insert(path.clone());
             }
             _ => {}
         })
@@ -198,7 +201,6 @@ impl ProjectDiagnosticsEditor {
                     }
                     let editor = workspace
                         .open_item(buffer, cx)
-                        .to_any()
                         .downcast::<Editor>()
                         .unwrap();
                     editor.update(cx, |editor, cx| {
@@ -522,10 +524,19 @@ impl workspace::Item for ProjectDiagnostics {
     fn build_view(
         handle: ModelHandle<Self>,
         workspace: &Workspace,
-        _: Rc<Navigation>,
+        nav_history: ItemNavHistory,
         cx: &mut ViewContext<Self::View>,
     ) -> Self::View {
-        ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), workspace.settings(), cx)
+        let diagnostics = ProjectDiagnosticsEditor::new(
+            handle,
+            workspace.weak_handle(),
+            workspace.settings(),
+            cx,
+        );
+        diagnostics
+            .editor
+            .update(cx, |editor, _| editor.set_nav_history(Some(nav_history)));
+        diagnostics
     }
 
     fn project_path(&self) -> Option<project::ProjectPath> {
@@ -548,6 +559,11 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
         None
     }
 
+    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
+        self.editor
+            .update(cx, |editor, cx| editor.navigate(data, cx));
+    }
+
     fn is_dirty(&self, cx: &AppContext) -> bool {
         self.excerpts.read(cx).read(cx).is_dirty()
     }
@@ -560,7 +576,7 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
         true
     }
 
-    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
+    fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
         self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
     }
 
@@ -570,8 +586,8 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
 
     fn save_as(
         &mut self,
-        _: ModelHandle<project::Worktree>,
-        _: &std::path::Path,
+        _: ModelHandle<Project>,
+        _: PathBuf,
         _: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
         unreachable!()
@@ -592,12 +608,40 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
     where
         Self: Sized,
     {
-        Some(ProjectDiagnosticsEditor::new(
+        let diagnostics = ProjectDiagnosticsEditor::new(
             self.model.clone(),
             self.workspace.clone(),
             self.settings.clone(),
             cx,
-        ))
+        );
+        diagnostics.editor.update(cx, |editor, cx| {
+            let nav_history = self
+                .editor
+                .read(cx)
+                .nav_history()
+                .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
+            editor.set_nav_history(nav_history);
+        });
+        Some(diagnostics)
+    }
+
+    fn act_as_type(
+        &self,
+        type_id: TypeId,
+        self_handle: &ViewHandle<Self>,
+        _: &AppContext,
+    ) -> Option<AnyViewHandle> {
+        if type_id == TypeId::of::<Self>() {
+            Some(self_handle.into())
+        } else if type_id == TypeId::of::<Editor>() {
+            Some((&self.editor).into())
+        } else {
+            None
+        }
+    }
+
+    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
     }
 }
 
@@ -680,7 +724,6 @@ mod tests {
     use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot};
     use gpui::TestAppContext;
     use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
-    use project::worktree;
     use serde_json::json;
     use std::sync::Arc;
     use unindent::Unindent as _;
@@ -721,16 +764,19 @@ 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();
+        let worktree_id = worktree.read_with(&cx, |tree, _| tree.id());
 
         // Create some diagnostics
         worktree.update(&mut cx, |worktree, cx| {
             worktree
+                .as_local_mut()
+                .unwrap()
                 .update_diagnostic_entries(
                     Arc::from("/test/main.rs".as_ref()),
                     None,
@@ -882,6 +928,8 @@ mod tests {
         // Diagnostics are added for another earlier path.
         worktree.update(&mut cx, |worktree, cx| {
             worktree
+                .as_local_mut()
+                .unwrap()
                 .update_diagnostic_entries(
                     Arc::from("/test/consts.rs".as_ref()),
                     None,
@@ -899,7 +947,13 @@ mod tests {
                     cx,
                 )
                 .unwrap();
-            cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
+        });
+        project.update(&mut cx, |_, cx| {
+            cx.emit(project::Event::DiagnosticsUpdated(ProjectPath {
+                worktree_id,
+                path: Arc::from("/test/consts.rs".as_ref()),
+            }));
+            cx.emit(project::Event::DiskBasedDiagnosticsFinished);
         });
 
         view.next_notification(&cx).await;
@@ -980,6 +1034,8 @@ mod tests {
         // Diagnostics are added to the first path
         worktree.update(&mut cx, |worktree, cx| {
             worktree
+                .as_local_mut()
+                .unwrap()
                 .update_diagnostic_entries(
                     Arc::from("/test/consts.rs".as_ref()),
                     None,
@@ -1011,7 +1067,13 @@ mod tests {
                     cx,
                 )
                 .unwrap();
-            cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
+        });
+        project.update(&mut cx, |_, cx| {
+            cx.emit(project::Event::DiagnosticsUpdated(ProjectPath {
+                worktree_id,
+                path: Arc::from("/test/consts.rs".as_ref()),
+            }));
+            cx.emit(project::Event::DiskBasedDiagnosticsFinished);
         });
 
         view.next_notification(&cx).await;

crates/diagnostics/src/items.rs 🔗

@@ -19,7 +19,7 @@ impl DiagnosticSummary {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         cx.subscribe(project, |this, project, event, cx| match event {
-            project::Event::DiskBasedDiagnosticsUpdated { .. } => {
+            project::Event::DiskBasedDiagnosticsUpdated => {
                 this.summary = project.read(cx).diagnostic_summary(cx);
                 cx.notify();
             }

crates/editor/src/display_map.rs 🔗

@@ -840,7 +840,7 @@ mod tests {
             ("mod.body".to_string(), Color::red().into()),
             ("fn.name".to_string(), Color::blue().into()),
         ]);
-        let lang = Arc::new(
+        let language = Arc::new(
             Language::new(
                 LanguageConfig {
                     name: "Test".to_string(),
@@ -857,10 +857,9 @@ mod tests {
             )
             .unwrap(),
         );
-        lang.set_theme(&theme);
+        language.set_theme(&theme);
 
-        let buffer =
-            cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Some(lang), None, cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
         buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
 
@@ -928,7 +927,7 @@ mod tests {
             ("mod.body".to_string(), Color::red().into()),
             ("fn.name".to_string(), Color::blue().into()),
         ]);
-        let lang = Arc::new(
+        let language = Arc::new(
             Language::new(
                 LanguageConfig {
                     name: "Test".to_string(),
@@ -945,10 +944,9 @@ mod tests {
             )
             .unwrap(),
         );
-        lang.set_theme(&theme);
+        language.set_theme(&theme);
 
-        let buffer =
-            cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Some(lang), None, cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
         buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
 

crates/editor/src/editor.rs 🔗

@@ -25,8 +25,8 @@ use gpui::{
 use items::BufferItemHandle;
 use itertools::Itertools as _;
 use language::{
-    BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal,
-    TransactionId,
+    AnchorRangeExt as _, BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point,
+    Selection, SelectionGoal, TransactionId,
 };
 pub use multi_buffer::{
     Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint,
@@ -41,7 +41,6 @@ use std::{
     iter::{self, FromIterator},
     mem,
     ops::{Deref, Range, RangeInclusive, Sub},
-    rc::Rc,
     sync::Arc,
     time::{Duration, Instant},
 };
@@ -49,7 +48,7 @@ use sum_tree::Bias;
 use text::rope::TextDimension;
 use theme::{DiagnosticStyle, EditorStyle};
 use util::post_inc;
-use workspace::{Navigation, PathOpener, Workspace};
+use workspace::{ItemNavHistory, PathOpener, Workspace};
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const MAX_LINE_LEN: usize = 1024;
@@ -107,6 +106,7 @@ action!(SelectLargerSyntaxNode);
 action!(SelectSmallerSyntaxNode);
 action!(MoveToEnclosingBracket);
 action!(ShowNextDiagnostic);
+action!(GoToDefinition);
 action!(PageUp);
 action!(PageDown);
 action!(Fold);
@@ -214,6 +214,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
         Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")),
         Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")),
         Binding::new("f8", ShowNextDiagnostic, Some("Editor")),
+        Binding::new("f12", GoToDefinition, Some("Editor")),
         Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")),
         Binding::new("pageup", PageUp, Some("Editor")),
         Binding::new("pagedown", PageDown, Some("Editor")),
@@ -277,6 +278,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpene
     cx.add_action(Editor::select_smaller_syntax_node);
     cx.add_action(Editor::move_to_enclosing_bracket);
     cx.add_action(Editor::show_next_diagnostic);
+    cx.add_action(Editor::go_to_definition);
     cx.add_action(Editor::page_up);
     cx.add_action(Editor::page_down);
     cx.add_action(Editor::fold);
@@ -379,7 +381,7 @@ pub struct Editor {
     mode: EditorMode,
     placeholder_text: Option<Arc<str>>,
     highlighted_rows: Option<Range<u32>>,
-    navigation: Option<Rc<Navigation>>,
+    nav_history: Option<ItemNavHistory>,
 }
 
 pub struct EditorSnapshot {
@@ -465,7 +467,10 @@ impl Editor {
         let mut clone = Self::new(self.buffer.clone(), self.build_settings.clone(), cx);
         clone.scroll_position = self.scroll_position;
         clone.scroll_top_anchor = self.scroll_top_anchor.clone();
-        clone.navigation = self.navigation.clone();
+        clone.nav_history = self
+            .nav_history
+            .as_ref()
+            .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
         clone
     }
 
@@ -515,7 +520,7 @@ impl Editor {
             mode: EditorMode::Full,
             placeholder_text: None,
             highlighted_rows: None,
-            navigation: None,
+            nav_history: None,
         };
         let selection = Selection {
             id: post_inc(&mut this.next_selection_id),
@@ -533,9 +538,8 @@ impl Editor {
         _: &workspace::OpenNew,
         cx: &mut ViewContext<Workspace>,
     ) {
-        let buffer = cx.add_model(|cx| {
-            Buffer::new(0, "", cx).with_language(Some(language::PLAIN_TEXT.clone()), None, cx)
-        });
+        let buffer = cx
+            .add_model(|cx| Buffer::new(0, "", cx).with_language(language::PLAIN_TEXT.clone(), cx));
         workspace.open_item(BufferItemHandle(buffer), cx);
     }
 
@@ -860,7 +864,7 @@ impl Editor {
             }
         }
 
-        self.push_to_navigation_history(newest_selection.head(), Some(end.to_point(&buffer)), cx);
+        self.push_to_nav_history(newest_selection.head(), Some(end.to_point(&buffer)), cx);
 
         let selection = Selection {
             id: post_inc(&mut self.next_selection_id),
@@ -2455,13 +2459,21 @@ impl Editor {
         self.update_selections(vec![selection], Some(Autoscroll::Fit), cx);
     }
 
-    fn push_to_navigation_history(
+    pub fn set_nav_history(&mut self, nav_history: Option<ItemNavHistory>) {
+        self.nav_history = nav_history;
+    }
+
+    pub fn nav_history(&self) -> Option<&ItemNavHistory> {
+        self.nav_history.as_ref()
+    }
+
+    fn push_to_nav_history(
         &self,
         position: Anchor,
         new_position: Option<Point>,
         cx: &mut ViewContext<Self>,
     ) {
-        if let Some(navigation) = &self.navigation {
+        if let Some(nav_history) = &self.nav_history {
             let buffer = self.buffer.read(cx).read(cx);
             let offset = position.to_offset(&buffer);
             let point = position.to_point(&buffer);
@@ -2474,13 +2486,10 @@ impl Editor {
                 }
             }
 
-            navigation.push(
-                Some(NavigationData {
-                    anchor: position,
-                    offset,
-                }),
-                cx,
-            );
+            nav_history.push(Some(NavigationData {
+                anchor: position,
+                offset,
+            }));
         }
     }
 
@@ -2985,6 +2994,61 @@ impl Editor {
         }
     }
 
+    pub fn go_to_definition(
+        workspace: &mut Workspace,
+        _: &GoToDefinition,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let active_item = workspace.active_item(cx);
+        let editor_handle = if let Some(editor) = active_item
+            .as_ref()
+            .and_then(|item| item.act_as::<Self>(cx))
+        {
+            editor
+        } else {
+            return;
+        };
+
+        let editor = editor_handle.read(cx);
+        let buffer = editor.buffer.read(cx);
+        let head = editor.newest_selection::<usize>(&buffer.read(cx)).head();
+        let (buffer, head) = editor.buffer.read(cx).text_anchor_for_position(head, cx);
+        let definitions = workspace
+            .project()
+            .update(cx, |project, cx| project.definition(&buffer, head, cx));
+        cx.spawn(|workspace, mut cx| async move {
+            let definitions = definitions.await?;
+            workspace.update(&mut cx, |workspace, cx| {
+                for definition in definitions {
+                    let range = definition
+                        .target_range
+                        .to_offset(definition.target_buffer.read(cx));
+                    let target_editor_handle = workspace
+                        .open_item(BufferItemHandle(definition.target_buffer), cx)
+                        .downcast::<Self>()
+                        .unwrap();
+
+                    target_editor_handle.update(cx, |target_editor, cx| {
+                        // When selecting a definition in a different buffer, disable the nav history
+                        // to avoid creating a history entry at the previous cursor location.
+                        let disabled_history = if editor_handle == target_editor_handle {
+                            None
+                        } else {
+                            target_editor.nav_history.take()
+                        };
+                        target_editor.select_ranges([range], Some(Autoscroll::Center), cx);
+                        if disabled_history.is_some() {
+                            target_editor.nav_history = disabled_history;
+                        }
+                    });
+                }
+            });
+
+            Ok::<(), anyhow::Error>(())
+        })
+        .detach_and_log_err(cx);
+    }
+
     fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext<Editor>) {
         if let Some(active_diagnostics) = self.active_diagnostics.as_mut() {
             let buffer = self.buffer.read(cx).snapshot(cx);
@@ -3330,7 +3394,7 @@ impl Editor {
                 .max_by_key(|s| s.id)
                 .map(|s| s.head().to_point(&buffer));
             if new_cursor_position.is_some() {
-                self.push_to_navigation_history(old_cursor_position, new_cursor_position, cx);
+                self.push_to_nav_history(old_cursor_position, new_cursor_position, cx);
             }
         }
 
@@ -3995,7 +4059,7 @@ pub fn settings_builder(
 mod tests {
     use super::*;
     use language::LanguageConfig;
-    use std::time::Instant;
+    use std::{cell::RefCell, rc::Rc, time::Instant};
     use text::Point;
     use unindent::Unindent;
     use util::test::sample_text;
@@ -4174,22 +4238,22 @@ mod tests {
     fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
         cx.add_window(Default::default(), |cx| {
             use workspace::ItemView;
-            let navigation = Rc::new(workspace::Navigation::default());
+            let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default()));
             let settings = EditorSettings::test(&cx);
             let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx);
             let mut editor = build_editor(buffer.clone(), settings, cx);
-            editor.navigation = Some(navigation.clone());
+            editor.nav_history = Some(ItemNavHistory::new(nav_history.clone(), &cx.handle()));
 
             // Move the cursor a small distance.
             // Nothing is added to the navigation history.
             editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
             editor.select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx);
-            assert!(navigation.pop_backward().is_none());
+            assert!(nav_history.borrow_mut().pop_backward().is_none());
 
             // Move the cursor a large distance.
             // The history can jump back to the previous position.
             editor.select_display_ranges(&[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)], cx);
-            let nav_entry = navigation.pop_backward().unwrap();
+            let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
             editor.navigate(nav_entry.data.unwrap(), cx);
             assert_eq!(nav_entry.item_view.id(), cx.view_id());
             assert_eq!(
@@ -4205,7 +4269,7 @@ mod tests {
                 editor.selected_display_ranges(cx),
                 &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
             );
-            assert!(navigation.pop_backward().is_none());
+            assert!(nav_history.borrow_mut().pop_backward().is_none());
 
             // Move the cursor a large distance via the mouse.
             // The history can jump back to the previous position.
@@ -4215,7 +4279,7 @@ mod tests {
                 editor.selected_display_ranges(cx),
                 &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
             );
-            let nav_entry = navigation.pop_backward().unwrap();
+            let nav_entry = nav_history.borrow_mut().pop_backward().unwrap();
             editor.navigate(nav_entry.data.unwrap(), cx);
             assert_eq!(nav_entry.item_view.id(), cx.view_id());
             assert_eq!(
@@ -5746,10 +5810,10 @@ mod tests {
     #[gpui::test]
     async fn test_select_larger_smaller_syntax_node(mut cx: gpui::TestAppContext) {
         let settings = cx.read(EditorSettings::test);
-        let language = Some(Arc::new(Language::new(
+        let language = Arc::new(Language::new(
             LanguageConfig::default(),
             Some(tree_sitter_rust::language()),
-        )));
+        ));
 
         let text = r#"
             use mod1::mod2::{mod3, mod4};
@@ -5760,7 +5824,7 @@ mod tests {
         "#
         .unindent();
 
-        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
         let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx));
         view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
@@ -5887,7 +5951,7 @@ mod tests {
     #[gpui::test]
     async fn test_autoindent_selections(mut cx: gpui::TestAppContext) {
         let settings = cx.read(EditorSettings::test);
-        let language = Some(Arc::new(
+        let language = Arc::new(
             Language::new(
                 LanguageConfig {
                     brackets: vec![
@@ -5915,11 +5979,11 @@ mod tests {
                 "#,
             )
             .unwrap(),
-        ));
+        );
 
         let text = "fn a() {}";
 
-        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
         let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx));
         editor
@@ -5944,7 +6008,7 @@ mod tests {
     #[gpui::test]
     async fn test_autoclose_pairs(mut cx: gpui::TestAppContext) {
         let settings = cx.read(EditorSettings::test);
-        let language = Some(Arc::new(Language::new(
+        let language = Arc::new(Language::new(
             LanguageConfig {
                 brackets: vec![
                     BracketPair {
@@ -5963,7 +6027,7 @@ mod tests {
                 ..Default::default()
             },
             Some(tree_sitter_rust::language()),
-        )));
+        ));
 
         let text = r#"
             a
@@ -5973,7 +6037,7 @@ mod tests {
         "#
         .unindent();
 
-        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
         let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx));
         view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
@@ -6055,13 +6119,13 @@ mod tests {
     #[gpui::test]
     async fn test_toggle_comment(mut cx: gpui::TestAppContext) {
         let settings = cx.read(EditorSettings::test);
-        let language = Some(Arc::new(Language::new(
+        let language = Arc::new(Language::new(
             LanguageConfig {
                 line_comment: Some("// ".to_string()),
                 ..Default::default()
             },
             Some(tree_sitter_rust::language()),
-        )));
+        ));
 
         let text = "
             fn a() {
@@ -6072,7 +6136,7 @@ mod tests {
         "
         .unindent();
 
-        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
         let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx));
 
@@ -6323,7 +6387,7 @@ mod tests {
     #[gpui::test]
     async fn test_extra_newline_insertion(mut cx: gpui::TestAppContext) {
         let settings = cx.read(EditorSettings::test);
-        let language = Some(Arc::new(Language::new(
+        let language = Arc::new(Language::new(
             LanguageConfig {
                 brackets: vec![
                     BracketPair {
@@ -6342,7 +6406,7 @@ mod tests {
                 ..Default::default()
             },
             Some(tree_sitter_rust::language()),
-        )));
+        ));
 
         let text = concat!(
             "{   }\n",     // Suppress rustfmt
@@ -6352,7 +6416,7 @@ mod tests {
             "{{} }\n",     //
         );
 
-        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx));
+        let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
         let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx));
         view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))

crates/editor/src/items.rs 🔗

@@ -6,15 +6,15 @@ use gpui::{
 };
 use language::{Bias, Buffer, Diagnostic, File as _};
 use postage::watch;
-use project::{File, ProjectPath, Worktree};
-use std::fmt::Write;
-use std::path::Path;
+use project::{File, Project, ProjectPath};
+use std::path::PathBuf;
 use std::rc::Rc;
+use std::{cell::RefCell, fmt::Write};
 use text::{Point, Selection};
 use util::TryFutureExt;
 use workspace::{
-    ItemHandle, ItemView, ItemViewHandle, Navigation, PathOpener, Settings, StatusItemView,
-    WeakItemHandle, Workspace,
+    ItemHandle, ItemNavHistory, ItemView, ItemViewHandle, NavHistory, PathOpener, Settings,
+    StatusItemView, WeakItemHandle, Workspace,
 };
 
 pub struct BufferOpener;
@@ -28,11 +28,11 @@ struct WeakBufferItemHandle(WeakModelHandle<Buffer>);
 impl PathOpener for BufferOpener {
     fn open(
         &self,
-        worktree: &mut Worktree,
+        project: &mut Project,
         project_path: ProjectPath,
-        cx: &mut ModelContext<Worktree>,
+        cx: &mut ModelContext<Project>,
     ) -> Option<Task<Result<Box<dyn ItemHandle>>>> {
-        let buffer = worktree.open_buffer(project_path.path, cx);
+        let buffer = project.open_buffer(project_path, cx);
         let task = cx.spawn(|_, _| async move {
             let buffer = buffer.await?;
             Ok(Box::new(BufferItemHandle(buffer)) as Box<dyn ItemHandle>)
@@ -46,7 +46,7 @@ impl ItemHandle for BufferItemHandle {
         &self,
         window_id: usize,
         workspace: &Workspace,
-        navigation: Rc<Navigation>,
+        nav_history: Rc<RefCell<NavHistory>>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx));
@@ -57,7 +57,7 @@ impl ItemHandle for BufferItemHandle {
                 crate::settings_builder(weak_buffer, workspace.settings()),
                 cx,
             );
-            editor.navigation = Some(navigation);
+            editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle()));
             editor
         }))
     }
@@ -115,9 +115,9 @@ impl ItemView for Editor {
             };
 
             drop(buffer);
-            let navigation = self.navigation.take();
+            let nav_history = self.nav_history.take();
             self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx);
-            self.navigation = navigation;
+            self.nav_history = nav_history;
         }
     }
 
@@ -150,7 +150,7 @@ impl ItemView for Editor {
 
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
         if let Some(selection) = self.newest_selection_internal() {
-            self.push_to_navigation_history(selection.head(), None, cx);
+            self.push_to_nav_history(selection.head(), None, cx);
         }
     }
 
@@ -166,20 +166,18 @@ impl ItemView for Editor {
         self.project_path(cx).is_some()
     }
 
-    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
+    fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
         let buffer = self.buffer().clone();
-        Ok(cx.spawn(|editor, mut cx| async move {
+        cx.spawn(|editor, mut cx| async move {
             buffer
                 .update(&mut cx, |buffer, cx| buffer.format(cx).log_err())
                 .await;
             editor.update(&mut cx, |editor, cx| {
                 editor.request_autoscroll(Autoscroll::Fit, cx)
             });
-            buffer
-                .update(&mut cx, |buffer, cx| buffer.save(cx))?
-                .await?;
+            buffer.update(&mut cx, |buffer, cx| buffer.save(cx)).await?;
             Ok(())
-        }))
+        })
     }
 
     fn can_save_as(&self, _: &AppContext) -> bool {
@@ -188,8 +186,8 @@ impl ItemView for Editor {
 
     fn save_as(
         &mut self,
-        worktree: ModelHandle<Worktree>,
-        path: &Path,
+        project: ModelHandle<Project>,
+        abs_path: PathBuf,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
         let buffer = self
@@ -199,38 +197,8 @@ impl ItemView for Editor {
             .expect("cannot call save_as on an excerpt list")
             .clone();
 
-        buffer.update(cx, |buffer, cx| {
-            let handle = cx.handle();
-            let text = buffer.as_rope().clone();
-            let version = buffer.version();
-
-            let save_as = worktree.update(cx, |worktree, cx| {
-                worktree
-                    .as_local_mut()
-                    .unwrap()
-                    .save_buffer_as(handle, path, text, cx)
-            });
-
-            cx.spawn(|buffer, mut cx| async move {
-                save_as.await.map(|new_file| {
-                    let (language, language_server) = worktree.update(&mut cx, |worktree, cx| {
-                        let worktree = worktree.as_local_mut().unwrap();
-                        let language = worktree
-                            .language_registry()
-                            .select_language(new_file.full_path())
-                            .cloned();
-                        let language_server = language
-                            .as_ref()
-                            .and_then(|language| worktree.register_language(language, cx));
-                        (language, language_server.clone())
-                    });
-
-                    buffer.update(&mut cx, |buffer, cx| {
-                        buffer.did_save(version, new_file.mtime, Some(Box::new(new_file)), cx);
-                        buffer.set_language(language, language_server, cx);
-                    });
-                })
-            })
+        project.update(cx, |project, cx| {
+            project.save_buffer_as(buffer, abs_path, cx)
         })
     }
 
@@ -317,7 +285,7 @@ impl StatusItemView for CursorPosition {
         active_pane_item: Option<&dyn ItemViewHandle>,
         cx: &mut ViewContext<Self>,
     ) {
-        if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::<Editor>()) {
+        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
             self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
             self.update_position(editor, cx);
         } else {
@@ -403,7 +371,7 @@ impl StatusItemView for DiagnosticMessage {
         active_pane_item: Option<&dyn ItemViewHandle>,
         cx: &mut ViewContext<Self>,
     ) {
-        if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::<Editor>()) {
+        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
             self._observe_active_editor = Some(cx.observe(&editor, Self::update));
             self.update(editor, cx);
         } else {

crates/editor/src/multi_buffer.rs 🔗

@@ -789,6 +789,19 @@ impl MultiBuffer {
         cx.notify();
     }
 
+    pub fn text_anchor_for_position<'a, T: ToOffset>(
+        &'a self,
+        position: T,
+        cx: &AppContext,
+    ) -> (ModelHandle<Buffer>, language::Anchor) {
+        let snapshot = self.read(cx);
+        let anchor = snapshot.anchor_before(position);
+        (
+            self.buffers.borrow()[&anchor.buffer_id].buffer.clone(),
+            anchor.text_anchor,
+        )
+    }
+
     fn on_buffer_event(
         &mut self,
         _: ModelHandle<Buffer>,
@@ -812,18 +825,18 @@ impl MultiBuffer {
         })
     }
 
-    pub fn save(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
+    pub fn save(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         let mut save_tasks = Vec::new();
         for BufferState { buffer, .. } in self.buffers.borrow().values() {
-            save_tasks.push(buffer.update(cx, |buffer, cx| buffer.save(cx))?);
+            save_tasks.push(buffer.update(cx, |buffer, cx| buffer.save(cx)));
         }
 
-        Ok(cx.spawn(|_, _| async move {
+        cx.spawn(|_, _| async move {
             for save in save_tasks {
                 save.await?;
             }
             Ok(())
-        }))
+        })
     }
 
     pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {

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/go_to_line/src/go_to_line.rs 🔗

@@ -80,17 +80,13 @@ impl GoToLine {
     }
 
     fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
-        workspace.toggle_modal(cx, |cx, workspace| {
-            let editor = workspace
-                .active_item(cx)
-                .unwrap()
-                .to_any()
-                .downcast::<Editor>()
-                .unwrap();
-            let view = cx.add_view(|cx| GoToLine::new(editor, workspace.settings.clone(), cx));
-            cx.subscribe(&view, Self::on_event).detach();
-            view
-        });
+        if let Some(editor) = workspace.active_item(cx).unwrap().downcast::<Editor>() {
+            workspace.toggle_modal(cx, |cx, workspace| {
+                let view = cx.add_view(|cx| GoToLine::new(editor, workspace.settings.clone(), cx));
+                cx.subscribe(&view, Self::on_event).detach();
+                view
+            });
+        }
     }
 
     fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {

crates/gpui/src/app.rs 🔗

@@ -11,7 +11,7 @@ use anyhow::{anyhow, Result};
 use keymap::MatchResult;
 use parking_lot::Mutex;
 use platform::Event;
-use postage::{mpsc, sink::Sink as _, stream::Stream as _};
+use postage::{mpsc, oneshot, sink::Sink as _, stream::Stream as _};
 use smol::prelude::*;
 use std::{
     any::{type_name, Any, TypeId},
@@ -498,11 +498,11 @@ impl TestAppContext {
             .as_any_mut()
             .downcast_mut::<platform::test::Window>()
             .unwrap();
-        let callback = test_window
+        let mut done_tx = test_window
             .last_prompt
             .take()
             .expect("prompt was not called");
-        (callback)(answer);
+        let _ = done_tx.try_send(answer);
     }
 }
 
@@ -660,6 +660,7 @@ type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext);
 
 type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
 type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
+type ReleaseObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
 
 pub struct MutableAppContext {
     weak_self: Option<rc::Weak<RefCell<Self>>>,
@@ -674,6 +675,7 @@ pub struct MutableAppContext {
     next_subscription_id: usize,
     subscriptions: Arc<Mutex<HashMap<usize, BTreeMap<usize, SubscriptionCallback>>>>,
     observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ObservationCallback>>>>,
+    release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
     presenters_and_platform_windows:
         HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
     debug_elements_callbacks: HashMap<usize, Box<dyn Fn(&AppContext) -> crate::json::Value>>,
@@ -717,6 +719,7 @@ impl MutableAppContext {
             next_subscription_id: 0,
             subscriptions: Default::default(),
             observations: Default::default(),
+            release_observations: Default::default(),
             presenters_and_platform_windows: HashMap::new(),
             debug_elements_callbacks: HashMap::new(),
             foreground,
@@ -928,61 +931,26 @@ impl MutableAppContext {
         self.foreground_platform.set_menus(menus);
     }
 
-    fn prompt<F>(
+    fn prompt(
         &self,
         window_id: usize,
         level: PromptLevel,
         msg: &str,
         answers: &[&str],
-        done_fn: F,
-    ) where
-        F: 'static + FnOnce(usize, &mut MutableAppContext),
-    {
-        let app = self.weak_self.as_ref().unwrap().upgrade().unwrap();
-        let foreground = self.foreground.clone();
+    ) -> oneshot::Receiver<usize> {
         let (_, window) = &self.presenters_and_platform_windows[&window_id];
-        window.prompt(
-            level,
-            msg,
-            answers,
-            Box::new(move |answer| {
-                foreground
-                    .spawn(async move { (done_fn)(answer, &mut *app.borrow_mut()) })
-                    .detach();
-            }),
-        );
+        window.prompt(level, msg, answers)
     }
 
-    pub fn prompt_for_paths<F>(&self, options: PathPromptOptions, done_fn: F)
-    where
-        F: 'static + FnOnce(Option<Vec<PathBuf>>, &mut MutableAppContext),
-    {
-        let app = self.weak_self.as_ref().unwrap().upgrade().unwrap();
-        let foreground = self.foreground.clone();
-        self.foreground_platform.prompt_for_paths(
-            options,
-            Box::new(move |paths| {
-                foreground
-                    .spawn(async move { (done_fn)(paths, &mut *app.borrow_mut()) })
-                    .detach();
-            }),
-        );
+    pub fn prompt_for_paths(
+        &self,
+        options: PathPromptOptions,
+    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
+        self.foreground_platform.prompt_for_paths(options)
     }
 
-    pub fn prompt_for_new_path<F>(&self, directory: &Path, done_fn: F)
-    where
-        F: 'static + FnOnce(Option<PathBuf>, &mut MutableAppContext),
-    {
-        let app = self.weak_self.as_ref().unwrap().upgrade().unwrap();
-        let foreground = self.foreground.clone();
-        self.foreground_platform.prompt_for_new_path(
-            directory,
-            Box::new(move |path| {
-                foreground
-                    .spawn(async move { (done_fn)(path, &mut *app.borrow_mut()) })
-                    .detach();
-            }),
-        );
+    pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
+        self.foreground_platform.prompt_for_new_path(directory)
     }
 
     pub fn subscribe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
@@ -1071,6 +1039,27 @@ impl MutableAppContext {
             observations: Some(Arc::downgrade(&self.observations)),
         }
     }
+
+    pub fn observe_release<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
+    where
+        E: Entity,
+        E::Event: 'static,
+        H: Handle<E>,
+        F: 'static + FnMut(&mut Self),
+    {
+        let id = post_inc(&mut self.next_subscription_id);
+        self.release_observations
+            .lock()
+            .entry(handle.id())
+            .or_default()
+            .insert(id, Box::new(move |cx| callback(cx)));
+        Subscription::ReleaseObservation {
+            id,
+            entity_id: handle.id(),
+            observations: Some(Arc::downgrade(&self.release_observations)),
+        }
+    }
+
     pub(crate) fn notify_model(&mut self, model_id: usize) {
         if self.pending_notifications.insert(model_id) {
             self.pending_effects
@@ -1249,6 +1238,7 @@ impl MutableAppContext {
         self.cx.windows.remove(&window_id);
         self.presenters_and_platform_windows.remove(&window_id);
         self.remove_dropped_entities();
+        self.flush_effects();
     }
 
     fn open_platform_window(&mut self, window_id: usize, window_options: WindowOptions) {
@@ -1399,6 +1389,9 @@ impl MutableAppContext {
                 self.observations.lock().remove(&model_id);
                 let mut model = self.cx.models.remove(&model_id).unwrap();
                 model.release(self);
+                self.pending_effects.push_back(Effect::Release {
+                    entity_id: model_id,
+                });
             }
 
             for (window_id, view_id) in dropped_views {
@@ -1422,6 +1415,9 @@ impl MutableAppContext {
                 if let Some(view_id) = change_focus_to {
                     self.focus(window_id, view_id);
                 }
+
+                self.pending_effects
+                    .push_back(Effect::Release { entity_id: view_id });
             }
 
             for key in dropped_element_states {
@@ -1447,6 +1443,7 @@ impl MutableAppContext {
                         Effect::ViewNotification { window_id, view_id } => {
                             self.notify_view_observers(window_id, view_id)
                         }
+                        Effect::Release { entity_id } => self.notify_release_observers(entity_id),
                         Effect::Focus { window_id, view_id } => {
                             self.focus(window_id, view_id);
                         }
@@ -1609,6 +1606,15 @@ impl MutableAppContext {
         }
     }
 
+    fn notify_release_observers(&mut self, entity_id: usize) {
+        let callbacks = self.release_observations.lock().remove(&entity_id);
+        if let Some(callbacks) = callbacks {
+            for (_, mut callback) in callbacks {
+                callback(self);
+            }
+        }
+    }
+
     fn focus(&mut self, window_id: usize, focused_id: usize) {
         if self
             .cx
@@ -1865,6 +1871,9 @@ pub enum Effect {
         window_id: usize,
         view_id: usize,
     },
+    Release {
+        entity_id: usize,
+    },
     Focus {
         window_id: usize,
         view_id: usize,
@@ -1891,6 +1900,10 @@ impl Debug for Effect {
                 .field("window_id", window_id)
                 .field("view_id", view_id)
                 .finish(),
+            Effect::Release { entity_id } => f
+                .debug_struct("Effect::Release")
+                .field("entity_id", entity_id)
+                .finish(),
             Effect::Focus { window_id, view_id } => f
                 .debug_struct("Effect::Focus")
                 .field("window_id", window_id)
@@ -2113,6 +2126,25 @@ impl<'a, T: Entity> ModelContext<'a, T> {
         })
     }
 
+    pub fn observe_release<S, F>(
+        &mut self,
+        handle: &ModelHandle<S>,
+        mut callback: F,
+    ) -> Subscription
+    where
+        S: Entity,
+        F: 'static + FnMut(&mut T, &mut ModelContext<T>),
+    {
+        let observer = self.weak_handle();
+        self.app.observe_release(handle, move |cx| {
+            if let Some(observer) = observer.upgrade(cx) {
+                observer.update(cx, |observer, cx| {
+                    callback(observer, cx);
+                });
+            }
+        })
+    }
+
     pub fn handle(&self) -> ModelHandle<T> {
         ModelHandle::new(self.model_id, &self.app.cx.ref_counts)
     }
@@ -2240,26 +2272,24 @@ impl<'a, T: View> ViewContext<'a, T> {
         self.app.platform()
     }
 
-    pub fn prompt<F>(&self, level: PromptLevel, msg: &str, answers: &[&str], done_fn: F)
-    where
-        F: 'static + FnOnce(usize, &mut MutableAppContext),
-    {
-        self.app
-            .prompt(self.window_id, level, msg, answers, done_fn)
+    pub fn prompt(
+        &self,
+        level: PromptLevel,
+        msg: &str,
+        answers: &[&str],
+    ) -> oneshot::Receiver<usize> {
+        self.app.prompt(self.window_id, level, msg, answers)
     }
 
-    pub fn prompt_for_paths<F>(&self, options: PathPromptOptions, done_fn: F)
-    where
-        F: 'static + FnOnce(Option<Vec<PathBuf>>, &mut MutableAppContext),
-    {
-        self.app.prompt_for_paths(options, done_fn)
+    pub fn prompt_for_paths(
+        &self,
+        options: PathPromptOptions,
+    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
+        self.app.prompt_for_paths(options)
     }
 
-    pub fn prompt_for_new_path<F>(&self, directory: &Path, done_fn: F)
-    where
-        F: 'static + FnOnce(Option<PathBuf>, &mut MutableAppContext),
-    {
-        self.app.prompt_for_new_path(directory, done_fn)
+    pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
+        self.app.prompt_for_new_path(directory)
     }
 
     pub fn debug_elements(&self) -> crate::json::Value {
@@ -2348,6 +2378,22 @@ impl<'a, T: View> ViewContext<'a, T> {
         })
     }
 
+    pub fn observe_release<E, F, H>(&mut self, handle: &H, mut callback: F) -> Subscription
+    where
+        E: Entity,
+        H: Handle<E>,
+        F: 'static + FnMut(&mut T, &mut ViewContext<T>),
+    {
+        let observer = self.weak_handle();
+        self.app.observe_release(handle, move |cx| {
+            if let Some(observer) = observer.upgrade(cx) {
+                observer.update(cx, |observer, cx| {
+                    callback(observer, cx);
+                });
+            }
+        })
+    }
+
     pub fn emit(&mut self, payload: T::Event) {
         self.app.pending_effects.push_back(Effect::Event {
             entity_id: self.view_id,
@@ -3306,6 +3352,12 @@ pub enum Subscription {
         entity_id: usize,
         observations: Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, ObservationCallback>>>>>,
     },
+    ReleaseObservation {
+        id: usize,
+        entity_id: usize,
+        observations:
+            Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>>,
+    },
 }
 
 impl Subscription {
@@ -3317,6 +3369,9 @@ impl Subscription {
             Subscription::Observation { observations, .. } => {
                 observations.take();
             }
+            Subscription::ReleaseObservation { observations, .. } => {
+                observations.take();
+            }
         }
     }
 }
@@ -3335,6 +3390,17 @@ impl Drop for Subscription {
                     }
                 }
             }
+            Subscription::ReleaseObservation {
+                id,
+                entity_id,
+                observations,
+            } => {
+                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
+                    if let Some(observations) = observations.lock().get_mut(entity_id) {
+                        observations.remove(id);
+                    }
+                }
+            }
             Subscription::Subscription {
                 id,
                 entity_id,
@@ -3444,7 +3510,10 @@ mod tests {
     use super::*;
     use crate::elements::*;
     use smol::future::poll_once;
-    use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
+    use std::{
+        cell::Cell,
+        sync::atomic::{AtomicUsize, Ordering::SeqCst},
+    };
 
     #[crate::test(self)]
     fn test_model_handles(cx: &mut MutableAppContext) {
@@ -3695,18 +3764,18 @@ mod tests {
     #[crate::test(self)]
     fn test_entity_release_hooks(cx: &mut MutableAppContext) {
         struct Model {
-            released: Arc<Mutex<bool>>,
+            released: Rc<Cell<bool>>,
         }
 
         struct View {
-            released: Arc<Mutex<bool>>,
+            released: Rc<Cell<bool>>,
         }
 
         impl Entity for Model {
             type Event = ();
 
             fn release(&mut self, _: &mut MutableAppContext) {
-                *self.released.lock() = true;
+                self.released.set(true);
             }
         }
 
@@ -3714,7 +3783,7 @@ mod tests {
             type Event = ();
 
             fn release(&mut self, _: &mut MutableAppContext) {
-                *self.released.lock() = true;
+                self.released.set(true);
             }
         }
 
@@ -3728,27 +3797,41 @@ mod tests {
             }
         }
 
-        let model_released = Arc::new(Mutex::new(false));
-        let view_released = Arc::new(Mutex::new(false));
+        let model_released = Rc::new(Cell::new(false));
+        let model_release_observed = Rc::new(Cell::new(false));
+        let view_released = Rc::new(Cell::new(false));
+        let view_release_observed = Rc::new(Cell::new(false));
 
         let model = cx.add_model(|_| Model {
             released: model_released.clone(),
         });
-
-        let (window_id, _) = cx.add_window(Default::default(), |_| View {
+        let (window_id, view) = cx.add_window(Default::default(), |_| View {
             released: view_released.clone(),
         });
+        assert!(!model_released.get());
+        assert!(!view_released.get());
 
-        assert!(!*model_released.lock());
-        assert!(!*view_released.lock());
+        cx.observe_release(&model, {
+            let model_release_observed = model_release_observed.clone();
+            move |_| model_release_observed.set(true)
+        })
+        .detach();
+        cx.observe_release(&view, {
+            let view_release_observed = view_release_observed.clone();
+            move |_| view_release_observed.set(true)
+        })
+        .detach();
 
         cx.update(move |_| {
             drop(model);
         });
-        assert!(*model_released.lock());
+        assert!(model_released.get());
+        assert!(model_release_observed.get());
 
-        drop(cx.remove_window(window_id));
-        assert!(*view_released.lock());
+        drop(view);
+        cx.remove_window(window_id);
+        assert!(view_released.get());
+        assert!(view_release_observed.get());
     }
 
     #[crate::test(self)]

crates/gpui/src/platform.rs 🔗

@@ -20,6 +20,7 @@ use crate::{
 use anyhow::Result;
 use async_task::Runnable;
 pub use event::Event;
+use postage::oneshot;
 use std::{
     any::Any,
     path::{Path, PathBuf},
@@ -70,13 +71,8 @@ pub(crate) trait ForegroundPlatform {
     fn prompt_for_paths(
         &self,
         options: PathPromptOptions,
-        done_fn: Box<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
-    );
-    fn prompt_for_new_path(
-        &self,
-        directory: &Path,
-        done_fn: Box<dyn FnOnce(Option<std::path::PathBuf>)>,
-    );
+    ) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
+    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
 }
 
 pub trait Dispatcher: Send + Sync {
@@ -89,13 +85,7 @@ pub trait Window: WindowContext {
     fn on_event(&mut self, callback: Box<dyn FnMut(Event)>);
     fn on_resize(&mut self, callback: Box<dyn FnMut()>);
     fn on_close(&mut self, callback: Box<dyn FnOnce()>);
-    fn prompt(
-        &self,
-        level: PromptLevel,
-        msg: &str,
-        answers: &[&str],
-        done_fn: Box<dyn FnOnce(usize)>,
-    );
+    fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
     fn activate(&self);
 }
 

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -33,6 +33,7 @@ use objc::{
     runtime::{Class, Object, Sel},
     sel, sel_impl,
 };
+use postage::oneshot;
 use ptr::null_mut;
 use std::{
     cell::{Cell, RefCell},
@@ -248,15 +249,15 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
     fn prompt_for_paths(
         &self,
         options: platform::PathPromptOptions,
-        done_fn: Box<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
-    ) {
+    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
         unsafe {
             let panel = NSOpenPanel::openPanel(nil);
             panel.setCanChooseDirectories_(options.directories.to_objc());
             panel.setCanChooseFiles_(options.files.to_objc());
             panel.setAllowsMultipleSelection_(options.multiple.to_objc());
             panel.setResolvesAliases_(false.to_objc());
-            let done_fn = Cell::new(Some(done_fn));
+            let (done_tx, done_rx) = oneshot::channel();
+            let done_tx = Cell::new(Some(done_tx));
             let block = ConcreteBlock::new(move |response: NSModalResponse| {
                 let result = if response == NSModalResponse::NSModalResponseOk {
                     let mut result = Vec::new();
@@ -275,27 +276,25 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
                     None
                 };
 
-                if let Some(done_fn) = done_fn.take() {
-                    (done_fn)(result);
+                if let Some(mut done_tx) = done_tx.take() {
+                    let _ = postage::sink::Sink::try_send(&mut done_tx, result);
                 }
             });
             let block = block.copy();
             let _: () = msg_send![panel, beginWithCompletionHandler: block];
+            done_rx
         }
     }
 
-    fn prompt_for_new_path(
-        &self,
-        directory: &Path,
-        done_fn: Box<dyn FnOnce(Option<std::path::PathBuf>)>,
-    ) {
+    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
         unsafe {
             let panel = NSSavePanel::savePanel(nil);
             let path = ns_string(directory.to_string_lossy().as_ref());
             let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
             panel.setDirectoryURL(url);
 
-            let done_fn = Cell::new(Some(done_fn));
+            let (done_tx, done_rx) = oneshot::channel();
+            let done_tx = Cell::new(Some(done_tx));
             let block = ConcreteBlock::new(move |response: NSModalResponse| {
                 let result = if response == NSModalResponse::NSModalResponseOk {
                     let url = panel.URL();
@@ -311,12 +310,13 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
                     None
                 };
 
-                if let Some(done_fn) = done_fn.take() {
-                    (done_fn)(result);
+                if let Some(mut done_tx) = done_tx.take() {
+                    let _ = postage::sink::Sink::try_send(&mut done_tx, result);
                 }
             });
             let block = block.copy();
             let _: () = msg_send![panel, beginWithCompletionHandler: block];
+            done_rx
         }
     }
 }

crates/gpui/src/platform/mac/window.rs 🔗

@@ -28,6 +28,7 @@ use objc::{
     runtime::{Class, Object, Protocol, Sel, BOOL, NO, YES},
     sel, sel_impl,
 };
+use postage::oneshot;
 use smol::Timer;
 use std::{
     any::Any,
@@ -317,8 +318,7 @@ impl platform::Window for Window {
         level: platform::PromptLevel,
         msg: &str,
         answers: &[&str],
-        done_fn: Box<dyn FnOnce(usize)>,
-    ) {
+    ) -> oneshot::Receiver<usize> {
         unsafe {
             let alert: id = msg_send![class!(NSAlert), alloc];
             let alert: id = msg_send![alert, init];
@@ -333,10 +333,11 @@ impl platform::Window for Window {
                 let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
                 let _: () = msg_send![button, setTag: ix as NSInteger];
             }
-            let done_fn = Cell::new(Some(done_fn));
+            let (done_tx, done_rx) = oneshot::channel();
+            let done_tx = Cell::new(Some(done_tx));
             let block = ConcreteBlock::new(move |answer: NSInteger| {
-                if let Some(done_fn) = done_fn.take() {
-                    (done_fn)(answer.try_into().unwrap());
+                if let Some(mut done_tx) = done_tx.take() {
+                    let _ = postage::sink::Sink::try_send(&mut done_tx, answer.try_into().unwrap());
                 }
             });
             let block = block.copy();
@@ -345,6 +346,7 @@ impl platform::Window for Window {
                 beginSheetModalForWindow: self.0.borrow().native_window
                 completionHandler: block
             ];
+            done_rx
         }
     }
 

crates/gpui/src/platform/test.rs 🔗

@@ -5,9 +5,10 @@ use crate::{
 };
 use anyhow::{anyhow, Result};
 use parking_lot::Mutex;
+use postage::oneshot;
 use std::{
     any::Any,
-    cell::RefCell,
+    cell::{Cell, RefCell},
     path::{Path, PathBuf},
     rc::Rc,
     sync::Arc,
@@ -23,7 +24,7 @@ pub struct Platform {
 
 #[derive(Default)]
 pub struct ForegroundPlatform {
-    last_prompt_for_new_path_args: RefCell<Option<(PathBuf, Box<dyn FnOnce(Option<PathBuf>)>)>>,
+    last_prompt_for_new_path_args: RefCell<Option<(PathBuf, oneshot::Sender<Option<PathBuf>>)>>,
 }
 
 struct Dispatcher;
@@ -35,7 +36,7 @@ pub struct Window {
     event_handlers: Vec<Box<dyn FnMut(super::Event)>>,
     resize_handlers: Vec<Box<dyn FnMut()>>,
     close_handlers: Vec<Box<dyn FnOnce()>>,
-    pub(crate) last_prompt: RefCell<Option<Box<dyn FnOnce(usize)>>>,
+    pub(crate) last_prompt: Cell<Option<oneshot::Sender<usize>>>,
 }
 
 impl ForegroundPlatform {
@@ -43,11 +44,11 @@ impl ForegroundPlatform {
         &self,
         result: impl FnOnce(PathBuf) -> Option<PathBuf>,
     ) {
-        let (dir_path, callback) = self
+        let (dir_path, mut done_tx) = self
             .last_prompt_for_new_path_args
             .take()
             .expect("prompt_for_new_path was not called");
-        callback(result(dir_path));
+        let _ = postage::sink::Sink::try_send(&mut done_tx, result(dir_path));
     }
 
     pub(crate) fn did_prompt_for_new_path(&self) -> bool {
@@ -77,12 +78,15 @@ impl super::ForegroundPlatform for ForegroundPlatform {
     fn prompt_for_paths(
         &self,
         _: super::PathPromptOptions,
-        _: Box<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
-    ) {
+    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
+        let (_done_tx, done_rx) = oneshot::channel();
+        done_rx
     }
 
-    fn prompt_for_new_path(&self, path: &Path, f: Box<dyn FnOnce(Option<std::path::PathBuf>)>) {
-        *self.last_prompt_for_new_path_args.borrow_mut() = Some((path.to_path_buf(), f));
+    fn prompt_for_new_path(&self, path: &Path) -> oneshot::Receiver<Option<PathBuf>> {
+        let (done_tx, done_rx) = oneshot::channel();
+        *self.last_prompt_for_new_path_args.borrow_mut() = Some((path.to_path_buf(), done_tx));
+        done_rx
     }
 }
 
@@ -170,7 +174,7 @@ impl Window {
             close_handlers: Vec::new(),
             scale_factor: 1.0,
             current_scene: None,
-            last_prompt: RefCell::new(None),
+            last_prompt: Default::default(),
         }
     }
 }
@@ -220,8 +224,10 @@ impl super::Window for Window {
         self.close_handlers.push(callback);
     }
 
-    fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str], f: Box<dyn FnOnce(usize)>) {
-        self.last_prompt.replace(Some(f));
+    fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str]) -> oneshot::Receiver<usize> {
+        let (done_tx, done_rx) = oneshot::channel();
+        self.last_prompt.replace(Some(done_tx));
+        done_rx
     }
 
     fn activate(&self) {}

crates/journal/src/journal.rs 🔗

@@ -55,7 +55,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
                 .await;
 
             if let Some(Some(Ok(item))) = opened.first() {
-                if let Some(editor) = item.to_any().downcast::<Editor>() {
+                if let Some(editor) = item.downcast::<Editor>() {
                     editor.update(&mut cx, |editor, cx| {
                         let len = editor.buffer().read(cx).read(cx).len();
                         editor.select_ranges([len..len], Some(Autoscroll::Center), cx);

crates/language/src/buffer.rs 🔗

@@ -385,13 +385,17 @@ impl Buffer {
         }
     }
 
-    pub fn with_language(
+    pub fn with_language(mut self, language: Arc<Language>, cx: &mut ModelContext<Self>) -> Self {
+        self.set_language(Some(language), cx);
+        self
+    }
+
+    pub fn with_language_server(
         mut self,
-        language: Option<Arc<Language>>,
-        language_server: Option<Arc<LanguageServer>>,
+        server: Arc<LanguageServer>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
-        self.set_language(language, language_server, cx);
+        self.set_language_server(Some(server), cx);
         self
     }
 
@@ -506,30 +510,34 @@ impl Buffer {
     pub fn save(
         &mut self,
         cx: &mut ModelContext<Self>,
-    ) -> Result<Task<Result<(clock::Global, SystemTime)>>> {
-        let file = self
-            .file
-            .as_ref()
-            .ok_or_else(|| anyhow!("buffer has no file"))?;
+    ) -> Task<Result<(clock::Global, SystemTime)>> {
+        let file = if let Some(file) = self.file.as_ref() {
+            file
+        } else {
+            return Task::ready(Err(anyhow!("buffer has no file")));
+        };
         let text = self.as_rope().clone();
         let version = self.version();
         let save = file.save(self.remote_id(), text, version, cx.as_mut());
-        Ok(cx.spawn(|this, mut cx| async move {
+        cx.spawn(|this, mut cx| async move {
             let (version, mtime) = save.await?;
             this.update(&mut cx, |this, cx| {
                 this.did_save(version.clone(), mtime, None, cx);
             });
             Ok((version, mtime))
-        }))
+        })
+    }
+
+    pub fn set_language(&mut self, language: Option<Arc<Language>>, cx: &mut ModelContext<Self>) {
+        self.language = language;
+        self.reparse(cx);
     }
 
-    pub fn set_language(
+    pub fn set_language_server(
         &mut self,
-        language: Option<Arc<Language>>,
         language_server: Option<Arc<lsp::LanguageServer>>,
         cx: &mut ModelContext<Self>,
     ) {
-        self.language = language;
         self.language_server = if let Some(server) = language_server {
             let (latest_snapshot_tx, mut latest_snapshot_rx) = watch::channel();
             Some(LanguageServerState {
@@ -611,7 +619,6 @@ impl Buffer {
             None
         };
 
-        self.reparse(cx);
         self.update_language_server();
     }
 

crates/language/src/tests.rs 🔗

@@ -145,9 +145,8 @@ async fn test_apply_diff(mut cx: gpui::TestAppContext) {
 #[gpui::test]
 async fn test_reparse(mut cx: gpui::TestAppContext) {
     let text = "fn a() {}";
-    let buffer = cx.add_model(|cx| {
-        Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx)
-    });
+    let buffer =
+        cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
 
     // Wait for the initial text to parse
     buffer
@@ -280,7 +279,7 @@ async fn test_reparse(mut cx: gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_outline(mut cx: gpui::TestAppContext) {
-    let language = Some(Arc::new(
+    let language = Arc::new(
         rust_lang()
             .with_outline_query(
                 r#"
@@ -308,7 +307,7 @@ async fn test_outline(mut cx: gpui::TestAppContext) {
                 "#,
             )
             .unwrap(),
-    ));
+    );
 
     let text = r#"
         struct Person {
@@ -337,7 +336,7 @@ async fn test_outline(mut cx: gpui::TestAppContext) {
     "#
     .unindent();
 
-    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx));
+    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
     let outline = buffer
         .read_with(&cx, |buffer, _| buffer.snapshot().outline(None))
         .unwrap();
@@ -422,7 +421,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
             }
         "
         .unindent();
-        Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx)
+        Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)
     });
     let buffer = buffer.read(cx);
     assert_eq!(
@@ -452,8 +451,7 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
 fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
     cx.add_model(|cx| {
         let text = "fn a() {}";
-        let mut buffer =
-            Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
+        let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
 
         buffer.edit_with_autoindent([8..8], "\n\n", cx);
         assert_eq!(buffer.text(), "fn a() {\n    \n}");
@@ -479,8 +477,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
         "
         .unindent();
 
-        let mut buffer =
-            Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
+        let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
 
         // Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
         // their indentation is not adjusted.
@@ -529,8 +526,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
         "
         .unindent();
 
-        let mut buffer =
-            Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
+        let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
 
         buffer.edit_with_autoindent([5..5], "\nb", cx);
         assert_eq!(
@@ -575,7 +571,9 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) {
     .unindent();
 
     let buffer = cx.add_model(|cx| {
-        Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang)), Some(language_server), cx)
+        Buffer::new(0, text, cx)
+            .with_language(Arc::new(rust_lang), cx)
+            .with_language_server(language_server, cx)
     });
 
     let open_notification = fake
@@ -849,7 +847,7 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
         );
 
         let mut buffer = Buffer::new(0, text, cx);
-        buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
+        buffer.set_language(Some(Arc::new(rust_lang())), cx);
         buffer
             .update_diagnostics(
                 None,

crates/lsp/src/lsp.rs 🔗

@@ -205,8 +205,7 @@ impl LanguageServer {
             output_done_rx: Mutex::new(Some(output_done_rx)),
         });
 
-        let root_uri =
-            lsp_types::Url::from_file_path(root_path).map_err(|_| anyhow!("invalid root path"))?;
+        let root_uri = Url::from_file_path(root_path).map_err(|_| anyhow!("invalid root path"))?;
         executor
             .spawn({
                 let this = this.clone();
@@ -220,18 +219,25 @@ impl LanguageServer {
         Ok(this)
     }
 
-    async fn init(self: Arc<Self>, root_uri: lsp_types::Url) -> Result<()> {
+    async fn init(self: Arc<Self>, root_uri: Url) -> Result<()> {
         #[allow(deprecated)]
-        let params = lsp_types::InitializeParams {
+        let params = InitializeParams {
             process_id: Default::default(),
             root_path: Default::default(),
             root_uri: Some(root_uri),
             initialization_options: Default::default(),
-            capabilities: lsp_types::ClientCapabilities {
+            capabilities: ClientCapabilities {
+                text_document: Some(TextDocumentClientCapabilities {
+                    definition: Some(GotoCapability {
+                        link_support: Some(true),
+                        ..Default::default()
+                    }),
+                    ..Default::default()
+                }),
                 experimental: Some(json!({
                     "serverStatusNotification": true,
                 })),
-                window: Some(lsp_types::WindowClientCapabilities {
+                window: Some(WindowClientCapabilities {
                     work_done_progress: Some(true),
                     ..Default::default()
                 }),
@@ -244,16 +250,16 @@ impl LanguageServer {
         };
 
         let this = self.clone();
-        let request = Self::request_internal::<lsp_types::request::Initialize>(
+        let request = Self::request_internal::<request::Initialize>(
             &this.next_id,
             &this.response_handlers,
             this.outbound_tx.read().as_ref(),
             params,
         );
         request.await?;
-        Self::notify_internal::<lsp_types::notification::Initialized>(
+        Self::notify_internal::<notification::Initialized>(
             this.outbound_tx.read().as_ref(),
-            lsp_types::InitializedParams {},
+            InitializedParams {},
         )?;
         Ok(())
     }
@@ -265,14 +271,14 @@ impl LanguageServer {
             let next_id = AtomicUsize::new(self.next_id.load(SeqCst));
             let mut output_done = self.output_done_rx.lock().take().unwrap();
             Some(async move {
-                Self::request_internal::<lsp_types::request::Shutdown>(
+                Self::request_internal::<request::Shutdown>(
                     &next_id,
                     &response_handlers,
                     outbound_tx.as_ref(),
                     (),
                 )
                 .await?;
-                Self::notify_internal::<lsp_types::notification::Exit>(outbound_tx.as_ref(), ())?;
+                Self::notify_internal::<notification::Exit>(outbound_tx.as_ref(), ())?;
                 drop(outbound_tx);
                 output_done.recv().await;
                 drop(tasks);
@@ -285,7 +291,7 @@ impl LanguageServer {
 
     pub fn on_notification<T, F>(&self, mut f: F) -> Subscription
     where
-        T: lsp_types::notification::Notification,
+        T: notification::Notification,
         F: 'static + Send + Sync + FnMut(T::Params),
     {
         let prev_handler = self.notification_handlers.write().insert(
@@ -309,8 +315,8 @@ impl LanguageServer {
         }
     }
 
-    pub fn request<T: lsp_types::request::Request>(
-        self: Arc<Self>,
+    pub fn request<T: request::Request>(
+        self: &Arc<Self>,
         params: T::Params,
     ) -> impl Future<Output = Result<T::Result>>
     where
@@ -329,7 +335,7 @@ impl LanguageServer {
         }
     }
 
-    fn request_internal<T: lsp_types::request::Request>(
+    fn request_internal<T: request::Request>(
         next_id: &AtomicUsize,
         response_handlers: &Mutex<HashMap<usize, ResponseHandler>>,
         outbound_tx: Option<&channel::Sender<Vec<u8>>>,
@@ -376,7 +382,7 @@ impl LanguageServer {
         }
     }
 
-    pub fn notify<T: lsp_types::notification::Notification>(
+    pub fn notify<T: notification::Notification>(
         self: &Arc<Self>,
         params: T::Params,
     ) -> impl Future<Output = Result<()>> {
@@ -388,7 +394,7 @@ impl LanguageServer {
         }
     }
 
-    fn notify_internal<T: lsp_types::notification::Notification>(
+    fn notify_internal<T: notification::Notification>(
         outbound_tx: Option<&channel::Sender<Vec<u8>>>,
         params: T::Params,
     ) -> Result<()> {
@@ -601,8 +607,7 @@ mod tests {
                 "lib.rs": &lib_source
             }
         }));
-        let lib_file_uri =
-            lsp_types::Url::from_file_path(root_dir.path().join("src/lib.rs")).unwrap();
+        let lib_file_uri = Url::from_file_path(root_dir.path().join("src/lib.rs")).unwrap();
 
         let server = cx.read(|cx| {
             LanguageServer::new(
@@ -615,24 +620,22 @@ mod tests {
         server.next_idle_notification().await;
 
         server
-            .notify::<lsp_types::notification::DidOpenTextDocument>(
-                lsp_types::DidOpenTextDocumentParams {
-                    text_document: lsp_types::TextDocumentItem::new(
-                        lib_file_uri.clone(),
-                        "rust".to_string(),
-                        0,
-                        lib_source,
-                    ),
-                },
-            )
+            .notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
+                text_document: TextDocumentItem::new(
+                    lib_file_uri.clone(),
+                    "rust".to_string(),
+                    0,
+                    lib_source,
+                ),
+            })
             .await
             .unwrap();
 
         let hover = server
-            .request::<lsp_types::request::HoverRequest>(lsp_types::HoverParams {
-                text_document_position_params: lsp_types::TextDocumentPositionParams {
-                    text_document: lsp_types::TextDocumentIdentifier::new(lib_file_uri),
-                    position: lsp_types::Position::new(1, 21),
+            .request::<request::HoverRequest>(HoverParams {
+                text_document_position_params: TextDocumentPositionParams {
+                    text_document: TextDocumentIdentifier::new(lib_file_uri),
+                    position: Position::new(1, 21),
                 },
                 work_done_progress_params: Default::default(),
             })
@@ -641,8 +644,8 @@ mod tests {
             .unwrap();
         assert_eq!(
             hover.contents,
-            lsp_types::HoverContents::Markup(lsp_types::MarkupContent {
-                kind: lsp_types::MarkupKind::Markdown,
+            HoverContents::Markup(MarkupContent {
+                kind: MarkupKind::Markdown,
                 value: "&str".to_string()
             })
         );
@@ -705,10 +708,9 @@ mod tests {
         );
 
         drop(server);
-        let (shutdown_request, _) = fake.receive_request::<lsp_types::request::Shutdown>().await;
+        let (shutdown_request, _) = fake.receive_request::<request::Shutdown>().await;
         fake.respond(shutdown_request, ()).await;
-        fake.receive_notification::<lsp_types::notification::Exit>()
-            .await;
+        fake.receive_notification::<notification::Exit>().await;
     }
 
     impl LanguageServer {
@@ -726,7 +728,7 @@ mod tests {
 
     pub enum ServerStatusNotification {}
 
-    impl lsp_types::notification::Notification for ServerStatusNotification {
+    impl notification::Notification for ServerStatusNotification {
         type Params = ServerStatusParams;
         const METHOD: &'static str = "experimental/serverStatus";
     }

crates/outline/src/outline.rs 🔗

@@ -144,7 +144,7 @@ impl OutlineView {
     fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
         if let Some(editor) = workspace
             .active_item(cx)
-            .and_then(|item| item.to_any().downcast::<Editor>())
+            .and_then(|item| item.downcast::<Editor>())
         {
             let settings = workspace.settings();
             let buffer = editor

crates/project/src/project.rs 🔗

@@ -5,35 +5,46 @@ pub mod worktree;
 use anyhow::{anyhow, Result};
 use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
 use clock::ReplicaId;
-use collections::HashMap;
+use collections::{hash_map, HashMap, HashSet};
 use futures::Future;
 use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 use gpui::{
     AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
+    WeakModelHandle,
 };
-use language::{Buffer, DiagnosticEntry, LanguageRegistry};
-use lsp::DiagnosticSeverity;
+use language::{
+    Bias, Buffer, DiagnosticEntry, File as _, Language, LanguageRegistry, ToOffset, ToPointUtf16,
+};
+use lsp::{DiagnosticSeverity, LanguageServer};
 use postage::{prelude::Stream, watch};
+use smol::block_on;
 use std::{
-    path::Path,
+    ops::Range,
+    path::{Path, PathBuf},
     sync::{atomic::AtomicBool, Arc},
 };
-use util::TryFutureExt as _;
+use util::{ResultExt, TryFutureExt as _};
 
 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>>,
     client: Arc<client::Client>,
     user_store: ModelHandle<UserStore>,
     fs: Arc<dyn Fs>,
     client_state: ProjectClientState,
     collaborators: HashMap<PeerId, Collaborator>,
     subscriptions: Vec<client::Subscription>,
-    pending_disk_based_diagnostics: isize,
+    language_servers_with_diagnostics_running: isize,
+}
+
+enum WorktreeHandle {
+    Strong(ModelHandle<Worktree>),
+    Weak(WeakModelHandle<Worktree>),
 }
 
 enum ProjectClientState {
@@ -57,12 +68,12 @@ pub struct Collaborator {
     pub replica_id: ReplicaId,
 }
 
-#[derive(Debug)]
+#[derive(Clone, Debug, PartialEq)]
 pub enum Event {
     ActiveEntryChanged(Option<ProjectEntry>),
     WorktreeRemoved(WorktreeId),
     DiskBasedDiagnosticsStarted,
-    DiskBasedDiagnosticsUpdated { worktree_id: WorktreeId },
+    DiskBasedDiagnosticsUpdated,
     DiskBasedDiagnosticsFinished,
     DiagnosticsUpdated(ProjectPath),
 }
@@ -81,6 +92,12 @@ pub struct DiagnosticSummary {
     pub hint_count: usize,
 }
 
+#[derive(Debug)]
+pub struct Definition {
+    pub target_buffer: ModelHandle<Buffer>,
+    pub target_range: Range<language::Anchor>,
+}
+
 impl DiagnosticSummary {
     fn new<'a, T: 'a>(diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<T>>) -> Self {
         let mut this = Self {
@@ -148,16 +165,13 @@ impl Project {
 
                                 if let Some(project_id) = remote_id {
                                     let mut registrations = Vec::new();
-                                    this.read_with(&cx, |this, cx| {
-                                        for worktree in &this.worktrees {
-                                            let worktree_id = worktree.id() as u64;
-                                            let worktree = worktree.read(cx).as_local().unwrap();
-                                            registrations.push(rpc.request(
-                                                proto::RegisterWorktree {
-                                                    project_id,
-                                                    worktree_id,
-                                                    root_name: worktree.root_name().to_string(),
-                                                    authorized_logins: worktree.authorized_logins(),
+                                    this.update(&mut cx, |this, cx| {
+                                        for worktree in this.worktrees(cx).collect::<Vec<_>>() {
+                                            registrations.push(worktree.update(
+                                                cx,
+                                                |worktree, cx| {
+                                                    let worktree = worktree.as_local_mut().unwrap();
+                                                    worktree.register(project_id, cx)
                                                 },
                                             ));
                                         }
@@ -190,7 +204,8 @@ impl Project {
                 client,
                 user_store,
                 fs,
-                pending_disk_based_diagnostics: 0,
+                language_servers_with_diagnostics_running: 0,
+                language_servers: Default::default(),
             }
         })
     }
@@ -222,7 +237,6 @@ impl Project {
                     worktree,
                     client.clone(),
                     user_store.clone(),
-                    languages.clone(),
                     cx,
                 )
                 .await?,
@@ -282,10 +296,11 @@ impl Project {
                     remote_id,
                     replica_id,
                 },
-                pending_disk_based_diagnostics: 0,
+                language_servers_with_diagnostics_running: 0,
+                language_servers: Default::default(),
             };
             for worktree in worktrees {
-                this.add_worktree(worktree, cx);
+                this.add_worktree(&worktree, cx);
             }
             this
         }))
@@ -354,8 +369,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(
@@ -363,10 +383,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<()>> {
@@ -391,10 +409,10 @@ 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(project_id, cx));
+                        tasks.push(worktree.share(cx));
                     });
                 }
             });
@@ -428,7 +446,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();
                     });
@@ -457,15 +475,397 @@ impl Project {
     }
 
     pub fn open_buffer(
-        &self,
+        &mut self,
         path: ProjectPath,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ModelHandle<Buffer>>> {
-        if let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) {
-            worktree.update(cx, |worktree, cx| worktree.open_buffer(path.path, cx))
+        let worktree = if let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) {
+            worktree
+        } else {
+            return cx.spawn(|_, _| async move { Err(anyhow!("no such worktree")) });
+        };
+        let buffer_task = worktree.update(cx, |worktree, cx| worktree.open_buffer(path.path, cx));
+        cx.spawn(|this, mut cx| async move {
+            let (buffer, buffer_is_new) = buffer_task.await?;
+            if buffer_is_new {
+                this.update(&mut cx, |this, cx| {
+                    this.assign_language_to_buffer(worktree, buffer.clone(), cx)
+                });
+            }
+            Ok(buffer)
+        })
+    }
+
+    pub fn save_buffer_as(
+        &self,
+        buffer: ModelHandle<Buffer>,
+        abs_path: PathBuf,
+        cx: &mut ModelContext<Project>,
+    ) -> Task<Result<()>> {
+        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
+                .update(&mut cx, |worktree, cx| {
+                    worktree
+                        .as_local_mut()
+                        .unwrap()
+                        .save_buffer_as(buffer.clone(), path, cx)
+                })
+                .await?;
+            this.update(&mut cx, |this, cx| {
+                this.assign_language_to_buffer(worktree, buffer, cx)
+            });
+            Ok(())
+        })
+    }
+
+    fn assign_language_to_buffer(
+        &mut self,
+        worktree: ModelHandle<Worktree>,
+        buffer: ModelHandle<Buffer>,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<()> {
+        // Set the buffer's language
+        let full_path = buffer.read(cx).file()?.full_path();
+        let language = self.languages.select_language(&full_path)?.clone();
+        buffer.update(cx, |buffer, cx| {
+            buffer.set_language(Some(language.clone()), cx);
+        });
+
+        // For local worktrees, start a language server if needed.
+        let worktree = worktree.read(cx);
+        let worktree_id = worktree.id();
+        let worktree_abs_path = worktree.as_local()?.abs_path().clone();
+        let language_server = match self
+            .language_servers
+            .entry((worktree_id, language.name().to_string()))
+        {
+            hash_map::Entry::Occupied(e) => Some(e.get().clone()),
+            hash_map::Entry::Vacant(e) => {
+                Self::start_language_server(self.client.clone(), language, &worktree_abs_path, cx)
+                    .map(|server| e.insert(server).clone())
+            }
+        };
+
+        buffer.update(cx, |buffer, cx| {
+            buffer.set_language_server(language_server, cx)
+        });
+
+        None
+    }
+
+    fn start_language_server(
+        rpc: Arc<Client>,
+        language: Arc<Language>,
+        worktree_path: &Path,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Arc<LanguageServer>> {
+        enum LspEvent {
+            DiagnosticsStart,
+            DiagnosticsUpdate(lsp::PublishDiagnosticsParams),
+            DiagnosticsFinish,
+        }
+
+        let language_server = language
+            .start_server(worktree_path, cx)
+            .log_err()
+            .flatten()?;
+        let disk_based_sources = language
+            .disk_based_diagnostic_sources()
+            .cloned()
+            .unwrap_or_default();
+        let disk_based_diagnostics_progress_token =
+            language.disk_based_diagnostics_progress_token().cloned();
+        let has_disk_based_diagnostic_progress_token =
+            disk_based_diagnostics_progress_token.is_some();
+        let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
+
+        // Listen for `PublishDiagnostics` notifications.
+        language_server
+            .on_notification::<lsp::notification::PublishDiagnostics, _>({
+                let diagnostics_tx = diagnostics_tx.clone();
+                move |params| {
+                    if !has_disk_based_diagnostic_progress_token {
+                        block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok();
+                    }
+                    block_on(diagnostics_tx.send(LspEvent::DiagnosticsUpdate(params))).ok();
+                    if !has_disk_based_diagnostic_progress_token {
+                        block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok();
+                    }
+                }
+            })
+            .detach();
+
+        // Listen for `Progress` notifications. Send an event when the language server
+        // transitions between running jobs and not running any jobs.
+        let mut running_jobs_for_this_server: i32 = 0;
+        language_server
+            .on_notification::<lsp::notification::Progress, _>(move |params| {
+                let token = match params.token {
+                    lsp::NumberOrString::Number(_) => None,
+                    lsp::NumberOrString::String(token) => Some(token),
+                };
+
+                if token == disk_based_diagnostics_progress_token {
+                    match params.value {
+                        lsp::ProgressParamsValue::WorkDone(progress) => match progress {
+                            lsp::WorkDoneProgress::Begin(_) => {
+                                running_jobs_for_this_server += 1;
+                                if running_jobs_for_this_server == 1 {
+                                    block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok();
+                                }
+                            }
+                            lsp::WorkDoneProgress::End(_) => {
+                                running_jobs_for_this_server -= 1;
+                                if running_jobs_for_this_server == 0 {
+                                    block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok();
+                                }
+                            }
+                            _ => {}
+                        },
+                    }
+                }
+            })
+            .detach();
+
+        // Process all the LSP events.
+        cx.spawn_weak(|this, mut cx| async move {
+            while let Ok(message) = diagnostics_rx.recv().await {
+                let this = cx.read(|cx| this.upgrade(cx))?;
+                match message {
+                    LspEvent::DiagnosticsStart => {
+                        let send = this.update(&mut cx, |this, cx| {
+                            this.disk_based_diagnostics_started(cx);
+                            this.remote_id().map(|project_id| {
+                                rpc.send(proto::DiskBasedDiagnosticsUpdating { project_id })
+                            })
+                        });
+                        if let Some(send) = send {
+                            send.await.log_err();
+                        }
+                    }
+                    LspEvent::DiagnosticsUpdate(params) => {
+                        this.update(&mut cx, |this, cx| {
+                            this.update_diagnostics(params, &disk_based_sources, cx)
+                                .log_err();
+                        });
+                    }
+                    LspEvent::DiagnosticsFinish => {
+                        let send = this.update(&mut cx, |this, cx| {
+                            this.disk_based_diagnostics_finished(cx);
+                            this.remote_id().map(|project_id| {
+                                rpc.send(proto::DiskBasedDiagnosticsUpdated { project_id })
+                            })
+                        });
+                        if let Some(send) = send {
+                            send.await.log_err();
+                        }
+                    }
+                }
+            }
+            Some(())
+        })
+        .detach();
+
+        Some(language_server)
+    }
+
+    fn update_diagnostics(
+        &mut self,
+        diagnostics: lsp::PublishDiagnosticsParams,
+        disk_based_sources: &HashSet<String>,
+        cx: &mut ModelContext<Self>,
+    ) -> Result<()> {
+        let path = diagnostics
+            .uri
+            .to_file_path()
+            .map_err(|_| anyhow!("URI is not a file"))?;
+        let (worktree, relative_path) = self
+            .find_worktree_for_abs_path(&path, cx)
+            .ok_or_else(|| anyhow!("no worktree found for diagnostics"))?;
+        let project_path = ProjectPath {
+            worktree_id: worktree.read(cx).id(),
+            path: relative_path.into(),
+        };
+        worktree.update(cx, |worktree, cx| {
+            worktree.as_local_mut().unwrap().update_diagnostics(
+                project_path.path.clone(),
+                diagnostics,
+                disk_based_sources,
+                cx,
+            )
+        })?;
+        cx.emit(Event::DiagnosticsUpdated(project_path));
+        Ok(())
+    }
+
+    pub fn definition<T: ToOffset>(
+        &self,
+        source_buffer_handle: &ModelHandle<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Definition>>> {
+        let source_buffer_handle = source_buffer_handle.clone();
+        let buffer = source_buffer_handle.read(cx);
+        let worktree;
+        let buffer_abs_path;
+        if let Some(file) = File::from_dyn(buffer.file()) {
+            worktree = file.worktree.clone();
+            buffer_abs_path = file.abs_path();
+        } else {
+            return Task::ready(Err(anyhow!("buffer does not belong to any worktree")));
+        };
+
+        if worktree.read(cx).as_local().is_some() {
+            let point = buffer.offset_to_point_utf16(position.to_offset(buffer));
+            let buffer_abs_path = buffer_abs_path.unwrap();
+            let lang_name;
+            let lang_server;
+            if let Some(lang) = buffer.language() {
+                lang_name = lang.name().to_string();
+                if let Some(server) = self
+                    .language_servers
+                    .get(&(worktree.read(cx).id(), lang_name.clone()))
+                {
+                    lang_server = server.clone();
+                } else {
+                    return Task::ready(Err(anyhow!("buffer does not have a language server")));
+                };
+            } else {
+                return Task::ready(Err(anyhow!("buffer does not have a language")));
+            }
+
+            cx.spawn(|this, mut cx| async move {
+                let response = lang_server
+                    .request::<lsp::request::GotoDefinition>(lsp::GotoDefinitionParams {
+                        text_document_position_params: lsp::TextDocumentPositionParams {
+                            text_document: lsp::TextDocumentIdentifier::new(
+                                lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
+                            ),
+                            position: lsp::Position::new(point.row, point.column),
+                        },
+                        work_done_progress_params: Default::default(),
+                        partial_result_params: Default::default(),
+                    })
+                    .await?;
+
+                let mut definitions = Vec::new();
+                if let Some(response) = response {
+                    let mut unresolved_locations = Vec::new();
+                    match response {
+                        lsp::GotoDefinitionResponse::Scalar(loc) => {
+                            unresolved_locations.push((loc.uri, loc.range));
+                        }
+                        lsp::GotoDefinitionResponse::Array(locs) => {
+                            unresolved_locations.extend(locs.into_iter().map(|l| (l.uri, l.range)));
+                        }
+                        lsp::GotoDefinitionResponse::Link(links) => {
+                            unresolved_locations.extend(
+                                links
+                                    .into_iter()
+                                    .map(|l| (l.target_uri, l.target_selection_range)),
+                            );
+                        }
+                    }
+
+                    for (target_uri, target_range) in unresolved_locations {
+                        let abs_path = target_uri
+                            .to_file_path()
+                            .map_err(|_| anyhow!("invalid target path"))?;
+
+                        let (worktree, relative_path) = if let Some(result) = this
+                            .read_with(&cx, |this, cx| {
+                                this.find_worktree_for_abs_path(&abs_path, cx)
+                            }) {
+                            result
+                        } else {
+                            let (worktree, relative_path) = this
+                                .update(&mut cx, |this, cx| {
+                                    this.create_worktree_for_abs_path(&abs_path, true, cx)
+                                })
+                                .await?;
+                            this.update(&mut cx, |this, cx| {
+                                this.language_servers.insert(
+                                    (worktree.read(cx).id(), lang_name.clone()),
+                                    lang_server.clone(),
+                                );
+                            });
+                            (worktree, relative_path)
+                        };
+
+                        let project_path = ProjectPath {
+                            worktree_id: worktree.read_with(&cx, |worktree, _| worktree.id()),
+                            path: relative_path.into(),
+                        };
+                        let target_buffer_handle = this
+                            .update(&mut cx, |this, cx| this.open_buffer(project_path, cx))
+                            .await?;
+                        cx.read(|cx| {
+                            let target_buffer = target_buffer_handle.read(cx);
+                            let target_start = target_buffer
+                                .clip_point_utf16(target_range.start.to_point_utf16(), Bias::Left);
+                            let target_end = target_buffer
+                                .clip_point_utf16(target_range.end.to_point_utf16(), Bias::Left);
+                            definitions.push(Definition {
+                                target_buffer: target_buffer_handle,
+                                target_range: target_buffer.anchor_after(target_start)
+                                    ..target_buffer.anchor_before(target_end),
+                            });
+                        });
+                    }
+                }
+
+                Ok(definitions)
+            })
+        } else {
+            log::info!("go to definition is not yet implemented for guests");
+            Task::ready(Ok(Default::default()))
+        }
+    }
+
+    pub fn find_or_create_worktree_for_abs_path(
+        &self,
+        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 {
-            cx.spawn(|_, _| async move { Err(anyhow!("no such worktree")) })
+            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, weak, cx);
+        cx.background().spawn(async move {
+            let worktree = worktree.await?;
+            Ok((worktree, PathBuf::new()))
+        })
+    }
+
+    fn find_worktree_for_abs_path(
+        &self,
+        abs_path: &Path,
+        cx: &AppContext,
+    ) -> Option<(ModelHandle<Worktree>, PathBuf)> {
+        for tree in self.worktrees(cx) {
+            if let Some(relative_path) = tree
+                .read(cx)
+                .as_local()
+                .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
+            {
+                return Some((tree.clone(), relative_path.into()));
+            }
         }
+        None
     }
 
     pub fn is_shared(&self) -> bool {
@@ -475,42 +875,35 @@ impl Project {
         }
     }
 
-    pub fn add_local_worktree(
-        &mut self,
+    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();
         let client = self.client.clone();
         let user_store = self.user_store.clone();
-        let languages = self.languages.clone();
         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, languages, &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, cx);
                 (project.remote_id(), project.is_shared())
             });
 
             if let Some(project_id) = remote_project_id {
-                let worktree_id = worktree.id() as u64;
-                let register_message = worktree.update(&mut cx, |worktree, _| {
-                    let worktree = worktree.as_local_mut().unwrap();
-                    proto::RegisterWorktree {
-                        project_id,
-                        worktree_id,
-                        root_name: worktree.root_name().to_string(),
-                        authorized_logins: worktree.authorized_logins(),
-                    }
-                });
-                client.request(register_message).await?;
+                worktree
+                    .update(&mut cx, |worktree, cx| {
+                        worktree.as_local_mut().unwrap().register(project_id, cx)
+                    })
+                    .await?;
                 if is_shared {
                     worktree
                         .update(&mut cx, |worktree, cx| {
-                            worktree.as_local_mut().unwrap().share(project_id, cx)
+                            worktree.as_local_mut().unwrap().share(cx)
                         })
                         .await?;
                 }
@@ -520,35 +913,35 @@ impl Project {
         })
     }
 
-    fn add_worktree(&mut self, worktree: ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
+    pub fn remove_worktree(&mut self, id: WorktreeId, cx: &mut ModelContext<Self>) {
+        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>) {
         cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
-        cx.subscribe(&worktree, move |this, worktree, event, cx| match event {
-            worktree::Event::DiagnosticsUpdated(path) => {
-                cx.emit(Event::DiagnosticsUpdated(ProjectPath {
-                    worktree_id: worktree.read(cx).id(),
-                    path: path.clone(),
-                }));
-            }
-            worktree::Event::DiskBasedDiagnosticsUpdating => {
-                if this.pending_disk_based_diagnostics == 0 {
-                    cx.emit(Event::DiskBasedDiagnosticsStarted);
-                }
-                this.pending_disk_based_diagnostics += 1;
-            }
-            worktree::Event::DiskBasedDiagnosticsUpdated => {
-                this.pending_disk_based_diagnostics -= 1;
-                cx.emit(Event::DiskBasedDiagnosticsUpdated {
-                    worktree_id: worktree.read(cx).id(),
-                });
-                if this.pending_disk_based_diagnostics == 0 {
-                    if this.pending_disk_based_diagnostics == 0 {
-                        cx.emit(Event::DiskBasedDiagnosticsFinished);
-                    }
-                }
-            }
-        })
-        .detach();
-        self.worktrees.push(worktree);
+
+        let push_weak_handle = {
+            let worktree = worktree.read(cx);
+            worktree.is_local() && worktree.is_weak()
+        };
+        if push_weak_handle {
+            cx.observe_release(&worktree, |this, cx| {
+                this.worktrees
+                    .retain(|worktree| worktree.upgrade(cx).is_some());
+                cx.notify();
+            })
+            .detach();
+            self.worktrees
+                .push(WorktreeHandle::Weak(worktree.downgrade()));
+        } else {
+            self.worktrees
+                .push(WorktreeHandle::Strong(worktree.clone()));
+        }
         cx.notify();
     }
 
@@ -568,7 +961,7 @@ impl Project {
     }
 
     pub fn is_running_disk_based_diagnostics(&self) -> bool {
-        self.pending_disk_based_diagnostics > 0
+        self.language_servers_with_diagnostics_running > 0
     }
 
     pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary {
@@ -586,7 +979,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
@@ -595,6 +988,21 @@ impl Project {
         })
     }
 
+    fn disk_based_diagnostics_started(&mut self, cx: &mut ModelContext<Self>) {
+        self.language_servers_with_diagnostics_running += 1;
+        if self.language_servers_with_diagnostics_running == 1 {
+            cx.emit(Event::DiskBasedDiagnosticsStarted);
+        }
+    }
+
+    fn disk_based_diagnostics_finished(&mut self, cx: &mut ModelContext<Self>) {
+        cx.emit(Event::DiskBasedDiagnosticsUpdated);
+        self.language_servers_with_diagnostics_running -= 1;
+        if self.language_servers_with_diagnostics_running == 0 {
+            cx.emit(Event::DiskBasedDiagnosticsFinished);
+        }
+    }
+
     pub fn active_entry(&self) -> Option<ProjectEntry> {
         self.active_entry
     }
@@ -664,7 +1072,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);
             })
@@ -685,14 +1093,12 @@ impl Project {
             .worktree
             .ok_or_else(|| anyhow!("invalid worktree"))?;
         let user_store = self.user_store.clone();
-        let languages = self.languages.clone();
         cx.spawn(|this, mut cx| {
             async move {
-                let worktree = Worktree::remote(
-                    remote_id, replica_id, worktree, client, user_store, languages, &mut cx,
-                )
-                .await?;
-                this.update(&mut cx, |this, cx| this.add_worktree(worktree, cx));
+                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));
                 Ok(())
             }
             .log_err()
@@ -708,9 +1114,7 @@ impl Project {
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
         let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
-        self.worktrees
-            .retain(|worktree| worktree.read(cx).as_remote().unwrap().id() != worktree_id);
-        cx.notify();
+        self.remove_worktree(worktree_id, cx);
         Ok(())
     }
 
@@ -738,49 +1142,40 @@ impl Project {
     ) -> Result<()> {
         let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
         if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
-            worktree.update(cx, |worktree, cx| {
-                worktree
-                    .as_remote_mut()
-                    .unwrap()
-                    .update_diagnostic_summary(envelope, cx);
-            });
+            if let Some(summary) = envelope.payload.summary {
+                let project_path = ProjectPath {
+                    worktree_id,
+                    path: Path::new(&summary.path).into(),
+                };
+                worktree.update(cx, |worktree, _| {
+                    worktree
+                        .as_remote_mut()
+                        .unwrap()
+                        .update_diagnostic_summary(project_path.path.clone(), &summary);
+                });
+                cx.emit(Event::DiagnosticsUpdated(project_path));
+            }
         }
         Ok(())
     }
 
     fn handle_disk_based_diagnostics_updating(
         &mut self,
-        envelope: TypedEnvelope<proto::DiskBasedDiagnosticsUpdating>,
+        _: TypedEnvelope<proto::DiskBasedDiagnosticsUpdating>,
         _: Arc<Client>,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
-        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
-        if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
-            worktree.update(cx, |worktree, cx| {
-                worktree
-                    .as_remote()
-                    .unwrap()
-                    .disk_based_diagnostics_updating(cx);
-            });
-        }
+        self.disk_based_diagnostics_started(cx);
         Ok(())
     }
 
     fn handle_disk_based_diagnostics_updated(
         &mut self,
-        envelope: TypedEnvelope<proto::DiskBasedDiagnosticsUpdated>,
+        _: TypedEnvelope<proto::DiskBasedDiagnosticsUpdated>,
         _: Arc<Client>,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
-        let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
-        if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
-            worktree.update(cx, |worktree, cx| {
-                worktree
-                    .as_remote()
-                    .unwrap()
-                    .disk_based_diagnostics_updated(cx);
-            });
-        }
+        self.disk_based_diagnostics_finished(cx);
         Ok(())
     }
 
@@ -835,26 +1230,51 @@ impl Project {
         rpc: Arc<Client>,
         cx: &mut ModelContext<Self>,
     ) -> anyhow::Result<()> {
+        let receipt = envelope.receipt();
+        let peer_id = envelope.original_sender_id()?;
         let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
-        if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
-            return worktree.update(cx, |worktree, cx| {
-                worktree.handle_open_buffer(envelope, rpc, cx)
-            });
-        } else {
-            Err(anyhow!("no such worktree"))
-        }
+        let worktree = self
+            .worktree_for_id(worktree_id, cx)
+            .ok_or_else(|| anyhow!("no such worktree"))?;
+
+        let task = self.open_buffer(
+            ProjectPath {
+                worktree_id,
+                path: PathBuf::from(envelope.payload.path).into(),
+            },
+            cx,
+        );
+        cx.spawn(|_, mut cx| {
+            async move {
+                let buffer = task.await?;
+                let response = worktree.update(&mut cx, |worktree, cx| {
+                    worktree
+                        .as_local_mut()
+                        .unwrap()
+                        .open_remote_buffer(peer_id, buffer, cx)
+                });
+                rpc.respond(receipt, response).await?;
+                Ok(())
+            }
+            .log_err()
+        })
+        .detach();
+        Ok(())
     }
 
     pub fn handle_close_buffer(
         &mut self,
         envelope: TypedEnvelope<proto::CloseBuffer>,
-        rpc: Arc<Client>,
+        _: Arc<Client>,
         cx: &mut ModelContext<Self>,
     ) -> anyhow::Result<()> {
         let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
         if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
             worktree.update(cx, |worktree, cx| {
-                worktree.handle_close_buffer(envelope, rpc, cx)
+                worktree
+                    .as_local_mut()
+                    .unwrap()
+                    .close_remote_buffer(envelope, cx)
             })?;
         }
         Ok(())
@@ -884,10 +1304,13 @@ impl Project {
         cancel_flag: &'a AtomicBool,
         cx: &AppContext,
     ) -> impl 'a + Future<Output = Vec<PathMatch>> {
-        let include_root_name = self.worktrees.len() > 1;
-        let candidate_sets = self
-            .worktrees
-            .iter()
+        let worktrees = self
+            .worktrees(cx)
+            .filter(|worktree| !worktree.read(cx).is_weak())
+            .collect::<Vec<_>>();
+        let include_root_name = worktrees.len() > 1;
+        let candidate_sets = worktrees
+            .into_iter()
             .map(|worktree| CandidateSet {
                 snapshot: worktree.read(cx).snapshot(),
                 include_ignored,
@@ -910,6 +1333,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,
@@ -997,6 +1429,25 @@ impl Entity for Project {
             }
         }
     }
+
+    fn app_will_quit(
+        &mut self,
+        _: &mut MutableAppContext,
+    ) -> Option<std::pin::Pin<Box<dyn 'static + Future<Output = ()>>>> {
+        use futures::FutureExt;
+
+        let shutdown_futures = self
+            .language_servers
+            .drain()
+            .filter_map(|(_, server)| server.shutdown())
+            .collect::<Vec<_>>();
+        Some(
+            async move {
+                futures::future::join_all(shutdown_futures).await;
+            }
+            .boxed(),
+        )
+    }
 }
 
 impl Collaborator {
@@ -1021,11 +1472,16 @@ impl Collaborator {
 
 #[cfg(test)]
 mod tests {
-    use super::*;
+    use super::{Event, *};
     use client::test::FakeHttpClient;
     use fs::RealFs;
-    use gpui::TestAppContext;
-    use language::LanguageRegistry;
+    use futures::StreamExt;
+    use gpui::{test::subscribe, TestAppContext};
+    use language::{
+        tree_sitter_rust, AnchorRangeExt, Diagnostic, LanguageConfig, LanguageRegistry,
+        LanguageServerConfig, Point,
+    };
+    use lsp::Url;
     use serde_json::json;
     use std::{os::unix, path::PathBuf};
     use util::test::temp_tree;
@@ -1057,9 +1513,9 @@ mod tests {
 
         let project = build_project(&mut cx);
 
-        let tree = project
+        let (tree, _) = project
             .update(&mut cx, |project, cx| {
-                project.add_local_worktree(&root_link_path, cx)
+                project.find_or_create_worktree_for_abs_path(&root_link_path, false, cx)
             })
             .await
             .unwrap();

crates/project/src/worktree.rs 🔗

@@ -4,7 +4,7 @@ use super::{
     DiagnosticSummary,
 };
 use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Result};
 use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
 use clock::ReplicaId;
 use collections::{hash_map, HashMap, HashSet};
@@ -15,11 +15,10 @@ use gpui::{
     Task, UpgradeModelHandle, WeakModelHandle,
 };
 use language::{
-    range_from_lsp, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, File as _, Language,
-    LanguageRegistry, Operation, PointUtf16, Rope,
+    range_from_lsp, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, File as _, Operation,
+    PointUtf16, Rope,
 };
 use lazy_static::lazy_static;
-use lsp::LanguageServer;
 use parking_lot::Mutex;
 use postage::{
     prelude::{Sink as _, Stream as _},
@@ -37,7 +36,7 @@ use std::{
     ops::Deref,
     path::{Path, PathBuf},
     sync::{
-        atomic::{AtomicUsize, Ordering::SeqCst},
+        atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
         Arc,
     },
     time::{Duration, SystemTime},
@@ -65,36 +64,24 @@ pub enum Worktree {
     Remote(RemoteWorktree),
 }
 
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub enum Event {
-    DiskBasedDiagnosticsUpdating,
-    DiskBasedDiagnosticsUpdated,
-    DiagnosticsUpdated(Arc<Path>),
-}
-
 impl Entity for Worktree {
-    type Event = Event;
+    type Event = ();
 
-    fn app_will_quit(
-        &mut self,
-        _: &mut MutableAppContext,
-    ) -> Option<std::pin::Pin<Box<dyn 'static + Future<Output = ()>>>> {
-        use futures::FutureExt;
-
-        if let Self::Local(worktree) = self {
-            let shutdown_futures = worktree
-                .language_servers
-                .drain()
-                .filter_map(|(_, server)| server.shutdown())
-                .collect::<Vec<_>>();
-            Some(
-                async move {
-                    futures::future::join_all(shutdown_futures).await;
-                }
-                .boxed(),
-            )
-        } else {
-            None
+    fn release(&mut self, cx: &mut MutableAppContext) {
+        if let Some(worktree) = self.as_local_mut() {
+            if let Registration::Done { project_id } = worktree.registration {
+                let client = worktree.client.clone();
+                let unregister_message = proto::UnregisterWorktree {
+                    project_id,
+                    worktree_id: worktree.id().to_proto(),
+                };
+                cx.foreground()
+                    .spawn(async move {
+                        client.send(unregister_message).await?;
+                        Ok::<_, anyhow::Error>(())
+                    })
+                    .detach_and_log_err(cx);
+            }
         }
     }
 }
@@ -104,12 +91,12 @@ impl Worktree {
         client: Arc<Client>,
         user_store: ModelHandle<UserStore>,
         path: impl Into<Arc<Path>>,
+        weak: bool,
         fs: Arc<dyn Fs>,
-        languages: Arc<LanguageRegistry>,
         cx: &mut AsyncAppContext,
     ) -> Result<ModelHandle<Self>> {
         let (tree, scan_states_tx) =
-            LocalWorktree::new(client, user_store, path, fs.clone(), languages, 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();
@@ -131,7 +118,6 @@ impl Worktree {
         worktree: proto::Worktree,
         client: Arc<Client>,
         user_store: ModelHandle<UserStore>,
-        languages: Arc<LanguageRegistry>,
         cx: &mut AsyncAppContext,
     ) -> Result<ModelHandle<Self>> {
         let remote_id = worktree.id;
@@ -141,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 {
@@ -238,9 +225,9 @@ impl Worktree {
                     loading_buffers: Default::default(),
                     open_buffers: Default::default(),
                     queued_operations: Default::default(),
-                    languages,
                     user_store,
                     diagnostic_summaries,
+                    weak,
                 })
             })
         });
@@ -280,6 +267,10 @@ impl Worktree {
         }
     }
 
+    pub fn is_local(&self) -> bool {
+        matches!(self, Worktree::Local(_))
+    }
+
     pub fn snapshot(&self) -> Snapshot {
         match self {
             Worktree::Local(worktree) => worktree.snapshot(),
@@ -287,6 +278,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,
@@ -306,13 +304,6 @@ impl Worktree {
         }
     }
 
-    pub fn languages(&self) -> &Arc<LanguageRegistry> {
-        match self {
-            Worktree::Local(worktree) => &worktree.language_registry,
-            Worktree::Remote(worktree) => &worktree.languages,
-        }
-    }
-
     pub fn user_store(&self) -> &ModelHandle<UserStore> {
         match self {
             Worktree::Local(worktree) => &worktree.user_store,
@@ -320,43 +311,6 @@ impl Worktree {
         }
     }
 
-    pub fn handle_open_buffer(
-        &mut self,
-        envelope: TypedEnvelope<proto::OpenBuffer>,
-        rpc: Arc<Client>,
-        cx: &mut ModelContext<Self>,
-    ) -> anyhow::Result<()> {
-        let receipt = envelope.receipt();
-
-        let response = self
-            .as_local_mut()
-            .unwrap()
-            .open_remote_buffer(envelope, cx);
-
-        cx.background()
-            .spawn(
-                async move {
-                    rpc.respond(receipt, response.await?).await?;
-                    Ok(())
-                }
-                .log_err(),
-            )
-            .detach();
-
-        Ok(())
-    }
-
-    pub fn handle_close_buffer(
-        &mut self,
-        envelope: TypedEnvelope<proto::CloseBuffer>,
-        _: Arc<Client>,
-        cx: &mut ModelContext<Self>,
-    ) -> anyhow::Result<()> {
-        self.as_local_mut()
-            .unwrap()
-            .close_remote_buffer(envelope, cx)
-    }
-
     pub fn diagnostic_summaries<'a>(
         &'a self,
     ) -> impl Iterator<Item = (Arc<Path>, DiagnosticSummary)> + 'a {
@@ -379,7 +333,7 @@ impl Worktree {
         &mut self,
         path: impl AsRef<Path>,
         cx: &mut ModelContext<Self>,
-    ) -> Task<Result<ModelHandle<Buffer>>> {
+    ) -> Task<Result<(ModelHandle<Buffer>, bool)>> {
         let path = path.as_ref();
 
         // If there is already a buffer for the given path, then return it.
@@ -388,9 +342,10 @@ impl Worktree {
             Worktree::Remote(worktree) => worktree.get_open_buffer(path, cx),
         };
         if let Some(existing_buffer) = existing_buffer {
-            return cx.spawn(move |_, _| async move { Ok(existing_buffer) });
+            return cx.spawn(move |_, _| async move { Ok((existing_buffer, false)) });
         }
 
+        let is_new = Arc::new(AtomicBool::new(true));
         let path: Arc<Path> = Arc::from(path);
         let mut loading_watch = match self.loading_buffers().entry(path.clone()) {
             // If the given path is already being loaded, then wait for that existing
@@ -412,7 +367,10 @@ impl Worktree {
                     // After the buffer loads, record the fact that it is no longer
                     // loading.
                     this.update(&mut cx, |this, _| this.loading_buffers().remove(&path));
-                    *tx.borrow_mut() = Some(result.map_err(|e| Arc::new(e)));
+                    *tx.borrow_mut() = Some(match result {
+                        Ok(buffer) => Ok((buffer, is_new)),
+                        Err(error) => Err(Arc::new(error)),
+                    });
                 })
                 .detach();
                 rx
@@ -422,7 +380,10 @@ impl Worktree {
         cx.spawn(|_, _| async move {
             loop {
                 if let Some(result) = loading_watch.borrow().as_ref() {
-                    return result.clone().map_err(|e| anyhow!("{}", e));
+                    return match result {
+                        Ok((buf, is_new)) => Ok((buf.clone(), is_new.fetch_and(false, SeqCst))),
+                        Err(error) => Err(anyhow!("{}", error)),
+                    };
                 }
                 loading_watch.recv().await;
             }
@@ -526,7 +487,7 @@ impl Worktree {
         let worktree_id = envelope.payload.worktree_id;
         let buffer_id = envelope.payload.buffer_id;
         let save = cx.spawn(|_, mut cx| async move {
-            buffer.update(&mut cx, |buffer, cx| buffer.save(cx))?.await
+            buffer.update(&mut cx, |buffer, cx| buffer.save(cx)).await
         });
 
         cx.background()
@@ -731,179 +692,6 @@ impl Worktree {
         }
     }
 
-    pub fn update_diagnostics(
-        &mut self,
-        params: lsp::PublishDiagnosticsParams,
-        disk_based_sources: &HashSet<String>,
-        cx: &mut ModelContext<Worktree>,
-    ) -> Result<()> {
-        let this = self.as_local_mut().ok_or_else(|| anyhow!("not local"))?;
-        let abs_path = params
-            .uri
-            .to_file_path()
-            .map_err(|_| anyhow!("URI is not a file"))?;
-        let worktree_path = Arc::from(
-            abs_path
-                .strip_prefix(&this.abs_path)
-                .context("path is not within worktree")?,
-        );
-
-        let mut next_group_id = 0;
-        let mut diagnostics = Vec::default();
-        let mut primary_diagnostic_group_ids = HashMap::default();
-        let mut sources_by_group_id = HashMap::default();
-        let mut supporting_diagnostic_severities = HashMap::default();
-        for diagnostic in &params.diagnostics {
-            let source = diagnostic.source.as_ref();
-            let code = diagnostic.code.as_ref().map(|code| match code {
-                lsp::NumberOrString::Number(code) => code.to_string(),
-                lsp::NumberOrString::String(code) => code.clone(),
-            });
-            let range = range_from_lsp(diagnostic.range);
-            let is_supporting = diagnostic
-                .related_information
-                .as_ref()
-                .map_or(false, |infos| {
-                    infos.iter().any(|info| {
-                        primary_diagnostic_group_ids.contains_key(&(
-                            source,
-                            code.clone(),
-                            range_from_lsp(info.location.range),
-                        ))
-                    })
-                });
-
-            if is_supporting {
-                if let Some(severity) = diagnostic.severity {
-                    supporting_diagnostic_severities
-                        .insert((source, code.clone(), range), severity);
-                }
-            } else {
-                let group_id = post_inc(&mut next_group_id);
-                let is_disk_based =
-                    source.map_or(false, |source| disk_based_sources.contains(source));
-
-                sources_by_group_id.insert(group_id, source);
-                primary_diagnostic_group_ids
-                    .insert((source, code.clone(), range.clone()), group_id);
-
-                diagnostics.push(DiagnosticEntry {
-                    range,
-                    diagnostic: Diagnostic {
-                        code: code.clone(),
-                        severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR),
-                        message: diagnostic.message.clone(),
-                        group_id,
-                        is_primary: true,
-                        is_valid: true,
-                        is_disk_based,
-                    },
-                });
-                if let Some(infos) = &diagnostic.related_information {
-                    for info in infos {
-                        if info.location.uri == params.uri {
-                            let range = range_from_lsp(info.location.range);
-                            diagnostics.push(DiagnosticEntry {
-                                range,
-                                diagnostic: Diagnostic {
-                                    code: code.clone(),
-                                    severity: DiagnosticSeverity::INFORMATION,
-                                    message: info.message.clone(),
-                                    group_id,
-                                    is_primary: false,
-                                    is_valid: true,
-                                    is_disk_based,
-                                },
-                            });
-                        }
-                    }
-                }
-            }
-        }
-
-        for entry in &mut diagnostics {
-            let diagnostic = &mut entry.diagnostic;
-            if !diagnostic.is_primary {
-                let source = *sources_by_group_id.get(&diagnostic.group_id).unwrap();
-                if let Some(&severity) = supporting_diagnostic_severities.get(&(
-                    source,
-                    diagnostic.code.clone(),
-                    entry.range.clone(),
-                )) {
-                    diagnostic.severity = severity;
-                }
-            }
-        }
-
-        self.update_diagnostic_entries(worktree_path, params.version, diagnostics, cx)?;
-        Ok(())
-    }
-
-    pub fn update_diagnostic_entries(
-        &mut self,
-        worktree_path: Arc<Path>,
-        version: Option<i32>,
-        diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
-        cx: &mut ModelContext<Self>,
-    ) -> Result<()> {
-        let this = self.as_local_mut().unwrap();
-        for buffer in this.open_buffers.values() {
-            if let Some(buffer) = buffer.upgrade(cx) {
-                if buffer
-                    .read(cx)
-                    .file()
-                    .map_or(false, |file| *file.path() == worktree_path)
-                {
-                    let (remote_id, operation) = buffer.update(cx, |buffer, cx| {
-                        (
-                            buffer.remote_id(),
-                            buffer.update_diagnostics(version, diagnostics.clone(), cx),
-                        )
-                    });
-                    self.send_buffer_update(remote_id, operation?, cx);
-                    break;
-                }
-            }
-        }
-
-        let this = self.as_local_mut().unwrap();
-        let summary = DiagnosticSummary::new(&diagnostics);
-        this.diagnostic_summaries
-            .insert(PathKey(worktree_path.clone()), summary.clone());
-        this.diagnostics.insert(worktree_path.clone(), diagnostics);
-
-        cx.emit(Event::DiagnosticsUpdated(worktree_path.clone()));
-
-        if let Some(share) = this.share.as_ref() {
-            cx.foreground()
-                .spawn({
-                    let client = this.client.clone();
-                    let project_id = share.project_id;
-                    let worktree_id = this.id().to_proto();
-                    let path = worktree_path.to_string_lossy().to_string();
-                    async move {
-                        client
-                            .send(proto::UpdateDiagnosticSummary {
-                                project_id,
-                                worktree_id,
-                                summary: Some(proto::DiagnosticSummary {
-                                    path,
-                                    error_count: summary.error_count as u32,
-                                    warning_count: summary.warning_count as u32,
-                                    info_count: summary.info_count as u32,
-                                    hint_count: summary.hint_count as u32,
-                                }),
-                            })
-                            .await
-                            .log_err()
-                    }
-                })
-                .detach();
-        }
-
-        Ok(())
-    }
-
     fn send_buffer_update(
         &mut self,
         buffer_id: u64,
@@ -991,6 +779,7 @@ pub struct LocalWorktree {
     last_scan_state_rx: watch::Receiver<ScanState>,
     _background_scanner_task: Option<Task<()>>,
     poll_task: Option<Task<()>>,
+    registration: Registration,
     share: Option<ShareState>,
     loading_buffers: LoadingBuffers,
     open_buffers: HashMap<usize, WeakModelHandle<Buffer>>,
@@ -998,12 +787,17 @@ pub struct LocalWorktree {
     diagnostics: HashMap<Arc<Path>, Vec<DiagnosticEntry<PointUtf16>>>,
     diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
     queued_operations: Vec<(u64, Operation)>,
-    language_registry: Arc<LanguageRegistry>,
     client: Arc<Client>,
     user_store: ModelHandle<UserStore>,
     fs: Arc<dyn Fs>,
-    languages: Vec<Arc<Language>>,
-    language_servers: HashMap<String, Arc<LanguageServer>>,
+    weak: bool,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+enum Registration {
+    None,
+    Pending,
+    Done { project_id: u64 },
 }
 
 struct ShareState {
@@ -1021,15 +815,17 @@ pub struct RemoteWorktree {
     replica_id: ReplicaId,
     loading_buffers: LoadingBuffers,
     open_buffers: HashMap<usize, RemoteBuffer>,
-    languages: Arc<LanguageRegistry>,
     user_store: ModelHandle<UserStore>,
     queued_operations: Vec<(u64, Operation)>,
     diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
+    weak: bool,
 }
 
 type LoadingBuffers = HashMap<
     Arc<Path>,
-    postage::watch::Receiver<Option<Result<ModelHandle<Buffer>, Arc<anyhow::Error>>>>,
+    postage::watch::Receiver<
+        Option<Result<(ModelHandle<Buffer>, Arc<AtomicBool>), Arc<anyhow::Error>>>,
+    >,
 >;
 
 #[derive(Default, Deserialize)]
@@ -1042,8 +838,8 @@ impl LocalWorktree {
         client: Arc<Client>,
         user_store: ModelHandle<UserStore>,
         path: impl Into<Arc<Path>>,
+        weak: bool,
         fs: Arc<dyn Fs>,
-        languages: Arc<LanguageRegistry>,
         cx: &mut AsyncAppContext,
     ) -> Result<(ModelHandle<Worktree>, Sender<ScanState>)> {
         let abs_path = path.into();
@@ -1098,6 +894,7 @@ impl LocalWorktree {
                 background_snapshot: Arc::new(Mutex::new(snapshot)),
                 last_scan_state_rx,
                 _background_scanner_task: None,
+                registration: Registration::None,
                 share: None,
                 poll_task: None,
                 loading_buffers: Default::default(),
@@ -1106,12 +903,10 @@ impl LocalWorktree {
                 diagnostics: Default::default(),
                 diagnostic_summaries: Default::default(),
                 queued_operations: Default::default(),
-                language_registry: languages,
                 client,
                 user_store,
                 fs,
-                languages: Default::default(),
-                language_servers: Default::default(),
+                weak,
             };
 
             cx.spawn_weak(|this, mut cx| async move {
@@ -1151,295 +946,299 @@ impl LocalWorktree {
         self.config.collaborators.clone()
     }
 
-    pub fn language_registry(&self) -> &LanguageRegistry {
-        &self.language_registry
-    }
-
-    pub fn languages(&self) -> &[Arc<Language>] {
-        &self.languages
+    fn get_open_buffer(
+        &mut self,
+        path: &Path,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Option<ModelHandle<Buffer>> {
+        let handle = cx.handle();
+        let mut result = None;
+        self.open_buffers.retain(|_buffer_id, buffer| {
+            if let Some(buffer) = buffer.upgrade(cx) {
+                if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
+                    if file.worktree == handle && file.path().as_ref() == path {
+                        result = Some(buffer);
+                    }
+                }
+                true
+            } else {
+                false
+            }
+        });
+        result
     }
 
-    pub fn register_language(
+    fn open_buffer(
         &mut self,
-        language: &Arc<Language>,
+        path: &Path,
         cx: &mut ModelContext<Worktree>,
-    ) -> Option<Arc<LanguageServer>> {
-        if !self.languages.iter().any(|l| Arc::ptr_eq(l, language)) {
-            self.languages.push(language.clone());
-        }
+    ) -> Task<Result<ModelHandle<Buffer>>> {
+        let path = Arc::from(path);
+        cx.spawn(move |this, mut cx| async move {
+            let (file, contents) = this
+                .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))
+                .await?;
 
-        if let Some(server) = self.language_servers.get(language.name()) {
-            return Some(server.clone());
-        }
+            let diagnostics = this.update(&mut cx, |this, _| {
+                this.as_local_mut().unwrap().diagnostics.get(&path).cloned()
+            });
 
-        if let Some(language_server) = language
-            .start_server(self.abs_path(), cx)
-            .log_err()
-            .flatten()
-        {
-            enum DiagnosticProgress {
-                Updating,
-                Updated,
-            }
+            let mut buffer_operations = Vec::new();
+            let buffer = cx.add_model(|cx| {
+                let mut buffer = Buffer::from_file(0, contents, Box::new(file), cx);
+                if let Some(diagnostics) = diagnostics {
+                    let op = buffer.update_diagnostics(None, diagnostics, cx).unwrap();
+                    buffer_operations.push(op);
+                }
+                buffer
+            });
 
-            let disk_based_sources = language
-                .disk_based_diagnostic_sources()
-                .cloned()
-                .unwrap_or_default();
-            let disk_based_diagnostics_progress_token =
-                language.disk_based_diagnostics_progress_token().cloned();
-            let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
-            let (disk_based_diagnostics_done_tx, disk_based_diagnostics_done_rx) =
-                smol::channel::unbounded();
-            language_server
-                .on_notification::<lsp::notification::PublishDiagnostics, _>(move |params| {
-                    smol::block_on(diagnostics_tx.send(params)).ok();
-                })
-                .detach();
-            cx.spawn_weak(|this, mut cx| {
-                let has_disk_based_diagnostic_progress_token =
-                    disk_based_diagnostics_progress_token.is_some();
-                let disk_based_diagnostics_done_tx = disk_based_diagnostics_done_tx.clone();
-                async move {
-                    while let Ok(diagnostics) = diagnostics_rx.recv().await {
-                        if let Some(handle) = cx.read(|cx| this.upgrade(cx)) {
-                            handle.update(&mut cx, |this, cx| {
-                                if !has_disk_based_diagnostic_progress_token {
-                                    smol::block_on(
-                                        disk_based_diagnostics_done_tx
-                                            .send(DiagnosticProgress::Updating),
-                                    )
-                                    .ok();
-                                }
-                                this.update_diagnostics(diagnostics, &disk_based_sources, cx)
-                                    .log_err();
-                                if !has_disk_based_diagnostic_progress_token {
-                                    smol::block_on(
-                                        disk_based_diagnostics_done_tx
-                                            .send(DiagnosticProgress::Updated),
-                                    )
-                                    .ok();
-                                }
-                            })
-                        } else {
-                            break;
-                        }
-                    }
+            this.update(&mut cx, |this, cx| {
+                for op in buffer_operations {
+                    this.send_buffer_update(buffer.read(cx).remote_id(), op, cx);
                 }
-            })
-            .detach();
+                let this = this.as_local_mut().unwrap();
+                this.open_buffers.insert(buffer.id(), buffer.downgrade());
+            });
 
-            let mut pending_disk_based_diagnostics: i32 = 0;
-            language_server
-                .on_notification::<lsp::notification::Progress, _>(move |params| {
-                    let token = match params.token {
-                        lsp::NumberOrString::Number(_) => None,
-                        lsp::NumberOrString::String(token) => Some(token),
-                    };
+            Ok(buffer)
+        })
+    }
 
-                    if token == disk_based_diagnostics_progress_token {
-                        match params.value {
-                            lsp::ProgressParamsValue::WorkDone(progress) => match progress {
-                                lsp::WorkDoneProgress::Begin(_) => {
-                                    if pending_disk_based_diagnostics == 0 {
-                                        smol::block_on(
-                                            disk_based_diagnostics_done_tx
-                                                .send(DiagnosticProgress::Updating),
-                                        )
-                                        .ok();
-                                    }
-                                    pending_disk_based_diagnostics += 1;
-                                }
-                                lsp::WorkDoneProgress::End(_) => {
-                                    pending_disk_based_diagnostics -= 1;
-                                    if pending_disk_based_diagnostics == 0 {
-                                        smol::block_on(
-                                            disk_based_diagnostics_done_tx
-                                                .send(DiagnosticProgress::Updated),
-                                        )
-                                        .ok();
-                                    }
-                                }
-                                _ => {}
-                            },
-                        }
-                    }
-                })
-                .detach();
-            let rpc = self.client.clone();
-            cx.spawn_weak(|this, mut cx| async move {
-                while let Ok(progress) = disk_based_diagnostics_done_rx.recv().await {
-                    if let Some(handle) = cx.read(|cx| this.upgrade(cx)) {
-                        match progress {
-                            DiagnosticProgress::Updating => {
-                                let message = handle.update(&mut cx, |this, cx| {
-                                    cx.emit(Event::DiskBasedDiagnosticsUpdating);
-                                    let this = this.as_local().unwrap();
-                                    this.share.as_ref().map(|share| {
-                                        proto::DiskBasedDiagnosticsUpdating {
-                                            project_id: share.project_id,
-                                            worktree_id: this.id().to_proto(),
-                                        }
-                                    })
-                                });
-
-                                if let Some(message) = message {
-                                    rpc.send(message).await.log_err();
-                                }
-                            }
-                            DiagnosticProgress::Updated => {
-                                let message = handle.update(&mut cx, |this, cx| {
-                                    cx.emit(Event::DiskBasedDiagnosticsUpdated);
-                                    let this = this.as_local().unwrap();
-                                    this.share.as_ref().map(|share| {
-                                        proto::DiskBasedDiagnosticsUpdated {
-                                            project_id: share.project_id,
-                                            worktree_id: this.id().to_proto(),
-                                        }
-                                    })
-                                });
-
-                                if let Some(message) = message {
-                                    rpc.send(message).await.log_err();
-                                }
-                            }
-                        }
-                    } else {
-                        break;
-                    }
-                }
-            })
-            .detach();
+    pub fn open_remote_buffer(
+        &mut self,
+        peer_id: PeerId,
+        buffer: ModelHandle<Buffer>,
+        cx: &mut ModelContext<Worktree>,
+    ) -> proto::OpenBufferResponse {
+        self.shared_buffers
+            .entry(peer_id)
+            .or_default()
+            .insert(buffer.id() as u64, buffer.clone());
+        proto::OpenBufferResponse {
+            buffer: Some(buffer.update(cx.as_mut(), |buffer, _| buffer.to_proto())),
+        }
+    }
 
-            self.language_servers
-                .insert(language.name().to_string(), language_server.clone());
-            Some(language_server.clone())
-        } else {
-            None
+    pub fn close_remote_buffer(
+        &mut self,
+        envelope: TypedEnvelope<proto::CloseBuffer>,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Result<()> {
+        if let Some(shared_buffers) = self.shared_buffers.get_mut(&envelope.original_sender_id()?) {
+            shared_buffers.remove(&envelope.payload.buffer_id);
+            cx.notify();
         }
+
+        Ok(())
     }
 
-    fn get_open_buffer(
+    pub fn remove_collaborator(
         &mut self,
-        path: &Path,
+        peer_id: PeerId,
+        replica_id: ReplicaId,
         cx: &mut ModelContext<Worktree>,
-    ) -> Option<ModelHandle<Buffer>> {
-        let handle = cx.handle();
-        let mut result = None;
-        self.open_buffers.retain(|_buffer_id, buffer| {
+    ) {
+        self.shared_buffers.remove(&peer_id);
+        for (_, buffer) in &self.open_buffers {
             if let Some(buffer) = buffer.upgrade(cx) {
-                if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
-                    if file.worktree == handle && file.path().as_ref() == path {
-                        result = Some(buffer);
+                buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
+            }
+        }
+        cx.notify();
+    }
+
+    pub fn update_diagnostics(
+        &mut self,
+        worktree_path: Arc<Path>,
+        params: lsp::PublishDiagnosticsParams,
+        disk_based_sources: &HashSet<String>,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Result<()> {
+        let mut next_group_id = 0;
+        let mut diagnostics = Vec::default();
+        let mut primary_diagnostic_group_ids = HashMap::default();
+        let mut sources_by_group_id = HashMap::default();
+        let mut supporting_diagnostic_severities = HashMap::default();
+        for diagnostic in &params.diagnostics {
+            let source = diagnostic.source.as_ref();
+            let code = diagnostic.code.as_ref().map(|code| match code {
+                lsp::NumberOrString::Number(code) => code.to_string(),
+                lsp::NumberOrString::String(code) => code.clone(),
+            });
+            let range = range_from_lsp(diagnostic.range);
+            let is_supporting = diagnostic
+                .related_information
+                .as_ref()
+                .map_or(false, |infos| {
+                    infos.iter().any(|info| {
+                        primary_diagnostic_group_ids.contains_key(&(
+                            source,
+                            code.clone(),
+                            range_from_lsp(info.location.range),
+                        ))
+                    })
+                });
+
+            if is_supporting {
+                if let Some(severity) = diagnostic.severity {
+                    supporting_diagnostic_severities
+                        .insert((source, code.clone(), range), severity);
+                }
+            } else {
+                let group_id = post_inc(&mut next_group_id);
+                let is_disk_based =
+                    source.map_or(false, |source| disk_based_sources.contains(source));
+
+                sources_by_group_id.insert(group_id, source);
+                primary_diagnostic_group_ids
+                    .insert((source, code.clone(), range.clone()), group_id);
+
+                diagnostics.push(DiagnosticEntry {
+                    range,
+                    diagnostic: Diagnostic {
+                        code: code.clone(),
+                        severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR),
+                        message: diagnostic.message.clone(),
+                        group_id,
+                        is_primary: true,
+                        is_valid: true,
+                        is_disk_based,
+                    },
+                });
+                if let Some(infos) = &diagnostic.related_information {
+                    for info in infos {
+                        if info.location.uri == params.uri {
+                            let range = range_from_lsp(info.location.range);
+                            diagnostics.push(DiagnosticEntry {
+                                range,
+                                diagnostic: Diagnostic {
+                                    code: code.clone(),
+                                    severity: DiagnosticSeverity::INFORMATION,
+                                    message: info.message.clone(),
+                                    group_id,
+                                    is_primary: false,
+                                    is_valid: true,
+                                    is_disk_based,
+                                },
+                            });
+                        }
                     }
                 }
-                true
-            } else {
-                false
             }
-        });
-        result
-    }
-
-    fn open_buffer(
-        &mut self,
-        path: &Path,
-        cx: &mut ModelContext<Worktree>,
-    ) -> Task<Result<ModelHandle<Buffer>>> {
-        let path = Arc::from(path);
-        cx.spawn(move |this, mut cx| async move {
-            let (file, contents) = this
-                .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))
-                .await?;
-
-            let (diagnostics, language, language_server) = this.update(&mut cx, |this, cx| {
-                let this = this.as_local_mut().unwrap();
-                let diagnostics = this.diagnostics.get(&path).cloned();
-                let language = this
-                    .language_registry
-                    .select_language(file.full_path())
-                    .cloned();
-                let server = language
-                    .as_ref()
-                    .and_then(|language| this.register_language(language, cx));
-                (diagnostics, language, server)
-            });
-
-            let mut buffer_operations = Vec::new();
-            let buffer = cx.add_model(|cx| {
-                let mut buffer = Buffer::from_file(0, contents, Box::new(file), cx);
-                buffer.set_language(language, language_server, cx);
-                if let Some(diagnostics) = diagnostics {
-                    let op = buffer.update_diagnostics(None, diagnostics, cx).unwrap();
-                    buffer_operations.push(op);
-                }
-                buffer
-            });
+        }
 
-            this.update(&mut cx, |this, cx| {
-                for op in buffer_operations {
-                    this.send_buffer_update(buffer.read(cx).remote_id(), op, cx);
+        for entry in &mut diagnostics {
+            let diagnostic = &mut entry.diagnostic;
+            if !diagnostic.is_primary {
+                let source = *sources_by_group_id.get(&diagnostic.group_id).unwrap();
+                if let Some(&severity) = supporting_diagnostic_severities.get(&(
+                    source,
+                    diagnostic.code.clone(),
+                    entry.range.clone(),
+                )) {
+                    diagnostic.severity = severity;
                 }
-                let this = this.as_local_mut().unwrap();
-                this.open_buffers.insert(buffer.id(), buffer.downgrade());
-            });
+            }
+        }
 
-            Ok(buffer)
-        })
+        self.update_diagnostic_entries(worktree_path, params.version, diagnostics, cx)?;
+        Ok(())
     }
 
-    pub fn open_remote_buffer(
+    pub fn update_diagnostic_entries(
         &mut self,
-        envelope: TypedEnvelope<proto::OpenBuffer>,
+        worktree_path: Arc<Path>,
+        version: Option<i32>,
+        diagnostics: Vec<DiagnosticEntry<PointUtf16>>,
         cx: &mut ModelContext<Worktree>,
-    ) -> Task<Result<proto::OpenBufferResponse>> {
-        cx.spawn(|this, mut cx| async move {
-            let peer_id = envelope.original_sender_id();
-            let path = Path::new(&envelope.payload.path);
-            let buffer = this
-                .update(&mut cx, |this, cx| this.open_buffer(path, cx))
-                .await?;
-            this.update(&mut cx, |this, cx| {
-                this.as_local_mut()
-                    .unwrap()
-                    .shared_buffers
-                    .entry(peer_id?)
-                    .or_default()
-                    .insert(buffer.id() as u64, buffer.clone());
+    ) -> Result<()> {
+        for buffer in self.open_buffers.values() {
+            if let Some(buffer) = buffer.upgrade(cx) {
+                if buffer
+                    .read(cx)
+                    .file()
+                    .map_or(false, |file| *file.path() == worktree_path)
+                {
+                    let (remote_id, operation) = buffer.update(cx, |buffer, cx| {
+                        (
+                            buffer.remote_id(),
+                            buffer.update_diagnostics(version, diagnostics.clone(), cx),
+                        )
+                    });
+                    self.send_buffer_update(remote_id, operation?, cx);
+                    break;
+                }
+            }
+        }
 
-                Ok(proto::OpenBufferResponse {
-                    buffer: Some(buffer.update(cx.as_mut(), |buffer, _| buffer.to_proto())),
-                })
-            })
-        })
-    }
+        let summary = DiagnosticSummary::new(&diagnostics);
+        self.diagnostic_summaries
+            .insert(PathKey(worktree_path.clone()), summary.clone());
+        self.diagnostics.insert(worktree_path.clone(), diagnostics);
 
-    pub fn close_remote_buffer(
-        &mut self,
-        envelope: TypedEnvelope<proto::CloseBuffer>,
-        cx: &mut ModelContext<Worktree>,
-    ) -> Result<()> {
-        if let Some(shared_buffers) = self.shared_buffers.get_mut(&envelope.original_sender_id()?) {
-            shared_buffers.remove(&envelope.payload.buffer_id);
-            cx.notify();
+        if let Some(share) = self.share.as_ref() {
+            cx.foreground()
+                .spawn({
+                    let client = self.client.clone();
+                    let project_id = share.project_id;
+                    let worktree_id = self.id().to_proto();
+                    let path = worktree_path.to_string_lossy().to_string();
+                    async move {
+                        client
+                            .send(proto::UpdateDiagnosticSummary {
+                                project_id,
+                                worktree_id,
+                                summary: Some(proto::DiagnosticSummary {
+                                    path,
+                                    error_count: summary.error_count as u32,
+                                    warning_count: summary.warning_count as u32,
+                                    info_count: summary.info_count as u32,
+                                    hint_count: summary.hint_count as u32,
+                                }),
+                            })
+                            .await
+                            .log_err()
+                    }
+                })
+                .detach();
         }
 
         Ok(())
     }
 
-    pub fn remove_collaborator(
+    fn send_buffer_update(
         &mut self,
-        peer_id: PeerId,
-        replica_id: ReplicaId,
+        buffer_id: u64,
+        operation: Operation,
         cx: &mut ModelContext<Worktree>,
-    ) {
-        self.shared_buffers.remove(&peer_id);
-        for (_, buffer) in &self.open_buffers {
-            if let Some(buffer) = buffer.upgrade(cx) {
-                buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
+    ) -> Option<()> {
+        let share = self.share.as_ref()?;
+        let project_id = share.project_id;
+        let worktree_id = self.id();
+        let rpc = self.client.clone();
+        cx.spawn(|worktree, mut cx| async move {
+            if let Err(error) = rpc
+                .request(proto::UpdateBuffer {
+                    project_id,
+                    worktree_id: worktree_id.0 as u64,
+                    buffer_id,
+                    operations: vec![language::proto::serialize_operation(&operation)],
+                })
+                .await
+            {
+                worktree.update(&mut cx, |worktree, _| {
+                    log::error!("error sending buffer operation: {}", error);
+                    worktree
+                        .as_local_mut()
+                        .unwrap()
+                        .queued_operations
+                        .push((buffer_id, operation));
+                });
             }
-        }
-        cx.notify();
+        })
+        .detach();
+        None
     }
 
     pub fn scan_complete(&self) -> impl Future<Output = ()> {

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 🔗

@@ -191,12 +191,10 @@ message DiagnosticSummary {
 
 message DiskBasedDiagnosticsUpdating {
     uint64 project_id = 1;
-    uint64 worktree_id = 2;
 }
 
 message DiskBasedDiagnosticsUpdated {
     uint64 project_id = 1;
-    uint64 worktree_id = 2;
 }
 
 message GetChannels {}
@@ -274,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!(
@@ -1213,7 +1217,8 @@ mod tests {
         let buffer_b = worktree_b
             .update(&mut cx_b, |worktree, cx| worktree.open_buffer("b.txt", cx))
             .await
-            .unwrap();
+            .unwrap()
+            .0;
         let buffer_b = cx_b.add_model(|cx| MultiBuffer::singleton(buffer_b, cx));
         buffer_b.read_with(&cx_b, |buf, cx| {
             assert_eq!(buf.read(cx).text(), "b-contents")
@@ -1222,7 +1227,8 @@ mod tests {
         let buffer_a = worktree_a
             .update(&mut cx_a, |tree, cx| tree.open_buffer("b.txt", cx))
             .await
-            .unwrap();
+            .unwrap()
+            .0;
 
         let editor_b = cx_b.add_view(window_b, |cx| {
             Editor::for_buffer(buffer_b, Arc::new(|cx| EditorSettings::test(cx)), cx)
@@ -1291,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
@@ -1319,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
@@ -1351,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
@@ -1393,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
@@ -1431,16 +1441,18 @@ 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
-            .unwrap();
+            .unwrap()
+            .0;
         let buffer_c = worktree_c
             .update(&mut cx_c, |tree, cx| tree.open_buffer("file1", cx))
             .await
-            .unwrap();
+            .unwrap()
+            .0;
         buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx));
         buffer_c.update(&mut cx_c, |buf, cx| buf.edit([0..0], "i-am-c, ", cx));
 
@@ -1448,7 +1460,8 @@ mod tests {
         let buffer_a = worktree_a
             .update(&mut cx_a, |tree, cx| tree.open_buffer("file1", cx))
             .await
-            .unwrap();
+            .unwrap()
+            .0;
 
         buffer_a
             .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ")
@@ -1469,7 +1482,7 @@ mod tests {
             .await;
 
         // Edit the buffer as the host and concurrently save as guest B.
-        let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx).unwrap());
+        let save_b = buffer_b.update(&mut cx_b, |buf, cx| buf.save(cx));
         buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "hi-a, ", cx));
         save_b.await.unwrap();
         assert_eq!(
@@ -1542,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
@@ -1568,13 +1583,14 @@ 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
             .update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))
             .await
-            .unwrap();
+            .unwrap()
+            .0;
         let mtime = buffer_b.read_with(&cx_b, |buf, _| buf.file().unwrap().mtime());
 
         buffer_b.update(&mut cx_b, |buf, cx| buf.edit([0..0], "world ", cx));
@@ -1585,7 +1601,6 @@ mod tests {
 
         buffer_b
             .update(&mut cx_b, |buf, cx| buf.save(cx))
-            .unwrap()
             .await
             .unwrap();
         worktree_b
@@ -1637,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
@@ -1663,13 +1680,14 @@ 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
             .update(&mut cx_a, |tree, cx| tree.open_buffer("a.txt", cx))
             .await
-            .unwrap();
+            .unwrap()
+            .0;
 
         // Start opening the same buffer as client B
         let buffer_b = cx_b
@@ -1681,7 +1699,7 @@ mod tests {
         buffer_a.update(&mut cx_a, |buf, cx| buf.edit([0..0], "z", cx));
 
         let text = buffer_a.read_with(&cx_a, |buf, _| buf.text());
-        let buffer_b = buffer_b.await.unwrap();
+        let buffer_b = buffer_b.await.unwrap().0;
         buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await;
     }
 
@@ -1717,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
@@ -1743,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
@@ -1793,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
@@ -1880,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
@@ -1899,8 +1923,14 @@ mod tests {
         // Cause the language server to start.
         let _ = cx_a
             .background()
-            .spawn(worktree_a.update(&mut cx_a, |worktree, cx| {
-                worktree.open_buffer("other.rs", cx)
+            .spawn(project_a.update(&mut cx_a, |project, cx| {
+                project.open_buffer(
+                    ProjectPath {
+                        worktree_id,
+                        path: Path::new("other.rs").into(),
+                    },
+                    cx,
+                )
             }))
             .await
             .unwrap();
@@ -2011,12 +2041,13 @@ 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)))
             .await
-            .unwrap();
+            .unwrap()
+            .0;
 
         buffer_b.read_with(&cx_b, |buffer, _| {
             assert_eq!(
@@ -2095,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
@@ -2123,12 +2156,13 @@ 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)))
             .await
-            .unwrap();
+            .unwrap()
+            .0;
 
         let format = buffer_b.update(&mut cx_b, |buffer, cx| buffer.format(cx));
         let (request_id, _) = fake_language_server
@@ -2602,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/pane.rs 🔗

@@ -76,14 +76,16 @@ pub struct Pane {
     item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
     active_item_index: usize,
     settings: watch::Receiver<Settings>,
-    navigation: Rc<Navigation>,
+    nav_history: Rc<RefCell<NavHistory>>,
 }
 
-#[derive(Default)]
-pub struct Navigation(RefCell<NavigationHistory>);
+pub struct ItemNavHistory {
+    history: Rc<RefCell<NavHistory>>,
+    item_view: Rc<dyn WeakItemViewHandle>,
+}
 
 #[derive(Default)]
-struct NavigationHistory {
+pub struct NavHistory {
     mode: NavigationMode,
     backward_stack: VecDeque<NavigationEntry>,
     forward_stack: VecDeque<NavigationEntry>,
@@ -104,7 +106,7 @@ impl Default for NavigationMode {
 }
 
 pub struct NavigationEntry {
-    pub item_view: Box<dyn WeakItemViewHandle>,
+    pub item_view: Rc<dyn WeakItemViewHandle>,
     pub data: Option<Box<dyn Any>>,
 }
 
@@ -114,7 +116,7 @@ impl Pane {
             item_views: Vec::new(),
             active_item_index: 0,
             settings,
-            navigation: Default::default(),
+            nav_history: Default::default(),
         }
     }
 
@@ -148,7 +150,7 @@ impl Pane {
     ) -> Task<()> {
         let to_load = pane.update(cx, |pane, cx| {
             // Retrieve the weak item handle from the history.
-            let entry = pane.navigation.pop(mode)?;
+            let entry = pane.nav_history.borrow_mut().pop(mode)?;
 
             // If the item is still present in this pane, then activate it.
             if let Some(index) = entry
@@ -157,9 +159,11 @@ impl Pane {
                 .and_then(|v| pane.index_for_item_view(v.as_ref()))
             {
                 if let Some(item_view) = pane.active_item() {
-                    pane.navigation.set_mode(mode);
+                    pane.nav_history.borrow_mut().set_mode(mode);
                     item_view.deactivated(cx);
-                    pane.navigation.set_mode(NavigationMode::Normal);
+                    pane.nav_history
+                        .borrow_mut()
+                        .set_mode(NavigationMode::Normal);
                 }
 
                 pane.active_item_index = index;
@@ -173,8 +177,7 @@ impl Pane {
             // If the item is no longer present in this pane, then retrieve its
             // project path in order to reopen it.
             else {
-                pane.navigation
-                    .0
+                pane.nav_history
                     .borrow_mut()
                     .paths_by_item
                     .get(&entry.item_view.id())
@@ -192,9 +195,11 @@ impl Pane {
                 if let Some(pane) = cx.read(|cx| pane.upgrade(cx)) {
                     if let Some(item) = item.log_err() {
                         workspace.update(&mut cx, |workspace, cx| {
-                            pane.update(cx, |p, _| p.navigation.set_mode(mode));
+                            pane.update(cx, |p, _| p.nav_history.borrow_mut().set_mode(mode));
                             let item_view = workspace.open_item_in_pane(item, &pane, cx);
-                            pane.update(cx, |p, _| p.navigation.set_mode(NavigationMode::Normal));
+                            pane.update(cx, |p, _| {
+                                p.nav_history.borrow_mut().set_mode(NavigationMode::Normal)
+                            });
 
                             if let Some(data) = entry.data {
                                 item_view.navigate(data, cx);
@@ -232,7 +237,7 @@ impl Pane {
         }
 
         let item_view =
-            item_handle.add_view(cx.window_id(), workspace, self.navigation.clone(), cx);
+            item_handle.add_view(cx.window_id(), workspace, self.nav_history.clone(), cx);
         self.add_item_view(item_view.boxed_clone(), cx);
         item_view
     }
@@ -322,11 +327,11 @@ impl Pane {
                     item_view.deactivated(cx);
                 }
 
-                let mut navigation = self.navigation.0.borrow_mut();
+                let mut nav_history = self.nav_history.borrow_mut();
                 if let Some(path) = item_view.project_path(cx) {
-                    navigation.paths_by_item.insert(item_view.id(), path);
+                    nav_history.paths_by_item.insert(item_view.id(), path);
                 } else {
-                    navigation.paths_by_item.remove(&item_view.id());
+                    nav_history.paths_by_item.remove(&item_view.id());
                 }
 
                 item_ix += 1;
@@ -349,7 +354,7 @@ impl Pane {
 
     fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
         if let Some(active_item) = self.active_item() {
-            cx.focus(active_item.to_any());
+            cx.focus(active_item);
         }
     }
 
@@ -536,16 +541,33 @@ impl View for Pane {
     }
 }
 
-impl Navigation {
-    pub fn pop_backward(&self) -> Option<NavigationEntry> {
-        self.0.borrow_mut().backward_stack.pop_back()
+impl ItemNavHistory {
+    pub fn new<T: ItemView>(history: Rc<RefCell<NavHistory>>, item_view: &ViewHandle<T>) -> Self {
+        Self {
+            history,
+            item_view: Rc::new(item_view.downgrade()),
+        }
     }
 
-    pub fn pop_forward(&self) -> Option<NavigationEntry> {
-        self.0.borrow_mut().forward_stack.pop_back()
+    pub fn history(&self) -> Rc<RefCell<NavHistory>> {
+        self.history.clone()
     }
 
-    fn pop(&self, mode: NavigationMode) -> Option<NavigationEntry> {
+    pub fn push<D: 'static + Any>(&self, data: Option<D>) {
+        self.history.borrow_mut().push(data, self.item_view.clone());
+    }
+}
+
+impl NavHistory {
+    pub fn pop_backward(&mut self) -> Option<NavigationEntry> {
+        self.backward_stack.pop_back()
+    }
+
+    pub fn pop_forward(&mut self) -> Option<NavigationEntry> {
+        self.forward_stack.pop_back()
+    }
+
+    fn pop(&mut self, mode: NavigationMode) -> Option<NavigationEntry> {
         match mode {
             NavigationMode::Normal => None,
             NavigationMode::GoingBack => self.pop_backward(),
@@ -553,38 +575,41 @@ impl Navigation {
         }
     }
 
-    fn set_mode(&self, mode: NavigationMode) {
-        self.0.borrow_mut().mode = mode;
+    fn set_mode(&mut self, mode: NavigationMode) {
+        self.mode = mode;
     }
 
-    pub fn push<D: 'static + Any, T: ItemView>(&self, data: Option<D>, cx: &mut ViewContext<T>) {
-        let mut state = self.0.borrow_mut();
-        match state.mode {
+    pub fn push<D: 'static + Any>(
+        &mut self,
+        data: Option<D>,
+        item_view: Rc<dyn WeakItemViewHandle>,
+    ) {
+        match self.mode {
             NavigationMode::Normal => {
-                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
-                    state.backward_stack.pop_front();
+                if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
+                    self.backward_stack.pop_front();
                 }
-                state.backward_stack.push_back(NavigationEntry {
-                    item_view: Box::new(cx.weak_handle()),
+                self.backward_stack.push_back(NavigationEntry {
+                    item_view,
                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
                 });
-                state.forward_stack.clear();
+                self.forward_stack.clear();
             }
             NavigationMode::GoingBack => {
-                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
-                    state.forward_stack.pop_front();
+                if self.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
+                    self.forward_stack.pop_front();
                 }
-                state.forward_stack.push_back(NavigationEntry {
-                    item_view: Box::new(cx.weak_handle()),
+                self.forward_stack.push_back(NavigationEntry {
+                    item_view,
                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
                 });
             }
             NavigationMode::GoingForward => {
-                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
-                    state.backward_stack.pop_front();
+                if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
+                    self.backward_stack.pop_front();
                 }
-                state.backward_stack.push_back(NavigationEntry {
-                    item_view: Box::new(cx.weak_handle()),
+                self.backward_stack.push_back(NavigationEntry {
+                    item_view,
                     data: data.map(|data| Box::new(data) as Box<dyn Any>),
                 });
             }

crates/workspace/src/workspace.rs 🔗

@@ -33,7 +33,8 @@ use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItem
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use std::{
-    any::Any,
+    any::{Any, TypeId},
+    cell::RefCell,
     future::Future,
     hash::{Hash, Hasher},
     path::{Path, PathBuf},
@@ -66,7 +67,11 @@ pub fn init(cx: &mut MutableAppContext) {
     });
 
     cx.add_action(Workspace::toggle_share);
-    cx.add_action(Workspace::save_active_item);
+    cx.add_action(
+        |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
+            workspace.save_active_item(cx).detach_and_log_err(cx);
+        },
+    );
     cx.add_action(Workspace::debug_elements);
     cx.add_action(Workspace::toggle_sidebar_item);
     cx.add_action(Workspace::toggle_sidebar_item_focus);
@@ -125,9 +130,9 @@ pub struct JoinProjectParams {
 pub trait PathOpener {
     fn open(
         &self,
-        worktree: &mut Worktree,
+        project: &mut Project,
         path: ProjectPath,
-        cx: &mut ModelContext<Worktree>,
+        cx: &mut ModelContext<Project>,
     ) -> Option<Task<Result<Box<dyn ItemHandle>>>>;
 }
 
@@ -137,7 +142,7 @@ pub trait Item: Entity + Sized {
     fn build_view(
         handle: ModelHandle<Self>,
         workspace: &Workspace,
-        navigation: Rc<Navigation>,
+        nav_history: ItemNavHistory,
         cx: &mut ViewContext<Self::View>,
     ) -> Self::View;
 
@@ -165,14 +170,14 @@ pub trait ItemView: View {
         false
     }
     fn can_save(&self, cx: &AppContext) -> bool;
-    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>>;
+    fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>>;
     fn can_save_as(&self, cx: &AppContext) -> bool;
     fn save_as(
         &mut self,
-        worktree: ModelHandle<Worktree>,
-        path: &Path,
+        project: ModelHandle<Project>,
+        abs_path: PathBuf,
         cx: &mut ViewContext<Self>,
-    ) -> Task<anyhow::Result<()>>;
+    ) -> Task<Result<()>>;
     fn should_activate_item_on_event(_: &Self::Event) -> bool {
         false
     }
@@ -182,6 +187,18 @@ pub trait ItemView: View {
     fn should_update_tab_on_event(_: &Self::Event) -> bool {
         false
     }
+    fn act_as_type(
+        &self,
+        type_id: TypeId,
+        self_handle: &ViewHandle<Self>,
+        _: &AppContext,
+    ) -> Option<AnyViewHandle> {
+        if TypeId::of::<Self>() == type_id {
+            Some(self_handle.into())
+        } else {
+            None
+        }
+    }
 }
 
 pub trait ItemHandle: Send + Sync {
@@ -190,7 +207,7 @@ pub trait ItemHandle: Send + Sync {
         &self,
         window_id: usize,
         workspace: &Workspace,
-        navigation: Rc<Navigation>,
+        nav_history: Rc<RefCell<NavHistory>>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle>;
     fn boxed_clone(&self) -> Box<dyn ItemHandle>;
@@ -204,7 +221,7 @@ pub trait WeakItemHandle {
     fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
 }
 
-pub trait ItemViewHandle {
+pub trait ItemViewHandle: 'static {
     fn item_handle(&self, cx: &AppContext) -> Box<dyn ItemHandle>;
     fn title(&self, cx: &AppContext) -> String;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
@@ -219,13 +236,14 @@ pub trait ItemViewHandle {
     fn has_conflict(&self, cx: &AppContext) -> bool;
     fn can_save(&self, cx: &AppContext) -> bool;
     fn can_save_as(&self, cx: &AppContext) -> bool;
-    fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>>;
+    fn save(&self, cx: &mut MutableAppContext) -> Task<Result<()>>;
     fn save_as(
         &self,
-        worktree: ModelHandle<Worktree>,
-        path: &Path,
+        project: ModelHandle<Project>,
+        abs_path: PathBuf,
         cx: &mut MutableAppContext,
-    ) -> Task<anyhow::Result<()>>;
+    ) -> Task<Result<()>>;
+    fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
 }
 
 pub trait WeakItemViewHandle {
@@ -242,11 +260,12 @@ impl<T: Item> ItemHandle for ModelHandle<T> {
         &self,
         window_id: usize,
         workspace: &Workspace,
-        navigation: Rc<Navigation>,
+        nav_history: Rc<RefCell<NavHistory>>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
         Box::new(cx.add_view(window_id, |cx| {
-            T::build_view(self.clone(), workspace, navigation, cx)
+            let nav_history = ItemNavHistory::new(nav_history, &cx.handle());
+            T::build_view(self.clone(), workspace, nav_history, cx)
         }))
     }
 
@@ -276,10 +295,10 @@ impl ItemHandle for Box<dyn ItemHandle> {
         &self,
         window_id: usize,
         workspace: &Workspace,
-        navigation: Rc<Navigation>,
+        nav_history: Rc<RefCell<NavHistory>>,
         cx: &mut MutableAppContext,
     ) -> Box<dyn ItemViewHandle> {
-        ItemHandle::add_view(self.as_ref(), window_id, workspace, navigation, cx)
+        ItemHandle::add_view(self.as_ref(), window_id, workspace, nav_history, cx)
     }
 
     fn boxed_clone(&self) -> Box<dyn ItemHandle> {
@@ -323,6 +342,17 @@ impl PartialEq for Box<dyn WeakItemHandle> {
 
 impl Eq for Box<dyn WeakItemHandle> {}
 
+impl dyn ItemViewHandle {
+    pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
+        self.to_any().downcast()
+    }
+
+    pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
+        self.act_as_type(TypeId::of::<T>(), cx)
+            .and_then(|t| t.downcast())
+    }
+}
+
 impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
     fn item_handle(&self, cx: &AppContext) -> Box<dyn ItemHandle> {
         Box::new(self.read(cx).item_handle(cx))
@@ -374,17 +404,17 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         self.update(cx, |this, cx| this.navigate(data, cx));
     }
 
-    fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>> {
+    fn save(&self, cx: &mut MutableAppContext) -> Task<Result<()>> {
         self.update(cx, |item, cx| item.save(cx))
     }
 
     fn save_as(
         &self,
-        worktree: ModelHandle<Worktree>,
-        path: &Path,
+        project: ModelHandle<Project>,
+        abs_path: PathBuf,
         cx: &mut MutableAppContext,
     ) -> Task<anyhow::Result<()>> {
-        self.update(cx, |item, cx| item.save_as(worktree, path, cx))
+        self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
     }
 
     fn is_dirty(&self, cx: &AppContext) -> bool {
@@ -410,6 +440,16 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
     fn can_save_as(&self, cx: &AppContext) -> bool {
         self.read(cx).can_save_as(cx)
     }
+
+    fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
+        self.read(cx).act_as_type(type_id, self, cx)
+    }
+}
+
+impl Into<AnyViewHandle> for Box<dyn ItemViewHandle> {
+    fn into(self) -> AnyViewHandle {
+        self.to_any()
+    }
 }
 
 impl Clone for Box<dyn ItemViewHandle> {
@@ -600,8 +640,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 {
@@ -621,7 +664,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<_>>();
@@ -675,44 +717,14 @@ impl Workspace {
         })
     }
 
-    fn worktree_for_abs_path(
-        &self,
-        abs_path: &Path,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
-        let abs_path: Arc<Path> = Arc::from(abs_path);
-        cx.spawn(|this, mut cx| async move {
-            let mut entry_id = None;
-            this.read_with(&cx, |this, cx| {
-                for tree in this.worktrees(cx) {
-                    if let Some(relative_path) = tree
-                        .read(cx)
-                        .as_local()
-                        .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
-                    {
-                        entry_id = Some((tree.clone(), relative_path.into()));
-                        break;
-                    }
-                }
-            });
-
-            if let Some(entry_id) = entry_id {
-                Ok(entry_id)
-            } else {
-                let worktree = this
-                    .update(&mut cx, |this, cx| this.add_worktree(&abs_path, cx))
-                    .await?;
-                Ok((worktree, PathBuf::new()))
-            }
-        })
-    }
-
     fn project_path_for_path(
         &self,
         abs_path: &Path,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<ProjectPath>> {
-        let entry = self.worktree_for_abs_path(abs_path, cx);
+        let entry = self.project().update(cx, |project, cx| {
+            project.find_or_create_worktree_for_abs_path(abs_path, false, cx)
+        });
         cx.spawn(|_, cx| async move {
             let (worktree, path) = entry.await?;
             Ok(ProjectPath {
@@ -722,15 +734,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,
@@ -785,18 +788,11 @@ impl Workspace {
             return Task::ready(Ok(existing_item));
         }
 
-        let worktree = match self.project.read(cx).worktree_for_id(path.worktree_id, cx) {
-            Some(worktree) => worktree,
-            None => {
-                return Task::ready(Err(anyhow!("worktree {} does not exist", path.worktree_id)));
-            }
-        };
-
         let project_path = path.clone();
         let path_openers = self.path_openers.clone();
-        worktree.update(cx, |worktree, cx| {
+        self.project.update(cx, |project, cx| {
             for opener in path_openers.iter() {
-                if let Some(task) = opener.open(worktree, project_path.clone(), cx) {
+                if let Some(task) = opener.open(project, project_path.clone(), cx) {
                     return task;
                 }
             }
@@ -825,70 +821,46 @@ impl Workspace {
         self.active_item(cx).and_then(|item| item.project_path(cx))
     }
 
-    pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
+    pub fn save_active_item(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
         if let Some(item) = self.active_item(cx) {
-            let handle = cx.handle();
             if item.can_save(cx) {
                 if item.has_conflict(cx.as_ref()) {
                     const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
 
-                    cx.prompt(
+                    let mut answer = cx.prompt(
                         PromptLevel::Warning,
                         CONFLICT_MESSAGE,
                         &["Overwrite", "Cancel"],
-                        move |answer, cx| {
-                            if answer == 0 {
-                                cx.spawn(|mut cx| async move {
-                                    if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await
-                                    {
-                                        error!("failed to save item: {:?}, ", error);
-                                    }
-                                })
-                                .detach();
-                            }
-                        },
                     );
-                } else {
                     cx.spawn(|_, mut cx| async move {
-                        if let Err(error) = cx.update(|cx| item.save(cx)).unwrap().await {
-                            error!("failed to save item: {:?}, ", error);
+                        let answer = answer.recv().await;
+                        if answer == Some(0) {
+                            cx.update(|cx| item.save(cx)).await?;
                         }
+                        Ok(())
                     })
-                    .detach();
+                } else {
+                    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())
                     .to_path_buf();
-                cx.prompt_for_new_path(&start_abs_path, move |abs_path, cx| {
-                    if let Some(abs_path) = abs_path {
-                        cx.spawn(|mut cx| async move {
-                            let result = match handle
-                                .update(&mut cx, |this, cx| {
-                                    this.worktree_for_abs_path(&abs_path, cx)
-                                })
-                                .await
-                            {
-                                Ok((worktree, path)) => {
-                                    handle
-                                        .update(&mut cx, |_, cx| {
-                                            item.save_as(worktree, &path, cx.as_mut())
-                                        })
-                                        .await
-                                }
-                                Err(error) => Err(error),
-                            };
-
-                            if let Err(error) = result {
-                                error!("failed to save item: {:?}, ", error);
-                            }
-                        })
-                        .detach()
+                let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
+                cx.spawn(|this, mut cx| async move {
+                    if let Some(abs_path) = abs_path.recv().await.flatten() {
+                        let project = this.read_with(&cx, |this, _| this.project().clone());
+                        cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
                     }
-                });
+                    Ok(())
+                })
+            } else {
+                Task::ready(Ok(()))
             }
+        } else {
+            Task::ready(Ok(()))
         }
     }
 
@@ -1348,7 +1320,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 {
@@ -1438,18 +1409,17 @@ impl std::fmt::Debug for OpenParams {
 
 fn open(action: &Open, cx: &mut MutableAppContext) {
     let app_state = action.0.clone();
-    cx.prompt_for_paths(
-        PathPromptOptions {
-            files: true,
-            directories: true,
-            multiple: true,
-        },
-        move |paths, cx| {
-            if let Some(paths) = paths {
-                cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state }));
-            }
-        },
-    );
+    let mut paths = cx.prompt_for_paths(PathPromptOptions {
+        files: true,
+        directories: true,
+        multiple: true,
+    });
+    cx.spawn(|mut cx| async move {
+        if let Some(paths) = paths.recv().await.flatten() {
+            cx.update(|cx| cx.dispatch_global_action(OpenPaths(OpenParams { paths, app_state })));
+        }
+    })
+    .detach();
 }
 
 pub fn open_paths(

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| {
@@ -205,7 +205,6 @@ mod tests {
             workspace
                 .active_item(cx)
                 .unwrap()
-                .to_any()
                 .downcast::<editor::Editor>()
                 .unwrap()
         });
@@ -214,18 +213,13 @@ mod tests {
             assert!(editor.text(cx).is_empty());
         });
 
-        workspace.update(&mut cx, |workspace, cx| {
-            workspace.save_active_item(&workspace::Save, cx)
-        });
-
+        let save_task = workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(cx));
         app_state.fs.as_fake().insert_dir("/root").await.unwrap();
         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
-
-        editor
-            .condition(&cx, |editor, cx| editor.title(cx) == "the-new-name")
-            .await;
-        editor.update(&mut cx, |editor, cx| {
+        save_task.await.unwrap();
+        editor.read_with(&cx, |editor, cx| {
             assert!(!editor.is_dirty(cx));
+            assert_eq!(editor.title(cx), "the-new-name");
         });
     }
 
@@ -248,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();
@@ -360,9 +355,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();
@@ -396,7 +392,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!(
@@ -427,9 +422,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();
@@ -444,7 +440,7 @@ mod tests {
         let editor = cx.read(|cx| {
             let pane = workspace.read(cx).active_pane().read(cx);
             let item = pane.active_item().unwrap();
-            item.to_any().downcast::<Editor>().unwrap()
+            item.downcast::<Editor>().unwrap()
         });
 
         cx.update(|cx| {
@@ -460,12 +456,13 @@ mod tests {
             .await;
         cx.read(|cx| assert!(editor.is_dirty(cx)));
 
-        cx.update(|cx| workspace.update(cx, |w, cx| w.save_active_item(&workspace::Save, cx)));
+        let save_task = workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(cx));
         cx.simulate_prompt_answer(window_id, 0);
-        editor
-            .condition(&cx, |editor, cx| !editor.is_dirty(cx))
-            .await;
-        cx.read(|cx| assert!(!editor.has_conflict(cx)));
+        save_task.await.unwrap();
+        editor.read_with(&cx, |editor, cx| {
+            assert!(!editor.is_dirty(cx));
+            assert!(!editor.has_conflict(cx));
+        });
     }
 
     #[gpui::test]
@@ -474,21 +471,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()));
@@ -496,7 +486,6 @@ mod tests {
             workspace
                 .active_item(cx)
                 .unwrap()
-                .to_any()
                 .downcast::<Editor>()
                 .unwrap()
         });
@@ -513,9 +502,7 @@ mod tests {
         });
 
         // Save the buffer. This prompts for a filename.
-        workspace.update(&mut cx, |workspace, cx| {
-            workspace.save_active_item(&workspace::Save, cx)
-        });
+        let save_task = workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(cx));
         cx.simulate_new_path_selection(|parent_dir| {
             assert_eq!(parent_dir, Path::new("/root"));
             Some(parent_dir.join("the-new-name.rs"))
@@ -525,17 +512,13 @@ mod tests {
             assert_eq!(editor.title(cx), "untitled");
         });
 
-        // When the save completes, the buffer's title is updated.
-        editor
-            .condition(&cx, |editor, cx| !editor.is_dirty(cx))
-            .await;
-        cx.read(|cx| {
+        // When the save completes, the buffer's title is updated and the language is assigned based
+        // on the path.
+        save_task.await.unwrap();
+        editor.read_with(&cx, |editor, cx| {
             assert!(!editor.is_dirty(cx));
             assert_eq!(editor.title(cx), "the-new-name.rs");
-        });
-        // The language is assigned based on the path
-        editor.read_with(&cx, |editor, cx| {
-            assert_eq!(editor.language(cx).unwrap().name(), "Rust")
+            assert_eq!(editor.language(cx).unwrap().name(), "Rust");
         });
 
         // Edit the file and save it again. This time, there is no filename prompt.
@@ -543,14 +526,13 @@ mod tests {
             editor.handle_input(&editor::Input(" there".into()), cx);
             assert_eq!(editor.is_dirty(cx.as_ref()), true);
         });
-        workspace.update(&mut cx, |workspace, cx| {
-            workspace.save_active_item(&workspace::Save, cx)
-        });
+        let save_task = workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(cx));
+        save_task.await.unwrap();
         assert!(!cx.did_prompt_for_new_path());
-        editor
-            .condition(&cx, |editor, cx| !editor.is_dirty(cx))
-            .await;
-        cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs"));
+        editor.read_with(&cx, |editor, cx| {
+            assert!(!editor.is_dirty(cx));
+            assert_eq!(editor.title(cx), "the-new-name.rs")
+        });
 
         // Open the same newly-created file in another pane item. The new editor should reuse
         // the same buffer.
@@ -572,7 +554,6 @@ mod tests {
             workspace
                 .active_item(cx)
                 .unwrap()
-                .to_any()
                 .downcast::<Editor>()
                 .unwrap()
         });
@@ -597,7 +578,6 @@ mod tests {
             workspace
                 .active_item(cx)
                 .unwrap()
-                .to_any()
                 .downcast::<Editor>()
                 .unwrap()
         });
@@ -612,17 +592,12 @@ mod tests {
         });
 
         // Save the buffer. This prompts for a filename.
-        workspace.update(&mut cx, |workspace, cx| {
-            workspace.save_active_item(&workspace::Save, cx)
-        });
+        let save_task = workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(cx));
         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
-
-        editor
-            .condition(&cx, |editor, cx| !editor.is_dirty(cx))
-            .await;
-
-        // The language is assigned based on the path
+        save_task.await.unwrap();
+        // The buffer is not dirty anymore and the language is assigned based on the path.
         editor.read_with(&cx, |editor, cx| {
+            assert!(!editor.is_dirty(cx));
             assert_eq!(editor.language(cx).unwrap().name(), "Rust")
         });
     }
@@ -648,9 +623,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();
@@ -710,9 +686,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();
@@ -727,7 +704,6 @@ mod tests {
             .update(&mut cx, |w, cx| w.open_path(file1.clone(), cx))
             .await
             .unwrap()
-            .to_any()
             .downcast::<Editor>()
             .unwrap();
         editor1.update(&mut cx, |editor, cx| {
@@ -737,14 +713,12 @@ mod tests {
             .update(&mut cx, |w, cx| w.open_path(file2.clone(), cx))
             .await
             .unwrap()
-            .to_any()
             .downcast::<Editor>()
             .unwrap();
         let editor3 = workspace
             .update(&mut cx, |w, cx| w.open_path(file3.clone(), cx))
             .await
             .unwrap()
-            .to_any()
             .downcast::<Editor>()
             .unwrap();
         editor3.update(&mut cx, |editor, cx| {
@@ -860,7 +834,7 @@ mod tests {
         ) -> (ProjectPath, DisplayPoint) {
             workspace.update(cx, |workspace, cx| {
                 let item = workspace.active_item(cx).unwrap();
-                let editor = item.to_any().downcast::<Editor>().unwrap();
+                let editor = item.downcast::<Editor>().unwrap();
                 let selections = editor.update(cx, |editor, cx| editor.selected_display_ranges(cx));
                 (item.project_path(cx).unwrap(), selections[0].start)
             })