WIP: Massage opening of editors

Nathan Sobo and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

crates/diagnostics/src/diagnostics.rs |   8 
crates/editor/src/editor.rs           |   9 
crates/editor/src/items.rs            |   8 
crates/project/src/project.rs         | 103 ++++++++++----
crates/search/src/project_search.rs   |   4 
crates/server/src/rpc.rs              | 102 ++++++++++----
crates/workspace/src/pane.rs          |  48 +++++--
crates/workspace/src/workspace.rs     | 190 +++++++++++++++++++++++-----
8 files changed, 338 insertions(+), 134 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -155,7 +155,9 @@ impl ProjectDiagnosticsEditor {
             async move {
                 for path in paths {
                     let buffer = project
-                        .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
+                        .update(&mut cx, |project, cx| {
+                            project.open_buffer_for_path(path.clone(), cx)
+                        })
                         .await?;
                     this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx))
                 }
@@ -449,10 +451,6 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
         None
     }
 
-    fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
-        None
-    }
-
     fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
         self.editor
             .update(cx, |editor, cx| editor.navigate(data, cx));

crates/editor/src/editor.rs 🔗

@@ -846,10 +846,7 @@ impl Editor {
             .and_then(|file| file.project_entry_id(cx))
         {
             return workspace
-                .open_item_for_project_entry(project_entry, cx, |cx| {
-                    let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-                    Editor::for_buffer(multibuffer, Some(project.clone()), cx)
-                })
+                .open_editor(project_entry, cx)
                 .downcast::<Editor>()
                 .unwrap();
         }
@@ -8442,7 +8439,9 @@ mod tests {
             .0
             .read_with(cx, |tree, _| tree.id());
         let buffer = project
-            .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
+            .update(cx, |project, cx| {
+                project.open_buffer_for_path((worktree_id, ""), cx)
+            })
             .await
             .unwrap();
         let mut fake_server = fake_servers.next().await.unwrap();

crates/editor/src/items.rs 🔗

@@ -5,7 +5,7 @@ use gpui::{
     View, ViewContext, ViewHandle, WeakModelHandle,
 };
 use language::{Bias, Buffer, Diagnostic, File as _};
-use project::{File, Project, ProjectEntryId, ProjectPath};
+use project::{File, Project, ProjectPath};
 use std::fmt::Write;
 use std::path::PathBuf;
 use text::{Point, Selection};
@@ -34,7 +34,7 @@ impl PathOpener for BufferOpener {
         window_id: usize,
         cx: &mut ModelContext<Project>,
     ) -> Option<Task<Result<Box<dyn ItemViewHandle>>>> {
-        let buffer = project.open_buffer(project_path, cx);
+        let buffer = project.open_buffer_for_path(project_path, cx);
         Some(cx.spawn(|project, mut cx| async move {
             let buffer = buffer.await?;
             let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
@@ -75,10 +75,6 @@ impl ItemView for Editor {
         })
     }
 
-    fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
-        File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry_id(cx))
-    }
-
     fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
     where
         Self: Sized,

crates/project/src/project.rs 🔗

@@ -818,7 +818,19 @@ impl Project {
         Ok(buffer)
     }
 
-    pub fn open_buffer(
+    pub fn open_buffer_for_entry(
+        &mut self,
+        entry_id: ProjectEntryId,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ModelHandle<Buffer>>> {
+        if let Some(project_path) = self.path_for_entry(entry_id, cx) {
+            self.open_buffer_for_path(project_path, cx)
+        } else {
+            Task::ready(Err(anyhow!("entry not found")))
+        }
+    }
+
+    pub fn open_buffer_for_path(
         &mut self,
         path: impl Into<ProjectPath>,
         cx: &mut ModelContext<Self>,
@@ -953,8 +965,10 @@ impl Project {
                 worktree_id: worktree.read_with(&cx, |worktree, _| worktree.id()),
                 path: relative_path.into(),
             };
-            this.update(&mut cx, |this, cx| this.open_buffer(project_path, cx))
-                .await
+            this.update(&mut cx, |this, cx| {
+                this.open_buffer_for_path(project_path, cx)
+            })
+            .await
         })
     }
 
@@ -2854,7 +2868,9 @@ impl Project {
                     let buffers_tx = buffers_tx.clone();
                     cx.spawn(|mut cx| async move {
                         if let Some(buffer) = this
-                            .update(&mut cx, |this, cx| this.open_buffer(project_path, cx))
+                            .update(&mut cx, |this, cx| {
+                                this.open_buffer_for_path(project_path, cx)
+                            })
                             .await
                             .log_err()
                         {
@@ -3258,6 +3274,14 @@ impl Project {
             .map(|entry| entry.id)
     }
 
+    pub fn path_for_entry(&self, entry_id: ProjectEntryId, cx: &AppContext) -> Option<ProjectPath> {
+        let worktree = self.worktree_for_entry(entry_id, cx)?;
+        let worktree = worktree.read(cx);
+        let worktree_id = worktree.id();
+        let path = worktree.entry_for_id(entry_id)?.path.clone();
+        Some(ProjectPath { worktree_id, path })
+    }
+
     // RPC message handlers
 
     async fn handle_unshare_project(
@@ -3867,7 +3891,7 @@ impl Project {
         let peer_id = envelope.original_sender_id()?;
         let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
         let open_buffer = this.update(&mut cx, |this, cx| {
-            this.open_buffer(
+            this.open_buffer_for_path(
                 ProjectPath {
                     worktree_id,
                     path: PathBuf::from(envelope.payload.path).into(),
@@ -4664,7 +4688,7 @@ mod tests {
         // Open a buffer without an associated language server.
         let toml_buffer = project
             .update(cx, |project, cx| {
-                project.open_buffer((worktree_id, "Cargo.toml"), cx)
+                project.open_buffer_for_path((worktree_id, "Cargo.toml"), cx)
             })
             .await
             .unwrap();
@@ -4672,7 +4696,7 @@ mod tests {
         // Open a buffer with an associated language server.
         let rust_buffer = project
             .update(cx, |project, cx| {
-                project.open_buffer((worktree_id, "test.rs"), cx)
+                project.open_buffer_for_path((worktree_id, "test.rs"), cx)
             })
             .await
             .unwrap();
@@ -4719,7 +4743,7 @@ mod tests {
         // Open a third buffer with a different associated language server.
         let json_buffer = project
             .update(cx, |project, cx| {
-                project.open_buffer((worktree_id, "package.json"), cx)
+                project.open_buffer_for_path((worktree_id, "package.json"), cx)
             })
             .await
             .unwrap();
@@ -4750,7 +4774,7 @@ mod tests {
         // it is also configured based on the existing language server's capabilities.
         let rust_buffer2 = project
             .update(cx, |project, cx| {
-                project.open_buffer((worktree_id, "test2.rs"), cx)
+                project.open_buffer_for_path((worktree_id, "test2.rs"), cx)
             })
             .await
             .unwrap();
@@ -4861,7 +4885,7 @@ mod tests {
         // Cause worktree to start the fake language server
         let _buffer = project
             .update(cx, |project, cx| {
-                project.open_buffer((worktree_id, Path::new("b.rs")), cx)
+                project.open_buffer_for_path((worktree_id, Path::new("b.rs")), cx)
             })
             .await
             .unwrap();
@@ -4908,7 +4932,9 @@ mod tests {
         );
 
         let buffer = project
-            .update(cx, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
+            .update(cx, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "a.rs"), cx)
+            })
             .await
             .unwrap();
 
@@ -4975,7 +5001,7 @@ mod tests {
 
         let buffer = project
             .update(cx, |project, cx| {
-                project.open_buffer((worktree_id, "a.rs"), cx)
+                project.open_buffer_for_path((worktree_id, "a.rs"), cx)
             })
             .await
             .unwrap();
@@ -5251,7 +5277,7 @@ mod tests {
 
         let buffer = project
             .update(cx, |project, cx| {
-                project.open_buffer((worktree_id, "a.rs"), cx)
+                project.open_buffer_for_path((worktree_id, "a.rs"), cx)
             })
             .await
             .unwrap();
@@ -5356,7 +5382,7 @@ mod tests {
 
         let buffer = project
             .update(cx, |project, cx| {
-                project.open_buffer((worktree_id, "a.rs"), cx)
+                project.open_buffer_for_path((worktree_id, "a.rs"), cx)
             })
             .await
             .unwrap();
@@ -5514,7 +5540,7 @@ mod tests {
 
         let buffer = project
             .update(cx, |project, cx| {
-                project.open_buffer((worktree_id, "a.rs"), cx)
+                project.open_buffer_for_path((worktree_id, "a.rs"), cx)
             })
             .await
             .unwrap();
@@ -5697,7 +5723,7 @@ mod tests {
 
         let buffer = project
             .update(cx, |project, cx| {
-                project.open_buffer(
+                project.open_buffer_for_path(
                     ProjectPath {
                         worktree_id,
                         path: Path::new("").into(),
@@ -5793,7 +5819,9 @@ mod tests {
             .read_with(cx, |tree, _| tree.id());
 
         let buffer = project
-            .update(cx, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
+            .update(cx, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "file1"), cx)
+            })
             .await
             .unwrap();
         buffer
@@ -5831,7 +5859,7 @@ mod tests {
             .read_with(cx, |tree, _| tree.id());
 
         let buffer = project
-            .update(cx, |p, cx| p.open_buffer((worktree_id, ""), cx))
+            .update(cx, |p, cx| p.open_buffer_for_path((worktree_id, ""), cx))
             .await
             .unwrap();
         buffer
@@ -5881,7 +5909,7 @@ mod tests {
 
         let opened_buffer = project
             .update(cx, |project, cx| {
-                project.open_buffer((worktree_id, "file1"), cx)
+                project.open_buffer_for_path((worktree_id, "file1"), cx)
             })
             .await
             .unwrap();
@@ -5916,7 +5944,8 @@ mod tests {
         let worktree_id = tree.read_with(cx, |tree, _| tree.id());
 
         let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
-            let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, path), cx));
+            let buffer =
+                project.update(cx, |p, cx| p.open_buffer_for_path((worktree_id, path), cx));
             async move { buffer.await.unwrap() }
         };
         let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| {
@@ -6065,9 +6094,9 @@ mod tests {
         // Spawn multiple tasks to open paths, repeating some paths.
         let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
             (
-                p.open_buffer((worktree_id, "a.txt"), cx),
-                p.open_buffer((worktree_id, "b.txt"), cx),
-                p.open_buffer((worktree_id, "a.txt"), cx),
+                p.open_buffer_for_path((worktree_id, "a.txt"), cx),
+                p.open_buffer_for_path((worktree_id, "b.txt"), cx),
+                p.open_buffer_for_path((worktree_id, "a.txt"), cx),
             )
         });
 
@@ -6084,7 +6113,9 @@ mod tests {
         // Open the same path again while it is still open.
         drop(buffer_a_1);
         let buffer_a_3 = project
-            .update(cx, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+            .update(cx, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "a.txt"), cx)
+            })
             .await
             .unwrap();
 
@@ -6117,7 +6148,9 @@ mod tests {
             .await;
 
         let buffer1 = project
-            .update(cx, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
+            .update(cx, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "file1"), cx)
+            })
             .await
             .unwrap();
         let events = Rc::new(RefCell::new(Vec::new()));
@@ -6187,7 +6220,9 @@ mod tests {
         // When a file is deleted, the buffer is considered dirty.
         let events = Rc::new(RefCell::new(Vec::new()));
         let buffer2 = project
-            .update(cx, |p, cx| p.open_buffer((worktree_id, "file2"), cx))
+            .update(cx, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "file2"), cx)
+            })
             .await
             .unwrap();
         buffer2.update(cx, |_, cx| {
@@ -6208,7 +6243,9 @@ mod tests {
         // When a file is already dirty when deleted, we don't emit a Dirtied event.
         let events = Rc::new(RefCell::new(Vec::new()));
         let buffer3 = project
-            .update(cx, |p, cx| p.open_buffer((worktree_id, "file3"), cx))
+            .update(cx, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "file3"), cx)
+            })
             .await
             .unwrap();
         buffer3.update(cx, |_, cx| {
@@ -6254,7 +6291,9 @@ mod tests {
 
         let abs_path = dir.path().join("the-file");
         let buffer = project
-            .update(cx, |p, cx| p.open_buffer((worktree_id, "the-file"), cx))
+            .update(cx, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "the-file"), cx)
+            })
             .await
             .unwrap();
 
@@ -6360,7 +6399,9 @@ mod tests {
         let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
 
         let buffer = project
-            .update(cx, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
+            .update(cx, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "a.rs"), cx)
+            })
             .await
             .unwrap();
 
@@ -6633,7 +6674,7 @@ mod tests {
 
         let buffer = project
             .update(cx, |project, cx| {
-                project.open_buffer((worktree_id, Path::new("one.rs")), cx)
+                project.open_buffer_for_path((worktree_id, Path::new("one.rs")), cx)
             })
             .await
             .unwrap();
@@ -6771,7 +6812,7 @@ mod tests {
 
         let buffer_4 = project
             .update(cx, |project, cx| {
-                project.open_buffer((worktree_id, "four.rs"), cx)
+                project.open_buffer_for_path((worktree_id, "four.rs"), cx)
             })
             .await
             .unwrap();

crates/search/src/project_search.rs 🔗

@@ -250,10 +250,6 @@ impl ItemView for ProjectSearchView {
         None
     }
 
-    fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
-        None
-    }
-
     fn can_save(&self, _: &gpui::AppContext) -> bool {
         true
     }

crates/server/src/rpc.rs 🔗

@@ -1137,7 +1137,9 @@ mod tests {
 
         // Open the same file as client B and client A.
         let buffer_b = project_b
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
+            .update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "b.txt"), cx)
+            })
             .await
             .unwrap();
         let buffer_b = cx_b.add_model(|cx| MultiBuffer::singleton(buffer_b, cx));
@@ -1148,7 +1150,9 @@ mod tests {
             assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
         });
         let buffer_a = project_a
-            .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
+            .update(cx_a, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "b.txt"), cx)
+            })
             .await
             .unwrap();
 
@@ -1238,7 +1242,9 @@ mod tests {
         .await
         .unwrap();
         project_b
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+            .update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "a.txt"), cx)
+            })
             .await
             .unwrap();
 
@@ -1273,7 +1279,9 @@ mod tests {
         .await
         .unwrap();
         project_b2
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+            .update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "a.txt"), cx)
+            })
             .await
             .unwrap();
     }
@@ -1352,11 +1360,15 @@ mod tests {
 
         // Open and edit a buffer as both guests B and C.
         let buffer_b = project_b
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
+            .update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "file1"), cx)
+            })
             .await
             .unwrap();
         let buffer_c = project_c
-            .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
+            .update(cx_c, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "file1"), cx)
+            })
             .await
             .unwrap();
         buffer_b.update(cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx));
