Merge branch 'main' into links

Conrad Irwin created

Change summary

Cargo.lock                                                   |  10 
Dockerfile                                                   |   2 
README.md                                                    |   4 
crates/call/Cargo.toml                                       |   1 
crates/call/src/call.rs                                      |   3 
crates/call/src/room.rs                                      |  40 
crates/channel/src/channel.rs                                |  10 
crates/channel/src/channel_store.rs                          |  11 
crates/channel/src/channel_store_tests.rs                    |   4 
crates/client/src/client.rs                                  |  18 
crates/client/src/telemetry.rs                               |  45 
crates/collab/Cargo.toml                                     |   2 
crates/collab/src/db/queries/messages.rs                     |   3 
crates/collab/src/db/tests/message_tests.rs                  |  67 
crates/collab/src/rpc.rs                                     |  16 
crates/collab/src/tests/following_tests.rs                   | 697 ++++-
crates/collab/src/tests/test_server.rs                       |  39 
crates/collab_ui/src/channel_view.rs                         |   2 
crates/collab_ui/src/chat_panel.rs                           |   2 
crates/collab_ui/src/collab_panel.rs                         |   2 
crates/file_finder/src/file_finder.rs                        | 127 +
crates/fs/Cargo.toml                                         |   1 
crates/fs/src/repository.rs                                  |  20 
crates/fuzzy/src/matcher.rs                                  |   2 
crates/fuzzy/src/paths.rs                                    |   6 
crates/gpui/src/app/window.rs                                |   6 
crates/gpui2/src/elements/text.rs                            |  16 
crates/language/Cargo.toml                                   |   1 
crates/language/src/buffer_tests.rs                          |   2 
crates/project/src/worktree.rs                               |  22 
crates/storybook/src/stories/components.rs                   |   4 
crates/storybook/src/stories/components/language_selector.rs |  16 
crates/storybook/src/stories/components/multi_buffer.rs      |  24 
crates/storybook/src/stories/components/recent_projects.rs   |  16 
crates/storybook/src/stories/components/theme_selector.rs    |  16 
crates/storybook/src/story_selector.rs                       |  14 
crates/ui/src/components.rs                                  |  10 
crates/ui/src/components/language_selector.rs                |  36 
crates/ui/src/components/list.rs                             |   4 
crates/ui/src/components/multi_buffer.rs                     |  42 
crates/ui/src/components/palette.rs                          |  20 
crates/ui/src/components/recent_projects.rs                  |  32 
crates/ui/src/components/theme_selector.rs                   |  37 
crates/ui/src/components/toast.rs                            |  66 
crates/ui/src/components/workspace.rs                        |  13 
crates/ui/src/elements/details.rs                            |   2 
crates/ui/src/elements/icon.rs                               |   2 
crates/ui/src/prelude.rs                                     |  20 
crates/util/src/paths.rs                                     |  15 
crates/vim/src/vim.rs                                        |   2 
crates/welcome/Cargo.toml                                    |   1 
crates/welcome/src/welcome.rs                                |  25 
crates/workspace/Cargo.toml                                  |   1 
crates/workspace/src/pane_group.rs                           |  33 
crates/workspace/src/workspace.rs                            | 407 +-
crates/zed/src/languages/rust.rs                             |  51 
crates/zed/src/main.rs                                       |  27 
crates/zed/src/open_listener.rs                              |   2 
crates/zed/src/zed.rs                                        |   1 
docs/building-zed.md                                         |   3 
docs/local-collaboration.md                                  |   2 
rust-toolchain.toml                                          |   2 
script/crate-dep-graph                                       |  19 
script/start-local-collaboration                             |  59 
script/zed-local                                             |  88 
script/zed-with-local-servers                                |   6 
66 files changed, 1,655 insertions(+), 644 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1082,7 +1082,6 @@ dependencies = [
  "anyhow",
  "async-broadcast",
  "audio",
- "channel",
  "client",
  "collections",
  "fs",
@@ -1467,7 +1466,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.23.1"
+version = "0.23.3"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -2079,9 +2078,9 @@ dependencies = [
 
 [[package]]
 name = "curl-sys"
-version = "0.4.66+curl-8.3.0"
+version = "0.4.67+curl-8.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9"
+checksum = "3cc35d066510b197a0f72de863736641539957628c8a42e70e27c66849e77c34"
 dependencies = [
  "cc",
  "libc",
@@ -2832,7 +2831,6 @@ dependencies = [
  "parking_lot 0.11.2",
  "regex",
  "rope",
- "rpc",
  "serde",
  "serde_derive",
  "serde_json",
@@ -9665,6 +9663,7 @@ dependencies = [
  "theme",
  "theme_selector",
  "util",
+ "vim",
  "workspace",
 ]
 
@@ -9971,7 +9970,6 @@ dependencies = [
  "async-recursion 1.0.5",
  "bincode",
  "call",
- "channel",
  "client",
  "collections",
  "context_menu",

Dockerfile 🔗

@@ -1,6 +1,6 @@
 # syntax = docker/dockerfile:1.2
 
-FROM rust:1.72-bullseye as builder
+FROM rust:1.73-bullseye as builder
 WORKDIR app
 COPY . .
 

README.md 🔗

@@ -83,9 +83,7 @@ foreman start
 If you want to run Zed pointed at the local servers, you can run:
 
 ```
-script/zed-with-local-servers
-# or...
-script/zed-with-local-servers --release
+script/zed-local
 ```
 
 ### Dump element JSON

crates/call/Cargo.toml 🔗

@@ -20,7 +20,6 @@ test-support = [
 
 [dependencies]
 audio = { path = "../audio" }
-channel = { path = "../channel" }
 client = { path = "../client" }
 collections = { path = "../collections" }
 gpui = { path = "../gpui" }

crates/call/src/call.rs 🔗

@@ -5,7 +5,6 @@ pub mod room;
 use anyhow::{anyhow, Result};
 use audio::Audio;
 use call_settings::CallSettings;
-use channel::ChannelId;
 use client::{
     proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
     ZED_ALWAYS_ACTIVE,
@@ -79,7 +78,7 @@ impl ActiveCall {
         }
     }
 
-    pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
+    pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
         self.room()?.read(cx).channel_id()
     }
 

crates/call/src/room.rs 🔗

@@ -606,35 +606,39 @@ impl Room {
 
     /// Returns the most 'active' projects, defined as most people in the project
     pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> {
-        let mut projects = HashMap::default();
-        let mut hosts = HashMap::default();
-
+        let mut project_hosts_and_guest_counts = HashMap::<u64, (Option<u64>, u32)>::default();
         for participant in self.remote_participants.values() {
             match participant.location {
                 ParticipantLocation::SharedProject { project_id } => {
-                    *projects.entry(project_id).or_insert(0) += 1;
+                    project_hosts_and_guest_counts
+                        .entry(project_id)
+                        .or_default()
+                        .1 += 1;
                 }
                 ParticipantLocation::External | ParticipantLocation::UnsharedProject => {}
             }
             for project in &participant.projects {
-                *projects.entry(project.id).or_insert(0) += 1;
-                hosts.insert(project.id, participant.user.id);
+                project_hosts_and_guest_counts
+                    .entry(project.id)
+                    .or_default()
+                    .0 = Some(participant.user.id);
             }
         }
 
         if let Some(user) = self.user_store.read(cx).current_user() {
             for project in &self.local_participant.projects {
-                *projects.entry(project.id).or_insert(0) += 1;
-                hosts.insert(project.id, user.id);
+                project_hosts_and_guest_counts
+                    .entry(project.id)
+                    .or_default()
+                    .0 = Some(user.id);
             }
         }
 
-        let mut pairs: Vec<(u64, usize)> = projects.into_iter().collect();
-        pairs.sort_by_key(|(_, count)| *count as i32);
-
-        pairs
-            .iter()
-            .find_map(|(project_id, _)| hosts.get(project_id).map(|host| (*project_id, *host)))
+        project_hosts_and_guest_counts
+            .into_iter()
+            .filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count)))
+            .max_by_key(|(_, _, guest_count)| *guest_count)
+            .map(|(id, host, _)| (id, host))
     }
 
     async fn handle_room_updated(
@@ -700,6 +704,7 @@ impl Room {
                         let Some(peer_id) = participant.peer_id else {
                             continue;
                         };
+                        let participant_index = ParticipantIndex(participant.participant_index);
                         this.participant_user_ids.insert(participant.user_id);
 
                         let old_projects = this
@@ -750,8 +755,9 @@ impl Room {
                         if let Some(remote_participant) =
                             this.remote_participants.get_mut(&participant.user_id)
                         {
-                            remote_participant.projects = participant.projects;
                             remote_participant.peer_id = peer_id;
+                            remote_participant.projects = participant.projects;
+                            remote_participant.participant_index = participant_index;
                             if location != remote_participant.location {
                                 remote_participant.location = location;
                                 cx.emit(Event::ParticipantLocationChanged {
@@ -763,9 +769,7 @@ impl Room {
                                 participant.user_id,
                                 RemoteParticipant {
                                     user: user.clone(),
-                                    participant_index: ParticipantIndex(
-                                        participant.participant_index,
-                                    ),
+                                    participant_index,
                                     peer_id,
                                     projects: participant.projects,
                                     location,

crates/channel/src/channel.rs 🔗

@@ -2,19 +2,21 @@ mod channel_buffer;
 mod channel_chat;
 mod channel_store;
 
+use client::{Client, UserStore};
+use gpui::{AppContext, ModelHandle};
+use std::sync::Arc;
+
 pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
 pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
 pub use channel_store::{
     Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore,
 };
 
-use client::Client;
-use std::sync::Arc;
-
 #[cfg(test)]
 mod channel_store_tests;
 
-pub fn init(client: &Arc<Client>) {
+pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
+    channel_store::init(client, user_store, cx);
     channel_buffer::init(client);
     channel_chat::init(client);
 }

crates/channel/src/channel_store.rs 🔗

@@ -2,6 +2,7 @@ mod channel_index;
 
 use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
 use anyhow::{anyhow, Result};
+use channel_index::ChannelIndex;
 use client::{Client, Subscription, User, UserId, UserStore};
 use collections::{hash_map, HashMap, HashSet};
 use db::RELEASE_CHANNEL;
@@ -15,7 +16,11 @@ use serde_derive::{Deserialize, Serialize};
 use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration};
 use util::ResultExt;
 
-use self::channel_index::ChannelIndex;
+pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
+    let channel_store =
+        cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
+    cx.set_global(channel_store);
+}
 
 pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
 
@@ -92,6 +97,10 @@ enum OpenedModelHandle<E: Entity> {
 }
 
 impl ChannelStore {
+    pub fn global(cx: &AppContext) -> ModelHandle<Self> {
+        cx.global::<ModelHandle<Self>>().clone()
+    }
+
     pub fn new(
         client: Arc<Client>,
         user_store: ModelHandle<UserStore>,

crates/channel/src/channel_store_tests.rs 🔗

@@ -340,10 +340,10 @@ fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
 
     cx.foreground().forbid_parking();
     cx.set_global(SettingsStore::test(cx));
-    crate::init(&client);
     client::init(&client, cx);
+    crate::init(&client, user_store, cx);
 
-    cx.add_model(|cx| ChannelStore::new(client, user_store, cx))
+    ChannelStore::global(cx)
 }
 
 fn update_channels(

crates/client/src/client.rs 🔗

@@ -70,7 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
 pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
 pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
 
-actions!(client, [SignIn, SignOut]);
+actions!(client, [SignIn, SignOut, Reconnect]);
 
 pub fn init_settings(cx: &mut AppContext) {
     settings::register::<TelemetrySettings>(cx);
@@ -102,6 +102,17 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
             }
         }
     });
+    cx.add_global_action({
+        let client = client.clone();
+        move |_: &Reconnect, cx| {
+            if let Some(client) = client.upgrade() {
+                cx.spawn(|cx| async move {
+                    client.reconnect(&cx);
+                })
+                .detach();
+            }
+        }
+    });
 }
 
 pub struct Client {
@@ -1212,6 +1223,11 @@ impl Client {
         self.set_status(Status::SignedOut, cx);
     }
 
+    pub fn reconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
+        self.peer.teardown();
+        self.set_status(Status::ConnectionLost, cx);
+    }
+
     fn connection_id(&self) -> Result<ConnectionId> {
         if let Status::Connected { connection_id, .. } = *self.status().borrow() {
             Ok(connection_id)

crates/client/src/telemetry.rs 🔗

@@ -8,7 +8,6 @@ use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
 use tempfile::NamedTempFile;
 use util::http::HttpClient;
 use util::{channel::ReleaseChannel, TryFutureExt};
-use uuid::Uuid;
 
 pub struct Telemetry {
     http_client: Arc<dyn HttpClient>,
@@ -20,7 +19,7 @@ pub struct Telemetry {
 struct TelemetryState {
     metrics_id: Option<Arc<str>>,      // Per logged-in user
     installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
-    session_id: String,                // Per app launch
+    session_id: Option<Arc<str>>,      // Per app launch
     app_version: Option<Arc<str>>,
     release_channel: Option<&'static str>,
     os_name: &'static str,
@@ -43,7 +42,7 @@ lazy_static! {
 struct ClickhouseEventRequestBody {
     token: &'static str,
     installation_id: Option<Arc<str>>,
-    session_id: String,
+    session_id: Option<Arc<str>>,
     is_staff: Option<bool>,
     app_version: Option<Arc<str>>,
     os_name: &'static str,
@@ -134,7 +133,7 @@ impl Telemetry {
                 release_channel,
                 installation_id: None,
                 metrics_id: None,
-                session_id: Uuid::new_v4().to_string(),
+                session_id: None,
                 clickhouse_events_queue: Default::default(),
                 flush_clickhouse_events_task: Default::default(),
                 log_file: None,
@@ -149,9 +148,15 @@ impl Telemetry {
         Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
     }
 
-    pub fn start(self: &Arc<Self>, installation_id: Option<String>, cx: &mut AppContext) {
+    pub fn start(
+        self: &Arc<Self>,
+        installation_id: Option<String>,
+        session_id: String,
+        cx: &mut AppContext,
+    ) {
         let mut state = self.state.lock();
         state.installation_id = installation_id.map(|id| id.into());
+        state.session_id = Some(session_id.into());
         let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
         drop(state);
 
@@ -283,23 +288,21 @@ impl Telemetry {
 
                     {
                         let state = this.state.lock();
+                        let request_body = ClickhouseEventRequestBody {
+                            token: ZED_SECRET_CLIENT_TOKEN,
+                            installation_id: state.installation_id.clone(),
+                            session_id: state.session_id.clone(),
+                            is_staff: state.is_staff.clone(),
+                            app_version: state.app_version.clone(),
+                            os_name: state.os_name,
+                            os_version: state.os_version.clone(),
+                            architecture: state.architecture,
+
+                            release_channel: state.release_channel,
+                            events,
+                        };
                         json_bytes.clear();
-                        serde_json::to_writer(
-                            &mut json_bytes,
-                            &ClickhouseEventRequestBody {
-                                token: ZED_SECRET_CLIENT_TOKEN,
-                                installation_id: state.installation_id.clone(),
-                                session_id: state.session_id.clone(),
-                                is_staff: state.is_staff.clone(),
-                                app_version: state.app_version.clone(),
-                                os_name: state.os_name,
-                                os_version: state.os_version.clone(),
-                                architecture: state.architecture,
-
-                                release_channel: state.release_channel,
-                                events,
-                            },
-                        )?;
+                        serde_json::to_writer(&mut json_bytes, &request_body)?;
                     }
 
                     this.http_client

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.23.1"
+version = "0.23.3"
 publish = false
 
 [[bin]]

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

@@ -89,7 +89,7 @@ impl Database {
 
             let mut rows = channel_message::Entity::find()
                 .filter(condition)
-                .order_by_asc(channel_message::Column::Id)
+                .order_by_desc(channel_message::Column::Id)
                 .limit(count as u64)
                 .stream(&*tx)
                 .await?;
@@ -110,6 +110,7 @@ impl Database {
                 });
             }
             drop(rows);
+            messages.reverse();
             Ok(messages)
         })
         .await

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

@@ -1,10 +1,75 @@
 use crate::{
-    db::{Database, NewUserParams},
+    db::{Database, MessageId, NewUserParams},
     test_both_dbs,
 };
 use std::sync::Arc;
 use time::OffsetDateTime;
 
+test_both_dbs!(
+    test_channel_message_retrieval,
+    test_channel_message_retrieval_postgres,
+    test_channel_message_retrieval_sqlite
+);
+
+async fn test_channel_message_retrieval(db: &Arc<Database>) {
+    let user = db
+        .create_user(
+            "user@example.com",
+            false,
+            NewUserParams {
+                github_login: "user".into(),
+                github_user_id: 1,
+                invite_count: 0,
+            },
+        )
+        .await
+        .unwrap()
+        .user_id;
+    let channel = db
+        .create_channel("channel", None, "room", user)
+        .await
+        .unwrap();
+
+    let owner_id = db.create_server("test").await.unwrap().0 as u32;
+    db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user)
+        .await
+        .unwrap();
+
+    let mut all_messages = Vec::new();
+    for i in 0..10 {
+        all_messages.push(
+            db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i)
+                .await
+                .unwrap()
+                .0
+                .to_proto(),
+        );
+    }
+
+    let messages = db
+        .get_channel_messages(channel, user, 3, None)
+        .await
+        .unwrap()
+        .into_iter()
+        .map(|message| message.id)
+        .collect::<Vec<_>>();
+    assert_eq!(messages, &all_messages[7..10]);
+
+    let messages = db
+        .get_channel_messages(
+            channel,
+            user,
+            4,
+            Some(MessageId::from_proto(all_messages[6])),
+        )
+        .await
+        .unwrap()
+        .into_iter()
+        .map(|message| message.id)
+        .collect::<Vec<_>>();
+    assert_eq!(messages, &all_messages[2..6]);
+}
+
 test_both_dbs!(
     test_channel_message_nonces,
     test_channel_message_nonces_postgres,

crates/collab/src/rpc.rs 🔗

@@ -1917,13 +1917,10 @@ async fn follow(
         .check_room_participants(room_id, leader_id, session.connection_id)
         .await?;
 
-    let mut response_payload = session
+    let response_payload = session
         .peer
         .forward_request(session.connection_id, leader_id, request)
         .await?;
-    response_payload
-        .views
-        .retain(|view| view.leader_id != Some(follower_id.into()));
     response.send(response_payload)?;
 
     if let Some(project_id) = project_id {
@@ -1984,14 +1981,17 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
             .await?
     };
 
-    let leader_id = request.variant.as_ref().and_then(|variant| match variant {
-        proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
+    // For now, don't send view update messages back to that view's current leader.
+    let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
         proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
-        proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
+        _ => None,
     });
+
     for follower_peer_id in request.follower_ids.iter().copied() {
         let follower_connection_id = follower_peer_id.into();
-        if Some(follower_peer_id) != leader_id && connection_ids.contains(&follower_connection_id) {
+        if Some(follower_peer_id) != connection_id_to_omit
+            && connection_ids.contains(&follower_connection_id)
+        {
             session.peer.forward_send(
                 session.connection_id,
                 follower_connection_id,

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

@@ -4,6 +4,7 @@ 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 rpc::proto::PeerId;
 use serde_json::json;
 use std::{borrow::Cow, sync::Arc};
 use workspace::{
@@ -183,20 +184,12 @@ async fn test_basic_following(
 
     // 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}"
-            );
-        });
+    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+        assert_eq!(
+            followers_by_leader(project_id, cx),
+            &[(peer_id_a, vec![peer_id_b, peer_id_c])],
+            "followers seen by {name}"
+        );
     }
 
     // Client C unfollows client A.
@@ -206,46 +199,39 @@ async fn test_basic_following(
 
     // 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}"
-            );
-        });
+    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+        assert_eq!(
+            followers_by_leader(project_id, cx),
+            &[(peer_id_a, vec![peer_id_b])],
+            "followers seen by {name}"
+        );
     }
 
     // Client C re-follows client A.
-    workspace_c.update(cx_c, |workspace, cx| {
-        workspace.follow(peer_id_a, cx);
-    });
+    workspace_c
+        .update(cx_c, |workspace, cx| {
+            workspace.follow(peer_id_a, cx).unwrap()
+        })
+        .await
+        .unwrap();
 
     // 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}"
-            );
-        });
+    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+        assert_eq!(
+            followers_by_leader(project_id, cx),
+            &[(peer_id_a, vec![peer_id_b, peer_id_c])],
+            "followers seen by {name}"
+        );
     }
 
-    // Client D follows client C.
+    // Client D follows client B, then switches to following client C.
+    workspace_d
+        .update(cx_d, |workspace, cx| {
+            workspace.follow(peer_id_b, cx).unwrap()
+        })
+        .await
+        .unwrap();
     workspace_d
         .update(cx_d, |workspace, cx| {
             workspace.follow(peer_id_c, cx).unwrap()
@@ -255,20 +241,15 @@ async fn test_basic_following(
 
     // 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}"
-            );
-        });
+    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+        assert_eq!(
+            followers_by_leader(project_id, cx),
+            &[
+                (peer_id_a, vec![peer_id_b, peer_id_c]),
+                (peer_id_c, vec![peer_id_d])
+            ],
+            "followers seen by {name}"
+        );
     }
 
     // Client C closes the project.
@@ -277,32 +258,12 @@ async fn test_basic_following(
 
     // 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}"
-            );
-        });
+    for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
+        assert_eq!(
+            followers_by_leader(project_id, cx),
+            &[(peer_id_a, vec![peer_id_b]),],
+            "followers seen by {name}"
+        );
     }
 
     // When client A activates a different editor, client B does so as well.
@@ -724,10 +685,9 @@ async fn test_peers_following_each_other(
         .await
         .unwrap();
 
-    // Client A opens some editors.
+    // Client A opens a file.
     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
+    workspace_a
         .update(cx_a, |workspace, cx| {
             workspace.open_path((worktree_id, "1.txt"), None, true, cx)
         })
@@ -736,10 +696,9 @@ async fn test_peers_following_each_other(
         .downcast::<Editor>()
         .unwrap();
 
-    // Client B opens an editor.
+    // Client B opens a different file.
     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
+    workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "2.txt"), None, true, cx)
         })
@@ -754,9 +713,7 @@ async fn test_peers_following_each_other(
     });
     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()
+            workspace.follow(client_b.peer_id().unwrap(), cx).unwrap()
         })
         .await
         .unwrap();
@@ -765,85 +722,443 @@ async fn test_peers_following_each_other(
     });
     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()
+            workspace.follow(client_a.peer_id().unwrap(), 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);
-    });
+    // Clients A and B return focus to the original files they had open
+    workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
+    workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+    deterministic.run_until_parked();
 
