Introduce `workspace::register_project_item`

Antonio Scandurra and Nathan Sobo created

This lets downstream crates like `editor` define how project items should be
opened.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

Cargo.lock                            |  2 
crates/editor/src/editor.rs           |  2 
crates/file_finder/Cargo.toml         |  2 
crates/file_finder/src/file_finder.rs |  7 ++
crates/gpui/src/app.rs                | 34 +++++++++++++
crates/project/src/project.rs         | 21 +++++++
crates/workspace/src/workspace.rs     | 69 ++++++++++++++++------------
7 files changed, 102 insertions(+), 35 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1782,7 +1782,9 @@ dependencies = [
 name = "file_finder"
 version = "0.1.0"
 dependencies = [
+ "ctor",
  "editor",
+ "env_logger",
  "fuzzy",
  "gpui",
  "postage",

crates/editor/src/editor.rs 🔗

@@ -340,7 +340,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_async_action(Editor::confirm_rename);
     cx.add_async_action(Editor::find_all_references);
 
-    workspace::register_editor_builder(cx, |project, buffer, cx| {
+    workspace::register_project_item(cx, |project, buffer, cx| {
         Editor::for_buffer(buffer, Some(project), cx)
     });
 }

crates/file_finder/Cargo.toml 🔗

@@ -21,3 +21,5 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 serde_json = { version = "1.0.64", features = ["preserve_order"] }
 workspace = { path = "../workspace", features = ["test-support"] }
+ctor = "0.1"
+env_logger = "0.8"

crates/file_finder/src/file_finder.rs 🔗

@@ -407,6 +407,13 @@ mod tests {
     use std::path::PathBuf;
     use workspace::{Workspace, WorkspaceParams};
 
+    #[ctor::ctor]
+    fn init_logger() {
+        if std::env::var("RUST_LOG").is_ok() {
+            env_logger::init();
+        }
+    }
+
     #[gpui::test]
     async fn test_matching_paths(cx: &mut gpui::TestAppContext) {
         cx.update(|cx| {

crates/gpui/src/app.rs 🔗

@@ -1364,12 +1364,38 @@ impl MutableAppContext {
         Ok(pending)
     }
 
+    pub fn default_global<T: 'static + Default>(&mut self) -> &T {
+        self.cx
+            .globals
+            .entry(TypeId::of::<T>())
+            .or_insert_with(|| Box::new(T::default()))
+            .downcast_ref()
+            .unwrap()
+    }
+
     pub fn set_global<T: 'static>(&mut self, state: T) {
         self.cx.globals.insert(TypeId::of::<T>(), Box::new(state));
     }
 
-    pub fn update_global<T: 'static, F, U>(&mut self, update: F) -> U
+    pub fn update_default_global<T, F, U>(&mut self, update: F) -> U
+    where
+        T: 'static + Default,
+        F: FnOnce(&mut T, &mut MutableAppContext) -> U,
+    {
+        let type_id = TypeId::of::<T>();
+        let mut state = self
+            .cx
+            .globals
+            .remove(&type_id)
+            .unwrap_or_else(|| Box::new(T::default()));
+        let result = update(state.downcast_mut().unwrap(), self);
+        self.cx.globals.insert(type_id, state);
+        result
+    }
+
+    pub fn update_global<T, F, U>(&mut self, update: F) -> U
     where
+        T: 'static,
         F: FnOnce(&mut T, &mut MutableAppContext) -> U,
     {
         let type_id = TypeId::of::<T>();
@@ -1377,7 +1403,7 @@ impl MutableAppContext {
             .cx
             .globals
             .remove(&type_id)
-            .expect("no app state has been added for this type");
+            .expect("no global has been added for this type");
         let result = update(state.downcast_mut().unwrap(), self);
         self.cx.globals.insert(type_id, state);
         result
@@ -3715,6 +3741,10 @@ impl AnyModelHandle {
     pub fn is<T: Entity>(&self) -> bool {
         self.model_type == TypeId::of::<T>()
     }
+
+    pub fn model_type(&self) -> TypeId {
+        self.model_type
+    }
 }
 
 impl<T: Entity> From<ModelHandle<T>> for AnyModelHandle {

crates/project/src/project.rs 🔗

@@ -11,8 +11,8 @@ use collections::{hash_map, BTreeMap, HashMap, HashSet};
 use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt};
 use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 use gpui::{
-    AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
-    UpgradeModelHandle, WeakModelHandle,
+    AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
+    MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
 };
 use language::{
     proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
@@ -822,6 +822,23 @@ impl Project {
         Ok(buffer)
     }
 
+    pub fn open_path(
+        &mut self,
+        path: impl Into<ProjectPath>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<(ProjectEntryId, AnyModelHandle)>> {
+        let task = self.open_buffer(path, cx);
+        cx.spawn_weak(|_, cx| async move {
+            let buffer = task.await?;
+            let project_entry_id = buffer
+                .read_with(&cx, |buffer, cx| {
+                    File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
+                })
+                .ok_or_else(|| anyhow!("no project entry"))?;
+            Ok((project_entry_id, buffer.into()))
+        })
+    }
+
     pub fn open_buffer(
         &mut self,
         path: impl Into<ProjectPath>,

crates/workspace/src/workspace.rs 🔗

@@ -9,6 +9,7 @@ mod status_bar;
 use anyhow::{anyhow, Result};
 use client::{Authenticate, ChannelList, Client, User, UserStore};
 use clock::ReplicaId;
+use collections::HashMap;
 use gpui::{
     action,
     color::Color,
@@ -17,11 +18,11 @@ use gpui::{
     json::{self, to_string_pretty, ToJson},
     keymap::Binding,
     platform::{CursorStyle, WindowOptions},
-    AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle, MutableAppContext,
-    PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
-    WeakViewHandle,
+    AnyModelHandle, AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle,
+    MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext,
+    ViewHandle, WeakViewHandle,
 };
-use language::{Buffer, LanguageRegistry};
+use language::LanguageRegistry;
 use log::error;
 pub use pane::*;
 pub use pane_group::*;
@@ -41,13 +42,16 @@ use std::{
 };
 use theme::{Theme, ThemeRegistry};
 
-pub type BuildEditor = Arc<
-    dyn Fn(
-        usize,
-        ModelHandle<Project>,
-        ModelHandle<Buffer>,
-        &mut MutableAppContext,
-    ) -> Box<dyn ItemHandle>,
+type ItemBuilders = HashMap<
+    TypeId,
+    Arc<
+        dyn Fn(
+            usize,
+            ModelHandle<Project>,
+            AnyModelHandle,
+            &mut MutableAppContext,
+        ) -> Box<dyn ItemHandle>,
+    >,
 >;
 
 action!(Open, Arc<AppState>);
@@ -104,14 +108,20 @@ pub fn init(cx: &mut MutableAppContext) {
     ]);
 }
 
-pub fn register_editor_builder<F, V>(cx: &mut MutableAppContext, build_editor: F)
+pub fn register_project_item<F, V>(cx: &mut MutableAppContext, build_item: F)
 where
-    V: Item,
-    F: 'static + Fn(ModelHandle<Project>, ModelHandle<Buffer>, &mut ViewContext<V>) -> V,
+    V: ProjectItem,
+    F: 'static + Fn(ModelHandle<Project>, ModelHandle<V::Item>, &mut ViewContext<V>) -> V,
 {
-    cx.set_global::<BuildEditor>(Arc::new(move |window_id, project, model, cx| {
-        Box::new(cx.add_view(window_id, |cx| build_editor(project, model, cx)))
-    }));
+    cx.update_default_global(|builders: &mut ItemBuilders, _| {
+        builders.insert(
+            TypeId::of::<V::Item>(),
+            Arc::new(move |window_id, project, model, cx| {
+                let model = model.downcast::<V::Item>().unwrap();
+                Box::new(cx.add_view(window_id, |cx| build_item(project, model, cx)))
+            }),
+        );
+    });
 }
 
 pub struct AppState {
@@ -826,20 +836,19 @@ impl Workspace {
         )>,
     > {
         let project = self.project().clone();
-        let buffer = project.update(cx, |project, cx| project.open_buffer(path, cx));
-        cx.spawn(|this, mut cx| async move {
-            let buffer = buffer.await?;
-            let project_entry_id = buffer.read_with(&cx, |buffer, cx| {
-                project::File::from_dyn(buffer.file())
-                    .and_then(|file| file.project_entry_id(cx))
-                    .ok_or_else(|| anyhow!("buffer has no entry"))
+        let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
+        let window_id = cx.window_id();
+        cx.as_mut().spawn(|mut cx| async move {
+            let (project_entry_id, project_item) = project_item.await?;
+            let build_item = cx.update(|cx| {
+                cx.default_global::<ItemBuilders>()
+                    .get(&project_item.model_type())
+                    .ok_or_else(|| anyhow!("no item builder for project item"))
+                    .cloned()
             })?;
-            let (window_id, build_editor) = this.update(&mut cx, |_, cx| {
-                (cx.window_id(), cx.global::<BuildEditor>().clone())
-            });
-            let build_editor =
-                move |cx: &mut MutableAppContext| build_editor(window_id, project, buffer, cx);
-            Ok((project_entry_id, build_editor))
+            let build_item =
+                move |cx: &mut MutableAppContext| build_item(window_id, project, project_item, cx);
+            Ok((project_entry_id, build_item))
         })
     }