@@ -1364,7 +1376,9 @@ mod tests {
 
         // Open and edit that buffer as the host.
         let buffer_a = project_a
-            .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
+            .update(cx_a, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "file1"), cx)
+            })
             .await
             .unwrap();
 
@@ -1514,7 +1528,9 @@ mod tests {
 
         // Open a buffer as client B
         let buffer_b = project_b
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+            .update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "a.txt"), cx)
+            })
             .await
             .unwrap();
 
@@ -1597,7 +1613,9 @@ mod tests {
 
         // Open a buffer as client B
         let buffer_b = project_b
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+            .update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "a.txt"), cx)
+            })
             .await
             .unwrap();
         buffer_b.read_with(cx_b, |buf, _| {
@@ -1677,14 +1695,16 @@ mod tests {
 
         // Open a buffer as client A
         let buffer_a = project_a
-            .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+            .update(cx_a, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "a.txt"), cx)
+            })
             .await
             .unwrap();
 
         // Start opening the same buffer as client B
-        let buffer_b = cx_b
-            .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
+        let buffer_b = cx_b.background().spawn(project_b.update(cx_b, |p, cx| {
+            p.open_buffer_for_path((worktree_id, "a.txt"), cx)
+        }));
 
         // Edit the buffer as client A while client B is still opening it.
         cx_b.background().simulate_random_delay().await;