+    // Both clients see the other client's focused file in their right pane.
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(true, "1.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_b.peer_id(),
+                items: vec![(false, "1.txt".into()), (true, "2.txt".into())]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(true, "2.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_a.peer_id(),
+                items: vec![(false, "2.txt".into()), (true, "1.txt".into())]
+            },
+        ]
+    );
+
+    // Clients A and B each open a new file.
     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();
+    deterministic.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);
+    // Both client's see the other client open the new file, but keep their
+    // focus on their own active pane.
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "2.txt".into()),
+                    (false, "1.txt".into()),
+                    (true, "3.txt".into())
+                ]
+            },
+        ]
+    );
+
+    // Client A focuses their right pane, in which they're following client B.
+    workspace_a.update(cx_a, |workspace, cx| workspace.activate_next_pane(cx));
+    deterministic.run_until_parked();
+
+    // Client B sees that client A is now looking at the same file as them.
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: false,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "2.txt".into()),
+                    (false, "1.txt".into()),
+                    (false, "3.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+
+    // Client B focuses their right pane, in which they're following client A,
+    // who is following them.
+    workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
+    deterministic.run_until_parked();
+
+    // Client A sees that client B is now looking at the same file as them.
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "2.txt".into()),
+                    (false, "1.txt".into()),
+                    (false, "3.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "4.txt".into())
+                ]
+            },
+        ]
+    );
+
+    // Client B focuses a file that they previously followed A to, breaking
+    // the follow.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.activate_prev_item(true, cx);
+        });
     });
-    workspace_b.read_with(cx_b, |workspace, _| {
-        assert_eq!(*workspace.active_pane(), pane_b1);
+    deterministic.run_until_parked();
+
+    // Both clients see that client B is looking at that previous file.
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![
+                    (false, "2.txt".into()),
+                    (false, "1.txt".into()),
+                    (true, "3.txt".into()),
+                    (false, "4.txt".into())
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (false, "4.txt".into()),
+                    (true, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+
+    // Client B closes tabs, some of which were originally opened by client A,
+    // and some of which were originally opened by client B.
+    workspace_b.update(cx_b, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.close_inactive_items(&Default::default(), cx)
+                .unwrap()
+                .detach();
+        });
     });
 
-    // Ensure peers following each other doesn't cause an infinite loop.
+    deterministic.run_until_parked();
+
+    // Both clients see that Client B is looking at the previous tab.
     assert_eq!(
-        workspace_a.read_with(cx_a, |workspace, cx| workspace
-            .active_item(cx)
-            .unwrap()
-            .project_path(cx)),
-        Some((worktree_id, "3.txt").into())
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![(true, "3.txt".into()),]
+            },
+        ]
     );
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_b.peer_id(),
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (false, "4.txt".into()),
+                    (true, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+
+    // Client B follows client A again.
+    workspace_b
+        .update(cx_b, |workspace, cx| {
+            workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
+        })
+        .await
+        .unwrap();
+
+    // Client A cycles through some tabs.
     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.active_pane().update(cx, |pane, cx| {
+            pane.activate_prev_item(true, cx);
+        });
     });
