catching up with main

KCaverly created

Change summary

Cargo.lock                                                                             |    9 
Cargo.toml                                                                             |    1 
Procfile                                                                               |    4 
assets/settings/default.json                                                           |   18 
crates/assistant/src/prompts.rs                                                        |   61 
crates/auto_update/src/auto_update.rs                                                  |    8 
crates/call/src/call.rs                                                                |   27 
crates/call/src/participant.rs                                                         |    2 
crates/call/src/room.rs                                                                |   21 
crates/channel/src/channel_buffer.rs                                                   |  105 
crates/channel/src/channel_store.rs                                                    |    3 
crates/client/Cargo.toml                                                               |    9 
crates/client/src/client.rs                                                            |    2 
crates/client/src/telemetry.rs                                                         |   55 
crates/client/src/user.rs                                                              |   43 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql                         |    3 
crates/collab/migrations/20230926102500_add_participant_index_to_room_participants.sql |    1 
crates/collab/src/db.rs                                                                |    2 
crates/collab/src/db/queries/buffers.rs                                                |   59 
crates/collab/src/db/queries/projects.rs                                               |   42 
crates/collab/src/db/queries/rooms.rs                                                  |   72 
crates/collab/src/db/tables/room_participant.rs                                        |   11 
crates/collab/src/db/tests/buffer_tests.rs                                             |    4 
crates/collab/src/rpc.rs                                                               |  140 
crates/collab/src/tests.rs                                                             |    1 
crates/collab/src/tests/channel_buffer_tests.rs                                        |  433 
crates/collab/src/tests/following_tests.rs                                             | 1306 
crates/collab/src/tests/integration_tests.rs                                           | 1129 
crates/collab/src/tests/random_channel_buffer_tests.rs                                 |    2 
crates/collab/src/tests/test_server.rs                                                 |   30 
crates/collab_ui/src/channel_view.rs                                                   |  152 
crates/collab_ui/src/chat_panel.rs                                                     |    4 
crates/collab_ui/src/collab_panel.rs                                                   |  267 
crates/collab_ui/src/collab_titlebar_item.rs                                           |  350 
crates/collab_ui/src/collab_ui.rs                                                      |    2 
crates/collab_ui/src/project_shared_notification.rs                                    |   15 
crates/editor/src/editor.rs                                                            |  101 
crates/editor/src/element.rs                                                           |  103 
crates/editor/src/items.rs                                                             |   16 
crates/feedback/Cargo.toml                                                             |    2 
crates/fs/Cargo.toml                                                                   |    6 
crates/fs/src/fs.rs                                                                    |   27 
crates/gpui/Cargo.toml                                                                 |    2 
crates/gpui/src/platform/test.rs                                                       |   35 
crates/project/src/project.rs                                                          |  120 
crates/project/src/terminals.rs                                                        |    1 
crates/rpc/proto/zed.proto                                                             |  331 
crates/rpc/src/proto.rs                                                                |   11 
crates/rpc/src/rpc.rs                                                                  |    2 
crates/semantic_index/src/parsing.rs                                                   |    5 
crates/semantic_index/src/semantic_index_tests.rs                                      |   17 
crates/storybook/Cargo.toml                                                            |    2 
crates/storybook/src/stories/components/breadcrumb.rs                                  |   33 
crates/storybook/src/stories/components/buffer.rs                                      |    8 
crates/storybook/src/stories/components/chat_panel.rs                                  |   48 
crates/storybook/src/stories/components/facepile.rs                                    |    2 
crates/storybook/src/stories/components/panel.rs                                       |    7 
crates/storybook/src/stories/components/project_panel.rs                               |    8 
crates/storybook/src/stories/components/tab_bar.rs                                     |   34 
crates/storybook/src/stories/components/toolbar.rs                                     |   60 
crates/storybook/src/stories/elements/avatar.rs                                        |    2 
crates/storybook/src/stories/elements/icon.rs                                          |    2 
crates/storybook/src/stories/kitchen_sink.rs                                           |    3 
crates/storybook/src/storybook.rs                                                      |   39 
crates/terminal/src/terminal_settings.rs                                               |    1 
crates/theme/src/theme.rs                                                              |   12 
crates/ui/Cargo.toml                                                                   |    1 
crates/ui/src/components.rs                                                            |    4 
crates/ui/src/components/breadcrumb.rs                                                 |   60 
crates/ui/src/components/buffer.rs                                                     |   40 
crates/ui/src/components/chat_panel.rs                                                 |   75 
crates/ui/src/components/editor.rs                                                     |   25 
crates/ui/src/components/editor_pane.rs                                                |   60 
crates/ui/src/components/panel.rs                                                      |   16 
crates/ui/src/components/player_stack.rs                                               |    9 
crates/ui/src/components/project_panel.rs                                              |   87 
crates/ui/src/components/tab.rs                                                        |    2 
crates/ui/src/components/tab_bar.rs                                                    |   57 
crates/ui/src/components/terminal.rs                                                   |   11 
crates/ui/src/components/title_bar.rs                                                  |   29 
crates/ui/src/components/toolbar.rs                                                    |   40 
crates/ui/src/components/workspace.rs                                                  |  101 
crates/ui/src/elements/icon.rs                                                         |    2 
crates/ui/src/elements/input.rs                                                        |    1 
crates/ui/src/elements/player.rs                                                       |    3 
crates/ui/src/prelude.rs                                                               |   19 
crates/ui/src/static_data.rs                                                           |  482 
crates/vim/src/vim.rs                                                                  |    4 
crates/workspace/src/item.rs                                                           |   24 
crates/workspace/src/pane_group.rs                                                     |   55 
crates/workspace/src/workspace.rs                                                      |  570 
crates/zed/src/languages.rs                                                            |   17 
crates/zed/src/languages/elixir.rs                                                     |  298 
crates/zed/src/languages/elixir_next.rs                                                |  266 
crates/zed/src/languages/rust/embedding.scm                                            |    4 
crates/zed/src/main.rs                                                                 |    6 
script/start-local-collaboration                                                       |    1 
styles/src/style_tree/titlebar.ts                                                      |    8 
98 files changed, 4,766 insertions(+), 3,044 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1416,6 +1416,7 @@ dependencies = [
  "settings",
  "smol",
  "sum_tree",
+ "sysinfo",
  "tempfile",
  "text",
  "thiserror",
@@ -2791,7 +2792,6 @@ dependencies = [
  "lazy_static",
  "libc",
  "log",
- "lsp",
  "parking_lot 0.11.2",
  "regex",
  "rope",
@@ -7404,6 +7404,8 @@ dependencies = [
  "anyhow",
  "chrono",
  "clap 4.4.4",
+ "fs",
+ "futures 0.3.28",
  "gpui2",
  "itertools 0.11.0",
  "log",
@@ -7607,9 +7609,9 @@ dependencies = [
 
 [[package]]
 name = "sysinfo"
-version = "0.27.8"
+version = "0.29.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a902e9050fca0a5d6877550b769abd2bd1ce8c04634b941dbe2809735e1a1e33"
+checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5"
 dependencies = [
  "cfg-if 1.0.0",
  "core-foundation-sys 0.8.3",
@@ -8624,6 +8626,7 @@ dependencies = [
  "anyhow",
  "chrono",
  "gpui2",
+ "rand 0.8.5",
  "serde",
  "settings",
  "smallvec",

Cargo.toml 🔗

@@ -111,6 +111,7 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
 smallvec = { version = "1.6", features = ["union"] }
 smol = { version = "1.2" }
+sysinfo = "0.29.10"
 tempdir = { version = "0.3.7" }
 thiserror = { version = "1.0.29" }
 time = { version = "0.3", features = ["serde", "serde-well-known"] }

Procfile 🔗

@@ -1,4 +1,4 @@
-web: cd ../zed.dev && PORT=3000 npx vercel dev
-collab: cd crates/collab && cargo run serve
+web: cd ../zed.dev && PORT=3000 npm run dev
+collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
 livekit: livekit-server --dev
 postgrest: postgrest crates/collab/admin_api.conf

assets/settings/default.json 🔗

@@ -361,7 +361,7 @@
           ".venv",
           "venv"
         ],
-        // Can also be 'csh' and 'fish'
+        // Can also be 'csh', 'fish', and `nushell`
         "activate_script": "default"
       }
     }
@@ -379,24 +379,24 @@
   },
   // Settings specific to our elixir integration
   "elixir": {
-    // Set Zed to use the experimental Next LS LSP server.
+    // Change the LSP zed uses for elixir.
     // Note that changing this setting requires a restart of Zed
     // to take effect.
     //
     // May take 3 values:
-    //  1. Use the standard elixir-ls LSP server
-    //         "next": "off"
-    //  2. Use a bundled version of the next Next LS LSP server
-    //         "next": "on",
-    //  3. Use a local build of the next Next LS LSP server:
-    //         "next": {
+    //  1. Use the standard ElixirLS, this is the default
+    //         "lsp": "elixir_ls"
+    //  2. Use the experimental NextLs
+    //         "lsp": "next_ls",
+    //  3. Use a language server installed locally on your machine:
+    //         "lsp": {
     //           "local": {
     //             "path": "~/next-ls/bin/start",
     //             "arguments": ["--stdio"]
     //            }
     //          },
     //
-    "next": "off"
+    "lsp": "elixir_ls"
   },
   // Different settings for specific languages.
   "languages": {

crates/assistant/src/prompts.rs 🔗

@@ -1,6 +1,6 @@
 use crate::codegen::CodegenKind;
 use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
-use std::cmp;
+use std::cmp::{self, Reverse};
 use std::fmt::Write;
 use std::iter;
 use std::ops::Range;
@@ -14,59 +14,58 @@ fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> S
     }
 
     let selected_range = selected_range.to_offset(buffer);
-    let mut matches = buffer.matches(0..buffer.len(), |grammar| {
+    let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| {
         Some(&grammar.embedding_config.as_ref()?.query)
     });
-    let configs = matches
+    let configs = ts_matches
         .grammars()
         .iter()
         .map(|g| g.embedding_config.as_ref().unwrap())
         .collect::<Vec<_>>();
-    let mut matches = iter::from_fn(move || {
-        while let Some(mat) = matches.peek() {
-            let config = &configs[mat.grammar_index];
-            if let Some(collapse) = mat.captures.iter().find_map(|cap| {
-                if Some(cap.index) == config.collapse_capture_ix {
-                    Some(cap.node.byte_range())
+    let mut matches = Vec::new();
+    while let Some(mat) = ts_matches.peek() {
+        let config = &configs[mat.grammar_index];
+        if let Some(collapse) = mat.captures.iter().find_map(|cap| {
+            if Some(cap.index) == config.collapse_capture_ix {
+                Some(cap.node.byte_range())
+            } else {
+                None
+            }
+        }) {
+            let mut keep = Vec::new();
+            for capture in mat.captures.iter() {
+                if Some(capture.index) == config.keep_capture_ix {
+                    keep.push(capture.node.byte_range());
                 } else {
-                    None
-                }
-            }) {
-                let mut keep = Vec::new();
-                for capture in mat.captures.iter() {
-                    if Some(capture.index) == config.keep_capture_ix {
-                        keep.push(capture.node.byte_range());
-                    } else {
-                        continue;
-                    }
+                    continue;
                 }
-                matches.advance();
-                return Some(Match { collapse, keep });
-            } else {
-                matches.advance();
             }
+            ts_matches.advance();
+            matches.push(Match { collapse, keep });
+        } else {
+            ts_matches.advance();
         }
-        None
-    })
-    .peekable();
+    }
+    matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end)));
+    let mut matches = matches.into_iter().peekable();
 
     let mut summary = String::new();
     let mut offset = 0;
     let mut flushed_selection = false;
-    while let Some(mut mat) = matches.next() {
+    while let Some(mat) = matches.next() {
         // Keep extending the collapsed range if the next match surrounds
         // the current one.
         while let Some(next_mat) = matches.peek() {
-            if next_mat.collapse.start <= mat.collapse.start
-                && next_mat.collapse.end >= mat.collapse.end
+            if mat.collapse.start <= next_mat.collapse.start
+                && mat.collapse.end >= next_mat.collapse.end
             {
-                mat = matches.next().unwrap();
+                matches.next().unwrap();
             } else {
                 break;
             }
         }
 
-        if offset >= mat.collapse.start {
+        if offset > mat.collapse.start {
             // Skip collapsed nodes that have already been summarized.
             offset = cmp::max(offset, mat.collapse.end);
             continue;

crates/auto_update/src/auto_update.rs 🔗

@@ -115,13 +115,15 @@ pub fn check(_: &Check, cx: &mut AppContext) {
 
 fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
     if let Some(auto_updater) = AutoUpdater::get(cx) {
-        let server_url = &auto_updater.read(cx).server_url;
+        let auto_updater = auto_updater.read(cx);
+        let server_url = &auto_updater.server_url;
+        let current_version = auto_updater.current_version;
         let latest_release_url = if cx.has_global::<ReleaseChannel>()
             && *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
         {
-            format!("{server_url}/releases/preview/latest")
+            format!("{server_url}/releases/preview/{current_version}")
         } else {
-            format!("{server_url}/releases/stable/latest")
+            format!("{server_url}/releases/stable/{current_version}")
         };
         cx.platform().open_url(&latest_release_url);
     }

crates/call/src/call.rs 🔗

@@ -2,22 +2,23 @@ pub mod call_settings;
 pub mod participant;
 pub mod room;
 
-use std::sync::Arc;
-
 use anyhow::{anyhow, Result};
 use audio::Audio;
 use call_settings::CallSettings;
 use channel::ChannelId;
-use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
+use client::{
+    proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
+    ZED_ALWAYS_ACTIVE,
+};
 use collections::HashSet;
 use futures::{future::Shared, FutureExt};
-use postage::watch;
-
 use gpui::{
     AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
     WeakModelHandle,
 };
+use postage::watch;
 use project::Project;
+use std::sync::Arc;
 
 pub use participant::ParticipantLocation;
 pub use room::Room;
@@ -68,6 +69,7 @@ impl ActiveCall {
             location: None,
             pending_invites: Default::default(),
             incoming_call: watch::channel(),
+
             _subscriptions: vec![
                 client.add_request_handler(cx.handle(), Self::handle_incoming_call),
                 client.add_message_handler(cx.handle(), Self::handle_call_canceled),
@@ -348,17 +350,22 @@ impl ActiveCall {
         }
     }
 
+    pub fn location(&self) -> Option<&WeakModelHandle<Project>> {
+        self.location.as_ref()
+    }
+
     pub fn set_location(
         &mut self,
         project: Option<&ModelHandle<Project>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
-        self.location = project.map(|project| project.downgrade());
-        if let Some((room, _)) = self.room.as_ref() {
-            room.update(cx, |room, cx| room.set_location(project, cx))
-        } else {
-            Task::ready(Ok(()))
+        if project.is_some() || !*ZED_ALWAYS_ACTIVE {
+            self.location = project.map(|project| project.downgrade());
+            if let Some((room, _)) = self.room.as_ref() {
+                return room.update(cx, |room, cx| room.set_location(project, cx));
+            }
         }
+        Task::ready(Ok(()))
     }
 
     fn set_room(

crates/call/src/participant.rs 🔗

@@ -1,4 +1,5 @@
 use anyhow::{anyhow, Result};
+use client::ParticipantIndex;
 use client::{proto, User};
 use collections::HashMap;
 use gpui::WeakModelHandle;
@@ -43,6 +44,7 @@ pub struct RemoteParticipant {
     pub peer_id: proto::PeerId,
     pub projects: Vec<proto::ParticipantProject>,
     pub location: ParticipantLocation,
+    pub participant_index: ParticipantIndex,
     pub muted: bool,
     pub speaking: bool,
     pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,

crates/call/src/room.rs 🔗

@@ -7,7 +7,7 @@ use anyhow::{anyhow, Result};
 use audio::{Audio, Sound};
 use client::{
     proto::{self, PeerId},
-    Client, TypedEnvelope, User, UserStore,
+    Client, ParticipantIndex, TypedEnvelope, User, UserStore,
 };
 use collections::{BTreeMap, HashMap, HashSet};
 use fs::Fs;
@@ -44,6 +44,12 @@ pub enum Event {
     RemoteProjectUnshared {
         project_id: u64,
     },
+    RemoteProjectJoined {
+        project_id: u64,
+    },
+    RemoteProjectInvitationDiscarded {
+        project_id: u64,
+    },
     Left,
 }
 
@@ -714,6 +720,9 @@ impl Room {
                                 participant.user_id,
                                 RemoteParticipant {
                                     user: user.clone(),
+                                    participant_index: ParticipantIndex(
+                                        participant.participant_index,
+                                    ),
                                     peer_id,
                                     projects: participant.projects,
                                     location,
@@ -807,6 +816,15 @@ impl Room {
                     let _ = this.leave(cx);
                 }
 
+                this.user_store.update(cx, |user_store, cx| {
+                    let participant_indices_by_user_id = this
+                        .remote_participants
+                        .iter()
+                        .map(|(user_id, participant)| (*user_id, participant.participant_index))
+                        .collect();
+                    user_store.set_participant_indices(participant_indices_by_user_id, cx);
+                });
+
                 this.check_invariants();
                 cx.notify();
             });
@@ -1003,6 +1021,7 @@ impl Room {
     ) -> Task<Result<ModelHandle<Project>>> {
         let client = self.client.clone();
         let user_store = self.user_store.clone();
+        cx.emit(Event::RemoteProjectJoined { project_id: id });
         cx.spawn(|this, mut cx| async move {
             let project =
                 Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;

crates/channel/src/channel_buffer.rs 🔗

@@ -1,22 +1,25 @@
 use crate::Channel;
 use anyhow::Result;
-use client::Client;
+use client::{Client, Collaborator, UserStore};
+use collections::HashMap;
 use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle};
-use rpc::{proto, TypedEnvelope};
+use rpc::{
+    proto::{self, PeerId},
+    TypedEnvelope,
+};
 use std::sync::Arc;
 use util::ResultExt;
 
 pub(crate) fn init(client: &Arc<Client>) {
     client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
-    client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator);
-    client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator);
-    client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborator);
+    client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators);
 }
 
 pub struct ChannelBuffer {
     pub(crate) channel: Arc<Channel>,
     connected: bool,
-    collaborators: Vec<proto::Collaborator>,
+    collaborators: HashMap<PeerId, Collaborator>,
+    user_store: ModelHandle<UserStore>,
     buffer: ModelHandle<language::Buffer>,
     buffer_epoch: u64,
     client: Arc<Client>,
@@ -46,6 +49,7 @@ impl ChannelBuffer {
     pub(crate) async fn new(
         channel: Arc<Channel>,
         client: Arc<Client>,
+        user_store: ModelHandle<UserStore>,
         mut cx: AsyncAppContext,
     ) -> Result<ModelHandle<Self>> {
         let response = client
@@ -61,8 +65,6 @@ impl ChannelBuffer {
             .map(language::proto::deserialize_operation)
             .collect::<Result<Vec<_>, _>>()?;
 
-        let collaborators = response.collaborators;
-
         let buffer = cx.add_model(|_| {
             language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
         });
@@ -73,34 +75,45 @@ impl ChannelBuffer {
         anyhow::Ok(cx.add_model(|cx| {
             cx.subscribe(&buffer, Self::on_buffer_update).detach();
 
-            Self {
+            let mut this = Self {
                 buffer,
                 buffer_epoch: response.epoch,
                 client,
                 connected: true,
-                collaborators,
+                collaborators: Default::default(),
                 channel,
                 subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
-            }
+                user_store,
+            };
+            this.replace_collaborators(response.collaborators, cx);
+            this
         }))
     }
 
+    pub fn user_store(&self) -> &ModelHandle<UserStore> {
+        &self.user_store
+    }
+
     pub(crate) fn replace_collaborators(
         &mut self,
         collaborators: Vec<proto::Collaborator>,
         cx: &mut ModelContext<Self>,
     ) {
-        for old_collaborator in &self.collaborators {
-            if collaborators
-                .iter()
-                .any(|c| c.replica_id == old_collaborator.replica_id)
-            {
+        let mut new_collaborators = HashMap::default();
+        for collaborator in collaborators {
+            if let Ok(collaborator) = Collaborator::from_proto(collaborator) {
+                new_collaborators.insert(collaborator.peer_id, collaborator);
+            }
+        }
+
+        for (_, old_collaborator) in &self.collaborators {
+            if !new_collaborators.contains_key(&old_collaborator.peer_id) {
                 self.buffer.update(cx, |buffer, cx| {
                     buffer.remove_peer(old_collaborator.replica_id as u16, cx)
                 });
             }
         }
-        self.collaborators = collaborators;
+        self.collaborators = new_collaborators;
         cx.emit(ChannelBufferEvent::CollaboratorsChanged);
         cx.notify();
     }
@@ -127,64 +140,14 @@ impl ChannelBuffer {
         Ok(())
     }
 
-    async fn handle_add_channel_buffer_collaborator(
-        this: ModelHandle<Self>,
-        envelope: TypedEnvelope<proto::AddChannelBufferCollaborator>,
-        _: Arc<Client>,
-        mut cx: AsyncAppContext,
-    ) -> Result<()> {
-        let collaborator = envelope.payload.collaborator.ok_or_else(|| {
-            anyhow::anyhow!(
-                "Should have gotten a collaborator in the AddChannelBufferCollaborator message"
-            )
-        })?;
-
-        this.update(&mut cx, |this, cx| {
-            this.collaborators.push(collaborator);
-            cx.emit(ChannelBufferEvent::CollaboratorsChanged);
-            cx.notify();
-        });
-
-        Ok(())
-    }
-
-    async fn handle_remove_channel_buffer_collaborator(
+    async fn handle_update_channel_buffer_collaborators(
         this: ModelHandle<Self>,
-        message: TypedEnvelope<proto::RemoveChannelBufferCollaborator>,
+        message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
         _: Arc<Client>,
         mut cx: AsyncAppContext,
     ) -> Result<()> {
         this.update(&mut cx, |this, cx| {
-            this.collaborators.retain(|collaborator| {
-                if collaborator.peer_id == message.payload.peer_id {
-                    this.buffer.update(cx, |buffer, cx| {
-                        buffer.remove_peer(collaborator.replica_id as u16, cx)
-                    });
-                    false
-                } else {
-                    true
-                }
-            });
-            cx.emit(ChannelBufferEvent::CollaboratorsChanged);
-            cx.notify();
-        });
-
-        Ok(())
-    }
-
-    async fn handle_update_channel_buffer_collaborator(
-        this: ModelHandle<Self>,
-        message: TypedEnvelope<proto::UpdateChannelBufferCollaborator>,
-        _: Arc<Client>,
-        mut cx: AsyncAppContext,
-    ) -> Result<()> {
-        this.update(&mut cx, |this, cx| {
-            for collaborator in &mut this.collaborators {
-                if collaborator.peer_id == message.payload.old_peer_id {
-                    collaborator.peer_id = message.payload.new_peer_id;
-                    break;
-                }
-            }
+            this.replace_collaborators(message.payload.collaborators, cx);
             cx.emit(ChannelBufferEvent::CollaboratorsChanged);
             cx.notify();
         });
@@ -217,7 +180,7 @@ impl ChannelBuffer {
         self.buffer.clone()
     }
 
-    pub fn collaborators(&self) -> &[proto::Collaborator] {
+    pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
         &self.collaborators
     }
 

crates/channel/src/channel_store.rs 🔗

@@ -198,10 +198,11 @@ impl ChannelStore {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ModelHandle<ChannelBuffer>>> {
         let client = self.client.clone();
+        let user_store = self.user_store.clone();
         self.open_channel_resource(
             channel_id,
             |this| &mut this.opened_buffers,
-            |channel, cx| ChannelBuffer::new(channel, client, cx),
+            |channel, cx| ChannelBuffer::new(channel, client, user_store, cx),
             cx,
         )
     }

crates/client/Cargo.toml 🔗

@@ -33,15 +33,16 @@ parking_lot.workspace = true
 postage.workspace = true
 rand.workspace = true
 schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
 smol.workspace = true
+sysinfo.workspace = true
+tempfile = "3"
 thiserror.workspace = true
 time.workspace = true
 tiny_http = "0.8"
-uuid = { version = "1.1.2", features = ["v4"] }
 url = "2.2"
-serde.workspace = true
-serde_derive.workspace = true
-tempfile = "3"
+uuid = { version = "1.1.2", features = ["v4"] }
 
 [dev-dependencies]
 collections = { path = "../collections", features = ["test-support"] }

crates/client/src/client.rs 🔗

@@ -62,6 +62,8 @@ lazy_static! {
         .and_then(|v| v.parse().ok());
     pub static ref ZED_APP_PATH: Option<PathBuf> =
         std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
+    pub static ref ZED_ALWAYS_ACTIVE: bool =
+        std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| e.len() > 0);
 }
 
 pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";

crates/client/src/telemetry.rs 🔗

@@ -4,6 +4,7 @@ use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use serde::Serialize;
 use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
+use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
 use tempfile::NamedTempFile;
 use util::http::HttpClient;
 use util::{channel::ReleaseChannel, TryFutureExt};
@@ -88,6 +89,16 @@ pub enum ClickhouseEvent {
         kind: AssistantKind,
         model: &'static str,
     },
+    Cpu {
+        usage_as_percent: f32,
+        core_count: u32,
+    },
+    Memory {
+        memory_in_bytes: u64,
+        virtual_memory_in_bytes: u64,
+        start_time_in_seconds: u64,
+        run_time_in_seconds: u64,
+    },
 }
 
 #[cfg(debug_assertions)]
@@ -136,7 +147,7 @@ impl Telemetry {
         Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
     }
 
-    pub fn start(self: &Arc<Self>, installation_id: Option<String>) {
+    pub fn start(self: &Arc<Self>, installation_id: Option<String>, cx: &mut AppContext) {
         let mut state = self.state.lock();
         state.installation_id = installation_id.map(|id| id.into());
         let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
@@ -145,6 +156,48 @@ impl Telemetry {
         if has_clickhouse_events {
             self.flush_clickhouse_events();
         }
+
+        let this = self.clone();
+        cx.spawn(|mut cx| async move {
+            let mut system = System::new_all();
+            system.refresh_all();
+
+            loop {
+                // Waiting some amount of time before the first query is important to get a reasonable value
+                // https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
+                const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
+                smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
+
+                let telemetry_settings = cx.update(|cx| *settings::get::<TelemetrySettings>(cx));
+
+                system.refresh_memory();
+                system.refresh_processes();
+
+                let current_process = Pid::from_u32(std::process::id());
+                let Some(process) = system.processes().get(&current_process) else {
+                    let process = current_process;
+                    log::error!("Failed to find own process {process:?} in system process table");
+                    // TODO: Fire an error telemetry event
+                    return;
+                };
+
+                let memory_event = ClickhouseEvent::Memory {
+                    memory_in_bytes: process.memory(),
+                    virtual_memory_in_bytes: process.virtual_memory(),
+                    start_time_in_seconds: process.start_time(),
+                    run_time_in_seconds: process.run_time(),
+                };
+
+                let cpu_event = ClickhouseEvent::Cpu {
+                    usage_as_percent: process.cpu_usage(),
+                    core_count: system.cpus().len() as u32,
+                };
+
+                this.report_clickhouse_event(memory_event, telemetry_settings);
+                this.report_clickhouse_event(cpu_event, telemetry_settings);
+            }
+        })
+        .detach();
     }
 
     pub fn set_authenticated_user_info(

crates/client/src/user.rs 🔗

@@ -7,11 +7,15 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
 use postage::{sink::Sink, watch};
 use rpc::proto::{RequestMessage, UsersResponse};
 use std::sync::{Arc, Weak};
+use text::ReplicaId;
 use util::http::HttpClient;
 use util::TryFutureExt as _;
 
 pub type UserId = u64;
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct ParticipantIndex(pub u32);
+
 #[derive(Default, Debug)]
 pub struct User {
     pub id: UserId,
@@ -19,6 +23,13 @@ pub struct User {
     pub avatar: Option<Arc<ImageData>>,
 }
 
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Collaborator {
+    pub peer_id: proto::PeerId,
+    pub replica_id: ReplicaId,
+    pub user_id: UserId,
+}
+
 impl PartialOrd for User {
     fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
         Some(self.cmp(other))
@@ -56,6 +67,7 @@ pub enum ContactRequestStatus {
 
 pub struct UserStore {
     users: HashMap<u64, Arc<User>>,
+    participant_indices: HashMap<u64, ParticipantIndex>,
     update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
     current_user: watch::Receiver<Option<Arc<User>>>,
     contacts: Vec<Arc<Contact>>,
@@ -81,6 +93,7 @@ pub enum Event {
         kind: ContactEventKind,
     },
     ShowContacts,
+    ParticipantIndicesChanged,
 }
 
 #[derive(Clone, Copy)]
@@ -118,6 +131,7 @@ impl UserStore {
             current_user: current_user_rx,
             contacts: Default::default(),
             incoming_contact_requests: Default::default(),
+            participant_indices: Default::default(),
             outgoing_contact_requests: Default::default(),
             invite_info: None,
             client: Arc::downgrade(&client),
@@ -581,6 +595,10 @@ impl UserStore {
         self.load_users(proto::FuzzySearchUsers { query }, cx)
     }
 
+    pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
+        self.users.get(&user_id).cloned()
+    }
+
     pub fn get_user(
         &mut self,
         user_id: u64,
@@ -641,6 +659,21 @@ impl UserStore {
             }
         })
     }
+
+    pub fn set_participant_indices(
+        &mut self,
+        participant_indices: HashMap<u64, ParticipantIndex>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        if participant_indices != self.participant_indices {
+            self.participant_indices = participant_indices;
+            cx.emit(Event::ParticipantIndicesChanged);
+        }
+    }
+
+    pub fn participant_indices(&self) -> &HashMap<u64, ParticipantIndex> {
+        &self.participant_indices
+    }
 }
 
 impl User {
@@ -672,6 +705,16 @@ impl Contact {
     }
 }
 
+impl Collaborator {
+    pub fn from_proto(message: proto::Collaborator) -> Result<Self> {
+        Ok(Self {
+            peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
+            replica_id: message.replica_id as ReplicaId,
+            user_id: message.user_id as UserId,
+        })
+    }
+}
+
 async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
     let mut response = http
         .get(url, Default::default(), true)

crates/collab/migrations.sqlite/20221109000000_test_schema.sql 🔗

@@ -158,7 +158,8 @@ CREATE TABLE "room_participants" (
     "initial_project_id" INTEGER,
     "calling_user_id" INTEGER NOT NULL REFERENCES users (id),
     "calling_connection_id" INTEGER NOT NULL,
-    "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL
+    "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL,
+    "participant_index" INTEGER
 );
 CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
 CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");

crates/collab/src/db.rs 🔗

@@ -510,7 +510,7 @@ pub struct RefreshedRoom {
 
 pub struct RefreshedChannelBuffer {
     pub connection_ids: Vec<ConnectionId>,
-    pub removed_collaborators: Vec<proto::RemoveChannelBufferCollaborator>,
+    pub collaborators: Vec<proto::Collaborator>,
 }
 
 pub struct Project {

crates/collab/src/db/queries/buffers.rs 🔗

@@ -2,6 +2,12 @@ use super::*;
 use prost::Message;
 use text::{EditOperation, UndoOperation};
 
+pub struct LeftChannelBuffer {
+    pub channel_id: ChannelId,
+    pub collaborators: Vec<proto::Collaborator>,
+    pub connections: Vec<ConnectionId>,
+}
+
 impl Database {
     pub async fn join_channel_buffer(
         &self,
@@ -204,23 +210,26 @@ impl Database {
         server_id: ServerId,
     ) -> Result<RefreshedChannelBuffer> {
         self.transaction(|tx| async move {
-            let collaborators = channel_buffer_collaborator::Entity::find()
+            let db_collaborators = channel_buffer_collaborator::Entity::find()
                 .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
                 .all(&*tx)
                 .await?;
 
             let mut connection_ids = Vec::new();
-            let mut removed_collaborators = Vec::new();
+            let mut collaborators = Vec::new();
             let mut collaborator_ids_to_remove = Vec::new();
-            for collaborator in &collaborators {
-                if !collaborator.connection_lost && collaborator.connection_server_id == server_id {
-                    connection_ids.push(collaborator.connection());
+            for db_collaborator in &db_collaborators {
+                if !db_collaborator.connection_lost
+                    && db_collaborator.connection_server_id == server_id
+                {
+                    connection_ids.push(db_collaborator.connection());
+                    collaborators.push(proto::Collaborator {
+                        peer_id: Some(db_collaborator.connection().into()),
+                        replica_id: db_collaborator.replica_id.0 as u32,
+                        user_id: db_collaborator.user_id.to_proto(),
+                    })
                 } else {
-                    removed_collaborators.push(proto::RemoveChannelBufferCollaborator {
-                        channel_id: channel_id.to_proto(),
-                        peer_id: Some(collaborator.connection().into()),
-                    });
-                    collaborator_ids_to_remove.push(collaborator.id);
+                    collaborator_ids_to_remove.push(db_collaborator.id);
                 }
             }
 
@@ -231,7 +240,7 @@ impl Database {
 
             Ok(RefreshedChannelBuffer {
                 connection_ids,
-                removed_collaborators,
+                collaborators,
             })
         })
         .await
@@ -241,7 +250,7 @@ impl Database {
         &self,
         channel_id: ChannelId,
         connection: ConnectionId,
-    ) -> Result<Vec<ConnectionId>> {
+    ) -> Result<LeftChannelBuffer> {
         self.transaction(|tx| async move {
             self.leave_channel_buffer_internal(channel_id, connection, &*tx)
                 .await
@@ -275,7 +284,7 @@ impl Database {
     pub async fn leave_channel_buffers(
         &self,
         connection: ConnectionId,
-    ) -> Result<Vec<(ChannelId, Vec<ConnectionId>)>> {
+    ) -> Result<Vec<LeftChannelBuffer>> {
         self.transaction(|tx| async move {
             #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
             enum QueryChannelIds {
@@ -294,10 +303,10 @@ impl Database {
 
             let mut result = Vec::new();
             for channel_id in channel_ids {
-                let collaborators = self
+                let left_channel_buffer = self
                     .leave_channel_buffer_internal(channel_id, connection, &*tx)
                     .await?;
-                result.push((channel_id, collaborators));
+                result.push(left_channel_buffer);
             }
 
             Ok(result)
@@ -310,7 +319,7 @@ impl Database {
         channel_id: ChannelId,
         connection: ConnectionId,
         tx: &DatabaseTransaction,
-    ) -> Result<Vec<ConnectionId>> {
+    ) -> Result<LeftChannelBuffer> {
         let result = channel_buffer_collaborator::Entity::delete_many()
             .filter(
                 Condition::all()
@@ -327,6 +336,7 @@ impl Database {
             Err(anyhow!("not a collaborator on this project"))?;
         }
 
+        let mut collaborators = Vec::new();
         let mut connections = Vec::new();
         let mut rows = channel_buffer_collaborator::Entity::find()
             .filter(
@@ -336,19 +346,26 @@ impl Database {
             .await?;
         while let Some(row) = rows.next().await {
             let row = row?;
-            connections.push(ConnectionId {
-                id: row.connection_id as u32,
-                owner_id: row.connection_server_id.0 as u32,
+            let connection = row.connection();
+            connections.push(connection);
+            collaborators.push(proto::Collaborator {
+                peer_id: Some(connection.into()),
+                replica_id: row.replica_id.0 as u32,
+                user_id: row.user_id.to_proto(),
             });
         }
 
         drop(rows);
 
-        if connections.is_empty() {
+        if collaborators.is_empty() {
             self.snapshot_channel_buffer(channel_id, &tx).await?;
         }
 
-        Ok(connections)
+        Ok(LeftChannelBuffer {
+            channel_id,
+            collaborators,
+            connections,
+        })
     }
 
     pub async fn get_channel_buffer_collaborators(

crates/collab/src/db/queries/projects.rs 🔗

@@ -738,7 +738,7 @@ impl Database {
                     Condition::any()
                         .add(
                             Condition::all()
-                                .add(follower::Column::ProjectId.eq(project_id))
+                                .add(follower::Column::ProjectId.eq(Some(project_id)))
                                 .add(
                                     follower::Column::LeaderConnectionServerId
                                         .eq(connection.owner_id),
@@ -747,7 +747,7 @@ impl Database {
                         )
                         .add(
                             Condition::all()
-                                .add(follower::Column::ProjectId.eq(project_id))
+                                .add(follower::Column::ProjectId.eq(Some(project_id)))
                                 .add(
                                     follower::Column::FollowerConnectionServerId
                                         .eq(connection.owner_id),
@@ -862,13 +862,46 @@ impl Database {
         .await
     }
 
+    pub async fn check_room_participants(
+        &self,
+        room_id: RoomId,
+        leader_id: ConnectionId,
+        follower_id: ConnectionId,
+    ) -> Result<()> {
+        self.transaction(|tx| async move {
+            use room_participant::Column;
+
+            let count = room_participant::Entity::find()
+                .filter(
+                    Condition::all().add(Column::RoomId.eq(room_id)).add(
+                        Condition::any()
+                            .add(Column::AnsweringConnectionId.eq(leader_id.id as i32).and(
+                                Column::AnsweringConnectionServerId.eq(leader_id.owner_id as i32),
+                            ))
+                            .add(Column::AnsweringConnectionId.eq(follower_id.id as i32).and(
+                                Column::AnsweringConnectionServerId.eq(follower_id.owner_id as i32),
+                            )),
+                    ),
+                )
+                .count(&*tx)
+                .await?;
+
+            if count < 2 {
+                Err(anyhow!("not room participants"))?;
+            }
+
+            Ok(())
+        })
+        .await
+    }
+
     pub async fn follow(
         &self,
+        room_id: RoomId,
         project_id: ProjectId,
         leader_connection: ConnectionId,
         follower_connection: ConnectionId,
     ) -> Result<RoomGuard<proto::Room>> {
-        let room_id = self.room_id_for_project(project_id).await?;
         self.room_transaction(room_id, |tx| async move {
             follower::ActiveModel {
                 room_id: ActiveValue::set(room_id),
@@ -894,15 +927,16 @@ impl Database {
 
     pub async fn unfollow(
         &self,
+        room_id: RoomId,
         project_id: ProjectId,
         leader_connection: ConnectionId,
         follower_connection: ConnectionId,
     ) -> Result<RoomGuard<proto::Room>> {
-        let room_id = self.room_id_for_project(project_id).await?;
         self.room_transaction(room_id, |tx| async move {
             follower::Entity::delete_many()
                 .filter(
                     Condition::all()
+                        .add(follower::Column::RoomId.eq(room_id))
                         .add(follower::Column::ProjectId.eq(project_id))
                         .add(
                             follower::Column::LeaderConnectionServerId

crates/collab/src/db/queries/rooms.rs 🔗

@@ -128,6 +128,7 @@ impl Database {
                 calling_connection_server_id: ActiveValue::set(Some(ServerId(
                     connection.owner_id as i32,
                 ))),
+                participant_index: ActiveValue::set(Some(0)),
                 ..Default::default()
             }
             .insert(&*tx)
@@ -152,6 +153,7 @@ impl Database {
                 room_id: ActiveValue::set(room_id),
                 user_id: ActiveValue::set(called_user_id),
                 answering_connection_lost: ActiveValue::set(false),
+                participant_index: ActiveValue::NotSet,
                 calling_user_id: ActiveValue::set(calling_user_id),
                 calling_connection_id: ActiveValue::set(calling_connection.id as i32),
                 calling_connection_server_id: ActiveValue::set(Some(ServerId(
@@ -283,6 +285,26 @@ impl Database {
                 .await?
                 .ok_or_else(|| anyhow!("no such room"))?;
 
+            #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+            enum QueryParticipantIndices {
+                ParticipantIndex,
+            }
+            let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
+                .filter(
+                    room_participant::Column::RoomId
+                        .eq(room_id)
+                        .and(room_participant::Column::ParticipantIndex.is_not_null()),
+                )
+                .select_only()
+                .column(room_participant::Column::ParticipantIndex)
+                .into_values::<_, QueryParticipantIndices>()
+                .all(&*tx)
+                .await?;
+            let mut participant_index = 0;
+            while existing_participant_indices.contains(&participant_index) {
+                participant_index += 1;
+            }
+
             if let Some(channel_id) = channel_id {
                 self.check_user_is_channel_member(channel_id, user_id, &*tx)
                     .await?;
@@ -300,6 +322,7 @@ impl Database {
                     calling_connection_server_id: ActiveValue::set(Some(ServerId(
                         connection.owner_id as i32,
                     ))),
+                    participant_index: ActiveValue::Set(Some(participant_index)),
                     ..Default::default()
                 }])
                 .on_conflict(
@@ -308,6 +331,7 @@ impl Database {
                             room_participant::Column::AnsweringConnectionId,
                             room_participant::Column::AnsweringConnectionServerId,
                             room_participant::Column::AnsweringConnectionLost,
+                            room_participant::Column::ParticipantIndex,
                         ])
                         .to_owned(),
                 )
@@ -322,6 +346,7 @@ impl Database {
                             .add(room_participant::Column::AnsweringConnectionId.is_null()),
                     )
                     .set(room_participant::ActiveModel {
+                        participant_index: ActiveValue::Set(Some(participant_index)),
                         answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
                         answering_connection_server_id: ActiveValue::set(Some(ServerId(
                             connection.owner_id as i32,
@@ -960,6 +985,39 @@ impl Database {
         Ok(room)
     }
 
+    pub async fn room_connection_ids(
+        &self,
+        room_id: RoomId,
+        connection_id: ConnectionId,
+    ) -> Result<RoomGuard<HashSet<ConnectionId>>> {
+        self.room_transaction(room_id, |tx| async move {
+            let mut participants = room_participant::Entity::find()
+                .filter(room_participant::Column::RoomId.eq(room_id))
+                .stream(&*tx)
+                .await?;
+
+            let mut is_participant = false;
+            let mut connection_ids = HashSet::default();
+            while let Some(participant) = participants.next().await {
+                let participant = participant?;
+                if let Some(answering_connection) = participant.answering_connection() {
+                    if answering_connection == connection_id {
+                        is_participant = true;
+                    } else {
+                        connection_ids.insert(answering_connection);
+                    }
+                }
+            }
+
+            if !is_participant {
+                Err(anyhow!("not a room participant"))?;
+            }
+
+            Ok(connection_ids)
+        })
+        .await
+    }
+
     async fn get_channel_room(
         &self,
         room_id: RoomId,
@@ -978,10 +1036,15 @@ impl Database {
         let mut pending_participants = Vec::new();
         while let Some(db_participant) = db_participants.next().await {
             let db_participant = db_participant?;
-            if let Some((answering_connection_id, answering_connection_server_id)) = db_participant
-                .answering_connection_id
-                .zip(db_participant.answering_connection_server_id)
-            {
+            if let (
+                Some(answering_connection_id),
+                Some(answering_connection_server_id),
+                Some(participant_index),
+            ) = (
+                db_participant.answering_connection_id,
+                db_participant.answering_connection_server_id,
+                db_participant.participant_index,
+            ) {
                 let location = match (
                     db_participant.location_kind,
                     db_participant.location_project_id,
@@ -1012,6 +1075,7 @@ impl Database {
                         peer_id: Some(answering_connection.into()),
                         projects: Default::default(),
                         location: Some(proto::ParticipantLocation { variant: location }),
+                        participant_index: participant_index as u32,
                     },
                 );
             } else {

crates/collab/src/db/tables/room_participant.rs 🔗

@@ -1,4 +1,5 @@
 use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
+use rpc::ConnectionId;
 use sea_orm::entity::prelude::*;
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@@ -17,6 +18,16 @@ pub struct Model {
     pub calling_user_id: UserId,
     pub calling_connection_id: i32,
     pub calling_connection_server_id: Option<ServerId>,
+    pub participant_index: Option<i32>,
+}
+
+impl Model {
+    pub fn answering_connection(&self) -> Option<ConnectionId> {
+        Some(ConnectionId {
+            owner_id: self.answering_connection_server_id?.0 as u32,
+            id: self.answering_connection_id? as u32,
+        })
+    }
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

crates/collab/src/db/tests/buffer_tests.rs 🔗

@@ -134,12 +134,12 @@ async fn test_channel_buffers(db: &Arc<Database>) {
     let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
     assert_eq!(zed_collaborats, &[a_id, b_id]);
 
-    let collaborators = db
+    let left_buffer = db
         .leave_channel_buffer(zed_id, connection_id_b)
         .await
         .unwrap();
 
-    assert_eq!(collaborators, &[connection_id_a],);
+    assert_eq!(left_buffer.connections, &[connection_id_a],);
 
     let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
     let _ = db

crates/collab/src/rpc.rs 🔗

@@ -38,8 +38,8 @@ use lazy_static::lazy_static;
 use prometheus::{register_int_gauge, IntGauge};
 use rpc::{
     proto::{
-        self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, ChannelEdge, EntityMessage,
-        EnvelopedMessage, LiveKitConnectionInfo, RequestMessage,
+        self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage,
+        LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators,
     },
     Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
 };
@@ -313,9 +313,16 @@ impl Server {
                             .trace_err()
                         {
                             for connection_id in refreshed_channel_buffer.connection_ids {
-                                for message in &refreshed_channel_buffer.removed_collaborators {
-                                    peer.send(connection_id, message.clone()).trace_err();
-                                }
+                                peer.send(
+                                    connection_id,
+                                    proto::UpdateChannelBufferCollaborators {
+                                        channel_id: channel_id.to_proto(),
+                                        collaborators: refreshed_channel_buffer
+                                            .collaborators
+                                            .clone(),
+                                    },
+                                )
+                                .trace_err();
                             }
                         }
                     }
@@ -1883,24 +1890,19 @@ async fn follow(
     response: Response<proto::Follow>,
     session: Session,
 ) -> Result<()> {
-    let project_id = ProjectId::from_proto(request.project_id);
+    let room_id = RoomId::from_proto(request.room_id);
+    let project_id = request.project_id.map(ProjectId::from_proto);
     let leader_id = request
         .leader_id
         .ok_or_else(|| anyhow!("invalid leader id"))?
         .into();
     let follower_id = session.connection_id;
 
-    {
-        let project_connection_ids = session
-            .db()
-            .await
-            .project_connection_ids(project_id, session.connection_id)
-            .await?;
-
-        if !project_connection_ids.contains(&leader_id) {
-            Err(anyhow!("no such peer"))?;
-        }
-    }
+    session
+        .db()
+        .await
+        .check_room_participants(room_id, leader_id, session.connection_id)
+        .await?;
 
     let mut response_payload = session
         .peer
@@ -1911,56 +1913,63 @@ async fn follow(
         .retain(|view| view.leader_id != Some(follower_id.into()));
     response.send(response_payload)?;
 
-    let room = session
-        .db()
-        .await
-        .follow(project_id, leader_id, follower_id)
-        .await?;
-    room_updated(&room, &session.peer);
+    if let Some(project_id) = project_id {
+        let room = session
+            .db()
+            .await
+            .follow(room_id, project_id, leader_id, follower_id)
+            .await?;
+        room_updated(&room, &session.peer);
+    }
 
     Ok(())
 }
 
 async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
-    let project_id = ProjectId::from_proto(request.project_id);
+    let room_id = RoomId::from_proto(request.room_id);
+    let project_id = request.project_id.map(ProjectId::from_proto);
     let leader_id = request
         .leader_id
         .ok_or_else(|| anyhow!("invalid leader id"))?
         .into();
     let follower_id = session.connection_id;
 
-    if !session
+    session
         .db()
         .await
-        .project_connection_ids(project_id, session.connection_id)
-        .await?
-        .contains(&leader_id)
-    {
-        Err(anyhow!("no such peer"))?;
-    }
+        .check_room_participants(room_id, leader_id, session.connection_id)
+        .await?;
 
     session
         .peer
         .forward_send(session.connection_id, leader_id, request)?;
 
-    let room = session
-        .db()
-        .await
-        .unfollow(project_id, leader_id, follower_id)
-        .await?;
-    room_updated(&room, &session.peer);
+    if let Some(project_id) = project_id {
+        let room = session
+            .db()
+            .await
+            .unfollow(room_id, project_id, leader_id, follower_id)
+            .await?;
+        room_updated(&room, &session.peer);
+    }
 
     Ok(())
 }
 
 async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> {
-    let project_id = ProjectId::from_proto(request.project_id);
-    let project_connection_ids = session
-        .db
-        .lock()
-        .await
-        .project_connection_ids(project_id, session.connection_id)
-        .await?;
+    let room_id = RoomId::from_proto(request.room_id);
+    let database = session.db.lock().await;
+
+    let connection_ids = if let Some(project_id) = request.project_id {
+        let project_id = ProjectId::from_proto(project_id);
+        database
+            .project_connection_ids(project_id, session.connection_id)
+            .await?
+    } else {
+        database
+            .room_connection_ids(room_id, session.connection_id)
+            .await?
+    };
 
     let leader_id = request.variant.as_ref().and_then(|variant| match variant {
         proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
@@ -1969,9 +1978,7 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
     });
     for follower_peer_id in request.follower_ids.iter().copied() {
         let follower_connection_id = follower_peer_id.into();
-        if project_connection_ids.contains(&follower_connection_id)
-            && Some(follower_peer_id) != leader_id
-        {
+        if Some(follower_peer_id) != leader_id && connection_ids.contains(&follower_connection_id) {
             session.peer.forward_send(
                 session.connection_id,
                 follower_connection_id,
@@ -2658,18 +2665,12 @@ async fn join_channel_buffer(
         .join_channel_buffer(channel_id, session.user_id, session.connection_id)
         .await?;
 
-    let replica_id = open_response.replica_id;
     let collaborators = open_response.collaborators.clone();
-
     response.send(open_response)?;
 
-    let update = AddChannelBufferCollaborator {
+    let update = UpdateChannelBufferCollaborators {
         channel_id: channel_id.to_proto(),
-        collaborator: Some(proto::Collaborator {
-            user_id: session.user_id.to_proto(),
-            peer_id: Some(session.connection_id.into()),
-            replica_id,
-        }),
+        collaborators: collaborators.clone(),
     };
     channel_buffer_updated(
         session.connection_id,
@@ -2716,8 +2717,8 @@ async fn rejoin_channel_buffers(
         .rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id)
         .await?;
 
-    for buffer in &buffers {
-        let collaborators_to_notify = buffer
+    for rejoined_buffer in &buffers {
+        let collaborators_to_notify = rejoined_buffer
             .buffer
             .collaborators
             .iter()
@@ -2725,10 +2726,9 @@ async fn rejoin_channel_buffers(
         channel_buffer_updated(
             session.connection_id,
             collaborators_to_notify,
-            &proto::UpdateChannelBufferCollaborator {
-                channel_id: buffer.buffer.channel_id,
-                old_peer_id: Some(buffer.old_connection_id.into()),
-                new_peer_id: Some(session.connection_id.into()),
+            &proto::UpdateChannelBufferCollaborators {
+                channel_id: rejoined_buffer.buffer.channel_id,
+                collaborators: rejoined_buffer.buffer.collaborators.clone(),
             },
             &session.peer,
         );
@@ -2749,7 +2749,7 @@ async fn leave_channel_buffer(
     let db = session.db().await;
     let channel_id = ChannelId::from_proto(request.channel_id);
 
-    let collaborators_to_notify = db
+    let left_buffer = db
         .leave_channel_buffer(channel_id, session.connection_id)
         .await?;
 
@@ -2757,10 +2757,10 @@ async fn leave_channel_buffer(
 
     channel_buffer_updated(
         session.connection_id,
-        collaborators_to_notify,
-        &proto::RemoveChannelBufferCollaborator {
+        left_buffer.connections,
+        &proto::UpdateChannelBufferCollaborators {
             channel_id: channel_id.to_proto(),
-            peer_id: Some(session.connection_id.into()),
+            collaborators: left_buffer.collaborators,
         },
         &session.peer,
     );
@@ -3235,13 +3235,13 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
         .leave_channel_buffers(session.connection_id)
         .await?;
 
-    for (channel_id, connections) in left_channel_buffers {
+    for left_buffer in left_channel_buffers {
         channel_buffer_updated(
             session.connection_id,
-            connections,
-            &proto::RemoveChannelBufferCollaborator {
-                channel_id: channel_id.to_proto(),
-                peer_id: Some(session.connection_id.into()),
+            left_buffer.connections,
+            &proto::UpdateChannelBufferCollaborators {
+                channel_id: left_buffer.channel_id.to_proto(),
+                collaborators: left_buffer.collaborators,
             },
             &session.peer,
         );

crates/collab/src/tests.rs 🔗

@@ -4,6 +4,7 @@ use gpui::{ModelHandle, TestAppContext};
 mod channel_buffer_tests;
 mod channel_message_tests;
 mod channel_tests;
+mod following_tests;
 mod integration_tests;
 mod random_channel_buffer_tests;
 mod random_project_collaboration_tests;

crates/collab/src/tests/channel_buffer_tests.rs 🔗

@@ -4,14 +4,16 @@ use crate::{
 };
 use call::ActiveCall;
 use channel::Channel;
-use client::UserId;
+use client::ParticipantIndex;
+use client::{Collaborator, UserId};
 use collab_ui::channel_view::ChannelView;
 use collections::HashMap;
+use editor::{Anchor, Editor, ToOffset};
 use futures::future;
-use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
-use rpc::{proto, RECEIVE_TIMEOUT};
+use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
+use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
 use serde_json::json;
-use std::sync::Arc;
+use std::{ops::Range, sync::Arc};
 
 #[gpui::test]
 async fn test_core_channel_buffers(
@@ -100,7 +102,7 @@ async fn test_core_channel_buffers(
     channel_buffer_b.read_with(cx_b, |buffer, _| {
         assert_collaborators(
             &buffer.collaborators(),
-            &[client_b.user_id(), client_a.user_id()],
+            &[client_a.user_id(), client_b.user_id()],
         );
     });
 
@@ -120,10 +122,10 @@ async fn test_core_channel_buffers(
 }
 
 #[gpui::test]
-async fn test_channel_buffer_replica_ids(
+async fn test_channel_notes_participant_indices(
     deterministic: Arc<Deterministic>,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
+    mut cx_a: &mut TestAppContext,
+    mut cx_b: &mut TestAppContext,
     cx_c: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
@@ -132,6 +134,13 @@ async fn test_channel_buffer_replica_ids(
     let client_b = server.create_client(cx_b, "user_b").await;
     let client_c = server.create_client(cx_c, "user_c").await;
 
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+    cx_c.update(editor::init);
+
     let channel_id = server
         .make_channel(
             "the-channel",
@@ -141,140 +150,173 @@ async fn test_channel_buffer_replica_ids(
         )
         .await;
 
-    let active_call_a = cx_a.read(ActiveCall::global);
-    let active_call_b = cx_b.read(ActiveCall::global);
-    let active_call_c = cx_c.read(ActiveCall::global);
-
-    // Clients A and B join a channel.
-    active_call_a
-        .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
-        .await
-        .unwrap();
-    active_call_b
-        .update(cx_b, |call, cx| call.join_channel(channel_id, cx))
-        .await
-        .unwrap();
-
-    // Clients A, B, and C join a channel buffer
-    // C first so that the replica IDs in the project and the channel buffer are different
-    let channel_buffer_c = client_c
-        .channel_store()
-        .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx))
+    client_a
+        .fs()
+        .insert_tree("/root", json!({"file.txt": "123"}))
+        .await;
+    let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await;
+    let project_b = client_b.build_empty_local_project(cx_b);
+    let project_c = client_c.build_empty_local_project(cx_c);
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+    let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
+
+    // Clients A, B, and C open the channel notes
+    let channel_view_a = cx_a
+        .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx))
         .await
         .unwrap();
-    let channel_buffer_b = client_b
-        .channel_store()
-        .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
+    let channel_view_b = cx_b
+        .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
         .await
         .unwrap();
-    let channel_buffer_a = client_a
-        .channel_store()
-        .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
-        .await
-        .unwrap();
-
-    // Client B shares a project
-    client_b
-        .fs()
-        .insert_tree("/dir", json!({ "file.txt": "contents" }))
-        .await;
-    let (project_b, _) = client_b.build_local_project("/dir", cx_b).await;
-    let shared_project_id = active_call_b
-        .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
+    let channel_view_c = cx_c
+        .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx))
         .await
         .unwrap();
 
-    // Client A joins the project
-    let project_a = client_a.build_remote_project(shared_project_id, cx_a).await;
-    deterministic.run_until_parked();
-
-    // Client C is in a separate project.
-    client_c.fs().insert_tree("/dir", json!({})).await;
-    let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await;
-
-    // Note that each user has a different replica id in the projects vs the
-    // channel buffer.
-    channel_buffer_a.read_with(cx_a, |channel_buffer, cx| {
-        assert_eq!(project_a.read(cx).replica_id(), 1);
-        assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2);
+    // Clients A, B, and C all insert and select some text
+    channel_view_a.update(cx_a, |notes, cx| {
+        notes.editor.update(cx, |editor, cx| {
+            editor.insert("a", cx);
+            editor.change_selections(None, cx, |selections| {
+                selections.select_ranges(vec![0..1]);
+            });
+        });
     });
-    channel_buffer_b.read_with(cx_b, |channel_buffer, cx| {
-        assert_eq!(project_b.read(cx).replica_id(), 0);
-        assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1);
+    deterministic.run_until_parked();
+    channel_view_b.update(cx_b, |notes, cx| {
+        notes.editor.update(cx, |editor, cx| {
+            editor.move_down(&Default::default(), cx);
+            editor.insert("b", cx);
+            editor.change_selections(None, cx, |selections| {
+                selections.select_ranges(vec![1..2]);
+            });
+        });
     });
-    channel_buffer_c.read_with(cx_c, |channel_buffer, cx| {
-        // C is not in the project
-        assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0);
+    deterministic.run_until_parked();
+    channel_view_c.update(cx_c, |notes, cx| {
+        notes.editor.update(cx, |editor, cx| {
+            editor.move_down(&Default::default(), cx);
+            editor.insert("c", cx);
+            editor.change_selections(None, cx, |selections| {
+                selections.select_ranges(vec![2..3]);
+            });
+        });
     });
 
-    let channel_window_a =
-        cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx));
-    let channel_window_b =
-        cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx));
-    let channel_window_c = cx_c.add_window(|cx| {
-        ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx)
+    // Client A sees clients B and C without assigned colors, because they aren't
+    // in a call together.
+    deterministic.run_until_parked();
+    channel_view_a.update(cx_a, |notes, cx| {
+        notes.editor.update(cx, |editor, cx| {
+            assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
+        });
     });
 
-    let channel_view_a = channel_window_a.root(cx_a);
-    let channel_view_b = channel_window_b.root(cx_b);
-    let channel_view_c = channel_window_c.root(cx_c);
+    // Clients A and B join the same call.
+    for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] {
+        call.update(*cx, |call, cx| call.join_channel(channel_id, cx))
+            .await
+            .unwrap();
+    }
 
-    // For clients A and B, the replica ids in the channel buffer are mapped
-    // so that they match the same users' replica ids in their shared project.
-    channel_view_a.read_with(cx_a, |view, cx| {
-        assert_eq!(
-            view.editor.read(cx).replica_id_map().unwrap(),
-            &[(1, 0), (2, 1)].into_iter().collect::<HashMap<_, _>>()
-        );
+    // Clients A and B see each other with two different assigned colors. Client C
+    // still doesn't have a color.
+    deterministic.run_until_parked();
+    channel_view_a.update(cx_a, |notes, cx| {
+        notes.editor.update(cx, |editor, cx| {
+            assert_remote_selections(
+                editor,
+                &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
+                cx,
+            );
+        });
     });
-    channel_view_b.read_with(cx_b, |view, cx| {
-        assert_eq!(
-            view.editor.read(cx).replica_id_map().unwrap(),
-            &[(1, 0), (2, 1)].into_iter().collect::<HashMap<u16, u16>>(),
-        )
+    channel_view_b.update(cx_b, |notes, cx| {
+        notes.editor.update(cx, |editor, cx| {
+            assert_remote_selections(
+                editor,
+                &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
+                cx,
+            );
+        });
     });
 
-    // Client C only sees themself, as they're not part of any shared project
-    channel_view_c.read_with(cx_c, |view, cx| {
-        assert_eq!(
-            view.editor.read(cx).replica_id_map().unwrap(),
-            &[(0, 0)].into_iter().collect::<HashMap<u16, u16>>(),
-        );
-    });
+    // Client A shares a project, and client B joins.
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
 
-    // Client C joins the project that clients A and B are in.
-    active_call_c
-        .update(cx_c, |call, cx| call.join_channel(channel_id, cx))
+    // Clients A and B open the same file.
+    let editor_a = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
+        })
         .await
+        .unwrap()
+        .downcast::<Editor>()
         .unwrap();
-    let project_c = client_c.build_remote_project(shared_project_id, cx_c).await;
-    deterministic.run_until_parked();
-    project_c.read_with(cx_c, |project, _| {
-        assert_eq!(project.replica_id(), 2);
+    let editor_b = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    editor_a.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |selections| {
+            selections.select_ranges(vec![0..1]);
+        });
     });
+    editor_b.update(cx_b, |editor, cx| {
+        editor.change_selections(None, cx, |selections| {
+            selections.select_ranges(vec![2..3]);
+        });
+    });
+    deterministic.run_until_parked();
 
-    // For clients A and B, client C's replica id in the channel buffer is
-    // now mapped to their replica id in the shared project.
-    channel_view_a.read_with(cx_a, |view, cx| {
-        assert_eq!(
-            view.editor.read(cx).replica_id_map().unwrap(),
-            &[(1, 0), (2, 1), (0, 2)]
-                .into_iter()
-                .collect::<HashMap<_, _>>()
-        );
+    // Clients A and B see each other with the same colors as in the channel notes.
+    editor_a.update(cx_a, |editor, cx| {
+        assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx);
     });
-    channel_view_b.read_with(cx_b, |view, cx| {
-        assert_eq!(
-            view.editor.read(cx).replica_id_map().unwrap(),
-            &[(1, 0), (2, 1), (0, 2)]
-                .into_iter()
-                .collect::<HashMap<_, _>>(),
-        )
+    editor_b.update(cx_b, |editor, cx| {
+        assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
     });
 }
 
+#[track_caller]
+fn assert_remote_selections(
+    editor: &mut Editor,
+    expected_selections: &[(Option<ParticipantIndex>, Range<usize>)],
+    cx: &mut ViewContext<Editor>,
+) {
+    let snapshot = editor.snapshot(cx);
+    let range = Anchor::min()..Anchor::max();
+    let remote_selections = snapshot
+        .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
+        .map(|s| {
+            let start = s.selection.start.to_offset(&snapshot.buffer_snapshot);
+            let end = s.selection.end.to_offset(&snapshot.buffer_snapshot);
+            (s.participant_index, start..end)
+        })
+        .collect::<Vec<_>>();
+    assert_eq!(
+        remote_selections, expected_selections,
+        "incorrect remote selections"
+    );
+}
+
 #[gpui::test]
-async fn test_reopen_channel_buffer(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
+async fn test_multiple_handles_to_channel_buffer(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+) {
     deterministic.forbid_parking();
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
@@ -565,26 +607,163 @@ async fn test_channel_buffers_and_server_restarts(
 
     channel_buffer_a.read_with(cx_a, |buffer_a, _| {
         channel_buffer_b.read_with(cx_b, |buffer_b, _| {
-            assert_eq!(
-                buffer_a
-                    .collaborators()
-                    .iter()
-                    .map(|c| c.user_id)
-                    .collect::<Vec<_>>(),
-                vec![client_a.user_id().unwrap(), client_b.user_id().unwrap()]
+            assert_collaborators(
+                buffer_a.collaborators(),
+                &[client_a.user_id(), client_b.user_id()],
             );
             assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
         });
     });
 }
 
+#[gpui::test(iterations = 10)]
+async fn test_following_to_channel_notes_without_a_shared_project(
+    deterministic: Arc<Deterministic>,
+    mut cx_a: &mut TestAppContext,
+    mut cx_b: &mut TestAppContext,
+    mut cx_c: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+    cx_c.update(editor::init);
+    cx_a.update(collab_ui::channel_view::init);
+    cx_b.update(collab_ui::channel_view::init);
+    cx_c.update(collab_ui::channel_view::init);
+
+    let channel_1_id = server
+        .make_channel(
+            "channel-1",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b), (&client_c, cx_c)],
+        )
+        .await;
+    let channel_2_id = server
+        .make_channel(
+            "channel-2",
+            None,
+            (&client_a, cx_a),
+            &mut [(&client_b, cx_b), (&client_c, cx_c)],
+        )
+        .await;
+
+    // Clients A, B, and C join a channel.
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+    let active_call_c = cx_c.read(ActiveCall::global);
+    for (call, cx) in [
+        (&active_call_a, &mut cx_a),
+        (&active_call_b, &mut cx_b),
+        (&active_call_c, &mut cx_c),
+    ] {
+        call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
+            .await
+            .unwrap();
+    }
+    deterministic.run_until_parked();
+
+    // Clients A, B, and C all open their own unshared projects.
+    client_a.fs().insert_tree("/a", json!({})).await;
+    client_b.fs().insert_tree("/b", json!({})).await;
+    client_c.fs().insert_tree("/c", json!({})).await;
+    let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
+    let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
+    let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+    let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
+
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+
+    // Client A opens the notes for channel 1.
+    let channel_view_1_a = cx_a
+        .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
+        .await
+        .unwrap();
+    channel_view_1_a.update(cx_a, |notes, cx| {
+        assert_eq!(notes.channel(cx).name, "channel-1");
+        notes.editor.update(cx, |editor, cx| {
+            editor.insert("Hello from A.", cx);
+            editor.change_selections(None, cx, |selections| {
+                selections.select_ranges(vec![3..4]);
+            });
+        });
+    });
+
+    // Client B follows client A.
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    // Client B is taken to the notes for channel 1, with the same
+    // text selected as client A.
+    deterministic.run_until_parked();
+    let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(
+            workspace.leader_for_pane(workspace.active_pane()),
+            Some(client_a.peer_id().unwrap())
+        );
+        workspace
+            .active_item(cx)
+            .expect("no active item")
+            .downcast::<ChannelView>()
+            .expect("active item is not a channel view")
+    });
+    channel_view_1_b.read_with(cx_b, |notes, cx| {
+        assert_eq!(notes.channel(cx).name, "channel-1");
+        let editor = notes.editor.read(cx);
+        assert_eq!(editor.text(cx), "Hello from A.");
+        assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
+    });
+
+    // Client A opens the notes for channel 2.
+    let channel_view_2_a = cx_a
+        .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
+        .await
+        .unwrap();
+    channel_view_2_a.read_with(cx_a, |notes, cx| {
+        assert_eq!(notes.channel(cx).name, "channel-2");
+    });
+
+    // Client B is taken to the notes for channel 2.
+    deterministic.run_until_parked();
+    let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(
+            workspace.leader_for_pane(workspace.active_pane()),
+            Some(client_a.peer_id().unwrap())
+        );
+        workspace
+            .active_item(cx)
+            .expect("no active item")
+            .downcast::<ChannelView>()
+            .expect("active item is not a channel view")
+    });
+    channel_view_2_b.read_with(cx_b, |notes, cx| {
+        assert_eq!(notes.channel(cx).name, "channel-2");
+    });
+}
+
 #[track_caller]
-fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option<UserId>]) {
+fn assert_collaborators(collaborators: &HashMap<PeerId, Collaborator>, ids: &[Option<UserId>]) {
+    let mut user_ids = collaborators
+        .values()
+        .map(|collaborator| collaborator.user_id)
+        .collect::<Vec<_>>();
+    user_ids.sort();
     assert_eq!(
-        collaborators
-            .into_iter()
-            .map(|collaborator| collaborator.user_id)
-            .collect::<Vec<_>>(),
+        user_ids,
         ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>()
     );
 }

crates/collab/src/tests/following_tests.rs 🔗

@@ -0,0 +1,1306 @@
+use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
+use call::ActiveCall;
+use collab_ui::project_shared_notification::ProjectSharedNotification;
+use editor::{Editor, ExcerptRange, MultiBuffer};
+use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
+use live_kit_client::MacOSDisplay;
+use serde_json::json;
+use std::{borrow::Cow, sync::Arc};
+use workspace::{
+    dock::{test::TestPanel, DockPosition},
+    item::{test::TestItem, ItemHandle as _},
+    shared_screen::SharedScreen,
+    SplitDirection, Workspace,
+};
+
+#[gpui::test(iterations = 10)]
+async fn test_basic_following(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+    cx_d: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+    let client_d = server.create_client(cx_d, "user_d").await;
+    server
+        .create_room(&mut [
+            (&client_a, cx_a),
+            (&client_b, cx_b),
+            (&client_c, cx_c),
+            (&client_d, cx_d),
+        ])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "1.txt": "one\none\none",
+                "2.txt": "two\ntwo\ntwo",
+                "3.txt": "three\nthree\nthree",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    let window_a = client_a.build_workspace(&project_a, cx_a);
+    let workspace_a = window_a.root(cx_a);
+    let window_b = client_b.build_workspace(&project_b, cx_b);
+    let workspace_b = window_b.root(cx_b);
+
+    // Client A opens some editors.
+    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"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+    let editor_a2 = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client B opens an editor.
+    let editor_b1 = workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    let peer_id_a = client_a.peer_id().unwrap();
+    let peer_id_b = client_b.peer_id().unwrap();
+    let peer_id_c = client_c.peer_id().unwrap();
+    let peer_id_d = client_d.peer_id().unwrap();
+
+    // Client A updates their selections in those editors
+    editor_a1.update(cx_a, |editor, cx| {
+        editor.handle_input("a", cx);
+        editor.handle_input("b", cx);
+        editor.handle_input("c", cx);
+        editor.select_left(&Default::default(), cx);
+        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+    });
+    editor_a2.update(cx_a, |editor, cx| {
+        editor.handle_input("d", cx);
+        editor.handle_input("e", cx);
+        editor.select_left(&Default::default(), cx);
+        assert_eq!(editor.selections.ranges(cx), vec![2..1]);
+    });
+
+    // When client B starts following client A, all visible view states are replicated to client B.
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.follow(peer_id_a, cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    cx_c.foreground().run_until_parked();
+    let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+    assert_eq!(
+        cx_b.read(|cx| editor_b2.project_path(cx)),
+        Some((worktree_id, "2.txt").into())
+    );
+    assert_eq!(
+        editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        vec![2..1]
+    );
+    assert_eq!(
+        editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+        vec![3..2]
+    );
+
+    cx_c.foreground().run_until_parked();
+    let active_call_c = cx_c.read(ActiveCall::global);
+    let project_c = client_c.build_remote_project(project_id, cx_c).await;
+    let window_c = client_c.build_workspace(&project_c, cx_c);
+    let workspace_c = window_c.root(cx_c);
+    active_call_c
+        .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
+        .await
+        .unwrap();
+    drop(project_c);
+
+    // Client C also follows client A.
+    workspace_c
+        .update(cx_c, |workspace, cx| {
+            workspace.follow(peer_id_a, cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    cx_d.foreground().run_until_parked();
+    let active_call_d = cx_d.read(ActiveCall::global);
+    let project_d = client_d.build_remote_project(project_id, cx_d).await;
+    let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
+    active_call_d
+        .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
+        .await
+        .unwrap();
+    drop(project_d);
+
+    // All clients see that clients B and C are following client A.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b, peer_id_c],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
+    // Client C unfollows client A.
+    workspace_c.update(cx_c, |workspace, cx| {
+        workspace.unfollow(&workspace.active_pane().clone(), cx);
+    });
+
+    // All clients see that clients B is following client A.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
+    // Client C re-follows client A.
+    workspace_c.update(cx_c, |workspace, cx| {
+        workspace.follow(peer_id_a, cx);
+    });
+
+    // All clients see that clients B and C are following client A.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b, peer_id_c],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
+    // Client D follows client C.
+    workspace_d
+        .update(cx_d, |workspace, cx| {
+            workspace.follow(peer_id_c, cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    // All clients see that D is following C
+    cx_d.foreground().run_until_parked();
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_c, project_id),
+                &[peer_id_d],
+                "checking followers for C as {name}"
+            );
+        });
+    }
+
+    // Client C closes the project.
+    window_c.remove(cx_c);
+    cx_c.drop_last(workspace_c);
+
+    // Clients A and B see that client B is following A, and client C is not present in the followers.
+    cx_c.foreground().run_until_parked();
+    for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_a, project_id),
+                &[peer_id_b],
+                "checking followers for A as {name}"
+            );
+        });
+    }
+
+    // All clients see that no-one is following C
+    for (name, active_call, cx) in [
+        ("A", &active_call_a, &cx_a),
+        ("B", &active_call_b, &cx_b),
+        ("C", &active_call_c, &cx_c),
+        ("D", &active_call_d, &cx_d),
+    ] {
+        active_call.read_with(*cx, |call, cx| {
+            let room = call.room().unwrap().read(cx);
+            assert_eq!(
+                room.followers_for(peer_id_c, project_id),
+                &[],
+                "checking followers for C as {name}"
+            );
+        });
+    }
+
+    // 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)
+    });
+    deterministic.run_until_parked();
+    workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+    });
+
+    // When client A opens a multibuffer, client B does so as well.
+    let multibuffer_a = cx_a.add_model(|cx| {
+        let buffer_a1 = project_a.update(cx, |project, cx| {
+            project
+                .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
+                .unwrap()
+        });
+        let buffer_a2 = project_a.update(cx, |project, cx| {
+            project
+                .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
+                .unwrap()
+        });
+        let mut result = MultiBuffer::new(0);
+        result.push_excerpts(
+            buffer_a1,
+            [ExcerptRange {
+                context: 0..3,
+                primary: None,
+            }],
+            cx,
+        );
+        result.push_excerpts(
+            buffer_a2,
+            [ExcerptRange {
+                context: 4..7,
+                primary: None,
+            }],
+            cx,
+        );
+        result
+    });
+    let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
+        let editor =
+            cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
+        workspace.add_item(Box::new(editor.clone()), cx);
+        editor
+    });
+    deterministic.run_until_parked();
+    let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .unwrap()
+            .downcast::<Editor>()
+            .unwrap()
+    });
+    assert_eq!(
+        multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
+        multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
+    );
+
+    // When client A navigates back and forth, client B does so as well.
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.go_back(workspace.active_pane().downgrade(), cx)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+    workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+    });
+
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.go_back(workspace.active_pane().downgrade(), cx)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+    workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
+    });
+
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.go_forward(workspace.active_pane().downgrade(), cx)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+    workspace_b.read_with(cx_b, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+    });
+
+    // Changes to client A's editor are reflected on client B.
+    editor_a1.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
+    });
+    deterministic.run_until_parked();
+    editor_b1.read_with(cx_b, |editor, cx| {
+        assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
+    });
+
+    editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
+    deterministic.run_until_parked();
+    editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
+
+    editor_a1.update(cx_a, |editor, cx| {
+        editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
+        editor.set_scroll_position(vec2f(0., 100.), cx);
+    });
+    deterministic.run_until_parked();
+    editor_b1.read_with(cx_b, |editor, cx| {
+        assert_eq!(editor.selections.ranges(cx), &[3..3]);
+    });
+
+    // 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)
+    });
+    deterministic.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.follow(peer_id_b, cx).unwrap()
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+        Some(peer_id_b)
+    );
+    assert_eq!(
+        workspace_a.read_with(cx_a, |workspace, cx| workspace
+            .active_item(cx)
+            .unwrap()
+            .id()),
+        editor_a1.id()
+    );
+
+    // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
+    let display = MacOSDisplay::new();
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(None, cx))
+        .await
+        .unwrap();
+    active_call_b
+        .update(cx_b, |call, cx| {
+            call.room().unwrap().update(cx, |room, cx| {
+                room.set_display_sources(vec![display.clone()]);
+                room.share_screen(cx)
+            })
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+    let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
+        workspace
+            .active_item(cx)
+            .expect("no active item")
+            .downcast::<SharedScreen>()
+            .expect("active item isn't a shared screen")
+    });
+
+    // Client B activates Zed again, which causes the previous editor to become focused again.
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+    workspace_a.read_with(cx_a, |workspace, cx| {
+        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
+    });
+
+    // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.activate_item(&multibuffer_editor_b, cx)
+    });
+    deterministic.run_until_parked();
+    workspace_a.read_with(cx_a, |workspace, cx| {
+        assert_eq!(
+            workspace.active_item(cx).unwrap().id(),
+            multibuffer_editor_a.id()
+        )
+    });
+
+    // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
+    let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.add_panel(panel, cx);
+        workspace.toggle_panel_focus::<TestPanel>(cx);
+    });
+    deterministic.run_until_parked();
+    assert_eq!(
+        workspace_a.read_with(cx_a, |workspace, cx| workspace
+            .active_item(cx)
+            .unwrap()
+            .id()),
+        shared_screen.id()
+    );
+
+    // Toggling the focus back to the pane causes client A to return to the multibuffer.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.toggle_panel_focus::<TestPanel>(cx);
+    });
+    deterministic.run_until_parked();
+    workspace_a.read_with(cx_a, |workspace, cx| {
+        assert_eq!(
+            workspace.active_item(cx).unwrap().id(),
+            multibuffer_editor_a.id()
+        )
+    });
+
+    // Client B activates an item that doesn't implement following,
+    // so the previously-opened screen-sharing item gets activated.
+    let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
+        })
+    });
+    deterministic.run_until_parked();
+    assert_eq!(
+        workspace_a.read_with(cx_a, |workspace, cx| workspace
+            .active_item(cx)
+            .unwrap()
+            .id()),
+        shared_screen.id()
+    );
+
+    // Following interrupts when client B disconnects.
+    client_b.disconnect(&cx_b.to_async());
+    deterministic.advance_clock(RECONNECT_TIMEOUT);
+    assert_eq!(
+        workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+        None
+    );
+}
+
+#[gpui::test]
+async fn test_following_tab_order(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+    let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+
+    let client_b_id = project_a.read_with(cx_a, |project, _| {
+        project.collaborators().values().next().unwrap().peer_id
+    });
+
+    //Open 1, 3 in that order on client A
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "3.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
+        pane.update(cx, |pane, cx| {
+            pane.items()
+                .map(|item| {
+                    item.project_path(cx)
+                        .unwrap()
+                        .path
+                        .to_str()
+                        .unwrap()
+                        .to_owned()
+                })
+                .collect::<Vec<_>>()
+        })
+    };
+
+    //Verify that the tabs opened in the order we expect
+    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
+
+    //Follow client B as client A
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.follow(client_b_id, cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    //Open just 2 on client B
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+
+    // Verify that newly opened followed file is at the end
+    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
+
+    //Open just 1 on client B
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
+    deterministic.run_until_parked();
+
+    // Verify that following into 1 did not reorder
+    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_peers_following_each_other(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    // Client A shares a project.
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+                "4.txt": "four",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    // Client B joins the project.
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    // Client A opens some editors.
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(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"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client B opens an editor.
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(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"), None, true, 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_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
+    });
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            assert_ne!(*workspace.active_pane(), pane_a1);
+            let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
+            workspace.follow(leader_id, cx).unwrap()
+        })
+        .await
+        .unwrap();
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
+    });
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            assert_ne!(*workspace.active_pane(), pane_b1);
+            let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
+            workspace.follow(leader_id, cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.activate_next_pane(cx);
+    });
+    // Wait for focus effects to be fully flushed
+    workspace_a.update(cx_a, |workspace, _| {
+        assert_eq!(*workspace.active_pane(), pane_a1);
+    });
+
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "3.txt"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.activate_next_pane(cx);
+    });
+
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            assert_eq!(*workspace.active_pane(), pane_b1);
+            workspace.open_path((worktree_id, "4.txt"), None, true, 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);
+    });
+
+    workspace_a.update(cx_a, |workspace, 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);
+    });
+
+    workspace_b.update(cx_b, |workspace, 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(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+
+    // 2 clients connect to a server.
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    // Client A shares a project.
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "1.txt": "one",
+                "2.txt": "two",
+                "3.txt": "three",
+            }),
+        )
+        .await;
+    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    // Client A opens some editors.
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    let _editor_a1 = workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+        })
+        .await
+        .unwrap()
+        .downcast::<Editor>()
+        .unwrap();
+
+    // Client B starts following client A.
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(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.follow(leader_id, 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.follow(leader_id, 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.follow(leader_id, 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.follow(leader_id, 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_and_clone(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"), None, true, cx)
+        })
+        .await
+        .unwrap();
+    assert_eq!(
+        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+        None
+    );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_peers_simultaneously_following_each_other(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    client_a.fs().insert_tree("/a", json!({})).await;
+    let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+
+    deterministic.run_until_parked();
+    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
+    });
+
+    let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
+        workspace.follow(client_b_id, cx).unwrap()
+    });
+    let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
+        workspace.follow(client_a_id, cx).unwrap()
+    });
+
+    futures::try_join!(a_follow_b, b_follow_a).unwrap();
+    workspace_a.read_with(cx_a, |workspace, _| {
+        assert_eq!(
+            workspace.leader_for_pane(workspace.active_pane()),
+            Some(client_b_id)
+        );
+    });
+    workspace_b.read_with(cx_b, |workspace, _| {
+        assert_eq!(
+            workspace.leader_for_pane(workspace.active_pane()),
+            Some(client_a_id)
+        );
+    });
+}
+
+fn visible_push_notifications(
+    cx: &mut TestAppContext,
+) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
+    let mut ret = Vec::new();
+    for window in cx.windows() {
+        window.read_with(cx, |window| {
+            if let Some(handle) = window
+                .root_view()
+                .clone()
+                .downcast::<ProjectSharedNotification>()
+            {
+                ret.push(handle)
+            }
+        });
+    }
+    ret
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_following_across_workspaces(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    // a and b join a channel/call
+    // a shares project 1
+    // b shares project 2
+    //
+    // b follows a: causes project 2 to be joined, and b to follow a.
+    // b opens a different file in project 2, a follows b
+    // b opens a different file in project 1, a cannot follow b
+    // b shares the project, a joins the project and follows b
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
+    client_a
+        .fs()
+        .insert_tree(
+            "/a",
+            json!({
+                "w.rs": "",
+                "x.rs": "",
+            }),
+        )
+        .await;
+
+    client_b
+        .fs()
+        .insert_tree(
+            "/b",
+            json!({
+                "y.rs": "",
+                "z.rs": "",
+            }),
+        )
+        .await;
+
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+
+    let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
+    let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
+
+    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+
+    cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
+    cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
+
+    active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    active_call_a
+        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+        .await
+        .unwrap();
+    active_call_b
+        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .await
+        .unwrap();
+
+    workspace_a
+        .update(cx_a, |workspace, cx| {
+            workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+    assert_eq!(visible_push_notifications(cx_b).len(), 1);
+
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace
+            .follow(client_a.peer_id().unwrap(), cx)
+            .unwrap()
+            .detach()
+    });
+
+    deterministic.run_until_parked();
+    let workspace_b_project_a = cx_b
+        .windows()
+        .iter()
+        .max_by_key(|window| window.id())
+        .unwrap()
+        .downcast::<Workspace>()
+        .unwrap()
+        .root(cx_b);
+
+    // assert that b is following a in project a in w.rs
+    workspace_b_project_a.update(cx_b, |workspace, cx| {
+        assert!(workspace.is_being_followed(client_a.peer_id().unwrap()));
+        assert_eq!(
+            client_a.peer_id(),
+            workspace.leader_for_pane(workspace.active_pane())
+        );
+        let item = workspace.active_item(cx).unwrap();
+        assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("w.rs"));
+    });
+
+    // TODO: in app code, this would be done by the collab_ui.
+    active_call_b
+        .update(cx_b, |call, cx| {
+            let project = workspace_b_project_a.read(cx).project().clone();
+            call.set_location(Some(&project), cx)
+        })
+        .await
+        .unwrap();
+
+    // assert that there are no share notifications open
+    assert_eq!(visible_push_notifications(cx_b).len(), 0);
+
+    // b moves to x.rs in a's project, and a follows
+    workspace_b_project_a
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id_a, "x.rs"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+    workspace_b_project_a.update(cx_b, |workspace, cx| {
+        let item = workspace.active_item(cx).unwrap();
+        assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
+    });
+
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace
+            .follow(client_b.peer_id().unwrap(), cx)
+            .unwrap()
+            .detach()
+    });
+
+    deterministic.run_until_parked();
+    workspace_a.update(cx_a, |workspace, cx| {
+        assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
+        assert_eq!(
+            client_b.peer_id(),
+            workspace.leader_for_pane(workspace.active_pane())
+        );
+        let item = workspace.active_pane().read(cx).active_item().unwrap();
+        assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
+    });
+
+    // b moves to y.rs in b's project, a is still following but can't yet see
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.open_path((worktree_id_b, "y.rs"), None, true, cx)
+        })
+        .await
+        .unwrap();
+
+    // TODO: in app code, this would be done by the collab_ui.
+    active_call_b
+        .update(cx_b, |call, cx| {
+            let project = workspace_b.read(cx).project().clone();
+            call.set_location(Some(&project), cx)
+        })
+        .await
+        .unwrap();
+
+    let project_b_id = active_call_b
+        .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
+        .await
+        .unwrap();
+
+    deterministic.run_until_parked();
+    assert_eq!(visible_push_notifications(cx_a).len(), 1);
+    cx_a.update(|cx| {
+        workspace::join_remote_project(
+            project_b_id,
+            client_b.user_id().unwrap(),
+            client_a.app_state.clone(),
+            cx,
+        )
+    })
+    .await
+    .unwrap();
+
+    deterministic.run_until_parked();
+
+    assert_eq!(visible_push_notifications(cx_a).len(), 0);
+    let workspace_a_project_b = cx_a
+        .windows()
+        .iter()
+        .max_by_key(|window| window.id())
+        .unwrap()
+        .downcast::<Workspace>()
+        .unwrap()
+        .root(cx_a);
+
+    workspace_a_project_b.update(cx_a, |workspace, cx| {
+        assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
+        assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
+        assert_eq!(
+            client_b.peer_id(),
+            workspace.leader_for_pane(workspace.active_pane())
+        );
+        let item = workspace.active_item(cx).unwrap();
+        assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
+    });
+}

crates/collab/src/tests/integration_tests.rs 🔗

@@ -7,14 +7,11 @@ use client::{User, RECEIVE_TIMEOUT};
 use collections::{HashMap, HashSet};
 use editor::{
     test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
-    ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo,
+    ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo,
 };
 use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions};
 use futures::StreamExt as _;
-use gpui::{
-    executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle,
-    TestAppContext, ViewHandle,
-};
+use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, TestAppContext};
 use indoc::indoc;
 use language::{
     language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
@@ -38,12 +35,7 @@ use std::{
     },
 };
 use unindent::Unindent as _;
-use workspace::{
-    dock::{test::TestPanel, DockPosition},
-    item::{test::TestItem, ItemHandle as _},
-    shared_screen::SharedScreen,
-    SplitDirection, Workspace,
-};
+use workspace::Workspace;
 
 #[ctor::ctor]
 fn init_logger() {
@@ -6388,455 +6380,49 @@ async fn test_contact_requests(
 }
 
 #[gpui::test(iterations = 10)]
-async fn test_basic_following(
+async fn test_join_call_after_screen_was_shared(
     deterministic: Arc<Deterministic>,
     cx_a: &mut TestAppContext,
     cx_b: &mut TestAppContext,
-    cx_c: &mut TestAppContext,
-    cx_d: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
-
     let mut server = TestServer::start(&deterministic).await;
+
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
-    let client_c = server.create_client(cx_c, "user_c").await;
-    let client_d = server.create_client(cx_d, "user_d").await;
     server
-        .create_room(&mut [
-            (&client_a, cx_a),
-            (&client_b, cx_b),
-            (&client_c, cx_c),
-            (&client_d, cx_d),
-        ])
+        .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
         .await;
+
     let active_call_a = cx_a.read(ActiveCall::global);
     let active_call_b = cx_b.read(ActiveCall::global);
 
-    cx_a.update(editor::init);
-    cx_b.update(editor::init);
-
-    client_a
-        .fs()
-        .insert_tree(
-            "/a",
-            json!({
-                "1.txt": "one\none\none",
-                "2.txt": "two\ntwo\ntwo",
-                "3.txt": "three\nthree\nthree",
-            }),
-        )
-        .await;
-    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+    // Call users B and C from client A.
     active_call_a
-        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-        .await
-        .unwrap();
-
-    let project_id = active_call_a
-        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-        .await
-        .unwrap();
-    let project_b = client_b.build_remote_project(project_id, cx_b).await;
-    active_call_b
-        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-        .await
-        .unwrap();
-
-    let window_a = client_a.build_workspace(&project_a, cx_a);
-    let workspace_a = window_a.root(cx_a);
-    let window_b = client_b.build_workspace(&project_b, cx_b);
-    let workspace_b = window_b.root(cx_b);
-
-    // Client A opens some editors.
-    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"), None, true, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-    let editor_a2 = workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-
-    // Client B opens an editor.
-    let editor_b1 = workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-
-    let peer_id_a = client_a.peer_id().unwrap();
-    let peer_id_b = client_b.peer_id().unwrap();
-    let peer_id_c = client_c.peer_id().unwrap();
-    let peer_id_d = client_d.peer_id().unwrap();
-
-    // Client A updates their selections in those editors
-    editor_a1.update(cx_a, |editor, cx| {
-        editor.handle_input("a", cx);
-        editor.handle_input("b", cx);
-        editor.handle_input("c", cx);
-        editor.select_left(&Default::default(), cx);
-        assert_eq!(editor.selections.ranges(cx), vec![3..2]);
-    });
-    editor_a2.update(cx_a, |editor, cx| {
-        editor.handle_input("d", cx);
-        editor.handle_input("e", cx);
-        editor.select_left(&Default::default(), cx);
-        assert_eq!(editor.selections.ranges(cx), vec![2..1]);
-    });
-
-    // When client B starts following client A, all visible view states are replicated to client B.
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.toggle_follow(peer_id_a, cx).unwrap()
-        })
-        .await
-        .unwrap();
-
-    cx_c.foreground().run_until_parked();
-    let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
-        workspace
-            .active_item(cx)
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap()
-    });
-    assert_eq!(
-        cx_b.read(|cx| editor_b2.project_path(cx)),
-        Some((worktree_id, "2.txt").into())
-    );
-    assert_eq!(
-        editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
-        vec![2..1]
-    );
-    assert_eq!(
-        editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
-        vec![3..2]
-    );
-
-    cx_c.foreground().run_until_parked();
-    let active_call_c = cx_c.read(ActiveCall::global);
-    let project_c = client_c.build_remote_project(project_id, cx_c).await;
-    let window_c = client_c.build_workspace(&project_c, cx_c);
-    let workspace_c = window_c.root(cx_c);
-    active_call_c
-        .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
-        .await
-        .unwrap();
-    drop(project_c);
-
-    // Client C also follows client A.
-    workspace_c
-        .update(cx_c, |workspace, cx| {
-            workspace.toggle_follow(peer_id_a, cx).unwrap()
-        })
-        .await
-        .unwrap();
-
-    cx_d.foreground().run_until_parked();
-    let active_call_d = cx_d.read(ActiveCall::global);
-    let project_d = client_d.build_remote_project(project_id, cx_d).await;
-    let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
-    active_call_d
-        .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
-        .await
-        .unwrap();
-    drop(project_d);
-
-    // All clients see that clients B and C are following client A.
-    cx_c.foreground().run_until_parked();
-    for (name, active_call, cx) in [
-        ("A", &active_call_a, &cx_a),
-        ("B", &active_call_b, &cx_b),
-        ("C", &active_call_c, &cx_c),
-        ("D", &active_call_d, &cx_d),
-    ] {
-        active_call.read_with(*cx, |call, cx| {
-            let room = call.room().unwrap().read(cx);
-            assert_eq!(
-                room.followers_for(peer_id_a, project_id),
-                &[peer_id_b, peer_id_c],
-                "checking followers for A as {name}"
-            );
-        });
-    }
-
-    // Client C unfollows client A.
-    workspace_c.update(cx_c, |workspace, cx| {
-        workspace.toggle_follow(peer_id_a, cx);
-    });
-
-    // All clients see that clients B is following client A.
-    cx_c.foreground().run_until_parked();
-    for (name, active_call, cx) in [
-        ("A", &active_call_a, &cx_a),
-        ("B", &active_call_b, &cx_b),
-        ("C", &active_call_c, &cx_c),
-        ("D", &active_call_d, &cx_d),
-    ] {
-        active_call.read_with(*cx, |call, cx| {
-            let room = call.room().unwrap().read(cx);
-            assert_eq!(
-                room.followers_for(peer_id_a, project_id),
-                &[peer_id_b],
-                "checking followers for A as {name}"
-            );
-        });
-    }
-
-    // Client C re-follows client A.
-    workspace_c.update(cx_c, |workspace, cx| {
-        workspace.toggle_follow(peer_id_a, cx);
-    });
-
-    // All clients see that clients B and C are following client A.
-    cx_c.foreground().run_until_parked();
-    for (name, active_call, cx) in [
-        ("A", &active_call_a, &cx_a),
-        ("B", &active_call_b, &cx_b),
-        ("C", &active_call_c, &cx_c),
-        ("D", &active_call_d, &cx_d),
-    ] {
-        active_call.read_with(*cx, |call, cx| {
-            let room = call.room().unwrap().read(cx);
-            assert_eq!(
-                room.followers_for(peer_id_a, project_id),
-                &[peer_id_b, peer_id_c],
-                "checking followers for A as {name}"
-            );
-        });
-    }
-
-    // Client D follows client C.
-    workspace_d
-        .update(cx_d, |workspace, cx| {
-            workspace.toggle_follow(peer_id_c, cx).unwrap()
-        })
-        .await
-        .unwrap();
-
-    // All clients see that D is following C
-    cx_d.foreground().run_until_parked();
-    for (name, active_call, cx) in [
-        ("A", &active_call_a, &cx_a),
-        ("B", &active_call_b, &cx_b),
-        ("C", &active_call_c, &cx_c),
-        ("D", &active_call_d, &cx_d),
-    ] {
-        active_call.read_with(*cx, |call, cx| {
-            let room = call.room().unwrap().read(cx);
-            assert_eq!(
-                room.followers_for(peer_id_c, project_id),
-                &[peer_id_d],
-                "checking followers for C as {name}"
-            );
-        });
-    }
-
-    // Client C closes the project.
-    window_c.remove(cx_c);
-    cx_c.drop_last(workspace_c);
-
-    // Clients A and B see that client B is following A, and client C is not present in the followers.
-    cx_c.foreground().run_until_parked();
-    for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] {
-        active_call.read_with(*cx, |call, cx| {
-            let room = call.room().unwrap().read(cx);
-            assert_eq!(
-                room.followers_for(peer_id_a, project_id),
-                &[peer_id_b],
-                "checking followers for A as {name}"
-            );
-        });
-    }
-
-    // All clients see that no-one is following C
-    for (name, active_call, cx) in [
-        ("A", &active_call_a, &cx_a),
-        ("B", &active_call_b, &cx_b),
-        ("C", &active_call_c, &cx_c),
-        ("D", &active_call_d, &cx_d),
-    ] {
-        active_call.read_with(*cx, |call, cx| {
-            let room = call.room().unwrap().read(cx);
-            assert_eq!(
-                room.followers_for(peer_id_c, project_id),
-                &[],
-                "checking followers for C as {name}"
-            );
-        });
-    }
-
-    // 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)
-    });
-    deterministic.run_until_parked();
-    workspace_b.read_with(cx_b, |workspace, cx| {
-        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
-    });
-
-    // When client A opens a multibuffer, client B does so as well.
-    let multibuffer_a = cx_a.add_model(|cx| {
-        let buffer_a1 = project_a.update(cx, |project, cx| {
-            project
-                .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
-                .unwrap()
-        });
-        let buffer_a2 = project_a.update(cx, |project, cx| {
-            project
-                .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
-                .unwrap()
-        });
-        let mut result = MultiBuffer::new(0);
-        result.push_excerpts(
-            buffer_a1,
-            [ExcerptRange {
-                context: 0..3,
-                primary: None,
-            }],
-            cx,
-        );
-        result.push_excerpts(
-            buffer_a2,
-            [ExcerptRange {
-                context: 4..7,
-                primary: None,
-            }],
-            cx,
-        );
-        result
-    });
-    let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
-        let editor =
-            cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
-        workspace.add_item(Box::new(editor.clone()), cx);
-        editor
-    });
-    deterministic.run_until_parked();
-    let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
-        workspace
-            .active_item(cx)
-            .unwrap()
-            .downcast::<Editor>()
-            .unwrap()
-    });
-    assert_eq!(
-        multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
-        multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
-    );
-
-    // When client A navigates back and forth, client B does so as well.
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.go_back(workspace.active_pane().downgrade(), cx)
-        })
-        .await
-        .unwrap();
-    deterministic.run_until_parked();
-    workspace_b.read_with(cx_b, |workspace, cx| {
-        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
-    });
-
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.go_back(workspace.active_pane().downgrade(), cx)
-        })
-        .await
-        .unwrap();
-    deterministic.run_until_parked();
-    workspace_b.read_with(cx_b, |workspace, cx| {
-        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
-    });
-
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.go_forward(workspace.active_pane().downgrade(), cx)
+        .update(cx_a, |call, cx| {
+            call.invite(client_b.user_id().unwrap(), None, cx)
         })
         .await
         .unwrap();
-    deterministic.run_until_parked();
-    workspace_b.read_with(cx_b, |workspace, cx| {
-        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
-    });
-
-    // Changes to client A's editor are reflected on client B.
-    editor_a1.update(cx_a, |editor, cx| {
-        editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
-    });
-    deterministic.run_until_parked();
-    editor_b1.read_with(cx_b, |editor, cx| {
-        assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
-    });
-
-    editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
-    deterministic.run_until_parked();
-    editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
-
-    editor_a1.update(cx_a, |editor, cx| {
-        editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
-        editor.set_scroll_position(vec2f(0., 100.), cx);
-    });
-    deterministic.run_until_parked();
-    editor_b1.read_with(cx_b, |editor, cx| {
-        assert_eq!(editor.selections.ranges(cx), &[3..3]);
-    });
-
-    // 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)
-    });
+    let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
     deterministic.run_until_parked();
     assert_eq!(
-        workspace_b.read_with(cx_b, |workspace, cx| workspace
-            .active_item(cx)
-            .unwrap()
-            .id()),
-        editor_b1.id()
+        room_participants(&room_a, cx_a),
+        RoomParticipants {
+            remote: Default::default(),
+            pending: vec!["user_b".to_string()]
+        }
     );
 
-    // Client A starts following client B.
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.toggle_follow(peer_id_b, cx).unwrap()
-        })
-        .await
-        .unwrap();
-    assert_eq!(
-        workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
-        Some(peer_id_b)
-    );
-    assert_eq!(
-        workspace_a.read_with(cx_a, |workspace, cx| workspace
-            .active_item(cx)
-            .unwrap()
-            .id()),
-        editor_a1.id()
-    );
+    // User B receives the call.
+    let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
+    let call_b = incoming_call_b.next().await.unwrap().unwrap();
+    assert_eq!(call_b.calling_user.github_login, "user_a");
 
-    // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
+    // User A shares their screen
     let display = MacOSDisplay::new();
-    active_call_b
-        .update(cx_b, |call, cx| call.set_location(None, cx))
-        .await
-        .unwrap();
-    active_call_b
-        .update(cx_b, |call, cx| {
+    active_call_a
+        .update(cx_a, |call, cx| {
             call.room().unwrap().update(cx, |room, cx| {
                 room.set_display_sources(vec![display.clone()]);
                 room.share_screen(cx)
@@ -6844,153 +6430,18 @@ async fn test_basic_following(
         })
         .await
         .unwrap();
-    deterministic.run_until_parked();
-    let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
-        workspace
-            .active_item(cx)
-            .unwrap()
-            .downcast::<SharedScreen>()
-            .unwrap()
+
+    client_b.user_store().update(cx_b, |user_store, _| {
+        user_store.clear_cache();
     });
 
-    // Client B activates Zed again, which causes the previous editor to become focused again.
+    // User B joins the room
     active_call_b
-        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+        .update(cx_b, |call, cx| call.accept_incoming(cx))
         .await
         .unwrap();
-    deterministic.run_until_parked();
-    workspace_a.read_with(cx_a, |workspace, cx| {
-        assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
-    });
-
-    // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
-    workspace_b.update(cx_b, |workspace, cx| {
-        workspace.activate_item(&multibuffer_editor_b, cx)
-    });
-    deterministic.run_until_parked();
-    workspace_a.read_with(cx_a, |workspace, cx| {
-        assert_eq!(
-            workspace.active_item(cx).unwrap().id(),
-            multibuffer_editor_a.id()
-        )
-    });
-
-    // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
-    let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
-    workspace_b.update(cx_b, |workspace, cx| {
-        workspace.add_panel(panel, cx);
-        workspace.toggle_panel_focus::<TestPanel>(cx);
-    });
-    deterministic.run_until_parked();
-    assert_eq!(
-        workspace_a.read_with(cx_a, |workspace, cx| workspace
-            .active_item(cx)
-            .unwrap()
-            .id()),
-        shared_screen.id()
-    );
-
-    // Toggling the focus back to the pane causes client A to return to the multibuffer.
-    workspace_b.update(cx_b, |workspace, cx| {
-        workspace.toggle_panel_focus::<TestPanel>(cx);
-    });
-    deterministic.run_until_parked();
-    workspace_a.read_with(cx_a, |workspace, cx| {
-        assert_eq!(
-            workspace.active_item(cx).unwrap().id(),
-            multibuffer_editor_a.id()
-        )
-    });
-
-    // Client B activates an item that doesn't implement following,
-    // so the previously-opened screen-sharing item gets activated.
-    let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
-    workspace_b.update(cx_b, |workspace, cx| {
-        workspace.active_pane().update(cx, |pane, cx| {
-            pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
-        })
-    });
-    deterministic.run_until_parked();
-    assert_eq!(
-        workspace_a.read_with(cx_a, |workspace, cx| workspace
-            .active_item(cx)
-            .unwrap()
-            .id()),
-        shared_screen.id()
-    );
-
-    // Following interrupts when client B disconnects.
-    client_b.disconnect(&cx_b.to_async());
-    deterministic.advance_clock(RECONNECT_TIMEOUT);
-    assert_eq!(
-        workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
-        None
-    );
-}
-
-#[gpui::test(iterations = 10)]
-async fn test_join_call_after_screen_was_shared(
-    deterministic: Arc<Deterministic>,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
-) {
-    deterministic.forbid_parking();
-    let mut server = TestServer::start(&deterministic).await;
-
-    let client_a = server.create_client(cx_a, "user_a").await;
-    let client_b = server.create_client(cx_b, "user_b").await;
-    server
-        .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-        .await;
-
-    let active_call_a = cx_a.read(ActiveCall::global);
-    let active_call_b = cx_b.read(ActiveCall::global);
-
-    // Call users B and C from client A.
-    active_call_a
-        .update(cx_a, |call, cx| {
-            call.invite(client_b.user_id().unwrap(), None, cx)
-        })
-        .await
-        .unwrap();
-    let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
-    deterministic.run_until_parked();
-    assert_eq!(
-        room_participants(&room_a, cx_a),
-        RoomParticipants {
-            remote: Default::default(),
-            pending: vec!["user_b".to_string()]
-        }
-    );
-
-    // User B receives the call.
-    let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
-    let call_b = incoming_call_b.next().await.unwrap().unwrap();
-    assert_eq!(call_b.calling_user.github_login, "user_a");
-
-    // User A shares their screen
-    let display = MacOSDisplay::new();
-    active_call_a
-        .update(cx_a, |call, cx| {
-            call.room().unwrap().update(cx, |room, cx| {
-                room.set_display_sources(vec![display.clone()]);
-                room.share_screen(cx)
-            })
-        })
-        .await
-        .unwrap();
-
-    client_b.user_store().update(cx_b, |user_store, _| {
-        user_store.clear_cache();
-    });
-
-    // User B joins the room
-    active_call_b
-        .update(cx_b, |call, cx| call.accept_incoming(cx))
-        .await
-        .unwrap();
-    let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
-    assert!(incoming_call_b.next().await.unwrap().is_none());
+    let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
+    assert!(incoming_call_b.next().await.unwrap().is_none());
 
     deterministic.run_until_parked();
     assert_eq!(
@@ -7021,526 +6472,6 @@ async fn test_join_call_after_screen_was_shared(
     });
 }
 
-#[gpui::test]
-async fn test_following_tab_order(
-    deterministic: Arc<Deterministic>,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
-) {
-    let mut server = TestServer::start(&deterministic).await;
-    let client_a = server.create_client(cx_a, "user_a").await;
-    let client_b = server.create_client(cx_b, "user_b").await;
-    server
-        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-        .await;
-    let active_call_a = cx_a.read(ActiveCall::global);
-    let active_call_b = cx_b.read(ActiveCall::global);
-
-    cx_a.update(editor::init);
-    cx_b.update(editor::init);
-
-    client_a
-        .fs()
-        .insert_tree(
-            "/a",
-            json!({
-                "1.txt": "one",
-                "2.txt": "two",
-                "3.txt": "three",
-            }),
-        )
-        .await;
-    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-    active_call_a
-        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-        .await
-        .unwrap();
-
-    let project_id = active_call_a
-        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-        .await
-        .unwrap();
-    let project_b = client_b.build_remote_project(project_id, cx_b).await;
-    active_call_b
-        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-        .await
-        .unwrap();
-
-    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-    let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
-
-    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-    let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
-
-    let client_b_id = project_a.read_with(cx_a, |project, _| {
-        project.collaborators().values().next().unwrap().peer_id
-    });
-
-    //Open 1, 3 in that order on client A
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-        })
-        .await
-        .unwrap();
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "3.txt"), None, true, cx)
-        })
-        .await
-        .unwrap();
-
-    let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
-        pane.update(cx, |pane, cx| {
-            pane.items()
-                .map(|item| {
-                    item.project_path(cx)
-                        .unwrap()
-                        .path
-                        .to_str()
-                        .unwrap()
-                        .to_owned()
-                })
-                .collect::<Vec<_>>()
-        })
-    };
-
-    //Verify that the tabs opened in the order we expect
-    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
-
-    //Follow client B as client A
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.toggle_follow(client_b_id, cx).unwrap()
-        })
-        .await
-        .unwrap();
-
-    //Open just 2 on client B
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "2.txt"), None, true, cx)
-        })
-        .await
-        .unwrap();
-    deterministic.run_until_parked();
-
-    // Verify that newly opened followed file is at the end
-    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
-
-    //Open just 1 on client B
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-        })
-        .await
-        .unwrap();
-    assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
-    deterministic.run_until_parked();
-
-    // Verify that following into 1 did not reorder
-    assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
-}
-
-#[gpui::test(iterations = 10)]
-async fn test_peers_following_each_other(
-    deterministic: Arc<Deterministic>,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
-) {
-    deterministic.forbid_parking();
-    let mut server = TestServer::start(&deterministic).await;
-    let client_a = server.create_client(cx_a, "user_a").await;
-    let client_b = server.create_client(cx_b, "user_b").await;
-    server
-        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-        .await;
-    let active_call_a = cx_a.read(ActiveCall::global);
-    let active_call_b = cx_b.read(ActiveCall::global);
-
-    cx_a.update(editor::init);
-    cx_b.update(editor::init);
-
-    // Client A shares a project.
-    client_a
-        .fs()
-        .insert_tree(
-            "/a",
-            json!({
-                "1.txt": "one",
-                "2.txt": "two",
-                "3.txt": "three",
-                "4.txt": "four",
-            }),
-        )
-        .await;
-    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-    active_call_a
-        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-        .await
-        .unwrap();
-    let project_id = active_call_a
-        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-        .await
-        .unwrap();
-
-    // Client B joins the project.
-    let project_b = client_b.build_remote_project(project_id, cx_b).await;
-    active_call_b
-        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-        .await
-        .unwrap();
-
-    // Client A opens some editors.
-    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(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"), None, true, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-
-    // Client B opens an editor.
-    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(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"), None, true, 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_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
-    });
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            assert_ne!(*workspace.active_pane(), pane_a1);
-            let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
-            workspace.toggle_follow(leader_id, cx).unwrap()
-        })
-        .await
-        .unwrap();
-    workspace_b.update(cx_b, |workspace, cx| {
-        workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx);
-    });
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            assert_ne!(*workspace.active_pane(), pane_b1);
-            let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
-            workspace.toggle_follow(leader_id, cx).unwrap()
-        })
-        .await
-        .unwrap();
-
-    workspace_a.update(cx_a, |workspace, cx| {
-        workspace.activate_next_pane(cx);
-    });
-    // Wait for focus effects to be fully flushed
-    workspace_a.update(cx_a, |workspace, _| {
-        assert_eq!(*workspace.active_pane(), pane_a1);
-    });
-
-    workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "3.txt"), None, true, cx)
-        })
-        .await
-        .unwrap();
-    workspace_b.update(cx_b, |workspace, cx| {
-        workspace.activate_next_pane(cx);
-    });
-
-    workspace_b
-        .update(cx_b, |workspace, cx| {
-            assert_eq!(*workspace.active_pane(), pane_b1);
-            workspace.open_path((worktree_id, "4.txt"), None, true, 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);
-    });
-
-    workspace_a.update(cx_a, |workspace, 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);
-    });
-
-    workspace_b.update(cx_b, |workspace, 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(
-    deterministic: Arc<Deterministic>,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
-) {
-    deterministic.forbid_parking();
-
-    // 2 clients connect to a server.
-    let mut server = TestServer::start(&deterministic).await;
-    let client_a = server.create_client(cx_a, "user_a").await;
-    let client_b = server.create_client(cx_b, "user_b").await;
-    server
-        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-        .await;
-    let active_call_a = cx_a.read(ActiveCall::global);
-    let active_call_b = cx_b.read(ActiveCall::global);
-
-    cx_a.update(editor::init);
-    cx_b.update(editor::init);
-
-    // Client A shares a project.
-    client_a
-        .fs()
-        .insert_tree(
-            "/a",
-            json!({
-                "1.txt": "one",
-                "2.txt": "two",
-                "3.txt": "three",
-            }),
-        )
-        .await;
-    let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
-    active_call_a
-        .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
-        .await
-        .unwrap();
-
-    let project_id = active_call_a
-        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-        .await
-        .unwrap();
-    let project_b = client_b.build_remote_project(project_id, cx_b).await;
-    active_call_b
-        .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
-        .await
-        .unwrap();
-
-    // Client A opens some editors.
-    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-    let _editor_a1 = workspace_a
-        .update(cx_a, |workspace, cx| {
-            workspace.open_path((worktree_id, "1.txt"), None, true, cx)
-        })
-        .await
-        .unwrap()
-        .downcast::<Editor>()
-        .unwrap();
-
-    // Client B starts following client A.
-    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(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, 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, 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, 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, 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_and_clone(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"), None, true, cx)
-        })
-        .await
-        .unwrap();
-    assert_eq!(
-        workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
-        None
-    );
-}
-
-#[gpui::test(iterations = 10)]
-async fn test_peers_simultaneously_following_each_other(
-    deterministic: Arc<Deterministic>,
-    cx_a: &mut TestAppContext,
-    cx_b: &mut TestAppContext,
-) {
-    deterministic.forbid_parking();
-
-    let mut server = TestServer::start(&deterministic).await;
-    let client_a = server.create_client(cx_a, "user_a").await;
-    let client_b = server.create_client(cx_b, "user_b").await;
-    server
-        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
-        .await;
-    let active_call_a = cx_a.read(ActiveCall::global);
-
-    cx_a.update(editor::init);
-    cx_b.update(editor::init);
-
-    client_a.fs().insert_tree("/a", json!({})).await;
-    let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
-    let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-    let project_id = active_call_a
-        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
-        .await
-        .unwrap();
-
-    let project_b = client_b.build_remote_project(project_id, cx_b).await;
-    let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-
-    deterministic.run_until_parked();
-    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
-    });
-
-    let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
-        workspace.toggle_follow(client_b_id, cx).unwrap()
-    });
-    let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
-        workspace.toggle_follow(client_a_id, cx).unwrap()
-    });
-
-    futures::try_join!(a_follow_b, b_follow_a).unwrap();
-    workspace_a.read_with(cx_a, |workspace, _| {
-        assert_eq!(
-            workspace.leader_for_pane(workspace.active_pane()),
-            Some(client_b_id)
-        );
-    });
-    workspace_b.read_with(cx_b, |workspace, _| {
-        assert_eq!(
-            workspace.leader_for_pane(workspace.active_pane()),
-            Some(client_a_id)
-        );
-    });
-}
-
 #[gpui::test(iterations = 10)]
 async fn test_on_input_format_from_host_to_guest(
     deterministic: Arc<Deterministic>,

crates/collab/src/tests/random_channel_buffer_tests.rs 🔗

@@ -273,7 +273,7 @@ impl RandomizedTest for RandomChannelBufferTest {
                         // channel buffer.
                         let collaborators = channel_buffer.collaborators();
                         let mut user_ids =
-                            collaborators.iter().map(|c| c.user_id).collect::<Vec<_>>();
+                            collaborators.values().map(|c| c.user_id).collect::<Vec<_>>();
                         user_ids.sort();
                         assert_eq!(
                             user_ids,

crates/collab/src/tests/test_server.rs 🔗

@@ -29,7 +29,7 @@ use std::{
     },
 };
 use util::http::FakeHttpClient;
-use workspace::Workspace;
+use workspace::{Workspace, WorkspaceStore};
 
 pub struct TestServer {
     pub app_state: Arc<AppState>,
@@ -204,13 +204,17 @@ impl TestServer {
 
         let fs = FakeFs::new(cx.background());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+        let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
         let channel_store =
             cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+        let mut language_registry = LanguageRegistry::test();
+        language_registry.set_executor(cx.background());
         let app_state = Arc::new(workspace::AppState {
             client: client.clone(),
             user_store: user_store.clone(),
+            workspace_store,
             channel_store: channel_store.clone(),
-            languages: Arc::new(LanguageRegistry::test()),
+            languages: Arc::new(language_registry),
             fs: fs.clone(),
             build_window_options: |_, _, _| Default::default(),
             initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
@@ -536,15 +540,7 @@ impl TestClient {
         root_path: impl AsRef<Path>,
         cx: &mut TestAppContext,
     ) -> (ModelHandle<Project>, WorktreeId) {
-        let project = cx.update(|cx| {
-            Project::local(
-                self.client().clone(),
-                self.app_state.user_store.clone(),
-                self.app_state.languages.clone(),
-                self.app_state.fs.clone(),
-                cx,
-            )
-        });
+        let project = self.build_empty_local_project(cx);
         let (worktree, _) = project
             .update(cx, |p, cx| {
                 p.find_or_create_local_worktree(root_path, true, cx)
@@ -557,6 +553,18 @@ impl TestClient {
         (project, worktree.read_with(cx, |tree, _| tree.id()))
     }
 
+    pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> ModelHandle<Project> {
+        cx.update(|cx| {
+            Project::local(
+                self.client().clone(),
+                self.app_state.user_store.clone(),
+                self.app_state.languages.clone(),
+                self.app_state.fs.clone(),
+                cx,
+            )
+        })
+    }
+
     pub async fn build_remote_project(
         &self,
         host_project_id: u64,

crates/collab_ui/src/channel_view.rs 🔗

@@ -1,10 +1,12 @@
 use anyhow::{anyhow, Result};
 use call::report_call_event_for_channel;
-use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId};
-use client::proto;
-use clock::ReplicaId;
+use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId};
+use client::{
+    proto::{self, PeerId},
+    Collaborator, ParticipantIndex,
+};
 use collections::HashMap;
-use editor::Editor;
+use editor::{CollaborationHub, Editor};
 use gpui::{
     actions,
     elements::{ChildView, Label},
@@ -13,7 +15,11 @@ use gpui::{
     ViewContext, ViewHandle,
 };
 use project::Project;
-use std::any::{Any, TypeId};
+use std::{
+    any::{Any, TypeId},
+    sync::Arc,
+};
+use util::ResultExt;
 use workspace::{
     item::{FollowableItem, Item, ItemHandle},
     register_followable_item,
@@ -23,7 +29,7 @@ use workspace::{
 
 actions!(channel_view, [Deploy]);
 
-pub(crate) fn init(cx: &mut AppContext) {
+pub fn init(cx: &mut AppContext) {
     register_followable_item::<ChannelView>(cx)
 }
 
@@ -36,9 +42,13 @@ pub struct ChannelView {
 }
 
 impl ChannelView {
-    pub fn deploy(channel_id: ChannelId, workspace: ViewHandle<Workspace>, cx: &mut AppContext) {
+    pub fn open(
+        channel_id: ChannelId,
+        workspace: ViewHandle<Workspace>,
+        cx: &mut AppContext,
+    ) -> Task<Result<ViewHandle<Self>>> {
         let pane = workspace.read(cx).active_pane().clone();
-        let channel_view = Self::open(channel_id, pane.clone(), workspace.clone(), cx);
+        let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
         cx.spawn(|mut cx| async move {
             let channel_view = channel_view.await?;
             pane.update(&mut cx, |pane, cx| {
@@ -48,14 +58,13 @@ impl ChannelView {
                     &workspace.read(cx).app_state().client,
                     cx,
                 );
-                pane.add_item(Box::new(channel_view), true, true, None, cx);
+                pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
             });
-            anyhow::Ok(())
+            anyhow::Ok(channel_view)
         })
-        .detach();
     }
 
-    pub fn open(
+    pub fn open_in_pane(
         channel_id: ChannelId,
         pane: ViewHandle<Pane>,
         workspace: ViewHandle<Workspace>,
@@ -74,12 +83,13 @@ impl ChannelView {
         cx.spawn(|mut cx| async move {
             let channel_buffer = channel_buffer.await?;
 
-            let markdown = markdown.await?;
-            channel_buffer.update(&mut cx, |buffer, cx| {
-                buffer.buffer().update(cx, |buffer, cx| {
-                    buffer.set_language(Some(markdown), cx);
-                })
-            });
+            if let Some(markdown) = markdown.await.log_err() {
+                channel_buffer.update(&mut cx, |buffer, cx| {
+                    buffer.buffer().update(cx, |buffer, cx| {
+                        buffer.set_language(Some(markdown), cx);
+                    })
+                });
+            }
 
             pane.update(&mut cx, |pane, cx| {
                 pane.items_of_type::<Self>()
@@ -96,40 +106,29 @@ impl ChannelView {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let buffer = channel_buffer.read(cx).buffer();
-        let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
+        let editor = cx.add_view(|cx| {
+            let mut editor = Editor::for_buffer(buffer, None, cx);
+            editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
+                channel_buffer.clone(),
+            )));
+            editor
+        });
         let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
 
-        cx.subscribe(&project, Self::handle_project_event).detach();
         cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
             .detach();
 
-        let this = Self {
+        Self {
             editor,
             project,
             channel_buffer,
             remote_id: None,
             _editor_event_subscription,
-        };
-        this.refresh_replica_id_map(cx);
-        this
+        }
     }
 
-    fn handle_project_event(
-        &mut self,
-        _: ModelHandle<Project>,
-        event: &project::Event,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            project::Event::RemoteIdChanged(_) => {}
-            project::Event::DisconnectedFromHost => {}
-            project::Event::Closed => {}
-            project::Event::CollaboratorUpdated { .. } => {}
-            project::Event::CollaboratorLeft(_) => {}
-            project::Event::CollaboratorJoined(_) => {}
-            _ => return,
-        }
-        self.refresh_replica_id_map(cx);
+    pub fn channel(&self, cx: &AppContext) -> Arc<Channel> {
+        self.channel_buffer.read(cx).channel()
     }
 
     fn handle_channel_buffer_event(
@@ -138,51 +137,13 @@ impl ChannelView {
         event: &ChannelBufferEvent,
         cx: &mut ViewContext<Self>,
     ) {
-        match event {
-            ChannelBufferEvent::CollaboratorsChanged => {
-                self.refresh_replica_id_map(cx);
-            }
-            ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
+        if let ChannelBufferEvent::Disconnected = event {
+            self.editor.update(cx, |editor, cx| {
                 editor.set_read_only(true);
                 cx.notify();
-            }),
+            })
         }
     }
-
-    /// Build a mapping of channel buffer replica ids to the corresponding
-    /// replica ids in the current project.
-    ///
-    /// Using this mapping, a given user can be displayed with the same color
-    /// in the channel buffer as in other files in the project. Users who are
-    /// in the channel buffer but not the project will not have a color.
-    fn refresh_replica_id_map(&self, cx: &mut ViewContext<Self>) {
-        let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default();
-        let project = self.project.read(cx);
-        let channel_buffer = self.channel_buffer.read(cx);
-        project_replica_ids_by_channel_buffer_replica_id
-            .insert(channel_buffer.replica_id(cx), project.replica_id());
-        project_replica_ids_by_channel_buffer_replica_id.extend(
-            channel_buffer
-                .collaborators()
-                .iter()
-                .filter_map(|channel_buffer_collaborator| {
-                    project
-                        .collaborators()
-                        .values()
-                        .find_map(|project_collaborator| {
-                            (project_collaborator.user_id == channel_buffer_collaborator.user_id)
-                                .then_some((
-                                    channel_buffer_collaborator.replica_id as ReplicaId,
-                                    project_collaborator.replica_id,
-                                ))
-                        })
-                }),
-        );
-
-        self.editor.update(cx, |editor, cx| {
-            editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx)
-        });
-    }
 }
 
 impl Entity for ChannelView {
@@ -311,7 +272,7 @@ impl FollowableItem for ChannelView {
             unreachable!()
         };
 
-        let open = ChannelView::open(state.channel_id, pane, workspace, cx);
+        let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
 
         Some(cx.spawn(|mut cx| async move {
             let this = open.await?;
@@ -371,17 +332,32 @@ impl FollowableItem for ChannelView {
         })
     }
 
-    fn set_leader_replica_id(
-        &mut self,
-        leader_replica_id: Option<u16>,
-        cx: &mut ViewContext<Self>,
-    ) {
+    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
         self.editor.update(cx, |editor, cx| {
-            editor.set_leader_replica_id(leader_replica_id, cx)
+            editor.set_leader_peer_id(leader_peer_id, cx)
         })
     }
 
     fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
         Editor::should_unfollow_on_event(event, cx)
     }
+
+    fn is_project_item(&self, _cx: &AppContext) -> bool {
+        false
+    }
+}
+
+struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
+
+impl CollaborationHub for ChannelBufferCollaborationHub {
+    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
+        self.0.read(cx).collaborators()
+    }
+
+    fn user_participant_indices<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> &'a HashMap<u64, ParticipantIndex> {
+        self.0.read(cx).user_store().read(cx).participant_indices()
+    }
 }

crates/collab_ui/src/chat_panel.rs 🔗

@@ -409,7 +409,7 @@ impl ChatPanel {
                 })
                 .on_click(MouseButton::Left, move |_, _, cx| {
                     if let Some(workspace) = workspace.upgrade(cx) {
-                        ChannelView::deploy(channel_id, workspace, cx);
+                        ChannelView::open(channel_id, workspace, cx).detach();
                     }
                 })
                 .with_tooltip::<OpenChannelNotes>(
@@ -546,7 +546,7 @@ impl ChatPanel {
         if let Some((chat, _)) = &self.active_chat {
             let channel_id = chat.read(cx).channel().id;
             if let Some(workspace) = self.workspace.upgrade(cx) {
-                ChannelView::deploy(channel_id, workspace, cx);
+                ChannelView::open(channel_id, workspace, cx).detach();
             }
         }
     }

crates/collab_ui/src/collab_panel.rs 🔗

@@ -47,7 +47,7 @@ use util::{iife, ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel},
     item::ItemHandle,
-    Workspace,
+    FollowNextCollaborator, Workspace,
 };
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -404,6 +404,7 @@ enum ListEntry {
     Header(Section),
     CallParticipant {
         user: Arc<User>,
+        peer_id: Option<PeerId>,
         is_pending: bool,
     },
     ParticipantProject {
@@ -508,14 +509,19 @@ impl CollabPanel {
                             let is_collapsed = this.collapsed_sections.contains(section);
                             this.render_header(*section, &theme, is_selected, is_collapsed, cx)
                         }
-                        ListEntry::CallParticipant { user, is_pending } => {
-                            Self::render_call_participant(
-                                user,
-                                *is_pending,
-                                is_selected,
-                                &theme.collab_panel,
-                            )
-                        }
+                        ListEntry::CallParticipant {
+                            user,
+                            peer_id,
+                            is_pending,
+                        } => Self::render_call_participant(
+                            user,
+                            *peer_id,
+                            this.user_store.clone(),
+                            *is_pending,
+                            is_selected,
+                            &theme,
+                            cx,
+                        ),
                         ListEntry::ParticipantProject {
                             project_id,
                             worktree_root_names,
@@ -528,7 +534,7 @@ impl CollabPanel {
                             Some(*project_id) == current_project_id,
                             *is_last,
                             is_selected,
-                            &theme.collab_panel,
+                            &theme,
                             cx,
                         ),
                         ListEntry::ParticipantScreen { peer_id, is_last } => {
@@ -793,6 +799,7 @@ impl CollabPanel {
                         let user_id = user.id;
                         self.entries.push(ListEntry::CallParticipant {
                             user,
+                            peer_id: None,
                             is_pending: false,
                         });
                         let mut projects = room.local_participant().projects.iter().peekable();
@@ -830,6 +837,7 @@ impl CollabPanel {
                     let participant = &room.remote_participants()[&user_id];
                     self.entries.push(ListEntry::CallParticipant {
                         user: participant.user.clone(),
+                        peer_id: Some(participant.peer_id),
                         is_pending: false,
                     });
                     let mut projects = participant.projects.iter().peekable();
@@ -871,6 +879,7 @@ impl CollabPanel {
                 self.entries
                     .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
                         user: room.pending_participants()[mat.candidate_id].clone(),
+                        peer_id: None,
                         is_pending: true,
                     }));
             }
@@ -1174,46 +1183,97 @@ impl CollabPanel {
 
     fn render_call_participant(
         user: &User,
+        peer_id: Option<PeerId>,
+        user_store: ModelHandle<UserStore>,
         is_pending: bool,
         is_selected: bool,
-        theme: &theme::CollabPanel,
+        theme: &theme::Theme,
+        cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
-        Flex::row()
-            .with_children(user.avatar.clone().map(|avatar| {
-                Image::from_data(avatar)
-                    .with_style(theme.contact_avatar)
-                    .aligned()
-                    .left()
-            }))
-            .with_child(
-                Label::new(
-                    user.github_login.clone(),
-                    theme.contact_username.text.clone(),
-                )
-                .contained()
-                .with_style(theme.contact_username.container)
-                .aligned()
-                .left()
-                .flex(1., true),
-            )
-            .with_children(if is_pending {
-                Some(
-                    Label::new("Calling", theme.calling_indicator.text.clone())
+        enum CallParticipant {}
+        enum CallParticipantTooltip {}
+
+        let collab_theme = &theme.collab_panel;
+
+        let is_current_user =
+            user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
+
+        let content =
+            MouseEventHandler::new::<CallParticipant, _>(user.id as usize, cx, |mouse_state, _| {
+                let style = if is_current_user {
+                    *collab_theme
+                        .contact_row
+                        .in_state(is_selected)
+                        .style_for(&mut Default::default())
+                } else {
+                    *collab_theme
+                        .contact_row
+                        .in_state(is_selected)
+                        .style_for(mouse_state)
+                };
+
+                Flex::row()
+                    .with_children(user.avatar.clone().map(|avatar| {
+                        Image::from_data(avatar)
+                            .with_style(collab_theme.contact_avatar)
+                            .aligned()
+                            .left()
+                    }))
+                    .with_child(
+                        Label::new(
+                            user.github_login.clone(),
+                            collab_theme.contact_username.text.clone(),
+                        )
                         .contained()
-                        .with_style(theme.calling_indicator.container)
-                        .aligned(),
-                )
-            } else {
-                None
+                        .with_style(collab_theme.contact_username.container)
+                        .aligned()
+                        .left()
+                        .flex(1., true),
+                    )
+                    .with_children(if is_pending {
+                        Some(
+                            Label::new("Calling", collab_theme.calling_indicator.text.clone())
+                                .contained()
+                                .with_style(collab_theme.calling_indicator.container)
+                                .aligned(),
+                        )
+                    } else if is_current_user {
+                        Some(
+                            Label::new("You", collab_theme.calling_indicator.text.clone())
+                                .contained()
+                                .with_style(collab_theme.calling_indicator.container)
+                                .aligned(),
+                        )
+                    } else {
+                        None
+                    })
+                    .constrained()
+                    .with_height(collab_theme.row_height)
+                    .contained()
+                    .with_style(style)
+            });
+
+        if is_current_user || is_pending || peer_id.is_none() {
+            return content.into_any();
+        }
+
+        let tooltip = format!("Follow {}", user.github_login);
+
+        content
+            .on_click(MouseButton::Left, move |_, this, cx| {
+                if let Some(workspace) = this.workspace.upgrade(cx) {
+                    workspace
+                        .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx))
+                        .map(|task| task.detach_and_log_err(cx));
+                }
             })
-            .constrained()
-            .with_height(theme.row_height)
-            .contained()
-            .with_style(
-                *theme
-                    .contact_row
-                    .in_state(is_selected)
-                    .style_for(&mut Default::default()),
+            .with_cursor_style(CursorStyle::PointingHand)
+            .with_tooltip::<CallParticipantTooltip>(
+                user.id as usize,
+                tooltip,
+                Some(Box::new(FollowNextCollaborator)),
+                theme.tooltip.clone(),
+                cx,
             )
             .into_any()
     }
@@ -1225,74 +1285,91 @@ impl CollabPanel {
         is_current: bool,
         is_last: bool,
         is_selected: bool,
-        theme: &theme::CollabPanel,
+        theme: &theme::Theme,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
         enum JoinProject {}
+        enum JoinProjectTooltip {}
 
-        let host_avatar_width = theme
+        let collab_theme = &theme.collab_panel;
+        let host_avatar_width = collab_theme
             .contact_avatar
             .width
-            .or(theme.contact_avatar.height)
+            .or(collab_theme.contact_avatar.height)
             .unwrap_or(0.);
-        let tree_branch = theme.tree_branch;
+        let tree_branch = collab_theme.tree_branch;
         let project_name = if worktree_root_names.is_empty() {
             "untitled".to_string()
         } else {
             worktree_root_names.join(", ")
         };
 
-        MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
-            let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
-            let row = theme
-                .project_row
-                .in_state(is_selected)
-                .style_for(mouse_state);
+        let content =
+            MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
+                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+                let row = if is_current {
+                    collab_theme
+                        .project_row
+                        .in_state(true)
+                        .style_for(&mut Default::default())
+                } else {
+                    collab_theme
+                        .project_row
+                        .in_state(is_selected)
+                        .style_for(mouse_state)
+                };
 
-            Flex::row()
-                .with_child(render_tree_branch(
-                    tree_branch,
-                    &row.name.text,
-                    is_last,
-                    vec2f(host_avatar_width, theme.row_height),
-                    cx.font_cache(),
-                ))
-                .with_child(
-                    Svg::new("icons/file_icons/folder.svg")
-                        .with_color(theme.channel_hash.color)
-                        .constrained()
-                        .with_width(theme.channel_hash.width)
-                        .aligned()
-                        .left(),
-                )
-                .with_child(
-                    Label::new(project_name, row.name.text.clone())
-                        .aligned()
-                        .left()
-                        .contained()
-                        .with_style(row.name.container)
-                        .flex(1., false),
-                )
-                .constrained()
-                .with_height(theme.row_height)
-                .contained()
-                .with_style(row.container)
-        })
-        .with_cursor_style(if !is_current {
-            CursorStyle::PointingHand
-        } else {
-            CursorStyle::Arrow
-        })
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            if !is_current {
+                Flex::row()
+                    .with_child(render_tree_branch(
+                        tree_branch,
+                        &row.name.text,
+                        is_last,
+                        vec2f(host_avatar_width, collab_theme.row_height),
+                        cx.font_cache(),
+                    ))
+                    .with_child(
+                        Svg::new("icons/file_icons/folder.svg")
+                            .with_color(collab_theme.channel_hash.color)
+                            .constrained()
+                            .with_width(collab_theme.channel_hash.width)
+                            .aligned()
+                            .left(),
+                    )
+                    .with_child(
+                        Label::new(project_name.clone(), row.name.text.clone())
+                            .aligned()
+                            .left()
+                            .contained()
+                            .with_style(row.name.container)
+                            .flex(1., false),
+                    )
+                    .constrained()
+                    .with_height(collab_theme.row_height)
+                    .contained()
+                    .with_style(row.container)
+            });
+
+        if is_current {
+            return content.into_any();
+        }
+
+        content
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, move |_, this, cx| {
                 if let Some(workspace) = this.workspace.upgrade(cx) {
                     let app_state = workspace.read(cx).app_state().clone();
                     workspace::join_remote_project(project_id, host_user_id, app_state, cx)
                         .detach_and_log_err(cx);
                 }
-            }
-        })
-        .into_any()
+            })
+            .with_tooltip::<JoinProjectTooltip>(
+                project_id as usize,
+                format!("Open {}", project_name),
+                None,
+                theme.tooltip.clone(),
+                cx,
+            )
+            .into_any()
     }
 
     fn render_participant_screen(
@@ -2755,7 +2832,7 @@ impl CollabPanel {
 
     fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
         if let Some(workspace) = self.workspace.upgrade(cx) {
-            ChannelView::deploy(action.channel_id, workspace, cx);
+            ChannelView::open(action.channel_id, workspace, cx).detach();
         }
     }
 

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -215,7 +215,13 @@ impl CollabTitlebarItem {
         let git_style = theme.titlebar.git_menu_button.clone();
         let item_spacing = theme.titlebar.item_spacing;
 
-        let mut ret = Flex::row().with_child(
+        let mut ret = Flex::row();
+
+        if let Some(project_host) = self.collect_project_host(theme.clone(), cx) {
+            ret = ret.with_child(project_host)
+        }
+
+        ret = ret.with_child(
             Stack::new()
                 .with_child(
                     MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
@@ -283,6 +289,71 @@ impl CollabTitlebarItem {
         ret.into_any()
     }
 
+    fn collect_project_host(
+        &self,
+        theme: Arc<Theme>,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<AnyElement<Self>> {
+        if ActiveCall::global(cx).read(cx).room().is_none() {
+            return None;
+        }
+        let project = self.project.read(cx);
+        let user_store = self.user_store.read(cx);
+
+        if project.is_local() {
+            return None;
+        }
+
+        let Some(host) = project.host() else {
+            return None;
+        };
+        let (Some(host_user), Some(participant_index)) = (
+            user_store.get_cached_user(host.user_id),
+            user_store.participant_indices().get(&host.user_id),
+        ) else {
+            return None;
+        };
+
+        enum ProjectHost {}
+        enum ProjectHostTooltip {}
+
+        let host_style = theme.titlebar.project_host.clone();
+        let selection_style = theme
+            .editor
+            .selection_style_for_room_participant(participant_index.0);
+        let peer_id = host.peer_id.clone();
+
+        Some(
+            MouseEventHandler::new::<ProjectHost, _>(0, cx, |mouse_state, _| {
+                let mut host_style = host_style.style_for(mouse_state).clone();
+                host_style.text.color = selection_style.cursor;
+                Label::new(host_user.github_login.clone(), host_style.text)
+                    .contained()
+                    .with_style(host_style.container)
+                    .aligned()
+                    .left()
+            })
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, move |_, this, cx| {
+                if let Some(workspace) = this.workspace.upgrade(cx) {
+                    if let Some(task) =
+                        workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+                    {
+                        task.detach_and_log_err(cx);
+                    }
+                }
+            })
+            .with_tooltip::<ProjectHostTooltip>(
+                0,
+                host_user.github_login.clone() + " is sharing this project. Click to follow.",
+                None,
+                theme.tooltip.clone(),
+                cx,
+            )
+            .into_any_named("project-host"),
+        )
+    }
+
     fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
         let project = if active {
             Some(self.project.clone())
@@ -877,7 +948,7 @@ impl CollabTitlebarItem {
     fn render_face_pile(
         &self,
         user: &User,
-        replica_id: Option<ReplicaId>,
+        _replica_id: Option<ReplicaId>,
         peer_id: PeerId,
         location: Option<ParticipantLocation>,
         muted: bool,
@@ -886,23 +957,20 @@ impl CollabTitlebarItem {
         theme: &Theme,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
+        let user_id = user.id;
         let project_id = workspace.read(cx).project().read(cx).remote_id();
-        let room = ActiveCall::global(cx).read(cx).room();
-        let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
-        let followed_by_self = room
-            .and_then(|room| {
-                Some(
-                    is_being_followed
-                        && room
-                            .read(cx)
-                            .followers_for(peer_id, project_id?)
-                            .iter()
-                            .any(|&follower| {
-                                Some(follower) == workspace.read(cx).client().peer_id()
-                            }),
-                )
-            })
-            .unwrap_or(false);
+        let room = ActiveCall::global(cx).read(cx).room().cloned();
+        let self_peer_id = workspace.read(cx).client().peer_id();
+        let self_following = workspace.read(cx).is_being_followed(peer_id);
+        let self_following_initialized = self_following
+            && room.as_ref().map_or(false, |room| match project_id {
+                None => true,
+                Some(project_id) => room
+                    .read(cx)
+                    .followers_for(peer_id, project_id)
+                    .iter()
+                    .any(|&follower| Some(follower) == self_peer_id),
+            });
 
         let leader_style = theme.titlebar.leader_avatar;
         let follower_style = theme.titlebar.follower_avatar;
@@ -921,147 +989,131 @@ impl CollabTitlebarItem {
             .background_color
             .unwrap_or_default();
 
-        if let Some(replica_id) = replica_id {
-            if followed_by_self {
-                let selection = theme.editor.replica_selection_style(replica_id).selection;
+        let participant_index = self
+            .user_store
+            .read(cx)
+            .participant_indices()
+            .get(&user_id)
+            .copied();
+        if let Some(participant_index) = participant_index {
+            if self_following_initialized {
+                let selection = theme
+                    .editor
+                    .selection_style_for_room_participant(participant_index.0)
+                    .selection;
                 background_color = Color::blend(selection, background_color);
                 background_color.a = 255;
             }
         }
 
-        let mut content = Stack::new()
-            .with_children(user.avatar.as_ref().map(|avatar| {
-                let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
-                    .with_child(Self::render_face(
-                        avatar.clone(),
-                        Self::location_style(workspace, location, leader_style, cx),
-                        background_color,
-                        microphone_state,
-                    ))
-                    .with_children(
-                        (|| {
-                            let project_id = project_id?;
-                            let room = room?.read(cx);
-                            let followers = room.followers_for(peer_id, project_id);
-
-                            Some(followers.into_iter().flat_map(|&follower| {
-                                let remote_participant =
-                                    room.remote_participant_for_peer_id(follower);
-
-                                let avatar = remote_participant
-                                    .and_then(|p| p.user.avatar.clone())
-                                    .or_else(|| {
-                                        if follower == workspace.read(cx).client().peer_id()? {
-                                            workspace
-                                                .read(cx)
-                                                .user_store()
-                                                .read(cx)
-                                                .current_user()?
-                                                .avatar
-                                                .clone()
-                                        } else {
-                                            None
-                                        }
-                                    })?;
-
-                                Some(Self::render_face(
-                                    avatar.clone(),
-                                    follower_style,
-                                    background_color,
-                                    None,
-                                ))
-                            }))
-                        })()
-                        .into_iter()
-                        .flatten(),
-                    );
-
-                let mut container = face_pile
-                    .contained()
-                    .with_style(theme.titlebar.leader_selection);
-
-                if let Some(replica_id) = replica_id {
-                    if followed_by_self {
-                        let color = theme.editor.replica_selection_style(replica_id).selection;
-                        container = container.with_background_color(color);
-                    }
-                }
-
-                container
-            }))
-            .with_children((|| {
-                let replica_id = replica_id?;
-                let color = theme.editor.replica_selection_style(replica_id).cursor;
-                Some(
-                    AvatarRibbon::new(color)
-                        .constrained()
-                        .with_width(theme.titlebar.avatar_ribbon.width)
-                        .with_height(theme.titlebar.avatar_ribbon.height)
-                        .aligned()
-                        .bottom(),
-                )
-            })())
-            .into_any();
-
-        if let Some(location) = location {
-            if let Some(replica_id) = replica_id {
-                enum ToggleFollow {}
+        enum TitlebarParticipant {}
 
-                content = MouseEventHandler::new::<ToggleFollow, _>(
-                    replica_id.into(),
-                    cx,
-                    move |_, _| content,
-                )
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, item, cx| {
-                    if let Some(workspace) = item.workspace.upgrade(cx) {
-                        if let Some(task) = workspace
-                            .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
-                        {
-                            task.detach_and_log_err(cx);
+        let content = MouseEventHandler::new::<TitlebarParticipant, _>(
+            peer_id.as_u64() as usize,
+            cx,
+            move |_, cx| {
+                Stack::new()
+                    .with_children(user.avatar.as_ref().map(|avatar| {
+                        let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
+                            .with_child(Self::render_face(
+                                avatar.clone(),
+                                Self::location_style(workspace, location, leader_style, cx),
+                                background_color,
+                                microphone_state,
+                            ))
+                            .with_children(
+                                (|| {
+                                    let project_id = project_id?;
+                                    let room = room?.read(cx);
+                                    let followers = room.followers_for(peer_id, project_id);
+                                    Some(followers.into_iter().filter_map(|&follower| {
+                                        if Some(follower) == self_peer_id {
+                                            return None;
+                                        }
+                                        let participant =
+                                            room.remote_participant_for_peer_id(follower)?;
+                                        Some(Self::render_face(
+                                            participant.user.avatar.clone()?,
+                                            follower_style,
+                                            background_color,
+                                            None,
+                                        ))
+                                    }))
+                                })()
+                                .into_iter()
+                                .flatten(),
+                            )
+                            .with_children(
+                                self_following_initialized
+                                    .then(|| self.user_store.read(cx).current_user())
+                                    .and_then(|user| {
+                                        Some(Self::render_face(
+                                            user?.avatar.clone()?,
+                                            follower_style,
+                                            background_color,
+                                            None,
+                                        ))
+                                    }),
+                            );
+
+                        let mut container = face_pile
+                            .contained()
+                            .with_style(theme.titlebar.leader_selection);
+
+                        if let Some(participant_index) = participant_index {
+                            if self_following_initialized {
+                                let color = theme
+                                    .editor
+                                    .selection_style_for_room_participant(participant_index.0)
+                                    .selection;
+                                container = container.with_background_color(color);
+                            }
                         }
-                    }
-                })
-                .with_tooltip::<ToggleFollow>(
-                    peer_id.as_u64() as usize,
-                    if is_being_followed {
-                        format!("Unfollow {}", user.github_login)
-                    } else {
-                        format!("Follow {}", user.github_login)
-                    },
-                    Some(Box::new(FollowNextCollaborator)),
-                    theme.tooltip.clone(),
-                    cx,
-                )
-                .into_any();
-            } else if let ParticipantLocation::SharedProject { project_id } = location {
-                enum JoinProject {}
 
-                let user_id = user.id;
-                content = MouseEventHandler::new::<JoinProject, _>(
-                    peer_id.as_u64() as usize,
-                    cx,
-                    move |_, _| content,
-                )
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    if let Some(workspace) = this.workspace.upgrade(cx) {
-                        let app_state = workspace.read(cx).app_state().clone();
-                        workspace::join_remote_project(project_id, user_id, app_state, cx)
-                            .detach_and_log_err(cx);
-                    }
-                })
-                .with_tooltip::<JoinProject>(
-                    peer_id.as_u64() as usize,
-                    format!("Follow {} into external project", user.github_login),
-                    Some(Box::new(FollowNextCollaborator)),
-                    theme.tooltip.clone(),
-                    cx,
-                )
-                .into_any();
-            }
+                        container
+                    }))
+                    .with_children((|| {
+                        let participant_index = participant_index?;
+                        let color = theme
+                            .editor
+                            .selection_style_for_room_participant(participant_index.0)
+                            .cursor;
+                        Some(
+                            AvatarRibbon::new(color)
+                                .constrained()
+                                .with_width(theme.titlebar.avatar_ribbon.width)
+                                .with_height(theme.titlebar.avatar_ribbon.height)
+                                .aligned()
+                                .bottom(),
+                        )
+                    })())
+            },
+        );
+
+        if Some(peer_id) == self_peer_id {
+            return content.into_any();
         }
+
         content
+            .with_cursor_style(CursorStyle::PointingHand)
+            .on_click(MouseButton::Left, move |_, this, cx| {
+                let Some(workspace) = this.workspace.upgrade(cx) else {
+                    return;
+                };
+                if let Some(task) =
+                    workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+                {
+                    task.detach_and_log_err(cx);
+                }
+            })
+            .with_tooltip::<TitlebarParticipant>(
+                peer_id.as_u64() as usize,
+                format!("Follow {}", user.github_login),
+                Some(Box::new(FollowNextCollaborator)),
+                theme.tooltip.clone(),
+                cx,
+            )
+            .into_any()
     }
 
     fn location_style(

crates/collab_ui/src/collab_ui.rs 🔗

@@ -7,7 +7,7 @@ mod face_pile;
 mod incoming_call_notification;
 mod notifications;
 mod panel_settings;
-mod project_shared_notification;
+pub mod project_shared_notification;
 mod sharing_status_indicator;
 
 use call::{report_call_event_for_room, ActiveCall, Room};

crates/collab_ui/src/project_shared_notification.rs 🔗

@@ -40,7 +40,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
                     .push(window);
             }
         }
-        room::Event::RemoteProjectUnshared { project_id } => {
+        room::Event::RemoteProjectUnshared { project_id }
+        | room::Event::RemoteProjectJoined { project_id }
+        | room::Event::RemoteProjectInvitationDiscarded { project_id } => {
             if let Some(windows) = notification_windows.remove(&project_id) {
                 for window in windows {
                     window.remove(cx);
@@ -82,7 +84,6 @@ impl ProjectSharedNotification {
     }
 
     fn join(&mut self, cx: &mut ViewContext<Self>) {
-        cx.remove_window();
         if let Some(app_state) = self.app_state.upgrade() {
             workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
                 .detach_and_log_err(cx);
@@ -90,7 +91,15 @@ impl ProjectSharedNotification {
     }
 
     fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
-        cx.remove_window();
+        if let Some(active_room) =
+            ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
+        {
+            active_room.update(cx, |_, cx| {
+                cx.emit(room::Event::RemoteProjectInvitationDiscarded {
+                    project_id: self.project_id,
+                });
+            });
+        }
     }
 
     fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {

crates/editor/src/editor.rs 🔗

@@ -25,7 +25,7 @@ use ::git::diff::DiffHunk;
 use aho_corasick::AhoCorasick;
 use anyhow::{anyhow, Context, Result};
 use blink_manager::BlinkManager;
-use client::{ClickhouseEvent, TelemetrySettings};
+use client::{ClickhouseEvent, Collaborator, ParticipantIndex, TelemetrySettings};
 use clock::{Global, ReplicaId};
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
 use convert_case::{Case, Casing};
@@ -79,6 +79,7 @@ pub use multi_buffer::{
 use ordered_float::OrderedFloat;
 use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
 use rand::{seq::SliceRandom, thread_rng};
+use rpc::proto::PeerId;
 use scroll::{
     autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
 };
@@ -581,11 +582,11 @@ pub struct Editor {
     get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
     override_text_style: Option<Box<OverrideTextStyle>>,
     project: Option<ModelHandle<Project>>,
+    collaboration_hub: Option<Box<dyn CollaborationHub>>,
     focused: bool,
     blink_manager: ModelHandle<BlinkManager>,
     pub show_local_selections: bool,
     mode: EditorMode,
-    replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
     show_gutter: bool,
     show_wrap_guides: Option<bool>,
     placeholder_text: Option<Arc<str>>,
@@ -609,7 +610,7 @@ pub struct Editor {
     keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
     input_enabled: bool,
     read_only: bool,
-    leader_replica_id: Option<u16>,
+    leader_peer_id: Option<PeerId>,
     remote_id: Option<ViewId>,
     hover_state: HoverState,
     gutter_hovered: bool,
@@ -631,6 +632,15 @@ pub struct EditorSnapshot {
     ongoing_scroll: OngoingScroll,
 }
 
+pub struct RemoteSelection {
+    pub replica_id: ReplicaId,
+    pub selection: Selection<Anchor>,
+    pub cursor_shape: CursorShape,
+    pub peer_id: PeerId,
+    pub line_mode: bool,
+    pub participant_index: Option<ParticipantIndex>,
+}
+
 #[derive(Clone, Debug)]
 struct SelectionHistoryEntry {
     selections: Arc<[Selection<Anchor>]>,
@@ -1047,7 +1057,8 @@ impl CompletionsMenu {
                                     item_ix: Some(item_ix),
                                 },
                                 cx,
-                            );
+                            )
+                            .map(|task| task.detach());
                         })
                         .into_any(),
                     );
@@ -1539,12 +1550,12 @@ impl Editor {
             active_diagnostics: None,
             soft_wrap_mode_override,
             get_field_editor_theme,
+            collaboration_hub: project.clone().map(|project| Box::new(project) as _),
             project,
             focused: false,
             blink_manager: blink_manager.clone(),
             show_local_selections: true,
             mode,
-            replica_id_mapping: None,
             show_gutter: mode == EditorMode::Full,
             show_wrap_guides: None,
             placeholder_text: None,
@@ -1571,7 +1582,7 @@ impl Editor {
             keymap_context_layers: Default::default(),
             input_enabled: true,
             read_only: false,
-            leader_replica_id: None,
+            leader_peer_id: None,
             remote_id: None,
             hover_state: Default::default(),
             link_go_to_definition_state: Default::default(),
@@ -1658,8 +1669,8 @@ impl Editor {
         self.buffer.read(cx).replica_id()
     }
 
-    pub fn leader_replica_id(&self) -> Option<ReplicaId> {
-        self.leader_replica_id
+    pub fn leader_peer_id(&self) -> Option<PeerId> {
+        self.leader_peer_id
     }
 
     pub fn buffer(&self) -> &ModelHandle<MultiBuffer> {
@@ -1723,6 +1734,14 @@ impl Editor {
         self.mode
     }
 
+    pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> {
+        self.collaboration_hub.as_deref()
+    }
+
+    pub fn set_collaboration_hub(&mut self, hub: Box<dyn CollaborationHub>) {
+        self.collaboration_hub = Some(hub);
+    }
+
     pub fn set_placeholder_text(
         &mut self,
         placeholder_text: impl Into<Arc<str>>,
@@ -1799,26 +1818,13 @@ impl Editor {
         cx.notify();
     }
 
-    pub fn replica_id_map(&self) -> Option<&HashMap<ReplicaId, ReplicaId>> {
-        self.replica_id_mapping.as_ref()
-    }
-
-    pub fn set_replica_id_map(
-        &mut self,
-        mapping: Option<HashMap<ReplicaId, ReplicaId>>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.replica_id_mapping = mapping;
-        cx.notify();
-    }
-
     fn selections_did_change(
         &mut self,
         local: bool,
         old_cursor_position: &Anchor,
         cx: &mut ViewContext<Self>,
     ) {
-        if self.focused && self.leader_replica_id.is_none() {
+        if self.focused && self.leader_peer_id.is_none() {
             self.buffer.update(cx, |buffer, cx| {
                 buffer.set_active_selections(
                     &self.selections.disjoint_anchors(),
@@ -8625,6 +8631,27 @@ impl Editor {
     }
 }
 
+pub trait CollaborationHub {
+    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator>;
+    fn user_participant_indices<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> &'a HashMap<u64, ParticipantIndex>;
+}
+
+impl CollaborationHub for ModelHandle<Project> {
+    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
+        self.read(cx).collaborators()
+    }
+
+    fn user_participant_indices<'a>(
+        &self,
+        cx: &'a AppContext,
+    ) -> &'a HashMap<u64, ParticipantIndex> {
+        self.read(cx).user_store().read(cx).participant_indices()
+    }
+}
+
 fn inlay_hint_settings(
     location: Anchor,
     snapshot: &MultiBufferSnapshot,
@@ -8668,6 +8695,34 @@ fn ending_row(next_selection: &Selection<Point>, display_map: &DisplaySnapshot)
 }
 
 impl EditorSnapshot {
+    pub fn remote_selections_in_range<'a>(
+        &'a self,
+        range: &'a Range<Anchor>,
+        collaboration_hub: &dyn CollaborationHub,
+        cx: &'a AppContext,
+    ) -> impl 'a + Iterator<Item = RemoteSelection> {
+        let participant_indices = collaboration_hub.user_participant_indices(cx);
+        let collaborators_by_peer_id = collaboration_hub.collaborators(cx);
+        let collaborators_by_replica_id = collaborators_by_peer_id
+            .iter()
+            .map(|(_, collaborator)| (collaborator.replica_id, collaborator))
+            .collect::<HashMap<_, _>>();
+        self.buffer_snapshot
+            .remote_selections_in_range(range)
+            .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| {
+                let collaborator = collaborators_by_replica_id.get(&replica_id)?;
+                let participant_index = participant_indices.get(&collaborator.user_id).copied();
+                Some(RemoteSelection {
+                    replica_id,
+                    selection,
+                    cursor_shape,
+                    line_mode,
+                    participant_index,
+                    peer_id: collaborator.peer_id,
+                })
+            })
+    }
+
     pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
         self.display_snapshot.buffer_snapshot.language_at(position)
     }
@@ -8781,7 +8836,7 @@ impl View for Editor {
             self.focused = true;
             self.buffer.update(cx, |buffer, cx| {
                 buffer.finalize_last_transaction(cx);
-                if self.leader_replica_id.is_none() {
+                if self.leader_peer_id.is_none() {
                     buffer.set_active_selections(
                         &self.selections.disjoint_anchors(),
                         self.selections.line_mode,

crates/editor/src/element.rs 🔗

@@ -17,7 +17,6 @@ use crate::{
     },
     mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
 };
-use clock::ReplicaId;
 use collections::{BTreeMap, HashMap};
 use git::diff::DiffHunkStatus;
 use gpui::{
@@ -55,6 +54,7 @@ use std::{
     sync::Arc,
 };
 use text::Point;
+use theme::SelectionStyle;
 use workspace::item::Item;
 
 enum FoldMarkers {}
@@ -868,14 +868,7 @@ impl EditorElement {
         let corner_radius = 0.15 * layout.position_map.line_height;
         let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
 
-        for (replica_id, selections) in &layout.selections {
-            let replica_id = *replica_id;
-            let selection_style = if let Some(replica_id) = replica_id {
-                style.replica_selection_style(replica_id)
-            } else {
-                &style.absent_selection
-            };
-
+        for (selection_style, selections) in &layout.selections {
             for selection in selections {
                 self.paint_highlighted_range(
                     selection.range.clone(),
@@ -2193,7 +2186,7 @@ impl Element<Editor> for EditorElement {
                 .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
         };
 
-        let mut selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)> = Vec::new();
+        let mut selections: Vec<(SelectionStyle, Vec<SelectionLayout>)> = Vec::new();
         let mut active_rows = BTreeMap::new();
         let mut fold_ranges = Vec::new();
         let is_singleton = editor.is_singleton(cx);
@@ -2219,35 +2212,6 @@ impl Element<Editor> for EditorElement {
                 }),
         );
 
-        let mut remote_selections = HashMap::default();
-        for (replica_id, line_mode, cursor_shape, selection) in snapshot
-            .buffer_snapshot
-            .remote_selections_in_range(&(start_anchor..end_anchor))
-        {
-            let replica_id = if let Some(mapping) = &editor.replica_id_mapping {
-                mapping.get(&replica_id).copied()
-            } else {
-                Some(replica_id)
-            };
-
-            // The local selections match the leader's selections.
-            if replica_id.is_some() && replica_id == editor.leader_replica_id {
-                continue;
-            }
-            remote_selections
-                .entry(replica_id)
-                .or_insert(Vec::new())
-                .push(SelectionLayout::new(
-                    selection,
-                    line_mode,
-                    cursor_shape,
-                    &snapshot.display_snapshot,
-                    false,
-                    false,
-                ));
-        }
-        selections.extend(remote_selections);
-
         let mut newest_selection_head = None;
 
         if editor.show_local_selections {
@@ -2282,19 +2246,58 @@ impl Element<Editor> for EditorElement {
                 layouts.push(layout);
             }
 
-            // Render the local selections in the leader's color when following.
-            let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id {
-                leader_replica_id
-            } else {
-                let replica_id = editor.replica_id(cx);
-                if let Some(mapping) = &editor.replica_id_mapping {
-                    mapping.get(&replica_id).copied().unwrap_or(replica_id)
+            selections.push((style.selection, layouts));
+        }
+
+        if let Some(collaboration_hub) = &editor.collaboration_hub {
+            // When following someone, render the local selections in their color.
+            if let Some(leader_id) = editor.leader_peer_id {
+                if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
+                    if let Some(participant_index) = collaboration_hub
+                        .user_participant_indices(cx)
+                        .get(&collaborator.user_id)
+                    {
+                        if let Some((local_selection_style, _)) = selections.first_mut() {
+                            *local_selection_style =
+                                style.selection_style_for_room_participant(participant_index.0);
+                        }
+                    }
+                }
+            }
+
+            let mut remote_selections = HashMap::default();
+            for selection in snapshot.remote_selections_in_range(
+                &(start_anchor..end_anchor),
+                collaboration_hub.as_ref(),
+                cx,
+            ) {
+                let selection_style = if let Some(participant_index) = selection.participant_index {
+                    style.selection_style_for_room_participant(participant_index.0)
                 } else {
-                    replica_id
+                    style.absent_selection
+                };
+
+                // Don't re-render the leader's selections, since the local selections
+                // match theirs.
+                if Some(selection.peer_id) == editor.leader_peer_id {
+                    continue;
                 }
-            };
 
-            selections.push((Some(local_replica_id), layouts));
+                remote_selections
+                    .entry(selection.replica_id)
+                    .or_insert((selection_style, Vec::new()))
+                    .1
+                    .push(SelectionLayout::new(
+                        selection.selection,
+                        selection.line_mode,
+                        selection.cursor_shape,
+                        &snapshot.display_snapshot,
+                        false,
+                        false,
+                    ));
+            }
+
+            selections.extend(remote_selections.into_values());
         }
 
         let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
@@ -2686,7 +2689,7 @@ pub struct LayoutState {
     blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
     fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
-    selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)>,
+    selections: Vec<(SelectionStyle, Vec<SelectionLayout>)>,
     scrollbar_row_range: Range<f32>,
     show_scrollbars: bool,
     is_singleton: bool,

crates/editor/src/items.rs 🔗

@@ -17,7 +17,7 @@ use language::{
     SelectionGoal,
 };
 use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
-use rpc::proto::{self, update_view};
+use rpc::proto::{self, update_view, PeerId};
 use smallvec::SmallVec;
 use std::{
     borrow::Cow,
@@ -156,13 +156,9 @@ impl FollowableItem for 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() {
+    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
+        self.leader_peer_id = leader_peer_id;
+        if self.leader_peer_id.is_some() {
             self.buffer.update(cx, |buffer, cx| {
                 buffer.remove_active_selections(cx);
             });
@@ -309,6 +305,10 @@ impl FollowableItem for Editor {
             _ => false,
         }
     }
+
+    fn is_project_item(&self, _cx: &AppContext) -> bool {
+        true
+    }
 }
 
 async fn update_editor_from_message(

crates/feedback/Cargo.toml 🔗

@@ -33,7 +33,7 @@ lazy_static.workspace = true
 postage.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
-sysinfo = "0.27.1"
+sysinfo.workspace = true
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
 urlencoding = "2.1.2"
 

crates/fs/Cargo.toml 🔗

@@ -9,8 +9,6 @@ path = "src/fs.rs"
 
 [dependencies]
 collections = { path = "../collections" }
-gpui = { path = "../gpui" }
-lsp = { path = "../lsp" }
 rope = { path = "../rope" }
 text = { path = "../text" }
 util = { path = "../util" }
@@ -34,8 +32,10 @@ log.workspace = true
 libc = "0.2"
 time.workspace = true
 
+gpui = { path = "../gpui", optional = true}
+
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
 
 [features]
-test-support = []
+test-support = ["gpui/test-support"]

crates/fs/src/fs.rs 🔗

@@ -93,33 +93,6 @@ pub struct Metadata {
     pub is_dir: bool,
 }
 
-impl From<lsp::CreateFileOptions> for CreateOptions {
-    fn from(options: lsp::CreateFileOptions) -> Self {
-        Self {
-            overwrite: options.overwrite.unwrap_or(false),
-            ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
-        }
-    }
-}
-
-impl From<lsp::RenameFileOptions> for RenameOptions {
-    fn from(options: lsp::RenameFileOptions) -> Self {
-        Self {
-            overwrite: options.overwrite.unwrap_or(false),
-            ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
-        }
-    }
-}
-
-impl From<lsp::DeleteFileOptions> for RemoveOptions {
-    fn from(options: lsp::DeleteFileOptions) -> Self {
-        Self {
-            recursive: options.recursive.unwrap_or(false),
-            ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false),
-        }
-    }
-}
-
 pub struct RealFs;
 
 #[async_trait::async_trait]

crates/gpui/Cargo.toml 🔗

@@ -11,7 +11,7 @@ path = "src/gpui.rs"
 doctest = false
 
 [features]
-test-support = ["backtrace", "dhat", "env_logger", "collections/test-support"]
+test-support = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"]
 
 [dependencies]
 collections = { path = "../collections" }

crates/gpui/src/platform/test.rs 🔗

@@ -103,6 +103,7 @@ pub struct Platform {
     current_clipboard_item: Mutex<Option<ClipboardItem>>,
     cursor: Mutex<CursorStyle>,
     active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
+    active_screen: Screen,
 }
 
 impl Platform {
@@ -113,6 +114,7 @@ impl Platform {
             current_clipboard_item: Default::default(),
             cursor: Mutex::new(CursorStyle::Arrow),
             active_window: Default::default(),
+            active_screen: Screen::new(),
         }
     }
 }
@@ -136,12 +138,16 @@ impl super::Platform for Platform {
 
     fn quit(&self) {}
 
-    fn screen_by_id(&self, _id: uuid::Uuid) -> Option<Rc<dyn crate::platform::Screen>> {
-        None
+    fn screen_by_id(&self, uuid: uuid::Uuid) -> Option<Rc<dyn crate::platform::Screen>> {
+        if self.active_screen.uuid == uuid {
+            Some(Rc::new(self.active_screen.clone()))
+        } else {
+            None
+        }
     }
 
     fn screens(&self) -> Vec<Rc<dyn crate::platform::Screen>> {
-        Default::default()
+        vec![Rc::new(self.active_screen.clone())]
     }
 
     fn open_window(
@@ -158,6 +164,7 @@ impl super::Platform for Platform {
                 WindowBounds::Fixed(rect) => rect.size(),
             },
             self.active_window.clone(),
+            Rc::new(self.active_screen.clone()),
         ))
     }
 
@@ -170,6 +177,7 @@ impl super::Platform for Platform {
             handle,
             vec2f(24., 24.),
             self.active_window.clone(),
+            Rc::new(self.active_screen.clone()),
         ))
     }
 
@@ -238,8 +246,18 @@ impl super::Platform for Platform {
     fn restart(&self) {}
 }
 
-#[derive(Debug)]
-pub struct Screen;
+#[derive(Debug, Clone)]
+pub struct Screen {
+    uuid: uuid::Uuid,
+}
+
+impl Screen {
+    fn new() -> Self {
+        Self {
+            uuid: uuid::Uuid::new_v4(),
+        }
+    }
+}
 
 impl super::Screen for Screen {
     fn as_any(&self) -> &dyn Any {
@@ -255,7 +273,7 @@ impl super::Screen for Screen {
     }
 
     fn display_uuid(&self) -> Option<uuid::Uuid> {
-        Some(uuid::Uuid::new_v4())
+        Some(self.uuid)
     }
 }
 
@@ -275,6 +293,7 @@ pub struct Window {
     pub(crate) edited: bool,
     pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
     active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
+    screen: Rc<Screen>,
 }
 
 impl Window {
@@ -282,6 +301,7 @@ impl Window {
         handle: AnyWindowHandle,
         size: Vector2F,
         active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
+        screen: Rc<Screen>,
     ) -> Self {
         Self {
             handle,
@@ -299,6 +319,7 @@ impl Window {
             edited: false,
             pending_prompts: Default::default(),
             active_window,
+            screen,
         }
     }
 
@@ -329,7 +350,7 @@ impl super::Window for Window {
     }
 
     fn screen(&self) -> Rc<dyn crate::platform::Screen> {
-        Rc::new(Screen)
+        self.screen.clone()
     }
 
     fn mouse_position(&self) -> Vector2F {

crates/project/src/project.rs 🔗

@@ -11,7 +11,7 @@ mod project_tests;
 mod worktree_tests;
 
 use anyhow::{anyhow, Context, Result};
-use client::{proto, Client, TypedEnvelope, UserId, UserStore};
+use client::{proto, Client, Collaborator, TypedEnvelope, UserStore};
 use clock::ReplicaId;
 use collections::{hash_map, BTreeMap, HashMap, HashSet};
 use copilot::Copilot;
@@ -253,13 +253,6 @@ enum ProjectClientState {
     },
 }
 
-#[derive(Clone, Debug)]
-pub struct Collaborator {
-    pub peer_id: proto::PeerId,
-    pub replica_id: ReplicaId,
-    pub user_id: UserId,
-}
-
 #[derive(Clone, Debug, PartialEq)]
 pub enum Event {
     LanguageServerAdded(LanguageServerId),
@@ -982,6 +975,10 @@ impl Project {
         &self.collaborators
     }
 
+    pub fn host(&self) -> Option<&Collaborator> {
+        self.collaborators.values().find(|c| c.replica_id == 0)
+    }
+
     /// Collect all worktrees, including ones that don't appear in the project panel
     pub fn worktrees<'a>(
         &'a self,
@@ -2231,26 +2228,62 @@ impl Project {
                         .get_mut(&buffer.remote_id())
                         .and_then(|m| m.get_mut(&language_server.server_id()))?;
                     let previous_snapshot = buffer_snapshots.last()?;
-                    let next_version = previous_snapshot.version + 1;
 
-                    let content_changes = buffer
-                        .edits_since::<(PointUtf16, usize)>(previous_snapshot.snapshot.version())
-                        .map(|edit| {
-                            let edit_start = edit.new.start.0;
-                            let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
-                            let new_text = next_snapshot
-                                .text_for_range(edit.new.start.1..edit.new.end.1)
-                                .collect();
-                            lsp::TextDocumentContentChangeEvent {
-                                range: Some(lsp::Range::new(
-                                    point_to_lsp(edit_start),
-                                    point_to_lsp(edit_end),
-                                )),
+                    let build_incremental_change = || {
+                        buffer
+                            .edits_since::<(PointUtf16, usize)>(
+                                previous_snapshot.snapshot.version(),
+                            )
+                            .map(|edit| {
+                                let edit_start = edit.new.start.0;
+                                let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0);
+                                let new_text = next_snapshot
+                                    .text_for_range(edit.new.start.1..edit.new.end.1)
+                                    .collect();
+                                lsp::TextDocumentContentChangeEvent {
+                                    range: Some(lsp::Range::new(
+                                        point_to_lsp(edit_start),
+                                        point_to_lsp(edit_end),
+                                    )),
+                                    range_length: None,
+                                    text: new_text,
+                                }
+                            })
+                            .collect()
+                    };
+
+                    let document_sync_kind = language_server
+                        .capabilities()
+                        .text_document_sync
+                        .as_ref()
+                        .and_then(|sync| match sync {
+                            lsp::TextDocumentSyncCapability::Kind(kind) => Some(*kind),
+                            lsp::TextDocumentSyncCapability::Options(options) => options.change,
+                        });
+
+                    let content_changes: Vec<_> = match document_sync_kind {
+                        Some(lsp::TextDocumentSyncKind::FULL) => {
+                            vec![lsp::TextDocumentContentChangeEvent {
+                                range: None,
                                 range_length: None,
-                                text: new_text,
+                                text: next_snapshot.text(),
+                            }]
+                        }
+                        Some(lsp::TextDocumentSyncKind::INCREMENTAL) => build_incremental_change(),
+                        _ => {
+                            #[cfg(any(test, feature = "test-support"))]
+                            {
+                                build_incremental_change()
                             }
-                        })
-                        .collect();
+
+                            #[cfg(not(any(test, feature = "test-support")))]
+                            {
+                                continue;
+                            }
+                        }
+                    };
+
+                    let next_version = previous_snapshot.version + 1;
 
                     buffer_snapshots.push(LspBufferSnapshot {
                         version: next_version,
@@ -4928,8 +4961,16 @@ impl Project {
                     if abs_path.ends_with("/") {
                         fs.create_dir(&abs_path).await?;
                     } else {
-                        fs.create_file(&abs_path, op.options.map(Into::into).unwrap_or_default())
-                            .await?;
+                        fs.create_file(
+                            &abs_path,
+                            op.options
+                                .map(|options| fs::CreateOptions {
+                                    overwrite: options.overwrite.unwrap_or(false),
+                                    ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
+                                })
+                                .unwrap_or_default(),
+                        )
+                        .await?;
                     }
                 }
 
@@ -4945,7 +4986,12 @@ impl Project {
                     fs.rename(
                         &source_abs_path,
                         &target_abs_path,
-                        op.options.map(Into::into).unwrap_or_default(),
+                        op.options
+                            .map(|options| fs::RenameOptions {
+                                overwrite: options.overwrite.unwrap_or(false),
+                                ignore_if_exists: options.ignore_if_exists.unwrap_or(false),
+                            })
+                            .unwrap_or_default(),
                     )
                     .await?;
                 }
@@ -4955,7 +5001,13 @@ impl Project {
                         .uri
                         .to_file_path()
                         .map_err(|_| anyhow!("can't convert URI to path"))?;
-                    let options = op.options.map(Into::into).unwrap_or_default();
+                    let options = op
+                        .options
+                        .map(|options| fs::RemoveOptions {
+                            recursive: options.recursive.unwrap_or(false),
+                            ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false),
+                        })
+                        .unwrap_or_default();
                     if abs_path.ends_with("/") {
                         fs.remove_dir(&abs_path, options).await?;
                     } else {
@@ -8216,16 +8268,6 @@ impl Entity for Project {
     }
 }
 
-impl Collaborator {
-    fn from_proto(message: proto::Collaborator) -> Result<Self> {
-        Ok(Self {
-            peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
-            replica_id: message.replica_id as ReplicaId,
-            user_id: message.user_id as UserId,
-        })
-    }
-}
-
 impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
     fn from((worktree_id, path): (WorktreeId, P)) -> Self {
         Self {

crates/project/src/terminals.rs 🔗

@@ -84,6 +84,7 @@ impl Project {
             terminal_settings::ActivateScript::Default => "activate",
             terminal_settings::ActivateScript::Csh => "activate.csh",
             terminal_settings::ActivateScript::Fish => "activate.fish",
+            terminal_settings::ActivateScript::Nushell => "activate.nu",
         };
 
         for virtual_environment_name in settings.directories {

crates/rpc/proto/zed.proto 🔗

@@ -23,154 +23,152 @@ message Envelope {
         CreateRoomResponse create_room_response = 10;
         JoinRoom join_room = 11;
         JoinRoomResponse join_room_response = 12;
-        RejoinRoom rejoin_room = 108;
-        RejoinRoomResponse rejoin_room_response = 109;
-        LeaveRoom leave_room = 13;
-        Call call = 14;
-        IncomingCall incoming_call = 15;
-        CallCanceled call_canceled = 16;
-        CancelCall cancel_call = 17;
-        DeclineCall decline_call = 18;
-        UpdateParticipantLocation update_participant_location = 19;
-        RoomUpdated room_updated = 20;
-
-        ShareProject share_project = 21;
-        ShareProjectResponse share_project_response = 22;
-        UnshareProject unshare_project = 23;
-        JoinProject join_project = 24;
-        JoinProjectResponse join_project_response = 25;
-        LeaveProject leave_project = 26;
-        AddProjectCollaborator add_project_collaborator = 27;
-        UpdateProjectCollaborator update_project_collaborator = 110;
-        RemoveProjectCollaborator remove_project_collaborator = 28;
-
-        GetDefinition get_definition = 29;
-        GetDefinitionResponse get_definition_response = 30;
-        GetTypeDefinition get_type_definition = 31;
-        GetTypeDefinitionResponse get_type_definition_response = 32;
-        GetReferences get_references = 33;
-        GetReferencesResponse get_references_response = 34;
-        GetDocumentHighlights get_document_highlights = 35;
-        GetDocumentHighlightsResponse get_document_highlights_response = 36;
-        GetProjectSymbols get_project_symbols = 37;
-        GetProjectSymbolsResponse get_project_symbols_response = 38;
-        OpenBufferForSymbol open_buffer_for_symbol = 39;
-        OpenBufferForSymbolResponse open_buffer_for_symbol_response = 40;
-
-        UpdateProject update_project = 41;
-        UpdateWorktree update_worktree = 43;
-
-        CreateProjectEntry create_project_entry = 45;
-        RenameProjectEntry rename_project_entry = 46;
-        CopyProjectEntry copy_project_entry = 47;
-        DeleteProjectEntry delete_project_entry = 48;
-        ProjectEntryResponse project_entry_response = 49;
-        ExpandProjectEntry expand_project_entry = 114;
-        ExpandProjectEntryResponse expand_project_entry_response = 115;
-
-        UpdateDiagnosticSummary update_diagnostic_summary = 50;
-        StartLanguageServer start_language_server = 51;
-        UpdateLanguageServer update_language_server = 52;
-
-        OpenBufferById open_buffer_by_id = 53;
-        OpenBufferByPath open_buffer_by_path = 54;
-        OpenBufferResponse open_buffer_response = 55;
-        CreateBufferForPeer create_buffer_for_peer = 56;
-        UpdateBuffer update_buffer = 57;
-        UpdateBufferFile update_buffer_file = 58;
-        SaveBuffer save_buffer = 59;
-        BufferSaved buffer_saved = 60;
-        BufferReloaded buffer_reloaded = 61;
-        ReloadBuffers reload_buffers = 62;
-        ReloadBuffersResponse reload_buffers_response = 63;
-        SynchronizeBuffers synchronize_buffers = 200;
-        SynchronizeBuffersResponse synchronize_buffers_response = 201;
-        FormatBuffers format_buffers = 64;
-        FormatBuffersResponse format_buffers_response = 65;
-        GetCompletions get_completions = 66;
-        GetCompletionsResponse get_completions_response = 67;
-        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 68;
-        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 69;
-        GetCodeActions get_code_actions = 70;
-        GetCodeActionsResponse get_code_actions_response = 71;
-        GetHover get_hover = 72;
-        GetHoverResponse get_hover_response = 73;
-        ApplyCodeAction apply_code_action = 74;
-        ApplyCodeActionResponse apply_code_action_response = 75;
-        PrepareRename prepare_rename = 76;
-        PrepareRenameResponse prepare_rename_response = 77;
-        PerformRename perform_rename = 78;
-        PerformRenameResponse perform_rename_response = 79;
-        SearchProject search_project = 80;
-        SearchProjectResponse search_project_response = 81;
-
-        UpdateContacts update_contacts = 92;
-        UpdateInviteInfo update_invite_info = 93;
-        ShowContacts show_contacts = 94;
-
-        GetUsers get_users = 95;
-        FuzzySearchUsers fuzzy_search_users = 96;
-        UsersResponse users_response = 97;
-        RequestContact request_contact = 98;
-        RespondToContactRequest respond_to_contact_request = 99;
-        RemoveContact remove_contact = 100;
-
-        Follow follow = 101;
-        FollowResponse follow_response = 102;
-        UpdateFollowers update_followers = 103;
-        Unfollow unfollow = 104;
-        GetPrivateUserInfo get_private_user_info = 105;
-        GetPrivateUserInfoResponse get_private_user_info_response = 106;
-        UpdateDiffBase update_diff_base = 107;
-
-        OnTypeFormatting on_type_formatting = 111;
-        OnTypeFormattingResponse on_type_formatting_response = 112;
-
-        UpdateWorktreeSettings update_worktree_settings = 113;
-
-        InlayHints inlay_hints = 116;
-        InlayHintsResponse inlay_hints_response = 117;
-        ResolveInlayHint resolve_inlay_hint = 137;
-        ResolveInlayHintResponse resolve_inlay_hint_response = 138;
-        RefreshInlayHints refresh_inlay_hints = 118;
-
-        CreateChannel create_channel = 119;
-        CreateChannelResponse create_channel_response = 120;
-        InviteChannelMember invite_channel_member = 121;
-        RemoveChannelMember remove_channel_member = 122;
-        RespondToChannelInvite respond_to_channel_invite = 123;
-        UpdateChannels update_channels = 124;
-        JoinChannel join_channel = 125;
-        DeleteChannel delete_channel = 126;
-        GetChannelMembers get_channel_members = 127;
-        GetChannelMembersResponse get_channel_members_response = 128;
-        SetChannelMemberAdmin set_channel_member_admin = 129;
-        RenameChannel rename_channel = 130;
-        RenameChannelResponse rename_channel_response = 154;
-
-        JoinChannelBuffer join_channel_buffer = 131;
-        JoinChannelBufferResponse join_channel_buffer_response = 132;
-        UpdateChannelBuffer update_channel_buffer = 133;
-        LeaveChannelBuffer leave_channel_buffer = 134;
-        AddChannelBufferCollaborator add_channel_buffer_collaborator = 135;
-        RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136;
-        UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139;
-        RejoinChannelBuffers rejoin_channel_buffers = 140;
-        RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141;
-
-        JoinChannelChat join_channel_chat = 142;
-        JoinChannelChatResponse join_channel_chat_response = 143;
-        LeaveChannelChat leave_channel_chat = 144;
-        SendChannelMessage send_channel_message = 145;
-        SendChannelMessageResponse send_channel_message_response = 146;
-        ChannelMessageSent channel_message_sent = 147;
-        GetChannelMessages get_channel_messages = 148;
-        GetChannelMessagesResponse get_channel_messages_response = 149;
-        RemoveChannelMessage remove_channel_message = 150;
-
-        LinkChannel link_channel = 151;
-        UnlinkChannel unlink_channel = 152;
-        MoveChannel move_channel = 153; // Current max: 154
+        RejoinRoom rejoin_room = 13;
+        RejoinRoomResponse rejoin_room_response = 14;
+        LeaveRoom leave_room = 15;
+        Call call = 16;
+        IncomingCall incoming_call = 17;
+        CallCanceled call_canceled = 18;
+        CancelCall cancel_call = 19;
+        DeclineCall decline_call = 20;
+        UpdateParticipantLocation update_participant_location = 21;
+        RoomUpdated room_updated = 22;
+
+        ShareProject share_project = 23;
+        ShareProjectResponse share_project_response = 24;
+        UnshareProject unshare_project = 25;
+        JoinProject join_project = 26;
+        JoinProjectResponse join_project_response = 27;
+        LeaveProject leave_project = 28;
+        AddProjectCollaborator add_project_collaborator = 29;
+        UpdateProjectCollaborator update_project_collaborator = 30;
+        RemoveProjectCollaborator remove_project_collaborator = 31;
+
+        GetDefinition get_definition = 32;
+        GetDefinitionResponse get_definition_response = 33;
+        GetTypeDefinition get_type_definition = 34;
+        GetTypeDefinitionResponse get_type_definition_response = 35;
+        GetReferences get_references = 36;
+        GetReferencesResponse get_references_response = 37;
+        GetDocumentHighlights get_document_highlights = 38;
+        GetDocumentHighlightsResponse get_document_highlights_response = 39;
+        GetProjectSymbols get_project_symbols = 40;
+        GetProjectSymbolsResponse get_project_symbols_response = 41;
+        OpenBufferForSymbol open_buffer_for_symbol = 42;
+        OpenBufferForSymbolResponse open_buffer_for_symbol_response = 43;
+
+        UpdateProject update_project = 44;
+        UpdateWorktree update_worktree = 45;
+
+        CreateProjectEntry create_project_entry = 46;
+        RenameProjectEntry rename_project_entry = 47;
+        CopyProjectEntry copy_project_entry = 48;
+        DeleteProjectEntry delete_project_entry = 49;
+        ProjectEntryResponse project_entry_response = 50;
+        ExpandProjectEntry expand_project_entry = 51;
+        ExpandProjectEntryResponse expand_project_entry_response = 52;
+
+        UpdateDiagnosticSummary update_diagnostic_summary = 53;
+        StartLanguageServer start_language_server = 54;
+        UpdateLanguageServer update_language_server = 55;
+
+        OpenBufferById open_buffer_by_id = 56;
+        OpenBufferByPath open_buffer_by_path = 57;
+        OpenBufferResponse open_buffer_response = 58;
+        CreateBufferForPeer create_buffer_for_peer = 59;
+        UpdateBuffer update_buffer = 60;
+        UpdateBufferFile update_buffer_file = 61;
+        SaveBuffer save_buffer = 62;
+        BufferSaved buffer_saved = 63;
+        BufferReloaded buffer_reloaded = 64;
+        ReloadBuffers reload_buffers = 65;
+        ReloadBuffersResponse reload_buffers_response = 66;
+        SynchronizeBuffers synchronize_buffers = 67;
+        SynchronizeBuffersResponse synchronize_buffers_response = 68;
+        FormatBuffers format_buffers = 69;
+        FormatBuffersResponse format_buffers_response = 70;
+        GetCompletions get_completions = 71;
+        GetCompletionsResponse get_completions_response = 72;
+        ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73;
+        ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74;
+        GetCodeActions get_code_actions = 75;
+        GetCodeActionsResponse get_code_actions_response = 76;
+        GetHover get_hover = 77;
+        GetHoverResponse get_hover_response = 78;
+        ApplyCodeAction apply_code_action = 79;
+        ApplyCodeActionResponse apply_code_action_response = 80;
+        PrepareRename prepare_rename = 81;
+        PrepareRenameResponse prepare_rename_response = 82;
+        PerformRename perform_rename = 83;
+        PerformRenameResponse perform_rename_response = 84;
+        SearchProject search_project = 85;
+        SearchProjectResponse search_project_response = 86;
+
+        UpdateContacts update_contacts = 87;
+        UpdateInviteInfo update_invite_info = 88;
+        ShowContacts show_contacts = 89;
+
+        GetUsers get_users = 90;
+        FuzzySearchUsers fuzzy_search_users = 91;
+        UsersResponse users_response = 92;
+        RequestContact request_contact = 93;
+        RespondToContactRequest respond_to_contact_request = 94;
+        RemoveContact remove_contact = 95;
+
+        Follow follow = 96;
+        FollowResponse follow_response = 97;
+        UpdateFollowers update_followers = 98;
+        Unfollow unfollow = 99;
+        GetPrivateUserInfo get_private_user_info = 100;
+        GetPrivateUserInfoResponse get_private_user_info_response = 101;
+        UpdateDiffBase update_diff_base = 102;
+
+        OnTypeFormatting on_type_formatting = 103;
+        OnTypeFormattingResponse on_type_formatting_response = 104;
+
+        UpdateWorktreeSettings update_worktree_settings = 105;
+
+        InlayHints inlay_hints = 106;
+        InlayHintsResponse inlay_hints_response = 107;
+        ResolveInlayHint resolve_inlay_hint = 108;
+        ResolveInlayHintResponse resolve_inlay_hint_response = 109;
+        RefreshInlayHints refresh_inlay_hints = 110;
+
+        CreateChannel create_channel = 111;
+        CreateChannelResponse create_channel_response = 112;
+        InviteChannelMember invite_channel_member = 113;
+        RemoveChannelMember remove_channel_member = 114;
+        RespondToChannelInvite respond_to_channel_invite = 115;
+        UpdateChannels update_channels = 116;
+        JoinChannel join_channel = 117;
+        DeleteChannel delete_channel = 118;
+        GetChannelMembers get_channel_members = 119;
+        GetChannelMembersResponse get_channel_members_response = 120;
+        SetChannelMemberAdmin set_channel_member_admin = 121;
+        RenameChannel rename_channel = 122;
+        RenameChannelResponse rename_channel_response = 123;
+
+        JoinChannelBuffer join_channel_buffer = 124;
+        JoinChannelBufferResponse join_channel_buffer_response = 125;
+        UpdateChannelBuffer update_channel_buffer = 126;
+        LeaveChannelBuffer leave_channel_buffer = 127;
+        UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128;
+        RejoinChannelBuffers rejoin_channel_buffers = 129;
+        RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130;
+
+        JoinChannelChat join_channel_chat = 131;
+        JoinChannelChatResponse join_channel_chat_response = 132;
+        LeaveChannelChat leave_channel_chat = 133;
+        SendChannelMessage send_channel_message = 134;
+        SendChannelMessageResponse send_channel_message_response = 135;
+        ChannelMessageSent channel_message_sent = 136;
+        GetChannelMessages get_channel_messages = 137;
+        GetChannelMessagesResponse get_channel_messages_response = 138;
+        RemoveChannelMessage remove_channel_message = 139;
+
+        LinkChannel link_channel = 140;
+        UnlinkChannel unlink_channel = 141;
+        MoveChannel move_channel = 142;
     }
 }
 
@@ -258,6 +256,7 @@ message Participant {
     PeerId peer_id = 2;
     repeated ParticipantProject projects = 3;
     ParticipantLocation location = 4;
+    uint32 participant_index = 5;
 }
 
 message PendingParticipant {
@@ -440,20 +439,9 @@ message RemoveProjectCollaborator {
     PeerId peer_id = 2;
 }
 
-message AddChannelBufferCollaborator {
+message UpdateChannelBufferCollaborators {
     uint64 channel_id = 1;
-    Collaborator collaborator = 2;
-}
-
-message RemoveChannelBufferCollaborator {
-    uint64 channel_id = 1;
-    PeerId peer_id = 2;
-}
-
-message UpdateChannelBufferCollaborator {
-    uint64 channel_id = 1;
-    PeerId old_peer_id = 2;
-    PeerId new_peer_id = 3;
+    repeated Collaborator collaborators = 2;
 }
 
 message GetDefinition {
@@ -1213,8 +1201,9 @@ message UpdateDiagnostics {
 }
 
 message Follow {
-    uint64 project_id = 1;
-    PeerId leader_id = 2;
+    uint64 room_id = 1;
+    optional uint64 project_id = 2;
+    PeerId leader_id = 3;
 }
 
 message FollowResponse {
@@ -1223,18 +1212,20 @@ message FollowResponse {
 }
 
 message UpdateFollowers {
-    uint64 project_id = 1;
-    repeated PeerId follower_ids = 2;
+    uint64 room_id = 1;
+    optional uint64 project_id = 2;
+    repeated PeerId follower_ids = 3;
     oneof variant {
-        UpdateActiveView update_active_view = 3;
-        View create_view = 4;
-        UpdateView update_view = 5;
+        UpdateActiveView update_active_view = 4;
+        View create_view = 5;
+        UpdateView update_view = 6;
     }
 }
 
 message Unfollow {
-    uint64 project_id = 1;
-    PeerId leader_id = 2;
+    uint64 room_id = 1;
+    optional uint64 project_id = 2;
+    PeerId leader_id = 3;
 }
 
 message GetPrivateUserInfo {}

crates/rpc/src/proto.rs 🔗

@@ -270,9 +270,7 @@ messages!(
     (JoinChannelBufferResponse, Foreground),
     (LeaveChannelBuffer, Background),
     (UpdateChannelBuffer, Foreground),
-    (RemoveChannelBufferCollaborator, Foreground),
-    (AddChannelBufferCollaborator, Foreground),
-    (UpdateChannelBufferCollaborator, Foreground),
+    (UpdateChannelBufferCollaborators, Foreground),
 );
 
 request_messages!(
@@ -364,7 +362,6 @@ entity_messages!(
     CreateProjectEntry,
     DeleteProjectEntry,
     ExpandProjectEntry,
-    Follow,
     FormatBuffers,
     GetCodeActions,
     GetCompletions,
@@ -392,12 +389,10 @@ entity_messages!(
     SearchProject,
     StartLanguageServer,
     SynchronizeBuffers,
-    Unfollow,
     UnshareProject,
     UpdateBuffer,
     UpdateBufferFile,
     UpdateDiagnosticSummary,
-    UpdateFollowers,
     UpdateLanguageServer,
     UpdateProject,
     UpdateProjectCollaborator,
@@ -410,10 +405,8 @@ entity_messages!(
     channel_id,
     ChannelMessageSent,
     UpdateChannelBuffer,
-    RemoveChannelBufferCollaborator,
     RemoveChannelMessage,
-    AddChannelBufferCollaborator,
-    UpdateChannelBufferCollaborator
+    UpdateChannelBufferCollaborators
 );
 
 const KIB: usize = 1024;

crates/rpc/src/rpc.rs 🔗

@@ -6,4 +6,4 @@ pub use conn::Connection;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 63;
+pub const PROTOCOL_VERSION: u32 = 64;

crates/semantic_index/src/parsing.rs 🔗

@@ -61,8 +61,9 @@ const CODE_CONTEXT_TEMPLATE: &str =
 const ENTIRE_FILE_TEMPLATE: &str =
     "The below snippet is from file '<path>'\n\n```<language>\n<item>\n```";
 const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file '<path>'\n\n<item>";
-pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] =
-    &["TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML"];
+pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = &[
+    "TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML", "Scheme",
+];
 
 pub struct CodeContextRetriever {
     pub parser: Parser,

crates/semantic_index/src/semantic_index_tests.rs 🔗

@@ -305,6 +305,11 @@ async fn test_code_context_retrieval_rust() {
                 todo!();
             }
         }
+
+        #[derive(Clone)]
+        struct D {
+            name: String
+        }
     "
     .unindent();
 
@@ -361,6 +366,15 @@ async fn test_code_context_retrieval_rust() {
                 .unindent(),
                 text.find("fn function_2").unwrap(),
             ),
+            (
+                "
+                #[derive(Clone)]
+                struct D {
+                    name: String
+                }"
+                .unindent(),
+                text.find("struct D").unwrap(),
+            ),
         ],
     );
 }
@@ -1422,6 +1436,9 @@ fn rust_lang() -> Arc<Language> {
                         name: (_) @name)
                 ] @item
             )
+
+            (attribute_item) @collapse
+            (use_declaration) @collapse
             "#,
         )
         .unwrap(),

crates/storybook/Cargo.toml 🔗

@@ -12,6 +12,8 @@ path = "src/storybook.rs"
 anyhow.workspace = true
 clap = { version = "4.4", features = ["derive", "string"] }
 chrono = "0.4"
+fs = { path = "../fs" }
+futures.workspace = true
 gpui2 = { path = "../gpui2" }
 itertools = "0.11.0"
 log.workspace = true

crates/storybook/src/stories/components/breadcrumb.rs 🔗

@@ -1,5 +1,8 @@
+use std::path::PathBuf;
+use std::str::FromStr;
+
 use ui::prelude::*;
-use ui::Breadcrumb;
+use ui::{Breadcrumb, HighlightedText, Symbol};
 
 use crate::story::Story;
 
@@ -8,9 +11,35 @@ pub struct BreadcrumbStory {}
 
 impl BreadcrumbStory {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
         Story::container(cx)
             .child(Story::title_for::<_, Breadcrumb>(cx))
             .child(Story::label(cx, "Default"))
-            .child(Breadcrumb::new())
+            .child(Breadcrumb::new(
+                PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
+                vec![
+                    Symbol(vec![
+                        HighlightedText {
+                            text: "impl ".to_string(),
+                            color: HighlightColor::Keyword.hsla(&theme),
+                        },
+                        HighlightedText {
+                            text: "BreadcrumbStory".to_string(),
+                            color: HighlightColor::Function.hsla(&theme),
+                        },
+                    ]),
+                    Symbol(vec![
+                        HighlightedText {
+                            text: "fn ".to_string(),
+                            color: HighlightColor::Keyword.hsla(&theme),
+                        },
+                        HighlightedText {
+                            text: "render".to_string(),
+                            color: HighlightColor::Function.hsla(&theme),
+                        },
+                    ]),
+                ],
+            ))
     }
 }

crates/storybook/src/stories/components/buffer.rs 🔗

@@ -12,8 +12,10 @@ pub struct BufferStory {}
 
 impl BufferStory {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
         Story::container(cx)
-            .child(Story::title_for::<_, Buffer<V>>(cx))
+            .child(Story::title_for::<_, Buffer>(cx))
             .child(Story::label(cx, "Default"))
             .child(div().w(rems(64.)).h_96().child(empty_buffer_example()))
             .child(Story::label(cx, "Hello World (Rust)"))
@@ -21,14 +23,14 @@ impl BufferStory {
                 div()
                     .w(rems(64.))
                     .h_96()
-                    .child(hello_world_rust_buffer_example(cx)),
+                    .child(hello_world_rust_buffer_example(&theme)),
             )
             .child(Story::label(cx, "Hello World (Rust) with Status"))
             .child(
                 div()
                     .w(rems(64.))
                     .h_96()
-                    .child(hello_world_rust_buffer_with_status_example(cx)),
+                    .child(hello_world_rust_buffer_with_status_example(&theme)),
             )
     }
 }

crates/storybook/src/stories/components/chat_panel.rs 🔗

@@ -1,6 +1,6 @@
 use chrono::DateTime;
 use ui::prelude::*;
-use ui::{ChatMessage, ChatPanel};
+use ui::{ChatMessage, ChatPanel, Panel};
 
 use crate::story::Story;
 
@@ -12,23 +12,35 @@ impl ChatPanelStory {
         Story::container(cx)
             .child(Story::title_for::<_, ChatPanel<V>>(cx))
             .child(Story::label(cx, "Default"))
-            .child(ChatPanel::new(ScrollState::default()))
+            .child(Panel::new(
+                ScrollState::default(),
+                |_, _| vec![ChatPanel::new(ScrollState::default()).into_any()],
+                Box::new(()),
+            ))
             .child(Story::label(cx, "With Mesages"))
-            .child(ChatPanel::new(ScrollState::default()).with_messages(vec![
-                    ChatMessage::new(
-                        "osiewicz".to_string(),
-                        "is this thing on?".to_string(),
-                        DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
-                            .unwrap()
-                            .naive_local(),
-                    ),
-                    ChatMessage::new(
-                        "maxdeviant".to_string(),
-                        "Reading you loud and clear!".to_string(),
-                        DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
-                            .unwrap()
-                            .naive_local(),
-                    ),
-                ]))
+            .child(Panel::new(
+                ScrollState::default(),
+                |_, _| {
+                    vec![ChatPanel::new(ScrollState::default())
+                        .with_messages(vec![
+                            ChatMessage::new(
+                                "osiewicz".to_string(),
+                                "is this thing on?".to_string(),
+                                DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
+                                    .unwrap()
+                                    .naive_local(),
+                            ),
+                            ChatMessage::new(
+                                "maxdeviant".to_string(),
+                                "Reading you loud and clear!".to_string(),
+                                DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
+                                    .unwrap()
+                                    .naive_local(),
+                            ),
+                        ])
+                        .into_any()]
+                },
+                Box::new(()),
+            ))
     }
 }

crates/storybook/src/stories/components/facepile.rs 🔗

@@ -11,7 +11,7 @@ impl FacepileStory {
         let players = static_players();
 
         Story::container(cx)
-            .child(Story::title_for::<_, ui::Facepile>(cx))
+            .child(Story::title_for::<_, Facepile>(cx))
             .child(Story::label(cx, "Default"))
             .child(
                 div()

crates/storybook/src/stories/components/panel.rs 🔗

@@ -14,9 +14,10 @@ impl PanelStory {
             .child(Panel::new(
                 ScrollState::default(),
                 |_, _| {
-                    (0..100)
-                        .map(|ix| Label::new(format!("Item {}", ix + 1)).into_any())
-                        .collect()
+                    vec![div()
+                        .overflow_y_scroll(ScrollState::default())
+                        .children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1))))
+                        .into_any()]
                 },
                 Box::new(()),
             ))

crates/storybook/src/stories/components/project_panel.rs 🔗

@@ -1,5 +1,5 @@
 use ui::prelude::*;
-use ui::ProjectPanel;
+use ui::{Panel, ProjectPanel};
 
 use crate::story::Story;
 
@@ -11,6 +11,10 @@ impl ProjectPanelStory {
         Story::container(cx)
             .child(Story::title_for::<_, ProjectPanel<V>>(cx))
             .child(Story::label(cx, "Default"))
-            .child(ProjectPanel::new(ScrollState::default()))
+            .child(Panel::new(
+                ScrollState::default(),
+                |_, _| vec![ProjectPanel::new(ScrollState::default()).into_any()],
+                Box::new(()),
+            ))
     }
 }

crates/storybook/src/stories/components/tab_bar.rs 🔗

@@ -1,5 +1,5 @@
 use ui::prelude::*;
-use ui::TabBar;
+use ui::{Tab, TabBar};
 
 use crate::story::Story;
 
@@ -11,6 +11,36 @@ impl TabBarStory {
         Story::container(cx)
             .child(Story::title_for::<_, TabBar<V>>(cx))
             .child(Story::label(cx, "Default"))
-            .child(TabBar::new(ScrollState::default()))
+            .child(TabBar::new(vec![
+                Tab::new()
+                    .title("Cargo.toml".to_string())
+                    .current(false)
+                    .git_status(GitStatus::Modified),
+                Tab::new()
+                    .title("Channels Panel".to_string())
+                    .current(false),
+                Tab::new()
+                    .title("channels_panel.rs".to_string())
+                    .current(true)
+                    .git_status(GitStatus::Modified),
+                Tab::new()
+                    .title("workspace.rs".to_string())
+                    .current(false)
+                    .git_status(GitStatus::Modified),
+                Tab::new()
+                    .title("icon_button.rs".to_string())
+                    .current(false),
+                Tab::new()
+                    .title("storybook.rs".to_string())
+                    .current(false)
+                    .git_status(GitStatus::Created),
+                Tab::new().title("theme.rs".to_string()).current(false),
+                Tab::new()
+                    .title("theme_registry.rs".to_string())
+                    .current(false),
+                Tab::new()
+                    .title("styleable_helpers.rs".to_string())
+                    .current(false),
+            ]))
     }
 }

crates/storybook/src/stories/components/toolbar.rs 🔗

@@ -1,5 +1,9 @@
+use std::path::PathBuf;
+use std::str::FromStr;
+use std::sync::Arc;
+
 use ui::prelude::*;
-use ui::Toolbar;
+use ui::{theme, Breadcrumb, HighlightColor, HighlightedText, Icon, IconButton, Symbol, Toolbar};
 
 use crate::story::Story;
 
@@ -8,9 +12,59 @@ pub struct ToolbarStory {}
 
 impl ToolbarStory {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        struct LeftItemsPayload {
+            pub theme: Arc<Theme>,
+        }
+
         Story::container(cx)
-            .child(Story::title_for::<_, Toolbar>(cx))
+            .child(Story::title_for::<_, Toolbar<V>>(cx))
             .child(Story::label(cx, "Default"))
-            .child(Toolbar::new())
+            .child(Toolbar::new(
+                |_, payload| {
+                    let payload = payload.downcast_ref::<LeftItemsPayload>().unwrap();
+
+                    let theme = payload.theme.clone();
+
+                    vec![Breadcrumb::new(
+                        PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
+                        vec![
+                            Symbol(vec![
+                                HighlightedText {
+                                    text: "impl ".to_string(),
+                                    color: HighlightColor::Keyword.hsla(&theme),
+                                },
+                                HighlightedText {
+                                    text: "ToolbarStory".to_string(),
+                                    color: HighlightColor::Function.hsla(&theme),
+                                },
+                            ]),
+                            Symbol(vec![
+                                HighlightedText {
+                                    text: "fn ".to_string(),
+                                    color: HighlightColor::Keyword.hsla(&theme),
+                                },
+                                HighlightedText {
+                                    text: "render".to_string(),
+                                    color: HighlightColor::Function.hsla(&theme),
+                                },
+                            ]),
+                        ],
+                    )
+                    .into_any()]
+                },
+                Box::new(LeftItemsPayload {
+                    theme: theme.clone(),
+                }),
+                |_, _| {
+                    vec![
+                        IconButton::new(Icon::InlayHint).into_any(),
+                        IconButton::new(Icon::MagnifyingGlass).into_any(),
+                        IconButton::new(Icon::MagicWand).into_any(),
+                    ]
+                },
+                Box::new(()),
+            ))
     }
 }

crates/storybook/src/stories/elements/avatar.rs 🔗

@@ -9,7 +9,7 @@ pub struct AvatarStory {}
 impl AvatarStory {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         Story::container(cx)
-            .child(Story::title_for::<_, ui::Avatar>(cx))
+            .child(Story::title_for::<_, Avatar>(cx))
             .child(Story::label(cx, "Default"))
             .child(Avatar::new(
                 "https://avatars.githubusercontent.com/u/1714999?v=4",

crates/storybook/src/stories/elements/icon.rs 🔗

@@ -12,7 +12,7 @@ impl IconStory {
         let icons = Icon::iter();
 
         Story::container(cx)
-            .child(Story::title_for::<_, ui::IconElement>(cx))
+            .child(Story::title_for::<_, IconElement>(cx))
             .child(Story::label(cx, "All Icons"))
             .child(div().flex().gap_3().children(icons.map(IconElement::new)))
     }

crates/storybook/src/stories/kitchen_sink.rs 🔗

@@ -19,5 +19,8 @@ impl KitchenSinkStory {
             .child(div().flex().flex_col().children_any(element_stories))
             .child(Story::label(cx, "Components"))
             .child(div().flex().flex_col().children_any(component_stories))
+            // Add a bit of space at the bottom of the kitchen sink so elements
+            // don't end up squished right up against the bottom of the screen.
+            .child(div().p_4())
     }
 }

crates/storybook/src/storybook.rs 🔗

@@ -4,7 +4,7 @@ mod stories;
 mod story;
 mod story_selector;
 
-use std::sync::Arc;
+use std::{process::Command, sync::Arc};
 
 use ::theme as legacy_theme;
 use clap::Parser;
@@ -38,11 +38,44 @@ struct Args {
     theme: Option<String>,
 }
 
+async fn watch_zed_changes(fs: Arc<dyn fs::Fs>) -> Option<()> {
+    if std::env::var("ZED_HOT_RELOAD").is_err() {
+        return None;
+    }
+    use futures::StreamExt;
+    let mut events = fs
+        .watch(".".as_ref(), std::time::Duration::from_millis(100))
+        .await;
+    let mut current_child: Option<std::process::Child> = None;
+    while let Some(events) = events.next().await {
+        if !events.iter().any(|event| {
+            event
+                .path
+                .to_str()
+                .map(|path| path.contains("/crates/"))
+                .unwrap_or_default()
+        }) {
+            continue;
+        }
+        let child = current_child.take().map(|mut child| child.kill());
+        log::info!("Storybook changed, rebuilding...");
+        current_child = Some(
+            Command::new("cargo")
+                .args(["run", "-p", "storybook"])
+                .spawn()
+                .ok()?,
+        );
+    }
+    Some(())
+}
+
 fn main() {
     SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 
     let args = Args::parse();
 
+    let fs = Arc::new(fs::RealFs);
+
     gpui2::App::new(Assets).unwrap().run(move |cx| {
         let mut store = SettingsStore::default();
         store
@@ -63,6 +96,10 @@ fn main() {
             })
             .and_then(|theme_name| theme_registry.get(&theme_name).ok());
 
+        cx.spawn(|_| async move {
+            watch_zed_changes(fs).await;
+        })
+        .detach();
         cx.add_window(
             gpui2::WindowOptions {
                 bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1700., 980.))),

crates/theme/src/theme.rs 🔗

@@ -131,6 +131,7 @@ pub struct Titlebar {
     pub menu: TitlebarMenu,
     pub project_menu_button: Toggleable<Interactive<ContainedText>>,
     pub git_menu_button: Toggleable<Interactive<ContainedText>>,
+    pub project_host: Interactive<ContainedText>,
     pub item_spacing: f32,
     pub face_pile_spacing: f32,
     pub avatar_ribbon: AvatarRibbon,
@@ -1065,13 +1066,12 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
 }
 
 impl Editor {
-    pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle {
-        let style_ix = replica_id as usize % (self.guest_selections.len() + 1);
-        if style_ix == 0 {
-            &self.selection
-        } else {
-            &self.guest_selections[style_ix - 1]
+    pub fn selection_style_for_room_participant(&self, participant_index: u32) -> SelectionStyle {
+        if self.guest_selections.is_empty() {
+            return SelectionStyle::default();
         }
+        let style_ix = participant_index as usize % self.guest_selections.len();
+        self.guest_selections[style_ix]
     }
 }
 

crates/ui/Cargo.toml 🔗

@@ -13,3 +13,4 @@ settings = { path = "../settings" }
 smallvec.workspace = true
 strum = { version = "0.25.0", features = ["derive"] }
 theme = { path = "../theme" }
+rand = "0.8"

crates/ui/src/components.rs 🔗

@@ -5,7 +5,7 @@ mod chat_panel;
 mod collab_panel;
 mod command_palette;
 mod context_menu;
-mod editor;
+mod editor_pane;
 mod facepile;
 mod icon_button;
 mod keybinding;
@@ -31,7 +31,7 @@ pub use chat_panel::*;
 pub use collab_panel::*;
 pub use command_palette::*;
 pub use context_menu::*;
-pub use editor::*;
+pub use editor_pane::*;
 pub use facepile::*;
 pub use icon_button::*;
 pub use keybinding::*;

crates/ui/src/components/breadcrumb.rs 🔗

@@ -1,17 +1,35 @@
-use crate::prelude::*;
+use std::path::PathBuf;
+
+use gpui2::elements::div::Div;
+
 use crate::{h_stack, theme};
+use crate::{prelude::*, HighlightedText};
+
+#[derive(Clone)]
+pub struct Symbol(pub Vec<HighlightedText>);
 
 #[derive(Element)]
-pub struct Breadcrumb {}
+pub struct Breadcrumb {
+    path: PathBuf,
+    symbols: Vec<Symbol>,
+}
 
 impl Breadcrumb {
-    pub fn new() -> Self {
-        Self {}
+    pub fn new(path: PathBuf, symbols: Vec<Symbol>) -> Self {
+        Self { path, symbols }
+    }
+
+    fn render_separator<V: 'static>(&self, theme: &Theme) -> Div<V> {
+        div()
+            .child(" › ")
+            .text_color(HighlightColor::Default.hsla(theme))
     }
 
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
+        let symbols_len = self.symbols.len();
+
         h_stack()
             .px_1()
             // TODO: Read font from theme (or settings?).
@@ -21,11 +39,33 @@ impl Breadcrumb {
             .rounded_md()
             .hover()
             .fill(theme.highest.base.hovered.background)
-            // TODO: Replace hardcoded breadcrumbs.
-            .child("crates/ui/src/components/toolbar.rs")
-            .child(" › ")
-            .child("impl Breadcrumb")
-            .child(" › ")
-            .child("fn render")
+            .child(self.path.clone().to_str().unwrap().to_string())
+            .child(if !self.symbols.is_empty() {
+                self.render_separator(&theme)
+            } else {
+                div()
+            })
+            .child(
+                div().flex().children(
+                    self.symbols
+                        .iter()
+                        .enumerate()
+                        // TODO: Could use something like `intersperse` here instead.
+                        .flat_map(|(ix, symbol)| {
+                            let mut items =
+                                vec![div().flex().children(symbol.0.iter().map(|segment| {
+                                    div().child(segment.text.clone()).text_color(segment.color)
+                                }))];
+
+                            let is_last_segment = ix == symbols_len - 1;
+                            if !is_last_segment {
+                                items.push(self.render_separator(&theme));
+                            }
+
+                            items
+                        })
+                        .collect::<Vec<_>>(),
+                ),
+            )
     }
 }

crates/ui/src/components/buffer.rs 🔗

@@ -1,5 +1,3 @@
-use std::marker::PhantomData;
-
 use gpui2::{Hsla, WindowContext};
 
 use crate::prelude::*;
@@ -33,6 +31,7 @@ pub struct BufferRow {
     pub show_line_number: bool,
 }
 
+#[derive(Clone)]
 pub struct BufferRows {
     pub show_line_numbers: bool,
     pub rows: Vec<BufferRow>,
@@ -108,9 +107,8 @@ impl BufferRow {
     }
 }
 
-#[derive(Element)]
-pub struct Buffer<V: 'static> {
-    view_type: PhantomData<V>,
+#[derive(Element, Clone)]
+pub struct Buffer {
     scroll_state: ScrollState,
     rows: Option<BufferRows>,
     readonly: bool,
@@ -119,10 +117,9 @@ pub struct Buffer<V: 'static> {
     path: Option<String>,
 }
 
-impl<V: 'static> Buffer<V> {
+impl Buffer {
     pub fn new() -> Self {
         Self {
-            view_type: PhantomData,
             scroll_state: ScrollState::default(),
             rows: Some(BufferRows::default()),
             readonly: false,
@@ -161,7 +158,7 @@ impl<V: 'static> Buffer<V> {
         self
     }
 
-    fn render_row(row: BufferRow, cx: &WindowContext) -> impl IntoElement<V> {
+    fn render_row<V: 'static>(row: BufferRow, cx: &WindowContext) -> impl IntoElement<V> {
         let theme = theme(cx);
         let system_color = SystemColor::new();
 
@@ -172,28 +169,35 @@ impl<V: 'static> Buffer<V> {
         };
 
         let line_number_color = if row.current {
-            HighlightColor::Default.hsla(cx)
+            HighlightColor::Default.hsla(&theme)
         } else {
-            HighlightColor::Comment.hsla(cx)
+            HighlightColor::Comment.hsla(&theme)
         };
 
         h_stack()
             .fill(line_background)
+            .w_full()
             .gap_2()
-            .px_2()
-            .child(h_stack().w_4().h_full().px_1().when(row.code_action, |c| {
-                div().child(IconElement::new(Icon::Bolt))
-            }))
+            .px_1()
+            .child(
+                h_stack()
+                    .w_4()
+                    .h_full()
+                    .px_0p5()
+                    .when(row.code_action, |c| {
+                        div().child(IconElement::new(Icon::Bolt))
+                    }),
+            )
             .when(row.show_line_number, |this| {
                 this.child(
-                    h_stack().justify_end().px_1().w_4().child(
+                    h_stack().justify_end().px_0p5().w_3().child(
                         div()
                             .text_color(line_number_color)
                             .child(row.line_number.to_string()),
                     ),
                 )
             })
-            .child(div().mx_1().w_1().h_full().fill(row.status.hsla(cx)))
+            .child(div().mx_0p5().w_1().h_full().fill(row.status.hsla(cx)))
             .children(row.line.map(|line| {
                 div()
                     .flex()
@@ -205,7 +209,7 @@ impl<V: 'static> Buffer<V> {
             }))
     }
 
-    fn render_rows(&self, cx: &WindowContext) -> Vec<impl IntoElement<V>> {
+    fn render_rows<V: 'static>(&self, cx: &WindowContext) -> Vec<impl IntoElement<V>> {
         match &self.rows {
             Some(rows) => rows
                 .rows
@@ -216,7 +220,7 @@ impl<V: 'static> Buffer<V> {
         }
     }
 
-    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
         let rows = self.render_rows(cx);
         v_stack()

crates/ui/src/components/chat_panel.rs 🔗

@@ -4,13 +4,12 @@ use chrono::NaiveDateTime;
 
 use crate::prelude::*;
 use crate::theme::theme;
-use crate::{Icon, IconButton, Input, Label, LabelColor, Panel, PanelSide};
+use crate::{Icon, IconButton, Input, Label, LabelColor};
 
 #[derive(Element)]
 pub struct ChatPanel<V: 'static> {
     view_type: PhantomData<V>,
     scroll_state: ScrollState,
-    current_side: PanelSide,
     messages: Vec<ChatMessage>,
 }
 
@@ -19,16 +18,10 @@ impl<V: 'static> ChatPanel<V> {
         Self {
             view_type: PhantomData,
             scroll_state,
-            current_side: PanelSide::default(),
             messages: Vec::new(),
         }
     }
 
-    pub fn side(mut self, side: PanelSide) -> Self {
-        self.current_side = side;
-        self
-    }
-
     pub fn with_messages(mut self, messages: Vec<ChatMessage>) -> Self {
         self.messages = messages;
         self
@@ -37,38 +30,33 @@ impl<V: 'static> ChatPanel<V> {
     fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
-        struct PanelPayload {
-            pub scroll_state: ScrollState,
-            pub messages: Vec<ChatMessage>,
-        }
-
-        Panel::new(
-            self.scroll_state.clone(),
-            |_, payload| {
-                let payload = payload.downcast_ref::<PanelPayload>().unwrap();
-
-                vec![div()
+        div()
+            .flex()
+            .flex_col()
+            .justify_between()
+            .h_full()
+            .px_2()
+            .gap_2()
+            // Header
+            .child(
+                div()
                     .flex()
-                    .flex_col()
-                    .h_full()
-                    .px_2()
-                    .gap_2()
-                    // Header
+                    .justify_between()
+                    .py_2()
+                    .child(div().flex().child(Label::new("#design")))
                     .child(
                         div()
                             .flex()
-                            .justify_between()
-                            .gap_2()
-                            .child(div().flex().child(Label::new("#design")))
-                            .child(
-                                div()
-                                    .flex()
-                                    .items_center()
-                                    .gap_px()
-                                    .child(IconButton::new(Icon::File))
-                                    .child(IconButton::new(Icon::AudioOn)),
-                            ),
-                    )
+                            .items_center()
+                            .gap_px()
+                            .child(IconButton::new(Icon::File))
+                            .child(IconButton::new(Icon::AudioOn)),
+                    ),
+            )
+            .child(
+                div()
+                    .flex()
+                    .flex_col()
                     // Chat Body
                     .child(
                         div()
@@ -76,19 +64,12 @@ impl<V: 'static> ChatPanel<V> {
                             .flex()
                             .flex_col()
                             .gap_3()
-                            .overflow_y_scroll(payload.scroll_state.clone())
-                            .children(payload.messages.clone()),
+                            .overflow_y_scroll(self.scroll_state.clone())
+                            .children(self.messages.clone()),
                     )
                     // Composer
-                    .child(div().flex().gap_2().child(Input::new("Message #design")))
-                    .into_any()]
-            },
-            Box::new(PanelPayload {
-                scroll_state: self.scroll_state.clone(),
-                messages: self.messages.clone(),
-            }),
-        )
-        .side(self.current_side)
+                    .child(div().flex().my_2().child(Input::new("Message #design"))),
+            )
     }
 }
 

crates/ui/src/components/editor.rs 🔗

@@ -1,25 +0,0 @@
-use std::marker::PhantomData;
-
-use crate::prelude::*;
-use crate::{Buffer, Toolbar};
-
-#[derive(Element)]
-struct Editor<V: 'static> {
-    view_type: PhantomData<V>,
-    toolbar: Toolbar,
-    buffer: Buffer<V>,
-}
-
-impl<V: 'static> Editor<V> {
-    pub fn new(toolbar: Toolbar, buffer: Buffer<V>) -> Self {
-        Self {
-            view_type: PhantomData,
-            toolbar,
-            buffer,
-        }
-    }
-
-    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        div().child(self.toolbar.clone())
-    }
-}

crates/ui/src/components/editor_pane.rs 🔗

@@ -0,0 +1,60 @@
+use std::marker::PhantomData;
+use std::path::PathBuf;
+
+use crate::prelude::*;
+use crate::{v_stack, Breadcrumb, Buffer, Icon, IconButton, Symbol, Tab, TabBar, Toolbar};
+
+pub struct Editor {
+    pub tabs: Vec<Tab>,
+    pub path: PathBuf,
+    pub symbols: Vec<Symbol>,
+    pub buffer: Buffer,
+}
+
+#[derive(Element)]
+pub struct EditorPane<V: 'static> {
+    view_type: PhantomData<V>,
+    editor: Editor,
+}
+
+impl<V: 'static> EditorPane<V> {
+    pub fn new(editor: Editor) -> Self {
+        Self {
+            view_type: PhantomData,
+            editor,
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        struct LeftItemsPayload {
+            path: PathBuf,
+            symbols: Vec<Symbol>,
+        }
+
+        v_stack()
+            .w_full()
+            .h_full()
+            .flex_1()
+            .child(TabBar::new(self.editor.tabs.clone()))
+            .child(Toolbar::new(
+                |_, payload| {
+                    let payload = payload.downcast_ref::<LeftItemsPayload>().unwrap();
+
+                    vec![Breadcrumb::new(payload.path.clone(), payload.symbols.clone()).into_any()]
+                },
+                Box::new(LeftItemsPayload {
+                    path: self.editor.path.clone(),
+                    symbols: self.editor.symbols.clone(),
+                }),
+                |_, _| {
+                    vec![
+                        IconButton::new(Icon::InlayHint).into_any(),
+                        IconButton::new(Icon::MagnifyingGlass).into_any(),
+                        IconButton::new(Icon::MagicWand).into_any(),
+                    ]
+                },
+                Box::new(()),
+            ))
+            .child(self.editor.buffer.clone())
+    }
+}

crates/ui/src/components/panel.rs 🔗

@@ -105,16 +105,12 @@ impl<V: 'static> Panel<V> {
         let theme = theme(cx);
 
         let panel_base;
-        let current_width = if let Some(width) = self.width {
-            width
-        } else {
-            self.initial_width
-        };
+        let current_width = self.width.unwrap_or(self.initial_width);
 
         match self.current_side {
             PanelSide::Left => {
                 panel_base = v_stack()
-                    .overflow_y_scroll(self.scroll_state.clone())
+                    .flex_initial()
                     .h_full()
                     .w(current_width)
                     .fill(theme.middle.base.default.background)
@@ -123,20 +119,20 @@ impl<V: 'static> Panel<V> {
             }
             PanelSide::Right => {
                 panel_base = v_stack()
-                    .overflow_y_scroll(self.scroll_state.clone())
+                    .flex_initial()
                     .h_full()
                     .w(current_width)
                     .fill(theme.middle.base.default.background)
-                    .border_r()
+                    .border_l()
                     .border_color(theme.middle.base.default.border);
             }
             PanelSide::Bottom => {
                 panel_base = v_stack()
-                    .overflow_y_scroll(self.scroll_state.clone())
+                    .flex_initial()
                     .w_full()
                     .h(current_width)
                     .fill(theme.middle.base.default.background)
-                    .border_r()
+                    .border_t()
                     .border_color(theme.middle.base.default.border);
             }
         }

crates/ui/src/components/player_stack.rs 🔗

@@ -38,9 +38,8 @@ impl PlayerStack {
                 div().flex().justify_center().w_full().child(
                     div()
                         .w_4()
-                        .h_1()
-                        .rounded_bl_sm()
-                        .rounded_br_sm()
+                        .h_0p5()
+                        .rounded_sm()
                         .fill(player.cursor_color(cx)),
                 ),
             )
@@ -50,7 +49,7 @@ impl PlayerStack {
                     .items_center()
                     .justify_center()
                     .h_6()
-                    .px_1()
+                    .pl_1()
                     .rounded_lg()
                     .fill(if followers.is_none() {
                         system_color.transparent
@@ -59,7 +58,7 @@ impl PlayerStack {
                     })
                     .child(Avatar::new(player.avatar_src().to_string()))
                     .children(followers.map(|followers| {
-                        div().neg_mr_1().child(Facepile::new(followers.into_iter()))
+                        div().neg_ml_2().child(Facepile::new(followers.into_iter()))
                     })),
             )
     }

crates/ui/src/components/project_panel.rs 🔗

@@ -1,17 +1,15 @@
 use std::marker::PhantomData;
-use std::sync::Arc;
 
 use crate::prelude::*;
 use crate::{
     static_project_panel_project_items, static_project_panel_single_items, theme, Input, List,
-    ListHeader, Panel, PanelSide, Theme,
+    ListHeader,
 };
 
 #[derive(Element)]
 pub struct ProjectPanel<V: 'static> {
     view_type: PhantomData<V>,
     scroll_state: ScrollState,
-    current_side: PanelSide,
 }
 
 impl<V: 'static> ProjectPanel<V> {
@@ -19,69 +17,42 @@ impl<V: 'static> ProjectPanel<V> {
         Self {
             view_type: PhantomData,
             scroll_state,
-            current_side: PanelSide::default(),
         }
     }
 
-    pub fn side(mut self, side: PanelSide) -> Self {
-        self.current_side = side;
-        self
-    }
-
     fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
-        struct PanelPayload {
-            pub theme: Arc<Theme>,
-            pub scroll_state: ScrollState,
-        }
-
-        Panel::new(
-            self.scroll_state.clone(),
-            |_, payload| {
-                let payload = payload.downcast_ref::<PanelPayload>().unwrap();
-
-                let theme = payload.theme.clone();
-
-                vec![div()
+        let theme = theme(cx);
+
+        div()
+            .flex()
+            .flex_col()
+            .w_full()
+            .h_full()
+            .px_2()
+            .fill(theme.middle.base.default.background)
+            .child(
+                div()
+                    .w_56()
                     .flex()
                     .flex_col()
-                    .w_56()
-                    .h_full()
-                    .px_2()
-                    .fill(theme.middle.base.default.background)
+                    .overflow_y_scroll(ScrollState::default())
                     .child(
-                        div()
-                            .w_56()
-                            .flex()
-                            .flex_col()
-                            .overflow_y_scroll(payload.scroll_state.clone())
-                            .child(
-                                List::new(static_project_panel_single_items())
-                                    .header(
-                                        ListHeader::new("FILES").set_toggle(ToggleState::Toggled),
-                                    )
-                                    .empty_message("No files in directory")
-                                    .set_toggle(ToggleState::Toggled),
-                            )
-                            .child(
-                                List::new(static_project_panel_project_items())
-                                    .header(
-                                        ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled),
-                                    )
-                                    .empty_message("No folders in directory")
-                                    .set_toggle(ToggleState::Toggled),
-                            ),
+                        List::new(static_project_panel_single_items())
+                            .header(ListHeader::new("FILES").set_toggle(ToggleState::Toggled))
+                            .empty_message("No files in directory")
+                            .set_toggle(ToggleState::Toggled),
                     )
                     .child(
-                        Input::new("Find something...")
-                            .value("buffe".to_string())
-                            .state(InteractionState::Focused),
-                    )
-                    .into_any()]
-            },
-            Box::new(PanelPayload {
-                theme: theme(cx),
-                scroll_state: self.scroll_state.clone(),
-            }),
-        )
+                        List::new(static_project_panel_project_items())
+                            .header(ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled))
+                            .empty_message("No folders in directory")
+                            .set_toggle(ToggleState::Toggled),
+                    ),
+            )
+            .child(
+                Input::new("Find something...")
+                    .value("buffe".to_string())
+                    .state(InteractionState::Focused),
+            )
     }
 }

crates/ui/src/components/tab.rs 🔗

@@ -1,7 +1,7 @@
 use crate::prelude::*;
 use crate::{theme, Icon, IconColor, IconElement, Label, LabelColor};
 
-#[derive(Element)]
+#[derive(Element, Clone)]
 pub struct Tab {
     title: String,
     icon: Option<Icon>,

crates/ui/src/components/tab_bar.rs 🔗

@@ -7,20 +7,27 @@ use crate::{theme, Icon, IconButton, Tab};
 pub struct TabBar<V: 'static> {
     view_type: PhantomData<V>,
     scroll_state: ScrollState,
+    tabs: Vec<Tab>,
 }
 
 impl<V: 'static> TabBar<V> {
-    pub fn new(scroll_state: ScrollState) -> Self {
+    pub fn new(tabs: Vec<Tab>) -> Self {
         Self {
             view_type: PhantomData,
-            scroll_state,
+            scroll_state: ScrollState::default(),
+            tabs,
         }
     }
 
+    pub fn bind_scroll_state(&mut self, scroll_state: ScrollState) {
+        self.scroll_state = scroll_state;
+    }
+
     fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
         let can_navigate_back = true;
         let can_navigate_forward = false;
+
         div()
             .w_full()
             .flex()
@@ -54,51 +61,7 @@ impl<V: 'static> TabBar<V> {
                     div()
                         .flex()
                         .overflow_x_scroll(self.scroll_state.clone())
-                        .child(
-                            Tab::new()
-                                .title("Cargo.toml".to_string())
-                                .current(false)
-                                .git_status(GitStatus::Modified),
-                        )
-                        .child(
-                            Tab::new()
-                                .title("Channels Panel".to_string())
-                                .current(false),
-                        )
-                        .child(
-                            Tab::new()
-                                .title("channels_panel.rs".to_string())
-                                .current(true)
-                                .git_status(GitStatus::Modified),
-                        )
-                        .child(
-                            Tab::new()
-                                .title("workspace.rs".to_string())
-                                .current(false)
-                                .git_status(GitStatus::Modified),
-                        )
-                        .child(
-                            Tab::new()
-                                .title("icon_button.rs".to_string())
-                                .current(false),
-                        )
-                        .child(
-                            Tab::new()
-                                .title("storybook.rs".to_string())
-                                .current(false)
-                                .git_status(GitStatus::Created),
-                        )
-                        .child(Tab::new().title("theme.rs".to_string()).current(false))
-                        .child(
-                            Tab::new()
-                                .title("theme_registry.rs".to_string())
-                                .current(false),
-                        )
-                        .child(
-                            Tab::new()
-                                .title("styleable_helpers.rs".to_string())
-                                .current(false),
-                        ),
+                        .children(self.tabs.clone()),
                 ),
             )
             // Right Side

crates/ui/src/components/terminal.rs 🔗

@@ -1,3 +1,5 @@
+use std::sync::Arc;
+
 use gpui2::geometry::{relative, rems, Size};
 
 use crate::prelude::*;
@@ -20,6 +22,7 @@ impl Terminal {
         div()
             .flex()
             .flex_col()
+            .w_full()
             .child(
                 // Terminal Tabs.
                 div()
@@ -70,8 +73,12 @@ impl Terminal {
                     width: relative(1.).into(),
                     height: rems(36.).into(),
                 },
-                |_, _| vec![],
-                Box::new(()),
+                |_, payload| {
+                    let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
+
+                    vec![crate::static_data::terminal_buffer(&theme).into_any()]
+                },
+                Box::new(theme),
             ))
     }
 }

crates/ui/src/components/title_bar.rs 🔗

@@ -2,16 +2,24 @@ use std::marker::PhantomData;
 use std::sync::atomic::AtomicBool;
 use std::sync::Arc;
 
-use crate::prelude::*;
+use crate::{prelude::*, PlayerWithCallStatus};
 use crate::{
-    static_players_with_call_status, theme, Avatar, Button, Icon, IconButton, IconColor,
-    PlayerStack, ToolDivider, TrafficLights,
+    theme, Avatar, Button, Icon, IconButton, IconColor, PlayerStack, ToolDivider, TrafficLights,
 };
 
+#[derive(Clone)]
+pub struct Livestream {
+    pub players: Vec<PlayerWithCallStatus>,
+    pub channel: Option<String>, // projects
+                                 // windows
+}
+
 #[derive(Element)]
 pub struct TitleBar<V: 'static> {
     view_type: PhantomData<V>,
+    /// If the window is active from the OS's perspective.
     is_active: Arc<AtomicBool>,
+    livestream: Option<Livestream>,
 }
 
 impl<V: 'static> TitleBar<V> {
@@ -28,14 +36,24 @@ impl<V: 'static> TitleBar<V> {
         Self {
             view_type: PhantomData,
             is_active,
+            livestream: None,
         }
     }
 
+    pub fn set_livestream(mut self, livestream: Option<Livestream>) -> Self {
+        self.livestream = livestream;
+        self
+    }
+
     fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
         let has_focus = cx.window_is_active();
 
-        let player_list = static_players_with_call_status().into_iter();
+        let player_list = if let Some(livestream) = &self.livestream {
+            livestream.players.clone().into_iter()
+        } else {
+            vec![].into_iter()
+        };
 
         div()
             .flex()
@@ -61,7 +79,8 @@ impl<V: 'static> TitleBar<V> {
                             .child(Button::new("zed"))
                             .child(Button::new("nate/gpui2-ui-components")),
                     )
-                    .children(player_list.map(|p| PlayerStack::new(p))),
+                    .children(player_list.map(|p| PlayerStack::new(p)))
+                    .child(IconButton::new(Icon::Plus)),
             )
             .child(
                 div()

crates/ui/src/components/toolbar.rs 🔗

@@ -1,33 +1,49 @@
 use crate::prelude::*;
-use crate::{theme, Breadcrumb, Icon, IconButton};
+use crate::theme;
 
 #[derive(Clone)]
 pub struct ToolbarItem {}
 
-#[derive(Element, Clone)]
-pub struct Toolbar {
-    items: Vec<ToolbarItem>,
+#[derive(Element)]
+pub struct Toolbar<V: 'static> {
+    left_items: HackyChildren<V>,
+    left_items_payload: HackyChildrenPayload,
+    right_items: HackyChildren<V>,
+    right_items_payload: HackyChildrenPayload,
 }
 
-impl Toolbar {
-    pub fn new() -> Self {
-        Self { items: Vec::new() }
+impl<V: 'static> Toolbar<V> {
+    pub fn new(
+        left_items: HackyChildren<V>,
+        left_items_payload: HackyChildrenPayload,
+        right_items: HackyChildren<V>,
+        right_items_payload: HackyChildrenPayload,
+    ) -> Self {
+        Self {
+            left_items,
+            left_items_payload,
+            right_items,
+            right_items_payload,
+        }
     }
 
-    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
         let theme = theme(cx);
 
         div()
+            .fill(theme.highest.base.default.background)
             .p_2()
             .flex()
             .justify_between()
-            .child(Breadcrumb::new())
             .child(
                 div()
                     .flex()
-                    .child(IconButton::new(Icon::InlayHint))
-                    .child(IconButton::new(Icon::MagnifyingGlass))
-                    .child(IconButton::new(Icon::MagicWand)),
+                    .children_any((self.left_items)(cx, self.left_items_payload.as_ref())),
+            )
+            .child(
+                div()
+                    .flex()
+                    .children_any((self.right_items)(cx, self.right_items_payload.as_ref())),
             )
     }
 }

crates/ui/src/components/workspace.rs 🔗

@@ -1,10 +1,15 @@
+use std::sync::Arc;
+
 use chrono::DateTime;
 use gpui2::geometry::{relative, rems, Size};
 
-use crate::prelude::*;
 use crate::{
-    theme, v_stack, ChatMessage, ChatPanel, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide,
-    ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
+    hello_world_rust_editor_with_status_example, prelude::*, random_players_with_call_status,
+    Livestream,
+};
+use crate::{
+    theme, v_stack, ChatMessage, ChatPanel, EditorPane, Pane, PaneGroup, Panel, PanelAllowedSides,
+    PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
 };
 
 #[derive(Element, Default)]
@@ -17,6 +22,8 @@ pub struct WorkspaceElement {
 
 impl WorkspaceElement {
     fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx).clone();
+
         let temp_size = rems(36.).into();
 
         let root_group = PaneGroup::new_groups(
@@ -29,8 +36,15 @@ impl WorkspaceElement {
                                 width: relative(1.).into(),
                                 height: temp_size,
                             },
-                            |_, _| vec![Terminal::new().into_any()],
-                            Box::new(()),
+                            |_, payload| {
+                                let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
+
+                                vec![EditorPane::new(hello_world_rust_editor_with_status_example(
+                                    &theme,
+                                ))
+                                .into_any()]
+                            },
+                            Box::new(theme.clone()),
                         ),
                         Pane::new(
                             ScrollState::default(),
@@ -51,8 +65,15 @@ impl WorkspaceElement {
                             width: relative(1.).into(),
                             height: relative(1.).into(),
                         },
-                        |_, _| vec![Terminal::new().into_any()],
-                        Box::new(()),
+                        |_, payload| {
+                            let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
+
+                            vec![EditorPane::new(hello_world_rust_editor_with_status_example(
+                                &theme,
+                            ))
+                            .into_any()]
+                        },
+                        Box::new(theme.clone()),
                     )],
                     SplitDirection::Vertical,
                 ),
@@ -60,8 +81,6 @@ impl WorkspaceElement {
             SplitDirection::Horizontal,
         );
 
-        let theme = theme(cx).clone();
-
         div()
             .size_full()
             .flex()
@@ -72,7 +91,10 @@ impl WorkspaceElement {
             .items_start()
             .text_color(theme.lowest.base.default.foreground)
             .fill(theme.lowest.base.default.background)
-            .child(TitleBar::new(cx))
+            .child(TitleBar::new(cx).set_livestream(Some(Livestream {
+                players: random_players_with_call_status(7),
+                channel: Some("gpui2-ui".to_string()),
+            })))
             .child(
                 div()
                     .flex_1()
@@ -84,8 +106,12 @@ impl WorkspaceElement {
                     .border_b()
                     .border_color(theme.lowest.base.default.border)
                     .child(
-                        ProjectPanel::new(self.left_panel_scroll_state.clone())
-                            .side(PanelSide::Left),
+                        Panel::new(
+                            self.left_panel_scroll_state.clone(),
+                            |_, payload| vec![ProjectPanel::new(ScrollState::default()).into_any()],
+                            Box::new(()),
+                        )
+                        .side(PanelSide::Left),
                     )
                     .child(
                         v_stack()
@@ -110,26 +136,37 @@ impl WorkspaceElement {
                                 .side(PanelSide::Bottom),
                             ),
                     )
-                    .child(ChatPanel::new(ScrollState::default()).with_messages(vec![
-                                ChatMessage::new(
-                                    "osiewicz".to_string(),
-                                    "is this thing on?".to_string(),
-                                    DateTime::parse_from_rfc3339(
-                                        "2023-09-27T15:40:52.707Z",
-                                    )
-                                    .unwrap()
-                                    .naive_local(),
-                                ),
-                                ChatMessage::new(
-                                    "maxdeviant".to_string(),
-                                    "Reading you loud and clear!".to_string(),
-                                    DateTime::parse_from_rfc3339(
-                                        "2023-09-28T15:40:52.707Z",
-                                    )
-                                    .unwrap()
-                                    .naive_local(),
-                                ),
-                            ])),
+                    .child(
+                        Panel::new(
+                            self.right_panel_scroll_state.clone(),
+                            |_, payload| {
+                                vec![ChatPanel::new(ScrollState::default())
+                                    .with_messages(vec![
+                                        ChatMessage::new(
+                                            "osiewicz".to_string(),
+                                            "is this thing on?".to_string(),
+                                            DateTime::parse_from_rfc3339(
+                                                "2023-09-27T15:40:52.707Z",
+                                            )
+                                            .unwrap()
+                                            .naive_local(),
+                                        ),
+                                        ChatMessage::new(
+                                            "maxdeviant".to_string(),
+                                            "Reading you loud and clear!".to_string(),
+                                            DateTime::parse_from_rfc3339(
+                                                "2023-09-28T15:40:52.707Z",
+                                            )
+                                            .unwrap()
+                                            .naive_local(),
+                                        ),
+                                    ])
+                                    .into_any()]
+                            },
+                            Box::new(()),
+                        )
+                        .side(PanelSide::Right),
+                    ),
             )
             .child(StatusBar::new())
     }

crates/ui/src/elements/icon.rs 🔗

@@ -84,6 +84,7 @@ pub enum Icon {
     Plus,
     Quote,
     Screen,
+    SelectAll,
     Split,
     SplitMessage,
     Terminal,
@@ -131,6 +132,7 @@ impl Icon {
             Icon::Plus => "icons/plus.svg",
             Icon::Quote => "icons/quote.svg",
             Icon::Screen => "icons/desktop.svg",
+            Icon::SelectAll => "icons/select-all.svg",
             Icon::Split => "icons/split.svg",
             Icon::SplitMessage => "icons/split_message.svg",
             Icon::Terminal => "icons/terminal.svg",

crates/ui/src/elements/player.rs 🔗

@@ -65,7 +65,7 @@ impl PlayerCallStatus {
     }
 }
 
-#[derive(Clone)]
+#[derive(PartialEq, Clone)]
 pub struct Player {
     index: usize,
     avatar_src: String,
@@ -73,6 +73,7 @@ pub struct Player {
     status: PlayerStatus,
 }
 
+#[derive(Clone)]
 pub struct PlayerWithCallStatus {
     player: Player,
     call_status: PlayerCallStatus,

crates/ui/src/prelude.rs 🔗

@@ -2,7 +2,7 @@ pub use gpui2::elements::div::{div, ScrollState};
 pub use gpui2::style::{StyleHelpers, Styleable};
 pub use gpui2::{Element, IntoElement, ParentElement, ViewContext};
 
-pub use crate::{theme, ButtonVariant, HackyChildren, HackyChildrenPayload, InputVariant};
+pub use crate::{theme, ButtonVariant, HackyChildren, HackyChildrenPayload, InputVariant, Theme};
 
 use gpui2::{hsla, rgb, Hsla, WindowContext};
 use strum::EnumIter;
@@ -40,8 +40,7 @@ pub enum HighlightColor {
 }
 
 impl HighlightColor {
-    pub fn hsla(&self, cx: &WindowContext) -> Hsla {
-        let theme = theme(cx);
+    pub fn hsla(&self, theme: &Theme) -> Hsla {
         let system_color = SystemColor::new();
 
         match self {
@@ -74,7 +73,7 @@ impl HighlightColor {
     }
 }
 
-#[derive(Default, PartialEq, EnumIter)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
 pub enum FileSystemStatus {
     #[default]
     None,
@@ -92,7 +91,7 @@ impl FileSystemStatus {
     }
 }
 
-#[derive(Default, PartialEq, EnumIter, Clone, Copy)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
 pub enum GitStatus {
     #[default]
     None,
@@ -130,7 +129,7 @@ impl GitStatus {
     }
 }
 
-#[derive(Default, PartialEq)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
 pub enum DiagnosticStatus {
     #[default]
     None,
@@ -139,14 +138,14 @@ pub enum DiagnosticStatus {
     Info,
 }
 
-#[derive(Default, PartialEq)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
 pub enum IconSide {
     #[default]
     Left,
     Right,
 }
 
-#[derive(Default, PartialEq)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
 pub enum OrderMethod {
     #[default]
     Ascending,
@@ -154,14 +153,14 @@ pub enum OrderMethod {
     MostRecent,
 }
 
-#[derive(Default, PartialEq, Clone, Copy)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
 pub enum Shape {
     #[default]
     Circle,
     RoundedRectangle,
 }
 
-#[derive(Default, PartialEq, Clone, Copy)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
 pub enum DisclosureControlVisibility {
     #[default]
     OnHover,

crates/ui/src/static_data.rs 🔗

@@ -1,12 +1,109 @@
-use gpui2::WindowContext;
+use std::path::PathBuf;
+use std::str::FromStr;
+
+use rand::Rng;
 
 use crate::{
-    Buffer, BufferRow, BufferRows, GitStatus, HighlightColor, HighlightedLine, HighlightedText,
-    Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem, MicStatus,
-    ModifierKeys, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus,
-    ToggleState,
+    Buffer, BufferRow, BufferRows, Editor, FileSystemStatus, GitStatus, HighlightColor,
+    HighlightedLine, HighlightedText, Icon, Keybinding, Label, LabelColor, ListEntry,
+    ListEntrySize, ListItem, Livestream, MicStatus, ModifierKeys, PaletteItem, Player,
+    PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, Theme, ToggleState,
+    VideoStatus,
 };
 
+pub fn static_tabs_example() -> Vec<Tab> {
+    vec![
+        Tab::new()
+            .title("wip.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .fs_status(FileSystemStatus::Deleted),
+        Tab::new()
+            .title("Cargo.toml".to_string())
+            .icon(Icon::FileToml)
+            .current(false)
+            .git_status(GitStatus::Modified),
+        Tab::new()
+            .title("Channels Panel".to_string())
+            .icon(Icon::Hash)
+            .current(false),
+        Tab::new()
+            .title("channels_panel.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(true)
+            .git_status(GitStatus::Modified),
+        Tab::new()
+            .title("workspace.rs".to_string())
+            .current(false)
+            .icon(Icon::FileRust)
+            .git_status(GitStatus::Modified),
+        Tab::new()
+            .title("icon_button.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+        Tab::new()
+            .title("storybook.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .git_status(GitStatus::Created),
+        Tab::new()
+            .title("theme.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+        Tab::new()
+            .title("theme_registry.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+        Tab::new()
+            .title("styleable_helpers.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+    ]
+}
+
+pub fn static_tabs_1() -> Vec<Tab> {
+    vec![
+        Tab::new()
+            .title("project_panel.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .fs_status(FileSystemStatus::Deleted),
+        Tab::new()
+            .title("tab_bar.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .git_status(GitStatus::Modified),
+        Tab::new()
+            .title("workspace.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false),
+        Tab::new()
+            .title("tab.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(true)
+            .git_status(GitStatus::Modified),
+    ]
+}
+
+pub fn static_tabs_2() -> Vec<Tab> {
+    vec![
+        Tab::new()
+            .title("tab_bar.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(false)
+            .fs_status(FileSystemStatus::Deleted),
+        Tab::new()
+            .title("static_data.rs".to_string())
+            .icon(Icon::FileRust)
+            .current(true)
+            .git_status(GitStatus::Modified),
+    ]
+}
+
+pub fn static_tabs_3() -> Vec<Tab> {
+    vec![Tab::new().git_status(GitStatus::Created).current(true)]
+}
+
 pub fn static_players() -> Vec<Player> {
     vec![
         Player::new(
@@ -37,6 +134,154 @@ pub fn static_players() -> Vec<Player> {
     ]
 }
 
+#[derive(Debug)]
+pub struct PlayerData {
+    pub url: String,
+    pub name: String,
+}
+pub fn static_player_data() -> Vec<PlayerData> {
+    vec![
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
+            name: "iamnbutler".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/326587?v=4".into(),
+            name: "maxbrunsfeld".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/482957?v=4".into(),
+            name: "as-cii".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1789?v=4".into(),
+            name: "nathansobo".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
+            name: "ForLoveOfCats".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/2690773?v=4".into(),
+            name: "SomeoneToIgnore".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/19867440?v=4".into(),
+            name: "JosephTLyons".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/24362066?v=4".into(),
+            name: "osiewicz".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/22121886?v=4".into(),
+            name: "KCaverly".into(),
+        },
+        PlayerData {
+            url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
+            name: "maxdeviant".into(),
+        },
+    ]
+}
+pub fn create_static_players(player_data: Vec<PlayerData>) -> Vec<Player> {
+    let mut players = Vec::new();
+    for data in player_data {
+        players.push(Player::new(players.len(), data.url, data.name));
+    }
+    players
+}
+pub fn static_player_1(data: &Vec<PlayerData>) -> Player {
+    Player::new(1, data[0].url.clone(), data[0].name.clone())
+}
+pub fn static_player_2(data: &Vec<PlayerData>) -> Player {
+    Player::new(2, data[1].url.clone(), data[1].name.clone())
+}
+pub fn static_player_3(data: &Vec<PlayerData>) -> Player {
+    Player::new(3, data[2].url.clone(), data[2].name.clone())
+}
+pub fn static_player_4(data: &Vec<PlayerData>) -> Player {
+    Player::new(4, data[3].url.clone(), data[3].name.clone())
+}
+pub fn static_player_5(data: &Vec<PlayerData>) -> Player {
+    Player::new(5, data[4].url.clone(), data[4].name.clone())
+}
+pub fn static_player_6(data: &Vec<PlayerData>) -> Player {
+    Player::new(6, data[5].url.clone(), data[5].name.clone())
+}
+pub fn static_player_7(data: &Vec<PlayerData>) -> Player {
+    Player::new(7, data[6].url.clone(), data[6].name.clone())
+}
+pub fn static_player_8(data: &Vec<PlayerData>) -> Player {
+    Player::new(8, data[7].url.clone(), data[7].name.clone())
+}
+pub fn static_player_9(data: &Vec<PlayerData>) -> Player {
+    Player::new(9, data[8].url.clone(), data[8].name.clone())
+}
+pub fn static_player_10(data: &Vec<PlayerData>) -> Player {
+    Player::new(10, data[9].url.clone(), data[9].name.clone())
+}
+pub fn static_livestream() -> Livestream {
+    Livestream {
+        players: random_players_with_call_status(7),
+        channel: Some("gpui2-ui".to_string()),
+    }
+}
+pub fn populate_player_call_status(
+    player: Player,
+    followers: Option<Vec<Player>>,
+) -> PlayerCallStatus {
+    let mut rng = rand::thread_rng();
+    let in_current_project: bool = rng.gen();
+    let disconnected: bool = rng.gen();
+    let voice_activity: f32 = rng.gen();
+    let mic_status = if rng.gen_bool(0.5) {
+        MicStatus::Muted
+    } else {
+        MicStatus::Unmuted
+    };
+    let video_status = if rng.gen_bool(0.5) {
+        VideoStatus::On
+    } else {
+        VideoStatus::Off
+    };
+    let screen_share_status = if rng.gen_bool(0.5) {
+        ScreenShareStatus::Shared
+    } else {
+        ScreenShareStatus::NotShared
+    };
+    PlayerCallStatus {
+        mic_status,
+        voice_activity,
+        video_status,
+        screen_share_status,
+        in_current_project,
+        disconnected,
+        following: None,
+        followers,
+    }
+}
+pub fn random_players_with_call_status(number_of_players: usize) -> Vec<PlayerWithCallStatus> {
+    let players = create_static_players(static_player_data());
+    let mut player_status = vec![];
+    for i in 0..number_of_players {
+        let followers = if i == 0 {
+            Some(vec![
+                players[1].clone(),
+                players[3].clone(),
+                players[5].clone(),
+                players[6].clone(),
+            ])
+        } else if i == 1 {
+            Some(vec![players[2].clone(), players[6].clone()])
+        } else {
+            None
+        };
+        let call_status = populate_player_call_status(players[i].clone(), followers);
+        player_status.push(PlayerWithCallStatus::new(players[i].clone(), call_status));
+    }
+    player_status
+}
+
 pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
     let players = static_players();
     let mut player_0_status = PlayerCallStatus::new();
@@ -123,7 +368,7 @@ pub fn static_project_panel_project_items() -> Vec<ListItem> {
             .left_icon(Icon::FolderOpen.into())
             .indent_level(3)
             .set_toggle(ToggleState::Toggled),
-        ListEntry::new(Label::new("derrive_element.rs"))
+        ListEntry::new(Label::new("derive_element.rs"))
             .left_icon(Icon::FileRust.into())
             .indent_level(4),
         ListEntry::new(Label::new("storybook").color(LabelColor::Modified))
@@ -337,33 +582,49 @@ pub fn example_editor_actions() -> Vec<PaletteItem> {
     ]
 }
 
-pub fn empty_buffer_example<V: 'static>() -> Buffer<V> {
+pub fn empty_editor_example() -> Editor {
+    Editor {
+        tabs: static_tabs_example(),
+        path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
+        symbols: vec![],
+        buffer: empty_buffer_example(),
+    }
+}
+
+pub fn empty_buffer_example() -> Buffer {
     Buffer::new().set_rows(Some(BufferRows::default()))
 }
 
-pub fn hello_world_rust_buffer_example<V: 'static>(cx: &WindowContext) -> Buffer<V> {
-    Buffer::new()
-        .set_title("hello_world.rs".to_string())
-        .set_path("src/hello_world.rs".to_string())
-        .set_language("rust".to_string())
-        .set_rows(Some(BufferRows {
-            show_line_numbers: true,
-            rows: hello_world_rust_buffer_rows(cx),
-        }))
+pub fn hello_world_rust_editor_example(theme: &Theme) -> Editor {
+    Editor {
+        tabs: static_tabs_example(),
+        path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
+        symbols: vec![Symbol(vec![
+            HighlightedText {
+                text: "fn ".to_string(),
+                color: HighlightColor::Keyword.hsla(&theme),
+            },
+            HighlightedText {
+                text: "main".to_string(),
+                color: HighlightColor::Function.hsla(&theme),
+            },
+        ])],
+        buffer: hello_world_rust_buffer_example(theme),
+    }
 }
 
-pub fn hello_world_rust_buffer_with_status_example<V: 'static>(cx: &WindowContext) -> Buffer<V> {
+pub fn hello_world_rust_buffer_example(theme: &Theme) -> Buffer {
     Buffer::new()
         .set_title("hello_world.rs".to_string())
         .set_path("src/hello_world.rs".to_string())
         .set_language("rust".to_string())
         .set_rows(Some(BufferRows {
             show_line_numbers: true,
-            rows: hello_world_rust_with_status_buffer_rows(cx),
+            rows: hello_world_rust_buffer_rows(theme),
         }))
 }
 
-pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
+pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
     let show_line_number = true;
 
     vec![
@@ -375,15 +636,15 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
                 highlighted_texts: vec![
                     HighlightedText {
                         text: "fn ".to_string(),
-                        color: HighlightColor::Keyword.hsla(cx),
+                        color: HighlightColor::Keyword.hsla(&theme),
                     },
                     HighlightedText {
                         text: "main".to_string(),
-                        color: HighlightColor::Function.hsla(cx),
+                        color: HighlightColor::Function.hsla(&theme),
                     },
                     HighlightedText {
                         text: "() {".to_string(),
-                        color: HighlightColor::Default.hsla(cx),
+                        color: HighlightColor::Default.hsla(&theme),
                     },
                 ],
             }),
@@ -399,7 +660,7 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
                 highlighted_texts: vec![HighlightedText {
                     text: "    // Statements here are executed when the compiled binary is called."
                         .to_string(),
-                    color: HighlightColor::Comment.hsla(cx),
+                    color: HighlightColor::Comment.hsla(&theme),
                 }],
             }),
             cursors: None,
@@ -422,7 +683,7 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
                     text: "    // Print text to the console.".to_string(),
-                    color: HighlightColor::Comment.hsla(cx),
+                    color: HighlightColor::Comment.hsla(&theme),
                 }],
             }),
             cursors: None,
@@ -433,10 +694,34 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
             line_number: 5,
             code_action: false,
             current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "    println!(".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "\"Hello, world!\"".to_string(),
+                        color: HighlightColor::String.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: ");".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 6,
+            code_action: false,
+            current: false,
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
                     text: "}".to_string(),
-                    color: HighlightColor::Default.hsla(cx),
+                    color: HighlightColor::Default.hsla(&theme),
                 }],
             }),
             cursors: None,
@@ -446,7 +731,36 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
     ]
 }
 
-pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<BufferRow> {
+pub fn hello_world_rust_editor_with_status_example(theme: &Theme) -> Editor {
+    Editor {
+        tabs: static_tabs_example(),
+        path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
+        symbols: vec![Symbol(vec![
+            HighlightedText {
+                text: "fn ".to_string(),
+                color: HighlightColor::Keyword.hsla(&theme),
+            },
+            HighlightedText {
+                text: "main".to_string(),
+                color: HighlightColor::Function.hsla(&theme),
+            },
+        ])],
+        buffer: hello_world_rust_buffer_with_status_example(theme),
+    }
+}
+
+pub fn hello_world_rust_buffer_with_status_example(theme: &Theme) -> Buffer {
+    Buffer::new()
+        .set_title("hello_world.rs".to_string())
+        .set_path("src/hello_world.rs".to_string())
+        .set_language("rust".to_string())
+        .set_rows(Some(BufferRows {
+            show_line_numbers: true,
+            rows: hello_world_rust_with_status_buffer_rows(theme),
+        }))
+}
+
+pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
     let show_line_number = true;
 
     vec![
@@ -458,15 +772,15 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
                 highlighted_texts: vec![
                     HighlightedText {
                         text: "fn ".to_string(),
-                        color: HighlightColor::Keyword.hsla(cx),
+                        color: HighlightColor::Keyword.hsla(&theme),
                     },
                     HighlightedText {
                         text: "main".to_string(),
-                        color: HighlightColor::Function.hsla(cx),
+                        color: HighlightColor::Function.hsla(&theme),
                     },
                     HighlightedText {
                         text: "() {".to_string(),
-                        color: HighlightColor::Default.hsla(cx),
+                        color: HighlightColor::Default.hsla(&theme),
                     },
                 ],
             }),
@@ -482,7 +796,7 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
                 highlighted_texts: vec![HighlightedText {
                     text: "// Statements here are executed when the compiled binary is called."
                         .to_string(),
-                    color: HighlightColor::Comment.hsla(cx),
+                    color: HighlightColor::Comment.hsla(&theme),
                 }],
             }),
             cursors: None,
@@ -505,7 +819,7 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
                     text: "    // Print text to the console.".to_string(),
-                    color: HighlightColor::Comment.hsla(cx),
+                    color: HighlightColor::Comment.hsla(&theme),
                 }],
             }),
             cursors: None,
@@ -516,10 +830,34 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
             line_number: 5,
             code_action: false,
             current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "    println!(".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "\"Hello, world!\"".to_string(),
+                        color: HighlightColor::String.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: ");".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 6,
+            code_action: false,
+            current: false,
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
                     text: "}".to_string(),
-                    color: HighlightColor::Default.hsla(cx),
+                    color: HighlightColor::Default.hsla(&theme),
                 }],
             }),
             cursors: None,
@@ -527,13 +865,13 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
             show_line_number,
         },
         BufferRow {
-            line_number: 6,
+            line_number: 7,
             code_action: false,
             current: false,
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
                     text: "".to_string(),
-                    color: HighlightColor::Default.hsla(cx),
+                    color: HighlightColor::Default.hsla(&theme),
                 }],
             }),
             cursors: None,
@@ -541,13 +879,13 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
             show_line_number,
         },
         BufferRow {
-            line_number: 7,
+            line_number: 8,
             code_action: false,
             current: false,
             line: Some(HighlightedLine {
                 highlighted_texts: vec![HighlightedText {
-                    text: "Marshall and Nate were here".to_string(),
-                    color: HighlightColor::Default.hsla(cx),
+                    text: "// Marshall and Nate were here".to_string(),
+                    color: HighlightColor::Comment.hsla(&theme),
                 }],
             }),
             cursors: None,
@@ -556,3 +894,73 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec<Buffe
         },
     ]
 }
+
+pub fn terminal_buffer(theme: &Theme) -> Buffer {
+    Buffer::new()
+        .set_title("zed — fish".to_string())
+        .set_rows(Some(BufferRows {
+            show_line_numbers: false,
+            rows: terminal_buffer_rows(theme),
+        }))
+}
+
+pub fn terminal_buffer_rows(theme: &Theme) -> Vec<BufferRow> {
+    let show_line_number = false;
+
+    vec![
+        BufferRow {
+            line_number: 1,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![
+                    HighlightedText {
+                        text: "maxdeviant ".to_string(),
+                        color: HighlightColor::Keyword.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "in ".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "profaned-capital ".to_string(),
+                        color: HighlightColor::Function.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "in ".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "~/p/zed ".to_string(),
+                        color: HighlightColor::Function.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: "on ".to_string(),
+                        color: HighlightColor::Default.hsla(&theme),
+                    },
+                    HighlightedText {
+                        text: " gpui2-ui ".to_string(),
+                        color: HighlightColor::Keyword.hsla(&theme),
+                    },
+                ],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+        BufferRow {
+            line_number: 2,
+            code_action: false,
+            current: false,
+            line: Some(HighlightedLine {
+                highlighted_texts: vec![HighlightedText {
+                    text: "λ ".to_string(),
+                    color: HighlightColor::String.hsla(&theme),
+                }],
+            }),
+            cursors: None,
+            status: GitStatus::None,
+            show_line_number,
+        },
+    ]
+}

crates/vim/src/vim.rs 🔗

@@ -171,7 +171,7 @@ impl Vim {
         self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
             Event::SelectionsChanged { local: true } => {
                 let editor = editor.read(cx);
-                if editor.leader_replica_id().is_none() {
+                if editor.leader_peer_id().is_none() {
                     let newest = editor.selections.newest::<usize>(cx);
                     local_selections_changed(newest, cx);
                 }
@@ -195,6 +195,8 @@ impl Vim {
             if editor_mode == EditorMode::Full
                 && !newest_selection_empty
                 && self.state().mode == Mode::Normal
+                // When following someone, don't switch vim mode.
+                && editor.leader_peer_id().is_none()
             {
                 self.switch_mode(Mode::Visual, true, cx);
             }

crates/workspace/src/item.rs 🔗

@@ -4,7 +4,10 @@ use crate::{
 };
 use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
 use anyhow::Result;
-use client::{proto, Client};
+use client::{
+    proto::{self, PeerId},
+    Client,
+};
 use gpui::geometry::vector::Vector2F;
 use gpui::AnyWindowHandle;
 use gpui::{
@@ -401,6 +404,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         if let Some(followed_item) = self.to_followable_item_handle(cx) {
             if let Some(message) = followed_item.to_state_proto(cx) {
                 workspace.update_followers(
+                    followed_item.is_project_item(cx),
                     proto::update_followers::Variant::CreateView(proto::View {
                         id: followed_item
                             .remote_id(&workspace.app_state.client, cx)
@@ -436,6 +440,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                     };
 
                     if let Some(item) = item.to_followable_item_handle(cx) {
+                        let is_project_item = item.is_project_item(cx);
                         let leader_id = workspace.leader_for_pane(&pane);
 
                         if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
@@ -455,6 +460,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                                 move |this, cx| {
                                     pending_update_scheduled.store(false, Ordering::SeqCst);
                                     this.update_followers(
+                                        is_project_item,
                                         proto::update_followers::Variant::UpdateView(
                                             proto::UpdateView {
                                                 id: item
@@ -692,14 +698,15 @@ pub trait FollowableItem: Item {
         message: proto::update_view::Variant,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>>;
+    fn is_project_item(&self, cx: &AppContext) -> bool;
 
-    fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
+    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
     fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
 }
 
 pub trait FollowableItemHandle: ItemHandle {
     fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
-    fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut WindowContext);
+    fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
     fn add_event_to_update_proto(
         &self,
@@ -714,6 +721,7 @@ pub trait FollowableItemHandle: ItemHandle {
         cx: &mut WindowContext,
     ) -> Task<Result<()>>;
     fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
+    fn is_project_item(&self, cx: &AppContext) -> bool;
 }
 
 impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
@@ -726,10 +734,8 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
         })
     }
 
-    fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut WindowContext) {
-        self.update(cx, |this, cx| {
-            this.set_leader_replica_id(leader_replica_id, cx)
-        })
+    fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext) {
+        self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
     }
 
     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
@@ -765,6 +771,10 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
             false
         }
     }
+
+    fn is_project_item(&self, cx: &AppContext) -> bool {
+        self.read(cx).is_project_item(cx)
+    }
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/workspace/src/pane_group.rs 🔗

@@ -190,25 +190,23 @@ impl Member {
                     })
                     .and_then(|leader_id| {
                         let room = active_call?.read(cx).room()?.read(cx);
-                        let collaborator = project.read(cx).collaborators().get(leader_id)?;
-                        let participant = room.remote_participant_for_peer_id(*leader_id)?;
-                        Some((collaborator.replica_id, participant))
+                        room.remote_participant_for_peer_id(*leader_id)
                     });
 
-                let border = if let Some((replica_id, _)) = leader.as_ref() {
-                    let leader_color = theme.editor.replica_selection_style(*replica_id).cursor;
-                    let mut border = Border::all(theme.workspace.leader_border_width, leader_color);
-                    border
+                let mut leader_border = Border::default();
+                let mut leader_status_box = None;
+                if let Some(leader) = &leader {
+                    let leader_color = theme
+                        .editor
+                        .selection_style_for_room_participant(leader.participant_index.0)
+                        .cursor;
+                    leader_border = Border::all(theme.workspace.leader_border_width, leader_color);
+                    leader_border
                         .color
                         .fade_out(1. - theme.workspace.leader_border_opacity);
-                    border.overlay = true;
-                    border
-                } else {
-                    Border::default()
-                };
+                    leader_border.overlay = true;
 
-                let leader_status_box = if let Some((_, leader)) = leader {
-                    match leader.location {
+                    leader_status_box = match leader.location {
                         ParticipantLocation::SharedProject {
                             project_id: leader_project_id,
                         } => {
@@ -217,7 +215,6 @@ impl Member {
                             } else {
                                 let leader_user = leader.user.clone();
                                 let leader_user_id = leader.user.id;
-                                let app_state = Arc::downgrade(app_state);
                                 Some(
                                     MouseEventHandler::new::<FollowIntoExternalProject, _>(
                                         pane.id(),
@@ -225,7 +222,7 @@ impl Member {
                                         |_, _| {
                                             Label::new(
                                                 format!(
-                                                    "Follow {} on their active project",
+                                                    "Follow {} to their active project",
                                                     leader_user.github_login,
                                                 ),
                                                 theme
@@ -241,16 +238,14 @@ impl Member {
                                         },
                                     )
                                     .with_cursor_style(CursorStyle::PointingHand)
-                                    .on_click(MouseButton::Left, move |_, _, cx| {
-                                        if let Some(app_state) = app_state.upgrade() {
-                                            crate::join_remote_project(
-                                                leader_project_id,
-                                                leader_user_id,
-                                                app_state,
-                                                cx,
-                                            )
-                                            .detach_and_log_err(cx);
-                                        }
+                                    .on_click(MouseButton::Left, move |_, this, cx| {
+                                        crate::join_remote_project(
+                                            leader_project_id,
+                                            leader_user_id,
+                                            this.app_state().clone(),
+                                            cx,
+                                        )
+                                        .detach_and_log_err(cx);
                                     })
                                     .aligned()
                                     .bottom()
@@ -289,13 +284,11 @@ impl Member {
                             .right()
                             .into_any(),
                         ),
-                    }
-                } else {
-                    None
-                };
+                    };
+                }
 
                 Stack::new()
-                    .with_child(pane_element.contained().with_border(border))
+                    .with_child(pane_element.contained().with_border(leader_border))
                     .with_children(leader_status_box)
                     .into_any()
             }

crates/workspace/src/workspace.rs 🔗

@@ -375,11 +375,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
         })
         .detach();
     });
-
-    let client = &app_state.client;
-    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);
 }
 
 type ProjectItemBuilders = HashMap<
@@ -456,6 +451,7 @@ pub struct AppState {
     pub client: Arc<Client>,
     pub user_store: ModelHandle<UserStore>,
     pub channel_store: ModelHandle<ChannelStore>,
+    pub workspace_store: ModelHandle<WorkspaceStore>,
     pub fs: Arc<dyn fs::Fs>,
     pub build_window_options:
         fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
@@ -464,6 +460,19 @@ pub struct AppState {
     pub background_actions: BackgroundActions,
 }
 
+pub struct WorkspaceStore {
+    workspaces: HashSet<WeakViewHandle<Workspace>>,
+    followers: Vec<Follower>,
+    client: Arc<Client>,
+    _subscriptions: Vec<client::Subscription>,
+}
+
+#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
+struct Follower {
+    project_id: Option<u64>,
+    peer_id: PeerId,
+}
+
 impl AppState {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut AppContext) -> Arc<Self> {
@@ -480,6 +489,7 @@ impl AppState {
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
         let channel_store =
             cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+        let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
 
         theme::init((), cx);
         client::init(&client, cx);
@@ -491,6 +501,7 @@ impl AppState {
             languages,
             user_store,
             channel_store,
+            workspace_store,
             initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
             build_window_options: |_, _, _| Default::default(),
             background_actions: || &[],
@@ -551,7 +562,6 @@ pub enum Event {
 
 pub struct Workspace {
     weak_self: WeakViewHandle<Self>,
-    remote_entity_subscription: Option<client::Subscription>,
     modal: Option<ActiveModal>,
     zoomed: Option<AnyWeakViewHandle>,
     zoomed_position: Option<DockPosition>,
@@ -567,7 +577,6 @@ pub struct Workspace {
     titlebar_item: Option<AnyViewHandle>,
     notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
     project: ModelHandle<Project>,
-    leader_state: LeaderState,
     follower_states_by_leader: FollowerStatesByLeader,
     last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
     window_edited: bool,
@@ -593,11 +602,6 @@ pub struct ViewId {
     pub id: u64,
 }
 
-#[derive(Default)]
-struct LeaderState {
-    followers: HashSet<PeerId>,
-}
-
 type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
 
 #[derive(Default)]
@@ -618,9 +622,8 @@ impl Workspace {
         cx.observe(&project, |_, _, cx| cx.notify()).detach();
         cx.subscribe(&project, move |this, _, event, cx| {
             match event {
-                project::Event::RemoteIdChanged(remote_id) => {
+                project::Event::RemoteIdChanged(_) => {
                     this.update_window_title(cx);
-                    this.project_remote_id_changed(*remote_id, cx);
                 }
 
                 project::Event::CollaboratorLeft(peer_id) => {
@@ -675,6 +678,10 @@ impl Workspace {
         cx.focus(&center_pane);
         cx.emit(Event::PaneAdded(center_pane.clone()));
 
+        app_state.workspace_store.update(cx, |store, _| {
+            store.workspaces.insert(weak_handle.clone());
+        });
+
         let mut current_user = app_state.user_store.read(cx).watch_current_user();
         let mut connection_status = app_state.client.status();
         let _observe_current_user = cx.spawn(|this, mut cx| async move {
@@ -768,7 +775,8 @@ impl Workspace {
             }),
         ];
 
-        let mut this = Workspace {
+        cx.defer(|this, cx| this.update_window_title(cx));
+        Workspace {
             weak_self: weak_handle.clone(),
             modal: None,
             zoomed: None,
@@ -781,12 +789,10 @@ impl Workspace {
             status_bar,
             titlebar_item: None,
             notifications: Default::default(),
-            remote_entity_subscription: None,
             left_dock,
             bottom_dock,
             right_dock,
             project: project.clone(),
-            leader_state: Default::default(),
             follower_states_by_leader: Default::default(),
             last_leaders_by_pane: Default::default(),
             window_edited: false,
@@ -799,10 +805,7 @@ impl Workspace {
             leader_updates_tx,
             subscriptions,
             pane_history_timestamp,
-        };
-        this.project_remote_id_changed(project.read(cx).remote_id(), cx);
-        cx.defer(|this, cx| this.update_window_title(cx));
-        this
+        }
     }
 
     fn new_local(
@@ -2506,43 +2509,24 @@ impl Workspace {
         &self.active_pane
     }
 
-    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.app_state
-                    .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() {
-                    item.set_leader_replica_id(None, cx);
+                    item.set_leader_peer_id(None, cx);
                 }
             }
         }
         cx.notify();
     }
 
-    pub fn toggle_follow(
+    fn start_following(
         &mut self,
         leader_id: PeerId,
         cx: &mut ViewContext<Self>,
     ) -> Option<Task<Result<()>>> {
         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
@@ -2551,8 +2535,10 @@ impl Workspace {
             .insert(pane.clone(), Default::default());
         cx.notify();
 
-        let project_id = self.project.read(cx).remote_id()?;
+        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+        let project_id = self.project.read(cx).remote_id();
         let request = self.app_state.client.request(proto::Follow {
+            room_id,
             project_id,
             leader_id: Some(leader_id),
         });
@@ -2611,9 +2597,64 @@ impl Workspace {
             None
         };
 
-        next_leader_id
-            .or_else(|| collaborators.keys().copied().next())
-            .and_then(|leader_id| self.toggle_follow(leader_id, cx))
+        let pane = self.active_pane.clone();
+        let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
+        else {
+            return None;
+        };
+        if Some(leader_id) == self.unfollow(&pane, cx) {
+            return None;
+        }
+        self.follow(leader_id, cx)
+    }
+
+    pub fn follow(
+        &mut self,
+        leader_id: PeerId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
+        let project = self.project.read(cx);
+
+        let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
+            return None;
+        };
+
+        let other_project_id = match remote_participant.location {
+            call::ParticipantLocation::External => None,
+            call::ParticipantLocation::UnsharedProject => None,
+            call::ParticipantLocation::SharedProject { project_id } => {
+                if Some(project_id) == project.remote_id() {
+                    None
+                } else {
+                    Some(project_id)
+                }
+            }
+        };
+
+        // if they are active in another project, follow there.
+        if let Some(project_id) = other_project_id {
+            let app_state = self.app_state.clone();
+            return Some(crate::join_remote_project(
+                project_id,
+                remote_participant.user.id,
+                app_state,
+                cx,
+            ));
+        }
+
+        // if you're already following, find the right pane and focus it.
+        for (existing_leader_id, states_by_pane) in &mut self.follower_states_by_leader {
+            if leader_id == *existing_leader_id {
+                for (pane, _) in states_by_pane {
+                    cx.focus(pane);
+                    return None;
+                }
+            }
+        }
+
+        // Otherwise, follow.
+        self.start_following(leader_id, cx)
     }
 
     pub fn unfollow(
@@ -2625,20 +2666,21 @@ impl Workspace {
             let leader_id = *leader_id;
             if let Some(state) = states_by_pane.remove(pane) {
                 for (_, item) in state.items_by_leader_view_id {
-                    item.set_leader_replica_id(None, cx);
+                    item.set_leader_peer_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.app_state
-                            .client
-                            .send(proto::Unfollow {
-                                project_id,
-                                leader_id: Some(leader_id),
-                            })
-                            .log_err();
-                    }
+                    let project_id = self.project.read(cx).remote_id();
+                    let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+                    self.app_state
+                        .client
+                        .send(proto::Unfollow {
+                            room_id,
+                            project_id,
+                            leader_id: Some(leader_id),
+                        })
+                        .log_err();
                 }
 
                 cx.notify();
@@ -2652,10 +2694,6 @@ impl Workspace {
         self.follower_states_by_leader.contains_key(&peer_id)
     }
 
-    pub fn is_followed_by(&self, peer_id: PeerId) -> bool {
-        self.leader_state.followers.contains(&peer_id)
-    }
-
     fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         // TODO: There should be a better system in place for this
         // (https://github.com/zed-industries/zed/issues/1290)
@@ -2806,81 +2844,64 @@ impl Workspace {
 
     // RPC handlers
 
-    async fn handle_follow(
-        this: WeakViewHandle<Self>,
-        envelope: TypedEnvelope<proto::Follow>,
-        _: Arc<Client>,
-        mut cx: AsyncAppContext,
-    ) -> Result<proto::FollowResponse> {
-        this.update(&mut cx, |this, cx| {
-            let client = &this.app_state.client;
-            this.leader_state
-                .followers
-                .insert(envelope.original_sender_id()?);
+    fn handle_follow(
+        &mut self,
+        follower_project_id: Option<u64>,
+        cx: &mut ViewContext<Self>,
+    ) -> proto::FollowResponse {
+        let client = &self.app_state.client;
+        let project_id = self.project.read(cx).remote_id();
 
-            let active_view_id = this.active_item(cx).and_then(|i| {
-                Some(
-                    i.to_followable_item_handle(cx)?
-                        .remote_id(client, cx)?
-                        .to_proto(),
-                )
-            });
+        let active_view_id = self.active_item(cx).and_then(|i| {
+            Some(
+                i.to_followable_item_handle(cx)?
+                    .remote_id(client, cx)?
+                    .to_proto(),
+            )
+        });
 
-            cx.notify();
+        cx.notify();
 
-            Ok(proto::FollowResponse {
-                active_view_id,
-                views: this
-                    .panes()
-                    .iter()
-                    .flat_map(|pane| {
-                        let leader_id = this.leader_for_pane(pane);
-                        pane.read(cx).items().filter_map({
-                            let cx = &cx;
-                            move |item| {
-                                let item = item.to_followable_item_handle(cx)?;
-                                let id = item.remote_id(client, cx)?.to_proto();
-                                let variant = item.to_state_proto(cx)?;
-                                Some(proto::View {
-                                    id: Some(id),
-                                    leader_id,
-                                    variant: Some(variant),
-                                })
+        proto::FollowResponse {
+            active_view_id,
+            views: self
+                .panes()
+                .iter()
+                .flat_map(|pane| {
+                    let leader_id = self.leader_for_pane(pane);
+                    pane.read(cx).items().filter_map({
+                        let cx = &cx;
+                        move |item| {
+                            let item = item.to_followable_item_handle(cx)?;
+                            if project_id.is_some()
+                                && project_id != follower_project_id
+                                && item.is_project_item(cx)
+                            {
+                                return None;
                             }
-                        })
+                            let id = item.remote_id(client, cx)?.to_proto();
+                            let variant = item.to_state_proto(cx)?;
+                            Some(proto::View {
+                                id: Some(id),
+                                leader_id,
+                                variant: Some(variant),
+                            })
+                        }
                     })
-                    .collect(),
-            })
-        })?
-    }
-
-    async fn handle_unfollow(
-        this: WeakViewHandle<Self>,
-        envelope: TypedEnvelope<proto::Unfollow>,
-        _: Arc<Client>,
-        mut cx: AsyncAppContext,
-    ) -> Result<()> {
-        this.update(&mut cx, |this, cx| {
-            this.leader_state
-                .followers
-                .remove(&envelope.original_sender_id()?);
-            cx.notify();
-            Ok(())
-        })?
+                })
+                .collect(),
+        }
     }
 
-    async fn handle_update_followers(
-        this: WeakViewHandle<Self>,
-        envelope: TypedEnvelope<proto::UpdateFollowers>,
-        _: Arc<Client>,
-        cx: AsyncAppContext,
-    ) -> Result<()> {
-        let leader_id = envelope.original_sender_id()?;
-        this.read_with(&cx, |this, _| {
-            this.leader_updates_tx
-                .unbounded_send((leader_id, envelope.payload))
-        })??;
-        Ok(())
+    fn handle_update_followers(
+        &mut self,
+        leader_id: PeerId,
+        message: proto::UpdateFollowers,
+        _cx: &mut ViewContext<Self>,
+    ) {
+        self.leader_updates_tx
+            .unbounded_send((leader_id, message))
+            .ok();
     }
 
     async fn process_leader_update(
@@ -2953,18 +2974,6 @@ impl Workspace {
         let this = this
             .upgrade(cx)
             .ok_or_else(|| anyhow!("workspace dropped"))?;
-        let project = this
-            .read_with(cx, |this, _| this.project.clone())
-            .ok_or_else(|| anyhow!("window dropped"))?;
-
-        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>()
@@ -3009,7 +3018,7 @@ impl Workspace {
                     .get_mut(&pane)?;
 
                 for (id, item) in leader_view_ids.into_iter().zip(items) {
-                    item.set_leader_replica_id(Some(replica_id), cx);
+                    item.set_leader_peer_id(Some(leader_id), cx);
                     state.items_by_leader_view_id.insert(id, item);
                 }
 
@@ -3020,46 +3029,44 @@ impl Workspace {
     }
 
     fn update_active_view_for_followers(&self, cx: &AppContext) {
+        let mut is_project_item = true;
+        let mut update = proto::UpdateActiveView::default();
         if self.active_pane.read(cx).has_focus() {
-            self.update_followers(
-                proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
-                    id: self.active_item(cx).and_then(|item| {
-                        item.to_followable_item_handle(cx)?
-                            .remote_id(&self.app_state.client, cx)
-                            .map(|id| id.to_proto())
-                    }),
+            let item = self
+                .active_item(cx)
+                .and_then(|item| item.to_followable_item_handle(cx));
+            if let Some(item) = item {
+                is_project_item = item.is_project_item(cx);
+                update = proto::UpdateActiveView {
+                    id: item
+                        .remote_id(&self.app_state.client, cx)
+                        .map(|id| id.to_proto()),
                     leader_id: self.leader_for_pane(&self.active_pane),
-                }),
-                cx,
-            );
-        } else {
-            self.update_followers(
-                proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
-                    id: None,
-                    leader_id: None,
-                }),
-                cx,
-            );
+                };
+            }
         }
+
+        self.update_followers(
+            is_project_item,
+            proto::update_followers::Variant::UpdateActiveView(update),
+            cx,
+        );
     }
 
     fn update_followers(
         &self,
+        project_only: bool,
         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.app_state
-                .client
-                .send(proto::UpdateFollowers {
-                    project_id,
-                    follower_ids: self.leader_state.followers.iter().copied().collect(),
-                    variant: Some(update),
-                })
-                .log_err();
-        }
-        None
+        let project_id = if project_only {
+            self.project.read(cx).remote_id()
+        } else {
+            None
+        };
+        self.app_state().workspace_store.read_with(cx, |store, cx| {
+            store.update_followers(project_id, update, cx)
+        })
     }
 
     pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
@@ -3081,31 +3088,39 @@ impl Workspace {
         let room = call.read(cx).room()?.read(cx);
         let participant = room.remote_participant_for_peer_id(leader_id)?;
         let mut items_to_activate = Vec::new();
+
+        let leader_in_this_app;
+        let leader_in_this_project;
         match participant.location {
             call::ParticipantLocation::SharedProject { project_id } => {
-                if Some(project_id) == self.project.read(cx).remote_id() {
-                    for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
-                        if let Some(item) = state
-                            .active_view_id
-                            .and_then(|id| state.items_by_leader_view_id.get(&id))
-                        {
-                            items_to_activate.push((pane.clone(), item.boxed_clone()));
-                        } else if let Some(shared_screen) =
-                            self.shared_screen_for_peer(leader_id, pane, cx)
-                        {
-                            items_to_activate.push((pane.clone(), Box::new(shared_screen)));
-                        }
-                    }
-                }
+                leader_in_this_app = true;
+                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
+            }
+            call::ParticipantLocation::UnsharedProject => {
+                leader_in_this_app = true;
+                leader_in_this_project = false;
             }
-            call::ParticipantLocation::UnsharedProject => {}
             call::ParticipantLocation::External => {
-                for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
-                    if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
-                        items_to_activate.push((pane.clone(), Box::new(shared_screen)));
+                leader_in_this_app = false;
+                leader_in_this_project = false;
+            }
+        };
+
+        for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
+            if leader_in_this_app {
+                let item = state
+                    .active_view_id
+                    .and_then(|id| state.items_by_leader_view_id.get(&id));
+                if let Some(item) = item {
+                    if leader_in_this_project || !item.is_project_item(cx) {
+                        items_to_activate.push((pane.clone(), item.boxed_clone()));
                     }
+                    continue;
                 }
             }
+            if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
+                items_to_activate.push((pane.clone(), Box::new(shared_screen)));
+            }
         }
 
         for (pane, item) in items_to_activate {
@@ -3149,6 +3164,7 @@ impl Workspace {
 
     pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
         if active {
+            self.update_active_view_for_followers(cx);
             cx.background()
                 .spawn(persistence::DB.update_timestamp(self.database_id()))
                 .detach();
@@ -3522,8 +3538,10 @@ impl Workspace {
 
         let channel_store =
             cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+        let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
         let app_state = Arc::new(AppState {
             languages: project.read(cx).languages().clone(),
+            workspace_store,
             client,
             user_store,
             channel_store,
@@ -3767,6 +3785,12 @@ fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut Asy
 
 impl Entity for Workspace {
     type Event = Event;
+
+    fn release(&mut self, cx: &mut AppContext) {
+        self.app_state.workspace_store.update(cx, |store, _| {
+            store.workspaces.remove(&self.weak_self);
+        })
+    }
 }
 
 impl View for Workspace {
@@ -3909,6 +3933,151 @@ impl View for Workspace {
     }
 }
 
+impl WorkspaceStore {
+    pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
+        Self {
+            workspaces: Default::default(),
+            followers: Default::default(),
+            _subscriptions: vec![
+                client.add_request_handler(cx.handle(), Self::handle_follow),
+                client.add_message_handler(cx.handle(), Self::handle_unfollow),
+                client.add_message_handler(cx.handle(), Self::handle_update_followers),
+            ],
+            client,
+        }
+    }
+
+    pub fn update_followers(
+        &self,
+        project_id: Option<u64>,
+        update: proto::update_followers::Variant,
+        cx: &AppContext,
+    ) -> Option<()> {
+        if !cx.has_global::<ModelHandle<ActiveCall>>() {
+            return None;
+        }
+
+        let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
+        let follower_ids: Vec<_> = self
+            .followers
+            .iter()
+            .filter_map(|follower| {
+                if follower.project_id == project_id || project_id.is_none() {
+                    Some(follower.peer_id.into())
+                } else {
+                    None
+                }
+            })
+            .collect();
+        if follower_ids.is_empty() {
+            return None;
+        }
+        self.client
+            .send(proto::UpdateFollowers {
+                room_id,
+                project_id,
+                follower_ids,
+                variant: Some(update),
+            })
+            .log_err()
+    }
+
+    async fn handle_follow(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::Follow>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::FollowResponse> {
+        this.update(&mut cx, |this, cx| {
+            let follower = Follower {
+                project_id: envelope.payload.project_id,
+                peer_id: envelope.original_sender_id()?,
+            };
+            let active_project = ActiveCall::global(cx)
+                .read(cx)
+                .location()
+                .map(|project| project.id());
+
+            let mut response = proto::FollowResponse::default();
+            for workspace in &this.workspaces {
+                let Some(workspace) = workspace.upgrade(cx) else {
+                    continue;
+                };
+
+                workspace.update(cx.as_mut(), |workspace, cx| {
+                    let handler_response = workspace.handle_follow(follower.project_id, cx);
+                    if response.views.is_empty() {
+                        response.views = handler_response.views;
+                    } else {
+                        response.views.extend_from_slice(&handler_response.views);
+                    }
+
+                    if let Some(active_view_id) = handler_response.active_view_id.clone() {
+                        if response.active_view_id.is_none()
+                            || Some(workspace.project.id()) == active_project
+                        {
+                            response.active_view_id = Some(active_view_id);
+                        }
+                    }
+                });
+            }
+
+            if let Err(ix) = this.followers.binary_search(&follower) {
+                this.followers.insert(ix, follower);
+            }
+
+            Ok(response)
+        })
+    }
+
+    async fn handle_unfollow(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::Unfollow>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, _| {
+            let follower = Follower {
+                project_id: envelope.payload.project_id,
+                peer_id: envelope.original_sender_id()?,
+            };
+            if let Ok(ix) = this.followers.binary_search(&follower) {
+                this.followers.remove(ix);
+            }
+            Ok(())
+        })
+    }
+
+    async fn handle_update_followers(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::UpdateFollowers>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        let leader_id = envelope.original_sender_id()?;
+        let update = envelope.payload;
+        this.update(&mut cx, |this, cx| {
+            for workspace in &this.workspaces {
+                let Some(workspace) = workspace.upgrade(cx) else {
+                    continue;
+                };
+                workspace.update(cx.as_mut(), |workspace, cx| {
+                    let project_id = workspace.project.read(cx).remote_id();
+                    if update.project_id != project_id && update.project_id.is_some() {
+                        return;
+                    }
+                    workspace.handle_update_followers(leader_id, update.clone(), cx);
+                });
+            }
+            Ok(())
+        })
+    }
+}
+
+impl Entity for WorkspaceStore {
+    type Event = ();
+}
+
 impl ViewId {
     pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
         Ok(Self {
@@ -4077,21 +4246,20 @@ pub fn join_remote_project(
     cx: &mut AppContext,
 ) -> Task<Result<()>> {
     cx.spawn(|mut cx| async move {
-        let existing_workspace = cx
-            .windows()
-            .into_iter()
-            .find_map(|window| {
-                window.downcast::<Workspace>().and_then(|window| {
-                    window.read_root_with(&cx, |workspace, cx| {
+        let windows = cx.windows();
+        let existing_workspace = windows.into_iter().find_map(|window| {
+            window.downcast::<Workspace>().and_then(|window| {
+                window
+                    .read_root_with(&cx, |workspace, cx| {
                         if workspace.project().read(cx).remote_id() == Some(project_id) {
                             Some(cx.handle().downgrade())
                         } else {
                             None
                         }
                     })
-                })
+                    .unwrap_or(None)
             })
-            .flatten();
+        });
 
         let workspace = if let Some(existing_workspace) = existing_workspace {
             existing_workspace
@@ -4156,11 +4324,9 @@ pub fn join_remote_project(
                     });
 
                 if let Some(follow_peer_id) = follow_peer_id {
-                    if !workspace.is_being_followed(follow_peer_id) {
-                        workspace
-                            .toggle_follow(follow_peer_id, cx)
-                            .map(|follow| follow.detach_and_log_err(cx));
-                    }
+                    workspace
+                        .follow(follow_peer_id, cx)
+                        .map(|follow| follow.detach_and_log_err(cx));
                 }
             }
         })?;

crates/zed/src/languages.rs 🔗

@@ -6,12 +6,11 @@ use rust_embed::RustEmbed;
 use std::{borrow::Cow, str, sync::Arc};
 use util::asset_str;
 
-use self::elixir_next::ElixirSettings;
+use self::elixir::ElixirSettings;
 
 mod c;
 mod css;
 mod elixir;
-mod elixir_next;
 mod go;
 mod html;
 mod json;
@@ -46,7 +45,7 @@ pub fn init(
     node_runtime: Arc<dyn NodeRuntime>,
     cx: &mut AppContext,
 ) {
-    settings::register::<elixir_next::ElixirSettings>(cx);
+    settings::register::<elixir::ElixirSettings>(cx);
 
     let language = |name, grammar, adapters| {
         languages.register(name, load_config(name), grammar, adapters, load_queries)
@@ -72,21 +71,21 @@ pub fn init(
         ],
     );
 
-    match &settings::get::<ElixirSettings>(cx).next {
-        elixir_next::ElixirNextSetting::Off => language(
+    match &settings::get::<ElixirSettings>(cx).lsp {
+        elixir::ElixirLspSetting::ElixirLs => language(
             "elixir",
             tree_sitter_elixir::language(),
             vec![Arc::new(elixir::ElixirLspAdapter)],
         ),
-        elixir_next::ElixirNextSetting::On => language(
+        elixir::ElixirLspSetting::NextLs => language(
             "elixir",
             tree_sitter_elixir::language(),
-            vec![Arc::new(elixir_next::NextLspAdapter)],
+            vec![Arc::new(elixir::NextLspAdapter)],
         ),
-        elixir_next::ElixirNextSetting::Local { path, arguments } => language(
+        elixir::ElixirLspSetting::Local { path, arguments } => language(
             "elixir",
             tree_sitter_elixir::language(),
-            vec![Arc::new(elixir_next::LocalNextLspAdapter {
+            vec![Arc::new(elixir::LocalLspAdapter {
                 path: path.clone(),
                 arguments: arguments.clone(),
             })],

crates/zed/src/languages/elixir.rs 🔗

@@ -1,12 +1,17 @@
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use async_trait::async_trait;
 use futures::StreamExt;
 use gpui::{AsyncAppContext, Task};
 pub use language::*;
 use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::Setting;
 use smol::fs::{self, File};
 use std::{
     any::Any,
+    env::consts,
+    ops::Deref,
     path::PathBuf,
     sync::{
         atomic::{AtomicBool, Ordering::SeqCst},
@@ -14,11 +19,50 @@ use std::{
     },
 };
 use util::{
+    async_iife,
     fs::remove_matching,
     github::{latest_github_release, GitHubLspBinaryVersion},
     ResultExt,
 };
 
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct ElixirSettings {
+    pub lsp: ElixirLspSetting,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ElixirLspSetting {
+    ElixirLs,
+    NextLs,
+    Local {
+        path: String,
+        arguments: Vec<String>,
+    },
+}
+
+#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
+pub struct ElixirSettingsContent {
+    lsp: Option<ElixirLspSetting>,
+}
+
+impl Setting for ElixirSettings {
+    const KEY: Option<&'static str> = Some("elixir");
+
+    type FileContent = ElixirSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> Result<Self>
+    where
+        Self: Sized,
+    {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
 pub struct ElixirLspAdapter;
 
 #[async_trait]
@@ -144,14 +188,14 @@ impl LspAdapter for ElixirLspAdapter {
         container_dir: PathBuf,
         _: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir).await
+        get_cached_server_binary_elixir_ls(container_dir).await
     }
 
     async fn installation_test_binary(
         &self,
         container_dir: PathBuf,
     ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir).await
+        get_cached_server_binary_elixir_ls(container_dir).await
     }
 
     async fn label_for_completion(
@@ -238,7 +282,9 @@ impl LspAdapter for ElixirLspAdapter {
     }
 }
 
-async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+async fn get_cached_server_binary_elixir_ls(
+    container_dir: PathBuf,
+) -> Option<LanguageServerBinary> {
     (|| async move {
         let mut last = None;
         let mut entries = fs::read_dir(&container_dir).await?;
@@ -254,3 +300,247 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
     .await
     .log_err()
 }
+
+pub struct NextLspAdapter;
+
+#[async_trait]
+impl LspAdapter for NextLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("next-ls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "next-ls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release =
+            latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?;
+        let version = release.name.clone();
+        let platform = match consts::ARCH {
+            "x86_64" => "darwin_arm64",
+            "aarch64" => "darwin_amd64",
+            other => bail!("Running on unsupported platform: {other}"),
+        };
+        let asset_name = format!("next_ls_{}", platform);
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        let version = GitHubLspBinaryVersion {
+            name: version,
+            url: asset.browser_download_url.clone(),
+        };
+        Ok(Box::new(version) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+
+        let binary_path = container_dir.join("next-ls");
+
+        if fs::metadata(&binary_path).await.is_err() {
+            let mut response = delegate
+                .http_client()
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+
+            let mut file = smol::fs::File::create(&binary_path).await?;
+            if !response.status().is_success() {
+                Err(anyhow!(
+                    "download failed with status {}",
+                    response.status().to_string()
+                ))?;
+            }
+            futures::io::copy(response.body_mut(), &mut file).await?;
+
+            fs::set_permissions(
+                &binary_path,
+                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
+            )
+            .await?;
+        }
+
+        Ok(LanguageServerBinary {
+            path: binary_path,
+            arguments: vec!["--stdio".into()],
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary_next(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--stdio".into()];
+                binary
+            })
+    }
+
+    async fn installation_test_binary(
+        &self,
+        container_dir: PathBuf,
+    ) -> Option<LanguageServerBinary> {
+        get_cached_server_binary_next(container_dir)
+            .await
+            .map(|mut binary| {
+                binary.arguments = vec!["--help".into()];
+                binary
+            })
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_completion_elixir(completion, language)
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        symbol_kind: SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_symbol_elixir(name, symbol_kind, language)
+    }
+}
+
+async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<LanguageServerBinary> {
+    async_iife!({
+        let mut last_binary_path = None;
+        let mut entries = fs::read_dir(&container_dir).await?;
+        while let Some(entry) = entries.next().await {
+            let entry = entry?;
+            if entry.file_type().await?.is_file()
+                && entry
+                    .file_name()
+                    .to_str()
+                    .map_or(false, |name| name == "next-ls")
+            {
+                last_binary_path = Some(entry.path());
+            }
+        }
+
+        if let Some(path) = last_binary_path {
+            Ok(LanguageServerBinary {
+                path,
+                arguments: Vec::new(),
+            })
+        } else {
+            Err(anyhow!("no cached binary"))
+        }
+    })
+    .await
+    .log_err()
+}
+
+pub struct LocalLspAdapter {
+    pub path: String,
+    pub arguments: Vec<String>,
+}
+
+#[async_trait]
+impl LspAdapter for LocalLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("local-ls".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "local-ls"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        Ok(Box::new(()) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _: Box<dyn 'static + Send + Any>,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path)?;
+        Ok(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path).ok()?;
+        Some(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        let path = shellexpand::full(&self.path).ok()?;
+        Some(LanguageServerBinary {
+            path: PathBuf::from(path.deref()),
+            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
+        })
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_completion_elixir(completion, language)
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        symbol: SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        label_for_symbol_elixir(name, symbol, language)
+    }
+}
+
+fn label_for_completion_elixir(
+    completion: &lsp::CompletionItem,
+    language: &Arc<Language>,
+) -> Option<CodeLabel> {
+    return Some(CodeLabel {
+        runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()),
+        text: completion.label.clone(),
+        filter_range: 0..completion.label.len(),
+    });
+}
+
+fn label_for_symbol_elixir(
+    name: &str,
+    _: SymbolKind,
+    language: &Arc<Language>,
+) -> Option<CodeLabel> {
+    Some(CodeLabel {
+        runs: language.highlight_text(&name.into(), 0..name.len()),
+        text: name.to_string(),
+        filter_range: 0..name.len(),
+    })
+}

crates/zed/src/languages/elixir_next.rs 🔗

@@ -1,266 +0,0 @@
-use anyhow::{anyhow, bail, Result};
-
-use async_trait::async_trait;
-pub use language::*;
-use lsp::{LanguageServerBinary, SymbolKind};
-use schemars::JsonSchema;
-use serde_derive::{Deserialize, Serialize};
-use settings::Setting;
-use smol::{fs, stream::StreamExt};
-use std::{any::Any, env::consts, ops::Deref, path::PathBuf, sync::Arc};
-use util::{
-    async_iife,
-    github::{latest_github_release, GitHubLspBinaryVersion},
-    ResultExt,
-};
-
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
-pub struct ElixirSettings {
-    pub next: ElixirNextSetting,
-}
-
-#[derive(Clone, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum ElixirNextSetting {
-    Off,
-    On,
-    Local {
-        path: String,
-        arguments: Vec<String>,
-    },
-}
-
-#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
-pub struct ElixirSettingsContent {
-    next: Option<ElixirNextSetting>,
-}
-
-impl Setting for ElixirSettings {
-    const KEY: Option<&'static str> = Some("elixir");
-
-    type FileContent = ElixirSettingsContent;
-
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &gpui::AppContext,
-    ) -> Result<Self>
-    where
-        Self: Sized,
-    {
-        Self::load_via_json_merge(default_value, user_values)
-    }
-}
-
-pub struct NextLspAdapter;
-
-#[async_trait]
-impl LspAdapter for NextLspAdapter {
-    async fn name(&self) -> LanguageServerName {
-        LanguageServerName("next-ls".into())
-    }
-
-    fn short_name(&self) -> &'static str {
-        "next-ls"
-    }
-
-    async fn fetch_latest_server_version(
-        &self,
-        delegate: &dyn LspAdapterDelegate,
-    ) -> Result<Box<dyn 'static + Send + Any>> {
-        let release =
-            latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?;
-        let version = release.name.clone();
-        let platform = match consts::ARCH {
-            "x86_64" => "darwin_arm64",
-            "aarch64" => "darwin_amd64",
-            other => bail!("Running on unsupported platform: {other}"),
-        };
-        let asset_name = format!("next_ls_{}", platform);
-        let asset = release
-            .assets
-            .iter()
-            .find(|asset| asset.name == asset_name)
-            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
-        let version = GitHubLspBinaryVersion {
-            name: version,
-            url: asset.browser_download_url.clone(),
-        };
-        Ok(Box::new(version) as Box<_>)
-    }
-
-    async fn fetch_server_binary(
-        &self,
-        version: Box<dyn 'static + Send + Any>,
-        container_dir: PathBuf,
-        delegate: &dyn LspAdapterDelegate,
-    ) -> Result<LanguageServerBinary> {
-        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
-
-        let binary_path = container_dir.join("next-ls");
-
-        if fs::metadata(&binary_path).await.is_err() {
-            let mut response = delegate
-                .http_client()
-                .get(&version.url, Default::default(), true)
-                .await
-                .map_err(|err| anyhow!("error downloading release: {}", err))?;
-
-            let mut file = smol::fs::File::create(&binary_path).await?;
-            if !response.status().is_success() {
-                Err(anyhow!(
-                    "download failed with status {}",
-                    response.status().to_string()
-                ))?;
-            }
-            futures::io::copy(response.body_mut(), &mut file).await?;
-
-            fs::set_permissions(
-                &binary_path,
-                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
-            )
-            .await?;
-        }
-
-        Ok(LanguageServerBinary {
-            path: binary_path,
-            arguments: vec!["--stdio".into()],
-        })
-    }
-
-    async fn cached_server_binary(
-        &self,
-        container_dir: PathBuf,
-        _: &dyn LspAdapterDelegate,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir)
-            .await
-            .map(|mut binary| {
-                binary.arguments = vec!["--stdio".into()];
-                binary
-            })
-    }
-
-    async fn installation_test_binary(
-        &self,
-        container_dir: PathBuf,
-    ) -> Option<LanguageServerBinary> {
-        get_cached_server_binary(container_dir)
-            .await
-            .map(|mut binary| {
-                binary.arguments = vec!["--help".into()];
-                binary
-            })
-    }
-
-    async fn label_for_symbol(
-        &self,
-        name: &str,
-        symbol_kind: SymbolKind,
-        language: &Arc<Language>,
-    ) -> Option<CodeLabel> {
-        label_for_symbol_next(name, symbol_kind, language)
-    }
-}
-
-async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
-    async_iife!({
-        let mut last_binary_path = None;
-        let mut entries = fs::read_dir(&container_dir).await?;
-        while let Some(entry) = entries.next().await {
-            let entry = entry?;
-            if entry.file_type().await?.is_file()
-                && entry
-                    .file_name()
-                    .to_str()
-                    .map_or(false, |name| name == "next-ls")
-            {
-                last_binary_path = Some(entry.path());
-            }
-        }
-
-        if let Some(path) = last_binary_path {
-            Ok(LanguageServerBinary {
-                path,
-                arguments: Vec::new(),
-            })
-        } else {
-            Err(anyhow!("no cached binary"))
-        }
-    })
-    .await
-    .log_err()
-}
-
-pub struct LocalNextLspAdapter {
-    pub path: String,
-    pub arguments: Vec<String>,
-}
-
-#[async_trait]
-impl LspAdapter for LocalNextLspAdapter {
-    async fn name(&self) -> LanguageServerName {
-        LanguageServerName("local-next-ls".into())
-    }
-
-    fn short_name(&self) -> &'static str {
-        "next-ls"
-    }
-
-    async fn fetch_latest_server_version(
-        &self,
-        _: &dyn LspAdapterDelegate,
-    ) -> Result<Box<dyn 'static + Send + Any>> {
-        Ok(Box::new(()) as Box<_>)
-    }
-
-    async fn fetch_server_binary(
-        &self,
-        _: Box<dyn 'static + Send + Any>,
-        _: PathBuf,
-        _: &dyn LspAdapterDelegate,
-    ) -> Result<LanguageServerBinary> {
-        let path = shellexpand::full(&self.path)?;
-        Ok(LanguageServerBinary {
-            path: PathBuf::from(path.deref()),
-            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
-        })
-    }
-
-    async fn cached_server_binary(
-        &self,
-        _: PathBuf,
-        _: &dyn LspAdapterDelegate,
-    ) -> Option<LanguageServerBinary> {
-        let path = shellexpand::full(&self.path).ok()?;
-        Some(LanguageServerBinary {
-            path: PathBuf::from(path.deref()),
-            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
-        })
-    }
-
-    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
-        let path = shellexpand::full(&self.path).ok()?;
-        Some(LanguageServerBinary {
-            path: PathBuf::from(path.deref()),
-            arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
-        })
-    }
-
-    async fn label_for_symbol(
-        &self,
-        name: &str,
-        symbol: SymbolKind,
-        language: &Arc<Language>,
-    ) -> Option<CodeLabel> {
-        label_for_symbol_next(name, symbol, language)
-    }
-}
-
-fn label_for_symbol_next(name: &str, _: SymbolKind, language: &Arc<Language>) -> Option<CodeLabel> {
-    Some(CodeLabel {
-        runs: language.highlight_text(&name.into(), 0..name.len()),
-        text: name.to_string(),
-        filter_range: 0..name.len(),
-    })
-}

crates/zed/src/languages/rust/embedding.scm 🔗

@@ -2,6 +2,7 @@
     [(line_comment) (attribute_item)]* @context
     .
     [
+
         (struct_item
             name: (_) @name)
 
@@ -26,3 +27,6 @@
             name: (_) @name)
         ] @item
     )
+
+(attribute_item) @collapse
+(use_declaration) @collapse

crates/zed/src/main.rs 🔗

@@ -54,7 +54,7 @@ use welcome::{show_welcome_experience, FIRST_OPEN};
 
 use fs::RealFs;
 use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
-use workspace::AppState;
+use workspace::{AppState, WorkspaceStore};
 use zed::{
     assets::Assets,
     build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
@@ -139,6 +139,7 @@ fn main() {
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
         let channel_store =
             cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+        let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
 
         cx.set_global(client.clone());
 
@@ -176,7 +177,7 @@ fn main() {
         })
         .detach();
 
-        client.telemetry().start(installation_id);
+        client.telemetry().start(installation_id, cx);
 
         let app_state = Arc::new(AppState {
             languages,
@@ -187,6 +188,7 @@ fn main() {
             build_window_options,
             initialize_workspace,
             background_actions,
+            workspace_store,
         });
         cx.set_global(Arc::downgrade(&app_state));
 

script/start-local-collaboration 🔗

@@ -44,6 +44,7 @@ position_2=${half_width},${y}
 
 # Authenticate using the collab server's admin secret.
 export ZED_STATELESS=1
+export ZED_ALWAYS_ACTIVE=1
 export ZED_ADMIN_API_TOKEN=secret
 export ZED_SERVER_URL=http://localhost:8080
 export ZED_WINDOW_SIZE=${half_width},${height}

styles/src/style_tree/titlebar.ts 🔗

@@ -1,4 +1,4 @@
-import { icon_button, toggleable_icon_button, toggleable_text_button } from "../component"
+import { icon_button, text_button, toggleable_icon_button, toggleable_text_button } from "../component"
 import { interactive, toggleable } from "../element"
 import { useTheme, with_opacity } from "../theme"
 import { background, border, foreground, text } from "./components"
@@ -191,6 +191,12 @@ export function titlebar(): any {
             color: "variant",
         }),
 
+        project_host: text_button({
+            text_properties: {
+                weight: "bold"
+            }
+        }),
+
         // Collaborators
         leader_avatar: {
             width: avatar_width,