@@ -1760,9 +1780,9 @@ mod tests {
             .await;
 
         // Begin opening a buffer as client B, but leave the project before the open completes.
-        let buffer_b = cx_b
-            .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
+        let buffer_b = cx_b.background().spawn(project_b.update(cx_b, |p, cx| {
+            p.open_buffer_for_path((worktree_id, "a.txt"), cx)
+        }));
         cx_b.update(|_| drop(project_b));
         drop(buffer_b);
 
@@ -1932,7 +1952,7 @@ mod tests {
         let _ = cx_a
             .background()
             .spawn(project_a.update(cx_a, |project, cx| {
-                project.open_buffer(
+                project.open_buffer_for_path(
                     ProjectPath {
                         worktree_id,
                         path: Path::new("other.rs").into(),
@@ -2053,7 +2073,9 @@ mod tests {
         // Open the file with the errors on client B. They should be present.
         let buffer_b = cx_b
             .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
+            .spawn(project_b.update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "a.rs"), cx)
+            }))
             .await
             .unwrap();
 
@@ -2171,7 +2193,9 @@ mod tests {
 
         // Open a file in an editor as the guest.
         let buffer_b = project_b
-            .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+            .update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "main.rs"), cx)
+            })
             .await
             .unwrap();
         let (window_b, _) = cx_b.add_window(|_| EmptyView);
@@ -2245,7 +2269,9 @@ mod tests {
 
         // Open the buffer on the host.
         let buffer_a = project_a
-            .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
+            .update(cx_a, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "main.rs"), cx)
+            })
             .await
             .unwrap();
         buffer_a
