Introduce a ViewId message, identifying views across calls

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs       |  4 +
crates/editor/src/editor_tests.rs | 25 +++++++++-
crates/editor/src/items.rs        | 25 +++++++---
crates/rpc/proto/zed.proto        | 13 +++-
crates/workspace/src/item.rs      | 30 ++++++++++--
crates/workspace/src/workspace.rs | 75 +++++++++++++++++++++++++-------
6 files changed, 134 insertions(+), 38 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -84,7 +84,7 @@ use std::{
 pub use sum_tree::Bias;
 use theme::{DiagnosticStyle, Theme};
 use util::{post_inc, ResultExt, TryFutureExt};
-use workspace::{ItemNavHistory, Workspace, WorkspaceId};
+use workspace::{ItemNavHistory, ViewId, Workspace, WorkspaceId};
 
 use crate::git::diff_hunk_to_display;
 
@@ -467,6 +467,7 @@ pub struct Editor {
     keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
     input_enabled: bool,
     leader_replica_id: Option<u16>,
+    remote_id: Option<ViewId>,
     hover_state: HoverState,
     link_go_to_definition_state: LinkGoToDefinitionState,
     _subscriptions: Vec<Subscription>,
@@ -1108,6 +1109,7 @@ impl Editor {
             keymap_context_layers: Default::default(),
             input_enabled: true,
             leader_replica_id: None,
+            remote_id: None,
             hover_state: Default::default(),
             link_go_to_definition_state: Default::default(),
             _subscriptions: vec![

crates/editor/src/editor_tests.rs 🔗

@@ -3,6 +3,7 @@ use std::{cell::RefCell, rc::Rc, time::Instant};
 use drag_and_drop::DragAndDrop;
 use futures::StreamExt;
 use indoc::indoc;
+use rpc::PeerId;
 use unindent::Unindent;
 
 use super::*;
@@ -24,7 +25,7 @@ use util::{
 };
 use workspace::{
     item::{FollowableItem, ItemHandle},
-    NavigationEntry, Pane,
+    NavigationEntry, Pane, ViewId,
 };
 
 #[gpui::test]
@@ -5123,7 +5124,16 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
     let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
     let follower_1 = cx
         .update(|cx| {
-            Editor::from_state_proto(pane.clone(), project.clone(), &mut state_message, cx)
+            Editor::from_state_proto(
+                pane.clone(),
+                project.clone(),
+                ViewId {
+                    creator: PeerId(0),
+                    id: 0,
+                },
+                &mut state_message,
+                cx,
+            )
         })
         .unwrap()
         .await
@@ -5209,7 +5219,16 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
     let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
     let follower_2 = cx
         .update(|cx| {
-            Editor::from_state_proto(pane.clone(), project.clone(), &mut state_message, cx)
+            Editor::from_state_proto(
+                pane.clone(),
+                project.clone(),
+                ViewId {
+                    creator: PeerId(0),
+                    id: 0,
+                },
+                &mut state_message,
+                cx,
+            )
         })
         .unwrap()
         .await

crates/editor/src/items.rs 🔗

@@ -28,24 +28,32 @@ use std::{
 };
 use text::Selection;
 use util::{ResultExt, TryFutureExt};
+use workspace::item::FollowableItemHandle;
 use workspace::{
     item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
     searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
-    ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, Workspace, WorkspaceId,
+    ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, Workspace,
+    WorkspaceId,
 };
 
 pub const MAX_TAB_TITLE_LEN: usize = 24;
 
 impl FollowableItem for Editor {
+    fn remote_id(&self) -> Option<ViewId> {
+        self.remote_id
+    }
+
     fn from_state_proto(
         pane: ViewHandle<workspace::Pane>,
         project: ModelHandle<Project>,
+        remote_id: ViewId,
         state: &mut Option<proto::view::Variant>,
         cx: &mut MutableAppContext,
     ) -> Option<Task<Result<ViewHandle<Self>>>> {
         let Some(proto::view::Variant::Editor(_)) = state else { return None };
         let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() };
 
+        let client = project.read(cx).client();
         let replica_id = project.read(cx).replica_id();
         let buffer_ids = state
             .excerpts
@@ -63,13 +71,13 @@ impl FollowableItem for Editor {
             let mut buffers = futures::future::try_join_all(buffers).await?;
             let editor = pane.read_with(&cx, |pane, cx| {
                 let mut editors = pane.items_of_type::<Self>();
-                if state.singleton && buffers.len() == 1 {
-                    editors.find(|editor| {
-                        editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffers[0])
-                    })
-                } else {
-                    None
-                }
+                editors.find(|editor| {
+                    editor.remote_id(&client, cx) == Some(remote_id)
+                        || state.singleton
+                            && buffers.len() == 1
+                            && editor.read(cx).buffer.read(cx).as_singleton().as_ref()
+                                == Some(&buffers[0])
+                })
             });
 
             let editor = editor.unwrap_or_else(|| {
@@ -112,6 +120,7 @@ impl FollowableItem for Editor {
             });
 
             editor.update(&mut cx, |editor, cx| {
+                editor.remote_id = Some(remote_id);
                 let buffer = editor.buffer.read(cx).read(cx);
                 let selections = state
                     .selections

crates/rpc/proto/zed.proto 🔗

@@ -796,7 +796,7 @@ message Follow {
 }
 
 message FollowResponse {
-    optional uint64 active_view_id = 1;
+    optional ViewId active_view_id = 1;
     repeated View views = 2;
 }
 
@@ -824,13 +824,18 @@ message GetPrivateUserInfoResponse {
 
 // Entities
 
+message ViewId {
+    uint32 creator = 1;
+    uint64 id = 2;
+}
+
 message UpdateActiveView {
-    optional uint64 id = 1;
+    optional ViewId id = 1;
     optional uint32 leader_id = 2;
 }
 
 message UpdateView {
-    uint64 id = 1;
+    ViewId id = 1;
     optional uint32 leader_id = 2;
 
     oneof variant {
@@ -848,7 +853,7 @@ message UpdateView {
 }
 
 message View {
-    uint64 id = 1;
+    ViewId id = 1;
     optional uint32 leader_id = 2;
 
     oneof variant {

crates/workspace/src/item.rs 🔗

@@ -5,12 +5,15 @@ use std::{
     fmt,
     path::PathBuf,
     rc::Rc,
-    sync::atomic::{AtomicBool, Ordering},
+    sync::{
+        atomic::{AtomicBool, Ordering},
+        Arc,
+    },
     time::Duration,
 };
 
 use anyhow::Result;
-use client::proto;
+use client::{proto, Client};
 use gpui::{
     AnyViewHandle, AppContext, ElementBox, ModelHandle, MutableAppContext, Task, View, ViewContext,
     ViewHandle, WeakViewHandle,
@@ -23,7 +26,8 @@ use util::ResultExt;
 
 use crate::{
     pane, persistence::model::ItemId, searchable::SearchableItemHandle, DelayedDebouncedEditAction,
-    FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+    FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace,
+    WorkspaceId,
 };
 
 #[derive(Eq, PartialEq, Hash)]
@@ -278,7 +282,9 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
             if let Some(message) = followed_item.to_state_proto(cx) {
                 workspace.update_followers(
                     proto::update_followers::Variant::CreateView(proto::View {
-                        id: followed_item.id() as u64,
+                        id: followed_item
+                            .remote_id(&workspace.client, cx)
+                            .map(|id| id.to_proto()),
                         variant: Some(message),
                         leader_id: workspace.leader_for_pane(&pane).map(|id| id.0),
                     }),
@@ -332,7 +338,9 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                                     this.update_followers(
                                         proto::update_followers::Variant::UpdateView(
                                             proto::UpdateView {
-                                                id: item.id() as u64,
+                                                id: item
+                                                    .remote_id(&this.client, cx)
+                                                    .map(|id| id.to_proto()),
                                                 variant: pending_update.borrow_mut().take(),
                                                 leader_id: leader_id.map(|id| id.0),
                                             },
@@ -584,10 +592,12 @@ pub trait ProjectItem: Item {
 }
 
 pub trait FollowableItem: Item {
+    fn remote_id(&self) -> Option<ViewId>;
     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
     fn from_state_proto(
         pane: ViewHandle<Pane>,
         project: ModelHandle<Project>,
+        id: ViewId,
         state: &mut Option<proto::view::Variant>,
         cx: &mut MutableAppContext,
     ) -> Option<Task<Result<ViewHandle<Self>>>>;
@@ -609,6 +619,7 @@ pub trait FollowableItem: Item {
 }
 
 pub trait FollowableItemHandle: ItemHandle {
+    fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
     fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext);
     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
     fn add_event_to_update_proto(
@@ -627,6 +638,15 @@ pub trait FollowableItemHandle: ItemHandle {
 }
 
 impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
+    fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId> {
+        self.read(cx).remote_id().or_else(|| {
+            client.peer_id().map(|creator| ViewId {
+                creator,
+                id: self.id() as u64,
+            })
+        })
+    }
+
     fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext) {
         self.update(cx, |this, cx| {
             this.set_leader_replica_id(leader_replica_id, cx)

crates/workspace/src/workspace.rs 🔗

@@ -318,6 +318,7 @@ pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
 type FollowableItemBuilder = fn(
     ViewHandle<Pane>,
     ModelHandle<Project>,
+    ViewId,
     &mut Option<proto::view::Variant>,
     &mut MutableAppContext,
 ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
@@ -333,8 +334,8 @@ pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
         builders.insert(
             TypeId::of::<I>(),
             (
-                |pane, project, state, cx| {
-                    I::from_state_proto(pane, project, state, cx).map(|task| {
+                |pane, project, id, state, cx| {
+                    I::from_state_proto(pane, project, id, state, cx).map(|task| {
                         cx.foreground()
                             .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
                     })
@@ -496,6 +497,12 @@ pub struct Workspace {
     _observe_current_user: Task<()>,
 }
 
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub struct ViewId {
+    pub creator: PeerId,
+    pub id: u64,
+}
+
 #[derive(Default)]
 struct LeaderState {
     followers: HashSet<PeerId>,
@@ -505,8 +512,8 @@ type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, Follower
 
 #[derive(Default)]
 struct FollowerState {
-    active_view_id: Option<u64>,
-    items_by_leader_view_id: HashMap<u64, Box<dyn FollowableItemHandle>>,
+    active_view_id: Option<ViewId>,
+    items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
 }
 
 impl Workspace {
@@ -1454,7 +1461,11 @@ impl Workspace {
 
         self.update_followers(
             proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
-                id: self.active_item(cx).map(|item| item.id() as u64),
+                id: self.active_item(cx).and_then(|item| {
+                    item.to_followable_item_handle(cx)?
+                        .remote_id(&self.client, cx)
+                        .map(|id| id.to_proto())
+                }),
                 leader_id: self.leader_for_pane(&pane).map(|id| id.0),
             }),
             cx,
@@ -1643,7 +1654,7 @@ impl Workspace {
                         .get_mut(&leader_id)
                         .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
                         .ok_or_else(|| anyhow!("following interrupted"))?;
-                    state.active_view_id = response.active_view_id;
+                    state.active_view_id = response.active_view_id.map(ViewId::from_proto);
                     Ok::<_, anyhow::Error>(())
                 })?;
                 Self::add_views_from_leader(
@@ -1891,14 +1902,18 @@ impl Workspace {
         mut cx: AsyncAppContext,
     ) -> Result<proto::FollowResponse> {
         this.update(&mut cx, |this, cx| {
+            let client = &this.client;
             this.leader_state
                 .followers
                 .insert(envelope.original_sender_id()?);
 
-            let active_view_id = this
-                .active_item(cx)
-                .and_then(|i| i.to_followable_item_handle(cx))
-                .map(|i| i.id() as u64);
+            let active_view_id = this.active_item(cx).and_then(|i| {
+                Some(
+                    i.to_followable_item_handle(cx)?
+                        .remote_id(client, cx)?
+                        .to_proto(),
+                )
+            });
             Ok(proto::FollowResponse {
                 active_view_id,
                 views: this
@@ -1909,11 +1924,11 @@ impl Workspace {
                         pane.read(cx).items().filter_map({
                             let cx = &cx;
                             move |item| {
-                                let id = item.id() as u64;
                                 let item = item.to_followable_item_handle(cx)?;
+                                let id = item.remote_id(client, cx)?.to_proto();
                                 let variant = item.to_state_proto(cx)?;
                                 Some(proto::View {
-                                    id,
+                                    id: Some(id),
                                     leader_id,
                                     variant: Some(variant),
                                 })
@@ -1964,7 +1979,8 @@ impl Workspace {
                 this.update(cx, |this, _| {
                     if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
                         for state in state.values_mut() {
-                            state.active_view_id = update_active_view.id;
+                            state.active_view_id =
+                                update_active_view.id.clone().map(ViewId::from_proto);
                         }
                     }
                 });
@@ -1973,12 +1989,18 @@ impl Workspace {
                 let variant = update_view
                     .variant
                     .ok_or_else(|| anyhow!("missing update view variant"))?;
+                let id = update_view
+                    .id
+                    .ok_or_else(|| anyhow!("missing update view id"))?;
                 let mut tasks = Vec::new();
                 this.update(cx, |this, cx| {
                     let project = this.project.clone();
                     if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
                         for state in state.values_mut() {
-                            if let Some(item) = state.items_by_leader_view_id.get(&update_view.id) {
+                            if let Some(item) = state
+                                .items_by_leader_view_id
+                                .get(&ViewId::from_proto(id.clone()))
+                            {
                                 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
                             }
                         }
@@ -2031,16 +2053,19 @@ impl Workspace {
             let mut item_tasks = Vec::new();
             let mut leader_view_ids = Vec::new();
             for view in &views {
+                let Some(id) = &view.id else { continue };
+                let id = ViewId::from_proto(id.clone());
                 let mut variant = view.variant.clone();
                 if variant.is_none() {
                     Err(anyhow!("missing variant"))?;
                 }
                 for build_item in &item_builders {
-                    let task =
-                        cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx));
+                    let task = cx.update(|cx| {
+                        build_item(pane.clone(), project.clone(), id, &mut variant, cx)
+                    });
                     if let Some(task) = task {
                         item_tasks.push(task);
-                        leader_view_ids.push(view.id);
+                        leader_view_ids.push(id);
                         break;
                     } else {
                         assert!(variant.is_some());
@@ -2561,6 +2586,22 @@ impl View for Workspace {
     }
 }
 
+impl ViewId {
+    pub(crate) fn from_proto(message: proto::ViewId) -> Self {
+        Self {
+            creator: PeerId(message.creator),
+            id: message.id,
+        }
+    }
+
+    pub(crate) fn to_proto(&self) -> proto::ViewId {
+        proto::ViewId {
+            creator: self.creator.0,
+            id: self.id,
+        }
+    }
+}
+
 pub trait WorkspaceHandle {
     fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
 }