+    deterministic.run_until_parked();
+
+    // Client B follows client A into those tabs.
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![
+                    (false, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "4.txt".into()),
+                    (false, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_a.peer_id(),
+                items: vec![(false, "3.txt".into()), (true, "4.txt".into())]
+            },
+        ]
+    );
 
     workspace_a.update(cx_a, |workspace, cx| {
-        assert_eq!(
-            workspace.active_item(cx).unwrap().project_path(cx),
-            Some((worktree_id, "4.txt").into())
-        );
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.activate_prev_item(true, cx);
+        });
     });
+    deterministic.run_until_parked();
 
-    workspace_b.update(cx_b, |workspace, cx| {
-        assert_eq!(
-            workspace.active_item(cx).unwrap().project_path(cx),
-            Some((worktree_id, "4.txt").into())
-        );
-        workspace.activate_next_pane(cx);
-    });
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![
+                    (false, "1.txt".into()),
+                    (true, "2.txt".into()),
+                    (false, "4.txt".into()),
+                    (false, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "3.txt".into()),
+                    (false, "4.txt".into()),
+                    (true, "2.txt".into())
+                ]
+            },
+        ]
+    );
 
-    workspace_b.update(cx_b, |workspace, cx| {
-        assert_eq!(
-            workspace.active_item(cx).unwrap().project_path(cx),
-            Some((worktree_id, "3.txt").into())
-        );
+    workspace_a.update(cx_a, |workspace, cx| {
+        workspace.active_pane().update(cx, |pane, cx| {
+            pane.activate_prev_item(true, cx);
+        });
     });
+    deterministic.run_until_parked();
+
+    assert_eq!(
+        pane_summaries(&workspace_a, cx_a),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "1.txt".into()), (true, "3.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: None,
+                items: vec![
+                    (true, "1.txt".into()),
+                    (false, "2.txt".into()),
+                    (false, "4.txt".into()),
+                    (false, "3.txt".into()),
+                ]
+            },
+        ]
+    );
+    assert_eq!(
+        pane_summaries(&workspace_b, cx_b),
+        &[
+            PaneSummary {
+                active: false,
+                leader: None,
+                items: vec![(false, "2.txt".into()), (true, "4.txt".into())]
+            },
+            PaneSummary {
+                active: true,
+                leader: client_a.peer_id(),
+                items: vec![
+                    (false, "3.txt".into()),
+                    (false, "4.txt".into()),
+                    (false, "2.txt".into()),
+                    (true, "1.txt".into()),
+                ]
+            },
+        ]
+    );
 }
 
 #[gpui::test(iterations = 10)]
@@ -1074,24 +1389,6 @@ async fn test_peers_simultaneously_following_each_other(
     });
 }
 
-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>,
@@ -1304,3 +1601,83 @@ async fn test_following_across_workspaces(
         assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs"));
     });
 }
+
+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
+}
+
+#[derive(Debug, PartialEq, Eq)]
+struct PaneSummary {
+    active: bool,
+    leader: Option<PeerId>,
+    items: Vec<(bool, String)>,
+}
+
+fn followers_by_leader(project_id: u64, cx: &TestAppContext) -> Vec<(PeerId, Vec<PeerId>)> {
+    cx.read(|cx| {
+        let active_call = ActiveCall::global(cx).read(cx);
+        let peer_id = active_call.client().peer_id();
+        let room = active_call.room().unwrap().read(cx);
+        let mut result = room
+            .remote_participants()
+            .values()
+            .map(|participant| participant.peer_id)
+            .chain(peer_id)
+            .filter_map(|peer_id| {
+                let followers = room.followers_for(peer_id, project_id);
+                if followers.is_empty() {
+                    None
+                } else {
+                    Some((peer_id, followers.to_vec()))
+                }
+            })
+            .collect::<Vec<_>>();
+        result.sort_by_key(|e| e.0);
+        result
+    })
+}
+
+fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
+    workspace.read_with(cx, |workspace, cx| {
+        let active_pane = workspace.active_pane();
+        workspace
+            .panes()
+            .iter()
+            .map(|pane| {
+                let leader = workspace.leader_for_pane(pane);
+                let active = pane == active_pane;
+                let pane = pane.read(cx);
+                let active_ix = pane.active_item_index();
+                PaneSummary {
+                    active,
+                    leader,
+                    items: pane
+                        .items()
+                        .enumerate()
+                        .map(|(ix, item)| {
+                            (
+                                ix == active_ix,
+                                item.tab_description(0, cx)
+                                    .map_or(String::new(), |s| s.to_string()),
+                            )
+                        })
+                        .collect(),
+                }
+            })
+            .collect()
+    })
+}

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

@@ -44,6 +44,7 @@ pub struct TestServer {
 pub struct TestClient {
     pub username: String,
     pub app_state: Arc<workspace::AppState>,
+    channel_store: ModelHandle<ChannelStore>,
     state: RefCell<TestClientState>,
 }
 
@@ -206,15 +207,12 @@ 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(language_registry),
             fs: fs.clone(),
             build_window_options: |_, _, _| Default::default(),
@@ -231,7 +229,7 @@ impl TestServer {
             workspace::init(app_state.clone(), cx);
             audio::init((), cx);
             call::init(client.clone(), user_store.clone(), cx);
-            channel::init(&client);
+            channel::init(&client, user_store, cx);
         });
 
         client
