Reuse views when moving between diagnostic view and editors

Max Brunsfeld and Nathan Sobo created

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

Change summary

crates/diagnostics/src/diagnostics.rs | 13 +++++++--
crates/editor/src/items.rs            |  4 ++
crates/gpui/src/app.rs                | 25 +++++++++++++++++
crates/workspace/src/pane.rs          | 34 ++++++++++++++++-------
crates/workspace/src/workspace.rs     | 41 ++++++++++++++++++++++++++--
crates/zed/src/zed.rs                 |  7 ++--
6 files changed, 103 insertions(+), 21 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -165,8 +165,13 @@ impl ProjectDiagnosticsEditor {
     }
 
     fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
-        let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
-        workspace.open_item(diagnostics, cx);
+        if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
+            workspace.activate_pane_for_item(&existing, cx);
+        } else {
+            let diagnostics =
+                cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
+            workspace.open_item(diagnostics, cx);
+        }
     }
 
     fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
@@ -191,8 +196,10 @@ impl ProjectDiagnosticsEditor {
 
             workspace.update(cx, |workspace, cx| {
                 for (buffer, ranges) in new_selections_by_buffer {
+                    let buffer = BufferItemHandle(buffer);
+                    workspace.activate_pane_for_item(&buffer, cx);
                     let editor = workspace
-                        .open_item(BufferItemHandle(buffer), cx)
+                        .open_item(buffer, cx)
                         .to_any()
                         .downcast::<Editor>()
                         .unwrap();

crates/editor/src/items.rs 🔗

@@ -62,6 +62,10 @@ impl ItemHandle for BufferItemHandle {
         Box::new(self.clone())
     }
 
+    fn to_any(&self) -> gpui::AnyModelHandle {
+        self.0.clone().into()
+    }
+
     fn downgrade(&self) -> Box<dyn workspace::WeakItemHandle> {
         Box::new(WeakBufferItemHandle(self.0.downgrade()))
     }

crates/gpui/src/app.rs 🔗

@@ -3097,14 +3097,39 @@ impl Drop for AnyViewHandle {
 
 pub struct AnyModelHandle {
     model_id: usize,
+    model_type: TypeId,
     ref_counts: Arc<Mutex<RefCounts>>,
 }
 
+impl AnyModelHandle {
+    pub fn downcast<T: Entity>(self) -> Option<ModelHandle<T>> {
+        if self.is::<T>() {
+            let result = Some(ModelHandle {
+                model_id: self.model_id,
+                model_type: PhantomData,
+                ref_counts: self.ref_counts.clone(),
+            });
+            unsafe {
+                Arc::decrement_strong_count(&self.ref_counts);
+            }
+            std::mem::forget(self);
+            result
+        } else {
+            None
+        }
+    }
+
+    pub fn is<T: Entity>(&self) -> bool {
+        self.model_type == TypeId::of::<T>()
+    }
+}
+
 impl<T: Entity> From<ModelHandle<T>> for AnyModelHandle {
     fn from(handle: ModelHandle<T>) -> Self {
         handle.ref_counts.lock().inc_model(handle.model_id);
         Self {
             model_id: handle.model_id,
+            model_type: TypeId::of::<T>(),
             ref_counts: handle.ref_counts.clone(),
         }
     }

crates/workspace/src/pane.rs 🔗

@@ -69,7 +69,7 @@ pub struct TabState {
 }
 
 pub struct Pane {
-    item_views: Vec<Box<dyn ItemViewHandle>>,
+    item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
     active_item: usize,
     settings: watch::Receiver<Settings>,
 }
@@ -96,8 +96,8 @@ impl Pane {
     where
         T: 'static + ItemHandle,
     {
-        for (ix, item_view) in self.item_views.iter().enumerate() {
-            if item_view.item_handle(cx).id() == item_handle.id() {
+        for (ix, (item_id, item_view)) in self.item_views.iter().enumerate() {
+            if *item_id == item_handle.id() {
                 let item_view = item_view.boxed_clone();
                 self.activate_item(ix, cx);
                 return item_view;
@@ -116,21 +116,33 @@ impl Pane {
     ) {
         item_view.added_to_pane(cx);
         let item_idx = cmp::min(self.active_item + 1, self.item_views.len());
-        self.item_views.insert(item_idx, item_view);
+        self.item_views
+            .insert(item_idx, (item_view.item_handle(cx).id(), item_view));
         self.activate_item(item_idx, cx);
         cx.notify();
     }
 
-    pub fn item_views(&self) -> &[Box<dyn ItemViewHandle>] {
-        &self.item_views
+    pub fn contains_item(&self, item: &dyn ItemHandle) -> bool {
+        let item_id = item.id();
+        self.item_views
+            .iter()
+            .any(|(existing_item_id, _)| *existing_item_id == item_id)
+    }
+
+    pub fn item_views(&self) -> impl Iterator<Item = &Box<dyn ItemViewHandle>> {
+        self.item_views.iter().map(|(_, view)| view)
     }
 
     pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
-        self.item_views.get(self.active_item).cloned()
+        self.item_views
+            .get(self.active_item)
+            .map(|(_, view)| view.clone())
     }
 
     pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option<usize> {
-        self.item_views.iter().position(|i| i.id() == item.id())
+        self.item_views
+            .iter()
+            .position(|(_, i)| i.id() == item.id())
     }
 
     pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
@@ -163,12 +175,12 @@ impl Pane {
 
     pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
         if !self.item_views.is_empty() {
-            self.close_item(self.item_views[self.active_item].id(), cx)
+            self.close_item(self.item_views[self.active_item].1.id(), cx)
         }
     }
 
     pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext<Self>) {
-        self.item_views.retain(|item| item.id() != item_id);
+        self.item_views.retain(|(_, item)| item.id() != item_id);
         self.active_item = cmp::min(self.active_item, self.item_views.len().saturating_sub(1));
         if self.item_views.is_empty() {
             cx.emit(Event::Remove);
@@ -193,7 +205,7 @@ impl Pane {
         enum Tabs {}
         let tabs = MouseEventHandler::new::<Tabs, _, _, _>(cx.view_id(), cx, |mouse_state, cx| {
             let mut row = Flex::row();
-            for (ix, item_view) in self.item_views.iter().enumerate() {
+            for (ix, (_, item_view)) in self.item_views.iter().enumerate() {
                 let is_active = ix == self.active_item;
 
                 row.add_child({

crates/workspace/src/workspace.rs 🔗

@@ -16,9 +16,9 @@ use gpui::{
     json::{self, to_string_pretty, ToJson},
     keymap::Binding,
     platform::{CursorStyle, WindowOptions},
-    AnyViewHandle, AppContext, ClipboardItem, Entity, ModelContext, ModelHandle, MutableAppContext,
-    PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
-    WeakModelHandle, WeakViewHandle,
+    AnyModelHandle, AnyViewHandle, AppContext, ClipboardItem, Entity, ModelContext, ModelHandle,
+    MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext,
+    ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use language::LanguageRegistry;
 use log::error;
@@ -186,6 +186,7 @@ pub trait ItemHandle: Send + Sync {
     ) -> Box<dyn ItemViewHandle>;
     fn boxed_clone(&self) -> Box<dyn ItemHandle>;
     fn downgrade(&self) -> Box<dyn WeakItemHandle>;
+    fn to_any(&self) -> AnyModelHandle;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
 }
 
@@ -238,6 +239,10 @@ impl<T: Item> ItemHandle for ModelHandle<T> {
         Box::new(self.downgrade())
     }
 
+    fn to_any(&self) -> AnyModelHandle {
+        self.clone().into()
+    }
+
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
         self.read(cx).project_path()
     }
@@ -265,6 +270,10 @@ impl ItemHandle for Box<dyn ItemHandle> {
         self.as_ref().downgrade()
     }
 
+    fn to_any(&self) -> AnyModelHandle {
+        self.as_ref().to_any()
+    }
+
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
         self.as_ref().project_path(cx)
     }
@@ -760,6 +769,12 @@ impl Workspace {
             .find(|i| i.project_path(cx).as_ref() == Some(path))
     }
 
+    pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ModelHandle<T>> {
+        self.items
+            .iter()
+            .find_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast()))
+    }
+
     pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemViewHandle>> {
         self.active_pane().read(cx).active_item()
     }
@@ -927,6 +942,26 @@ impl Workspace {
         pane.update(cx, |pane, cx| pane.open_item(item_handle, self, cx))
     }
 
+    pub fn activate_pane_for_item(
+        &mut self,
+        item: &dyn ItemHandle,
+        cx: &mut ViewContext<Self>,
+    ) -> bool {
+        let pane = self.panes.iter().find_map(|pane| {
+            if pane.read(cx).contains_item(item) {
+                Some(pane.clone())
+            } else {
+                None
+            }
+        });
+        if let Some(pane) = pane {
+            self.activate_pane(pane.clone(), cx);
+            true
+        } else {
+            false
+        }
+    }
+
     fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
         self.active_pane = pane;
         self.status_bar.update(cx, |status_bar, cx| {

crates/zed/src/zed.rs 🔗

@@ -273,7 +273,7 @@ mod tests {
                 pane.active_item().unwrap().project_path(cx),
                 Some(file1.clone())
             );
-            assert_eq!(pane.item_views().len(), 1);
+            assert_eq!(pane.item_views().count(), 1);
         });
 
         // Open the second entry
@@ -287,7 +287,7 @@ mod tests {
                 pane.active_item().unwrap().project_path(cx),
                 Some(file2.clone())
             );
-            assert_eq!(pane.item_views().len(), 2);
+            assert_eq!(pane.item_views().count(), 2);
         });
 
         // Open the first entry again. The existing pane item is activated.
@@ -303,7 +303,7 @@ mod tests {
                 pane.active_item().unwrap().project_path(cx),
                 Some(file1.clone())
             );
-            assert_eq!(pane.item_views().len(), 2);
+            assert_eq!(pane.item_views().count(), 2);
         });
 
         // Split the pane with the first entry, then open the second entry again.
@@ -343,7 +343,6 @@ mod tests {
             );
             let pane_entries = pane
                 .item_views()
-                .iter()
                 .map(|i| i.project_path(cx).unwrap())
                 .collect::<Vec<_>>();
             assert_eq!(pane_entries, &[file1, file2, file3]);