@@ -2369,7 +2395,9 @@ mod tests {
 
         let buffer_b = cx_b
             .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
+            .spawn(project_b.update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "a.rs"), cx)
+            }))
             .await
             .unwrap();
 
@@ -2477,7 +2505,9 @@ mod tests {
         // Open the file on client B.
         let buffer_b = cx_b
             .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
+            .spawn(project_b.update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "a.rs"), cx)
+            }))
             .await
             .unwrap();
 
@@ -2616,7 +2646,9 @@ mod tests {
         // Open the file on client B.
         let buffer_b = cx_b
             .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
+            .spawn(project_b.update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "one.rs"), cx)
+            }))
             .await
             .unwrap();
 
@@ -2845,7 +2877,9 @@ mod tests {
         // Open the file on client B.
         let buffer_b = cx_b
             .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
+            .spawn(project_b.update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "main.rs"), cx)
+            }))
             .await
             .unwrap();
 
@@ -2992,7 +3026,9 @@ mod tests {
         // Cause the language server to start.
         let _buffer = cx_b
             .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
+            .spawn(project_b.update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "one.rs"), cx)
+            }))
             .await
             .unwrap();
 
@@ -3123,7 +3159,9 @@ mod tests {
 
         let buffer_b1 = cx_b
             .background()
-            .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
+            .spawn(project_b.update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "a.rs"), cx)
+            }))
             .await
             .unwrap();
 