@@ -242,6 +240,7 @@ impl TestServer {
         let client = TestClient {
             app_state,
             username: name.to_string(),
+            channel_store: cx.read(ChannelStore::global).clone(),
             state: Default::default(),
         };
         client.wait_for_current_user(cx).await;
@@ -310,10 +309,9 @@ impl TestServer {
         admin: (&TestClient, &mut TestAppContext),
         members: &mut [(&TestClient, &mut TestAppContext)],
     ) -> u64 {
-        let (admin_client, admin_cx) = admin;
-        let channel_id = admin_client
-            .app_state
-            .channel_store
+        let (_, admin_cx) = admin;
+        let channel_id = admin_cx
+            .read(ChannelStore::global)
             .update(admin_cx, |channel_store, cx| {
                 channel_store.create_channel(channel, parent, cx)
             })
@@ -321,9 +319,8 @@ impl TestServer {
             .unwrap();
 
         for (member_client, member_cx) in members {
-            admin_client
-                .app_state
-                .channel_store
+            admin_cx
+                .read(ChannelStore::global)
                 .update(admin_cx, |channel_store, cx| {
                     channel_store.invite_member(
                         channel_id,
@@ -337,9 +334,8 @@ impl TestServer {
 
             admin_cx.foreground().run_until_parked();
 
-            member_client
-                .app_state
-                .channel_store
+            member_cx
+                .read(ChannelStore::global)
                 .update(*member_cx, |channels, _| {
                     channels.respond_to_channel_invite(channel_id, true)
                 })
@@ -447,7 +443,7 @@ impl TestClient {
     }
 
     pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
-        &self.app_state.channel_store
+        &self.channel_store
     }
 
     pub fn user_store(&self) -> &ModelHandle<UserStore> {
@@ -614,8 +610,8 @@ impl TestClient {
     ) {
         let (other_client, other_cx) = user;
 
-        self.app_state
-            .channel_store
+        cx_self
+            .read(ChannelStore::global)
             .update(cx_self, |channel_store, cx| {
                 channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
             })
@@ -624,11 +620,10 @@ impl TestClient {
 
         cx_self.foreground().run_until_parked();
 
-        other_client
-            .app_state
-            .channel_store
-            .update(other_cx, |channels, _| {
-                channels.respond_to_channel_invite(channel, true)
+        other_cx
+            .read(ChannelStore::global)
+            .update(other_cx, |channel_store, _| {
+                channel_store.respond_to_channel_invite(channel, true)
             })
             .await
             .unwrap();

crates/collab_ui/src/channel_view.rs 🔗

@@ -73,7 +73,7 @@ impl ChannelView {
     ) -> Task<Result<ViewHandle<Self>>> {
         let workspace = workspace.read(cx);
         let project = workspace.project().to_owned();
-        let channel_store = workspace.app_state().channel_store.clone();
+        let channel_store = ChannelStore::global(cx);
         let markdown = workspace
             .app_state()
             .languages

crates/collab_ui/src/chat_panel.rs 🔗

@@ -81,7 +81,7 @@ impl ChatPanel {
     pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
         let fs = workspace.app_state().fs.clone();
         let client = workspace.app_state().client.clone();
-        let channel_store = workspace.app_state().channel_store.clone();
+        let channel_store = ChannelStore::global(cx);
         let languages = workspace.app_state().languages.clone();
 
         let input_editor = cx.add_view(|cx| {

crates/collab_ui/src/collab_panel.rs 🔗

@@ -655,7 +655,7 @@ impl CollabPanel {
                 channel_editing_state: None,
                 selection: None,
                 user_store: workspace.user_store().clone(),
-                channel_store: workspace.app_state().channel_store.clone(),
+                channel_store: ChannelStore::global(cx),
                 project: workspace.project().clone(),
                 subscriptions: Vec::default(),
                 match_candidates: Vec::default(),

crates/file_finder/src/file_finder.rs 🔗

@@ -107,13 +107,23 @@ fn matching_history_item_paths(
 ) -> HashMap<Arc<Path>, PathMatch> {
     let history_items_by_worktrees = history_items
         .iter()
-        .map(|found_path| {
-            let path = &found_path.project.path;
+        .filter_map(|found_path| {
             let candidate = PathMatchCandidate {
-                path,
-                char_bag: CharBag::from_iter(path.to_string_lossy().to_lowercase().chars()),
+                path: &found_path.project.path,
+                // Only match history items names, otherwise their paths may match too many queries, producing false positives.
+                // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
+                // it would be shown first always, despite the latter being a better match.
+                char_bag: CharBag::from_iter(
+                    found_path
+                        .project
+                        .path
+                        .file_name()?
+                        .to_string_lossy()
+                        .to_lowercase()
+                        .chars(),
+                ),
             };
-            (found_path.project.worktree_id, candidate)
+            Some((found_path.project.worktree_id, candidate))
         })
         .fold(
             HashMap::default(),
@@ -1803,6 +1813,113 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_history_items_vs_very_good_external_match(
+        deterministic: Arc<gpui::executor::Deterministic>,
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let app_state = init_test(cx);
+
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/src",
+                json!({
+                    "collab_ui": {
+                        "first.rs": "// First Rust file",
+                        "second.rs": "// Second Rust file",
+                        "third.rs": "// Third Rust file",
+                        "collab_ui.rs": "// Fourth Rust file",
+                    }
+                }),
+            )
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+        let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+        let workspace = window.root(cx);
+        // generate some history to select from
+        open_close_queried_buffer(
+            "fir",
+            1,
+            "first.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        open_close_queried_buffer(
+            "sec",
+            1,
+            "second.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        open_close_queried_buffer(
+            "thi",
+            1,
+            "third.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+        open_close_queried_buffer(
+            "sec",
+            1,
+            "second.rs",
+            window.into(),
+            &workspace,
+            &deterministic,
+            cx,
+        )
+        .await;
+
+        cx.dispatch_action(window.into(), Toggle);
+        let query = "collab_ui";
+        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+        finder
+            .update(cx, |finder, cx| {
+                finder.delegate_mut().update_matches(query.to_string(), cx)
+            })
+            .await;
+        finder.read_with(cx, |finder, _| {
+            let delegate = finder.delegate();
+            assert!(
+                delegate.matches.history.is_empty(),
+                "History items should not math query {query}, they should be matched by name only"
+            );
+
+            let search_entries = delegate
+                .matches
+                .search
+                .iter()
+                .map(|e| e.path.to_path_buf())
+                .collect::<Vec<_>>();
+            assert_eq!(
+                search_entries.len(),
+                4,
+                "All history and the new file should be found after query {query} as search results"
+            );
+            assert_eq!(
+                search_entries,
+                vec![
+                    PathBuf::from("collab_ui/collab_ui.rs"),
+                    PathBuf::from("collab_ui/third.rs"),
+                    PathBuf::from("collab_ui/first.rs"),
+                    PathBuf::from("collab_ui/second.rs"),
+                ],
+                "Despite all search results having the same directory name, the most matching one should be on top"
+            );
+        });
+    }
+
     async fn open_close_queried_buffer(
         input: &str,
         expected_matches: usize,

crates/fs/Cargo.toml 🔗

@@ -13,7 +13,6 @@ rope = { path = "../rope" }
 text = { path = "../text" }
 util = { path = "../util" }
 sum_tree = { path = "../sum_tree" }
-rpc = { path = "../rpc" }
 
 anyhow.workspace = true
 async-trait.workspace = true

crates/fs/src/repository.rs 🔗

@@ -2,7 +2,6 @@ use anyhow::Result;
 use collections::HashMap;
 use git2::{BranchType, StatusShow};
 use parking_lot::Mutex;
-use rpc::proto;
 use serde_derive::{Deserialize, Serialize};
 use std::{
     cmp::Ordering,
@@ -23,6 +22,7 @@ pub struct Branch {
     /// Timestamp of most recent commit, normalized to Unix Epoch format.
     pub unix_timestamp: Option<i64>,
 }
+
 #[async_trait::async_trait]
 pub trait GitRepository: Send {
     fn reload_index(&self);
@@ -358,24 +358,6 @@ impl GitFileStatus {
             }
         }
     }
-
-    pub fn from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
-        git_status.and_then(|status| {
-            proto::GitStatus::from_i32(status).map(|status| match status {
-                proto::GitStatus::Added => GitFileStatus::Added,
-                proto::GitStatus::Modified => GitFileStatus::Modified,
-                proto::GitStatus::Conflict => GitFileStatus::Conflict,
-            })
-        })
-    }
-
-    pub fn to_proto(self) -> i32 {
-        match self {
-            GitFileStatus::Added => proto::GitStatus::Added as i32,
-            GitFileStatus::Modified => proto::GitStatus::Modified as i32,
-            GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
-        }
-    }
 }
 
 #[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]

crates/fuzzy/src/matcher.rs 🔗

@@ -441,7 +441,7 @@ mod tests {
                 score,
                 worktree_id: 0,
                 positions: Vec::new(),
-                path: candidate.path.clone(),
+                path: Arc::from(candidate.path),
                 path_prefix: "".into(),
                 distance_to_relative_ancestor: usize::MAX,
             },

crates/fuzzy/src/paths.rs 🔗

@@ -14,7 +14,7 @@ use crate::{
 
 #[derive(Clone, Debug)]
 pub struct PathMatchCandidate<'a> {
-    pub path: &'a Arc<Path>,
+    pub path: &'a Path,
     pub char_bag: CharBag,
 }
 
@@ -120,7 +120,7 @@ pub fn match_fixed_path_set(
             score,
             worktree_id,
             positions: Vec::new(),
-            path: candidate.path.clone(),
+            path: Arc::from(candidate.path),
             path_prefix: Arc::from(""),
             distance_to_relative_ancestor: usize::MAX,
         },
@@ -195,7 +195,7 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
                                     score,
                                     worktree_id,
                                     positions: Vec::new(),
-                                    path: candidate.path.clone(),
+                                    path: Arc::from(candidate.path),
                                     path_prefix: candidate_set.prefix(),
                                     distance_to_relative_ancestor: relative_to.as_ref().map_or(
                                         usize::MAX,

crates/gpui/src/app/window.rs 🔗

@@ -71,7 +71,7 @@ pub struct Window {
     pub(crate) hovered_region_ids: Vec<MouseRegionId>,
     pub(crate) clicked_region_ids: Vec<MouseRegionId>,
     pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
-    text_layout_cache: TextLayoutCache,
+    text_layout_cache: Arc<TextLayoutCache>,
     refreshing: bool,
 }
 
@@ -107,7 +107,7 @@ impl Window {
             cursor_regions: Default::default(),
             mouse_regions: Default::default(),
             event_handlers: Default::default(),
-            text_layout_cache: TextLayoutCache::new(cx.font_system.clone()),
+            text_layout_cache: Arc::new(TextLayoutCache::new(cx.font_system.clone())),
             last_mouse_moved_event: None,
             last_mouse_position: Vector2F::zero(),
             pressed_buttons: Default::default(),
@@ -303,7 +303,7 @@ impl<'a> WindowContext<'a> {
         self.window.refreshing
     }
 
-    pub fn text_layout_cache(&self) -> &TextLayoutCache {
+    pub fn text_layout_cache(&self) -> &Arc<TextLayoutCache> {
         &self.window.text_layout_cache
     }
 

crates/gpui2/src/elements/text.rs 🔗

@@ -5,7 +5,7 @@ use crate::{
 use anyhow::Result;
 use gpui::{
     geometry::{vector::Vector2F, Size},
-    text_layout::LineLayout,
+    text_layout::Line,
     LayoutId,
 };
 use parking_lot::Mutex;
@@ -32,7 +32,7 @@ impl<V: 'static> Element<V> for Text {
         _view: &mut V,
         cx: &mut ViewContext<V>,
     ) -> Result<(LayoutId, Self::PaintState)> {
-        let fonts = cx.platform().fonts();
+        let layout_cache = cx.text_layout_cache().clone();
         let text_style = cx.text_style();
         let line_height = cx.font_cache().line_height(text_style.font_size);
         let text = self.text.clone();
@@ -41,14 +41,14 @@ impl<V: 'static> Element<V> for Text {
         let layout_id = cx.add_measured_layout_node(Default::default(), {
             let paint_state = paint_state.clone();
             move |_params| {
-                let line_layout = fonts.layout_line(
+                let line_layout = layout_cache.layout_str(
                     text.as_ref(),
                     text_style.font_size,
                     &[(text.len(), text_style.to_run())],
                 );
 
                 let size = Size {
-                    width: line_layout.width,
+                    width: line_layout.width(),
                     height: line_height,
                 };
 
@@ -85,13 +85,9 @@ impl<V: 'static> Element<V> for Text {
             line_height = paint_state.line_height;
         }
 
-        let text_style = cx.text_style();
-        let line =
-            gpui::text_layout::Line::new(line_layout, &[(self.text.len(), text_style.to_run())]);
-
         // TODO: We haven't added visible bounds to the new element system yet, so this is a placeholder.
         let visible_bounds = bounds;
-        line.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx);
+        line_layout.paint(bounds.origin(), visible_bounds, line_height, cx.legacy_cx);
     }
 }
 
@@ -104,6 +100,6 @@ impl<V: 'static> IntoElement<V> for Text {
 }
 
 pub struct TextLayout {
-    line_layout: Arc<LineLayout>,
+    line_layout: Arc<Line>,
     line_height: f32,
 }

crates/language/Cargo.toml 🔗

@@ -22,7 +22,6 @@ test-support = [
 ]
 
 [dependencies]
-client = { path = "../client" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }
 fuzzy = { path = "../fuzzy" }

crates/language/src/buffer_tests.rs 🔗

@@ -1427,7 +1427,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex
         // Insert the block at column zero. The entire block is indented
         // so that the first line matches the previous line's indentation.
         buffer.edit(
-            [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
+            [(Point::new(2, 0)..Point::new(2, 0), inserted_text)],
             Some(AutoindentMode::Block {
                 original_indent_columns: original_indent_columns.clone(),
             }),

crates/project/src/worktree.rs 🔗

@@ -4310,7 +4310,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
             is_symlink: entry.is_symlink,
             is_ignored: entry.is_ignored,
             is_external: entry.is_external,
-            git_status: entry.git_status.map(|status| status.to_proto()),
+            git_status: entry.git_status.map(git_status_to_proto),
         }
     }
 }
@@ -4337,7 +4337,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
                 is_symlink: entry.is_symlink,
                 is_ignored: entry.is_ignored,
                 is_external: entry.is_external,
-                git_status: GitFileStatus::from_proto(entry.git_status),
+                git_status: git_status_from_proto(entry.git_status),
             })
         } else {
             Err(anyhow!(
@@ -4366,3 +4366,21 @@ fn combine_git_statuses(
         unstaged
     }
 }
+
+fn git_status_from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
+    git_status.and_then(|status| {
+        proto::GitStatus::from_i32(status).map(|status| match status {
+            proto::GitStatus::Added => GitFileStatus::Added,
+            proto::GitStatus::Modified => GitFileStatus::Modified,
+            proto::GitStatus::Conflict => GitFileStatus::Conflict,
+        })
+    })
+}
+
+fn git_status_to_proto(status: GitFileStatus) -> i32 {
+    match status {
+        GitFileStatus::Added => proto::GitStatus::Added as i32,
+        GitFileStatus::Modified => proto::GitStatus::Modified as i32,
+        GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
+    }
+}

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

@@ -6,13 +6,17 @@ pub mod collab_panel;
 pub mod context_menu;
 pub mod facepile;
 pub mod keybinding;
+pub mod language_selector;
+pub mod multi_buffer;
 pub mod palette;
 pub mod panel;
 pub mod project_panel;
+pub mod recent_projects;
 pub mod status_bar;
 pub mod tab;
 pub mod tab_bar;
 pub mod terminal;
+pub mod theme_selector;
 pub mod title_bar;
 pub mod toolbar;
 pub mod traffic_lights;

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

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::LanguageSelector;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct LanguageSelectorStory {}
+
+impl LanguageSelectorStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, LanguageSelector>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(LanguageSelector::new())
+    }
+}

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

@@ -0,0 +1,24 @@
+use ui::prelude::*;
+use ui::{hello_world_rust_buffer_example, MultiBuffer};
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct MultiBufferStory {}
+
+impl MultiBufferStory {
+    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::<_, MultiBuffer<V>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(MultiBuffer::new(vec![
+                hello_world_rust_buffer_example(&theme),
+                hello_world_rust_buffer_example(&theme),
+                hello_world_rust_buffer_example(&theme),
+                hello_world_rust_buffer_example(&theme),
+                hello_world_rust_buffer_example(&theme),
+            ]))
+    }
+}

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

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::RecentProjects;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct RecentProjectsStory {}
+
+impl RecentProjectsStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, RecentProjects>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(RecentProjects::new())
+    }
+}

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

@@ -0,0 +1,16 @@
+use ui::prelude::*;
+use ui::ThemeSelector;
+
+use crate::story::Story;
+
+#[derive(Element, Default)]
+pub struct ThemeSelectorStory {}
+
+impl ThemeSelectorStory {
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        Story::container(cx)
+            .child(Story::title_for::<_, ThemeSelector>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(ThemeSelector::new())
+    }
+}

crates/storybook/src/story_selector.rs 🔗

@@ -42,13 +42,17 @@ pub enum ComponentStory {
     CollabPanel,
     Facepile,
     Keybinding,
+    LanguageSelector,
+    MultiBuffer,
     Palette,
     Panel,
     ProjectPanel,
+    RecentProjects,
     StatusBar,
     Tab,
     TabBar,
     Terminal,
+    ThemeSelector,
     TitleBar,
     Toolbar,
     TrafficLights,
@@ -69,15 +73,25 @@ impl ComponentStory {
             Self::CollabPanel => components::collab_panel::CollabPanelStory::default().into_any(),
             Self::Facepile => components::facepile::FacepileStory::default().into_any(),
             Self::Keybinding => components::keybinding::KeybindingStory::default().into_any(),
+            Self::LanguageSelector => {
+                components::language_selector::LanguageSelectorStory::default().into_any()
+            }
+            Self::MultiBuffer => components::multi_buffer::MultiBufferStory::default().into_any(),
             Self::Palette => components::palette::PaletteStory::default().into_any(),
             Self::Panel => components::panel::PanelStory::default().into_any(),
             Self::ProjectPanel => {
                 components::project_panel::ProjectPanelStory::default().into_any()
             }
+            Self::RecentProjects => {
+                components::recent_projects::RecentProjectsStory::default().into_any()
+            }
             Self::StatusBar => components::status_bar::StatusBarStory::default().into_any(),
             Self::Tab => components::tab::TabStory::default().into_any(),
             Self::TabBar => components::tab_bar::TabBarStory::default().into_any(),
             Self::Terminal => components::terminal::TerminalStory::default().into_any(),
+            Self::ThemeSelector => {
+                components::theme_selector::ThemeSelectorStory::default().into_any()
+            }
             Self::TitleBar => components::title_bar::TitleBarStory::default().into_any(),
             Self::Toolbar => components::toolbar::ToolbarStory::default().into_any(),
             Self::TrafficLights => {

crates/ui/src/components.rs 🔗

@@ -9,17 +9,22 @@ mod editor_pane;
 mod facepile;
 mod icon_button;
 mod keybinding;
+mod language_selector;
 mod list;
+mod multi_buffer;
 mod palette;
 mod panel;
 mod panes;
 mod player_stack;
 mod project_panel;
+mod recent_projects;
 mod status_bar;
 mod tab;
 mod tab_bar;
 mod terminal;
+mod theme_selector;
 mod title_bar;
+mod toast;
 mod toolbar;
 mod traffic_lights;
 mod workspace;
@@ -35,17 +40,22 @@ pub use editor_pane::*;
 pub use facepile::*;
 pub use icon_button::*;
 pub use keybinding::*;
+pub use language_selector::*;
 pub use list::*;
+pub use multi_buffer::*;
 pub use palette::*;
 pub use panel::*;
 pub use panes::*;
 pub use player_stack::*;
 pub use project_panel::*;
+pub use recent_projects::*;
 pub use status_bar::*;
 pub use tab::*;
 pub use tab_bar::*;
 pub use terminal::*;
+pub use theme_selector::*;
 pub use title_bar::*;
+pub use toast::*;
 pub use toolbar::*;
 pub use traffic_lights::*;
 pub use workspace::*;

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

@@ -0,0 +1,36 @@
+use crate::prelude::*;
+use crate::{OrderMethod, Palette, PaletteItem};
+
+#[derive(Element)]
+pub struct LanguageSelector {
+    scroll_state: ScrollState,
+}
+
+impl LanguageSelector {
+    pub fn new() -> Self {
+        Self {
+            scroll_state: ScrollState::default(),
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        div().child(
+            Palette::new(self.scroll_state.clone())
+                .items(vec![
+                    PaletteItem::new("C"),
+                    PaletteItem::new("C++"),
+                    PaletteItem::new("CSS"),
+                    PaletteItem::new("Elixir"),
+                    PaletteItem::new("Elm"),
+                    PaletteItem::new("ERB"),
+                    PaletteItem::new("Rust (current)"),
+                    PaletteItem::new("Scheme"),
+                    PaletteItem::new("TOML"),
+                    PaletteItem::new("TypeScript"),
+                ])
+                .placeholder("Select a language...")
+                .empty_string("No matches")
+                .default_order(OrderMethod::Ascending),
+        )
+    }
+}

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

@@ -135,7 +135,7 @@ impl ListHeader {
                                     .size(IconSize::Small)
                             }))
                             .child(
-                                Label::new(self.label.clone())
+                                Label::new(self.label)
                                     .color(LabelColor::Muted)
                                     .size(LabelSize::Small),
                             ),
@@ -191,7 +191,7 @@ impl ListSubHeader {
                                 .size(IconSize::Small)
                         }))
                         .child(
-                            Label::new(self.label.clone())
+                            Label::new(self.label)
                                 .color(LabelColor::Muted)
                                 .size(LabelSize::Small),
                         ),

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

@@ -0,0 +1,42 @@
+use std::marker::PhantomData;
+
+use crate::prelude::*;
+use crate::{v_stack, Buffer, Icon, IconButton, Label, LabelSize};
+
+#[derive(Element)]
+pub struct MultiBuffer<V: 'static> {
+    view_type: PhantomData<V>,
+    buffers: Vec<Buffer>,
+}
+
+impl<V: 'static> MultiBuffer<V> {
+    pub fn new(buffers: Vec<Buffer>) -> Self {
+        Self {
+            view_type: PhantomData,
+            buffers,
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let theme = theme(cx);
+
+        v_stack()
+            .w_full()
+            .h_full()
+            .flex_1()
+            .children(self.buffers.clone().into_iter().map(|buffer| {
+                v_stack()
+                    .child(
+                        div()
+                            .flex()
+                            .items_center()
+                            .justify_between()
+                            .p_4()
+                            .fill(theme.lowest.base.default.background)
+                            .child(Label::new("main.rs").size(LabelSize::Small))
+                            .child(IconButton::new(Icon::ArrowUpRight)),
+                    )
+                    .child(buffer)
+            }))
+    }
+}

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

@@ -93,19 +93,17 @@ impl<V: 'static> Palette<V> {
                                     .fill(theme.lowest.base.hovered.background)
                                     .active()
                                     .fill(theme.lowest.base.pressed.background)
-                                    .child(
-                                        PaletteItem::new(item.label)
-                                            .keybinding(item.keybinding.clone()),
-                                    )
+                                    .child(item.clone())
                             })),
                     ),
             )
     }
 }
 
-#[derive(Element)]
+#[derive(Element, Clone)]
 pub struct PaletteItem {
     pub label: &'static str,
+    pub sublabel: Option<&'static str>,
     pub keybinding: Option<Keybinding>,
 }
 
@@ -113,6 +111,7 @@ impl PaletteItem {
     pub fn new(label: &'static str) -> Self {
         Self {
             label,
+            sublabel: None,
             keybinding: None,
         }
     }
@@ -122,6 +121,11 @@ impl PaletteItem {
         self
     }
 
+    pub fn sublabel<L: Into<Option<&'static str>>>(mut self, sublabel: L) -> Self {
+        self.sublabel = sublabel.into();
+        self
+    }
+
     pub fn keybinding<K>(mut self, keybinding: K) -> Self
     where
         K: Into<Option<Keybinding>>,
@@ -138,7 +142,11 @@ impl PaletteItem {
             .flex_row()
             .grow()
             .justify_between()
-            .child(Label::new(self.label))
+            .child(
+                v_stack()
+                    .child(Label::new(self.label))
+                    .children(self.sublabel.map(|sublabel| Label::new(sublabel))),
+            )
             .children(self.keybinding.clone())
     }
 }

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

@@ -0,0 +1,32 @@
+use crate::prelude::*;
+use crate::{OrderMethod, Palette, PaletteItem};
+
+#[derive(Element)]
+pub struct RecentProjects {
+    scroll_state: ScrollState,
+}
+
+impl RecentProjects {
+    pub fn new() -> Self {
+        Self {
+            scroll_state: ScrollState::default(),
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        div().child(
+            Palette::new(self.scroll_state.clone())
+                .items(vec![
+                    PaletteItem::new("zed").sublabel("~/projects/zed"),
+                    PaletteItem::new("saga").sublabel("~/projects/saga"),
+                    PaletteItem::new("journal").sublabel("~/journal"),
+                    PaletteItem::new("dotfiles").sublabel("~/dotfiles"),
+                    PaletteItem::new("zed.dev").sublabel("~/projects/zed.dev"),
+                    PaletteItem::new("laminar").sublabel("~/projects/laminar"),
+                ])
+                .placeholder("Recent Projects...")
+                .empty_string("No matches")
+                .default_order(OrderMethod::Ascending),
+        )
+    }
+}

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

@@ -0,0 +1,37 @@
+use crate::prelude::*;
+use crate::{OrderMethod, Palette, PaletteItem};
+
+#[derive(Element)]
+pub struct ThemeSelector {
+    scroll_state: ScrollState,
+}
+
+impl ThemeSelector {
+    pub fn new() -> Self {
+        Self {
+            scroll_state: ScrollState::default(),
+        }
+    }
+
+    fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        div().child(
+            Palette::new(self.scroll_state.clone())
+                .items(vec![
+                    PaletteItem::new("One Dark"),
+                    PaletteItem::new("Rosé Pine"),
+                    PaletteItem::new("Rosé Pine Moon"),
+                    PaletteItem::new("Sandcastle"),
+                    PaletteItem::new("Solarized Dark"),
+                    PaletteItem::new("Summercamp"),
+                    PaletteItem::new("Atelier Cave Light"),
+                    PaletteItem::new("Atelier Dune Light"),
+                    PaletteItem::new("Atelier Estuary Light"),
+                    PaletteItem::new("Atelier Forest Light"),
+                    PaletteItem::new("Atelier Heath Light"),
+                ])
+                .placeholder("Select Theme...")
+                .empty_string("No matches")
+                .default_order(OrderMethod::Ascending),
+        )
+    }
+}

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

@@ -0,0 +1,66 @@
+use crate::prelude::*;
+
+#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
+pub enum ToastOrigin {
+    #[default]
+    Bottom,
+    BottomRight,
+}
+
+#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
+pub enum ToastVariant {
+    #[default]
+    Toast,
+    Status,
+}
+
+/// A toast is a small, temporary window that appears to show a message to the user
+/// or indicate a required action.
+///
+/// Toasts should not persist on the screen for more than a few seconds unless
+/// they are actively showing the a process in progress.
+///
+/// Only one toast may be visible at a time.
+#[derive(Element)]
+pub struct Toast<V: 'static> {
+    origin: ToastOrigin,
+    children: HackyChildren<V>,
+    payload: HackyChildrenPayload,
+}
+
+impl<V: 'static> Toast<V> {
+    pub fn new(
+        origin: ToastOrigin,
+        children: HackyChildren<V>,
+        payload: HackyChildrenPayload,
+    ) -> Self {
+        Self {
+            origin,
+            children,
+            payload,
+        }
+    }
+
+    fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
+        let color = ThemeColor::new(cx);
+
+        let mut div = div();
+
+        if self.origin == ToastOrigin::Bottom {
+            div = div.right_1_2();
+        } else {
+            div = div.right_4();
+        }
+
+        div.absolute()
+            .bottom_4()
+            .flex()
+            .py_2()
+            .px_1p5()
+            .min_w_40()
+            .rounded_md()
+            .fill(color.elevated_surface)
+            .max_w_64()
+            .children_any((self.children)(cx, self.payload.as_ref()))
+    }
+}

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

@@ -82,6 +82,7 @@ impl WorkspaceElement {
         );
 
         div()
+            .relative()
             .size_full()
             .flex()
             .flex_col()
@@ -169,5 +170,17 @@ impl WorkspaceElement {
                     ),
             )
             .child(StatusBar::new())
+        // An example of a toast is below
+        // Currently because of stacking order this gets obscured by other elements
+
+        // .child(Toast::new(
+        //     ToastOrigin::Bottom,
+        //     |_, payload| {
+        //         let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
+
+        //         vec![Label::new("label").into_any()]
+        //     },
+        //     Box::new(theme.clone()),
+        // ))
     }
 }

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

