Merge pull request #661 from zed-industries/follow

Antonio Scandurra created

Introduce basic following experience

Change summary

Cargo.lock                                    |   1 
crates/client/src/channel.rs                  |   2 
crates/client/src/client.rs                   | 238 +++-
crates/diagnostics/src/diagnostics.rs         |   4 
crates/editor/Cargo.toml                      |   1 
crates/editor/src/editor.rs                   | 178 +++
crates/editor/src/element.rs                  |  59 
crates/editor/src/items.rs                    | 250 +++++
crates/editor/src/multi_buffer.rs             |   8 
crates/file_finder/src/file_finder.rs         |   2 
crates/go_to_line/src/go_to_line.rs           |   2 
crates/gpui/src/app.rs                        | 639 ++++++++++++-
crates/gpui/src/presenter.rs                  |   4 
crates/language/src/buffer.rs                 |  25 
crates/language/src/proto.rs                  |  39 
crates/language/src/tests.rs                  |  12 
crates/outline/src/outline.rs                 |   2 
crates/project/src/project.rs                 | 126 ++
crates/project_symbols/src/project_symbols.rs |   2 
crates/rpc/proto/zed.proto                    | 103 +
crates/rpc/src/proto.rs                       |  19 
crates/rpc/src/rpc.rs                         |   2 
crates/search/src/buffer_search.rs            |   6 
crates/search/src/project_search.rs           |   6 
crates/server/src/rpc.rs                      | 649 ++++++++++++++
crates/text/src/selection.rs                  |   6 
crates/theme/src/theme.rs                     |   2 
crates/theme_selector/src/theme_selector.rs   |   2 
crates/workspace/src/pane.rs                  | 146 ++-
crates/workspace/src/pane_group.rs            |  78 +
crates/workspace/src/workspace.rs             | 929 ++++++++++++++++++--
crates/zed/Cargo.toml                         |   1 
crates/zed/assets/themes/_base.toml           |   2 
crates/zed/src/main.rs                        |   8 
crates/zed/src/zed.rs                         |   2 
35 files changed, 3,049 insertions(+), 506 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1629,6 +1629,7 @@ dependencies = [
  "postage",
  "project",
  "rand 0.8.3",
+ "rpc",
  "serde",
  "smallvec",
  "smol",

crates/client/src/channel.rs 🔗

@@ -181,7 +181,7 @@ impl Entity for Channel {
 
 impl Channel {
     pub fn init(rpc: &Arc<Client>) {
-        rpc.add_entity_message_handler(Self::handle_message_sent);
+        rpc.add_model_message_handler(Self::handle_message_sent);
     }
 
     pub fn new(

crates/client/src/client.rs 🔗

@@ -13,8 +13,8 @@ use async_tungstenite::tungstenite::{
 };
 use futures::{future::LocalBoxFuture, FutureExt, StreamExt};
 use gpui::{
-    action, AnyModelHandle, AnyWeakModelHandle, AsyncAppContext, Entity, ModelContext, ModelHandle,
-    MutableAppContext, Task,
+    action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
+    Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
 };
 use http::HttpClient;
 use lazy_static::lazy_static;
@@ -136,26 +136,37 @@ impl Status {
 struct ClientState {
     credentials: Option<Credentials>,
     status: (watch::Sender<Status>, watch::Receiver<Status>),
-    entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
+    entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
     _reconnect_task: Option<Task<()>>,
     reconnect_interval: Duration,
-    models_by_entity_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakModelHandle>,
+    entities_by_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakEntityHandle>,
     models_by_message_type: HashMap<TypeId, AnyWeakModelHandle>,
-    model_types_by_message_type: HashMap<TypeId, TypeId>,
+    entity_types_by_message_type: HashMap<TypeId, TypeId>,
     message_handlers: HashMap<
         TypeId,
         Arc<
             dyn Send
                 + Sync
                 + Fn(
-                    AnyModelHandle,
+                    AnyEntityHandle,
                     Box<dyn AnyTypedEnvelope>,
+                    &Arc<Client>,
                     AsyncAppContext,
                 ) -> LocalBoxFuture<'static, Result<()>>,
         >,
     >,
 }
 
+enum AnyWeakEntityHandle {
+    Model(AnyWeakModelHandle),
+    View(AnyWeakViewHandle),
+}
+
+enum AnyEntityHandle {
+    Model(AnyModelHandle),
+    View(AnyViewHandle),
+}
+
 #[derive(Clone, Debug)]
 pub struct Credentials {
     pub user_id: u64,
@@ -171,8 +182,8 @@ impl Default for ClientState {
             _reconnect_task: None,
             reconnect_interval: Duration::from_secs(5),
             models_by_message_type: Default::default(),
-            models_by_entity_type_and_remote_id: Default::default(),
-            model_types_by_message_type: Default::default(),
+            entities_by_type_and_remote_id: Default::default(),
+            entity_types_by_message_type: Default::default(),
             message_handlers: Default::default(),
         }
     }
@@ -195,13 +206,13 @@ impl Drop for Subscription {
             Subscription::Entity { client, id } => {
                 if let Some(client) = client.upgrade() {
                     let mut state = client.state.write();
-                    let _ = state.models_by_entity_type_and_remote_id.remove(id);
+                    let _ = state.entities_by_type_and_remote_id.remove(id);
                 }
             }
             Subscription::Message { client, id } => {
                 if let Some(client) = client.upgrade() {
                     let mut state = client.state.write();
-                    let _ = state.model_types_by_message_type.remove(id);
+                    let _ = state.entity_types_by_message_type.remove(id);
                     let _ = state.message_handlers.remove(id);
                 }
             }
@@ -239,7 +250,7 @@ impl Client {
         state._reconnect_task.take();
         state.message_handlers.clear();
         state.models_by_message_type.clear();
-        state.models_by_entity_type_and_remote_id.clear();
+        state.entities_by_type_and_remote_id.clear();
         state.entity_id_extractors.clear();
         self.peer.reset();
     }
@@ -313,17 +324,32 @@ impl Client {
         }
     }
 
+    pub fn add_view_for_remote_entity<T: View>(
+        self: &Arc<Self>,
+        remote_id: u64,
+        cx: &mut ViewContext<T>,
+    ) -> Subscription {
+        let id = (TypeId::of::<T>(), remote_id);
+        self.state
+            .write()
+            .entities_by_type_and_remote_id
+            .insert(id, AnyWeakEntityHandle::View(cx.weak_handle().into()));
+        Subscription::Entity {
+            client: Arc::downgrade(self),
+            id,
+        }
+    }
+
     pub fn add_model_for_remote_entity<T: Entity>(
         self: &Arc<Self>,
         remote_id: u64,
         cx: &mut ModelContext<T>,
     ) -> Subscription {
-        let handle = AnyModelHandle::from(cx.handle());
-        let mut state = self.state.write();
         let id = (TypeId::of::<T>(), remote_id);
-        state
-            .models_by_entity_type_and_remote_id
-            .insert(id, handle.downgrade());
+        self.state
+            .write()
+            .entities_by_type_and_remote_id
+            .insert(id, AnyWeakEntityHandle::Model(cx.weak_handle().into()));
         Subscription::Entity {
             client: Arc::downgrade(self),
             id,
@@ -346,7 +372,6 @@ impl Client {
     {
         let message_type_id = TypeId::of::<M>();
 
-        let client = Arc::downgrade(self);
         let mut state = self.state.write();
         state
             .models_by_message_type
@@ -354,14 +379,15 @@ impl Client {
 
         let prev_handler = state.message_handlers.insert(
             message_type_id,
-            Arc::new(move |handle, envelope, cx| {
+            Arc::new(move |handle, envelope, client, cx| {
+                let handle = if let AnyEntityHandle::Model(handle) = handle {
+                    handle
+                } else {
+                    unreachable!();
+                };
                 let model = handle.downcast::<E>().unwrap();
                 let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
-                if let Some(client) = client.upgrade() {
-                    handler(model, *envelope, client.clone(), cx).boxed_local()
-                } else {
-                    async move { Ok(()) }.boxed_local()
-                }
+                handler(model, *envelope, client.clone(), cx).boxed_local()
             }),
         );
         if prev_handler.is_some() {
@@ -374,7 +400,26 @@ impl Client {
         }
     }
 
-    pub fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    pub fn add_view_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    where
+        M: EntityMessage,
+        E: View,
+        H: 'static
+            + Send
+            + Sync
+            + Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+        F: 'static + Future<Output = Result<()>>,
+    {
+        self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
+            if let AnyEntityHandle::View(handle) = handle {
+                handler(handle.downcast::<E>().unwrap(), message, client, cx)
+            } else {
+                unreachable!();
+            }
+        })
+    }
+
+    pub fn add_model_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
     where
         M: EntityMessage,
         E: Entity,
@@ -383,38 +428,51 @@ impl Client {
             + Sync
             + Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<()>>,
+    {
+        self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
+            if let AnyEntityHandle::Model(handle) = handle {
+                handler(handle.downcast::<E>().unwrap(), message, client, cx)
+            } else {
+                unreachable!();
+            }
+        })
+    }
+
+    fn add_entity_message_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    where
+        M: EntityMessage,
+        E: Entity,
+        H: 'static
+            + Send
+            + Sync
+            + Fn(AnyEntityHandle, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+        F: 'static + Future<Output = Result<()>>,
     {
         let model_type_id = TypeId::of::<E>();
         let message_type_id = TypeId::of::<M>();
 
-        let client = Arc::downgrade(self);
         let mut state = self.state.write();
         state
-            .model_types_by_message_type
+            .entity_types_by_message_type
             .insert(message_type_id, model_type_id);
         state
             .entity_id_extractors
             .entry(message_type_id)
             .or_insert_with(|| {
-                Box::new(|envelope| {
-                    let envelope = envelope
+                |envelope| {
+                    envelope
                         .as_any()
                         .downcast_ref::<TypedEnvelope<M>>()
-                        .unwrap();
-                    envelope.payload.remote_entity_id()
-                })
+                        .unwrap()
+                        .payload
+                        .remote_entity_id()
+                }
             });
-
         let prev_handler = state.message_handlers.insert(
             message_type_id,
-            Arc::new(move |handle, envelope, cx| {
-                let model = handle.downcast::<E>().unwrap();
+            Arc::new(move |handle, envelope, client, cx| {
                 let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
-                if let Some(client) = client.upgrade() {
-                    handler(model, *envelope, client.clone(), cx).boxed_local()
-                } else {
-                    async move { Ok(()) }.boxed_local()
-                }
+                handler(handle, *envelope, client.clone(), cx).boxed_local()
             }),
         );
         if prev_handler.is_some() {
@@ -422,7 +480,7 @@ impl Client {
         }
     }
 
-    pub fn add_entity_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    pub fn add_model_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
     where
         M: EntityMessage + RequestMessage,
         E: Entity,
@@ -432,29 +490,56 @@ impl Client {
             + Fn(ModelHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
         F: 'static + Future<Output = Result<M::Response>>,
     {
-        self.add_entity_message_handler(move |model, envelope, client, cx| {
-            let receipt = envelope.receipt();
-            let response = handler(model, envelope, client.clone(), cx);
-            async move {
-                match response.await {
-                    Ok(response) => {
-                        client.respond(receipt, response)?;
-                        Ok(())
-                    }
-                    Err(error) => {
-                        client.respond_with_error(
-                            receipt,
-                            proto::Error {
-                                message: error.to_string(),
-                            },
-                        )?;
-                        Err(error)
-                    }
-                }
-            }
+        self.add_model_message_handler(move |entity, envelope, client, cx| {
+            Self::respond_to_request::<M, _>(
+                envelope.receipt(),
+                handler(entity, envelope, client.clone(), cx),
+                client,
+            )
+        })
+    }
+
+    pub fn add_view_request_handler<M, E, H, F>(self: &Arc<Self>, handler: H)
+    where
+        M: EntityMessage + RequestMessage,
+        E: View,
+        H: 'static
+            + Send
+            + Sync
+            + Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
+        F: 'static + Future<Output = Result<M::Response>>,
+    {
+        self.add_view_message_handler(move |entity, envelope, client, cx| {
+            Self::respond_to_request::<M, _>(
+                envelope.receipt(),
+                handler(entity, envelope, client.clone(), cx),
+                client,
+            )
         })
     }
 
+    async fn respond_to_request<T: RequestMessage, F: Future<Output = Result<T::Response>>>(
+        receipt: Receipt<T>,
+        response: F,
+        client: Arc<Self>,
+    ) -> Result<()> {
+        match response.await {
+            Ok(response) => {
+                client.respond(receipt, response)?;
+                Ok(())
+            }
+            Err(error) => {
+                client.respond_with_error(
+                    receipt,
+                    proto::Error {
+                        message: error.to_string(),
+                    },
+                )?;
+                Err(error)
+            }
+        }
+    }
+
     pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
         read_credentials_from_keychain(cx).is_some()
     }
@@ -561,24 +646,26 @@ impl Client {
                             .models_by_message_type
                             .get(&payload_type_id)
                             .and_then(|model| model.upgrade(&cx))
+                            .map(AnyEntityHandle::Model)
                             .or_else(|| {
-                                let model_type_id =
-                                    *state.model_types_by_message_type.get(&payload_type_id)?;
+                                let entity_type_id =
+                                    *state.entity_types_by_message_type.get(&payload_type_id)?;
                                 let entity_id = state
                                     .entity_id_extractors
                                     .get(&message.payload_type_id())
                                     .map(|extract_entity_id| {
                                         (extract_entity_id)(message.as_ref())
                                     })?;
-                                let model = state
-                                    .models_by_entity_type_and_remote_id
-                                    .get(&(model_type_id, entity_id))?;
-                                if let Some(model) = model.upgrade(&cx) {
-                                    Some(model)
+
+                                let entity = state
+                                    .entities_by_type_and_remote_id
+                                    .get(&(entity_type_id, entity_id))?;
+                                if let Some(entity) = entity.upgrade(&cx) {
+                                    Some(entity)
                                 } else {
                                     state
-                                        .models_by_entity_type_and_remote_id
-                                        .remove(&(model_type_id, entity_id));
+                                        .entities_by_type_and_remote_id
+                                        .remove(&(entity_type_id, entity_id));
                                     None
                                 }
                             });
@@ -593,7 +680,7 @@ impl Client {
                         if let Some(handler) = state.message_handlers.get(&payload_type_id).cloned()
                         {
                             drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
-                            let future = handler(model, message, cx.clone());
+                            let future = handler(model, message, &this, cx.clone());
 
                             let client_id = this.id;
                             log::debug!(
@@ -891,6 +978,15 @@ impl Client {
     }
 }
 
+impl AnyWeakEntityHandle {
+    fn upgrade(&self, cx: &AsyncAppContext) -> Option<AnyEntityHandle> {
+        match self {
+            AnyWeakEntityHandle::Model(handle) => handle.upgrade(cx).map(AnyEntityHandle::Model),
+            AnyWeakEntityHandle::View(handle) => handle.upgrade(cx).map(AnyEntityHandle::View),
+        }
+    }
+}
+
 fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
     if IMPERSONATE_LOGIN.is_some() {
         return None;
@@ -994,7 +1090,7 @@ mod tests {
 
         let (done_tx1, mut done_rx1) = smol::channel::unbounded();
         let (done_tx2, mut done_rx2) = smol::channel::unbounded();
-        client.add_entity_message_handler(
+        client.add_model_message_handler(
             move |model: ModelHandle<Model>, _: TypedEnvelope<proto::UnshareProject>, _, cx| {
                 match model.read_with(&cx, |model, _| model.id) {
                     1 => done_tx1.try_send(()).unwrap(),

crates/diagnostics/src/diagnostics.rs 🔗

@@ -450,6 +450,10 @@ impl workspace::Item 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/Cargo.toml 🔗

@@ -27,6 +27,7 @@ gpui = { path = "../gpui" }
 language = { path = "../language" }
 lsp = { path = "../lsp" }
 project = { path = "../project" }
+rpc = { path = "../rpc" }
 snippet = { path = "../snippet" }
 sum_tree = { path = "../sum_tree" }
 theme = { path = "../theme" }

crates/editor/src/editor.rs 🔗

@@ -340,9 +340,8 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_async_action(Editor::confirm_rename);
     cx.add_async_action(Editor::find_all_references);
 
-    workspace::register_project_item(cx, |project, buffer, cx| {
-        Editor::for_buffer(buffer, Some(project), cx)
-    });
+    workspace::register_project_item::<Editor>(cx);
+    workspace::register_followable_item::<Editor>(cx);
 }
 
 trait InvalidationRegion {
@@ -431,8 +430,8 @@ pub struct Editor {
     select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
     active_diagnostics: Option<ActiveDiagnosticGroup>,
     scroll_position: Vector2F,
-    scroll_top_anchor: Option<Anchor>,
-    autoscroll_request: Option<Autoscroll>,
+    scroll_top_anchor: Anchor,
+    autoscroll_request: Option<(Autoscroll, bool)>,
     soft_wrap_mode_override: Option<settings::SoftWrap>,
     get_field_editor_theme: Option<GetFieldEditorTheme>,
     override_text_style: Option<Box<OverrideTextStyle>>,
@@ -457,6 +456,7 @@ pub struct Editor {
     pending_rename: Option<RenameState>,
     searchable: bool,
     cursor_shape: CursorShape,
+    leader_replica_id: Option<u16>,
 }
 
 pub struct EditorSnapshot {
@@ -465,7 +465,7 @@ pub struct EditorSnapshot {
     pub placeholder_text: Option<Arc<str>>,
     is_focused: bool,
     scroll_position: Vector2F,
-    scroll_top_anchor: Option<Anchor>,
+    scroll_top_anchor: Anchor,
 }
 
 #[derive(Clone)]
@@ -909,7 +909,7 @@ impl Editor {
             get_field_editor_theme,
             project,
             scroll_position: Vector2F::zero(),
-            scroll_top_anchor: None,
+            scroll_top_anchor: Anchor::min(),
             autoscroll_request: None,
             focused: false,
             show_local_cursors: false,
@@ -932,6 +932,7 @@ impl Editor {
             searchable: true,
             override_text_style: None,
             cursor_shape: Default::default(),
+            leader_replica_id: None,
         };
         this.end_selection(cx);
 
@@ -1014,10 +1015,19 @@ impl Editor {
     }
 
     pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
+        self.set_scroll_position_internal(scroll_position, true, cx);
+    }
+
+    fn set_scroll_position_internal(
+        &mut self,
+        scroll_position: Vector2F,
+        local: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
         let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
 
         if scroll_position.y() == 0. {
-            self.scroll_top_anchor = None;
+            self.scroll_top_anchor = Anchor::min();
             self.scroll_position = scroll_position;
         } else {
             let scroll_top_buffer_offset =
@@ -1029,9 +1039,22 @@ impl Editor {
                 scroll_position.x(),
                 scroll_position.y() - anchor.to_display_point(&map).row() as f32,
             );
-            self.scroll_top_anchor = Some(anchor);
+            self.scroll_top_anchor = anchor;
         }
 
+        cx.emit(Event::ScrollPositionChanged { local });
+        cx.notify();
+    }
+
+    fn set_scroll_top_anchor(
+        &mut self,
+        anchor: Anchor,
+        position: Vector2F,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.scroll_top_anchor = anchor;
+        self.scroll_position = position;
+        cx.emit(Event::ScrollPositionChanged { local: false });
         cx.notify();
     }
 
@@ -1074,7 +1097,7 @@ impl Editor {
             self.set_scroll_position(scroll_position, cx);
         }
 
-        let autoscroll = if let Some(autoscroll) = self.autoscroll_request.take() {
+        let (autoscroll, local) = if let Some(autoscroll) = self.autoscroll_request.take() {
             autoscroll
         } else {
             return false;
@@ -1126,15 +1149,15 @@ impl Editor {
 
                 if target_top < start_row {
                     scroll_position.set_y(target_top);
-                    self.set_scroll_position(scroll_position, cx);
+                    self.set_scroll_position_internal(scroll_position, local, cx);
                 } else if target_bottom >= end_row {
                     scroll_position.set_y(target_bottom - visible_lines);
-                    self.set_scroll_position(scroll_position, cx);
+                    self.set_scroll_position_internal(scroll_position, local, cx);
                 }
             }
             Autoscroll::Center => {
                 scroll_position.set_y((first_cursor_top - margin).max(0.0));
-                self.set_scroll_position(scroll_position, cx);
+                self.set_scroll_position_internal(scroll_position, local, cx);
             }
         }
 
@@ -1316,7 +1339,7 @@ impl Editor {
             _ => {}
         }
 
-        self.set_selections(self.selections.clone(), Some(pending), cx);
+        self.set_selections(self.selections.clone(), Some(pending), true, cx);
     }
 
     fn begin_selection(
@@ -1396,7 +1419,12 @@ impl Editor {
         } else {
             selections = Arc::from([]);
         }
-        self.set_selections(selections, Some(PendingSelection { selection, mode }), cx);
+        self.set_selections(
+            selections,
+            Some(PendingSelection { selection, mode }),
+            true,
+            cx,
+        );
 
         cx.notify();
     }
@@ -1510,7 +1538,7 @@ impl Editor {
                 pending.selection.end = buffer.anchor_before(head);
                 pending.selection.reversed = false;
             }
-            self.set_selections(self.selections.clone(), Some(pending), cx);
+            self.set_selections(self.selections.clone(), Some(pending), true, cx);
         } else {
             log::error!("update_selection dispatched with no pending selection");
             return;
@@ -1597,7 +1625,7 @@ impl Editor {
             if selections.is_empty() {
                 selections = Arc::from([pending.selection]);
             }
-            self.set_selections(selections, None, cx);
+            self.set_selections(selections, None, true, cx);
             self.request_autoscroll(Autoscroll::Fit, cx);
         } else {
             let mut oldest_selection = self.oldest_selection::<usize>(&cx);
@@ -1617,7 +1645,7 @@ impl Editor {
     #[cfg(any(test, feature = "test-support"))]
     pub fn selected_ranges<D: TextDimension + Ord + Sub<D, Output = D>>(
         &self,
-        cx: &mut MutableAppContext,
+        cx: &AppContext,
     ) -> Vec<Range<D>> {
         self.local_selections::<D>(cx)
             .iter()
@@ -1944,7 +1972,7 @@ impl Editor {
                 }
                 drop(snapshot);
 
-                self.set_selections(selections.into(), None, cx);
+                self.set_selections(selections.into(), None, true, cx);
                 true
             }
         } else {
@@ -3334,7 +3362,7 @@ impl Editor {
     pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
         if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
             if let Some((selections, _)) = self.selection_history.get(&tx_id).cloned() {
-                self.set_selections(selections, None, cx);
+                self.set_selections(selections, None, true, cx);
             }
             self.request_autoscroll(Autoscroll::Fit, cx);
         }
@@ -3343,7 +3371,7 @@ impl Editor {
     pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext<Self>) {
         if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) {
             if let Some((_, Some(selections))) = self.selection_history.get(&tx_id).cloned() {
-                self.set_selections(selections, None, cx);
+                self.set_selections(selections, None, true, cx);
             }
             self.request_autoscroll(Autoscroll::Fit, cx);
         }
@@ -4870,6 +4898,7 @@ impl Editor {
                 }
             })),
             None,
+            true,
             cx,
         );
     }
@@ -4930,6 +4959,7 @@ impl Editor {
         &mut self,
         selections: Arc<[Selection<Anchor>]>,
         pending_selection: Option<PendingSelection>,
+        local: bool,
         cx: &mut ViewContext<Self>,
     ) {
         assert!(
@@ -4941,7 +4971,7 @@ impl Editor {
 
         self.selections = selections;
         self.pending_selection = pending_selection;
-        if self.focused {
+        if self.focused && self.leader_replica_id.is_none() {
             self.buffer.update(cx, |buffer, cx| {
                 buffer.set_active_selections(&self.selections, cx)
             });
@@ -4998,11 +5028,16 @@ impl Editor {
         self.refresh_document_highlights(cx);
 
         self.pause_cursor_blinking(cx);
-        cx.emit(Event::SelectionsChanged);
+        cx.emit(Event::SelectionsChanged { local });
     }
 
     pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
-        self.autoscroll_request = Some(autoscroll);
+        self.autoscroll_request = Some((autoscroll, true));
+        cx.notify();
+    }
+
+    fn request_autoscroll_remotely(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
+        self.autoscroll_request = Some((autoscroll, false));
         cx.notify();
     }
 
@@ -5407,7 +5442,7 @@ impl Editor {
     }
 
     pub fn show_local_cursors(&self) -> bool {
-        self.show_local_cursors
+        self.show_local_cursors && self.focused
     }
 
     fn on_buffer_changed(&mut self, _: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Self>) {
@@ -5421,10 +5456,10 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            language::Event::Edited => {
+            language::Event::Edited { local } => {
                 self.refresh_active_diagnostics(cx);
                 self.refresh_code_actions(cx);
-                cx.emit(Event::Edited);
+                cx.emit(Event::Edited { local: *local });
             }
             language::Event::Dirtied => cx.emit(Event::Dirtied),
             language::Event::Saved => cx.emit(Event::Saved),
@@ -5537,10 +5572,10 @@ impl Deref for EditorSnapshot {
 fn compute_scroll_position(
     snapshot: &DisplaySnapshot,
     mut scroll_position: Vector2F,
-    scroll_top_anchor: &Option<Anchor>,
+    scroll_top_anchor: &Anchor,
 ) -> Vector2F {
-    if let Some(anchor) = scroll_top_anchor {
-        let scroll_top = anchor.to_display_point(snapshot).row() as f32;
+    if *scroll_top_anchor != Anchor::min() {
+        let scroll_top = scroll_top_anchor.to_display_point(snapshot).row() as f32;
         scroll_position.set_y(scroll_top + scroll_position.y());
     } else {
         scroll_position.set_y(0.);
@@ -5551,12 +5586,13 @@ fn compute_scroll_position(
 #[derive(Copy, Clone)]
 pub enum Event {
     Activate,
-    Edited,
+    Edited { local: bool },
     Blurred,
     Dirtied,
     Saved,
     TitleChanged,
-    SelectionsChanged,
+    SelectionsChanged { local: bool },
+    ScrollPositionChanged { local: bool },
     Closed,
 }
 
@@ -5595,7 +5631,9 @@ impl View for Editor {
             self.blink_cursors(self.blink_epoch, cx);
             self.buffer.update(cx, |buffer, cx| {
                 buffer.finalize_last_transaction(cx);
-                buffer.set_active_selections(&self.selections, cx)
+                if self.leader_replica_id.is_none() {
+                    buffer.set_active_selections(&self.selections, cx);
+                }
             });
         }
     }
@@ -6013,6 +6051,10 @@ mod tests {
     use crate::test::marked_text_by;
 
     use super::*;
+    use gpui::{
+        geometry::rect::RectF,
+        platform::{WindowBounds, WindowOptions},
+    };
     use language::{LanguageConfig, LanguageServerConfig};
     use lsp::FakeLanguageServer;
     use project::FakeFs;
@@ -6021,6 +6063,7 @@ mod tests {
     use text::Point;
     use unindent::Unindent;
     use util::test::sample_text;
+    use workspace::FollowableItem;
 
     #[gpui::test]
     fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) {
@@ -8921,6 +8964,75 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    fn test_following(cx: &mut gpui::MutableAppContext) {
+        let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
+        populate_settings(cx);
+
+        let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
+        let (_, follower) = cx.add_window(
+            WindowOptions {
+                bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
+                ..Default::default()
+            },
+            |cx| build_editor(buffer.clone(), cx),
+        );
+
+        let pending_update = Rc::new(RefCell::new(None));
+        follower.update(cx, {
+            let update = pending_update.clone();
+            |_, cx| {
+                cx.subscribe(&leader, move |_, leader, event, cx| {
+                    leader
+                        .read(cx)
+                        .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
+                })
+                .detach();
+            }
+        });
+
+        // Update the selections only
+        leader.update(cx, |leader, cx| {
+            leader.select_ranges([1..1], None, cx);
+        });
+        follower.update(cx, |follower, cx| {
+            follower
+                .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
+                .unwrap();
+        });
+        assert_eq!(follower.read(cx).selected_ranges(cx), vec![1..1]);
+
+        // Update the scroll position only
+        leader.update(cx, |leader, cx| {
+            leader.set_scroll_position(vec2f(1.5, 3.5), cx);
+        });
+        follower.update(cx, |follower, cx| {
+            follower
+                .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
+                .unwrap();
+        });
+        assert_eq!(
+            follower.update(cx, |follower, cx| follower.scroll_position(cx)),
+            vec2f(1.5, 3.5)
+        );
+
+        // Update the selections and scroll position
+        leader.update(cx, |leader, cx| {
+            leader.select_ranges([0..0], None, cx);
+            leader.request_autoscroll(Autoscroll::Newest, cx);
+            leader.set_scroll_position(vec2f(1.5, 3.5), cx);
+        });
+        follower.update(cx, |follower, cx| {
+            let initial_scroll_position = follower.scroll_position(cx);
+            follower
+                .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
+                .unwrap();
+            assert_eq!(follower.scroll_position(cx), initial_scroll_position);
+            assert!(follower.autoscroll_request.is_some());
+        });
+        assert_eq!(follower.read(cx).selected_ranges(cx), vec![0..0]);
+    }
+
     #[test]
     fn test_combine_syntax_and_fuzzy_match_highlights() {
         let string = "abcdefghijklmnop";

crates/editor/src/element.rs 🔗

@@ -909,7 +909,7 @@ impl Element for EditorElement {
                 .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
         };
 
-        let mut selections = HashMap::default();
+        let mut selections = Vec::new();
         let mut active_rows = BTreeMap::new();
         let mut highlighted_rows = None;
         let mut highlighted_ranges = Vec::new();
@@ -922,11 +922,32 @@ impl Element for EditorElement {
                 &display_map,
             );
 
+            let mut remote_selections = HashMap::default();
+            for (replica_id, selection) in display_map
+                .buffer_snapshot
+                .remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone()))
+            {
+                // The local selections match the leader's selections.
+                if Some(replica_id) == view.leader_replica_id {
+                    continue;
+                }
+
+                remote_selections
+                    .entry(replica_id)
+                    .or_insert(Vec::new())
+                    .push(crate::Selection {
+                        id: selection.id,
+                        goal: selection.goal,
+                        reversed: selection.reversed,
+                        start: selection.start.to_display_point(&display_map),
+                        end: selection.end.to_display_point(&display_map),
+                    });
+            }
+            selections.extend(remote_selections);
+
             if view.show_local_selections {
-                let local_selections = view.local_selections_in_range(
-                    start_anchor.clone()..end_anchor.clone(),
-                    &display_map,
-                );
+                let local_selections =
+                    view.local_selections_in_range(start_anchor..end_anchor, &display_map);
                 for selection in &local_selections {
                     let is_empty = selection.start == selection.end;
                     let selection_start = snapshot.prev_line_boundary(selection.start).1;
@@ -939,8 +960,12 @@ impl Element for EditorElement {
                         *contains_non_empty_selection |= !is_empty;
                     }
                 }
-                selections.insert(
-                    view.replica_id(cx),
+
+                // Render the local selections in the leader's color when following.
+                let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx));
+
+                selections.push((
+                    local_replica_id,
                     local_selections
                         .into_iter()
                         .map(|selection| crate::Selection {
@@ -951,23 +976,7 @@ impl Element for EditorElement {
                             end: selection.end.to_display_point(&display_map),
                         })
                         .collect(),
-                );
-            }
-
-            for (replica_id, selection) in display_map
-                .buffer_snapshot
-                .remote_selections_in_range(&(start_anchor..end_anchor))
-            {
-                selections
-                    .entry(replica_id)
-                    .or_insert(Vec::new())
-                    .push(crate::Selection {
-                        id: selection.id,
-                        goal: selection.goal,
-                        reversed: selection.reversed,
-                        start: selection.start.to_display_point(&display_map),
-                        end: selection.end.to_display_point(&display_map),
-                    });
+                ));
             }
         });
 
@@ -1213,7 +1222,7 @@ pub struct LayoutState {
     em_width: f32,
     em_advance: f32,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
-    selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
+    selections: Vec<(ReplicaId, Vec<text::Selection<DisplayPoint>>)>,
     context_menu: Option<(DisplayPoint, ElementBox)>,
     code_actions_indicator: Option<(u32, ElementBox)>,
 }

crates/editor/src/items.rs 🔗

@@ -1,16 +1,244 @@
-use crate::{Autoscroll, Editor, Event, NavigationData, ToOffset, ToPoint as _};
-use anyhow::Result;
+use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToOffset, ToPoint as _};
+use anyhow::{anyhow, Result};
 use gpui::{
-    elements::*, AppContext, Entity, ModelHandle, RenderContext, Subscription, Task, View,
-    ViewContext, ViewHandle,
+    elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
+    RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
 };
-use language::{Bias, Buffer, Diagnostic, File as _};
-use project::{File, Project, ProjectPath};
-use std::fmt::Write;
-use std::path::PathBuf;
+use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal};
+use project::{File, Project, ProjectEntryId, ProjectPath};
+use rpc::proto::{self, update_view};
+use std::{fmt::Write, path::PathBuf};
 use text::{Point, Selection};
 use util::ResultExt;
-use workspace::{Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView};
+use workspace::{
+    FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView,
+};
+
+impl FollowableItem for Editor {
+    fn from_state_proto(
+        pane: ViewHandle<workspace::Pane>,
+        project: ModelHandle<Project>,
+        state: &mut Option<proto::view::Variant>,
+        cx: &mut MutableAppContext,
+    ) -> Option<Task<Result<ViewHandle<Self>>>> {
+        let state = if matches!(state, Some(proto::view::Variant::Editor(_))) {
+            if let Some(proto::view::Variant::Editor(state)) = state.take() {
+                state
+            } else {
+                unreachable!()
+            }
+        } else {
+            return None;
+        };
+
+        let buffer = project.update(cx, |project, cx| {
+            project.open_buffer_by_id(state.buffer_id, cx)
+        });
+        Some(cx.spawn(|mut cx| async move {
+            let buffer = buffer.await?;
+            let editor = pane
+                .read_with(&cx, |pane, cx| {
+                    pane.items_of_type::<Self>().find(|editor| {
+                        editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer)
+                    })
+                })
+                .unwrap_or_else(|| {
+                    cx.add_view(pane.window_id(), |cx| {
+                        Editor::for_buffer(buffer, Some(project), cx)
+                    })
+                });
+            editor.update(&mut cx, |editor, cx| {
+                let excerpt_id;
+                let buffer_id;
+                {
+                    let buffer = editor.buffer.read(cx).read(cx);
+                    let singleton = buffer.as_singleton().unwrap();
+                    excerpt_id = singleton.0.clone();
+                    buffer_id = singleton.1;
+                }
+                let selections = state
+                    .selections
+                    .into_iter()
+                    .map(|selection| {
+                        deserialize_selection(&excerpt_id, buffer_id, selection)
+                            .ok_or_else(|| anyhow!("invalid selection"))
+                    })
+                    .collect::<Result<Vec<_>>>()?;
+                if !selections.is_empty() {
+                    editor.set_selections(selections.into(), None, false, cx);
+                }
+
+                if let Some(anchor) = state.scroll_top_anchor {
+                    editor.set_scroll_top_anchor(
+                        Anchor {
+                            buffer_id: Some(state.buffer_id as usize),
+                            excerpt_id: excerpt_id.clone(),
+                            text_anchor: language::proto::deserialize_anchor(anchor)
+                                .ok_or_else(|| anyhow!("invalid scroll top"))?,
+                        },
+                        vec2f(state.scroll_x, state.scroll_y),
+                        cx,
+                    );
+                }
+
+                Ok::<_, anyhow::Error>(())
+            })?;
+            Ok(editor)
+        }))
+    }
+
+    fn set_leader_replica_id(
+        &mut self,
+        leader_replica_id: Option<u16>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.leader_replica_id = leader_replica_id;
+        if self.leader_replica_id.is_some() {
+            self.buffer.update(cx, |buffer, cx| {
+                buffer.remove_active_selections(cx);
+            });
+        } else {
+            self.buffer.update(cx, |buffer, cx| {
+                if self.focused {
+                    buffer.set_active_selections(&self.selections, cx);
+                }
+            });
+        }
+        cx.notify();
+    }
+
+    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+        let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id();
+        Some(proto::view::Variant::Editor(proto::view::Editor {
+            buffer_id,
+            scroll_top_anchor: Some(language::proto::serialize_anchor(
+                &self.scroll_top_anchor.text_anchor,
+            )),
+            scroll_x: self.scroll_position.x(),
+            scroll_y: self.scroll_position.y(),
+            selections: self.selections.iter().map(serialize_selection).collect(),
+        }))
+    }
+
+    fn add_event_to_update_proto(
+        &self,
+        event: &Self::Event,
+        update: &mut Option<proto::update_view::Variant>,
+        _: &AppContext,
+    ) -> bool {
+        let update =
+            update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
+
+        match update {
+            proto::update_view::Variant::Editor(update) => match event {
+                Event::ScrollPositionChanged { .. } => {
+                    update.scroll_top_anchor = Some(language::proto::serialize_anchor(
+                        &self.scroll_top_anchor.text_anchor,
+                    ));
+                    update.scroll_x = self.scroll_position.x();
+                    update.scroll_y = self.scroll_position.y();
+                    true
+                }
+                Event::SelectionsChanged { .. } => {
+                    update.selections = self
+                        .selections
+                        .iter()
+                        .chain(self.pending_selection.as_ref().map(|p| &p.selection))
+                        .map(serialize_selection)
+                        .collect();
+                    true
+                }
+                _ => false,
+            },
+        }
+    }
+
+    fn apply_update_proto(
+        &mut self,
+        message: update_view::Variant,
+        cx: &mut ViewContext<Self>,
+    ) -> Result<()> {
+        match message {
+            update_view::Variant::Editor(message) => {
+                let buffer = self.buffer.read(cx);
+                let buffer = buffer.read(cx);
+                let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
+                let excerpt_id = excerpt_id.clone();
+                drop(buffer);
+
+                let selections = message
+                    .selections
+                    .into_iter()
+                    .filter_map(|selection| {
+                        deserialize_selection(&excerpt_id, buffer_id, selection)
+                    })
+                    .collect::<Vec<_>>();
+                if !selections.is_empty() {
+                    self.set_selections(selections.into(), None, false, cx);
+                    self.request_autoscroll_remotely(Autoscroll::Newest, cx);
+                } else {
+                    if let Some(anchor) = message.scroll_top_anchor {
+                        self.set_scroll_top_anchor(
+                            Anchor {
+                                buffer_id: Some(buffer_id),
+                                excerpt_id: excerpt_id.clone(),
+                                text_anchor: language::proto::deserialize_anchor(anchor)
+                                    .ok_or_else(|| anyhow!("invalid scroll top"))?,
+                            },
+                            vec2f(message.scroll_x, message.scroll_y),
+                            cx,
+                        );
+                    }
+                }
+            }
+        }
+        Ok(())
+    }
+
+    fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
+        match event {
+            Event::Edited { local } => *local,
+            Event::SelectionsChanged { local } => *local,
+            Event::ScrollPositionChanged { local } => *local,
+            _ => false,
+        }
+    }
+}
+
+fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
+    proto::Selection {
+        id: selection.id as u64,
+        start: Some(language::proto::serialize_anchor(
+            &selection.start.text_anchor,
+        )),
+        end: Some(language::proto::serialize_anchor(
+            &selection.end.text_anchor,
+        )),
+        reversed: selection.reversed,
+    }
+}
+
+fn deserialize_selection(
+    excerpt_id: &ExcerptId,
+    buffer_id: usize,
+    selection: proto::Selection,
+) -> Option<Selection<Anchor>> {
+    Some(Selection {
+        id: selection.id as usize,
+        start: Anchor {
+            buffer_id: Some(buffer_id),
+            excerpt_id: excerpt_id.clone(),
+            text_anchor: language::proto::deserialize_anchor(selection.start?)?,
+        },
+        end: Anchor {
+            buffer_id: Some(buffer_id),
+            excerpt_id: excerpt_id.clone(),
+            text_anchor: language::proto::deserialize_anchor(selection.end?)?,
+        },
+        reversed: selection.reversed,
+        goal: SelectionGoal::None,
+    })
+}
 
 impl Item for Editor {
     fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) {
@@ -41,6 +269,10 @@ impl Item 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/editor/src/multi_buffer.rs 🔗

@@ -821,6 +821,14 @@ impl MultiBuffer {
             .map_or(Vec::new(), |state| state.excerpts.clone())
     }
 
+    pub fn excerpt_ids(&self) -> Vec<ExcerptId> {
+        self.buffers
+            .borrow()
+            .values()
+            .flat_map(|state| state.excerpts.iter().cloned())
+            .collect()
+    }
+
     pub fn excerpt_containing(
         &self,
         position: impl ToOffset,

crates/file_finder/src/file_finder.rs 🔗

@@ -291,7 +291,7 @@ impl FileFinder {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            editor::Event::Edited => {
+            editor::Event::Edited { .. } => {
                 let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
                 if query.is_empty() {
                     self.latest_search_id = post_inc(&mut self.search_count);

crates/go_to_line/src/go_to_line.rs 🔗

@@ -102,7 +102,7 @@ impl GoToLine {
     ) {
         match event {
             editor::Event::Blurred => cx.emit(Event::Dismissed),
-            editor::Event::Edited => {
+            editor::Event::Edited { .. } => {
                 let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text();
                 let mut components = line_editor.trim().split(&[',', ':'][..]);
                 let row = components.next().and_then(|row| row.parse::<u32>().ok());

crates/gpui/src/app.rs 🔗

@@ -8,6 +8,7 @@ use crate::{
     AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache,
 };
 use anyhow::{anyhow, Result};
+use collections::btree_map;
 use keymap::MatchResult;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
@@ -93,6 +94,8 @@ pub trait UpgradeModelHandle {
 
 pub trait UpgradeViewHandle {
     fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>>;
+
+    fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle>;
 }
 
 pub trait ReadView {
@@ -182,6 +185,12 @@ macro_rules! action {
                 Box::new(self.clone())
             }
         }
+
+        impl From<$arg> for $name {
+            fn from(arg: $arg) -> Self {
+                Self(arg)
+            }
+        }
     };
 
     ($name:ident) => {
@@ -647,6 +656,10 @@ impl UpgradeViewHandle for AsyncAppContext {
     fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
         self.0.borrow_mut().upgrade_view_handle(handle)
     }
+
+    fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
+        self.0.borrow_mut().upgrade_any_view_handle(handle)
+    }
 }
 
 impl ReadModelWith for AsyncAppContext {
@@ -1098,21 +1111,18 @@ impl MutableAppContext {
         E: Any,
         F: 'static + FnMut(&E, &mut Self),
     {
-        let id = post_inc(&mut self.next_subscription_id);
+        let subscription_id = post_inc(&mut self.next_subscription_id);
         let type_id = TypeId::of::<E>();
-        self.global_subscriptions
-            .lock()
-            .entry(type_id)
-            .or_default()
-            .insert(
-                id,
-                Some(Box::new(move |payload, cx| {
-                    let payload = payload.downcast_ref().expect("downcast is type safe");
-                    callback(payload, cx)
-                })),
-            );
+        self.pending_effects.push_back(Effect::GlobalSubscription {
+            type_id,
+            subscription_id,
+            callback: Box::new(move |payload, cx| {
+                let payload = payload.downcast_ref().expect("downcast is type safe");
+                callback(payload, cx)
+            }),
+        });
         Subscription::GlobalSubscription {
-            id,
+            id: subscription_id,
             type_id,
             subscriptions: Some(Arc::downgrade(&self.global_subscriptions)),
         }
@@ -1138,25 +1148,22 @@ impl MutableAppContext {
         H: Handle<E>,
         F: 'static + FnMut(H, &E::Event, &mut Self) -> bool,
     {
-        let id = post_inc(&mut self.next_subscription_id);
+        let subscription_id = post_inc(&mut self.next_subscription_id);
         let emitter = handle.downgrade();
-        self.subscriptions
-            .lock()
-            .entry(handle.id())
-            .or_default()
-            .insert(
-                id,
-                Some(Box::new(move |payload, cx| {
-                    if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) {
-                        let payload = payload.downcast_ref().expect("downcast is type safe");
-                        callback(emitter, payload, cx)
-                    } else {
-                        false
-                    }
-                })),
-            );
+        self.pending_effects.push_back(Effect::Subscription {
+            entity_id: handle.id(),
+            subscription_id,
+            callback: Box::new(move |payload, cx| {
+                if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) {
+                    let payload = payload.downcast_ref().expect("downcast is type safe");
+                    callback(emitter, payload, cx)
+                } else {
+                    false
+                }
+            }),
+        });
         Subscription::Subscription {
-            id,
+            id: subscription_id,
             entity_id: handle.id(),
             subscriptions: Some(Arc::downgrade(&self.subscriptions)),
         }
@@ -1169,25 +1176,23 @@ impl MutableAppContext {
         H: Handle<E>,
         F: 'static + FnMut(H, &mut Self) -> bool,
     {
-        let id = post_inc(&mut self.next_subscription_id);
+        let subscription_id = post_inc(&mut self.next_subscription_id);
         let observed = handle.downgrade();
-        self.observations
-            .lock()
-            .entry(handle.id())
-            .or_default()
-            .insert(
-                id,
-                Some(Box::new(move |cx| {
-                    if let Some(observed) = H::upgrade_from(&observed, cx) {
-                        callback(observed, cx)
-                    } else {
-                        false
-                    }
-                })),
-            );
+        let entity_id = handle.id();
+        self.pending_effects.push_back(Effect::Observation {
+            entity_id,
+            subscription_id,
+            callback: Box::new(move |cx| {
+                if let Some(observed) = H::upgrade_from(&observed, cx) {
+                    callback(observed, cx)
+                } else {
+                    false
+                }
+            }),
+        });
         Subscription::Observation {
-            id,
-            entity_id: handle.id(),
+            id: subscription_id,
+            entity_id,
             observations: Some(Arc::downgrade(&self.observations)),
         }
     }
@@ -1219,7 +1224,17 @@ impl MutableAppContext {
     }
 
     fn defer(&mut self, callback: Box<dyn FnOnce(&mut MutableAppContext)>) {
-        self.pending_effects.push_back(Effect::Deferred(callback))
+        self.pending_effects.push_back(Effect::Deferred {
+            callback,
+            after_window_update: false,
+        })
+    }
+
+    pub fn after_window_update(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
+        self.pending_effects.push_back(Effect::Deferred {
+            callback: Box::new(callback),
+            after_window_update: true,
+        })
     }
 
     pub(crate) fn notify_model(&mut self, model_id: usize) {
@@ -1635,6 +1650,7 @@ impl MutableAppContext {
 
     fn flush_effects(&mut self) {
         self.pending_flushes = self.pending_flushes.saturating_sub(1);
+        let mut after_window_update_callbacks = Vec::new();
 
         if !self.flushing_effects && self.pending_flushes == 0 {
             self.flushing_effects = true;
@@ -1643,15 +1659,43 @@ impl MutableAppContext {
             loop {
                 if let Some(effect) = self.pending_effects.pop_front() {
                     match effect {
+                        Effect::Subscription {
+                            entity_id,
+                            subscription_id,
+                            callback,
+                        } => self.handle_subscription_effect(entity_id, subscription_id, callback),
                         Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload),
+                        Effect::GlobalSubscription {
+                            type_id,
+                            subscription_id,
+                            callback,
+                        } => self.handle_global_subscription_effect(
+                            type_id,
+                            subscription_id,
+                            callback,
+                        ),
                         Effect::GlobalEvent { payload } => self.emit_global_event(payload),
+                        Effect::Observation {
+                            entity_id,
+                            subscription_id,
+                            callback,
+                        } => self.handle_observation_effect(entity_id, subscription_id, callback),
                         Effect::ModelNotification { model_id } => {
                             self.notify_model_observers(model_id)
                         }
                         Effect::ViewNotification { window_id, view_id } => {
                             self.notify_view_observers(window_id, view_id)
                         }
-                        Effect::Deferred(callback) => callback(self),
+                        Effect::Deferred {
+                            callback,
+                            after_window_update,
+                        } => {
+                            if after_window_update {
+                                after_window_update_callbacks.push(callback);
+                            } else {
+                                callback(self)
+                            }
+                        }
                         Effect::ModelRelease { model_id, model } => {
                             self.notify_release_observers(model_id, model.as_any())
                         }
@@ -1683,12 +1727,18 @@ impl MutableAppContext {
                     }
 
                     if self.pending_effects.is_empty() {
-                        self.flushing_effects = false;
-                        self.pending_notifications.clear();
-                        break;
-                    } else {
-                        refreshing = false;
+                        for callback in after_window_update_callbacks.drain(..) {
+                            callback(self);
+                        }
+
+                        if self.pending_effects.is_empty() {
+                            self.flushing_effects = false;
+                            self.pending_notifications.clear();
+                            break;
+                        }
                     }
+
+                    refreshing = false;
                 }
             }
         }
@@ -1759,6 +1809,30 @@ impl MutableAppContext {
         }
     }
 
+    fn handle_subscription_effect(
+        &mut self,
+        entity_id: usize,
+        subscription_id: usize,
+        callback: SubscriptionCallback,
+    ) {
+        match self
+            .subscriptions
+            .lock()
+            .entry(entity_id)
+            .or_default()
+            .entry(subscription_id)
+        {
+            btree_map::Entry::Vacant(entry) => {
+                entry.insert(Some(callback));
+            }
+            // Subscription was dropped before effect was processed
+            btree_map::Entry::Occupied(entry) => {
+                debug_assert!(entry.get().is_none());
+                entry.remove();
+            }
+        }
+    }
+
     fn emit_event(&mut self, entity_id: usize, payload: Box<dyn Any>) {
         let callbacks = self.subscriptions.lock().remove(&entity_id);
         if let Some(callbacks) = callbacks {
@@ -1773,10 +1847,10 @@ impl MutableAppContext {
                             .or_default()
                             .entry(id)
                         {
-                            collections::btree_map::Entry::Vacant(entry) => {
+                            btree_map::Entry::Vacant(entry) => {
                                 entry.insert(Some(callback));
                             }
-                            collections::btree_map::Entry::Occupied(entry) => {
+                            btree_map::Entry::Occupied(entry) => {
                                 entry.remove();
                             }
                         }
@@ -1786,6 +1860,30 @@ impl MutableAppContext {
         }
     }
 
+    fn handle_global_subscription_effect(
+        &mut self,
+        type_id: TypeId,
+        subscription_id: usize,
+        callback: GlobalSubscriptionCallback,
+    ) {
+        match self
+            .global_subscriptions
+            .lock()
+            .entry(type_id)
+            .or_default()
+            .entry(subscription_id)
+        {
+            btree_map::Entry::Vacant(entry) => {
+                entry.insert(Some(callback));
+            }
+            // Subscription was dropped before effect was processed
+            btree_map::Entry::Occupied(entry) => {
+                debug_assert!(entry.get().is_none());
+                entry.remove();
+            }
+        }
+    }
+
     fn emit_global_event(&mut self, payload: Box<dyn Any>) {
         let type_id = (&*payload).type_id();
         let callbacks = self.global_subscriptions.lock().remove(&type_id);
@@ -1800,10 +1898,10 @@ impl MutableAppContext {
                         .or_default()
                         .entry(id)
                     {
-                        collections::btree_map::Entry::Vacant(entry) => {
+                        btree_map::Entry::Vacant(entry) => {
                             entry.insert(Some(callback));
                         }
-                        collections::btree_map::Entry::Occupied(entry) => {
+                        btree_map::Entry::Occupied(entry) => {
                             entry.remove();
                         }
                     }
@@ -1812,6 +1910,30 @@ impl MutableAppContext {
         }
     }
 
+    fn handle_observation_effect(
+        &mut self,
+        entity_id: usize,
+        subscription_id: usize,
+        callback: ObservationCallback,
+    ) {
+        match self
+            .observations
+            .lock()
+            .entry(entity_id)
+            .or_default()
+            .entry(subscription_id)
+        {
+            btree_map::Entry::Vacant(entry) => {
+                entry.insert(Some(callback));
+            }
+            // Observation was dropped before effect was processed
+            btree_map::Entry::Occupied(entry) => {
+                debug_assert!(entry.get().is_none());
+                entry.remove();
+            }
+        }
+    }
+
     fn notify_model_observers(&mut self, observed_id: usize) {
         let callbacks = self.observations.lock().remove(&observed_id);
         if let Some(callbacks) = callbacks {
@@ -1827,10 +1949,10 @@ impl MutableAppContext {
                                 .or_default()
                                 .entry(id)
                             {
-                                collections::btree_map::Entry::Vacant(entry) => {
+                                btree_map::Entry::Vacant(entry) => {
                                     entry.insert(Some(callback));
                                 }
-                                collections::btree_map::Entry::Occupied(entry) => {
+                                btree_map::Entry::Occupied(entry) => {
                                     entry.remove();
                                 }
                             }
@@ -1868,10 +1990,10 @@ impl MutableAppContext {
                                 .or_default()
                                 .entry(id)
                             {
-                                collections::btree_map::Entry::Vacant(entry) => {
+                                btree_map::Entry::Vacant(entry) => {
                                     entry.insert(Some(callback));
                                 }
-                                collections::btree_map::Entry::Occupied(entry) => {
+                                btree_map::Entry::Occupied(entry) => {
                                     entry.remove();
                                 }
                             }
@@ -2017,6 +2139,10 @@ impl UpgradeViewHandle for MutableAppContext {
     fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
         self.cx.upgrade_view_handle(handle)
     }
+
+    fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
+        self.cx.upgrade_any_view_handle(handle)
+    }
 }
 
 impl ReadView for MutableAppContext {
@@ -2111,6 +2237,10 @@ impl AppContext {
         &self.platform
     }
 
+    pub fn has_global<T: 'static>(&self) -> bool {
+        self.globals.contains_key(&TypeId::of::<T>())
+    }
+
     pub fn global<T: 'static>(&self) -> &T {
         self.globals
             .get(&TypeId::of::<T>())
@@ -2174,6 +2304,19 @@ impl UpgradeViewHandle for AppContext {
             None
         }
     }
+
+    fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
+        if self.ref_counts.lock().is_entity_alive(handle.view_id) {
+            Some(AnyViewHandle::new(
+                handle.window_id,
+                handle.view_id,
+                handle.view_type,
+                self.ref_counts.clone(),
+            ))
+        } else {
+            None
+        }
+    }
 }
 
 impl ReadView for AppContext {
@@ -2201,13 +2344,28 @@ pub struct WindowInvalidation {
 }
 
 pub enum Effect {
+    Subscription {
+        entity_id: usize,
+        subscription_id: usize,
+        callback: SubscriptionCallback,
+    },
     Event {
         entity_id: usize,
         payload: Box<dyn Any>,
     },
+    GlobalSubscription {
+        type_id: TypeId,
+        subscription_id: usize,
+        callback: GlobalSubscriptionCallback,
+    },
     GlobalEvent {
         payload: Box<dyn Any>,
     },
+    Observation {
+        entity_id: usize,
+        subscription_id: usize,
+        callback: ObservationCallback,
+    },
     ModelNotification {
         model_id: usize,
     },
@@ -2215,7 +2373,10 @@ pub enum Effect {
         window_id: usize,
         view_id: usize,
     },
-    Deferred(Box<dyn FnOnce(&mut MutableAppContext)>),
+    Deferred {
+        callback: Box<dyn FnOnce(&mut MutableAppContext)>,
+        after_window_update: bool,
+    },
     ModelRelease {
         model_id: usize,
         model: Box<dyn AnyModel>,
@@ -2237,14 +2398,41 @@ pub enum Effect {
 impl Debug for Effect {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
+            Effect::Subscription {
+                entity_id,
+                subscription_id,
+                ..
+            } => f
+                .debug_struct("Effect::Subscribe")
+                .field("entity_id", entity_id)
+                .field("subscription_id", subscription_id)
+                .finish(),
             Effect::Event { entity_id, .. } => f
                 .debug_struct("Effect::Event")
                 .field("entity_id", entity_id)
                 .finish(),
+            Effect::GlobalSubscription {
+                type_id,
+                subscription_id,
+                ..
+            } => f
+                .debug_struct("Effect::Subscribe")
+                .field("type_id", type_id)
+                .field("subscription_id", subscription_id)
+                .finish(),
             Effect::GlobalEvent { payload, .. } => f
                 .debug_struct("Effect::GlobalEvent")
                 .field("type_id", &(&*payload).type_id())
                 .finish(),
+            Effect::Observation {
+                entity_id,
+                subscription_id,
+                ..
+            } => f
+                .debug_struct("Effect::Observation")
+                .field("entity_id", entity_id)
+                .field("subscription_id", subscription_id)
+                .finish(),
             Effect::ModelNotification { model_id } => f
                 .debug_struct("Effect::ModelNotification")
                 .field("model_id", model_id)
@@ -2254,7 +2442,7 @@ impl Debug for Effect {
                 .field("window_id", window_id)
                 .field("view_id", view_id)
                 .finish(),
-            Effect::Deferred(_) => f.debug_struct("Effect::Deferred").finish(),
+            Effect::Deferred { .. } => f.debug_struct("Effect::Deferred").finish(),
             Effect::ModelRelease { model_id, .. } => f
                 .debug_struct("Effect::ModelRelease")
                 .field("model_id", model_id)
@@ -2786,6 +2974,18 @@ impl<'a, T: View> ViewContext<'a, T> {
         }))
     }
 
+    pub fn after_window_update(
+        &mut self,
+        callback: impl 'static + FnOnce(&mut T, &mut ViewContext<T>),
+    ) {
+        let handle = self.handle();
+        self.app.after_window_update(move |cx| {
+            handle.update(cx, |view, cx| {
+                callback(view, cx);
+            })
+        })
+    }
+
     pub fn propagate_action(&mut self) {
         self.app.halt_action_dispatch = false;
     }
@@ -2931,6 +3131,10 @@ impl<V> UpgradeViewHandle for ViewContext<'_, V> {
     fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
         self.cx.upgrade_view_handle(handle)
     }
+
+    fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option<AnyViewHandle> {
+        self.cx.upgrade_any_view_handle(handle)
+    }
 }
 
 impl<V: View> UpdateModel for ViewContext<'_, V> {
@@ -3505,6 +3709,13 @@ impl<T> PartialEq<ViewHandle<T>> for WeakViewHandle<T> {
 
 impl<T> Eq for ViewHandle<T> {}
 
+impl<T> Hash for ViewHandle<T> {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.window_id.hash(state);
+        self.view_id.hash(state);
+    }
+}
+
 impl<T> Debug for ViewHandle<T> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.debug_struct(&format!("ViewHandle<{}>", type_name::<T>()))
@@ -3619,6 +3830,18 @@ impl AnyViewHandle {
             None
         }
     }
+
+    pub fn downgrade(&self) -> AnyWeakViewHandle {
+        AnyWeakViewHandle {
+            window_id: self.window_id,
+            view_id: self.view_id,
+            view_type: self.view_type,
+        }
+    }
+
+    pub fn view_type(&self) -> TypeId {
+        self.view_type
+    }
 }
 
 impl Clone for AnyViewHandle {
@@ -3845,6 +4068,28 @@ impl<T> Hash for WeakViewHandle<T> {
     }
 }
 
+pub struct AnyWeakViewHandle {
+    window_id: usize,
+    view_id: usize,
+    view_type: TypeId,
+}
+
+impl AnyWeakViewHandle {
+    pub fn upgrade(&self, cx: &impl UpgradeViewHandle) -> Option<AnyViewHandle> {
+        cx.upgrade_any_view_handle(self)
+    }
+}
+
+impl<T: View> From<WeakViewHandle<T>> for AnyWeakViewHandle {
+    fn from(handle: WeakViewHandle<T>) -> Self {
+        AnyWeakViewHandle {
+            window_id: handle.window_id,
+            view_id: handle.view_id,
+            view_type: TypeId::of::<T>(),
+        }
+    }
+}
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
 pub struct ElementStateId {
     view_id: usize,
@@ -3975,10 +4220,10 @@ impl Drop for Subscription {
                         .or_default()
                         .entry(*id)
                     {
-                        collections::btree_map::Entry::Vacant(entry) => {
+                        btree_map::Entry::Vacant(entry) => {
                             entry.insert(None);
                         }
-                        collections::btree_map::Entry::Occupied(entry) => {
+                        btree_map::Entry::Occupied(entry) => {
                             entry.remove();
                         }
                     }
@@ -3991,10 +4236,10 @@ impl Drop for Subscription {
             } => {
                 if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) {
                     match subscriptions.lock().entry(*type_id).or_default().entry(*id) {
-                        collections::btree_map::Entry::Vacant(entry) => {
+                        btree_map::Entry::Vacant(entry) => {
                             entry.insert(None);
                         }
-                        collections::btree_map::Entry::Occupied(entry) => {
+                        btree_map::Entry::Occupied(entry) => {
                             entry.remove();
                         }
                     }
@@ -4012,10 +4257,10 @@ impl Drop for Subscription {
                         .or_default()
                         .entry(*id)
                     {
-                        collections::btree_map::Entry::Vacant(entry) => {
+                        btree_map::Entry::Vacant(entry) => {
                             entry.insert(None);
                         }
-                        collections::btree_map::Entry::Occupied(entry) => {
+                        btree_map::Entry::Occupied(entry) => {
                             entry.remove();
                         }
                     }
@@ -4220,7 +4465,7 @@ mod tests {
     use smol::future::poll_once;
     use std::{
         cell::Cell,
-        sync::atomic::{AtomicUsize, Ordering::SeqCst},
+        sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
     };
 
     #[crate::test(self)]
@@ -4297,6 +4542,7 @@ mod tests {
 
         let handle_1 = cx.add_model(|_| Model::default());
         let handle_2 = cx.add_model(|_| Model::default());
+
         handle_1.update(cx, |_, cx| {
             cx.subscribe(&handle_2, move |model: &mut Model, emitter, event, cx| {
                 model.events.push(*event);
@@ -4316,6 +4562,37 @@ mod tests {
         assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]);
     }
 
+    #[crate::test(self)]
+    fn test_model_emit_before_subscribe_in_same_update_cycle(cx: &mut MutableAppContext) {
+        #[derive(Default)]
+        struct Model;
+
+        impl Entity for Model {
+            type Event = ();
+        }
+
+        let events = Rc::new(RefCell::new(Vec::new()));
+        cx.add_model(|cx| {
+            drop(cx.subscribe(&cx.handle(), {
+                let events = events.clone();
+                move |_, _, _, _| events.borrow_mut().push("dropped before flush")
+            }));
+            cx.subscribe(&cx.handle(), {
+                let events = events.clone();
+                move |_, _, _, _| events.borrow_mut().push("before emit")
+            })
+            .detach();
+            cx.emit(());
+            cx.subscribe(&cx.handle(), {
+                let events = events.clone();
+                move |_, _, _, _| events.borrow_mut().push("after emit")
+            })
+            .detach();
+            Model
+        });
+        assert_eq!(*events.borrow(), ["before emit"]);
+    }
+
     #[crate::test(self)]
     fn test_observe_and_notify_from_model(cx: &mut MutableAppContext) {
         #[derive(Default)]
@@ -4355,6 +4632,89 @@ mod tests {
         assert_eq!(handle_1.read(cx).events, vec![7, 5, 10])
     }
 
+    #[crate::test(self)]
+    fn test_model_notify_before_observe_in_same_update_cycle(cx: &mut MutableAppContext) {
+        #[derive(Default)]
+        struct Model;
+
+        impl Entity for Model {
+            type Event = ();
+        }
+
+        let events = Rc::new(RefCell::new(Vec::new()));
+        cx.add_model(|cx| {
+            drop(cx.observe(&cx.handle(), {
+                let events = events.clone();
+                move |_, _, _| events.borrow_mut().push("dropped before flush")
+            }));
+            cx.observe(&cx.handle(), {
+                let events = events.clone();
+                move |_, _, _| events.borrow_mut().push("before notify")
+            })
+            .detach();
+            cx.notify();
+            cx.observe(&cx.handle(), {
+                let events = events.clone();
+                move |_, _, _| events.borrow_mut().push("after notify")
+            })
+            .detach();
+            Model
+        });
+        assert_eq!(*events.borrow(), ["before notify"]);
+    }
+
+    #[crate::test(self)]
+    fn test_defer_and_after_window_update(cx: &mut MutableAppContext) {
+        struct View {
+            render_count: usize,
+        }
+
+        impl Entity for View {
+            type Event = usize;
+        }
+
+        impl super::View for View {
+            fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+                post_inc(&mut self.render_count);
+                Empty::new().boxed()
+            }
+
+            fn ui_name() -> &'static str {
+                "View"
+            }
+        }
+
+        let (_, view) = cx.add_window(Default::default(), |_| View { render_count: 0 });
+        let called_defer = Rc::new(AtomicBool::new(false));
+        let called_after_window_update = Rc::new(AtomicBool::new(false));
+
+        view.update(cx, |this, cx| {
+            assert_eq!(this.render_count, 1);
+            cx.defer({
+                let called_defer = called_defer.clone();
+                move |this, _| {
+                    assert_eq!(this.render_count, 1);
+                    called_defer.store(true, SeqCst);
+                }
+            });
+            cx.after_window_update({
+                let called_after_window_update = called_after_window_update.clone();
+                move |this, cx| {
+                    assert_eq!(this.render_count, 2);
+                    called_after_window_update.store(true, SeqCst);
+                    cx.notify();
+                }
+            });
+            assert!(!called_defer.load(SeqCst));
+            assert!(!called_after_window_update.load(SeqCst));
+            cx.notify();
+        });
+
+        assert!(called_defer.load(SeqCst));
+        assert!(called_after_window_update.load(SeqCst));
+        assert_eq!(view.read(cx).render_count, 3);
+    }
+
     #[crate::test(self)]
     fn test_view_handles(cx: &mut MutableAppContext) {
         struct View {
@@ -4649,6 +5009,41 @@ mod tests {
         );
     }
 
+    #[crate::test(self)]
+    fn test_global_events_emitted_before_subscription_in_same_update_cycle(
+        cx: &mut MutableAppContext,
+    ) {
+        let events = Rc::new(RefCell::new(Vec::new()));
+        cx.update(|cx| {
+            {
+                let events = events.clone();
+                drop(cx.subscribe_global(move |_: &(), _| {
+                    events.borrow_mut().push("dropped before emit");
+                }));
+            }
+
+            {
+                let events = events.clone();
+                cx.subscribe_global(move |_: &(), _| {
+                    events.borrow_mut().push("before emit");
+                })
+                .detach();
+            }
+
+            cx.emit_global(());
+
+            {
+                let events = events.clone();
+                cx.subscribe_global(move |_: &(), _| {
+                    events.borrow_mut().push("after emit");
+                })
+                .detach();
+            }
+        });
+
+        assert_eq!(*events.borrow(), ["before emit"]);
+    }
+
     #[crate::test(self)]
     fn test_global_nested_events(cx: &mut MutableAppContext) {
         #[derive(Clone, Debug, Eq, PartialEq)]
@@ -4661,11 +5056,13 @@ mod tests {
             cx.subscribe_global(move |e: &GlobalEvent, cx| {
                 events.borrow_mut().push(("Outer", e.clone()));
 
-                let events = events.clone();
-                cx.subscribe_global(move |e: &GlobalEvent, _| {
-                    events.borrow_mut().push(("Inner", e.clone()));
-                })
-                .detach();
+                if e.0 == 1 {
+                    let events = events.clone();
+                    cx.subscribe_global(move |e: &GlobalEvent, _| {
+                        events.borrow_mut().push(("Inner", e.clone()));
+                    })
+                    .detach();
+                }
             })
             .detach();
         }
@@ -4675,16 +5072,18 @@ mod tests {
             cx.emit_global(GlobalEvent(2));
             cx.emit_global(GlobalEvent(3));
         });
+        cx.update(|cx| {
+            cx.emit_global(GlobalEvent(4));
+        });
 
         assert_eq!(
             &*events.borrow(),
             &[
                 ("Outer", GlobalEvent(1)),
                 ("Outer", GlobalEvent(2)),
-                ("Inner", GlobalEvent(2)),
                 ("Outer", GlobalEvent(3)),
-                ("Inner", GlobalEvent(3)),
-                ("Inner", GlobalEvent(3)),
+                ("Outer", GlobalEvent(4)),
+                ("Inner", GlobalEvent(4)),
             ]
         );
     }
@@ -4736,6 +5135,47 @@ mod tests {
         observed_model.update(cx, |_, cx| cx.emit(()));
     }
 
+    #[crate::test(self)]
+    fn test_view_emit_before_subscribe_in_same_update_cycle(cx: &mut MutableAppContext) {
+        #[derive(Default)]
+        struct TestView;
+
+        impl Entity for TestView {
+            type Event = ();
+        }
+
+        impl View for TestView {
+            fn ui_name() -> &'static str {
+                "TestView"
+            }
+
+            fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+                Empty::new().boxed()
+            }
+        }
+
+        let events = Rc::new(RefCell::new(Vec::new()));
+        cx.add_window(Default::default(), |cx| {
+            drop(cx.subscribe(&cx.handle(), {
+                let events = events.clone();
+                move |_, _, _, _| events.borrow_mut().push("dropped before flush")
+            }));
+            cx.subscribe(&cx.handle(), {
+                let events = events.clone();
+                move |_, _, _, _| events.borrow_mut().push("before emit")
+            })
+            .detach();
+            cx.emit(());
+            cx.subscribe(&cx.handle(), {
+                let events = events.clone();
+                move |_, _, _, _| events.borrow_mut().push("after emit")
+            })
+            .detach();
+            TestView
+        });
+        assert_eq!(*events.borrow(), ["before emit"]);
+    }
+
     #[crate::test(self)]
     fn test_observe_and_notify_from_view(cx: &mut MutableAppContext) {
         #[derive(Default)]
@@ -4783,6 +5223,47 @@ mod tests {
         assert_eq!(view.read(cx).events, vec![11]);
     }
 
+    #[crate::test(self)]
+    fn test_view_notify_before_observe_in_same_update_cycle(cx: &mut MutableAppContext) {
+        #[derive(Default)]
+        struct TestView;
+
+        impl Entity for TestView {
+            type Event = ();
+        }
+
+        impl View for TestView {
+            fn ui_name() -> &'static str {
+                "TestView"
+            }
+
+            fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+                Empty::new().boxed()
+            }
+        }
+
+        let events = Rc::new(RefCell::new(Vec::new()));
+        cx.add_window(Default::default(), |cx| {
+            drop(cx.observe(&cx.handle(), {
+                let events = events.clone();
+                move |_, _, _| events.borrow_mut().push("dropped before flush")
+            }));
+            cx.observe(&cx.handle(), {
+                let events = events.clone();
+                move |_, _, _| events.borrow_mut().push("before notify")
+            })
+            .detach();
+            cx.notify();
+            cx.observe(&cx.handle(), {
+                let events = events.clone();
+                move |_, _, _| events.borrow_mut().push("after notify")
+            })
+            .detach();
+            TestView
+        });
+        assert_eq!(*events.borrow(), ["before notify"]);
+    }
+
     #[crate::test(self)]
     fn test_dropping_observers(cx: &mut MutableAppContext) {
         struct View;

crates/gpui/src/presenter.rs 🔗

@@ -299,6 +299,10 @@ impl<'a> UpgradeViewHandle for LayoutContext<'a> {
     fn upgrade_view_handle<T: View>(&self, handle: &WeakViewHandle<T>) -> Option<ViewHandle<T>> {
         self.app.upgrade_view_handle(handle)
     }
+
+    fn upgrade_any_view_handle(&self, handle: &crate::AnyWeakViewHandle) -> Option<AnyViewHandle> {
+        self.app.upgrade_any_view_handle(handle)
+    }
 }
 
 impl<'a> ElementStateContext for LayoutContext<'a> {

crates/language/src/buffer.rs 🔗

@@ -142,7 +142,7 @@ pub enum Operation {
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum Event {
     Operation(Operation),
-    Edited,
+    Edited { local: bool },
     Dirtied,
     Saved,
     FileHandleChanged,
@@ -967,7 +967,7 @@ impl Buffer {
     ) -> Option<TransactionId> {
         if let Some((transaction_id, start_version)) = self.text.end_transaction_at(now) {
             let was_dirty = start_version != self.saved_version;
-            self.did_edit(&start_version, was_dirty, cx);
+            self.did_edit(&start_version, was_dirty, true, cx);
             Some(transaction_id)
         } else {
             None
@@ -1160,6 +1160,7 @@ impl Buffer {
         &mut self,
         old_version: &clock::Global,
         was_dirty: bool,
+        local: bool,
         cx: &mut ModelContext<Self>,
     ) {
         if self.edits_since::<usize>(old_version).next().is_none() {
@@ -1168,7 +1169,7 @@ impl Buffer {
 
         self.reparse(cx);
 
-        cx.emit(Event::Edited);
+        cx.emit(Event::Edited { local });
         if !was_dirty {
             cx.emit(Event::Dirtied);
         }
@@ -1205,7 +1206,7 @@ impl Buffer {
         self.text.apply_ops(buffer_ops)?;
         self.deferred_ops.insert(deferred_ops);
         self.flush_deferred_ops(cx);
-        self.did_edit(&old_version, was_dirty, cx);
+        self.did_edit(&old_version, was_dirty, false, cx);
         // Notify independently of whether the buffer was edited as the operations could include a
         // selection update.
         cx.notify();
@@ -1320,7 +1321,7 @@ impl Buffer {
 
         if let Some((transaction_id, operation)) = self.text.undo() {
             self.send_operation(Operation::Buffer(operation), cx);
-            self.did_edit(&old_version, was_dirty, cx);
+            self.did_edit(&old_version, was_dirty, true, cx);
             Some(transaction_id)
         } else {
             None
@@ -1341,7 +1342,7 @@ impl Buffer {
             self.send_operation(Operation::Buffer(operation), cx);
         }
         if undone {
-            self.did_edit(&old_version, was_dirty, cx)
+            self.did_edit(&old_version, was_dirty, true, cx)
         }
         undone
     }
@@ -1352,7 +1353,7 @@ impl Buffer {
 
         if let Some((transaction_id, operation)) = self.text.redo() {
             self.send_operation(Operation::Buffer(operation), cx);
-            self.did_edit(&old_version, was_dirty, cx);
+            self.did_edit(&old_version, was_dirty, true, cx);
             Some(transaction_id)
         } else {
             None
@@ -1373,7 +1374,7 @@ impl Buffer {
             self.send_operation(Operation::Buffer(operation), cx);
         }
         if redone {
-            self.did_edit(&old_version, was_dirty, cx)
+            self.did_edit(&old_version, was_dirty, true, cx)
         }
         redone
     }
@@ -1439,7 +1440,7 @@ impl Buffer {
         if !ops.is_empty() {
             for op in ops {
                 self.send_operation(Operation::Buffer(op), cx);
-                self.did_edit(&old_version, was_dirty, cx);
+                self.did_edit(&old_version, was_dirty, true, cx);
             }
         }
     }
@@ -1800,12 +1801,6 @@ impl BufferSnapshot {
             .min_by_key(|(open_range, close_range)| close_range.end - open_range.start)
     }
 
-    /*
-    impl BufferSnapshot
-      pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, impl Iterator<Item = &Selection<Anchor>>)>
-      pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, i
-    */
-
     pub fn remote_selections_in_range<'a>(
         &'a self,
         range: Range<Anchor>,

crates/language/src/proto.rs 🔗

@@ -100,15 +100,16 @@ pub fn serialize_undo_map_entry(
 }
 
 pub fn serialize_selections(selections: &Arc<[Selection<Anchor>]>) -> Vec<proto::Selection> {
-    selections
-        .iter()
-        .map(|selection| proto::Selection {
-            id: selection.id as u64,
-            start: Some(serialize_anchor(&selection.start)),
-            end: Some(serialize_anchor(&selection.end)),
-            reversed: selection.reversed,
-        })
-        .collect()
+    selections.iter().map(serialize_selection).collect()
+}
+
+pub fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
+    proto::Selection {
+        id: selection.id as u64,
+        start: Some(serialize_anchor(&selection.start)),
+        end: Some(serialize_anchor(&selection.end)),
+        reversed: selection.reversed,
+    }
 }
 
 pub fn serialize_diagnostics<'a>(
@@ -274,19 +275,21 @@ pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selecti
     Arc::from(
         selections
             .into_iter()
-            .filter_map(|selection| {
-                Some(Selection {
-                    id: selection.id as usize,
-                    start: deserialize_anchor(selection.start?)?,
-                    end: deserialize_anchor(selection.end?)?,
-                    reversed: selection.reversed,
-                    goal: SelectionGoal::None,
-                })
-            })
+            .filter_map(deserialize_selection)
             .collect::<Vec<_>>(),
     )
 }
 
+pub fn deserialize_selection(selection: proto::Selection) -> Option<Selection<Anchor>> {
+    Some(Selection {
+        id: selection.id as usize,
+        start: deserialize_anchor(selection.start?)?,
+        end: deserialize_anchor(selection.end?)?,
+        reversed: selection.reversed,
+        goal: SelectionGoal::None,
+    })
+}
+
 pub fn deserialize_diagnostics(
     diagnostics: Vec<proto::Diagnostic>,
 ) -> Arc<[DiagnosticEntry<Anchor>]> {

crates/language/src/tests.rs 🔗

@@ -122,11 +122,19 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) {
     let buffer_1_events = buffer_1_events.borrow();
     assert_eq!(
         *buffer_1_events,
-        vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited]
+        vec![
+            Event::Edited { local: true },
+            Event::Dirtied,
+            Event::Edited { local: true },
+            Event::Edited { local: true }
+        ]
     );
 
     let buffer_2_events = buffer_2_events.borrow();
-    assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]);
+    assert_eq!(
+        *buffer_2_events,
+        vec![Event::Edited { local: false }, Event::Dirtied]
+    );
 }
 
 #[gpui::test]

crates/outline/src/outline.rs 🔗

@@ -224,7 +224,7 @@ impl OutlineView {
     ) {
         match event {
             editor::Event::Blurred => cx.emit(Event::Dismissed),
-            editor::Event::Edited => self.update_matches(cx),
+            editor::Event::Edited { .. } => self.update_matches(cx),
             _ => {}
         }
     }

crates/project/src/project.rs 🔗

@@ -124,6 +124,8 @@ pub enum Event {
     DiskBasedDiagnosticsUpdated,
     DiskBasedDiagnosticsFinished,
     DiagnosticsUpdated(ProjectPath),
+    RemoteIdChanged(Option<u64>),
+    CollaboratorLeft(PeerId),
 }
 
 enum LanguageServerEvent {
@@ -253,34 +255,35 @@ impl ProjectEntryId {
 
 impl Project {
     pub fn init(client: &Arc<Client>) {
-        client.add_entity_message_handler(Self::handle_add_collaborator);
-        client.add_entity_message_handler(Self::handle_buffer_reloaded);
-        client.add_entity_message_handler(Self::handle_buffer_saved);
-        client.add_entity_message_handler(Self::handle_start_language_server);
-        client.add_entity_message_handler(Self::handle_update_language_server);
-        client.add_entity_message_handler(Self::handle_remove_collaborator);
-        client.add_entity_message_handler(Self::handle_register_worktree);
-        client.add_entity_message_handler(Self::handle_unregister_worktree);
-        client.add_entity_message_handler(Self::handle_unshare_project);
-        client.add_entity_message_handler(Self::handle_update_buffer_file);
-        client.add_entity_message_handler(Self::handle_update_buffer);
-        client.add_entity_message_handler(Self::handle_update_diagnostic_summary);
-        client.add_entity_message_handler(Self::handle_update_worktree);
-        client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion);
-        client.add_entity_request_handler(Self::handle_apply_code_action);
-        client.add_entity_request_handler(Self::handle_format_buffers);
-        client.add_entity_request_handler(Self::handle_get_code_actions);
-        client.add_entity_request_handler(Self::handle_get_completions);
-        client.add_entity_request_handler(Self::handle_lsp_command::<GetDefinition>);
-        client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
-        client.add_entity_request_handler(Self::handle_lsp_command::<GetReferences>);
-        client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
-        client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
-        client.add_entity_request_handler(Self::handle_search_project);
-        client.add_entity_request_handler(Self::handle_get_project_symbols);
-        client.add_entity_request_handler(Self::handle_open_buffer_for_symbol);
-        client.add_entity_request_handler(Self::handle_open_buffer);
-        client.add_entity_request_handler(Self::handle_save_buffer);
+        client.add_model_message_handler(Self::handle_add_collaborator);
+        client.add_model_message_handler(Self::handle_buffer_reloaded);
+        client.add_model_message_handler(Self::handle_buffer_saved);
+        client.add_model_message_handler(Self::handle_start_language_server);
+        client.add_model_message_handler(Self::handle_update_language_server);
+        client.add_model_message_handler(Self::handle_remove_collaborator);
+        client.add_model_message_handler(Self::handle_register_worktree);
+        client.add_model_message_handler(Self::handle_unregister_worktree);
+        client.add_model_message_handler(Self::handle_unshare_project);
+        client.add_model_message_handler(Self::handle_update_buffer_file);
+        client.add_model_message_handler(Self::handle_update_buffer);
+        client.add_model_message_handler(Self::handle_update_diagnostic_summary);
+        client.add_model_message_handler(Self::handle_update_worktree);
+        client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
+        client.add_model_request_handler(Self::handle_apply_code_action);
+        client.add_model_request_handler(Self::handle_format_buffers);
+        client.add_model_request_handler(Self::handle_get_code_actions);
+        client.add_model_request_handler(Self::handle_get_completions);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
+        client.add_model_request_handler(Self::handle_lsp_command::<PrepareRename>);
+        client.add_model_request_handler(Self::handle_lsp_command::<PerformRename>);
+        client.add_model_request_handler(Self::handle_search_project);
+        client.add_model_request_handler(Self::handle_get_project_symbols);
+        client.add_model_request_handler(Self::handle_open_buffer_for_symbol);
+        client.add_model_request_handler(Self::handle_open_buffer_by_id);
+        client.add_model_request_handler(Self::handle_open_buffer_by_path);
+        client.add_model_request_handler(Self::handle_save_buffer);
     }
 
     pub fn local(
@@ -487,7 +490,6 @@ impl Project {
         cx.update(|cx| Project::local(client, user_store, languages, fs, cx))
     }
 
-    #[cfg(any(test, feature = "test-support"))]
     pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option<ModelHandle<Buffer>> {
         self.opened_buffers
             .get(&remote_id)
@@ -566,6 +568,7 @@ impl Project {
             self.subscriptions
                 .push(self.client.add_model_for_remote_entity(remote_id, cx));
         }
+        cx.emit(Event::RemoteIdChanged(remote_id))
     }
 
     pub fn remote_id(&self) -> Option<u64> {
@@ -930,7 +933,7 @@ impl Project {
         let path_string = path.to_string_lossy().to_string();
         cx.spawn(|this, mut cx| async move {
             let response = rpc
-                .request(proto::OpenBuffer {
+                .request(proto::OpenBufferByPath {
                     project_id,
                     worktree_id: remote_worktree_id.to_proto(),
                     path: path_string,
@@ -979,6 +982,32 @@ impl Project {
         })
     }
 
+    pub fn open_buffer_by_id(
+        &mut self,
+        id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ModelHandle<Buffer>>> {
+        if let Some(buffer) = self.buffer_for_id(id, cx) {
+            Task::ready(Ok(buffer))
+        } else if self.is_local() {
+            Task::ready(Err(anyhow!("buffer {} does not exist", id)))
+        } else if let Some(project_id) = self.remote_id() {
+            let request = self
+                .client
+                .request(proto::OpenBufferById { project_id, id });
+            cx.spawn(|this, mut cx| async move {
+                let buffer = request
+                    .await?
+                    .buffer
+                    .ok_or_else(|| anyhow!("invalid buffer"))?;
+                this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx))
+                    .await
+            })
+        } else {
+            Task::ready(Err(anyhow!("cannot open buffer while disconnected")))
+        }
+    }
+
     pub fn save_buffer_as(
         &mut self,
         buffer: ModelHandle<Buffer>,
@@ -1150,7 +1179,7 @@ impl Project {
                 });
                 cx.background().spawn(request).detach_and_log_err(cx);
             }
-            BufferEvent::Edited => {
+            BufferEvent::Edited { .. } => {
                 let language_server = self
                     .language_server_for_buffer(buffer.read(cx), cx)?
                     .clone();
@@ -3340,6 +3369,7 @@ impl Project {
                     buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx));
                 }
             }
+            cx.emit(Event::CollaboratorLeft(peer_id));
             cx.notify();
             Ok(())
         })
@@ -3887,9 +3917,28 @@ impl Project {
         hasher.finalize().as_slice().try_into().unwrap()
     }
 
-    async fn handle_open_buffer(
+    async fn handle_open_buffer_by_id(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::OpenBufferById>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::OpenBufferResponse> {
+        let peer_id = envelope.original_sender_id()?;
+        let buffer = this
+            .update(&mut cx, |this, cx| {
+                this.open_buffer_by_id(envelope.payload.id, cx)
+            })
+            .await?;
+        this.update(&mut cx, |this, cx| {
+            Ok(proto::OpenBufferResponse {
+                buffer: Some(this.serialize_buffer_for_peer(&buffer, peer_id, cx)),
+            })
+        })
+    }
+
+    async fn handle_open_buffer_by_path(
         this: ModelHandle<Self>,
-        envelope: TypedEnvelope<proto::OpenBuffer>,
+        envelope: TypedEnvelope<proto::OpenBufferByPath>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
     ) -> Result<proto::OpenBufferResponse> {
@@ -6180,7 +6229,10 @@ mod tests {
             assert!(buffer.is_dirty());
             assert_eq!(
                 *events.borrow(),
-                &[language::Event::Edited, language::Event::Dirtied]
+                &[
+                    language::Event::Edited { local: true },
+                    language::Event::Dirtied
+                ]
             );
             events.borrow_mut().clear();
             buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), None, cx);
@@ -6203,9 +6255,9 @@ mod tests {
             assert_eq!(
                 *events.borrow(),
                 &[
-                    language::Event::Edited,
+                    language::Event::Edited { local: true },
                     language::Event::Dirtied,
-                    language::Event::Edited,
+                    language::Event::Edited { local: true },
                 ],
             );
             events.borrow_mut().clear();
@@ -6217,7 +6269,7 @@ mod tests {
             assert!(buffer.is_dirty());
         });
 
-        assert_eq!(*events.borrow(), &[language::Event::Edited]);
+        assert_eq!(*events.borrow(), &[language::Event::Edited { local: true }]);
 
         // When a file is deleted, the buffer is considered dirty.
         let events = Rc::new(RefCell::new(Vec::new()));

crates/project_symbols/src/project_symbols.rs 🔗

@@ -328,7 +328,7 @@ impl ProjectSymbolsView {
     ) {
         match event {
             editor::Event::Blurred => cx.emit(Event::Dismissed),
-            editor::Event::Edited => self.update_matches(cx),
+            editor::Event::Edited { .. } => self.update_matches(cx),
             _ => {}
         }
     }

crates/rpc/proto/zed.proto 🔗

@@ -40,8 +40,9 @@ message Envelope {
         StartLanguageServer start_language_server = 33;
         UpdateLanguageServer update_language_server = 34;
 
-        OpenBuffer open_buffer = 35;
-        OpenBufferResponse open_buffer_response = 36;
+        OpenBufferById open_buffer_by_id = 35;
+        OpenBufferByPath open_buffer_by_path = 36;
+        OpenBufferResponse open_buffer_response = 37;
         UpdateBuffer update_buffer = 38;
         UpdateBufferFile update_buffer_file = 39;
         SaveBuffer save_buffer = 40;
@@ -79,6 +80,11 @@ message Envelope {
 
         GetUsers get_users = 70;
         GetUsersResponse get_users_response = 71;
+
+        Follow follow = 72;
+        FollowResponse follow_response = 73;
+        UpdateFollowers update_followers = 74;
+        Unfollow unfollow = 75;
     }
 }
 
@@ -241,12 +247,17 @@ message OpenBufferForSymbolResponse {
     Buffer buffer = 1;
 }
 
-message OpenBuffer {
+message OpenBufferByPath {
     uint64 project_id = 1;
     uint64 worktree_id = 2;
     string path = 3;
 }
 
+message OpenBufferById {
+    uint64 project_id = 1;
+    uint64 id = 2;
+}
+
 message OpenBufferResponse {
     Buffer buffer = 1;
 }
@@ -521,8 +532,77 @@ message UpdateContacts {
     repeated Contact contacts = 1;
 }
 
+message UpdateDiagnostics {
+    uint32 replica_id = 1;
+    uint32 lamport_timestamp = 2;
+    repeated Diagnostic diagnostics = 3;
+}
+
+message Follow {
+    uint64 project_id = 1;
+    uint32 leader_id = 2;
+}
+
+message FollowResponse {
+    optional uint64 active_view_id = 1;
+    repeated View views = 2;
+}
+
+message UpdateFollowers {
+    uint64 project_id = 1;
+    repeated uint32 follower_ids = 2;
+    oneof variant {
+        UpdateActiveView update_active_view = 3;
+        View create_view = 4;
+        UpdateView update_view = 5;
+    }
+}
+
+message Unfollow {
+    uint64 project_id = 1;
+    uint32 leader_id = 2;
+}
+
 // Entities
 
+message UpdateActiveView {
+    optional uint64 id = 1;
+    optional uint32 leader_id = 2;
+}
+
+message UpdateView {
+    uint64 id = 1;
+    optional uint32 leader_id = 2;
+
+    oneof variant {
+        Editor editor = 3;
+    }
+
+    message Editor {
+        repeated Selection selections = 1;
+        Anchor scroll_top_anchor = 2;
+        float scroll_x = 3;
+        float scroll_y = 4;
+    }
+}
+
+message View {
+    uint64 id = 1;
+    optional uint32 leader_id = 2;
+
+    oneof variant {
+        Editor editor = 3;
+    }
+
+    message Editor {
+        uint64 buffer_id = 1;
+        repeated Selection selections = 2;
+        Anchor scroll_top_anchor = 3;
+        float scroll_x = 4;
+        float scroll_y = 5;
+    }
+}
+
 message Collaborator {
     uint32 peer_id = 1;
     uint32 replica_id = 2;
@@ -578,17 +658,6 @@ message BufferState {
     repeated string completion_triggers = 8;
 }
 
-message BufferFragment {
-    uint32 replica_id = 1;
-    uint32 local_timestamp = 2;
-    uint32 lamport_timestamp = 3;
-    uint32 insertion_offset = 4;
-    uint32 len = 5;
-    bool visible = 6;
-    repeated VectorClockEntry deletions = 7;
-    repeated VectorClockEntry max_undos = 8;
-}
-
 message SelectionSet {
     uint32 replica_id = 1;
     repeated Selection selections = 2;
@@ -614,12 +683,6 @@ enum Bias {
     Right = 1;
 }
 
-message UpdateDiagnostics {
-    uint32 replica_id = 1;
-    uint32 lamport_timestamp = 2;
-    repeated Diagnostic diagnostics = 3;
-}
-
 message Diagnostic {
     Anchor start = 1;
     Anchor end = 2;

crates/rpc/src/proto.rs 🔗

@@ -147,6 +147,8 @@ messages!(
     (BufferSaved, Foreground),
     (ChannelMessageSent, Foreground),
     (Error, Foreground),
+    (Follow, Foreground),
+    (FollowResponse, Foreground),
     (FormatBuffers, Foreground),
     (FormatBuffersResponse, Foreground),
     (GetChannelMessages, Foreground),
@@ -175,7 +177,8 @@ messages!(
     (UpdateLanguageServer, Foreground),
     (LeaveChannel, Foreground),
     (LeaveProject, Foreground),
-    (OpenBuffer, Background),
+    (OpenBufferById, Background),
+    (OpenBufferByPath, Background),
     (OpenBufferForSymbol, Background),
     (OpenBufferForSymbolResponse, Background),
     (OpenBufferResponse, Background),
@@ -195,13 +198,15 @@ messages!(
     (SendChannelMessageResponse, Foreground),
     (ShareProject, Foreground),
     (Test, Foreground),
+    (Unfollow, Foreground),
     (UnregisterProject, Foreground),
     (UnregisterWorktree, Foreground),
     (UnshareProject, Foreground),
-    (UpdateBuffer, Background),
+    (UpdateBuffer, Foreground),
     (UpdateBufferFile, Foreground),
     (UpdateContacts, Foreground),
     (UpdateDiagnosticSummary, Foreground),
+    (UpdateFollowers, Foreground),
     (UpdateWorktree, Foreground),
 );
 
@@ -211,6 +216,7 @@ request_messages!(
         ApplyCompletionAdditionalEdits,
         ApplyCompletionAdditionalEditsResponse
     ),
+    (Follow, FollowResponse),
     (FormatBuffers, FormatBuffersResponse),
     (GetChannelMessages, GetChannelMessagesResponse),
     (GetChannels, GetChannelsResponse),
@@ -223,7 +229,8 @@ request_messages!(
     (GetUsers, GetUsersResponse),
     (JoinChannel, JoinChannelResponse),
     (JoinProject, JoinProjectResponse),
-    (OpenBuffer, OpenBufferResponse),
+    (OpenBufferById, OpenBufferResponse),
+    (OpenBufferByPath, OpenBufferResponse),
     (OpenBufferForSymbol, OpenBufferForSymbolResponse),
     (Ping, Ack),
     (PerformRename, PerformRenameResponse),
@@ -246,6 +253,7 @@ entity_messages!(
     ApplyCompletionAdditionalEdits,
     BufferReloaded,
     BufferSaved,
+    Follow,
     FormatBuffers,
     GetCodeActions,
     GetCompletions,
@@ -255,7 +263,8 @@ entity_messages!(
     GetProjectSymbols,
     JoinProject,
     LeaveProject,
-    OpenBuffer,
+    OpenBufferById,
+    OpenBufferByPath,
     OpenBufferForSymbol,
     PerformRename,
     PrepareRename,
@@ -263,11 +272,13 @@ entity_messages!(
     SaveBuffer,
     SearchProject,
     StartLanguageServer,
+    Unfollow,
     UnregisterWorktree,
     UnshareProject,
     UpdateBuffer,
     UpdateBufferFile,
     UpdateDiagnosticSummary,
+    UpdateFollowers,
     UpdateLanguageServer,
     RegisterWorktree,
     UpdateWorktree,

crates/rpc/src/rpc.rs 🔗

@@ -5,4 +5,4 @@ pub mod proto;
 pub use conn::Connection;
 pub use peer::*;
 
-pub const PROTOCOL_VERSION: u32 = 11;
+pub const PROTOCOL_VERSION: u32 = 12;

crates/search/src/buffer_search.rs 🔗

@@ -360,7 +360,7 @@ impl SearchBar {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            editor::Event::Edited => {
+            editor::Event::Edited { .. } => {
                 self.query_contains_error = false;
                 self.clear_matches(cx);
                 self.update_matches(true, cx);
@@ -377,8 +377,8 @@ impl SearchBar {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            editor::Event::Edited => self.update_matches(false, cx),
-            editor::Event::SelectionsChanged => self.update_match_index(cx),
+            editor::Event::Edited { .. } => self.update_matches(false, cx),
+            editor::Event::SelectionsChanged { .. } => self.update_match_index(cx),
             _ => {}
         }
     }

crates/search/src/project_search.rs 🔗

@@ -250,6 +250,10 @@ impl Item for ProjectSearchView {
         None
     }
 
+    fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
+        None
+    }
+
     fn can_save(&self, _: &gpui::AppContext) -> bool {
         true
     }
@@ -346,7 +350,7 @@ impl ProjectSearchView {
         cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
             .detach();
         cx.subscribe(&results_editor, |this, _, event, cx| {
-            if matches!(event, editor::Event::SelectionsChanged) {
+            if matches!(event, editor::Event::SelectionsChanged { .. }) {
                 this.update_match_index(cx);
             }
         })

crates/server/src/rpc.rs 🔗

@@ -92,7 +92,8 @@ impl Server {
             .add_request_handler(Server::forward_project_request::<proto::GetDocumentHighlights>)
             .add_request_handler(Server::forward_project_request::<proto::GetProjectSymbols>)
             .add_request_handler(Server::forward_project_request::<proto::OpenBufferForSymbol>)
-            .add_request_handler(Server::forward_project_request::<proto::OpenBuffer>)
+            .add_request_handler(Server::forward_project_request::<proto::OpenBufferById>)
+            .add_request_handler(Server::forward_project_request::<proto::OpenBufferByPath>)
             .add_request_handler(Server::forward_project_request::<proto::GetCompletions>)
             .add_request_handler(
                 Server::forward_project_request::<proto::ApplyCompletionAdditionalEdits>,
@@ -112,6 +113,9 @@ impl Server {
             .add_request_handler(Server::join_channel)
             .add_message_handler(Server::leave_channel)
             .add_request_handler(Server::send_channel_message)
+            .add_request_handler(Server::follow)
+            .add_message_handler(Server::unfollow)
+            .add_message_handler(Server::update_followers)
             .add_request_handler(Server::get_channel_messages);
 
         Arc::new(server)
@@ -669,6 +673,72 @@ impl Server {
         Ok(())
     }
 
+    async fn follow(
+        self: Arc<Self>,
+        request: TypedEnvelope<proto::Follow>,
+    ) -> tide::Result<proto::FollowResponse> {
+        let leader_id = ConnectionId(request.payload.leader_id);
+        let follower_id = request.sender_id;
+        if !self
+            .state()
+            .project_connection_ids(request.payload.project_id, follower_id)?
+            .contains(&leader_id)
+        {
+            Err(anyhow!("no such peer"))?;
+        }
+        let mut response = self
+            .peer
+            .forward_request(request.sender_id, leader_id, request.payload)
+            .await?;
+        response
+            .views
+            .retain(|view| view.leader_id != Some(follower_id.0));
+        Ok(response)
+    }
+
+    async fn unfollow(
+        self: Arc<Self>,
+        request: TypedEnvelope<proto::Unfollow>,
+    ) -> tide::Result<()> {
+        let leader_id = ConnectionId(request.payload.leader_id);
+        if !self
+            .state()
+            .project_connection_ids(request.payload.project_id, request.sender_id)?
+            .contains(&leader_id)
+        {
+            Err(anyhow!("no such peer"))?;
+        }
+        self.peer
+            .forward_send(request.sender_id, leader_id, request.payload)?;
+        Ok(())
+    }
+
+    async fn update_followers(
+        self: Arc<Self>,
+        request: TypedEnvelope<proto::UpdateFollowers>,
+    ) -> tide::Result<()> {
+        let connection_ids = self
+            .state()
+            .project_connection_ids(request.payload.project_id, request.sender_id)?;
+        let leader_id = request
+            .payload
+            .variant
+            .as_ref()
+            .and_then(|variant| match variant {
+                proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
+                proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
+                proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
+            });
+        for follower_id in &request.payload.follower_ids {
+            let follower_id = ConnectionId(*follower_id);
+            if connection_ids.contains(&follower_id) && Some(follower_id.0) != leader_id {
+                self.peer
+                    .forward_send(request.sender_id, follower_id, request.payload.clone())?;
+            }
+        }
+        Ok(())
+    }
+
     async fn get_channels(
         self: Arc<Server>,
         request: TypedEnvelope<proto::GetChannels>,
@@ -1016,7 +1086,7 @@ mod tests {
         self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename,
         ToOffset, ToggleCodeActions, Undo,
     };
-    use gpui::{executor, ModelHandle, TestAppContext};
+    use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle};
     use language::{
         tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry,
         LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition,
@@ -1028,7 +1098,7 @@ mod tests {
         fs::{FakeFs, Fs as _},
         search::SearchQuery,
         worktree::WorktreeHandle,
-        DiagnosticSummary, Project, ProjectPath,
+        DiagnosticSummary, Project, ProjectPath, WorktreeId,
     };
     use rand::prelude::*;
     use rpc::PeerId;
@@ -1046,7 +1116,7 @@ mod tests {
         },
         time::Duration,
     };
-    use workspace::{Settings, Workspace, WorkspaceParams};
+    use workspace::{Item, Settings, SplitDirection, Workspace, WorkspaceParams};
 
     #[cfg(test)]
     #[ctor::ctor]
@@ -3225,7 +3295,7 @@ mod tests {
         let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
         let editor_b = workspace_b
             .update(cx_b, |workspace, cx| {
-                workspace.open_path((worktree_id, "main.rs").into(), cx)
+                workspace.open_path((worktree_id, "main.rs"), cx)
             })
             .await
             .unwrap()
@@ -3459,7 +3529,7 @@ mod tests {
         let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
         let editor_b = workspace_b
             .update(cx_b, |workspace, cx| {
-                workspace.open_path((worktree_id, "one.rs").into(), cx)
+                workspace.open_path((worktree_id, "one.rs"), cx)
             })
             .await
             .unwrap()
@@ -4148,6 +4218,494 @@ mod tests {
         }
     }
 
+    #[gpui::test(iterations = 10)]
+    async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+        cx_a.foreground().forbid_parking();
+        let fs = FakeFs::new(cx_a.background());
+
+        // 2 clients connect to a server.
+        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+        let mut client_a = server.create_client(cx_a, "user_a").await;
+        let mut client_b = server.create_client(cx_b, "user_b").await;
+        cx_a.update(editor::init);
+        cx_b.update(editor::init);
+
+        // Client A shares a project.
+        fs.insert_tree(
+            "/a",
+            json!({
+                ".zed.toml": r#"collaborators = ["user_b"]"#,
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+            }),
+        )
+        .await;
+        let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
+        project_a
+            .update(cx_a, |project, cx| project.share(cx))
+            .await
+            .unwrap();
+
+        // Client B joins the project.
+        let project_b = client_b
+            .build_remote_project(
+                project_a
+                    .read_with(cx_a, |project, _| project.remote_id())
+                    .unwrap(),
+                cx_b,
+            )
+            .await;
+
+        // Client A opens some editors.
+        let workspace_a = client_a.build_workspace(&project_a, cx_a);
+        let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+        let editor_a1 = workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.open_path((worktree_id, "1.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+        let editor_a2 = workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.open_path((worktree_id, "2.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        // Client B opens an editor.
+        let workspace_b = client_b.build_workspace(&project_b, cx_b);
+        let editor_b1 = workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.open_path((worktree_id, "1.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        let client_a_id = project_b.read_with(cx_b, |project, _| {
+            project.collaborators().values().next().unwrap().peer_id
+        });
+        let client_b_id = project_a.read_with(cx_a, |project, _| {
+            project.collaborators().values().next().unwrap().peer_id
+        });
+
+        // When client B starts following client A, all visible view states are replicated to client B.
+        editor_a1.update(cx_a, |editor, cx| editor.select_ranges([0..1], None, cx));
+        editor_a2.update(cx_a, |editor, cx| editor.select_ranges([2..3], None, cx));
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.toggle_follow(&client_a_id.into(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+            workspace
+                .active_item(cx)
+                .unwrap()
+                .downcast::<Editor>()
+                .unwrap()
+        });
+        assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
+        assert_eq!(
+            editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
+            Some((worktree_id, "2.txt").into())
+        );
+        assert_eq!(
+            editor_b2.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)),
+            vec![2..3]
+        );
+        assert_eq!(
+            editor_b1.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)),
+            vec![0..1]
+        );
+
+        // When client A activates a different editor, client B does so as well.
+        workspace_a.update(cx_a, |workspace, cx| {
+            workspace.activate_item(&editor_a1, cx)
+        });
+        workspace_b
+            .condition(cx_b, |workspace, cx| {
+                workspace.active_item(cx).unwrap().id() == editor_b1.id()
+            })
+            .await;
+
+        // Changes to client A's editor are reflected on client B.
+        editor_a1.update(cx_a, |editor, cx| {
+            editor.select_ranges([1..1, 2..2], None, cx);
+        });
+        editor_b1
+            .condition(cx_b, |editor, cx| {
+                editor.selected_ranges(cx) == vec![1..1, 2..2]
+            })
+            .await;
+
+        editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
+        editor_b1
+            .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
+            .await;
+
+        editor_a1.update(cx_a, |editor, cx| {
+            editor.select_ranges([3..3], None, cx);
+            editor.set_scroll_position(vec2f(0., 100.), cx);
+        });
+        editor_b1
+            .condition(cx_b, |editor, cx| editor.selected_ranges(cx) == vec![3..3])
+            .await;
+
+        // After unfollowing, client B stops receiving updates from client A.
+        workspace_b.update(cx_b, |workspace, cx| {
+            workspace.unfollow(&workspace.active_pane().clone(), cx)
+        });
+        workspace_a.update(cx_a, |workspace, cx| {
+            workspace.activate_item(&editor_a2, cx)
+        });
+        cx_a.foreground().run_until_parked();
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, cx| workspace
+                .active_item(cx)
+                .unwrap()
+                .id()),
+            editor_b1.id()
+        );
+
+        // Client A starts following client B.
+        workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.toggle_follow(&client_b_id.into(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+            Some(client_b_id)
+        );
+        assert_eq!(
+            workspace_a.read_with(cx_a, |workspace, cx| workspace
+                .active_item(cx)
+                .unwrap()
+                .id()),
+            editor_a1.id()
+        );
+
+        // Following interrupts when client B disconnects.
+        client_b.disconnect(&cx_b.to_async()).unwrap();
+        cx_a.foreground().run_until_parked();
+        assert_eq!(
+            workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+            None
+        );
+    }
+
+    #[gpui::test(iterations = 10)]
+    async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+        cx_a.foreground().forbid_parking();
+        let fs = FakeFs::new(cx_a.background());
+
+        // 2 clients connect to a server.
+        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+        let mut client_a = server.create_client(cx_a, "user_a").await;
+        let mut client_b = server.create_client(cx_b, "user_b").await;
+        cx_a.update(editor::init);
+        cx_b.update(editor::init);
+
+        // Client A shares a project.
+        fs.insert_tree(
+            "/a",
+            json!({
+                ".zed.toml": r#"collaborators = ["user_b"]"#,
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+                "4.txt": "four",
+            }),
+        )
+        .await;
+        let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
+        project_a
+            .update(cx_a, |project, cx| project.share(cx))
+            .await
+            .unwrap();
+
+        // Client B joins the project.
+        let project_b = client_b
+            .build_remote_project(
+                project_a
+                    .read_with(cx_a, |project, _| project.remote_id())
+                    .unwrap(),
+                cx_b,
+            )
+            .await;
+
+        // Client A opens some editors.
+        let workspace_a = client_a.build_workspace(&project_a, cx_a);
+        let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+        let _editor_a1 = workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.open_path((worktree_id, "1.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        // Client B opens an editor.
+        let workspace_b = client_b.build_workspace(&project_b, cx_b);
+        let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+        let _editor_b1 = workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.open_path((worktree_id, "2.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        // Clients A and B follow each other in split panes
+        workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+                assert_ne!(*workspace.active_pane(), pane_a1);
+                let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
+                workspace
+                    .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
+                    .unwrap()
+            })
+            .await
+            .unwrap();
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
+                assert_ne!(*workspace.active_pane(), pane_b1);
+                let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
+                workspace
+                    .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
+                    .unwrap()
+            })
+            .await
+            .unwrap();
+
+        workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.activate_next_pane(cx);
+                assert_eq!(*workspace.active_pane(), pane_a1);
+                workspace.open_path((worktree_id, "3.txt"), cx)
+            })
+            .await
+            .unwrap();
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.activate_next_pane(cx);
+                assert_eq!(*workspace.active_pane(), pane_b1);
+                workspace.open_path((worktree_id, "4.txt"), cx)
+            })
+            .await
+            .unwrap();
+        cx_a.foreground().run_until_parked();
+
+        // Ensure leader updates don't change the active pane of followers
+        workspace_a.read_with(cx_a, |workspace, _| {
+            assert_eq!(*workspace.active_pane(), pane_a1);
+        });
+        workspace_b.read_with(cx_b, |workspace, _| {
+            assert_eq!(*workspace.active_pane(), pane_b1);
+        });
+
+        // Ensure peers following each other doesn't cause an infinite loop.
+        assert_eq!(
+            workspace_a.read_with(cx_a, |workspace, cx| workspace
+                .active_item(cx)
+                .unwrap()
+                .project_path(cx)),
+            Some((worktree_id, "3.txt").into())
+        );
+        workspace_a.update(cx_a, |workspace, cx| {
+            assert_eq!(
+                workspace.active_item(cx).unwrap().project_path(cx),
+                Some((worktree_id, "3.txt").into())
+            );
+            workspace.activate_next_pane(cx);
+            assert_eq!(
+                workspace.active_item(cx).unwrap().project_path(cx),
+                Some((worktree_id, "4.txt").into())
+            );
+        });
+        workspace_b.update(cx_b, |workspace, cx| {
+            assert_eq!(
+                workspace.active_item(cx).unwrap().project_path(cx),
+                Some((worktree_id, "4.txt").into())
+            );
+            workspace.activate_next_pane(cx);
+            assert_eq!(
+                workspace.active_item(cx).unwrap().project_path(cx),
+                Some((worktree_id, "3.txt").into())
+            );
+        });
+    }
+
+    #[gpui::test(iterations = 10)]
+    async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+        cx_a.foreground().forbid_parking();
+        let fs = FakeFs::new(cx_a.background());
+
+        // 2 clients connect to a server.
+        let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+        let mut client_a = server.create_client(cx_a, "user_a").await;
+        let mut client_b = server.create_client(cx_b, "user_b").await;
+        cx_a.update(editor::init);
+        cx_b.update(editor::init);
+
+        // Client A shares a project.
+        fs.insert_tree(
+            "/a",
+            json!({
+                ".zed.toml": r#"collaborators = ["user_b"]"#,
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+            }),
+        )
+        .await;
+        let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await;
+        project_a
+            .update(cx_a, |project, cx| project.share(cx))
+            .await
+            .unwrap();
+
+        // Client B joins the project.
+        let project_b = client_b
+            .build_remote_project(
+                project_a
+                    .read_with(cx_a, |project, _| project.remote_id())
+                    .unwrap(),
+                cx_b,
+            )
+            .await;
+
+        // Client A opens some editors.
+        let workspace_a = client_a.build_workspace(&project_a, cx_a);
+        let _editor_a1 = workspace_a
+            .update(cx_a, |workspace, cx| {
+                workspace.open_path((worktree_id, "1.txt"), cx)
+            })
+            .await
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap();
+
+        // Client B starts following client A.
+        let workspace_b = client_b.build_workspace(&project_b, cx_b);
+        let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+        let leader_id = project_b.read_with(cx_b, |project, _| {
+            project.collaborators().values().next().unwrap().peer_id
+        });
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            Some(leader_id)
+        );
+        let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+            workspace
+                .active_item(cx)
+                .unwrap()
+                .downcast::<Editor>()
+                .unwrap()
+        });
+
+        // When client B moves, it automatically stops following client A.
+        editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            None
+        );
+
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            Some(leader_id)
+        );
+
+        // When client B edits, it automatically stops following client A.
+        editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            None
+        );
+
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            Some(leader_id)
+        );
+
+        // When client B scrolls, it automatically stops following client A.
+        editor_b2.update(cx_b, |editor, cx| {
+            editor.set_scroll_position(vec2f(0., 3.), cx)
+        });
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            None
+        );
+
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.toggle_follow(&leader_id.into(), cx).unwrap()
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            Some(leader_id)
+        );
+
+        // When client B activates a different pane, it continues following client A in the original pane.
+        workspace_b.update(cx_b, |workspace, cx| {
+            workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
+        });
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            Some(leader_id)
+        );
+
+        workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            Some(leader_id)
+        );
+
+        // When client B activates a different item in the original pane, it automatically stops following client A.
+        workspace_b
+            .update(cx_b, |workspace, cx| {
+                workspace.open_path((worktree_id, "2.txt"), cx)
+            })
+            .await
+            .unwrap();
+        assert_eq!(
+            workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+            None
+        );
+    }
+
     #[gpui::test(iterations = 100)]
     async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) {
         cx.foreground().forbid_parking();
@@ -4469,6 +5027,9 @@ mod tests {
 
             Channel::init(&client);
             Project::init(&client);
+            cx.update(|cx| {
+                workspace::init(&client, cx);
+            });
 
             let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
             let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
@@ -4477,6 +5038,7 @@ mod tests {
                 client,
                 peer_id,
                 user_store,
+                language_registry: Arc::new(LanguageRegistry::test()),
                 project: Default::default(),
                 buffers: Default::default(),
             };
@@ -4541,6 +5103,7 @@ mod tests {
         client: Arc<Client>,
         pub peer_id: PeerId,
         pub user_store: ModelHandle<UserStore>,
+        language_registry: Arc<LanguageRegistry>,
         project: Option<ModelHandle<Project>>,
         buffers: HashSet<ModelHandle<language::Buffer>>,
     }
@@ -4568,6 +5131,80 @@ mod tests {
             while authed_user.next().await.unwrap().is_none() {}
         }
 
+        async fn build_local_project(
+            &mut self,
+            fs: Arc<FakeFs>,
+            root_path: impl AsRef<Path>,
+            cx: &mut TestAppContext,
+        ) -> (ModelHandle<Project>, WorktreeId) {
+            let project = cx.update(|cx| {
+                Project::local(
+                    self.client.clone(),
+                    self.user_store.clone(),
+                    self.language_registry.clone(),
+                    fs,
+                    cx,
+                )
+            });
+            self.project = Some(project.clone());
+            let (worktree, _) = project
+                .update(cx, |p, cx| {
+                    p.find_or_create_local_worktree(root_path, true, cx)
+                })
+                .await
+                .unwrap();
+            worktree
+                .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
+                .await;
+            project
+                .update(cx, |project, _| project.next_remote_id())
+                .await;
+            (project, worktree.read_with(cx, |tree, _| tree.id()))
+        }
+
+        async fn build_remote_project(
+            &mut self,
+            project_id: u64,
+            cx: &mut TestAppContext,
+        ) -> ModelHandle<Project> {
+            let project = Project::remote(
+                project_id,
+                self.client.clone(),
+                self.user_store.clone(),
+                self.language_registry.clone(),
+                FakeFs::new(cx.background()),
+                &mut cx.to_async(),
+            )
+            .await
+            .unwrap();
+            self.project = Some(project.clone());
+            project
+        }
+
+        fn build_workspace(
+            &self,
+            project: &ModelHandle<Project>,
+            cx: &mut TestAppContext,
+        ) -> ViewHandle<Workspace> {
+            let (window_id, _) = cx.add_window(|_| EmptyView);
+            cx.add_view(window_id, |cx| {
+                let fs = project.read(cx).fs().clone();
+                Workspace::new(
+                    &WorkspaceParams {
+                        fs,
+                        project: project.clone(),
+                        user_store: self.user_store.clone(),
+                        languages: self.language_registry.clone(),
+                        channel_list: cx.add_model(|cx| {
+                            ChannelList::new(self.user_store.clone(), self.client.clone(), cx)
+                        }),
+                        client: self.client.clone(),
+                    },
+                    cx,
+                )
+            })
+        }
+
         fn simulate_host(
             mut self,
             project: ModelHandle<Project>,

crates/text/src/selection.rs 🔗

@@ -18,6 +18,12 @@ pub struct Selection<T> {
     pub goal: SelectionGoal,
 }
 
+impl Default for SelectionGoal {
+    fn default() -> Self {
+        Self::None
+    }
+}
+
 impl<T: Clone> Selection<T> {
     pub fn head(&self) -> T {
         if self.reversed {

crates/theme/src/theme.rs 🔗

@@ -35,6 +35,8 @@ pub struct Workspace {
     pub tab: Tab,
     pub active_tab: Tab,
     pub pane_divider: Border,
+    pub leader_border_opacity: f32,
+    pub leader_border_width: f32,
     pub left_sidebar: Sidebar,
     pub right_sidebar: Sidebar,
     pub status_bar: StatusBar,

crates/theme_selector/src/theme_selector.rs 🔗

@@ -204,7 +204,7 @@ impl ThemeSelector {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
-            editor::Event::Edited => {
+            editor::Event::Edited { .. } => {
                 self.update_matches(cx);
                 self.select_if_matching(&cx.global::<Settings>().theme.name);
                 self.show_selected_theme(cx);

crates/workspace/src/pane.rs 🔗

@@ -7,8 +7,8 @@ use gpui::{
     geometry::{rect::RectF, vector::vec2f},
     keymap::Binding,
     platform::{CursorStyle, NavigationDirection},
-    AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
-    ViewHandle, WeakViewHandle,
+    AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
 use project::{ProjectEntryId, ProjectPath};
 use std::{
@@ -33,7 +33,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
-        pane.activate_item(action.0, cx);
+        pane.activate_item(action.0, true, cx);
     });
     cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
         pane.activate_prev_item(cx);
@@ -92,12 +92,13 @@ pub fn init(cx: &mut MutableAppContext) {
 
 pub enum Event {
     Activate,
+    ActivateItem { local: bool },
     Remove,
     Split(SplitDirection),
 }
 
 pub struct Pane {
-    items: Vec<(Option<ProjectEntryId>, Box<dyn ItemHandle>)>,
+    items: Vec<Box<dyn ItemHandle>>,
     active_item_index: usize,
     nav_history: Rc<RefCell<NavHistory>>,
     toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
@@ -256,9 +257,19 @@ impl Pane {
                 let task = task.await;
                 if let Some(pane) = pane.upgrade(&cx) {
                     if let Some((project_entry_id, build_item)) = task.log_err() {
-                        pane.update(&mut cx, |pane, cx| {
+                        pane.update(&mut cx, |pane, _| {
                             pane.nav_history.borrow_mut().set_mode(mode);
-                            let item = pane.open_item(project_entry_id, cx, build_item);
+                        });
+                        let item = workspace.update(&mut cx, |workspace, cx| {
+                            Self::open_item(
+                                workspace,
+                                pane.clone(),
+                                project_entry_id,
+                                cx,
+                                build_item,
+                            )
+                        });
+                        pane.update(&mut cx, |pane, cx| {
                             pane.nav_history
                                 .borrow_mut()
                                 .set_mode(NavigationMode::Normal);
@@ -280,63 +291,91 @@ impl Pane {
         }
     }
 
-    pub fn open_item(
-        &mut self,
+    pub(crate) fn open_item(
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
         project_entry_id: ProjectEntryId,
-        cx: &mut ViewContext<Self>,
+        cx: &mut ViewContext<Workspace>,
         build_item: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
     ) -> Box<dyn ItemHandle> {
-        for (ix, (existing_entry_id, item)) in self.items.iter().enumerate() {
-            if *existing_entry_id == Some(project_entry_id) {
-                let item = item.boxed_clone();
-                self.activate_item(ix, cx);
-                return item;
+        let existing_item = pane.update(cx, |pane, cx| {
+            for (ix, item) in pane.items.iter().enumerate() {
+                if item.project_entry_id(cx) == Some(project_entry_id) {
+                    let item = item.boxed_clone();
+                    pane.activate_item(ix, true, cx);
+                    return Some(item);
+                }
             }
+            None
+        });
+        if let Some(existing_item) = existing_item {
+            existing_item
+        } else {
+            let item = build_item(cx);
+            Self::add_item(workspace, pane, item.boxed_clone(), true, cx);
+            item
         }
-
-        let item = build_item(cx);
-        self.add_item(Some(project_entry_id), item.boxed_clone(), cx);
-        item
     }
 
     pub(crate) fn add_item(
-        &mut self,
-        project_entry_id: Option<ProjectEntryId>,
-        mut item: Box<dyn ItemHandle>,
-        cx: &mut ViewContext<Self>,
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        item: Box<dyn ItemHandle>,
+        local: bool,
+        cx: &mut ViewContext<Workspace>,
     ) {
-        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.items.len());
-        self.items.insert(item_idx, (project_entry_id, item));
-        self.activate_item(item_idx, cx);
-        cx.notify();
+        // Prevent adding the same item to the pane more than once.
+        if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
+            pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx));
+            return;
+        }
+
+        item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
+        item.added_to_pane(workspace, pane.clone(), cx);
+        pane.update(cx, |pane, cx| {
+            let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len());
+            pane.items.insert(item_idx, item);
+            pane.activate_item(item_idx, local, cx);
+            cx.notify();
+        });
     }
 
     pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
-        self.items.iter().map(|(_, view)| view)
+        self.items.iter()
     }
 
-    pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
+    pub fn items_of_type<'a, T: View>(&'a self) -> impl 'a + Iterator<Item = ViewHandle<T>> {
         self.items
-            .get(self.active_item_index)
-            .map(|(_, view)| view.clone())
+            .iter()
+            .filter_map(|item| item.to_any().downcast())
     }
 
-    pub fn project_entry_id_for_item(&self, item: &dyn ItemHandle) -> Option<ProjectEntryId> {
-        self.items.iter().find_map(|(entry_id, existing)| {
+    pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
+        self.items.get(self.active_item_index).cloned()
+    }
+
+    pub fn project_entry_id_for_item(
+        &self,
+        item: &dyn ItemHandle,
+        cx: &AppContext,
+    ) -> Option<ProjectEntryId> {
+        self.items.iter().find_map(|existing| {
             if existing.id() == item.id() {
-                *entry_id
+                existing.project_entry_id(cx)
             } else {
                 None
             }
         })
     }
 
-    pub fn item_for_entry(&self, entry_id: ProjectEntryId) -> Option<Box<dyn ItemHandle>> {
-        self.items.iter().find_map(|(id, view)| {
-            if *id == Some(entry_id) {
-                Some(view.boxed_clone())
+    pub fn item_for_entry(
+        &self,
+        entry_id: ProjectEntryId,
+        cx: &AppContext,
+    ) -> Option<Box<dyn ItemHandle>> {
+        self.items.iter().find_map(|item| {
+            if item.project_entry_id(cx) == Some(entry_id) {
+                Some(item.boxed_clone())
             } else {
                 None
             }
@@ -344,20 +383,23 @@ impl Pane {
     }
 
     pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
-        self.items.iter().position(|(_, i)| i.id() == item.id())
+        self.items.iter().position(|i| i.id() == item.id())
     }
 
-    pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
+    pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext<Self>) {
         if index < self.items.len() {
             let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
             if prev_active_item_ix != self.active_item_index
                 && prev_active_item_ix < self.items.len()
             {
-                self.items[prev_active_item_ix].1.deactivated(cx);
+                self.items[prev_active_item_ix].deactivated(cx);
+                cx.emit(Event::ActivateItem { local });
             }
             self.update_active_toolbar(cx);
-            self.focus_active_item(cx);
-            self.activate(cx);
+            if local {
+                self.focus_active_item(cx);
+                self.activate(cx);
+            }
             cx.notify();
         }
     }
@@ -369,7 +411,7 @@ impl Pane {
         } else if self.items.len() > 0 {
             index = self.items.len() - 1;
         }
-        self.activate_item(index, cx);
+        self.activate_item(index, true, cx);
     }
 
     pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
@@ -379,18 +421,18 @@ impl Pane {
         } else {
             index = 0;
         }
-        self.activate_item(index, cx);
+        self.activate_item(index, true, cx);
     }
 
     pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
         if !self.items.is_empty() {
-            self.close_item(self.items[self.active_item_index].1.id(), cx)
+            self.close_item(self.items[self.active_item_index].id(), cx)
         }
     }
 
     pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) {
         if !self.items.is_empty() {
-            let active_item_id = self.items[self.active_item_index].1.id();
+            let active_item_id = self.items[self.active_item_index].id();
             self.close_items(cx, |id| id != active_item_id);
         }
     }
@@ -406,7 +448,7 @@ impl Pane {
     ) {
         let mut item_ix = 0;
         let mut new_active_item_index = self.active_item_index;
-        self.items.retain(|(_, item)| {
+        self.items.retain(|item| {
             if should_close(item.id()) {
                 if item_ix == self.active_item_index {
                     item.deactivated(cx);
@@ -443,7 +485,7 @@ impl Pane {
         cx.notify();
     }
 
-    fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
+    pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
         if let Some(active_item) = self.active_item() {
             cx.focus(active_item);
         }
@@ -505,7 +547,7 @@ impl Pane {
     fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
         let active_item = self.items.get(self.active_item_index);
         for (toolbar_type_id, toolbar) in &self.toolbars {
-            let visible = toolbar.active_item_changed(active_item.map(|i| i.1.clone()), cx);
+            let visible = toolbar.active_item_changed(active_item.cloned(), cx);
             if Some(*toolbar_type_id) == self.active_toolbar_type {
                 self.active_toolbar_visible = visible;
             }
@@ -518,7 +560,7 @@ impl Pane {
         enum Tabs {}
         let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
             let mut row = Flex::row();
-            for (ix, (_, item)) in self.items.iter().enumerate() {
+            for (ix, item) in self.items.iter().enumerate() {
                 let is_active = ix == self.active_item_index;
 
                 row.add_child({

crates/workspace/src/pane_group.rs 🔗

@@ -1,9 +1,11 @@
+use crate::{FollowerStatesByLeader, Pane};
 use anyhow::{anyhow, Result};
-use gpui::{elements::*, Axis, ViewHandle};
+use client::PeerId;
+use collections::HashMap;
+use gpui::{elements::*, Axis, Border, ViewHandle};
+use project::Collaborator;
 use theme::Theme;
 
-use crate::Pane;
-
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct PaneGroup {
     root: Member,
@@ -47,8 +49,19 @@ impl PaneGroup {
         }
     }
 
-    pub fn render<'a>(&self, theme: &Theme) -> ElementBox {
-        self.root.render(theme)
+    pub(crate) fn render<'a>(
+        &self,
+        theme: &Theme,
+        follower_states: &FollowerStatesByLeader,
+        collaborators: &HashMap<PeerId, Collaborator>,
+    ) -> ElementBox {
+        self.root.render(theme, follower_states, collaborators)
+    }
+
+    pub(crate) fn panes(&self) -> Vec<&ViewHandle<Pane>> {
+        let mut panes = Vec::new();
+        self.root.collect_panes(&mut panes);
+        panes
     }
 }
 
@@ -80,10 +93,50 @@ impl Member {
         Member::Axis(PaneAxis { axis, members })
     }
 
-    pub fn render(&self, theme: &Theme) -> ElementBox {
+    pub fn render(
+        &self,
+        theme: &Theme,
+        follower_states: &FollowerStatesByLeader,
+        collaborators: &HashMap<PeerId, Collaborator>,
+    ) -> ElementBox {
         match self {
-            Member::Pane(pane) => ChildView::new(pane).boxed(),
-            Member::Axis(axis) => axis.render(theme),
+            Member::Pane(pane) => {
+                let mut border = Border::default();
+                let leader = follower_states
+                    .iter()
+                    .find_map(|(leader_id, follower_states)| {
+                        if follower_states.contains_key(pane) {
+                            Some(leader_id)
+                        } else {
+                            None
+                        }
+                    })
+                    .and_then(|leader_id| collaborators.get(leader_id));
+                if let Some(leader) = leader {
+                    let leader_color = theme
+                        .editor
+                        .replica_selection_style(leader.replica_id)
+                        .cursor;
+                    border = Border::all(theme.workspace.leader_border_width, leader_color);
+                    border
+                        .color
+                        .fade_out(1. - theme.workspace.leader_border_opacity);
+                    border.overlay = true;
+                }
+                ChildView::new(pane).contained().with_border(border).boxed()
+            }
+            Member::Axis(axis) => axis.render(theme, follower_states, collaborators),
+        }
+    }
+
+    fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle<Pane>>) {
+        match self {
+            Member::Axis(axis) => {
+                for member in &axis.members {
+                    member.collect_panes(panes);
+                }
+            }
+            Member::Pane(pane) => panes.push(pane),
         }
     }
 }
@@ -172,11 +225,16 @@ impl PaneAxis {
         }
     }
 
-    fn render<'a>(&self, theme: &Theme) -> ElementBox {
+    fn render(
+        &self,
+        theme: &Theme,
+        follower_state: &FollowerStatesByLeader,
+        collaborators: &HashMap<PeerId, Collaborator>,
+    ) -> ElementBox {
         let last_member_ix = self.members.len() - 1;
         Flex::new(self.axis)
             .with_children(self.members.iter().enumerate().map(|(ix, member)| {
-                let mut member = member.render(theme);
+                let mut member = member.render(theme, follower_state, collaborators);
                 if ix < last_member_ix {
                     let mut border = theme.workspace.pane_divider;
                     border.left = false;

crates/workspace/src/workspace.rs 🔗

@@ -6,10 +6,12 @@ pub mod settings;
 pub mod sidebar;
 mod status_bar;
 
-use anyhow::{anyhow, Result};
-use client::{Authenticate, ChannelList, Client, User, UserStore};
+use anyhow::{anyhow, Context, Result};
+use client::{
+    proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore,
+};
 use clock::ReplicaId;
-use collections::HashMap;
+use collections::{hash_map, HashMap, HashSet};
 use gpui::{
     action,
     color::Color,
@@ -18,9 +20,9 @@ use gpui::{
     json::{self, to_string_pretty, ToJson},
     keymap::Binding,
     platform::{CursorStyle, WindowOptions},
-    AnyModelHandle, AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle,
-    MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext,
-    ViewHandle, WeakViewHandle,
+    AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, ClipboardItem, Entity,
+    ImageData, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task,
+    View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use language::LanguageRegistry;
 use log::error;
@@ -35,34 +37,51 @@ pub use status_bar::StatusItemView;
 use std::{
     any::{Any, TypeId},
     cell::RefCell,
+    fmt,
     future::Future,
     path::{Path, PathBuf},
     rc::Rc,
-    sync::Arc,
+    sync::{
+        atomic::{AtomicBool, Ordering::SeqCst},
+        Arc,
+    },
 };
 use theme::{Theme, ThemeRegistry};
+use util::ResultExt;
 
-type ItemBuilders = HashMap<
+type ProjectItemBuilders = HashMap<
     TypeId,
-    Arc<
-        dyn Fn(
-            usize,
-            ModelHandle<Project>,
-            AnyModelHandle,
-            &mut MutableAppContext,
-        ) -> Box<dyn ItemHandle>,
-    >,
+    fn(usize, ModelHandle<Project>, AnyModelHandle, &mut MutableAppContext) -> Box<dyn ItemHandle>,
+>;
+
+type FollowableItemBuilder = fn(
+    ViewHandle<Pane>,
+    ModelHandle<Project>,
+    &mut Option<proto::view::Variant>,
+    &mut MutableAppContext,
+) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
+type FollowableItemBuilders = HashMap<
+    TypeId,
+    (
+        FollowableItemBuilder,
+        fn(AnyViewHandle) -> Box<dyn FollowableItemHandle>,
+    ),
 >;
 
 action!(Open, Arc<AppState>);
 action!(OpenNew, Arc<AppState>);
 action!(OpenPaths, OpenParams);
 action!(ToggleShare);
+action!(ToggleFollow, PeerId);
+action!(FollowNextCollaborator);
+action!(Unfollow);
 action!(JoinProject, JoinProjectParams);
 action!(Save);
 action!(DebugElements);
+action!(ActivatePreviousPane);
+action!(ActivateNextPane);
 
-pub fn init(cx: &mut MutableAppContext) {
+pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
     pane::init(cx);
     menu::init(cx);
 
@@ -78,6 +97,14 @@ pub fn init(cx: &mut MutableAppContext) {
     });
 
     cx.add_action(Workspace::toggle_share);
+    cx.add_async_action(Workspace::toggle_follow);
+    cx.add_async_action(Workspace::follow_next_collaborator);
+    cx.add_action(
+        |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
+            let pane = workspace.active_pane().clone();
+            workspace.unfollow(&pane, cx);
+        },
+    );
     cx.add_action(
         |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
             workspace.save_active_item(cx).detach_and_log_err(cx);
@@ -86,9 +113,18 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Workspace::debug_elements);
     cx.add_action(Workspace::toggle_sidebar_item);
     cx.add_action(Workspace::toggle_sidebar_item_focus);
+    cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
+        workspace.activate_previous_pane(cx)
+    });
+    cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
+        workspace.activate_next_pane(cx)
+    });
     cx.add_bindings(vec![
+        Binding::new("ctrl-alt-cmd-f", FollowNextCollaborator, None),
         Binding::new("cmd-s", Save, None),
         Binding::new("cmd-alt-i", DebugElements, None),
+        Binding::new("cmd-k cmd-left", ActivatePreviousPane, None),
+        Binding::new("cmd-k cmd-right", ActivateNextPane, None),
         Binding::new(
             "cmd-shift-!",
             ToggleSidebarItem(SidebarItemId {
@@ -106,20 +142,34 @@ pub fn init(cx: &mut MutableAppContext) {
             None,
         ),
     ]);
+
+    client.add_view_request_handler(Workspace::handle_follow);
+    client.add_view_message_handler(Workspace::handle_unfollow);
+    client.add_view_message_handler(Workspace::handle_update_followers);
+}
+
+pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
+    cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
+        builders.insert(TypeId::of::<I::Item>(), |window_id, project, model, cx| {
+            let item = model.downcast::<I::Item>().unwrap();
+            Box::new(cx.add_view(window_id, |cx| I::for_project_item(project, item, cx)))
+        });
+    });
 }
 
-pub fn register_project_item<F, V>(cx: &mut MutableAppContext, build_item: F)
-where
-    V: ProjectItem,
-    F: 'static + Fn(ModelHandle<Project>, ModelHandle<V::Item>, &mut ViewContext<V>) -> V,
-{
-    cx.update_default_global(|builders: &mut ItemBuilders, _| {
+pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
+    cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
         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)))
-            }),
+            TypeId::of::<I>(),
+            (
+                |pane, project, state, cx| {
+                    I::from_state_proto(pane, project, state, cx).map(|task| {
+                        cx.foreground()
+                            .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
+                    })
+                },
+                |this| Box::new(this.downcast::<I>().unwrap()),
+            ),
         );
     });
 }
@@ -156,6 +206,7 @@ pub trait Item: 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
@@ -215,13 +266,101 @@ pub trait ProjectItem: Item {
     ) -> Self;
 }
 
-pub trait ItemHandle: 'static {
+pub trait FollowableItem: Item {
+    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+    fn from_state_proto(
+        pane: ViewHandle<Pane>,
+        project: ModelHandle<Project>,
+        state: &mut Option<proto::view::Variant>,
+        cx: &mut MutableAppContext,
+    ) -> Option<Task<Result<ViewHandle<Self>>>>;
+    fn add_event_to_update_proto(
+        &self,
+        event: &Self::Event,
+        update: &mut Option<proto::update_view::Variant>,
+        cx: &AppContext,
+    ) -> bool;
+    fn apply_update_proto(
+        &mut self,
+        message: proto::update_view::Variant,
+        cx: &mut ViewContext<Self>,
+    ) -> Result<()>;
+
+    fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
+    fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
+}
+
+pub trait FollowableItemHandle: ItemHandle {
+    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(
+        &self,
+        event: &dyn Any,
+        update: &mut Option<proto::update_view::Variant>,
+        cx: &AppContext,
+    ) -> bool;
+    fn apply_update_proto(
+        &self,
+        message: proto::update_view::Variant,
+        cx: &mut MutableAppContext,
+    ) -> Result<()>;
+    fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
+}
+
+impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
+    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)
+        })
+    }
+
+    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+        self.read(cx).to_state_proto(cx)
+    }
+
+    fn add_event_to_update_proto(
+        &self,
+        event: &dyn Any,
+        update: &mut Option<proto::update_view::Variant>,
+        cx: &AppContext,
+    ) -> bool {
+        if let Some(event) = event.downcast_ref() {
+            self.read(cx).add_event_to_update_proto(event, update, cx)
+        } else {
+            false
+        }
+    }
+
+    fn apply_update_proto(
+        &self,
+        message: proto::update_view::Variant,
+        cx: &mut MutableAppContext,
+    ) -> Result<()> {
+        self.update(cx, |this, cx| this.apply_update_proto(message, cx))
+    }
+
+    fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
+        if let Some(event) = event.downcast_ref() {
+            T::should_unfollow_on_event(event, cx)
+        } else {
+            false
+        }
+    }
+}
+
+pub trait ItemHandle: 'static + fmt::Debug {
     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 ItemHandle>;
     fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext);
     fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
-    fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>);
+    fn added_to_pane(
+        &self,
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        cx: &mut ViewContext<Workspace>,
+    );
     fn deactivated(&self, cx: &mut MutableAppContext);
     fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext);
     fn id(&self) -> usize;
@@ -238,6 +377,7 @@ pub trait ItemHandle: 'static {
         cx: &mut MutableAppContext,
     ) -> Task<Result<()>>;
     fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
+    fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
 }
 
 pub trait WeakItemHandle {
@@ -265,15 +405,15 @@ impl<T: Item> ItemHandle 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 ItemHandle> {
         Box::new(self.clone())
     }
 
-    fn clone_on_split(
-        &self,
-        // nav_history: Rc<RefCell<NavHistory>>,
-        cx: &mut MutableAppContext,
-    ) -> Option<Box<dyn ItemHandle>> {
+    fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
         self.update(cx, |item, cx| {
             cx.add_option_view(|cx| item.clone_on_split(cx))
         })
@@ -286,20 +426,81 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         })
     }
 
-    fn added_to_pane(&mut self, cx: &mut ViewContext<Pane>) {
-        cx.subscribe(self, |pane, item, event, cx| {
+    fn added_to_pane(
+        &self,
+        workspace: &mut Workspace,
+        pane: ViewHandle<Pane>,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        if let Some(followed_item) = self.to_followable_item_handle(cx) {
+            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,
+                        variant: Some(message),
+                        leader_id: workspace.leader_for_pane(&pane).map(|id| id.0),
+                    }),
+                    cx,
+                );
+            }
+        }
+
+        let pending_update = Rc::new(RefCell::new(None));
+        let pending_update_scheduled = Rc::new(AtomicBool::new(false));
+        let pane = pane.downgrade();
+        cx.subscribe(self, move |workspace, item, event, cx| {
+            let pane = if let Some(pane) = pane.upgrade(cx) {
+                pane
+            } else {
+                log::error!("unexpected item event after pane was dropped");
+                return;
+            };
+
+            if let Some(item) = item.to_followable_item_handle(cx) {
+                let leader_id = workspace.leader_for_pane(&pane);
+
+                if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
+                    workspace.unfollow(&pane, cx);
+                }
+
+                if item.add_event_to_update_proto(event, &mut *pending_update.borrow_mut(), cx)
+                    && !pending_update_scheduled.load(SeqCst)
+                {
+                    pending_update_scheduled.store(true, SeqCst);
+                    cx.after_window_update({
+                        let pending_update = pending_update.clone();
+                        let pending_update_scheduled = pending_update_scheduled.clone();
+                        move |this, cx| {
+                            pending_update_scheduled.store(false, SeqCst);
+                            this.update_followers(
+                                proto::update_followers::Variant::UpdateView(proto::UpdateView {
+                                    id: item.id() as u64,
+                                    variant: pending_update.borrow_mut().take(),
+                                    leader_id: leader_id.map(|id| id.0),
+                                }),
+                                cx,
+                            );
+                        }
+                    });
+                }
+            }
+
             if T::should_close_item_on_event(event) {
-                pane.close_item(item.id(), cx);
+                pane.update(cx, |pane, cx| pane.close_item(item.id(), cx));
                 return;
             }
+
             if T::should_activate_item_on_event(event) {
-                if let Some(ix) = pane.index_for_item(&item) {
-                    pane.activate_item(ix, cx);
-                    pane.activate(cx);
-                }
+                pane.update(cx, |pane, cx| {
+                    if let Some(ix) = pane.index_for_item(&item) {
+                        pane.activate_item(ix, true, cx);
+                        pane.activate(cx);
+                    }
+                });
             }
+
             if T::should_update_tab_on_event(event) {
-                cx.notify()
+                pane.update(cx, |_, cx| cx.notify());
             }
         })
         .detach();
@@ -353,6 +554,16 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
     fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
         self.read(cx).act_as_type(type_id, self, cx)
     }
+
+    fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
+        if cx.has_global::<FollowableItemBuilders>() {
+            let builders = cx.global::<FollowableItemBuilders>();
+            let item = self.to_any();
+            Some(builders.get(&item.view_type())?.1(item))
+        } else {
+            None
+        }
+    }
 }
 
 impl Into<AnyViewHandle> for Box<dyn ItemHandle> {
@@ -441,6 +652,7 @@ pub struct Workspace {
     weak_self: WeakViewHandle<Self>,
     client: Arc<Client>,
     user_store: ModelHandle<client::UserStore>,
+    remote_entity_subscription: Option<Subscription>,
     fs: Arc<dyn Fs>,
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
@@ -450,9 +662,31 @@ pub struct Workspace {
     active_pane: ViewHandle<Pane>,
     status_bar: ViewHandle<StatusBar>,
     project: ModelHandle<Project>,
+    leader_state: LeaderState,
+    follower_states_by_leader: FollowerStatesByLeader,
+    last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
     _observe_current_user: Task<()>,
 }
 
+#[derive(Default)]
+struct LeaderState {
+    followers: HashSet<PeerId>,
+}
+
+type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
+
+#[derive(Default)]
+struct FollowerState {
+    active_view_id: Option<u64>,
+    items_by_leader_view_id: HashMap<u64, FollowerItem>,
+}
+
+#[derive(Debug)]
+enum FollowerItem {
+    Loading(Vec<proto::update_view::Variant>),
+    Loaded(Box<dyn FollowableItemHandle>),
+}
+
 impl Workspace {
     pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
         cx.observe(&params.project, |_, project, cx| {
@@ -463,6 +697,23 @@ impl Workspace {
         })
         .detach();
 
+        cx.subscribe(&params.project, move |this, project, event, cx| {
+            match event {
+                project::Event::RemoteIdChanged(remote_id) => {
+                    this.project_remote_id_changed(*remote_id, cx);
+                }
+                project::Event::CollaboratorLeft(peer_id) => {
+                    this.collaborator_left(*peer_id, cx);
+                }
+                _ => {}
+            }
+            if project.read(cx).is_read_only() {
+                cx.blur();
+            }
+            cx.notify()
+        })
+        .detach();
+
         let pane = cx.add_view(|_| Pane::new());
         let pane_id = pane.id();
         cx.observe(&pane, move |me, _, cx| {
@@ -499,7 +750,7 @@ impl Workspace {
 
         cx.emit_global(WorkspaceCreated(weak_self.clone()));
 
-        Workspace {
+        let mut this = Workspace {
             modal: None,
             weak_self,
             center: PaneGroup::new(pane.clone()),
@@ -507,13 +758,19 @@ impl Workspace {
             active_pane: pane.clone(),
             status_bar,
             client: params.client.clone(),
+            remote_entity_subscription: None,
             user_store: params.user_store.clone(),
             fs: params.fs.clone(),
             left_sidebar: Sidebar::new(Side::Left),
             right_sidebar: Sidebar::new(Side::Right),
             project: params.project.clone(),
+            leader_state: Default::default(),
+            follower_states_by_leader: Default::default(),
+            last_leaders_by_pane: Default::default(),
             _observe_current_user,
-        }
+        };
+        this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
+        this
     }
 
     pub fn weak_handle(&self) -> WeakViewHandle<Self> {
@@ -666,6 +923,13 @@ impl Workspace {
         }
     }
 
+    pub fn items<'a>(
+        &'a self,
+        cx: &'a AppContext,
+    ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
+        self.panes.iter().flat_map(|pane| pane.read(cx).items())
+    }
+
     pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
         self.items_of_type(cx).max_by_key(|item| item.id())
     }
@@ -674,11 +938,9 @@ impl Workspace {
         &'a self,
         cx: &'a AppContext,
     ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
-        self.panes.iter().flat_map(|pane| {
-            pane.read(cx)
-                .items()
-                .filter_map(|item| item.to_any().downcast())
-        })
+        self.panes
+            .iter()
+            .flat_map(|pane| pane.read(cx).items_of_type())
     }
 
     pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
@@ -801,26 +1063,30 @@ impl Workspace {
     }
 
     pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
-        self.active_pane()
-            .update(cx, |pane, cx| pane.add_item(None, item, cx))
+        let pane = self.active_pane().clone();
+        Pane::add_item(self, pane, item, true, cx);
     }
 
     pub fn open_path(
         &mut self,
-        path: ProjectPath,
+        path: impl Into<ProjectPath>,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
         let pane = self.active_pane().downgrade();
-        let task = self.load_path(path, cx);
+        let task = self.load_path(path.into(), cx);
         cx.spawn(|this, mut cx| async move {
-            let (project_entry_id, build_editor) = task.await?;
+            let (project_entry_id, build_item) = task.await?;
             let pane = pane
                 .upgrade(&cx)
                 .ok_or_else(|| anyhow!("pane was closed"))?;
-            this.update(&mut cx, |_, cx| {
-                pane.update(cx, |pane, cx| {
-                    Ok(pane.open_item(project_entry_id, cx, build_editor))
-                })
+            this.update(&mut cx, |this, cx| {
+                Ok(Pane::open_item(
+                    this,
+                    pane,
+                    project_entry_id,
+                    cx,
+                    build_item,
+                ))
             })
         })
     }
@@ -841,7 +1107,7 @@ impl Workspace {
         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>()
+                cx.default_global::<ProjectItemBuilders>()
                     .get(&project_item.model_type())
                     .ok_or_else(|| anyhow!("no item builder for project item"))
                     .cloned()
@@ -864,7 +1130,7 @@ impl Workspace {
 
         let entry_id = project_item.read(cx).entry_id(cx);
         if let Some(item) = entry_id
-            .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(dbg!(entry_id)))
+            .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
             .and_then(|item| item.downcast())
         {
             self.activate_item(&item, cx);
@@ -872,9 +1138,7 @@ impl Workspace {
         }
 
         let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
-        self.active_pane().update(cx, |pane, cx| {
-            pane.add_item(entry_id, Box::new(item.clone()), cx)
-        });
+        self.add_item(Box::new(item.clone()), cx);
         item
     }
 
@@ -888,7 +1152,7 @@ impl Workspace {
         });
         if let Some((pane, ix)) = result {
             self.activate_pane(pane.clone(), cx);
-            pane.update(cx, |pane, cx| pane.activate_item(ix, cx));
+            pane.update(cx, |pane, cx| pane.activate_item(ix, true, cx));
             true
         } else {
             false
@@ -896,24 +1160,48 @@ impl Workspace {
     }
 
     pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
-        let ix = self
-            .panes
-            .iter()
-            .position(|pane| pane == &self.active_pane)
-            .unwrap();
-        let next_ix = (ix + 1) % self.panes.len();
-        self.activate_pane(self.panes[next_ix].clone(), cx);
+        let next_pane = {
+            let panes = self.center.panes();
+            let ix = panes
+                .iter()
+                .position(|pane| **pane == self.active_pane)
+                .unwrap();
+            let next_ix = (ix + 1) % panes.len();
+            panes[next_ix].clone()
+        };
+        self.activate_pane(next_pane, cx);
+    }
+
+    pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
+        let prev_pane = {
+            let panes = self.center.panes();
+            let ix = panes
+                .iter()
+                .position(|pane| **pane == self.active_pane)
+                .unwrap();
+            let prev_ix = if ix == 0 { panes.len() - 1 } else { ix - 1 };
+            panes[prev_ix].clone()
+        };
+        self.activate_pane(prev_pane, cx);
     }
 
     fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
         if self.active_pane != pane {
-            self.active_pane = pane;
+            self.active_pane = pane.clone();
             self.status_bar.update(cx, |status_bar, cx| {
                 status_bar.set_active_pane(&self.active_pane, cx);
             });
             cx.focus(&self.active_pane);
             cx.notify();
         }
+
+        self.update_followers(
+            proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
+                id: self.active_item(cx).map(|item| item.id() as u64),
+                leader_id: self.leader_for_pane(&pane).map(|id| id.0),
+            }),
+            cx,
+        );
     }
 
     fn handle_pane_event(
@@ -933,6 +1221,11 @@ impl Workspace {
                 pane::Event::Activate => {
                     self.activate_pane(pane, cx);
                 }
+                pane::Event::ActivateItem { local } => {
+                    if *local {
+                        self.unfollow(&pane, cx);
+                    }
+                }
             }
         } else {
             error!("pane {} not found", pane_id);
@@ -948,11 +1241,8 @@ 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 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()) {
-                new_pane.update(cx, |new_pane, cx| {
-                    new_pane.add_item(project_entry_id, clone, cx);
-                });
+                Pane::add_item(self, new_pane.clone(), clone, true, cx);
             }
         }
         self.center.split(&pane, &new_pane, direction).unwrap();
@@ -964,6 +1254,8 @@ impl Workspace {
         if self.center.remove(&pane).unwrap() {
             self.panes.retain(|p| p != &pane);
             self.activate_pane(self.panes.last().unwrap().clone(), cx);
+            self.unfollow(&pane, cx);
+            self.last_leaders_by_pane.remove(&pane.downgrade());
             cx.notify();
         }
     }
@@ -992,6 +1284,139 @@ impl Workspace {
         });
     }
 
+    fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
+        if let Some(remote_id) = remote_id {
+            self.remote_entity_subscription =
+                Some(self.client.add_view_for_remote_entity(remote_id, cx));
+        } else {
+            self.remote_entity_subscription.take();
+        }
+    }
+
+    fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
+        self.leader_state.followers.remove(&peer_id);
+        if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
+            for state in states_by_pane.into_values() {
+                for item in state.items_by_leader_view_id.into_values() {
+                    if let FollowerItem::Loaded(item) = item {
+                        item.set_leader_replica_id(None, cx);
+                    }
+                }
+            }
+        }
+        cx.notify();
+    }
+
+    pub fn toggle_follow(
+        &mut self,
+        ToggleFollow(leader_id): &ToggleFollow,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let leader_id = *leader_id;
+        let pane = self.active_pane().clone();
+
+        if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
+            if leader_id == prev_leader_id {
+                return None;
+            }
+        }
+
+        self.last_leaders_by_pane
+            .insert(pane.downgrade(), leader_id);
+        self.follower_states_by_leader
+            .entry(leader_id)
+            .or_default()
+            .insert(pane.clone(), Default::default());
+        cx.notify();
+
+        let project_id = self.project.read(cx).remote_id()?;
+        let request = self.client.request(proto::Follow {
+            project_id,
+            leader_id: leader_id.0,
+        });
+        Some(cx.spawn_weak(|this, mut cx| async move {
+            let response = request.await?;
+            if let Some(this) = this.upgrade(&cx) {
+                this.update(&mut cx, |this, _| {
+                    let state = this
+                        .follower_states_by_leader
+                        .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;
+                    Ok::<_, anyhow::Error>(())
+                })?;
+                Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx)
+                    .await?;
+            }
+            Ok(())
+        }))
+    }
+
+    pub fn follow_next_collaborator(
+        &mut self,
+        _: &FollowNextCollaborator,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let collaborators = self.project.read(cx).collaborators();
+        let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
+            let mut collaborators = collaborators.keys().copied();
+            while let Some(peer_id) = collaborators.next() {
+                if peer_id == leader_id {
+                    break;
+                }
+            }
+            collaborators.next()
+        } else if let Some(last_leader_id) =
+            self.last_leaders_by_pane.get(&self.active_pane.downgrade())
+        {
+            if collaborators.contains_key(last_leader_id) {
+                Some(*last_leader_id)
+            } else {
+                None
+            }
+        } else {
+            None
+        };
+
+        next_leader_id
+            .or_else(|| collaborators.keys().copied().next())
+            .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
+    }
+
+    pub fn unfollow(
+        &mut self,
+        pane: &ViewHandle<Pane>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<PeerId> {
+        for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
+            let leader_id = *leader_id;
+            if let Some(state) = states_by_pane.remove(&pane) {
+                for (_, item) in state.items_by_leader_view_id {
+                    if let FollowerItem::Loaded(item) = item {
+                        item.set_leader_replica_id(None, cx);
+                    }
+                }
+
+                if states_by_pane.is_empty() {
+                    self.follower_states_by_leader.remove(&leader_id);
+                    if let Some(project_id) = self.project.read(cx).remote_id() {
+                        self.client
+                            .send(proto::Unfollow {
+                                project_id,
+                                leader_id: leader_id.0,
+                            })
+                            .log_err();
+                    }
+                }
+
+                cx.notify();
+                return Some(leader_id);
+            }
+        }
+        None
+    }
+
     fn render_connection_status(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
         let theme = &cx.global::<Settings>().theme;
         match &*self.client.status().borrow() {
@@ -1081,7 +1506,9 @@ impl Workspace {
                 Some(self.render_avatar(
                     collaborator.user.avatar.clone()?,
                     collaborator.replica_id,
+                    Some(collaborator.peer_id),
                     theme,
+                    cx,
                 ))
             })
             .collect()
@@ -1095,7 +1522,7 @@ impl Workspace {
         cx: &mut RenderContext<Self>,
     ) -> ElementBox {
         if let Some(avatar) = user.and_then(|user| user.avatar.clone()) {
-            self.render_avatar(avatar, replica_id, theme)
+            self.render_avatar(avatar, replica_id, None, theme, cx)
         } else {
             MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| {
                 let style = if state.hovered {
@@ -1119,52 +1546,65 @@ impl Workspace {
         &self,
         avatar: Arc<ImageData>,
         replica_id: ReplicaId,
+        peer_id: Option<PeerId>,
         theme: &Theme,
+        cx: &mut RenderContext<Self>,
     ) -> ElementBox {
-        ConstrainedBox::new(
-            Stack::new()
-                .with_child(
-                    ConstrainedBox::new(
-                        Image::new(avatar)
-                            .with_style(theme.workspace.titlebar.avatar)
-                            .boxed(),
-                    )
+        let replica_color = theme.editor.replica_selection_style(replica_id).cursor;
+        let is_followed = peer_id.map_or(false, |peer_id| {
+            self.follower_states_by_leader.contains_key(&peer_id)
+        });
+        let mut avatar_style = theme.workspace.titlebar.avatar;
+        if is_followed {
+            avatar_style.border = Border::all(1.0, replica_color);
+        }
+        let content = Stack::new()
+            .with_child(
+                Image::new(avatar)
+                    .with_style(avatar_style)
+                    .constrained()
                     .with_width(theme.workspace.titlebar.avatar_width)
                     .aligned()
                     .boxed(),
-                )
-                .with_child(
-                    AvatarRibbon::new(theme.editor.replica_selection_style(replica_id).cursor)
-                        .constrained()
-                        .with_width(theme.workspace.titlebar.avatar_ribbon.width)
-                        .with_height(theme.workspace.titlebar.avatar_ribbon.height)
-                        .aligned()
-                        .bottom()
-                        .boxed(),
-                )
-                .boxed(),
-        )
-        .with_width(theme.workspace.right_sidebar.width)
-        .boxed()
+            )
+            .with_child(
+                AvatarRibbon::new(replica_color)
+                    .constrained()
+                    .with_width(theme.workspace.titlebar.avatar_ribbon.width)
+                    .with_height(theme.workspace.titlebar.avatar_ribbon.height)
+                    .aligned()
+                    .bottom()
+                    .boxed(),
+            )
+            .constrained()
+            .with_width(theme.workspace.right_sidebar.width)
+            .boxed();
+
+        if let Some(peer_id) = peer_id {
+            MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(move |cx| cx.dispatch_action(ToggleFollow(peer_id)))
+                .boxed()
+        } else {
+            content
+        }
     }
 
     fn render_share_icon(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
         if self.project().read(cx).is_local() && self.client.user_id().is_some() {
-            enum Share {}
-
             let color = if self.project().read(cx).is_shared() {
                 theme.workspace.titlebar.share_icon_active_color
             } else {
                 theme.workspace.titlebar.share_icon_color
             };
             Some(
-                MouseEventHandler::new::<Share, _, _>(0, cx, |_, _| {
+                MouseEventHandler::new::<ToggleShare, _, _>(0, cx, |_, _| {
                     Align::new(
-                        ConstrainedBox::new(
-                            Svg::new("icons/broadcast-24.svg").with_color(color).boxed(),
-                        )
-                        .with_width(24.)
-                        .boxed(),
+                        Svg::new("icons/broadcast-24.svg")
+                            .with_color(color)
+                            .constrained()
+                            .with_width(24.)
+                            .boxed(),
                     )
                     .boxed()
                 })
@@ -1198,6 +1638,279 @@ impl Workspace {
             None
         }
     }
+
+    // RPC handlers
+
+    async fn handle_follow(
+        this: ViewHandle<Self>,
+        envelope: TypedEnvelope<proto::Follow>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::FollowResponse> {
+        this.update(&mut cx, |this, cx| {
+            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);
+            Ok(proto::FollowResponse {
+                active_view_id,
+                views: this
+                    .panes()
+                    .iter()
+                    .flat_map(|pane| {
+                        let leader_id = this.leader_for_pane(pane).map(|id| id.0);
+                        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 variant = item.to_state_proto(cx)?;
+                                Some(proto::View {
+                                    id,
+                                    leader_id,
+                                    variant: Some(variant),
+                                })
+                            }
+                        })
+                    })
+                    .collect(),
+            })
+        })
+    }
+
+    async fn handle_unfollow(
+        this: ViewHandle<Self>,
+        envelope: TypedEnvelope<proto::Unfollow>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, _| {
+            this.leader_state
+                .followers
+                .remove(&envelope.original_sender_id()?);
+            Ok(())
+        })
+    }
+
+    async fn handle_update_followers(
+        this: ViewHandle<Self>,
+        envelope: TypedEnvelope<proto::UpdateFollowers>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let leader_id = envelope.original_sender_id()?;
+        match envelope
+            .payload
+            .variant
+            .ok_or_else(|| anyhow!("invalid update"))?
+        {
+            proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
+                this.update(&mut cx, |this, cx| {
+                    this.update_leader_state(leader_id, cx, |state, _| {
+                        state.active_view_id = update_active_view.id;
+                    });
+                    Ok::<_, anyhow::Error>(())
+                })
+            }
+            proto::update_followers::Variant::UpdateView(update_view) => {
+                this.update(&mut cx, |this, cx| {
+                    let variant = update_view
+                        .variant
+                        .ok_or_else(|| anyhow!("missing update view variant"))?;
+                    this.update_leader_state(leader_id, cx, |state, cx| {
+                        let variant = variant.clone();
+                        match state
+                            .items_by_leader_view_id
+                            .entry(update_view.id)
+                            .or_insert(FollowerItem::Loading(Vec::new()))
+                        {
+                            FollowerItem::Loaded(item) => {
+                                item.apply_update_proto(variant, cx).log_err();
+                            }
+                            FollowerItem::Loading(updates) => updates.push(variant),
+                        }
+                    });
+                    Ok(())
+                })
+            }
+            proto::update_followers::Variant::CreateView(view) => {
+                let panes = this.read_with(&cx, |this, _| {
+                    this.follower_states_by_leader
+                        .get(&leader_id)
+                        .into_iter()
+                        .flat_map(|states_by_pane| states_by_pane.keys())
+                        .cloned()
+                        .collect()
+                });
+                Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx)
+                    .await?;
+                Ok(())
+            }
+        }
+        .log_err();
+
+        Ok(())
+    }
+
+    async fn add_views_from_leader(
+        this: ViewHandle<Self>,
+        leader_id: PeerId,
+        panes: Vec<ViewHandle<Pane>>,
+        views: Vec<proto::View>,
+        cx: &mut AsyncAppContext,
+    ) -> Result<()> {
+        let project = this.read_with(cx, |this, _| this.project.clone());
+        let replica_id = project
+            .read_with(cx, |project, _| {
+                project
+                    .collaborators()
+                    .get(&leader_id)
+                    .map(|c| c.replica_id)
+            })
+            .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
+
+        let item_builders = cx.update(|cx| {
+            cx.default_global::<FollowableItemBuilders>()
+                .values()
+                .map(|b| b.0)
+                .collect::<Vec<_>>()
+                .clone()
+        });
+
+        let mut item_tasks_by_pane = HashMap::default();
+        for pane in panes {
+            let mut item_tasks = Vec::new();
+            let mut leader_view_ids = Vec::new();
+            for view in &views {
+                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));
+                    if let Some(task) = task {
+                        item_tasks.push(task);
+                        leader_view_ids.push(view.id);
+                        break;
+                    } else {
+                        assert!(variant.is_some());
+                    }
+                }
+            }
+
+            item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
+        }
+
+        for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
+            let items = futures::future::try_join_all(item_tasks).await?;
+            this.update(cx, |this, cx| {
+                let state = this
+                    .follower_states_by_leader
+                    .get_mut(&leader_id)?
+                    .get_mut(&pane)?;
+
+                for (id, item) in leader_view_ids.into_iter().zip(items) {
+                    item.set_leader_replica_id(Some(replica_id), cx);
+                    match state.items_by_leader_view_id.entry(id) {
+                        hash_map::Entry::Occupied(e) => {
+                            let e = e.into_mut();
+                            if let FollowerItem::Loading(updates) = e {
+                                for update in updates.drain(..) {
+                                    item.apply_update_proto(update, cx)
+                                        .context("failed to apply view update")
+                                        .log_err();
+                                }
+                            }
+                            *e = FollowerItem::Loaded(item);
+                        }
+                        hash_map::Entry::Vacant(e) => {
+                            e.insert(FollowerItem::Loaded(item));
+                        }
+                    }
+                }
+
+                Some(())
+            });
+        }
+        this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
+
+        Ok(())
+    }
+
+    fn update_followers(
+        &self,
+        update: proto::update_followers::Variant,
+        cx: &AppContext,
+    ) -> Option<()> {
+        let project_id = self.project.read(cx).remote_id()?;
+        if !self.leader_state.followers.is_empty() {
+            self.client
+                .send(proto::UpdateFollowers {
+                    project_id,
+                    follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(),
+                    variant: Some(update),
+                })
+                .log_err();
+        }
+        None
+    }
+
+    pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
+        self.follower_states_by_leader
+            .iter()
+            .find_map(|(leader_id, state)| {
+                if state.contains_key(pane) {
+                    Some(*leader_id)
+                } else {
+                    None
+                }
+            })
+    }
+
+    fn update_leader_state(
+        &mut self,
+        leader_id: PeerId,
+        cx: &mut ViewContext<Self>,
+        mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext<Self>),
+    ) {
+        for (_, state) in self
+            .follower_states_by_leader
+            .get_mut(&leader_id)
+            .into_iter()
+            .flatten()
+        {
+            update_fn(state, cx);
+        }
+        self.leader_updated(leader_id, cx);
+    }
+
+    fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
+        let mut items_to_add = Vec::new();
+        for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
+            if let Some(active_item) = state
+                .active_view_id
+                .and_then(|id| state.items_by_leader_view_id.get(&id))
+            {
+                if let FollowerItem::Loaded(item) = active_item {
+                    items_to_add.push((pane.clone(), item.boxed_clone()));
+                }
+            }
+        }
+
+        for (pane, item) in items_to_add {
+            Pane::add_item(self, pane.clone(), item.boxed_clone(), false, cx);
+            if pane == self.active_pane {
+                pane.update(cx, |pane, cx| pane.focus_active_item(cx));
+            }
+            cx.notify();
+        }
+        None
+    }
 }
 
 impl Entity for Workspace {

crates/zed/Cargo.toml 🔗

@@ -64,6 +64,7 @@ crossbeam-channel = "0.5.0"
 ctor = "0.1.20"
 dirs = "3.0"
 easy-parallel = "3.1.0"
+env_logger = "0.8"
 futures = "0.3"
 http-auth-basic = "0.1.3"
 ignore = "0.4"

crates/zed/assets/themes/_base.toml 🔗

@@ -4,6 +4,8 @@ base = { family = "Zed Sans", size = 14 }
 [workspace]
 background = "$surface.0"
 pane_divider = { width = 1, color = "$border.0" }
+leader_border_opacity = 0.7
+leader_border_width = 2.0
 
 [workspace.titlebar]
 height = 32

crates/zed/src/main.rs 🔗

@@ -9,7 +9,6 @@ use gpui::{App, AssetSource, Task};
 use log::LevelFilter;
 use parking_lot::Mutex;
 use project::Fs;
-use simplelog::SimpleLogger;
 use smol::process::Command;
 use std::{env, fs, path::PathBuf, sync::Arc};
 use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
@@ -69,7 +68,7 @@ fn main() {
         project::Project::init(&client);
         client::Channel::init(&client);
         client::init(client.clone(), cx);
-        workspace::init(cx);
+        workspace::init(&client, cx);
         editor::init(cx);
         go_to_line::init(cx);
         file_finder::init(cx);
@@ -142,11 +141,10 @@ fn main() {
 }
 
 fn init_logger() {
-    let level = LevelFilter::Info;
-
     if stdout_is_a_pty() {
-        SimpleLogger::init(level, Default::default()).expect("could not initialize logger");
+        env_logger::init();
     } else {
+        let level = LevelFilter::Info;
         let log_dir_path = dirs::home_dir()
             .expect("could not locate home directory for logging")
             .join("Library/Logs/");

crates/zed/src/zed.rs 🔗

@@ -252,7 +252,7 @@ mod tests {
     async fn test_new_empty_workspace(cx: &mut TestAppContext) {
         let app_state = cx.update(test_app_state);
         cx.update(|cx| {
-            workspace::init(cx);
+            workspace::init(&app_state.client, cx);
         });
         cx.dispatch_global_action(workspace::OpenNew(app_state.clone()));
         let window_id = *cx.window_ids().first().unwrap();