@@ -3139,9 +3177,13 @@ mod tests {
         let buffer_b2;
         if rng.gen() {
             definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
-            buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
+            buffer_b2 = project_b.update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "b.rs"), cx)
+            });
         } else {
-            buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
+            buffer_b2 = project_b.update(cx_b, |p, cx| {
+                p.open_buffer_for_path((worktree_id, "b.rs"), cx)
+            });
             definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
         }
 
@@ -4762,7 +4804,7 @@ mod tests {
                                 );
                                 let buffer = project
                                     .update(&mut cx, |project, cx| {
-                                        project.open_buffer(project_path, cx)
+                                        project.open_buffer_for_path(project_path, cx)
                                     })
                                     .await
                                     .unwrap();
@@ -4879,7 +4921,7 @@ mod tests {
                     );
                     let buffer = project
                         .update(&mut cx, |project, cx| {
-                            project.open_buffer(project_path.clone(), cx)
+                            project.open_buffer_for_path(project_path.clone(), cx)
                         })
                         .await
                         .unwrap();

crates/workspace/src/pane.rs 🔗

@@ -258,14 +258,13 @@ impl Pane {
                     if let Some(item) = item.log_err() {
                         pane.update(&mut cx, |pane, cx| {
                             pane.nav_history.borrow_mut().set_mode(mode);
-                            let item = pane.open_item(item, cx);
+                            pane.open_item(item, cx);
                             pane.nav_history
                                 .borrow_mut()
                                 .set_mode(NavigationMode::Normal);
                             if let Some(data) = entry.data {
                                 item.navigate(data, cx);
                             }
-                            item
                         });
                     } else {
                         workspace
@@ -281,34 +280,43 @@ impl Pane {
         }
     }
 
-    pub fn open_item(
+    pub(crate) fn open_editor(
         &mut self,
-        item_view_to_open: Box<dyn ItemViewHandle>,
+        project_entry_id: ProjectEntryId,
         cx: &mut ViewContext<Self>,
+        build_editor: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemViewHandle>,
     ) -> Box<dyn ItemViewHandle> {
-        // Find an existing view for the same project entry.
-        for (ix, (entry_id, item_view)) in self.item_views.iter().enumerate() {
-            if *entry_id == item_view_to_open.project_entry_id(cx) {
+        for (ix, (existing_entry_id, item_view)) in self.item_views.iter().enumerate() {
+            if *existing_entry_id == Some(project_entry_id) {
                 let item_view = item_view.boxed_clone();
                 self.activate_item(ix, cx);
                 return item_view;
             }
         }
 
-        item_view_to_open.set_nav_history(self.nav_history.clone(), cx);
-        self.add_item_view(item_view_to_open.boxed_clone(), cx);
-        item_view_to_open
+        let item_view = build_editor(cx);
+        self.add_item(Some(project_entry_id), item_view.boxed_clone(), cx);
+        item_view
+    }
+
+    pub fn open_item(
+        &mut self,
+        item_view_to_open: Box<dyn ItemViewHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.add_item(None, item_view_to_open.boxed_clone(), cx);
     }
 
-    pub fn add_item_view(
+    pub(crate) fn add_item(
         &mut self,
-        mut item_view: Box<dyn ItemViewHandle>,
+        project_entry_id: Option<ProjectEntryId>,
+        mut item: Box<dyn ItemViewHandle>,
         cx: &mut ViewContext<Self>,
     ) {
-        item_view.added_to_pane(cx);
+        item.set_nav_history(self.nav_history.clone(), cx);
+        item.added_to_pane(cx);
         let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len());
-        self.item_views
-            .insert(item_idx, (item_view.project_entry_id(cx), item_view));
+        self.item_views.insert(item_idx, (project_entry_id, item));
         self.activate_item(item_idx, cx);
         cx.notify();
     }
@@ -323,6 +331,16 @@ impl Pane {
             .map(|(_, view)| view.clone())
     }
 
+    pub fn project_entry_id_for_item(&self, item: &dyn ItemViewHandle) -> Option<ProjectEntryId> {
+        self.item_views.iter().find_map(|(entry_id, existing)| {
+            if existing.id() == item.id() {
+                *entry_id
+            } else {
+                None
+            }
+        })
+    }
+
     pub fn item_for_entry(&self, entry_id: ProjectEntryId) -> Option<Box<dyn ItemViewHandle>> {
         self.item_views.iter().find_map(|(id, view)| {
             if *id == Some(entry_id) {

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 futures::TryFutureExt;
 use gpui::{
     action,
     color::Color,
@@ -21,7 +22,7 @@ use gpui::{
     MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext,
     ViewHandle, WeakViewHandle,
 };
-use language::LanguageRegistry;
+use language::{Buffer, LanguageRegistry};
 use log::error;
 pub use pane::*;
 pub use pane_group::*;
@@ -41,6 +42,15 @@ use std::{
 };
 use theme::{Theme, ThemeRegistry};
 
+pub type BuildEditor = Box<
+    dyn Fn(
+        usize,
+        ModelHandle<Project>,
+        ModelHandle<Buffer>,
+        &mut MutableAppContext,
+    ) -> Box<dyn ItemViewHandle>,
+>;
+
 action!(Open, Arc<AppState>);
 action!(OpenNew, Arc<AppState>);
 action!(OpenPaths, OpenParams);
@@ -95,6 +105,16 @@ pub fn init(cx: &mut MutableAppContext) {
     ]);
 }
 
+pub fn register_editor_builder<F, V>(cx: &mut MutableAppContext, build_editor: F)
+where
+    V: ItemView,
+    F: 'static + Fn(ModelHandle<Project>, ModelHandle<Buffer>, &mut ViewContext<V>) -> V,
+{
+    cx.add_app_state::<BuildEditor>(Box::new(|window_id, project, model, cx| {
+        Box::new(cx.add_view(window_id, |cx| build_editor(project, model, cx)))
+    }));
+}
+
 pub struct AppState {
     pub languages: Arc<LanguageRegistry>,
     pub themes: Arc<ThemeRegistry>,
@@ -138,7 +158,6 @@ pub trait ItemView: View {
     fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) {}
     fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
-    fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
     fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
     fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
     where
@@ -191,7 +210,6 @@ pub trait ItemView: View {
 pub trait ItemViewHandle: 'static {
     fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
-    fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
     fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
     fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext);
     fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
@@ -239,10 +257,6 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         self.read(cx).project_path(cx)
     }
 
-    fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
-        self.read(cx).project_entry_id(cx)
-    }
-
     fn boxed_clone(&self) -> Box<dyn ItemViewHandle> {
         Box::new(self.clone())
     }
@@ -656,6 +670,24 @@ impl Workspace {
         path: ProjectPath,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<Box<dyn ItemViewHandle>, Arc<anyhow::Error>>> {
+        let project_entry = self.project.read(cx).entry_for_path(&path, cx);
+
+        let existing_entry = self
+            .active_pane()
+            .update(cx, |pane, cx| pane.activate_project_entry(project_entry));
+
+        cx.spawn(|this, cx| {
+            if let Some(existing_entry) = existing_entry {
+                return Ok(existing_entry);
+            }
+
+            let load_task = this
+                .update(&mut cx, |this, cx| {
+                    this.load_project_entry(project_entry, cx)
+                })
+                .await;
+        });
+
         let load_task = self.load_path(path, cx);
         let pane = self.active_pane().clone().downgrade();
         cx.as_mut().spawn(|mut cx| async move {
@@ -663,7 +695,7 @@ impl Workspace {
             let pane = pane
                 .upgrade(&cx)
                 .ok_or_else(|| anyhow!("could not upgrade pane reference"))?;
-            Ok(pane.update(&mut cx, |pane, cx| pane.open_item(item, cx)))
+            Ok(pane.update(&mut cx, |pane, cx| pane.open_editor(item, cx)))
         })
     }
 
@@ -672,13 +704,23 @@ impl Workspace {
         path: ProjectPath,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<Box<dyn ItemViewHandle>>> {
-        let project_entry = self.project.read(cx).entry_for_path(&path, cx);
+        if let Some(project_entry) = self.project.read(cx).entry_for_path(&path, cx) {
+            self.load_project_entry(project_entry, cx)
+        } else {
+            Task::ready(Err(anyhow!("no such file {:?}", path)))
+        }
+    }
 
-        if let Some(existing_item) = project_entry.and_then(|entry| {
-            self.panes
-                .iter()
-                .find_map(|pane| pane.read(cx).item_for_entry(entry))
-        }) {
+    pub fn load_project_entry(
+        &mut self,
+        project_entry: ProjectEntryId,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<Box<dyn ItemViewHandle>>> {
+        if let Some(existing_item) = self
+            .panes
+            .iter()
+            .find_map(|pane| pane.read(cx).item_for_entry(project_entry))
+        {
             return Task::ready(Ok(existing_item));
         }
 
@@ -829,37 +871,108 @@ impl Workspace {
         pane
     }
 
-    pub fn open_item(
-        &mut self,
-        item_view: Box<dyn ItemViewHandle>,
-        cx: &mut ViewContext<Self>,
-    ) -> Box<dyn ItemViewHandle> {
+    pub fn open_item(&mut self, item_view: Box<dyn ItemViewHandle>, cx: &mut ViewContext<Self>) {
         self.active_pane()
             .update(cx, |pane, cx| pane.open_item(item_view, cx))
     }
 
-    pub fn open_item_for_project_entry<T, F>(
+    pub fn open_editor(
         &mut self,
         project_entry: ProjectEntryId,
         cx: &mut ViewContext<Self>,
-        build_view: F,
-    ) -> Box<dyn ItemViewHandle>
-    where
-        T: ItemView,
-        F: FnOnce(&mut ViewContext<T>) -> T,
-    {
-        if let Some(existing_item) = self
-            .panes
-            .iter()
-            .find_map(|pane| pane.read(cx).item_for_entry(project_entry))
-        {
-            return existing_item.boxed_clone();
-        }
+    ) -> Task<Result<Box<dyn ItemViewHandle>, Arc<anyhow::Error>>> {
+        let pane = self.active_pane().clone();
+        let project = self.project().clone();
+        let buffer = project.update(cx, |project, cx| {
+            project.open_buffer_for_entry(project_entry, cx)
+        });
 
-        let view = Box::new(cx.add_view(build_view));
-        self.open_item(view, cx)
+        cx.spawn(|this, cx| async move {
+            let buffer = buffer.await?;
+            let editor = this.update(&mut cx, |this, cx| {
+                let window_id = cx.window_id();
+                pane.update(cx, |pane, cx| {
+                    pane.open_editor(project_entry, cx, |cx| {
+                        cx.app_state::<BuildEditor>()(window_id, project, buffer, cx)
+                    })
+                })
+            });
+            Ok(editor)
+        })
     }
 
+    //     pub fn open_path(
+    //     &mut self,
+    //     path: ProjectPath,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> Task<Result<Box<dyn ItemViewHandle>, Arc<anyhow::Error>>> {
+    //     let project_entry = self.project.read(cx).entry_for_path(&path, cx);
+
+    //     let existing_entry = self
+    //         .active_pane()
+    //         .update(cx, |pane, cx| pane.activate_project_entry(project_entry));
+
+    //     cx.spawn(|this, cx| {
+    //         if let Some(existing_entry) = existing_entry {
+    //             return Ok(existing_entry);
+    //         }
+
+    //         let load_task = this
+    //             .update(&mut cx, |this, cx| {
+    //                 this.load_project_entry(project_entry, cx)
+    //             })
+    //             .await;
+    //     });
+
+    //     let load_task = self.load_path(path, cx);
+    //     let pane = self.active_pane().clone().downgrade();
+    //     cx.as_mut().spawn(|mut cx| async move {
+    //         let item = load_task.await?;
+    //         let pane = pane
+    //             .upgrade(&cx)
+    //             .ok_or_else(|| anyhow!("could not upgrade pane reference"))?;
+    //         Ok(pane.update(&mut cx, |pane, cx| pane.open_editor(item, cx)))
+    //     })
+    // }
+
+    // pub fn load_path(
+    //     &mut self,
+    //     path: ProjectPath,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> Task<Result<Box<dyn ItemViewHandle>>> {
+    //     if let Some(project_entry) = self.project.read(cx).entry_for_path(&path, cx) {
+    //         self.load_project_entry(project_entry, cx)
+    //     } else {
+    //         Task::ready(Err(anyhow!("no such file {:?}", path)))
+    //     }
+    // }
+
+    // pub fn load_project_entry(
+    //     &mut self,
+    //     project_entry: ProjectEntryId,
+    //     cx: &mut ViewContext<Self>,
+    // ) -> Task<Result<Box<dyn ItemViewHandle>>> {
+    //     if let Some(existing_item) = self
+    //         .panes
+    //         .iter()
+    //         .find_map(|pane| pane.read(cx).item_for_entry(project_entry))
+    //     {
+    //         return Task::ready(Ok(existing_item));
+    //     }
+
+    //     let project_path = path.clone();
+    //     let path_openers = self.path_openers.clone();
+    //     let window_id = cx.window_id();
+    //     self.project.update(cx, |project, cx| {
+    //         for opener in path_openers.iter() {
+    //             if let Some(task) = opener.open(project, project_path.clone(), window_id, cx) {
+    //                 return task;
+    //             }
+    //         }
+    //         Task::ready(Err(anyhow!("no opener found for path {:?}", project_path)))
+    //     })
+    // }
+
     pub fn activate_item(&mut self, item: &dyn ItemViewHandle, cx: &mut ViewContext<Self>) -> bool {
         let result = self.panes.iter().find_map(|pane| {
             if let Some(ix) = pane.read(cx).index_for_item(item) {
@@ -930,10 +1043,11 @@ impl Workspace {
         let new_pane = self.add_pane(cx);
         self.activate_pane(new_pane.clone(), cx);
         if let Some(item) = pane.read(cx).active_item() {
-            let nav_history = new_pane.read(cx).nav_history().clone();
+            let project_entry_id = pane.read(cx).project_entry_id_for_item(item.as_ref());
             if let Some(clone) = item.clone_on_split(cx.as_mut()) {
-                clone.set_nav_history(nav_history, cx);
-                new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx));
+                new_pane.update(cx, |new_pane, cx| {
+                    new_pane.open_item(project_entry_id, clone, cx);
+                });
             }
         }
         self.center.split(&pane, &new_pane, direction).unwrap();