@@ -27,7 +27,7 @@ impl Details {
             .gap_0p5()
             .text_xs()
             .text_color(theme.lowest.base.default.foreground)
-            .child(self.text.clone())
+            .child(self.text)
             .children(self.meta.map(|m| m))
     }
 }

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

@@ -60,6 +60,7 @@ pub enum Icon {
     ChevronUp,
     Close,
     ExclamationTriangle,
+    ExternalLink,
     File,
     FileGeneric,
     FileDoc,
@@ -109,6 +110,7 @@ impl Icon {
             Icon::ChevronUp => "icons/chevron_up.svg",
             Icon::Close => "icons/x.svg",
             Icon::ExclamationTriangle => "icons/warning.svg",
+            Icon::ExternalLink => "icons/external_link.svg",
             Icon::File => "icons/file.svg",
             Icon::FileGeneric => "icons/file_icons/file.svg",
             Icon::FileDoc => "icons/file_icons/book.svg",

crates/ui/src/prelude.rs 🔗

@@ -29,6 +29,26 @@ impl SystemColor {
     }
 }
 
+#[derive(Clone, Copy)]
+pub struct ThemeColor {
+    pub border: Hsla,
+    pub border_variant: Hsla,
+    /// The background color of an elevated surface, like a modal, tooltip or toast.
+    pub elevated_surface: Hsla,
+}
+
+impl ThemeColor {
+    pub fn new(cx: &WindowContext) -> Self {
+        let theme = theme(cx);
+
+        Self {
+            border: theme.lowest.base.default.border,
+            border_variant: theme.lowest.variant.default.border,
+            elevated_surface: theme.middle.base.default.background,
+        }
+    }
+}
+
 #[derive(Default, PartialEq, EnumIter, Clone, Copy)]
 pub enum HighlightColor {
     #[default]

crates/util/src/paths.rs 🔗

@@ -139,6 +139,12 @@ impl<P> PathLikeWithPosition<P> {
                                     column: None,
                                 })
                             } else {
+                                let maybe_col_str =
+                                    if maybe_col_str.ends_with(FILE_ROW_COLUMN_DELIMITER) {
+                                        &maybe_col_str[..maybe_col_str.len() - 1]
+                                    } else {
+                                        maybe_col_str
+                                    };
                                 match maybe_col_str.parse::<u32>() {
                                     Ok(col) => Ok(Self {
                                         path_like: parse_path_like_str(path_like_str)?,
@@ -241,7 +247,6 @@ mod tests {
             "test_file.rs:1::",
             "test_file.rs::1:2",
             "test_file.rs:1::2",
-            "test_file.rs:1:2:",
             "test_file.rs:1:2:3",
         ] {
             let actual = parse_str(input);
@@ -277,6 +282,14 @@ mod tests {
                     column: None,
                 },
             ),
+            (
+                "crates/file_finder/src/file_finder.rs:1902:13:",
+                PathLikeWithPosition {
+                    path_like: "crates/file_finder/src/file_finder.rs".to_string(),
+                    row: Some(1902),
+                    column: Some(13),
+                },
+            ),
         ];
 
         for (input, expected) in input_and_expected {

crates/vim/src/vim.rs 🔗

@@ -33,7 +33,7 @@ use workspace::{self, Workspace};
 
 use crate::state::ReplayableAction;
 
-struct VimModeSetting(bool);
+pub struct VimModeSetting(pub bool);
 
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct SwitchMode(pub Mode);

crates/welcome/Cargo.toml 🔗

@@ -25,6 +25,7 @@ theme_selector = { path = "../theme_selector" }
 util = { path = "../util" }
 picker = { path = "../picker" }
 workspace = { path = "../workspace" }
+vim = { path = "../vim" }
 
 anyhow.workspace = true
 log.workspace = true

crates/welcome/src/welcome.rs 🔗

@@ -10,6 +10,7 @@ use gpui::{
 };
 use settings::{update_settings_file, SettingsStore};
 use std::{borrow::Cow, sync::Arc};
+use vim::VimModeSetting;
 use workspace::{
     dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace,
     WorkspaceId,
@@ -65,6 +66,7 @@ impl View for WelcomePage {
         let width = theme.welcome.page_width;
 
         let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+        let vim_mode_setting = settings::get::<VimModeSetting>(cx).0;
 
         enum Metrics {}
         enum Diagnostics {}
@@ -144,6 +146,27 @@ impl View for WelcomePage {
                 )
                 .with_child(
                     Flex::column()
+                        .with_child(
+                            theme::ui::checkbox::<Diagnostics, Self, _>(
+                                "Enable vim mode",
+                                &theme.welcome.checkbox,
+                                vim_mode_setting,
+                                0,
+                                cx,
+                                |this, checked, cx| {
+                                    if let Some(workspace) = this.workspace.upgrade(cx) {
+                                        let fs = workspace.read(cx).app_state().fs.clone();
+                                        update_settings_file::<VimModeSetting>(
+                                            fs,
+                                            cx,
+                                            move |setting| *setting = Some(checked),
+                                        )
+                                    }
+                                },
+                            )
+                            .contained()
+                            .with_style(theme.welcome.checkbox_container),
+                        )
                         .with_child(
                             theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
                                 Flex::column()
@@ -186,7 +209,7 @@ impl View for WelcomePage {
                                 "Send crash reports",
                                 &theme.welcome.checkbox,
                                 telemetry_settings.diagnostics,
-                                0,
+                                1,
                                 cx,
                                 |this, checked, cx| {
                                     if let Some(workspace) = this.workspace.upgrade(cx) {

crates/workspace/Cargo.toml 🔗

@@ -22,7 +22,6 @@ test-support = [
 db = { path = "../db" }
 call = { path = "../call" }
 client = { path = "../client" }
-channel = { path = "../channel" }
 collections = { path = "../collections" }
 context_menu = { path = "../context_menu" }
 drag_and_drop = { path = "../drag_and_drop" }

crates/workspace/src/pane_group.rs 🔗

@@ -1,10 +1,7 @@
-use std::{cell::RefCell, rc::Rc, sync::Arc};
-
-use crate::{
-    pane_group::element::PaneAxisElement, AppState, FollowerStatesByLeader, Pane, Workspace,
-};
+use crate::{pane_group::element::PaneAxisElement, AppState, FollowerState, Pane, Workspace};
 use anyhow::{anyhow, Result};
 use call::{ActiveCall, ParticipantLocation};
+use collections::HashMap;
 use gpui::{
     elements::*,
     geometry::{rect::RectF, vector::Vector2F},
@@ -13,6 +10,7 @@ use gpui::{
 };
 use project::Project;
 use serde::Deserialize;
+use std::{cell::RefCell, rc::Rc, sync::Arc};
 use theme::Theme;
 
 const HANDLE_HITBOX_SIZE: f32 = 4.0;
@@ -95,7 +93,7 @@ impl PaneGroup {
         &self,
         project: &ModelHandle<Project>,
         theme: &Theme,
-        follower_states: &FollowerStatesByLeader,
+        follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
         active_call: Option<&ModelHandle<ActiveCall>>,
         active_pane: &ViewHandle<Pane>,
         zoomed: Option<&AnyViewHandle>,
@@ -162,7 +160,7 @@ impl Member {
         project: &ModelHandle<Project>,
         basis: usize,
         theme: &Theme,
-        follower_states: &FollowerStatesByLeader,
+        follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
         active_call: Option<&ModelHandle<ActiveCall>>,
         active_pane: &ViewHandle<Pane>,
         zoomed: Option<&AnyViewHandle>,
@@ -179,19 +177,10 @@ impl Member {
                     ChildView::new(pane, cx).into_any()
                 };
 
-                let leader = follower_states
-                    .iter()
-                    .find_map(|(leader_id, follower_states)| {
-                        if follower_states.contains_key(pane) {
-                            Some(leader_id)
-                        } else {
-                            None
-                        }
-                    })
-                    .and_then(|leader_id| {
-                        let room = active_call?.read(cx).room()?.read(cx);
-                        room.remote_participant_for_peer_id(*leader_id)
-                    });
+                let leader = follower_states.get(pane).and_then(|state| {
+                    let room = active_call?.read(cx).room()?.read(cx);
+                    room.remote_participant_for_peer_id(state.leader_id)
+                });
 
                 let mut leader_border = Border::default();
                 let mut leader_status_box = None;
@@ -486,7 +475,7 @@ impl PaneAxis {
         project: &ModelHandle<Project>,
         basis: usize,
         theme: &Theme,
-        follower_state: &FollowerStatesByLeader,
+        follower_states: &HashMap<ViewHandle<Pane>, FollowerState>,
         active_call: Option<&ModelHandle<ActiveCall>>,
         active_pane: &ViewHandle<Pane>,
         zoomed: Option<&AnyViewHandle>,
@@ -515,7 +504,7 @@ impl PaneAxis {
                 project,
                 (basis + ix) * 10,
                 theme,
-                follower_state,
+                follower_states,
                 active_call,
                 active_pane,
                 zoomed,

crates/workspace/src/workspace.rs 🔗

@@ -12,7 +12,6 @@ mod workspace_settings;
 
 use anyhow::{anyhow, Context, Result};
 use call::ActiveCall;
-use channel::ChannelStore;
 use client::{
     proto::{self, PeerId},
     Client, Status, TypedEnvelope, UserStore,
@@ -79,7 +78,7 @@ use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use theme::{Theme, ThemeSettings};
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
-use util::{async_iife, ResultExt};
+use util::ResultExt;
 pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
 
 lazy_static! {
@@ -450,7 +449,6 @@ pub struct AppState {
     pub languages: Arc<LanguageRegistry>,
     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:
@@ -487,8 +485,6 @@ impl AppState {
         let http_client = util::http::FakeHttpClient::with_404_response();
         let client = Client::new(http_client.clone(), cx);
         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);
@@ -500,7 +496,7 @@ impl AppState {
             fs,
             languages,
             user_store,
-            channel_store,
+            // channel_store,
             workspace_store,
             initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
             build_window_options: |_, _, _| Default::default(),
@@ -573,11 +569,12 @@ pub struct Workspace {
     panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
     active_pane: ViewHandle<Pane>,
     last_active_center_pane: Option<WeakViewHandle<Pane>>,
+    last_active_view_id: Option<proto::ViewId>,
     status_bar: ViewHandle<StatusBar>,
     titlebar_item: Option<AnyViewHandle>,
     notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
     project: ModelHandle<Project>,
-    follower_states_by_leader: FollowerStatesByLeader,
+    follower_states: HashMap<ViewHandle<Pane>, FollowerState>,
     last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
     window_edited: bool,
     active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
@@ -602,10 +599,9 @@ pub struct ViewId {
     pub id: u64,
 }
 
-type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
-
 #[derive(Default)]
 struct FollowerState {
+    leader_id: PeerId,
     active_view_id: Option<ViewId>,
     items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
 }
@@ -786,6 +782,7 @@ impl Workspace {
             panes_by_item: Default::default(),
             active_pane: center_pane.clone(),
             last_active_center_pane: Some(center_pane.downgrade()),
+            last_active_view_id: None,
             status_bar,
             titlebar_item: None,
             notifications: Default::default(),
@@ -793,7 +790,7 @@ impl Workspace {
             bottom_dock,
             right_dock,
             project: project.clone(),
-            follower_states_by_leader: Default::default(),
+            follower_states: Default::default(),
             last_leaders_by_pane: Default::default(),
             window_edited: false,
             active_call,
@@ -934,7 +931,8 @@ impl Workspace {
                 app_state,
                 cx,
             )
-            .await;
+            .await
+            .unwrap_or_default();
 
             (workspace, opened_items)
         })
@@ -2510,13 +2508,16 @@ impl Workspace {
     }
 
     fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
-        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() {
+        self.follower_states.retain(|_, state| {
+            if state.leader_id == peer_id {
+                for item in state.items_by_leader_view_id.values() {
                     item.set_leader_peer_id(None, cx);
                 }
+                false
+            } else {
+                true
             }
-        }
+        });
         cx.notify();
     }
 
@@ -2529,10 +2530,15 @@ impl Workspace {
 
         self.last_leaders_by_pane
             .insert(pane.downgrade(), leader_id);
-        self.follower_states_by_leader
-            .entry(leader_id)
-            .or_default()
-            .insert(pane.clone(), Default::default());
+        self.unfollow(&pane, cx);
+        self.follower_states.insert(
+            pane.clone(),
+            FollowerState {
+                leader_id,
+                active_view_id: None,
+                items_by_leader_view_id: Default::default(),
+            },
+        );
         cx.notify();
 
         let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
@@ -2547,9 +2553,8 @@ impl Workspace {
             let response = request.await?;
             this.update(&mut cx, |this, _| {
                 let state = this
-                    .follower_states_by_leader
-                    .get_mut(&leader_id)
-                    .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
+                    .follower_states
+                    .get_mut(&pane)
                     .ok_or_else(|| anyhow!("following interrupted"))?;
                 state.active_view_id = if let Some(active_view_id) = response.active_view_id {
                     Some(ViewId::from_proto(active_view_id)?)
@@ -2644,12 +2649,10 @@ impl Workspace {
         }
 
         // 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;
-                }
+        for (pane, state) in &self.follower_states {
+            if leader_id == state.leader_id {
+                cx.focus(pane);
+                return None;
             }
         }
 
@@ -2662,36 +2665,37 @@ impl Workspace {
         pane: &ViewHandle<Pane>,
         cx: &mut ViewContext<Self>,
     ) -> Option<PeerId> {
-        for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
-            let leader_id = *leader_id;
-            if let Some(state) = states_by_pane.remove(pane) {
-                for (_, item) in state.items_by_leader_view_id {
-                    item.set_leader_peer_id(None, cx);
-                }
-
-                if states_by_pane.is_empty() {
-                    self.follower_states_by_leader.remove(&leader_id);
-                    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();
-                }
+        let state = self.follower_states.remove(pane)?;
+        let leader_id = state.leader_id;
+        for (_, item) in state.items_by_leader_view_id {
+            item.set_leader_peer_id(None, cx);
+        }
 
-                cx.notify();
-                return Some(leader_id);
-            }
+        if self
+            .follower_states
+            .values()
+            .all(|state| state.leader_id != state.leader_id)
+        {
+            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();
         }
-        None
+
+        cx.notify();
+        Some(leader_id)
     }
 
     pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
-        self.follower_states_by_leader.contains_key(&peer_id)
+        self.follower_states
+            .values()
+            .any(|state| state.leader_id == peer_id)
     }
 
     fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
@@ -2862,6 +2866,7 @@ impl Workspace {
 
         cx.notify();
 
+        self.last_active_view_id = active_view_id.clone();
         proto::FollowResponse {
             active_view_id,
             views: self
@@ -2913,8 +2918,8 @@ impl Workspace {
         match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
             proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
                 this.update(cx, |this, _| {
-                    if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
-                        for state in state.values_mut() {
+                    for (_, state) in &mut this.follower_states {
+                        if state.leader_id == leader_id {
                             state.active_view_id =
                                 if let Some(active_view_id) = update_active_view.id.clone() {
                                     Some(ViewId::from_proto(active_view_id)?)
@@ -2936,8 +2941,8 @@ impl Workspace {
                 let mut tasks = Vec::new();
                 this.update(cx, |this, cx| {
                     let project = this.project.clone();
-                    if let Some(state) = this.follower_states_by_leader.get_mut(&leader_id) {
-                        for state in state.values_mut() {
+                    for (_, state) in &mut this.follower_states {
+                        if state.leader_id == leader_id {
                             let view_id = ViewId::from_proto(id.clone())?;
                             if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
                                 tasks.push(item.apply_update_proto(&project, variant.clone(), cx));
@@ -2950,10 +2955,9 @@ impl Workspace {
             }
             proto::update_followers::Variant::CreateView(view) => {
                 let panes = this.read_with(cx, |this, _| {
-                    this.follower_states_by_leader
-                        .get(&leader_id)
-                        .into_iter()
-                        .flat_map(|states_by_pane| states_by_pane.keys())
+                    this.follower_states
+                        .iter()
+                        .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane))
                         .cloned()
                         .collect()
                 })?;
@@ -3012,11 +3016,7 @@ impl Workspace {
         for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
             let items = futures::future::try_join_all(item_tasks).await?;
             this.update(cx, |this, cx| {
-                let state = this
-                    .follower_states_by_leader
-                    .get_mut(&leader_id)?
-                    .get_mut(&pane)?;
-
+                let state = this.follower_states.get_mut(&pane)?;
                 for (id, item) in leader_view_ids.into_iter().zip(items) {
                     item.set_leader_peer_id(Some(leader_id), cx);
                     state.items_by_leader_view_id.insert(id, item);
@@ -3028,7 +3028,7 @@ impl Workspace {
         Ok(())
     }
 
-    fn update_active_view_for_followers(&self, cx: &AppContext) {
+    fn update_active_view_for_followers(&mut self, cx: &AppContext) {
         let mut is_project_item = true;
         let mut update = proto::UpdateActiveView::default();
         if self.active_pane.read(cx).has_focus() {
@@ -3046,11 +3046,14 @@ impl Workspace {
             }
         }
 
-        self.update_followers(
-            is_project_item,
-            proto::update_followers::Variant::UpdateActiveView(update),
-            cx,
-        );
+        if update.id != self.last_active_view_id {
+            self.last_active_view_id = update.id.clone();
+            self.update_followers(
+                is_project_item,
+                proto::update_followers::Variant::UpdateActiveView(update),
+                cx,
+            );
+        }
     }
 
     fn update_followers(
@@ -3070,15 +3073,7 @@ impl Workspace {
     }
 
     pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
-        self.follower_states_by_leader
-            .iter()
-            .find_map(|(leader_id, state)| {
-                if state.contains_key(pane) {
-                    Some(*leader_id)
-                } else {
-                    None
-                }
-            })
+        self.follower_states.get(pane).map(|state| state.leader_id)
     }
 
     fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
@@ -3106,17 +3101,23 @@ impl Workspace {
             }
         };
 
-        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 {
+        for (pane, state) in &self.follower_states {
+            if state.leader_id != leader_id {
+                continue;
+            }
+            if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
+                if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
                     if leader_in_this_project || !item.is_project_item(cx) {
                         items_to_activate.push((pane.clone(), item.boxed_clone()));
                     }
-                    continue;
+                } else {
+                    log::warn!(
+                        "unknown view id {:?} for leader {:?}",
+                        active_view_id,
+                        leader_id
+                    );
                 }
+                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)));
@@ -3394,140 +3395,124 @@ impl Workspace {
         serialized_workspace: SerializedWorkspace,
         paths_to_open: Vec<Option<ProjectPath>>,
         cx: &mut AppContext,
-    ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
+    ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
         cx.spawn(|mut cx| async move {
-            let result = async_iife! {{
-                let (project, old_center_pane) =
-                workspace.read_with(&cx, |workspace, _| {
-                    (
-                        workspace.project().clone(),
-                        workspace.last_active_center_pane.clone(),
-                    )
-                })?;
+            let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| {
+                (
+                    workspace.project().clone(),
+                    workspace.last_active_center_pane.clone(),
+                )
+            })?;
 
-                let mut center_items = None;
-                let mut center_group = None;
-                // Traverse the splits tree and add to things
-                if let Some((group, active_pane, items)) = serialized_workspace
-                        .center_group
-                        .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
-                        .await {
-                    center_items = Some(items);
-                    center_group = Some((group, active_pane))
-                }
+            let mut center_group = None;
+            let mut center_items = None;
+            // Traverse the splits tree and add to things
+            if let Some((group, active_pane, items)) = serialized_workspace
+                .center_group
+                .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
+                .await
+            {
+                center_items = Some(items);
+                center_group = Some((group, active_pane))
+            }
 
-                let resulting_list = cx.read(|cx| {
-                    let mut opened_items = center_items
-                        .unwrap_or_default()
-                        .into_iter()
-                        .filter_map(|item| {
-                            let item = item?;
-                            let project_path = item.project_path(cx)?;
-                            Some((project_path, item))
-                        })
-                        .collect::<HashMap<_, _>>();
+            let mut items_by_project_path = cx.read(|cx| {
+                center_items
+                    .unwrap_or_default()
+                    .into_iter()
+                    .filter_map(|item| {
+                        let item = item?;
+                        let project_path = item.project_path(cx)?;
+                        Some((project_path, item))
+                    })
+                    .collect::<HashMap<_, _>>()
+            });
 
-                    paths_to_open
-                        .into_iter()
-                        .map(|path_to_open| {
-                            path_to_open.map(|path_to_open| {
-                                Ok(opened_items.remove(&path_to_open))
-                            })
-                            .transpose()
-                            .map(|item| item.flatten())
-                            .transpose()
-                        })
-                        .collect::<Vec<_>>()
-                });
+            let opened_items = paths_to_open
+                .into_iter()
+                .map(|path_to_open| {
+                    path_to_open
+                        .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
+                })
+                .collect::<Vec<_>>();
 
-                // Remove old panes from workspace panes list
-                workspace.update(&mut cx, |workspace, cx| {
-                    if let Some((center_group, active_pane)) = center_group {
-                        workspace.remove_panes(workspace.center.root.clone(), cx);
+            // Remove old panes from workspace panes list
+            workspace.update(&mut cx, |workspace, cx| {
+                if let Some((center_group, active_pane)) = center_group {
+                    workspace.remove_panes(workspace.center.root.clone(), cx);
 
-                        // Swap workspace center group
-                        workspace.center = PaneGroup::with_root(center_group);
+                    // Swap workspace center group
+                    workspace.center = PaneGroup::with_root(center_group);
 
-                        // Change the focus to the workspace first so that we retrigger focus in on the pane.
-                        cx.focus_self();
+                    // Change the focus to the workspace first so that we retrigger focus in on the pane.
+                    cx.focus_self();
 
-                        if let Some(active_pane) = active_pane {
-                            cx.focus(&active_pane);
-                        } else {
-                            cx.focus(workspace.panes.last().unwrap());
-                        }
+                    if let Some(active_pane) = active_pane {
+                        cx.focus(&active_pane);
                     } else {
-                        let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
-                        if let Some(old_center_handle) = old_center_handle {
-                            cx.focus(&old_center_handle)
-                        } else {
-                            cx.focus_self()
-                        }
+                        cx.focus(workspace.panes.last().unwrap());
                     }
+                } else {
+                    let old_center_handle = old_center_pane.and_then(|weak| weak.upgrade(cx));
+                    if let Some(old_center_handle) = old_center_handle {
+                        cx.focus(&old_center_handle)
+                    } else {
+                        cx.focus_self()
+                    }
+                }
 
-                    let docks = serialized_workspace.docks;
-                    workspace.left_dock.update(cx, |dock, cx| {
-                        dock.set_open(docks.left.visible, cx);
-                        if let Some(active_panel) = docks.left.active_panel {
-                            if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
-                                dock.activate_panel(ix, cx);
-                            }
-                        }
-                                dock.active_panel()
-                                    .map(|panel| {
-                                        panel.set_zoomed(docks.left.zoom, cx)
-                                    });
-                                if docks.left.visible && docks.left.zoom {
-                                    cx.focus_self()
-                                }
-                    });
-                    // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
-                    workspace.right_dock.update(cx, |dock, cx| {
-                        dock.set_open(docks.right.visible, cx);
-                        if let Some(active_panel) = docks.right.active_panel {
-                            if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
-                                dock.activate_panel(ix, cx);
-
-                            }
+                let docks = serialized_workspace.docks;
+                workspace.left_dock.update(cx, |dock, cx| {
+                    dock.set_open(docks.left.visible, cx);
+                    if let Some(active_panel) = docks.left.active_panel {
+                        if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+                            dock.activate_panel(ix, cx);
                         }
-                                dock.active_panel()
-                                    .map(|panel| {
-                                        panel.set_zoomed(docks.right.zoom, cx)
-                                    });
-
-                                if docks.right.visible && docks.right.zoom {
-                                    cx.focus_self()
-                                }
-                    });
-                    workspace.bottom_dock.update(cx, |dock, cx| {
-                        dock.set_open(docks.bottom.visible, cx);
-                        if let Some(active_panel) = docks.bottom.active_panel {
-                            if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
-                                dock.activate_panel(ix, cx);
-                            }
+                    }
+                    dock.active_panel()
+                        .map(|panel| panel.set_zoomed(docks.left.zoom, cx));
+                    if docks.left.visible && docks.left.zoom {
+                        cx.focus_self()
+                    }
+                });
+                // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something
+                workspace.right_dock.update(cx, |dock, cx| {
+                    dock.set_open(docks.right.visible, cx);
+                    if let Some(active_panel) = docks.right.active_panel {
+                        if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+                            dock.activate_panel(ix, cx);
                         }
+                    }
+                    dock.active_panel()
+                        .map(|panel| panel.set_zoomed(docks.right.zoom, cx));
 
-                        dock.active_panel()
-                            .map(|panel| {
-                                panel.set_zoomed(docks.bottom.zoom, cx)
-                            });
-
-                        if docks.bottom.visible && docks.bottom.zoom {
-                            cx.focus_self()
+                    if docks.right.visible && docks.right.zoom {
+                        cx.focus_self()
+                    }
+                });
+                workspace.bottom_dock.update(cx, |dock, cx| {
+                    dock.set_open(docks.bottom.visible, cx);
+                    if let Some(active_panel) = docks.bottom.active_panel {
+                        if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) {
+                            dock.activate_panel(ix, cx);
                         }
-                    });
+                    }
 
+                    dock.active_panel()
+                        .map(|panel| panel.set_zoomed(docks.bottom.zoom, cx));
 
-                    cx.notify();
-                })?;
+                    if docks.bottom.visible && docks.bottom.zoom {
+                        cx.focus_self()
+                    }
+                });
 
-                // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
-                workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
+                cx.notify();
+            })?;
 
-                Ok::<_, anyhow::Error>(resulting_list)
-            }};
+            // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
+            workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))?;
 
-            result.await.unwrap_or_default()
+            Ok(opened_items)
         })
     }
 
@@ -3536,15 +3521,12 @@ impl Workspace {
         let client = project.read(cx).client();
         let user_store = project.read(cx).user_store();
 
-        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,
             fs: project.read(cx).fs().clone(),
             build_window_options: |_, _, _| Default::default(),
             initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
@@ -3601,7 +3583,7 @@ async fn open_items(
     mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
     app_state: Arc<AppState>,
     mut cx: AsyncAppContext,
-) -> Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>> {
+) -> Result<Vec<Option<Result<Box<dyn ItemHandle>>>>> {
     let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
 
     if let Some(serialized_workspace) = serialized_workspace {
@@ -3619,16 +3601,19 @@ async fn open_items(
                     cx,
                 )
             })
-            .await;
+            .await?;
 
         let restored_project_paths = cx.read(|cx| {
             restored_items
                 .iter()
-                .filter_map(|item| item.as_ref()?.as_ref().ok()?.project_path(cx))
+                .filter_map(|item| item.as_ref()?.project_path(cx))
                 .collect::<HashSet<_>>()
         });
 
-        opened_items = restored_items;
+        for restored_item in restored_items {
+            opened_items.push(restored_item.map(Ok));
+        }
+
         project_paths_to_open
             .iter_mut()
             .for_each(|(_, project_path)| {
@@ -3681,7 +3666,7 @@ async fn open_items(
         }
     }
 
-    opened_items
+    Ok(opened_items)
 }
 
 fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
@@ -3817,7 +3802,7 @@ impl View for Workspace {
                                                     self.center.render(
                                                         &project,
                                                         &theme,
-                                                        &self.follower_states_by_leader,
+                                                        &self.follower_states,
                                                         self.active_call(),
                                                         self.active_pane(),
                                                         self.zoomed

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

@@ -165,17 +165,25 @@ impl LspAdapter for RustLspAdapter {
                 lazy_static! {
                     static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap();
                 }
-
                 let detail = completion.detail.as_ref().unwrap();
-                if detail.starts_with("fn(") {
-                    let text = REGEX.replace(&completion.label, &detail[2..]).to_string();
-                    let source = Rope::from(format!("fn {} {{}}", text).as_str());
-                    let runs = language.highlight_text(&source, 3..3 + text.len());
-                    return Some(CodeLabel {
-                        filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
-                        text,
-                        runs,
-                    });
+                const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"];
+                let prefix = FUNCTION_PREFIXES
+                    .iter()
+                    .find_map(|prefix| detail.strip_prefix(*prefix).map(|suffix| (prefix, suffix)));
+                // fn keyword should be followed by opening parenthesis.
+                if let Some((prefix, suffix)) = prefix {
+                    if suffix.starts_with('(') {
+                        let text = REGEX.replace(&completion.label, suffix).to_string();
+                        let source = Rope::from(format!("{prefix} {} {{}}", text).as_str());
+                        let run_start = prefix.len() + 1;
+                        let runs =
+                            language.highlight_text(&source, run_start..run_start + text.len());
+                        return Some(CodeLabel {
+                            filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
+                            text,
+                            runs,
+                        });
+                    }
                 }
             }
             Some(kind) => {
@@ -377,7 +385,28 @@ mod tests {
                 ],
             })
         );
-
+        assert_eq!(
+            language
+                .label_for_completion(&lsp::CompletionItem {
+                    kind: Some(lsp::CompletionItemKind::FUNCTION),
+                    label: "hello(…)".to_string(),
+                    detail: Some("async fn(&mut Option<T>) -> Vec<T>".to_string()),
+                    ..Default::default()
+                })
+                .await,
+            Some(CodeLabel {
+                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
+                filter_range: 0..5,
+                runs: vec![
+                    (0..5, highlight_function),
+                    (7..10, highlight_keyword),
+                    (11..17, highlight_type),
+                    (18..19, highlight_type),
+                    (25..28, highlight_type),
+                    (29..30, highlight_type),
+                ],
+            })
+        );
         assert_eq!(
             language
                 .label_for_completion(&lsp::CompletionItem {

crates/zed/src/main.rs 🔗

@@ -3,7 +3,6 @@
 
 use anyhow::{anyhow, Context, Result};
 use backtrace::Backtrace;
-use channel::ChannelStore;
 use cli::{
     ipc::{self, IpcSender},
     CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
@@ -78,7 +77,8 @@ fn main() {
     let mut app = gpui::App::new(Assets).unwrap();
 
     let installation_id = app.background().block(installation_id()).ok();
-    init_panic_hook(&app, installation_id.clone());
+    let session_id = Uuid::new_v4().to_string();
+    init_panic_hook(&app, installation_id.clone(), session_id.clone());
 
     load_embedded_fonts(&app);
 
@@ -132,8 +132,6 @@ fn main() {
 
         languages::init(languages.clone(), node_runtime.clone(), cx);
         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());
@@ -150,7 +148,7 @@ fn main() {
         outline::init(cx);
         project_symbols::init(cx);
         project_panel::init(Assets, cx);
-        channel::init(&client);
+        channel::init(&client, user_store.clone(), cx);
         diagnostics::init(cx);
         search::init(cx);
         semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
@@ -172,13 +170,12 @@ fn main() {
         })
         .detach();
 
-        client.telemetry().start(installation_id, cx);
+        client.telemetry().start(installation_id, session_id, cx);
 
         let app_state = Arc::new(AppState {
             languages,
             client: client.clone(),
             user_store,
-            channel_store,
             fs,
             build_window_options,
             initialize_workspace,
@@ -387,6 +384,7 @@ struct Panic {
     panicked_on: u128,
     #[serde(skip_serializing_if = "Option::is_none")]
     installation_id: Option<String>,
+    session_id: String,
 }
 
 #[derive(Serialize)]
@@ -397,7 +395,7 @@ struct PanicRequest {
 
 static PANIC_COUNT: AtomicU32 = AtomicU32::new(0);
 
-fn init_panic_hook(app: &App, installation_id: Option<String>) {
+fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: String) {
     let is_pty = stdout_is_a_pty();
     let platform = app.platform();
 
@@ -462,7 +460,7 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
                 line: location.line(),
             }),
             app_version: app_version.clone(),
-            release_channel: RELEASE_CHANNEL.dev_name().into(),
+            release_channel: RELEASE_CHANNEL.display_name().into(),
             os_name: platform.os_name().into(),
             os_version: platform
                 .os_version()
@@ -475,13 +473,14 @@ fn init_panic_hook(app: &App, installation_id: Option<String>) {
                 .as_millis(),
             backtrace,
             installation_id: installation_id.clone(),
+            session_id: session_id.clone(),
         };
 
-        if is_pty {
-            if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
-                eprintln!("{}", panic_data_json);
-            }
-        } else {
+        if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
+            log::error!("{}", panic_data_json);
+        }
+
+        if !is_pty {
             if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() {
                 let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
                 let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));

crates/zed/src/open_listener.rs 🔗

@@ -3,8 +3,6 @@ use cli::{ipc::IpcSender, CliRequest, CliResponse};
 use futures::channel::mpsc;
 use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
 use std::ffi::OsStr;
-use std::fs::OpenOptions;
-use std::io::Write;
 use std::os::unix::prelude::OsStrExt;
 use std::sync::atomic::Ordering;
 use std::{path::PathBuf, sync::atomic::AtomicBool};

crates/zed/src/zed.rs 🔗

@@ -2424,6 +2424,7 @@ mod tests {
             state.build_window_options = build_window_options;
             theme::init((), cx);
             audio::init((), cx);
+            channel::init(&app_state.client, app_state.user_store.clone(), cx);
             call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
             workspace::init(app_state.clone(), cx);
             Project::init_settings(cx);

docs/building-zed.md 🔗

@@ -75,8 +75,7 @@ Expect this to take 30min to an hour! Some of these steps will take quite a whil
     - If you are just using the latest version, but not working on zed:
       - `cargo run --release`
     - If you need to run the collaboration server locally:
-      - `script/zed-with-local-servers`
-    - If you need to test collaboration with mutl
+      - `script/zed-local`
 
 ## Troubleshooting
 

docs/local-collaboration.md 🔗

@@ -17,6 +17,6 @@
 ## Testing collab locally
 
 1. Run `foreman start` from the root of the repo.
-1. In another terminal run `script/start-local-collaboration`.
+1. In another terminal run `script/zed-local -2`.
 1. Two copies of Zed will open. Add yourself as a contact in the one that is not you.
 1. Start a collaboration session as normal with any open project.

rust-toolchain.toml 🔗

@@ -1,4 +1,4 @@
 [toolchain]
-channel = "1.72.1"
+channel = "1.73"
 components = [ "rustfmt" ]
 targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ]

script/crate-dep-graph 🔗

@@ -0,0 +1,19 @@
+#!/bin/bash
+
+set -e
+
+if [[ -x cargo-depgraph ]]; then
+    cargo install cargo-depgraph
+fi
+
+graph_file=target/crate-graph.html
+
+cargo depgraph \
+    --workspace-only \
+    --offline \
+    --root=zed,cli,collab \
+    --dedup-transitive-deps \
+    | dot -Tsvg > $graph_file
+
+echo "open $graph_file"
+open $graph_file

script/start-local-collaboration 🔗

@@ -1,59 +0,0 @@
-#!/bin/bash
-
-set -e
-
-if [[ -z "$GITHUB_TOKEN" ]]; then
-  cat <<-MESSAGE
-Missing \`GITHUB_TOKEN\` environment variable. This token is needed
-for fetching your GitHub identity from the command-line.
-
-Create an access token here: https://github.com/settings/tokens
-Then edit your \`~/.zshrc\` (or other shell initialization script),
-adding a line like this:
-
-    export GITHUB_TOKEN="(the token)"
-
-MESSAGE
-  exit 1
-fi
-
-# Install jq if it's not installed
-if ! command -v jq &> /dev/null; then
-    echo "Installing jq..."
-    brew install jq
-fi
-
-# Start one Zed instance as the current user and a second instance with a different user.
-username_1=$(curl -sH "Authorization: bearer $GITHUB_TOKEN" https://api.github.com/user | jq -r .login)
-username_2=nathansobo
-if [[ $username_1 == $username_2 ]]; then
-  username_2=as-cii
-fi
-
-# Make each Zed instance take up half of the screen.
-output=$(system_profiler SPDisplaysDataType -json)
-main_display=$(echo "$output" | jq '.SPDisplaysDataType[].spdisplays_ndrvs[] | select(.spdisplays_main == "spdisplays_yes")')
-resolution=$(echo "$main_display" | jq -r '._spdisplays_resolution')
-width=$(echo "$resolution" | jq -Rr 'match("(\\d+) x (\\d+)").captures[0].string')
-half_width=$(($width / 2))
-height=$(echo "$resolution" | jq -Rr 'match("(\\d+) x (\\d+)").captures[1].string')
-y=0
-
-position_1=0,${y}
-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}
-
-cargo build
-sleep 0.5
-
-# Start the two Zed child processes. Open the given paths with the first instance.
-trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
-ZED_IMPERSONATE=${ZED_IMPERSONATE:=${username_1}} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ &
-SECOND=true ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed &
-wait

script/zed-local 🔗

@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+
+const {spawn, execFileSync} = require('child_process')
+
+const RESOLUTION_REGEX = /(\d+) x (\d+)/
+const DIGIT_FLAG_REGEX = /^--?(\d+)$/
+
+const args = process.argv.slice(2)
+
+// Parse the number of Zed instances to spawn.
+let instanceCount = 1
+const digitMatch = args[0]?.match(DIGIT_FLAG_REGEX)
+if (digitMatch) {
+  instanceCount = parseInt(digitMatch[1])
+  args.shift()
+}
+if (instanceCount > 4) {
+  throw new Error('Cannot spawn more than 4 instances')
+}
+
+// Parse the resolution of the main screen
+const displayInfo = JSON.parse(
+  execFileSync(
+    'system_profiler',
+    ['SPDisplaysDataType', '-json'],
+    {encoding: 'utf8'}
+  )
+)
+const mainDisplayResolution = displayInfo
+  ?.SPDisplaysDataType[0]
+  ?.spdisplays_ndrvs
+  ?.find(entry => entry.spdisplays_main === "spdisplays_yes")
+  ?._spdisplays_resolution
+  ?.match(RESOLUTION_REGEX)
+if (!mainDisplayResolution) {
+  throw new Error('Could not parse screen resolution')
+}
+const screenWidth = parseInt(mainDisplayResolution[1])
+const screenHeight = parseInt(mainDisplayResolution[2])
+
+// Determine the window size for each instance
+let instanceWidth = screenWidth
+let instanceHeight = screenHeight
+if (instanceCount > 1) {
+  instanceWidth = Math.floor(screenWidth / 2)
+  if (instanceCount > 2) {
+    instanceHeight = Math.floor(screenHeight / 2)
+  }
+}
+
+let users = [
+  'nathansobo',
+  'as-cii',
+  'maxbrunsfeld',
+  'iamnbutler'
+]
+
+// If a user is specified, make sure it's first in the list
+const user = process.env.ZED_IMPERSONATE
+if (user) {
+  users = [user].concat(users.filter(u => u !== user))
+}
+
+const positions = [
+  '0,0',
+  `${instanceWidth},0`,
+  `0,${instanceHeight}`,
+  `${instanceWidth},${instanceHeight}`
+]
+
+execFileSync('cargo', ['build'], {stdio: 'inherit'})
+
+setTimeout(() => {
+  for (let i = 0; i < instanceCount; i++) {
+    spawn('target/debug/Zed', i == 0 ? args : [], {
+      stdio: 'inherit',
+      env: {
+        ZED_IMPERSONATE: users[i],
+        ZED_WINDOW_POSITION: positions[i],
+        ZED_STATELESS: '1',
+        ZED_ALWAYS_ACTIVE: '1',
+        ZED_SERVER_URL: 'http://localhost:8080',
+        ZED_ADMIN_API_TOKEN: 'secret',
+        ZED_WINDOW_SIZE: `${instanceWidth},${instanceHeight}`
+      }
+    })
+  }
+}, 0.1)

script/zed-with-local-servers 🔗

@@ -1,6 +0,0 @@
-#!/bin/bash
-
-: "${ZED_IMPERSONATE:=as-cii}"
-export ZED_IMPERSONATE
-
-ZED_ADMIN_API_TOKEN=secret ZED_SERVER_URL=http://localhost:8080 cargo run $@