From 83455028b0343fe952bcd3edff928885e53b4f2a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 19 Sep 2023 09:17:57 -0700 Subject: [PATCH 01/45] Procfile: run zed.dev via 'next dev', not 'vercel dev' --- Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Procfile b/Procfile index 127fffbed1..f6fde3cd92 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,4 @@ -web: cd ../zed.dev && PORT=3000 npx vercel dev +web: cd ../zed.dev && PORT=3000 npm run dev collab: cd crates/collab && cargo run serve livekit: livekit-server --dev postgrest: postgrest crates/collab/admin_api.conf From c71566e7f5a1a7023214bb5ae91630f6cae42e8d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 19 Sep 2023 15:37:33 -0700 Subject: [PATCH 02/45] Make project id optional when following - server only --- .../20221109000000_test_schema.sql | 2 +- ...142700_allow_following_without_project.sql | 1 + crates/collab/src/db/queries/projects.rs | 95 +++++++++++++++++-- crates/collab/src/db/queries/rooms.rs | 61 +++++++++++- crates/collab/src/db/tables/follower.rs | 2 +- .../collab/src/db/tables/room_participant.rs | 10 ++ crates/collab/src/rpc.rs | 60 ++++++------ crates/rpc/proto/zed.proto | 23 +++-- crates/rpc/src/proto.rs | 3 - crates/rpc/src/rpc.rs | 2 +- 10 files changed, 204 insertions(+), 55 deletions(-) create mode 100644 crates/collab/migrations/20230918142700_allow_following_without_project.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index ab12039b10..d0c4ead5ad 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -175,7 +175,7 @@ CREATE TABLE "servers" ( CREATE TABLE "followers" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE, - "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, + "project_id" INTEGER REFERENCES projects (id) ON DELETE CASCADE, "leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, "leader_connection_id" INTEGER NOT NULL, "follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, diff --git a/crates/collab/migrations/20230918142700_allow_following_without_project.sql b/crates/collab/migrations/20230918142700_allow_following_without_project.sql new file mode 100644 index 0000000000..e0cc0141ec --- /dev/null +++ b/crates/collab/migrations/20230918142700_allow_following_without_project.sql @@ -0,0 +1 @@ +ALTER TABLE followers ALTER COLUMN project_id DROP NOT NULL; diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 31c7cdae3e..80e71eb1eb 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -738,7 +738,7 @@ impl Database { Condition::any() .add( Condition::all() - .add(follower::Column::ProjectId.eq(project_id)) + .add(follower::Column::ProjectId.eq(Some(project_id))) .add( follower::Column::LeaderConnectionServerId .eq(connection.owner_id), @@ -747,7 +747,7 @@ impl Database { ) .add( Condition::all() - .add(follower::Column::ProjectId.eq(project_id)) + .add(follower::Column::ProjectId.eq(Some(project_id))) .add( follower::Column::FollowerConnectionServerId .eq(connection.owner_id), @@ -862,13 +862,95 @@ impl Database { .await } + pub async fn check_can_follow( + &self, + room_id: RoomId, + project_id: Option, + leader_id: ConnectionId, + follower_id: ConnectionId, + ) -> Result<()> { + let mut found_leader = false; + let mut found_follower = false; + self.transaction(|tx| async move { + if let Some(project_id) = project_id { + let mut rows = project_collaborator::Entity::find() + .filter(project_collaborator::Column::ProjectId.eq(project_id)) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + let connection = row.connection(); + if connection == leader_id { + found_leader = true; + } else if connection == follower_id { + found_follower = true; + } + } + } else { + let mut rows = room_participant::Entity::find() + .filter(room_participant::Column::RoomId.eq(room_id)) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + if let Some(connection) = row.answering_connection() { + if connection == leader_id { + found_leader = true; + } else if connection == follower_id { + found_follower = true; + } + } + } + } + + if !found_leader || !found_follower { + Err(anyhow!("not a room participant"))?; + } + + Ok(()) + }) + .await + } + + pub async fn check_can_unfollow( + &self, + room_id: RoomId, + project_id: Option, + leader_id: ConnectionId, + follower_id: ConnectionId, + ) -> Result<()> { + self.transaction(|tx| async move { + follower::Entity::find() + .filter( + Condition::all() + .add(follower::Column::RoomId.eq(room_id)) + .add(follower::Column::ProjectId.eq(project_id)) + .add(follower::Column::LeaderConnectionId.eq(leader_id.id as i32)) + .add(follower::Column::FollowerConnectionId.eq(follower_id.id as i32)) + .add( + follower::Column::LeaderConnectionServerId + .eq(leader_id.owner_id as i32), + ) + .add( + follower::Column::FollowerConnectionServerId + .eq(follower_id.owner_id as i32), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("not a follower"))?; + Ok(()) + }) + .await + } + pub async fn follow( &self, - project_id: ProjectId, + room_id: RoomId, + project_id: Option, leader_connection: ConnectionId, follower_connection: ConnectionId, ) -> Result> { - let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { follower::ActiveModel { room_id: ActiveValue::set(room_id), @@ -894,15 +976,16 @@ impl Database { pub async fn unfollow( &self, - project_id: ProjectId, + room_id: RoomId, + project_id: Option, leader_connection: ConnectionId, follower_connection: ConnectionId, ) -> Result> { - let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { follower::Entity::delete_many() .filter( Condition::all() + .add(follower::Column::RoomId.eq(room_id)) .add(follower::Column::ProjectId.eq(project_id)) .add( follower::Column::LeaderConnectionServerId diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index fb81fef176..651d58c265 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -960,6 +960,65 @@ impl Database { Ok(room) } + pub async fn room_id_for_connection(&self, connection_id: ConnectionId) -> Result { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryRoomId { + RoomId, + } + + self.transaction(|tx| async move { + Ok(room_participant::Entity::find() + .select_only() + .column(room_participant::Column::RoomId) + .filter( + Condition::all() + .add(room_participant::Column::AnsweringConnectionId.eq(connection_id.id)) + .add( + room_participant::Column::AnsweringConnectionServerId + .eq(ServerId(connection_id.owner_id as i32)), + ), + ) + .into_values::<_, QueryRoomId>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no room for connection {:?}", connection_id))?) + }) + .await + } + + pub async fn room_connection_ids( + &self, + room_id: RoomId, + connection_id: ConnectionId, + ) -> Result>> { + self.room_transaction(room_id, |tx| async move { + let mut participants = room_participant::Entity::find() + .filter(room_participant::Column::RoomId.eq(room_id)) + .stream(&*tx) + .await?; + + let mut is_participant = false; + let mut connection_ids = HashSet::default(); + while let Some(participant) = participants.next().await { + let participant = participant?; + if let Some(answering_connection) = participant.answering_connection() { + if answering_connection == connection_id { + is_participant = true; + } else { + connection_ids.insert(answering_connection); + } + } + } + + if !is_participant { + Err(anyhow!("not a room participant"))?; + } + + Ok(connection_ids) + }) + .await + } + async fn get_channel_room( &self, room_id: RoomId, @@ -1064,7 +1123,7 @@ impl Database { followers.push(proto::Follower { leader_id: Some(db_follower.leader_connection().into()), follower_id: Some(db_follower.follower_connection().into()), - project_id: db_follower.project_id.to_proto(), + project_id: db_follower.project_id.map(|id| id.to_proto()), }); } diff --git a/crates/collab/src/db/tables/follower.rs b/crates/collab/src/db/tables/follower.rs index ffd45434e9..b5bc163b21 100644 --- a/crates/collab/src/db/tables/follower.rs +++ b/crates/collab/src/db/tables/follower.rs @@ -8,7 +8,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: FollowerId, pub room_id: RoomId, - pub project_id: ProjectId, + pub project_id: Option, pub leader_connection_server_id: ServerId, pub leader_connection_id: i32, pub follower_connection_server_id: ServerId, diff --git a/crates/collab/src/db/tables/room_participant.rs b/crates/collab/src/db/tables/room_participant.rs index 537cac9f14..57d79fa830 100644 --- a/crates/collab/src/db/tables/room_participant.rs +++ b/crates/collab/src/db/tables/room_participant.rs @@ -1,4 +1,5 @@ use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId}; +use rpc::ConnectionId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] @@ -19,6 +20,15 @@ pub struct Model { pub calling_connection_server_id: Option, } +impl Model { + pub fn answering_connection(&self) -> Option { + Some(ConnectionId { + owner_id: self.answering_connection_server_id?.0 as u32, + id: self.answering_connection_id? as u32, + }) + } +} + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index fa9cc5ef1e..b3af2d4e98 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1883,24 +1883,19 @@ async fn follow( response: Response, session: Session, ) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); + let room_id = RoomId::from_proto(request.room_id); + let project_id = request.project_id.map(ProjectId::from_proto); let leader_id = request .leader_id .ok_or_else(|| anyhow!("invalid leader id"))? .into(); let follower_id = session.connection_id; - { - let project_connection_ids = session - .db() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; - - if !project_connection_ids.contains(&leader_id) { - Err(anyhow!("no such peer"))?; - } - } + session + .db() + .await + .check_can_follow(room_id, project_id, leader_id, session.connection_id) + .await?; let mut response_payload = session .peer @@ -1914,7 +1909,7 @@ async fn follow( let room = session .db() .await - .follow(project_id, leader_id, follower_id) + .follow(room_id, project_id, leader_id, follower_id) .await?; room_updated(&room, &session.peer); @@ -1922,22 +1917,19 @@ async fn follow( } async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); + let room_id = RoomId::from_proto(request.room_id); + let project_id = request.project_id.map(ProjectId::from_proto); let leader_id = request .leader_id .ok_or_else(|| anyhow!("invalid leader id"))? .into(); let follower_id = session.connection_id; - if !session + session .db() .await - .project_connection_ids(project_id, session.connection_id) - .await? - .contains(&leader_id) - { - Err(anyhow!("no such peer"))?; - } + .check_can_unfollow(room_id, project_id, leader_id, session.connection_id) + .await?; session .peer @@ -1946,7 +1938,7 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { let room = session .db() .await - .unfollow(project_id, leader_id, follower_id) + .unfollow(room_id, project_id, leader_id, follower_id) .await?; room_updated(&room, &session.peer); @@ -1954,13 +1946,19 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { } async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); - let project_connection_ids = session - .db - .lock() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; + let room_id = RoomId::from_proto(request.room_id); + let database = session.db.lock().await; + + let connection_ids = if let Some(project_id) = request.project_id { + let project_id = ProjectId::from_proto(project_id); + database + .project_connection_ids(project_id, session.connection_id) + .await? + } else { + database + .room_connection_ids(room_id, session.connection_id) + .await? + }; let leader_id = request.variant.as_ref().and_then(|variant| match variant { proto::update_followers::Variant::CreateView(payload) => payload.leader_id, @@ -1969,9 +1967,7 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) -> }); for follower_peer_id in request.follower_ids.iter().copied() { let follower_connection_id = follower_peer_id.into(); - if project_connection_ids.contains(&follower_connection_id) - && Some(follower_peer_id) != leader_id - { + if Some(follower_peer_id) != leader_id && connection_ids.contains(&follower_connection_id) { session.peer.forward_send( session.connection_id, follower_connection_id, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index c2bb9e9cef..9c1ec4e613 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -274,7 +274,7 @@ message ParticipantProject { message Follower { PeerId leader_id = 1; PeerId follower_id = 2; - uint64 project_id = 3; + optional uint64 project_id = 3; } message ParticipantLocation { @@ -1213,8 +1213,9 @@ message UpdateDiagnostics { } message Follow { - uint64 project_id = 1; - PeerId leader_id = 2; + uint64 room_id = 1; + optional uint64 project_id = 2; + PeerId leader_id = 3; } message FollowResponse { @@ -1223,18 +1224,20 @@ message FollowResponse { } message UpdateFollowers { - uint64 project_id = 1; - repeated PeerId follower_ids = 2; + uint64 room_id = 1; + optional uint64 project_id = 2; + repeated PeerId follower_ids = 3; oneof variant { - UpdateActiveView update_active_view = 3; - View create_view = 4; - UpdateView update_view = 5; + UpdateActiveView update_active_view = 4; + View create_view = 5; + UpdateView update_view = 6; } } message Unfollow { - uint64 project_id = 1; - PeerId leader_id = 2; + uint64 room_id = 1; + optional uint64 project_id = 2; + PeerId leader_id = 3; } message GetPrivateUserInfo {} diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 44a7df3b74..48e9eef710 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -364,7 +364,6 @@ entity_messages!( CreateProjectEntry, DeleteProjectEntry, ExpandProjectEntry, - Follow, FormatBuffers, GetCodeActions, GetCompletions, @@ -392,12 +391,10 @@ entity_messages!( SearchProject, StartLanguageServer, SynchronizeBuffers, - Unfollow, UnshareProject, UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, - UpdateFollowers, UpdateLanguageServer, UpdateProject, UpdateProjectCollaborator, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index a1393f56e9..942672b94b 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 63; +pub const PROTOCOL_VERSION: u32 = 64; From f34c6bd1ced921ebd3f60d5b0066237b678edb1e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 19 Sep 2023 17:25:42 -0700 Subject: [PATCH 03/45] Start work on allowing following without a shared project --- crates/call/src/call.rs | 194 +++++++++++++++++++++++++++++- crates/call/src/room.rs | 4 +- crates/workspace/src/workspace.rs | 186 +++++++++++----------------- 3 files changed, 263 insertions(+), 121 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 4db298fe98..3c5e18713b 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -8,19 +8,23 @@ use anyhow::{anyhow, Result}; use audio::Audio; use call_settings::CallSettings; use channel::ChannelId; -use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore}; +use client::{ + proto::{self, PeerId}, + ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore, +}; use collections::HashSet; use futures::{future::Shared, FutureExt}; use postage::watch; use gpui::{ - AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task, - WeakModelHandle, + AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, + ModelHandle, Subscription, Task, ViewContext, WeakModelHandle, }; use project::Project; pub use participant::ParticipantLocation; pub use room::Room; +use util::ResultExt; pub fn init(client: Arc, user_store: ModelHandle, cx: &mut AppContext) { settings::register::(cx); @@ -49,9 +53,25 @@ pub struct ActiveCall { ), client: Arc, user_store: ModelHandle, + follow_handlers: Vec, + followers: Vec, _subscriptions: Vec, } +#[derive(PartialEq, Eq, PartialOrd, Ord)] +struct Follower { + project_id: Option, + peer_id: PeerId, +} + +struct FollowHandler { + project_id: Option, + root_view: AnyWeakViewHandle, + get_views: + Box, &mut AppContext) -> Option>, + update_view: Box, +} + impl Entity for ActiveCall { type Event = room::Event; } @@ -68,9 +88,14 @@ impl ActiveCall { location: None, pending_invites: Default::default(), incoming_call: watch::channel(), + follow_handlers: Default::default(), + followers: Default::default(), _subscriptions: vec![ client.add_request_handler(cx.handle(), Self::handle_incoming_call), client.add_message_handler(cx.handle(), Self::handle_call_canceled), + client.add_request_handler(cx.handle(), Self::handle_follow), + client.add_message_handler(cx.handle(), Self::handle_unfollow), + client.add_message_handler(cx.handle(), Self::handle_update_followers), ], client, user_store, @@ -81,6 +106,48 @@ impl ActiveCall { self.room()?.read(cx).channel_id() } + pub fn add_follow_handler( + &mut self, + root_view: gpui::ViewHandle, + project_id: Option, + get_views: GetViews, + update_view: UpdateView, + _cx: &mut ModelContext, + ) where + GetViews: 'static + + Fn(&mut V, Option, &mut gpui::ViewContext) -> Result, + UpdateView: + 'static + Fn(&mut V, PeerId, proto::UpdateFollowers, &mut ViewContext) -> Result<()>, + { + self.follow_handlers + .retain(|h| h.root_view.id() != root_view.id()); + if let Err(ix) = self + .follow_handlers + .binary_search_by_key(&(project_id, root_view.id()), |f| { + (f.project_id, f.root_view.id()) + }) + { + self.follow_handlers.insert( + ix, + FollowHandler { + project_id, + root_view: root_view.into_any().downgrade(), + get_views: Box::new(move |view, project_id, cx| { + let view = view.clone().downcast::().unwrap(); + view.update(cx, |view, cx| get_views(view, project_id, cx).log_err()) + .flatten() + }), + update_view: Box::new(move |view, leader_id, message, cx| { + let view = view.clone().downcast::().unwrap(); + view.update(cx, |view, cx| { + update_view(view, leader_id, message, cx).log_err() + }); + }), + }, + ); + } + } + async fn handle_incoming_call( this: ModelHandle, envelope: TypedEnvelope, @@ -127,6 +194,127 @@ impl ActiveCall { Ok(()) } + async fn handle_follow( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + this.update(&mut cx, |this, cx| { + let follower = Follower { + project_id: envelope.payload.project_id, + peer_id: envelope.original_sender_id()?, + }; + let active_project_id = this + .location + .as_ref() + .and_then(|project| project.upgrade(cx)?.read(cx).remote_id()); + + let mut response = proto::FollowResponse::default(); + for handler in &this.follow_handlers { + if follower.project_id != handler.project_id && follower.project_id.is_some() { + continue; + } + + let Some(root_view) = handler.root_view.upgrade(cx) else { + continue; + }; + + let Some(handler_response) = + (handler.get_views)(&root_view, follower.project_id, cx) + else { + continue; + }; + + if response.views.is_empty() { + response.views = handler_response.views; + } else { + response.views.extend_from_slice(&handler_response.views); + } + + if let Some(active_view_id) = handler_response.active_view_id.clone() { + if response.active_view_id.is_none() || handler.project_id == active_project_id + { + response.active_view_id = Some(active_view_id); + } + } + } + + if let Err(ix) = this.followers.binary_search(&follower) { + this.followers.insert(ix, follower); + } + + Ok(response) + }) + } + + async fn handle_unfollow( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, _| { + let follower = Follower { + project_id: envelope.payload.project_id, + peer_id: envelope.original_sender_id()?, + }; + if let Err(ix) = this.followers.binary_search(&follower) { + this.followers.remove(ix); + } + Ok(()) + }) + } + + async fn handle_update_followers( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let leader_id = envelope.original_sender_id()?; + let update = envelope.payload; + this.update(&mut cx, |this, cx| { + for handler in &this.follow_handlers { + if update.project_id != handler.project_id && update.project_id.is_some() { + continue; + } + let Some(root_view) = handler.root_view.upgrade(cx) else { + continue; + }; + (handler.update_view)(&root_view, leader_id, update.clone(), cx); + } + Ok(()) + }) + } + + pub fn update_followers( + &self, + project_id: Option, + update: proto::update_followers::Variant, + cx: &AppContext, + ) -> Option<()> { + let room_id = self.room()?.read(cx).id(); + let follower_ids: Vec<_> = self + .followers + .iter() + .filter_map(|follower| { + (follower.project_id == project_id).then_some(follower.peer_id.into()) + }) + .collect(); + if follower_ids.is_empty() { + return None; + } + self.client + .send(proto::UpdateFollowers { + room_id, + project_id, + follower_ids, + variant: Some(update), + }) + .log_err() + } + pub fn global(cx: &AppContext) -> ModelHandle { cx.global::>().clone() } diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index e7899ab2d8..ffa941bfa1 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -62,7 +62,7 @@ pub struct Room { leave_when_empty: bool, client: Arc, user_store: ModelHandle, - follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec>, + follows_by_leader_id_project_id: HashMap<(PeerId, Option), Vec>, subscriptions: Vec, pending_room_update: Option>, maintain_connection: Option>>, @@ -584,7 +584,7 @@ impl Room { pub fn followers_for(&self, leader_id: PeerId, project_id: u64) -> &[PeerId] { self.follows_by_leader_id_project_id - .get(&(leader_id, project_id)) + .get(&(leader_id, Some(project_id))) .map_or(&[], |v| v.as_slice()) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index feab53d094..11cb47afc5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -15,7 +15,7 @@ use call::ActiveCall; use channel::ChannelStore; use client::{ proto::{self, PeerId}, - Client, TypedEnvelope, UserStore, + Client, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use drag_and_drop::DragAndDrop; @@ -331,11 +331,6 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { }) .detach(); }); - - let client = &app_state.client; - client.add_view_request_handler(Workspace::handle_follow); - client.add_view_message_handler(Workspace::handle_unfollow); - client.add_view_message_handler(Workspace::handle_update_followers); } type ProjectItemBuilders = HashMap< @@ -507,7 +502,6 @@ pub enum Event { pub struct Workspace { weak_self: WeakViewHandle, - remote_entity_subscription: Option, modal: Option, zoomed: Option, zoomed_position: Option, @@ -523,7 +517,6 @@ pub struct Workspace { titlebar_item: Option, notifications: Vec<(TypeId, usize, Box)>, project: ModelHandle, - leader_state: LeaderState, follower_states_by_leader: FollowerStatesByLeader, last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, @@ -549,11 +542,6 @@ pub struct ViewId { pub id: u64, } -#[derive(Default)] -struct LeaderState { - followers: HashSet, -} - type FollowerStatesByLeader = HashMap, FollowerState>>; #[derive(Default)] @@ -737,12 +725,10 @@ impl Workspace { status_bar, titlebar_item: None, notifications: Default::default(), - remote_entity_subscription: None, left_dock, bottom_dock, right_dock, project: project.clone(), - leader_state: Default::default(), follower_states_by_leader: Default::default(), last_leaders_by_pane: Default::default(), window_edited: false, @@ -2419,19 +2405,21 @@ impl Workspace { } fn project_remote_id_changed(&mut self, remote_id: Option, cx: &mut ViewContext) { - if let Some(remote_id) = remote_id { - self.remote_entity_subscription = Some( - self.app_state - .client - .add_view_for_remote_entity(remote_id, cx), - ); - } else { - self.remote_entity_subscription.take(); + let handle = cx.handle(); + if let Some(call) = self.active_call() { + call.update(cx, |call, cx| { + call.add_follow_handler( + handle, + remote_id, + Self::get_views_for_followers, + Self::handle_update_followers, + cx, + ); + }); } } fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { - self.leader_state.followers.remove(&peer_id); if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) { for state in states_by_pane.into_values() { for item in state.items_by_leader_view_id.into_values() { @@ -2463,8 +2451,10 @@ impl Workspace { .insert(pane.clone(), Default::default()); cx.notify(); - let project_id = self.project.read(cx).remote_id()?; + let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); + let project_id = self.project.read(cx).remote_id(); let request = self.app_state.client.request(proto::Follow { + room_id, project_id, leader_id: Some(leader_id), }); @@ -2542,15 +2532,16 @@ impl Workspace { if states_by_pane.is_empty() { self.follower_states_by_leader.remove(&leader_id); - if let Some(project_id) = self.project.read(cx).remote_id() { - self.app_state - .client - .send(proto::Unfollow { - project_id, - leader_id: Some(leader_id), - }) - .log_err(); - } + let project_id = self.project.read(cx).remote_id(); + let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); + self.app_state + .client + .send(proto::Unfollow { + room_id, + project_id, + leader_id: Some(leader_id), + }) + .log_err(); } cx.notify(); @@ -2564,10 +2555,6 @@ impl Workspace { self.follower_states_by_leader.contains_key(&peer_id) } - pub fn is_followed_by(&self, peer_id: PeerId) -> bool { - self.leader_state.followers.contains(&peer_id) - } - fn render_titlebar(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { // TODO: There should be a better system in place for this // (https://github.com/zed-industries/zed/issues/1290) @@ -2718,80 +2705,56 @@ impl Workspace { // RPC handlers - async fn handle_follow( - this: WeakViewHandle, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, + fn get_views_for_followers( + &mut self, + _project_id: Option, + cx: &mut ViewContext, ) -> Result { - this.update(&mut cx, |this, cx| { - let client = &this.app_state.client; - this.leader_state - .followers - .insert(envelope.original_sender_id()?); + let client = &self.app_state.client; - let active_view_id = this.active_item(cx).and_then(|i| { - Some( - i.to_followable_item_handle(cx)? - .remote_id(client, cx)? - .to_proto(), - ) - }); + let active_view_id = self.active_item(cx).and_then(|i| { + Some( + i.to_followable_item_handle(cx)? + .remote_id(client, cx)? + .to_proto(), + ) + }); - cx.notify(); + cx.notify(); - Ok(proto::FollowResponse { - active_view_id, - views: this - .panes() - .iter() - .flat_map(|pane| { - let leader_id = this.leader_for_pane(pane); - pane.read(cx).items().filter_map({ - let cx = &cx; - move |item| { - let item = item.to_followable_item_handle(cx)?; - let id = item.remote_id(client, cx)?.to_proto(); - let variant = item.to_state_proto(cx)?; - Some(proto::View { - id: Some(id), - leader_id, - variant: Some(variant), - }) - } - }) + Ok(proto::FollowResponse { + active_view_id, + views: self + .panes() + .iter() + .flat_map(|pane| { + let leader_id = self.leader_for_pane(pane); + pane.read(cx).items().filter_map({ + let cx = &cx; + move |item| { + let item = item.to_followable_item_handle(cx)?; + let id = item.remote_id(client, cx)?.to_proto(); + let variant = item.to_state_proto(cx)?; + Some(proto::View { + id: Some(id), + leader_id, + variant: Some(variant), + }) + } }) - .collect(), - }) - })? + }) + .collect(), + }) } - async fn handle_unfollow( - this: WeakViewHandle, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, + fn handle_update_followers( + &mut self, + leader_id: PeerId, + message: proto::UpdateFollowers, + _cx: &mut ViewContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| { - this.leader_state - .followers - .remove(&envelope.original_sender_id()?); - cx.notify(); - Ok(()) - })? - } - - async fn handle_update_followers( - this: WeakViewHandle, - envelope: TypedEnvelope, - _: Arc, - cx: AsyncAppContext, - ) -> Result<()> { - let leader_id = envelope.original_sender_id()?; - this.read_with(&cx, |this, _| { - this.leader_updates_tx - .unbounded_send((leader_id, envelope.payload)) - })??; + self.leader_updates_tx + .unbounded_send((leader_id, message))?; Ok(()) } @@ -2960,18 +2923,9 @@ impl Workspace { update: proto::update_followers::Variant, cx: &AppContext, ) -> Option<()> { - let project_id = self.project.read(cx).remote_id()?; - if !self.leader_state.followers.is_empty() { - self.app_state - .client - .send(proto::UpdateFollowers { - project_id, - follower_ids: self.leader_state.followers.iter().copied().collect(), - variant: Some(update), - }) - .log_err(); - } - None + self.active_call()? + .read(cx) + .update_followers(self.project.read(cx).remote_id(), update, cx) } pub fn leader_for_pane(&self, pane: &ViewHandle) -> Option { From ed8b022b51d75c891378525f12fae9412b6bddfb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 19 Sep 2023 18:03:38 -0700 Subject: [PATCH 04/45] Add initial failing test for following to channel notes in an unshared project --- .../collab/src/tests/channel_buffer_tests.rs | 91 ++++++++++++++++++- crates/collab/src/tests/test_server.rs | 4 +- crates/collab_ui/src/channel_view.rs | 16 ++-- 3 files changed, 102 insertions(+), 9 deletions(-) diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index ba5a70895a..fc2d118cf8 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -274,7 +274,10 @@ async fn test_channel_buffer_replica_ids( } #[gpui::test] -async fn test_reopen_channel_buffer(deterministic: Arc, cx_a: &mut TestAppContext) { +async fn test_multiple_handles_to_channel_buffer( + deterministic: Arc, + cx_a: &mut TestAppContext, +) { deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; @@ -578,6 +581,92 @@ async fn test_channel_buffers_and_server_restarts( }); } +#[gpui::test(iterations = 10)] +async fn test_following_to_channel_notes_without_a_shared_project( + deterministic: Arc, + mut cx_a: &mut TestAppContext, + mut cx_b: &mut TestAppContext, + mut cx_c: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + let channel_1_id = server + .make_channel( + "channel-1", + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + let channel_2_id = server + .make_channel( + "channel-2", + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + // Clients A, B, and C join a channel. + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); + for (call, cx) in [ + (&active_call_a, &mut cx_a), + (&active_call_b, &mut cx_b), + (&active_call_c, &mut cx_c), + ] { + call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx)) + .await + .unwrap(); + } + deterministic.run_until_parked(); + + // Clients A, B, and C all open their own unshared projects. + client_a.fs().insert_tree("/a", json!({})).await; + client_b.fs().insert_tree("/b", json!({})).await; + client_c.fs().insert_tree("/c", json!({})).await; + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; + let (project_b, _) = client_b.build_local_project("/b", cx_b).await; + let (project_c, _) = client_b.build_local_project("/c", cx_c).await; + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); + + // Client A opens the notes for channel 1. + let channel_view_1_a = cx_a + .update(|cx| { + ChannelView::open( + channel_1_id, + workspace_a.read(cx).active_pane().clone(), + workspace_a.clone(), + cx, + ) + }) + .await + .unwrap(); + + // Client B follows client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace + .toggle_follow(client_a.peer_id().unwrap(), cx) + .unwrap() + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, _| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a.peer_id().unwrap()) + ); + }); +} + #[track_caller] fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { assert_eq!( diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 6572722df3..6a15cac9e9 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -206,11 +206,13 @@ impl TestServer { let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, 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(), channel_store: channel_store.clone(), - languages: Arc::new(LanguageRegistry::test()), + languages: Arc::new(language_registry), fs: fs.clone(), build_window_options: |_, _, _| Default::default(), initialize_workspace: |_, _, _, _| Task::ready(Ok(())), diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index c6f32cecd2..4e6a369db6 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -14,6 +14,7 @@ use gpui::{ }; use project::Project; use std::any::{Any, TypeId}; +use util::ResultExt; use workspace::{ item::{FollowableItem, Item, ItemHandle}, register_followable_item, @@ -53,7 +54,7 @@ impl ChannelView { &workspace.read(cx).app_state().client, cx, ); - pane.add_item(Box::new(channel_view), true, true, None, cx); + pane.add_item(Box::new(channel_view.clone()), true, true, None, cx); }); anyhow::Ok(()) }) @@ -79,12 +80,13 @@ impl ChannelView { cx.spawn(|mut cx| async move { let channel_buffer = channel_buffer.await?; - let markdown = markdown.await?; - channel_buffer.update(&mut cx, |buffer, cx| { - buffer.buffer().update(cx, |buffer, cx| { - buffer.set_language(Some(markdown), cx); - }) - }); + if let Some(markdown) = markdown.await.log_err() { + channel_buffer.update(&mut cx, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.set_language(Some(markdown), cx); + }) + }); + } pane.update(&mut cx, |pane, cx| { pane.items_of_type::() From 4ffa167256dc91335473fb631af625d4a6d6c7d2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2023 16:13:12 -0700 Subject: [PATCH 05/45] Allow following into channel notes regardless of project --- .../collab/src/tests/channel_buffer_tests.rs | 75 ++++++++++-- crates/collab/src/tests/integration_tests.rs | 4 +- crates/collab_ui/src/channel_view.rs | 32 ++++-- crates/collab_ui/src/chat_panel.rs | 4 +- crates/collab_ui/src/collab_panel.rs | 2 +- crates/collab_ui/src/collab_titlebar_item.rs | 107 +++++++++--------- crates/editor/src/items.rs | 4 + crates/workspace/src/item.rs | 6 + crates/workspace/src/pane_group.rs | 19 ++-- crates/workspace/src/workspace.rs | 62 +++++----- 10 files changed, 201 insertions(+), 114 deletions(-) diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index fc2d118cf8..baab675a1c 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -594,9 +594,17 @@ async fn test_following_to_channel_notes_without_a_shared_project( let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; + cx_a.update(editor::init); + cx_b.update(editor::init); + cx_c.update(editor::init); + cx_a.update(collab_ui::channel_view::init); + cx_b.update(collab_ui::channel_view::init); + cx_c.update(collab_ui::channel_view::init); + let channel_1_id = server .make_channel( "channel-1", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) @@ -604,6 +612,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( let channel_2_id = server .make_channel( "channel-2", + None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) @@ -633,20 +642,27 @@ async fn test_following_to_channel_notes_without_a_shared_project( let (project_c, _) = client_b.build_local_project("/c", cx_c).await; let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); + let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); + + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); // Client A opens the notes for channel 1. let channel_view_1_a = cx_a - .update(|cx| { - ChannelView::open( - channel_1_id, - workspace_a.read(cx).active_pane().clone(), - workspace_a.clone(), - cx, - ) - }) + .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx)) .await .unwrap(); + channel_view_1_a.update(cx_a, |notes, cx| { + assert_eq!(notes.channel(cx).name, "channel-1"); + notes.editor.update(cx, |editor, cx| { + editor.insert("Hello from A.", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![3..4]); + }); + }); + }); // Client B follows client A. workspace_b @@ -658,12 +674,51 @@ async fn test_following_to_channel_notes_without_a_shared_project( .await .unwrap(); + // Client B is taken to the notes for channel 1, with the same + // text selected as client A. deterministic.run_until_parked(); - workspace_b.read_with(cx_b, |workspace, _| { + let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| { assert_eq!( workspace.leader_for_pane(workspace.active_pane()), Some(client_a.peer_id().unwrap()) ); + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item is not a channel view") + }); + channel_view_1_b.read_with(cx_b, |notes, cx| { + assert_eq!(notes.channel(cx).name, "channel-1"); + let editor = notes.editor.read(cx); + assert_eq!(editor.text(cx), "Hello from A."); + assert_eq!(editor.selections.ranges::(cx), &[3..4]); + }); + + // Client A opens the notes for channel 2. + let channel_view_2_a = cx_a + .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx)) + .await + .unwrap(); + channel_view_2_a.read_with(cx_a, |notes, cx| { + assert_eq!(notes.channel(cx).name, "channel-2"); + }); + + // Client B is taken to the notes for channel 2. + deterministic.run_until_parked(); + let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a.peer_id().unwrap()) + ); + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item is not a channel view") + }); + channel_view_2_b.read_with(cx_b, |notes, cx| { + assert_eq!(notes.channel(cx).name, "channel-2"); }); } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 10d6baec19..b17b7b3fc2 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -6848,9 +6848,9 @@ async fn test_basic_following( let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| { workspace .active_item(cx) - .unwrap() + .expect("no active item") .downcast::() - .unwrap() + .expect("active item isn't a shared screen") }); // Client B activates Zed again, which causes the previous editor to become focused again. diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 4e6a369db6..b66d1ab7c7 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Result}; use call::ActiveCall; -use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId}; +use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId}; use client::proto; use clock::ReplicaId; use collections::HashMap; @@ -13,7 +13,10 @@ use gpui::{ ViewContext, ViewHandle, }; use project::Project; -use std::any::{Any, TypeId}; +use std::{ + any::{Any, TypeId}, + sync::Arc, +}; use util::ResultExt; use workspace::{ item::{FollowableItem, Item, ItemHandle}, @@ -24,7 +27,7 @@ use workspace::{ actions!(channel_view, [Deploy]); -pub(crate) fn init(cx: &mut AppContext) { +pub fn init(cx: &mut AppContext) { register_followable_item::(cx) } @@ -37,9 +40,13 @@ pub struct ChannelView { } impl ChannelView { - pub fn deploy(channel_id: ChannelId, workspace: ViewHandle, cx: &mut AppContext) { + pub fn open( + channel_id: ChannelId, + workspace: ViewHandle, + cx: &mut AppContext, + ) -> Task>> { let pane = workspace.read(cx).active_pane().clone(); - let channel_view = Self::open(channel_id, pane.clone(), workspace.clone(), cx); + let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx); cx.spawn(|mut cx| async move { let channel_view = channel_view.await?; pane.update(&mut cx, |pane, cx| { @@ -56,12 +63,11 @@ impl ChannelView { ); pane.add_item(Box::new(channel_view.clone()), true, true, None, cx); }); - anyhow::Ok(()) + anyhow::Ok(channel_view) }) - .detach(); } - pub fn open( + pub fn open_in_pane( channel_id: ChannelId, pane: ViewHandle, workspace: ViewHandle, @@ -121,6 +127,10 @@ impl ChannelView { this } + pub fn channel(&self, cx: &AppContext) -> Arc { + self.channel_buffer.read(cx).channel() + } + fn handle_project_event( &mut self, _: ModelHandle, @@ -318,7 +328,7 @@ impl FollowableItem for ChannelView { unreachable!() }; - let open = ChannelView::open(state.channel_id, pane, workspace, cx); + let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx); Some(cx.spawn(|mut cx| async move { let this = open.await?; @@ -391,4 +401,8 @@ impl FollowableItem for ChannelView { fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { Editor::should_unfollow_on_event(event, cx) } + + fn is_project_item(&self, _cx: &AppContext) -> bool { + false + } } diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 082702feda..81a421e8d9 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -409,7 +409,7 @@ impl ChatPanel { }) .on_click(MouseButton::Left, move |_, _, cx| { if let Some(workspace) = workspace.upgrade(cx) { - ChannelView::deploy(channel_id, workspace, cx); + ChannelView::open(channel_id, workspace, cx).detach(); } }) .with_tooltip::( @@ -546,7 +546,7 @@ impl ChatPanel { if let Some((chat, _)) = &self.active_chat { let channel_id = chat.read(cx).channel().id; if let Some(workspace) = self.workspace.upgrade(cx) { - ChannelView::deploy(channel_id, workspace, cx); + ChannelView::open(channel_id, workspace, cx).detach(); } } } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 6013ea4907..a13e7a09ee 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2745,7 +2745,7 @@ impl CollabPanel { fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade(cx) { - ChannelView::deploy(action.channel_id, workspace, cx); + ChannelView::open(action.channel_id, workspace, cx).detach(); } } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 4537bf04e0..eb70da8dba 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1004,61 +1004,64 @@ impl CollabTitlebarItem { .into_any(); if let Some(location) = location { - if let Some(replica_id) = replica_id { - enum ToggleFollow {} + match (replica_id, location) { + (None, ParticipantLocation::SharedProject { project_id }) => { + enum JoinProject {} - content = MouseEventHandler::new::( - replica_id.into(), - cx, - move |_, _| content, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, item, cx| { - if let Some(workspace) = item.workspace.upgrade(cx) { - if let Some(task) = workspace - .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx)) - { - task.detach_and_log_err(cx); + let user_id = user.id; + content = MouseEventHandler::new::( + peer_id.as_u64() as usize, + cx, + move |_, _| content, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + let app_state = workspace.read(cx).app_state().clone(); + workspace::join_remote_project(project_id, user_id, app_state, cx) + .detach_and_log_err(cx); } - } - }) - .with_tooltip::( - peer_id.as_u64() as usize, - if is_being_followed { - format!("Unfollow {}", user.github_login) - } else { - format!("Follow {}", user.github_login) - }, - Some(Box::new(FollowNextCollaborator)), - theme.tooltip.clone(), - cx, - ) - .into_any(); - } else if let ParticipantLocation::SharedProject { project_id } = location { - enum JoinProject {} + }) + .with_tooltip::( + peer_id.as_u64() as usize, + format!("Follow {} into external project", user.github_login), + Some(Box::new(FollowNextCollaborator)), + theme.tooltip.clone(), + cx, + ) + .into_any(); + } + _ => { + enum ToggleFollow {} - let user_id = user.id; - content = MouseEventHandler::new::( - peer_id.as_u64() as usize, - cx, - move |_, _| content, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - let app_state = workspace.read(cx).app_state().clone(); - workspace::join_remote_project(project_id, user_id, app_state, cx) - .detach_and_log_err(cx); - } - }) - .with_tooltip::( - peer_id.as_u64() as usize, - format!("Follow {} into external project", user.github_login), - Some(Box::new(FollowNextCollaborator)), - theme.tooltip.clone(), - cx, - ) - .into_any(); + content = MouseEventHandler::new::( + user.id as usize, + cx, + move |_, _| content, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, item, cx| { + if let Some(workspace) = item.workspace.upgrade(cx) { + if let Some(task) = workspace + .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx)) + { + task.detach_and_log_err(cx); + } + } + }) + .with_tooltip::( + peer_id.as_u64() as usize, + if is_being_followed { + format!("Unfollow {}", user.github_login) + } else { + format!("Follow {}", user.github_login) + }, + Some(Box::new(FollowNextCollaborator)), + theme.tooltip.clone(), + cx, + ) + .into_any(); + } } } content diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 568ea223cc..7fdbe82a9a 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -309,6 +309,10 @@ impl FollowableItem for Editor { _ => false, } } + + fn is_project_item(&self, _cx: &AppContext) -> bool { + true + } } async fn update_editor_from_message( diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index ea747b3a36..de19e82c8b 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -696,6 +696,7 @@ pub trait FollowableItem: Item { message: proto::update_view::Variant, cx: &mut ViewContext, ) -> Task>; + fn is_project_item(&self, cx: &AppContext) -> bool; fn set_leader_replica_id(&mut self, leader_replica_id: Option, cx: &mut ViewContext); fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; @@ -718,6 +719,7 @@ pub trait FollowableItemHandle: ItemHandle { cx: &mut WindowContext, ) -> Task>; fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool; + fn is_project_item(&self, cx: &AppContext) -> bool; } impl FollowableItemHandle for ViewHandle { @@ -769,6 +771,10 @@ impl FollowableItemHandle for ViewHandle { false } } + + fn is_project_item(&self, cx: &AppContext) -> bool { + self.read(cx).is_project_item(cx) + } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index bffdce0f3e..29068ce923 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -210,7 +210,6 @@ impl Member { } else { let leader_user = leader.user.clone(); let leader_user_id = leader.user.id; - let app_state = Arc::downgrade(app_state); Some( MouseEventHandler::new::( pane.id(), @@ -234,16 +233,14 @@ impl Member { }, ) .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, _, cx| { - if let Some(app_state) = app_state.upgrade() { - crate::join_remote_project( - leader_project_id, - leader_user_id, - app_state, - cx, - ) - .detach_and_log_err(cx); - } + .on_click(MouseButton::Left, move |_, this, cx| { + crate::join_remote_project( + leader_project_id, + leader_user_id, + this.app_state().clone(), + cx, + ) + .detach_and_log_err(cx); }) .aligned() .bottom() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 11cb47afc5..f874f5ee16 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2832,14 +2832,12 @@ impl Workspace { .read_with(cx, |this, _| this.project.clone()) .ok_or_else(|| anyhow!("window dropped"))?; - let replica_id = project - .read_with(cx, |project, _| { - project - .collaborators() - .get(&leader_id) - .map(|c| c.replica_id) - }) - .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?; + let replica_id = project.read_with(cx, |project, _| { + project + .collaborators() + .get(&leader_id) + .map(|c| c.replica_id) + }); let item_builders = cx.update(|cx| { cx.default_global::() @@ -2884,7 +2882,7 @@ impl Workspace { .get_mut(&pane)?; for (id, item) in leader_view_ids.into_iter().zip(items) { - item.set_leader_replica_id(Some(replica_id), cx); + item.set_leader_replica_id(replica_id, cx); state.items_by_leader_view_id.insert(id, item); } @@ -2947,30 +2945,40 @@ impl Workspace { let room = call.read(cx).room()?.read(cx); let participant = room.remote_participant_for_peer_id(leader_id)?; let mut items_to_activate = Vec::new(); + + let leader_in_this_app; + let leader_in_this_project; match participant.location { call::ParticipantLocation::SharedProject { project_id } => { - if Some(project_id) == self.project.read(cx).remote_id() { - for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { - if let Some(item) = state - .active_view_id - .and_then(|id| state.items_by_leader_view_id.get(&id)) - { - items_to_activate.push((pane.clone(), item.boxed_clone())); - } else if let Some(shared_screen) = - self.shared_screen_for_peer(leader_id, pane, cx) - { - items_to_activate.push((pane.clone(), Box::new(shared_screen))); - } - } - } + leader_in_this_app = true; + leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id(); + } + call::ParticipantLocation::UnsharedProject => { + leader_in_this_app = true; + leader_in_this_project = false; } - call::ParticipantLocation::UnsharedProject => {} call::ParticipantLocation::External => { - for (pane, _) in self.follower_states_by_leader.get(&leader_id)? { - if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { - items_to_activate.push((pane.clone(), Box::new(shared_screen))); + leader_in_this_app = false; + leader_in_this_project = false; + } + }; + + for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { + let item = state + .active_view_id + .and_then(|id| state.items_by_leader_view_id.get(&id)); + let shared_screen = self.shared_screen_for_peer(leader_id, pane, cx); + + if leader_in_this_app { + if let Some(item) = item { + if leader_in_this_project || !item.is_project_item(cx) { + items_to_activate.push((pane.clone(), item.boxed_clone())); } + } else if let Some(shared_screen) = shared_screen { + items_to_activate.push((pane.clone(), Box::new(shared_screen))); } + } else if let Some(shared_screen) = shared_screen { + items_to_activate.push((pane.clone(), Box::new(shared_screen))); } } From 77115307045ef4ef4c303448efa24b5ea9d6a0c7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2023 16:43:59 -0700 Subject: [PATCH 06/45] Simplify titlebar facepile click rendering / mouse handling --- crates/collab_ui/src/collab_titlebar_item.rs | 255 +++++++++---------- 1 file changed, 127 insertions(+), 128 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index eb70da8dba..a676b40b75 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -886,10 +886,12 @@ impl CollabTitlebarItem { theme: &Theme, cx: &mut ViewContext, ) -> AnyElement { + let user_id = user.id; let project_id = workspace.read(cx).project().read(cx).remote_id(); - let room = ActiveCall::global(cx).read(cx).room(); + let room = ActiveCall::global(cx).read(cx).room().cloned(); let is_being_followed = workspace.read(cx).is_being_followed(peer_id); let followed_by_self = room + .as_ref() .and_then(|room| { Some( is_being_followed @@ -929,142 +931,139 @@ impl CollabTitlebarItem { } } - let mut content = Stack::new() - .with_children(user.avatar.as_ref().map(|avatar| { - let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap) - .with_child(Self::render_face( - avatar.clone(), - Self::location_style(workspace, location, leader_style, cx), - background_color, - microphone_state, - )) - .with_children( - (|| { - let project_id = project_id?; - let room = room?.read(cx); - let followers = room.followers_for(peer_id, project_id); + enum TitlebarParticipant {} - Some(followers.into_iter().flat_map(|&follower| { - let remote_participant = - room.remote_participant_for_peer_id(follower); + let content = MouseEventHandler::new::( + peer_id.as_u64() as usize, + cx, + move |_, cx| { + Stack::new() + .with_children(user.avatar.as_ref().map(|avatar| { + let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap) + .with_child(Self::render_face( + avatar.clone(), + Self::location_style(workspace, location, leader_style, cx), + background_color, + microphone_state, + )) + .with_children( + (|| { + let project_id = project_id?; + let room = room?.read(cx); + let followers = room.followers_for(peer_id, project_id); - let avatar = remote_participant - .and_then(|p| p.user.avatar.clone()) - .or_else(|| { - if follower == workspace.read(cx).client().peer_id()? { - workspace - .read(cx) - .user_store() - .read(cx) - .current_user()? - .avatar - .clone() - } else { - None - } - })?; + Some(followers.into_iter().flat_map(|&follower| { + let remote_participant = + room.remote_participant_for_peer_id(follower); - Some(Self::render_face( - avatar.clone(), - follower_style, - background_color, - None, - )) - })) - })() - .into_iter() - .flatten(), - ); + let avatar = remote_participant + .and_then(|p| p.user.avatar.clone()) + .or_else(|| { + if follower + == workspace.read(cx).client().peer_id()? + { + workspace + .read(cx) + .user_store() + .read(cx) + .current_user()? + .avatar + .clone() + } else { + None + } + })?; - let mut container = face_pile - .contained() - .with_style(theme.titlebar.leader_selection); + Some(Self::render_face( + avatar.clone(), + follower_style, + background_color, + None, + )) + })) + })() + .into_iter() + .flatten(), + ); - if let Some(replica_id) = replica_id { - if followed_by_self { - let color = theme.editor.replica_selection_style(replica_id).selection; - container = container.with_background_color(color); - } - } + let mut container = face_pile + .contained() + .with_style(theme.titlebar.leader_selection); - container - })) - .with_children((|| { - let replica_id = replica_id?; - let color = theme.editor.replica_selection_style(replica_id).cursor; - Some( - AvatarRibbon::new(color) - .constrained() - .with_width(theme.titlebar.avatar_ribbon.width) - .with_height(theme.titlebar.avatar_ribbon.height) - .aligned() - .bottom(), - ) - })()) - .into_any(); - - if let Some(location) = location { - match (replica_id, location) { - (None, ParticipantLocation::SharedProject { project_id }) => { - enum JoinProject {} - - let user_id = user.id; - content = MouseEventHandler::new::( - peer_id.as_u64() as usize, - cx, - move |_, _| content, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - let app_state = workspace.read(cx).app_state().clone(); - workspace::join_remote_project(project_id, user_id, app_state, cx) - .detach_and_log_err(cx); - } - }) - .with_tooltip::( - peer_id.as_u64() as usize, - format!("Follow {} into external project", user.github_login), - Some(Box::new(FollowNextCollaborator)), - theme.tooltip.clone(), - cx, - ) - .into_any(); - } - _ => { - enum ToggleFollow {} - - content = MouseEventHandler::new::( - user.id as usize, - cx, - move |_, _| content, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, item, cx| { - if let Some(workspace) = item.workspace.upgrade(cx) { - if let Some(task) = workspace - .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx)) - { - task.detach_and_log_err(cx); + if let Some(replica_id) = replica_id { + if followed_by_self { + let color = + theme.editor.replica_selection_style(replica_id).selection; + container = container.with_background_color(color); } } - }) - .with_tooltip::( - peer_id.as_u64() as usize, - if is_being_followed { - format!("Unfollow {}", user.github_login) - } else { - format!("Follow {}", user.github_login) - }, - Some(Box::new(FollowNextCollaborator)), - theme.tooltip.clone(), - cx, - ) - .into_any(); - } - } + + container + })) + .with_children((|| { + let replica_id = replica_id?; + let color = theme.editor.replica_selection_style(replica_id).cursor; + Some( + AvatarRibbon::new(color) + .constrained() + .with_width(theme.titlebar.avatar_ribbon.width) + .with_height(theme.titlebar.avatar_ribbon.height) + .aligned() + .bottom(), + ) + })()) + }, + ); + + match (replica_id, location) { + // If the user's location isn't known, do nothing. + (_, None) => content.into_any(), + + // If the user is not in this project, but is in another share project, + // join that project. + (None, Some(ParticipantLocation::SharedProject { project_id })) => content + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + let app_state = workspace.read(cx).app_state().clone(); + workspace::join_remote_project(project_id, user_id, app_state, cx) + .detach_and_log_err(cx); + } + }) + .with_tooltip::( + peer_id.as_u64() as usize, + format!("Follow {} into external project", user.github_login), + Some(Box::new(FollowNextCollaborator)), + theme.tooltip.clone(), + cx, + ) + .into_any(), + + // Otherwise, follow the user in the current window. + _ => content + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, item, cx| { + if let Some(workspace) = item.workspace.upgrade(cx) { + if let Some(task) = workspace + .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx)) + { + task.detach_and_log_err(cx); + } + } + }) + .with_tooltip::( + peer_id.as_u64() as usize, + if is_being_followed { + format!("Unfollow {}", user.github_login) + } else { + format!("Follow {}", user.github_login) + }, + Some(Box::new(FollowNextCollaborator)), + theme.tooltip.clone(), + cx, + ) + .into_any(), } - content } fn location_style( From 545b5e0161127e8de7b0896dbe03514af194867d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Sep 2023 15:19:38 -0600 Subject: [PATCH 07/45] Assign unique color indices to room participants, use those instead of replica_ids Co-authored-by: Conrad Co-authored-by: Antonio --- Cargo.lock | 4 + crates/call/Cargo.toml | 1 + crates/call/src/participant.rs | 2 + crates/call/src/room.rs | 11 + crates/channel/Cargo.toml | 1 + crates/channel/src/channel_buffer.rs | 105 +++---- crates/channel/src/channel_store.rs | 3 +- crates/client/Cargo.toml | 1 + crates/client/src/user.rs | 37 +++ .../20221109000000_test_schema.sql | 3 +- ...0_add_color_index_to_room_participants.sql | 1 + crates/collab/src/db.rs | 2 +- crates/collab/src/db/queries/buffers.rs | 59 ++-- crates/collab/src/db/queries/rooms.rs | 20 ++ .../collab/src/db/tables/room_participant.rs | 1 + crates/collab/src/db/tests/buffer_tests.rs | 4 +- crates/collab/src/rpc.rs | 56 ++-- .../collab/src/tests/channel_buffer_tests.rs | 279 ++++++++++-------- .../src/tests/random_channel_buffer_tests.rs | 2 +- crates/collab/src/tests/test_server.rs | 22 +- crates/collab_ui/src/channel_view.rs | 105 ++----- crates/collab_ui/src/collab_titlebar_item.rs | 18 +- crates/editor/src/editor.rs | 94 ++++-- crates/editor/src/element.rs | 94 +++--- crates/editor/src/items.rs | 12 +- crates/project/Cargo.toml | 1 + crates/project/src/project.rs | 27 +- crates/rpc/proto/zed.proto | 278 +++++++++-------- crates/rpc/src/proto.rs | 8 +- crates/theme/src/theme.rs | 14 +- crates/vim/src/vim.rs | 2 +- crates/workspace/src/item.rs | 15 +- crates/workspace/src/pane_group.rs | 34 +-- crates/workspace/src/workspace.rs | 16 +- selection-color-notes.txt | 14 + 35 files changed, 707 insertions(+), 639 deletions(-) create mode 100644 crates/collab/migrations/20230926102500_add_color_index_to_room_participants.sql create mode 100644 selection-color-notes.txt diff --git a/Cargo.lock b/Cargo.lock index 3cced78c42..46d8deb62b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1079,6 +1079,7 @@ dependencies = [ "serde_derive", "serde_json", "settings", + "theme", "util", ] @@ -1219,6 +1220,7 @@ dependencies = [ "sum_tree", "tempfile", "text", + "theme", "thiserror", "time", "tiny_http", @@ -1390,6 +1392,7 @@ dependencies = [ "sum_tree", "tempfile", "text", + "theme", "thiserror", "time", "tiny_http", @@ -5510,6 +5513,7 @@ dependencies = [ "tempdir", "terminal", "text", + "theme", "thiserror", "toml 0.5.11", "unindent", diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index b4e94fe56c..716bc3c27b 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -31,6 +31,7 @@ language = { path = "../language" } media = { path = "../media" } project = { path = "../project" } settings = { path = "../settings" } +theme = { path = "../theme" } util = { path = "../util" } anyhow.workspace = true diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index e7858869ce..b0751be919 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -6,6 +6,7 @@ pub use live_kit_client::Frame; use live_kit_client::RemoteAudioTrack; use project::Project; use std::{fmt, sync::Arc}; +use theme::ColorIndex; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ParticipantLocation { @@ -43,6 +44,7 @@ pub struct RemoteParticipant { pub peer_id: proto::PeerId, pub projects: Vec, pub location: ParticipantLocation, + pub color_index: ColorIndex, pub muted: bool, pub speaking: bool, pub video_tracks: HashMap>, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index ffa941bfa1..e6759d87ca 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -21,6 +21,7 @@ use live_kit_client::{ use postage::stream::Stream; use project::Project; use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration}; +use theme::ColorIndex; use util::{post_inc, ResultExt, TryFutureExt}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -714,6 +715,7 @@ impl Room { participant.user_id, RemoteParticipant { user: user.clone(), + color_index: ColorIndex(participant.color_index), peer_id, projects: participant.projects, location, @@ -807,6 +809,15 @@ impl Room { let _ = this.leave(cx); } + this.user_store.update(cx, |user_store, cx| { + let color_indices_by_user_id = this + .remote_participants + .iter() + .map(|(user_id, participant)| (*user_id, participant.color_index)) + .collect(); + user_store.set_color_indices(color_indices_by_user_id, cx); + }); + this.check_invariants(); cx.notify(); }); diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index 16a1d418d5..e3a74ecbe6 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -23,6 +23,7 @@ language = { path = "../language" } settings = { path = "../settings" } feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } +theme = { path = "../theme" } anyhow.workspace = true futures.workspace = true diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 06f9093fb5..a03eb1f1b5 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,22 +1,25 @@ use crate::Channel; use anyhow::Result; -use client::Client; +use client::{Client, Collaborator, UserStore}; +use collections::HashMap; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle}; -use rpc::{proto, TypedEnvelope}; +use rpc::{ + proto::{self, PeerId}, + TypedEnvelope, +}; use std::sync::Arc; use util::ResultExt; pub(crate) fn init(client: &Arc) { client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer); - client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator); - client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator); - client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborator); + client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators); } pub struct ChannelBuffer { pub(crate) channel: Arc, connected: bool, - collaborators: Vec, + collaborators: HashMap, + user_store: ModelHandle, buffer: ModelHandle, buffer_epoch: u64, client: Arc, @@ -46,6 +49,7 @@ impl ChannelBuffer { pub(crate) async fn new( channel: Arc, client: Arc, + user_store: ModelHandle, mut cx: AsyncAppContext, ) -> Result> { let response = client @@ -61,8 +65,6 @@ impl ChannelBuffer { .map(language::proto::deserialize_operation) .collect::, _>>()?; - let collaborators = response.collaborators; - let buffer = cx.add_model(|_| { language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) }); @@ -73,34 +75,45 @@ impl ChannelBuffer { anyhow::Ok(cx.add_model(|cx| { cx.subscribe(&buffer, Self::on_buffer_update).detach(); - Self { + let mut this = Self { buffer, buffer_epoch: response.epoch, client, connected: true, - collaborators, + collaborators: Default::default(), channel, subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), - } + user_store, + }; + this.replace_collaborators(response.collaborators, cx); + this })) } + pub fn user_store(&self) -> &ModelHandle { + &self.user_store + } + pub(crate) fn replace_collaborators( &mut self, collaborators: Vec, cx: &mut ModelContext, ) { - for old_collaborator in &self.collaborators { - if collaborators - .iter() - .any(|c| c.replica_id == old_collaborator.replica_id) - { + let mut new_collaborators = HashMap::default(); + for collaborator in collaborators { + if let Ok(collaborator) = Collaborator::from_proto(collaborator) { + new_collaborators.insert(collaborator.peer_id, collaborator); + } + } + + for (_, old_collaborator) in &self.collaborators { + if !new_collaborators.contains_key(&old_collaborator.peer_id) { self.buffer.update(cx, |buffer, cx| { buffer.remove_peer(old_collaborator.replica_id as u16, cx) }); } } - self.collaborators = collaborators; + self.collaborators = new_collaborators; cx.emit(ChannelBufferEvent::CollaboratorsChanged); cx.notify(); } @@ -127,64 +140,14 @@ impl ChannelBuffer { Ok(()) } - async fn handle_add_channel_buffer_collaborator( + async fn handle_update_channel_buffer_collaborators( this: ModelHandle, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - let collaborator = envelope.payload.collaborator.ok_or_else(|| { - anyhow::anyhow!( - "Should have gotten a collaborator in the AddChannelBufferCollaborator message" - ) - })?; - - this.update(&mut cx, |this, cx| { - this.collaborators.push(collaborator); - cx.emit(ChannelBufferEvent::CollaboratorsChanged); - cx.notify(); - }); - - Ok(()) - } - - async fn handle_remove_channel_buffer_collaborator( - this: ModelHandle, - message: TypedEnvelope, + message: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { this.update(&mut cx, |this, cx| { - this.collaborators.retain(|collaborator| { - if collaborator.peer_id == message.payload.peer_id { - this.buffer.update(cx, |buffer, cx| { - buffer.remove_peer(collaborator.replica_id as u16, cx) - }); - false - } else { - true - } - }); - cx.emit(ChannelBufferEvent::CollaboratorsChanged); - cx.notify(); - }); - - Ok(()) - } - - async fn handle_update_channel_buffer_collaborator( - this: ModelHandle, - message: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - for collaborator in &mut this.collaborators { - if collaborator.peer_id == message.payload.old_peer_id { - collaborator.peer_id = message.payload.new_peer_id; - break; - } - } + this.replace_collaborators(message.payload.collaborators, cx); cx.emit(ChannelBufferEvent::CollaboratorsChanged); cx.notify(); }); @@ -217,7 +180,7 @@ impl ChannelBuffer { self.buffer.clone() } - pub fn collaborators(&self) -> &[proto::Collaborator] { + pub fn collaborators(&self) -> &HashMap { &self.collaborators } diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index a5a0a92246..a8f6dd67b6 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -198,10 +198,11 @@ impl ChannelStore { cx: &mut ModelContext, ) -> Task>> { let client = self.client.clone(); + let user_store = self.user_store.clone(); self.open_channel_resource( channel_id, |this| &mut this.opened_buffers, - |channel, cx| ChannelBuffer::new(channel, client, cx), + |channel, cx| ChannelBuffer::new(channel, client, user_store, cx), cx, ) } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index e3038e5bcc..2bd03d789f 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -21,6 +21,7 @@ text = { path = "../text" } settings = { path = "../settings" } feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } +theme = { path = "../theme" } anyhow.workspace = true async-recursion = "0.3" diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 5f13aa40ac..0522545587 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -7,6 +7,8 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; +use text::ReplicaId; +use theme::ColorIndex; use util::http::HttpClient; use util::TryFutureExt as _; @@ -19,6 +21,13 @@ pub struct User { pub avatar: Option>, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Collaborator { + pub peer_id: proto::PeerId, + pub replica_id: ReplicaId, + pub user_id: UserId, +} + impl PartialOrd for User { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -56,6 +65,7 @@ pub enum ContactRequestStatus { pub struct UserStore { users: HashMap>, + color_indices: HashMap, update_contacts_tx: mpsc::UnboundedSender, current_user: watch::Receiver>>, contacts: Vec>, @@ -81,6 +91,7 @@ pub enum Event { kind: ContactEventKind, }, ShowContacts, + ColorIndicesChanged, } #[derive(Clone, Copy)] @@ -118,6 +129,7 @@ impl UserStore { current_user: current_user_rx, contacts: Default::default(), incoming_contact_requests: Default::default(), + color_indices: Default::default(), outgoing_contact_requests: Default::default(), invite_info: None, client: Arc::downgrade(&client), @@ -641,6 +653,21 @@ impl UserStore { } }) } + + pub fn set_color_indices( + &mut self, + color_indices: HashMap, + cx: &mut ModelContext, + ) { + if color_indices != self.color_indices { + self.color_indices = color_indices; + cx.emit(Event::ColorIndicesChanged); + } + } + + pub fn color_indices(&self) -> &HashMap { + &self.color_indices + } } impl User { @@ -672,6 +699,16 @@ impl Contact { } } +impl Collaborator { + pub fn from_proto(message: proto::Collaborator) -> Result { + Ok(Self { + peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?, + replica_id: message.replica_id as ReplicaId, + user_id: message.user_id as UserId, + }) + } +} + async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { let mut response = http .get(url, Default::default(), true) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index d0c4ead5ad..5f5484679f 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -158,7 +158,8 @@ CREATE TABLE "room_participants" ( "initial_project_id" INTEGER, "calling_user_id" INTEGER NOT NULL REFERENCES users (id), "calling_connection_id" INTEGER NOT NULL, - "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL + "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL, + "color_index" INTEGER ); CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id"); CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id"); diff --git a/crates/collab/migrations/20230926102500_add_color_index_to_room_participants.sql b/crates/collab/migrations/20230926102500_add_color_index_to_room_participants.sql new file mode 100644 index 0000000000..626268bd5c --- /dev/null +++ b/crates/collab/migrations/20230926102500_add_color_index_to_room_participants.sql @@ -0,0 +1 @@ +ALTER TABLE room_participants ADD COLUMN color_index INTEGER; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 527c4faaa5..ab2fbe3945 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -510,7 +510,7 @@ pub struct RefreshedRoom { pub struct RefreshedChannelBuffer { pub connection_ids: Vec, - pub removed_collaborators: Vec, + pub collaborators: Vec, } pub struct Project { diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 62ead11932..4b149faf2a 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -2,6 +2,12 @@ use super::*; use prost::Message; use text::{EditOperation, UndoOperation}; +pub struct LeftChannelBuffer { + pub channel_id: ChannelId, + pub collaborators: Vec, + pub connections: Vec, +} + impl Database { pub async fn join_channel_buffer( &self, @@ -204,23 +210,26 @@ impl Database { server_id: ServerId, ) -> Result { self.transaction(|tx| async move { - let collaborators = channel_buffer_collaborator::Entity::find() + let db_collaborators = channel_buffer_collaborator::Entity::find() .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) .all(&*tx) .await?; let mut connection_ids = Vec::new(); - let mut removed_collaborators = Vec::new(); + let mut collaborators = Vec::new(); let mut collaborator_ids_to_remove = Vec::new(); - for collaborator in &collaborators { - if !collaborator.connection_lost && collaborator.connection_server_id == server_id { - connection_ids.push(collaborator.connection()); + for db_collaborator in &db_collaborators { + if !db_collaborator.connection_lost + && db_collaborator.connection_server_id == server_id + { + connection_ids.push(db_collaborator.connection()); + collaborators.push(proto::Collaborator { + peer_id: Some(db_collaborator.connection().into()), + replica_id: db_collaborator.replica_id.0 as u32, + user_id: db_collaborator.user_id.to_proto(), + }) } else { - removed_collaborators.push(proto::RemoveChannelBufferCollaborator { - channel_id: channel_id.to_proto(), - peer_id: Some(collaborator.connection().into()), - }); - collaborator_ids_to_remove.push(collaborator.id); + collaborator_ids_to_remove.push(db_collaborator.id); } } @@ -231,7 +240,7 @@ impl Database { Ok(RefreshedChannelBuffer { connection_ids, - removed_collaborators, + collaborators, }) }) .await @@ -241,7 +250,7 @@ impl Database { &self, channel_id: ChannelId, connection: ConnectionId, - ) -> Result> { + ) -> Result { self.transaction(|tx| async move { self.leave_channel_buffer_internal(channel_id, connection, &*tx) .await @@ -275,7 +284,7 @@ impl Database { pub async fn leave_channel_buffers( &self, connection: ConnectionId, - ) -> Result)>> { + ) -> Result> { self.transaction(|tx| async move { #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] enum QueryChannelIds { @@ -294,10 +303,10 @@ impl Database { let mut result = Vec::new(); for channel_id in channel_ids { - let collaborators = self + let left_channel_buffer = self .leave_channel_buffer_internal(channel_id, connection, &*tx) .await?; - result.push((channel_id, collaborators)); + result.push(left_channel_buffer); } Ok(result) @@ -310,7 +319,7 @@ impl Database { channel_id: ChannelId, connection: ConnectionId, tx: &DatabaseTransaction, - ) -> Result> { + ) -> Result { let result = channel_buffer_collaborator::Entity::delete_many() .filter( Condition::all() @@ -327,6 +336,7 @@ impl Database { Err(anyhow!("not a collaborator on this project"))?; } + let mut collaborators = Vec::new(); let mut connections = Vec::new(); let mut rows = channel_buffer_collaborator::Entity::find() .filter( @@ -336,19 +346,26 @@ impl Database { .await?; while let Some(row) = rows.next().await { let row = row?; - connections.push(ConnectionId { - id: row.connection_id as u32, - owner_id: row.connection_server_id.0 as u32, + let connection = row.connection(); + connections.push(connection); + collaborators.push(proto::Collaborator { + peer_id: Some(connection.into()), + replica_id: row.replica_id.0 as u32, + user_id: row.user_id.to_proto(), }); } drop(rows); - if connections.is_empty() { + if collaborators.is_empty() { self.snapshot_channel_buffer(channel_id, &tx).await?; } - Ok(connections) + Ok(LeftChannelBuffer { + channel_id, + collaborators, + connections, + }) } pub async fn get_channel_buffer_collaborators( diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 651d58c265..fca4c67690 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -152,6 +152,7 @@ impl Database { room_id: ActiveValue::set(room_id), user_id: ActiveValue::set(called_user_id), answering_connection_lost: ActiveValue::set(false), + color_index: ActiveValue::NotSet, calling_user_id: ActiveValue::set(calling_user_id), calling_connection_id: ActiveValue::set(calling_connection.id as i32), calling_connection_server_id: ActiveValue::set(Some(ServerId( @@ -283,6 +284,22 @@ impl Database { .await? .ok_or_else(|| anyhow!("no such room"))?; + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryColorIndices { + ColorIndex, + } + let existing_color_indices: Vec = room_participant::Entity::find() + .filter(room_participant::Column::RoomId.eq(room_id)) + .select_only() + .column(room_participant::Column::ColorIndex) + .into_values::<_, QueryColorIndices>() + .all(&*tx) + .await?; + let mut color_index = 0; + while existing_color_indices.contains(&color_index) { + color_index += 1; + } + if let Some(channel_id) = channel_id { self.check_user_is_channel_member(channel_id, user_id, &*tx) .await?; @@ -300,6 +317,7 @@ impl Database { calling_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, ))), + color_index: ActiveValue::Set(color_index), ..Default::default() }]) .on_conflict( @@ -322,6 +340,7 @@ impl Database { .add(room_participant::Column::AnsweringConnectionId.is_null()), ) .set(room_participant::ActiveModel { + color_index: ActiveValue::Set(color_index), answering_connection_id: ActiveValue::set(Some(connection.id as i32)), answering_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, @@ -1071,6 +1090,7 @@ impl Database { peer_id: Some(answering_connection.into()), projects: Default::default(), location: Some(proto::ParticipantLocation { variant: location }), + color_index: db_participant.color_index as u32, }, ); } else { diff --git a/crates/collab/src/db/tables/room_participant.rs b/crates/collab/src/db/tables/room_participant.rs index 57d79fa830..8072fed69c 100644 --- a/crates/collab/src/db/tables/room_participant.rs +++ b/crates/collab/src/db/tables/room_participant.rs @@ -18,6 +18,7 @@ pub struct Model { pub calling_user_id: UserId, pub calling_connection_id: i32, pub calling_connection_server_id: Option, + pub color_index: i32, } impl Model { diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index e71748b88b..9808a9955b 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -134,12 +134,12 @@ async fn test_channel_buffers(db: &Arc) { let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); assert_eq!(zed_collaborats, &[a_id, b_id]); - let collaborators = db + let left_buffer = db .leave_channel_buffer(zed_id, connection_id_b) .await .unwrap(); - assert_eq!(collaborators, &[connection_id_a],); + assert_eq!(left_buffer.connections, &[connection_id_a],); let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap(); let _ = db diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b3af2d4e98..56cecb2e74 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -38,8 +38,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, ChannelEdge, EntityMessage, - EnvelopedMessage, LiveKitConnectionInfo, RequestMessage, + self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage, + LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -313,9 +313,16 @@ impl Server { .trace_err() { for connection_id in refreshed_channel_buffer.connection_ids { - for message in &refreshed_channel_buffer.removed_collaborators { - peer.send(connection_id, message.clone()).trace_err(); - } + peer.send( + connection_id, + proto::UpdateChannelBufferCollaborators { + channel_id: channel_id.to_proto(), + collaborators: refreshed_channel_buffer + .collaborators + .clone(), + }, + ) + .trace_err(); } } } @@ -2654,18 +2661,12 @@ async fn join_channel_buffer( .join_channel_buffer(channel_id, session.user_id, session.connection_id) .await?; - let replica_id = open_response.replica_id; let collaborators = open_response.collaborators.clone(); - response.send(open_response)?; - let update = AddChannelBufferCollaborator { + let update = UpdateChannelBufferCollaborators { channel_id: channel_id.to_proto(), - collaborator: Some(proto::Collaborator { - user_id: session.user_id.to_proto(), - peer_id: Some(session.connection_id.into()), - replica_id, - }), + collaborators: collaborators.clone(), }; channel_buffer_updated( session.connection_id, @@ -2712,8 +2713,8 @@ async fn rejoin_channel_buffers( .rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id) .await?; - for buffer in &buffers { - let collaborators_to_notify = buffer + for rejoined_buffer in &buffers { + let collaborators_to_notify = rejoined_buffer .buffer .collaborators .iter() @@ -2721,10 +2722,9 @@ async fn rejoin_channel_buffers( channel_buffer_updated( session.connection_id, collaborators_to_notify, - &proto::UpdateChannelBufferCollaborator { - channel_id: buffer.buffer.channel_id, - old_peer_id: Some(buffer.old_connection_id.into()), - new_peer_id: Some(session.connection_id.into()), + &proto::UpdateChannelBufferCollaborators { + channel_id: rejoined_buffer.buffer.channel_id, + collaborators: rejoined_buffer.buffer.collaborators.clone(), }, &session.peer, ); @@ -2745,7 +2745,7 @@ async fn leave_channel_buffer( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let collaborators_to_notify = db + let left_buffer = db .leave_channel_buffer(channel_id, session.connection_id) .await?; @@ -2753,10 +2753,10 @@ async fn leave_channel_buffer( channel_buffer_updated( session.connection_id, - collaborators_to_notify, - &proto::RemoveChannelBufferCollaborator { + left_buffer.connections, + &proto::UpdateChannelBufferCollaborators { channel_id: channel_id.to_proto(), - peer_id: Some(session.connection_id.into()), + collaborators: left_buffer.collaborators, }, &session.peer, ); @@ -3231,13 +3231,13 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> { .leave_channel_buffers(session.connection_id) .await?; - for (channel_id, connections) in left_channel_buffers { + for left_buffer in left_channel_buffers { channel_buffer_updated( session.connection_id, - connections, - &proto::RemoveChannelBufferCollaborator { - channel_id: channel_id.to_proto(), - peer_id: Some(session.connection_id.into()), + left_buffer.connections, + &proto::UpdateChannelBufferCollaborators { + channel_id: left_buffer.channel_id.to_proto(), + collaborators: left_buffer.collaborators, }, &session.peer, ); diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index baab675a1c..e403ed6f94 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -4,14 +4,16 @@ use crate::{ }; use call::ActiveCall; use channel::Channel; -use client::UserId; +use client::{Collaborator, UserId}; use collab_ui::channel_view::ChannelView; use collections::HashMap; +use editor::{Anchor, Editor, ToOffset}; use futures::future; -use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; -use rpc::{proto, RECEIVE_TIMEOUT}; +use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext}; +use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; use serde_json::json; -use std::sync::Arc; +use std::{ops::Range, sync::Arc}; +use theme::ColorIndex; #[gpui::test] async fn test_core_channel_buffers( @@ -120,10 +122,10 @@ async fn test_core_channel_buffers( } #[gpui::test] -async fn test_channel_buffer_replica_ids( +async fn test_channel_notes_color_indices( deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, + mut cx_a: &mut TestAppContext, + mut cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); @@ -132,6 +134,13 @@ async fn test_channel_buffer_replica_ids( let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + cx_c.update(editor::init); + let channel_id = server .make_channel( "the-channel", @@ -141,136 +150,158 @@ async fn test_channel_buffer_replica_ids( ) .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - let active_call_c = cx_c.read(ActiveCall::global); - - // Clients A and B join a channel. - active_call_a - .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) - .await - .unwrap(); - active_call_b - .update(cx_b, |call, cx| call.join_channel(channel_id, cx)) - .await - .unwrap(); - - // Clients A, B, and C join a channel buffer - // C first so that the replica IDs in the project and the channel buffer are different - let channel_buffer_c = client_c - .channel_store() - .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - let channel_buffer_b = client_b - .channel_store() - .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - let channel_buffer_a = client_a - .channel_store() - .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) - .await - .unwrap(); - - // Client B shares a project - client_b + client_a .fs() - .insert_tree("/dir", json!({ "file.txt": "contents" })) + .insert_tree("/root", json!({"file.txt": "123"})) .await; - let (project_b, _) = client_b.build_local_project("/dir", cx_b).await; - let shared_project_id = active_call_b - .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await; + let project_b = client_b.build_empty_local_project(cx_b); + let project_c = client_c.build_empty_local_project(cx_c); + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); + + // Clients A, B, and C open the channel notes + let channel_view_a = cx_a + .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx)) + .await + .unwrap(); + let channel_view_b = cx_b + .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) + .await + .unwrap(); + let channel_view_c = cx_c + .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx)) .await .unwrap(); - // Client A joins the project - let project_a = client_a.build_remote_project(shared_project_id, cx_a).await; + // Clients A, B, and C all insert and select some text + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.insert("a", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![0..1]); + }); + }); + }); deterministic.run_until_parked(); - - // Client C is in a separate project. - client_c.fs().insert_tree("/dir", json!({})).await; - let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await; - - // Note that each user has a different replica id in the projects vs the - // channel buffer. - channel_buffer_a.read_with(cx_a, |channel_buffer, cx| { - assert_eq!(project_a.read(cx).replica_id(), 1); - assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2); + channel_view_b.update(cx_b, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.move_down(&Default::default(), cx); + editor.insert("b", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![1..2]); + }); + }); }); - channel_buffer_b.read_with(cx_b, |channel_buffer, cx| { - assert_eq!(project_b.read(cx).replica_id(), 0); - assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1); - }); - channel_buffer_c.read_with(cx_c, |channel_buffer, cx| { - // C is not in the project - assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0); + deterministic.run_until_parked(); + channel_view_c.update(cx_c, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.move_down(&Default::default(), cx); + editor.insert("c", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![2..3]); + }); + }); }); - let channel_window_a = - cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx)); - let channel_window_b = - cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx)); - let channel_window_c = cx_c.add_window(|cx| { - ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx) + // Client A sees clients B and C without assigned colors, because they aren't + // in a call together. + deterministic.run_until_parked(); + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx); + }); }); - let channel_view_a = channel_window_a.root(cx_a); - let channel_view_b = channel_window_b.root(cx_b); - let channel_view_c = channel_window_c.root(cx_c); + // Clients A and B join the same call. + for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] { + call.update(*cx, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + } - // For clients A and B, the replica ids in the channel buffer are mapped - // so that they match the same users' replica ids in their shared project. - channel_view_a.read_with(cx_a, |view, cx| { - assert_eq!( - view.editor.read(cx).replica_id_map().unwrap(), - &[(1, 0), (2, 1)].into_iter().collect::>() - ); + // Clients A and B see each other with two different assigned colors. Client C + // still doesn't have a color. + deterministic.run_until_parked(); + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections(editor, &[(Some(ColorIndex(1)), 1..2), (None, 2..3)], cx); + }); }); - channel_view_b.read_with(cx_b, |view, cx| { - assert_eq!( - view.editor.read(cx).replica_id_map().unwrap(), - &[(1, 0), (2, 1)].into_iter().collect::>(), - ) + channel_view_b.update(cx_b, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections(editor, &[(Some(ColorIndex(0)), 0..1), (None, 2..3)], cx); + }); }); - // Client C only sees themself, as they're not part of any shared project - channel_view_c.read_with(cx_c, |view, cx| { - assert_eq!( - view.editor.read(cx).replica_id_map().unwrap(), - &[(0, 0)].into_iter().collect::>(), - ); - }); - - // Client C joins the project that clients A and B are in. - active_call_c - .update(cx_c, |call, cx| call.join_channel(channel_id, cx)) + // Client A shares a project, and client B joins. + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_c = client_c.build_remote_project(shared_project_id, cx_c).await; - deterministic.run_until_parked(); - project_c.read_with(cx_c, |project, _| { - assert_eq!(project.replica_id(), 2); - }); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - // For clients A and B, client C's replica id in the channel buffer is - // now mapped to their replica id in the shared project. - channel_view_a.read_with(cx_a, |view, cx| { - assert_eq!( - view.editor.read(cx).replica_id_map().unwrap(), - &[(1, 0), (2, 1), (0, 2)] - .into_iter() - .collect::>() - ); + // Clients A and B open the same file. + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + editor_a.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![0..1]); + }); }); - channel_view_b.read_with(cx_b, |view, cx| { - assert_eq!( - view.editor.read(cx).replica_id_map().unwrap(), - &[(1, 0), (2, 1), (0, 2)] - .into_iter() - .collect::>(), - ) + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![2..3]); + }); }); + deterministic.run_until_parked(); + + // Clients A and B see each other with the same colors as in the channel notes. + editor_a.update(cx_a, |editor, cx| { + assert_remote_selections(editor, &[(Some(ColorIndex(1)), 2..3)], cx); + }); + editor_b.update(cx_b, |editor, cx| { + assert_remote_selections(editor, &[(Some(ColorIndex(0)), 0..1)], cx); + }); +} + +#[track_caller] +fn assert_remote_selections( + editor: &mut Editor, + expected_selections: &[(Option, Range)], + cx: &mut ViewContext, +) { + let snapshot = editor.snapshot(cx); + let range = Anchor::min()..Anchor::max(); + let remote_selections = snapshot + .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) + .map(|s| { + let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); + let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); + (s.color_index, start..end) + }) + .collect::>(); + assert_eq!( + remote_selections, expected_selections, + "incorrect remote selections" + ); } #[gpui::test] @@ -568,13 +599,9 @@ async fn test_channel_buffers_and_server_restarts( channel_buffer_a.read_with(cx_a, |buffer_a, _| { channel_buffer_b.read_with(cx_b, |buffer_b, _| { - assert_eq!( - buffer_a - .collaborators() - .iter() - .map(|c| c.user_id) - .collect::>(), - vec![client_a.user_id().unwrap(), client_b.user_id().unwrap()] + assert_collaborators( + buffer_a.collaborators(), + &[client_a.user_id(), client_b.user_id()], ); assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); }); @@ -723,10 +750,10 @@ async fn test_following_to_channel_notes_without_a_shared_project( } #[track_caller] -fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { +fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { assert_eq!( collaborators - .into_iter() + .values() .map(|collaborator| collaborator.user_id) .collect::>(), ids.into_iter().map(|id| id.unwrap()).collect::>() diff --git a/crates/collab/src/tests/random_channel_buffer_tests.rs b/crates/collab/src/tests/random_channel_buffer_tests.rs index 2950922e7c..ad0181602c 100644 --- a/crates/collab/src/tests/random_channel_buffer_tests.rs +++ b/crates/collab/src/tests/random_channel_buffer_tests.rs @@ -273,7 +273,7 @@ impl RandomizedTest for RandomChannelBufferTest { // channel buffer. let collaborators = channel_buffer.collaborators(); let mut user_ids = - collaborators.iter().map(|c| c.user_id).collect::>(); + collaborators.values().map(|c| c.user_id).collect::>(); user_ids.sort(); assert_eq!( user_ids, diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 6a15cac9e9..71537f069f 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -538,15 +538,7 @@ impl TestClient { root_path: impl AsRef, cx: &mut TestAppContext, ) -> (ModelHandle, WorktreeId) { - let project = cx.update(|cx| { - Project::local( - self.client().clone(), - self.app_state.user_store.clone(), - self.app_state.languages.clone(), - self.app_state.fs.clone(), - cx, - ) - }); + let project = self.build_empty_local_project(cx); let (worktree, _) = project .update(cx, |p, cx| { p.find_or_create_local_worktree(root_path, true, cx) @@ -559,6 +551,18 @@ impl TestClient { (project, worktree.read_with(cx, |tree, _| tree.id())) } + pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> ModelHandle { + cx.update(|cx| { + Project::local( + self.client().clone(), + self.app_state.user_store.clone(), + self.app_state.languages.clone(), + self.app_state.fs.clone(), + cx, + ) + }) + } + pub async fn build_remote_project( &self, host_project_id: u64, diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index b66d1ab7c7..1d103350de 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,10 +1,12 @@ use anyhow::{anyhow, Result}; use call::ActiveCall; use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId}; -use client::proto; -use clock::ReplicaId; +use client::{ + proto::{self, PeerId}, + Collaborator, +}; use collections::HashMap; -use editor::Editor; +use editor::{CollaborationHub, Editor}; use gpui::{ actions, elements::{ChildView, Label}, @@ -109,97 +111,44 @@ impl ChannelView { cx: &mut ViewContext, ) -> Self { let buffer = channel_buffer.read(cx).buffer(); - let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); + let editor = cx.add_view(|cx| { + let mut editor = Editor::for_buffer(buffer, None, cx); + editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( + channel_buffer.clone(), + ))); + editor + }); let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); - cx.subscribe(&project, Self::handle_project_event).detach(); cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) .detach(); - let this = Self { + Self { editor, project, channel_buffer, remote_id: None, _editor_event_subscription, - }; - this.refresh_replica_id_map(cx); - this + } } pub fn channel(&self, cx: &AppContext) -> Arc { self.channel_buffer.read(cx).channel() } - fn handle_project_event( - &mut self, - _: ModelHandle, - event: &project::Event, - cx: &mut ViewContext, - ) { - match event { - project::Event::RemoteIdChanged(_) => {} - project::Event::DisconnectedFromHost => {} - project::Event::Closed => {} - project::Event::CollaboratorUpdated { .. } => {} - project::Event::CollaboratorLeft(_) => {} - project::Event::CollaboratorJoined(_) => {} - _ => return, - } - self.refresh_replica_id_map(cx); - } - fn handle_channel_buffer_event( &mut self, _: ModelHandle, event: &ChannelBufferEvent, cx: &mut ViewContext, ) { - match event { - ChannelBufferEvent::CollaboratorsChanged => { - self.refresh_replica_id_map(cx); - } - ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { + if let ChannelBufferEvent::Disconnected = event { + self.editor.update(cx, |editor, cx| { editor.set_read_only(true); cx.notify(); - }), + }) } } - - /// Build a mapping of channel buffer replica ids to the corresponding - /// replica ids in the current project. - /// - /// Using this mapping, a given user can be displayed with the same color - /// in the channel buffer as in other files in the project. Users who are - /// in the channel buffer but not the project will not have a color. - fn refresh_replica_id_map(&self, cx: &mut ViewContext) { - let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default(); - let project = self.project.read(cx); - let channel_buffer = self.channel_buffer.read(cx); - project_replica_ids_by_channel_buffer_replica_id - .insert(channel_buffer.replica_id(cx), project.replica_id()); - project_replica_ids_by_channel_buffer_replica_id.extend( - channel_buffer - .collaborators() - .iter() - .filter_map(|channel_buffer_collaborator| { - project - .collaborators() - .values() - .find_map(|project_collaborator| { - (project_collaborator.user_id == channel_buffer_collaborator.user_id) - .then_some(( - channel_buffer_collaborator.replica_id as ReplicaId, - project_collaborator.replica_id, - )) - }) - }), - ); - - self.editor.update(cx, |editor, cx| { - editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx) - }); - } } impl Entity for ChannelView { @@ -388,13 +337,9 @@ impl FollowableItem for ChannelView { }) } - fn set_leader_replica_id( - &mut self, - leader_replica_id: Option, - cx: &mut ViewContext, - ) { + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { self.editor.update(cx, |editor, cx| { - editor.set_leader_replica_id(leader_replica_id, cx) + editor.set_leader_peer_id(leader_peer_id, cx) }) } @@ -406,3 +351,15 @@ impl FollowableItem for ChannelView { false } } + +struct ChannelBufferCollaborationHub(ModelHandle); + +impl CollaborationHub for ChannelBufferCollaborationHub { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.0.read(cx).collaborators() + } + + fn user_color_indices<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.0.read(cx).user_store().read(cx).color_indices() + } +} diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index a676b40b75..5dafae0cd5 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -923,9 +923,15 @@ impl CollabTitlebarItem { .background_color .unwrap_or_default(); - if let Some(replica_id) = replica_id { + let color_index = self + .user_store + .read(cx) + .color_indices() + .get(&user_id) + .copied(); + if let Some(color_index) = color_index { if followed_by_self { - let selection = theme.editor.replica_selection_style(replica_id).selection; + let selection = theme.editor.replica_selection_style(color_index).selection; background_color = Color::blend(selection, background_color); background_color.a = 255; } @@ -990,10 +996,10 @@ impl CollabTitlebarItem { .contained() .with_style(theme.titlebar.leader_selection); - if let Some(replica_id) = replica_id { + if let Some(color_index) = color_index { if followed_by_self { let color = - theme.editor.replica_selection_style(replica_id).selection; + theme.editor.replica_selection_style(color_index).selection; container = container.with_background_color(color); } } @@ -1001,8 +1007,8 @@ impl CollabTitlebarItem { container })) .with_children((|| { - let replica_id = replica_id?; - let color = theme.editor.replica_selection_style(replica_id).cursor; + let color_index = color_index?; + let color = theme.editor.replica_selection_style(color_index).cursor; Some( AvatarRibbon::new(color) .constrained() diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0827e13264..7692a54b01 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -25,7 +25,7 @@ use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context, Result}; use blink_manager::BlinkManager; -use client::{ClickhouseEvent, TelemetrySettings}; +use client::{ClickhouseEvent, Collaborator, TelemetrySettings}; use clock::{Global, ReplicaId}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; @@ -79,6 +79,7 @@ pub use multi_buffer::{ use ordered_float::OrderedFloat; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::{seq::SliceRandom, thread_rng}; +use rpc::proto::PeerId; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, }; @@ -101,7 +102,7 @@ use std::{ pub use sum_tree::Bias; use sum_tree::TreeMap; use text::Rope; -use theme::{DiagnosticStyle, Theme, ThemeSettings}; +use theme::{ColorIndex, DiagnosticStyle, Theme, ThemeSettings}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ItemNavHistory, ViewId, Workspace}; @@ -580,11 +581,11 @@ pub struct Editor { get_field_editor_theme: Option>, override_text_style: Option>, project: Option>, + collaboration_hub: Option>, focused: bool, blink_manager: ModelHandle, pub show_local_selections: bool, mode: EditorMode, - replica_id_mapping: Option>, show_gutter: bool, show_wrap_guides: Option, placeholder_text: Option>, @@ -608,7 +609,7 @@ pub struct Editor { keymap_context_layers: BTreeMap, input_enabled: bool, read_only: bool, - leader_replica_id: Option, + leader_peer_id: Option, remote_id: Option, hover_state: HoverState, gutter_hovered: bool, @@ -630,6 +631,15 @@ pub struct EditorSnapshot { ongoing_scroll: OngoingScroll, } +pub struct RemoteSelection { + pub replica_id: ReplicaId, + pub selection: Selection, + pub cursor_shape: CursorShape, + pub peer_id: PeerId, + pub line_mode: bool, + pub color_index: Option, +} + #[derive(Clone, Debug)] struct SelectionHistoryEntry { selections: Arc<[Selection]>, @@ -1532,12 +1542,12 @@ impl Editor { active_diagnostics: None, soft_wrap_mode_override, get_field_editor_theme, + collaboration_hub: project.clone().map(|project| Box::new(project) as _), project, focused: false, blink_manager: blink_manager.clone(), show_local_selections: true, mode, - replica_id_mapping: None, show_gutter: mode == EditorMode::Full, show_wrap_guides: None, placeholder_text: None, @@ -1564,7 +1574,7 @@ impl Editor { keymap_context_layers: Default::default(), input_enabled: true, read_only: false, - leader_replica_id: None, + leader_peer_id: None, remote_id: None, hover_state: Default::default(), link_go_to_definition_state: Default::default(), @@ -1631,8 +1641,8 @@ impl Editor { self.buffer.read(cx).replica_id() } - pub fn leader_replica_id(&self) -> Option { - self.leader_replica_id + pub fn leader_peer_id(&self) -> Option { + self.leader_peer_id } pub fn buffer(&self) -> &ModelHandle { @@ -1696,6 +1706,14 @@ impl Editor { self.mode } + pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> { + self.collaboration_hub.as_deref() + } + + pub fn set_collaboration_hub(&mut self, hub: Box) { + self.collaboration_hub = Some(hub); + } + pub fn set_placeholder_text( &mut self, placeholder_text: impl Into>, @@ -1772,26 +1790,13 @@ impl Editor { cx.notify(); } - pub fn replica_id_map(&self) -> Option<&HashMap> { - self.replica_id_mapping.as_ref() - } - - pub fn set_replica_id_map( - &mut self, - mapping: Option>, - cx: &mut ViewContext, - ) { - self.replica_id_mapping = mapping; - cx.notify(); - } - fn selections_did_change( &mut self, local: bool, old_cursor_position: &Anchor, cx: &mut ViewContext, ) { - if self.focused && self.leader_replica_id.is_none() { + if self.focused && self.leader_peer_id.is_none() { self.buffer.update(cx, |buffer, cx| { buffer.set_active_selections( &self.selections.disjoint_anchors(), @@ -8563,6 +8568,21 @@ impl Editor { } } +pub trait CollaborationHub { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap; + fn user_color_indices<'a>(&self, cx: &'a AppContext) -> &'a HashMap; +} + +impl CollaborationHub for ModelHandle { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.read(cx).collaborators() + } + + fn user_color_indices<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.read(cx).user_store().read(cx).color_indices() + } +} + fn inlay_hint_settings( location: Anchor, snapshot: &MultiBufferSnapshot, @@ -8606,6 +8626,34 @@ fn ending_row(next_selection: &Selection, display_map: &DisplaySnapshot) } impl EditorSnapshot { + pub fn remote_selections_in_range<'a>( + &'a self, + range: &'a Range, + collaboration_hub: &dyn CollaborationHub, + cx: &'a AppContext, + ) -> impl 'a + Iterator { + let color_indices = collaboration_hub.user_color_indices(cx); + let collaborators_by_peer_id = collaboration_hub.collaborators(cx); + let collaborators_by_replica_id = collaborators_by_peer_id + .iter() + .map(|(_, collaborator)| (collaborator.replica_id, collaborator)) + .collect::>(); + self.buffer_snapshot + .remote_selections_in_range(range) + .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| { + let collaborator = collaborators_by_replica_id.get(&replica_id)?; + let color_index = color_indices.get(&collaborator.user_id).copied(); + Some(RemoteSelection { + replica_id, + selection, + cursor_shape, + line_mode, + color_index, + peer_id: collaborator.peer_id, + }) + }) + } + pub fn language_at(&self, position: T) -> Option<&Arc> { self.display_snapshot.buffer_snapshot.language_at(position) } @@ -8719,7 +8767,7 @@ impl View for Editor { self.focused = true; self.buffer.update(cx, |buffer, cx| { buffer.finalize_last_transaction(cx); - if self.leader_replica_id.is_none() { + if self.leader_peer_id.is_none() { buffer.set_active_selections( &self.selections.disjoint_anchors(), self.selections.line_mode, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3390b70530..dad5b06626 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -17,7 +17,6 @@ use crate::{ }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; -use clock::ReplicaId; use collections::{BTreeMap, HashMap}; use git::diff::DiffHunkStatus; use gpui::{ @@ -55,6 +54,7 @@ use std::{ sync::Arc, }; use text::Point; +use theme::SelectionStyle; use workspace::item::Item; enum FoldMarkers {} @@ -868,14 +868,7 @@ impl EditorElement { let corner_radius = 0.15 * layout.position_map.line_height; let mut invisible_display_ranges = SmallVec::<[Range; 32]>::new(); - for (replica_id, selections) in &layout.selections { - let replica_id = *replica_id; - let selection_style = if let Some(replica_id) = replica_id { - style.replica_selection_style(replica_id) - } else { - &style.absent_selection - }; - + for (selection_style, selections) in &layout.selections { for selection in selections { self.paint_highlighted_range( selection.range.clone(), @@ -2193,7 +2186,7 @@ impl Element for EditorElement { .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) }; - let mut selections: Vec<(Option, Vec)> = Vec::new(); + let mut selections: Vec<(SelectionStyle, Vec)> = Vec::new(); let mut active_rows = BTreeMap::new(); let mut fold_ranges = Vec::new(); let is_singleton = editor.is_singleton(cx); @@ -2219,35 +2212,6 @@ impl Element for EditorElement { }), ); - let mut remote_selections = HashMap::default(); - for (replica_id, line_mode, cursor_shape, selection) in snapshot - .buffer_snapshot - .remote_selections_in_range(&(start_anchor..end_anchor)) - { - let replica_id = if let Some(mapping) = &editor.replica_id_mapping { - mapping.get(&replica_id).copied() - } else { - Some(replica_id) - }; - - // The local selections match the leader's selections. - if replica_id.is_some() && replica_id == editor.leader_replica_id { - continue; - } - remote_selections - .entry(replica_id) - .or_insert(Vec::new()) - .push(SelectionLayout::new( - selection, - line_mode, - cursor_shape, - &snapshot.display_snapshot, - false, - false, - )); - } - selections.extend(remote_selections); - let mut newest_selection_head = None; if editor.show_local_selections { @@ -2282,19 +2246,45 @@ impl Element for EditorElement { layouts.push(layout); } - // Render the local selections in the leader's color when following. - let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id { - leader_replica_id - } else { - let replica_id = editor.replica_id(cx); - if let Some(mapping) = &editor.replica_id_mapping { - mapping.get(&replica_id).copied().unwrap_or(replica_id) - } else { - replica_id - } - }; + selections.push((style.selection, layouts)); + } - selections.push((Some(local_replica_id), layouts)); + if let Some(collaboration_hub) = &editor.collaboration_hub { + let mut remote_selections = HashMap::default(); + for selection in snapshot.remote_selections_in_range( + &(start_anchor..end_anchor), + collaboration_hub.as_ref(), + cx, + ) { + let selection_style = if let Some(color_index) = selection.color_index { + style.replica_selection_style(color_index) + } else { + style.absent_selection + }; + + // The local selections match the leader's selections. + if Some(selection.peer_id) == editor.leader_peer_id { + if let Some((local_selection_style, _)) = selections.first_mut() { + *local_selection_style = selection_style; + } + continue; + } + + remote_selections + .entry(selection.replica_id) + .or_insert((selection_style, Vec::new())) + .1 + .push(SelectionLayout::new( + selection.selection, + selection.line_mode, + selection.cursor_shape, + &snapshot.display_snapshot, + false, + false, + )); + } + + selections.extend(remote_selections.into_values()); } let scrollbar_settings = &settings::get::(cx).scrollbar; @@ -2686,7 +2676,7 @@ pub struct LayoutState { blocks: Vec, highlighted_ranges: Vec<(Range, Color)>, fold_ranges: Vec<(BufferRow, Range, Color)>, - selections: Vec<(Option, Vec)>, + selections: Vec<(SelectionStyle, Vec)>, scrollbar_row_range: Range, show_scrollbars: bool, is_singleton: bool, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7fdbe82a9a..1fee309181 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -17,7 +17,7 @@ use language::{ SelectionGoal, }; use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; -use rpc::proto::{self, update_view}; +use rpc::proto::{self, update_view, PeerId}; use smallvec::SmallVec; use std::{ borrow::Cow, @@ -156,13 +156,9 @@ impl FollowableItem for Editor { })) } - fn set_leader_replica_id( - &mut self, - leader_replica_id: Option, - cx: &mut ViewContext, - ) { - self.leader_replica_id = leader_replica_id; - if self.leader_replica_id.is_some() { + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { + self.leader_peer_id = leader_peer_id; + if self.leader_peer_id.is_some() { self.buffer.update(cx, |buffer, cx| { buffer.remove_active_selections(cx); }); diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index ffea6646e9..07f1bfa43e 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -35,6 +35,7 @@ rpc = { path = "../rpc" } settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } terminal = { path = "../terminal" } +theme = { path = "../theme" } util = { path = "../util" } aho-corasick = "1.1" diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index e4858587ad..62df8425c4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11,7 +11,7 @@ mod project_tests; mod worktree_tests; use anyhow::{anyhow, Context, Result}; -use client::{proto, Client, TypedEnvelope, UserId, UserStore}; +use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; use copilot::Copilot; @@ -76,6 +76,7 @@ use std::{ }; use terminals::Terminals; use text::Anchor; +use theme::ColorIndex; use util::{ debug_panic, defer, http::HttpClient, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, @@ -119,6 +120,7 @@ pub struct Project { join_project_response_message_id: u32, next_diagnostic_group_id: usize, user_store: ModelHandle, + user_color_indices: HashMap, fs: Arc, client_state: Option, collaborators: HashMap, @@ -253,13 +255,6 @@ enum ProjectClientState { }, } -#[derive(Clone, Debug)] -pub struct Collaborator { - pub peer_id: proto::PeerId, - pub replica_id: ReplicaId, - pub user_id: UserId, -} - #[derive(Clone, Debug, PartialEq)] pub enum Event { LanguageServerAdded(LanguageServerId), @@ -649,6 +644,7 @@ impl Project { languages, client, user_store, + user_color_indices: Default::default(), fs, next_entry_id: Default::default(), next_diagnostic_group_id: Default::default(), @@ -721,6 +717,7 @@ impl Project { _maintain_workspace_config: Self::maintain_workspace_config(cx), languages, user_store: user_store.clone(), + user_color_indices: Default::default(), fs, next_entry_id: Default::default(), next_diagnostic_group_id: Default::default(), @@ -925,6 +922,10 @@ impl Project { self.user_store.clone() } + pub fn user_color_indices(&self) -> &HashMap { + &self.user_color_indices + } + pub fn opened_buffers(&self, cx: &AppContext) -> Vec> { self.opened_buffers .values() @@ -8211,16 +8212,6 @@ impl Entity for Project { } } -impl Collaborator { - fn from_proto(message: proto::Collaborator) -> Result { - Ok(Self { - peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?, - replica_id: message.replica_id as ReplicaId, - user_id: message.user_id as UserId, - }) - } -} - impl> From<(WorktreeId, P)> for ProjectPath { fn from((worktree_id, path): (WorktreeId, P)) -> Self { Self { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 9c1ec4e613..da97cd35c7 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -23,154 +23,152 @@ message Envelope { CreateRoomResponse create_room_response = 10; JoinRoom join_room = 11; JoinRoomResponse join_room_response = 12; - RejoinRoom rejoin_room = 108; - RejoinRoomResponse rejoin_room_response = 109; - LeaveRoom leave_room = 13; - Call call = 14; - IncomingCall incoming_call = 15; - CallCanceled call_canceled = 16; - CancelCall cancel_call = 17; - DeclineCall decline_call = 18; - UpdateParticipantLocation update_participant_location = 19; - RoomUpdated room_updated = 20; + RejoinRoom rejoin_room = 13; + RejoinRoomResponse rejoin_room_response = 14; + LeaveRoom leave_room = 15; + Call call = 16; + IncomingCall incoming_call = 17; + CallCanceled call_canceled = 18; + CancelCall cancel_call = 19; + DeclineCall decline_call = 20; + UpdateParticipantLocation update_participant_location = 21; + RoomUpdated room_updated = 22; - ShareProject share_project = 21; - ShareProjectResponse share_project_response = 22; - UnshareProject unshare_project = 23; - JoinProject join_project = 24; - JoinProjectResponse join_project_response = 25; - LeaveProject leave_project = 26; - AddProjectCollaborator add_project_collaborator = 27; - UpdateProjectCollaborator update_project_collaborator = 110; - RemoveProjectCollaborator remove_project_collaborator = 28; + ShareProject share_project = 23; + ShareProjectResponse share_project_response = 24; + UnshareProject unshare_project = 25; + JoinProject join_project = 26; + JoinProjectResponse join_project_response = 27; + LeaveProject leave_project = 28; + AddProjectCollaborator add_project_collaborator = 29; + UpdateProjectCollaborator update_project_collaborator = 30; + RemoveProjectCollaborator remove_project_collaborator = 31; - GetDefinition get_definition = 29; - GetDefinitionResponse get_definition_response = 30; - GetTypeDefinition get_type_definition = 31; - GetTypeDefinitionResponse get_type_definition_response = 32; - GetReferences get_references = 33; - GetReferencesResponse get_references_response = 34; - GetDocumentHighlights get_document_highlights = 35; - GetDocumentHighlightsResponse get_document_highlights_response = 36; - GetProjectSymbols get_project_symbols = 37; - GetProjectSymbolsResponse get_project_symbols_response = 38; - OpenBufferForSymbol open_buffer_for_symbol = 39; - OpenBufferForSymbolResponse open_buffer_for_symbol_response = 40; + GetDefinition get_definition = 32; + GetDefinitionResponse get_definition_response = 33; + GetTypeDefinition get_type_definition = 34; + GetTypeDefinitionResponse get_type_definition_response = 35; + GetReferences get_references = 36; + GetReferencesResponse get_references_response = 37; + GetDocumentHighlights get_document_highlights = 38; + GetDocumentHighlightsResponse get_document_highlights_response = 39; + GetProjectSymbols get_project_symbols = 40; + GetProjectSymbolsResponse get_project_symbols_response = 41; + OpenBufferForSymbol open_buffer_for_symbol = 42; + OpenBufferForSymbolResponse open_buffer_for_symbol_response = 43; - UpdateProject update_project = 41; - UpdateWorktree update_worktree = 43; + UpdateProject update_project = 44; + UpdateWorktree update_worktree = 45; - CreateProjectEntry create_project_entry = 45; - RenameProjectEntry rename_project_entry = 46; - CopyProjectEntry copy_project_entry = 47; - DeleteProjectEntry delete_project_entry = 48; - ProjectEntryResponse project_entry_response = 49; - ExpandProjectEntry expand_project_entry = 114; - ExpandProjectEntryResponse expand_project_entry_response = 115; + CreateProjectEntry create_project_entry = 46; + RenameProjectEntry rename_project_entry = 47; + CopyProjectEntry copy_project_entry = 48; + DeleteProjectEntry delete_project_entry = 49; + ProjectEntryResponse project_entry_response = 50; + ExpandProjectEntry expand_project_entry = 51; + ExpandProjectEntryResponse expand_project_entry_response = 52; - UpdateDiagnosticSummary update_diagnostic_summary = 50; - StartLanguageServer start_language_server = 51; - UpdateLanguageServer update_language_server = 52; + UpdateDiagnosticSummary update_diagnostic_summary = 53; + StartLanguageServer start_language_server = 54; + UpdateLanguageServer update_language_server = 55; - OpenBufferById open_buffer_by_id = 53; - OpenBufferByPath open_buffer_by_path = 54; - OpenBufferResponse open_buffer_response = 55; - CreateBufferForPeer create_buffer_for_peer = 56; - UpdateBuffer update_buffer = 57; - UpdateBufferFile update_buffer_file = 58; - SaveBuffer save_buffer = 59; - BufferSaved buffer_saved = 60; - BufferReloaded buffer_reloaded = 61; - ReloadBuffers reload_buffers = 62; - ReloadBuffersResponse reload_buffers_response = 63; - SynchronizeBuffers synchronize_buffers = 200; - SynchronizeBuffersResponse synchronize_buffers_response = 201; - FormatBuffers format_buffers = 64; - FormatBuffersResponse format_buffers_response = 65; - GetCompletions get_completions = 66; - GetCompletionsResponse get_completions_response = 67; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 68; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 69; - GetCodeActions get_code_actions = 70; - GetCodeActionsResponse get_code_actions_response = 71; - GetHover get_hover = 72; - GetHoverResponse get_hover_response = 73; - ApplyCodeAction apply_code_action = 74; - ApplyCodeActionResponse apply_code_action_response = 75; - PrepareRename prepare_rename = 76; - PrepareRenameResponse prepare_rename_response = 77; - PerformRename perform_rename = 78; - PerformRenameResponse perform_rename_response = 79; - SearchProject search_project = 80; - SearchProjectResponse search_project_response = 81; + OpenBufferById open_buffer_by_id = 56; + OpenBufferByPath open_buffer_by_path = 57; + OpenBufferResponse open_buffer_response = 58; + CreateBufferForPeer create_buffer_for_peer = 59; + UpdateBuffer update_buffer = 60; + UpdateBufferFile update_buffer_file = 61; + SaveBuffer save_buffer = 62; + BufferSaved buffer_saved = 63; + BufferReloaded buffer_reloaded = 64; + ReloadBuffers reload_buffers = 65; + ReloadBuffersResponse reload_buffers_response = 66; + SynchronizeBuffers synchronize_buffers = 67; + SynchronizeBuffersResponse synchronize_buffers_response = 68; + FormatBuffers format_buffers = 69; + FormatBuffersResponse format_buffers_response = 70; + GetCompletions get_completions = 71; + GetCompletionsResponse get_completions_response = 72; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74; + GetCodeActions get_code_actions = 75; + GetCodeActionsResponse get_code_actions_response = 76; + GetHover get_hover = 77; + GetHoverResponse get_hover_response = 78; + ApplyCodeAction apply_code_action = 79; + ApplyCodeActionResponse apply_code_action_response = 80; + PrepareRename prepare_rename = 81; + PrepareRenameResponse prepare_rename_response = 82; + PerformRename perform_rename = 83; + PerformRenameResponse perform_rename_response = 84; + SearchProject search_project = 85; + SearchProjectResponse search_project_response = 86; - UpdateContacts update_contacts = 92; - UpdateInviteInfo update_invite_info = 93; - ShowContacts show_contacts = 94; + UpdateContacts update_contacts = 87; + UpdateInviteInfo update_invite_info = 88; + ShowContacts show_contacts = 89; - GetUsers get_users = 95; - FuzzySearchUsers fuzzy_search_users = 96; - UsersResponse users_response = 97; - RequestContact request_contact = 98; - RespondToContactRequest respond_to_contact_request = 99; - RemoveContact remove_contact = 100; + GetUsers get_users = 90; + FuzzySearchUsers fuzzy_search_users = 91; + UsersResponse users_response = 92; + RequestContact request_contact = 93; + RespondToContactRequest respond_to_contact_request = 94; + RemoveContact remove_contact = 95; - Follow follow = 101; - FollowResponse follow_response = 102; - UpdateFollowers update_followers = 103; - Unfollow unfollow = 104; - GetPrivateUserInfo get_private_user_info = 105; - GetPrivateUserInfoResponse get_private_user_info_response = 106; - UpdateDiffBase update_diff_base = 107; + Follow follow = 96; + FollowResponse follow_response = 97; + UpdateFollowers update_followers = 98; + Unfollow unfollow = 99; + GetPrivateUserInfo get_private_user_info = 100; + GetPrivateUserInfoResponse get_private_user_info_response = 101; + UpdateDiffBase update_diff_base = 102; - OnTypeFormatting on_type_formatting = 111; - OnTypeFormattingResponse on_type_formatting_response = 112; + OnTypeFormatting on_type_formatting = 103; + OnTypeFormattingResponse on_type_formatting_response = 104; - UpdateWorktreeSettings update_worktree_settings = 113; + UpdateWorktreeSettings update_worktree_settings = 105; - InlayHints inlay_hints = 116; - InlayHintsResponse inlay_hints_response = 117; - ResolveInlayHint resolve_inlay_hint = 137; - ResolveInlayHintResponse resolve_inlay_hint_response = 138; - RefreshInlayHints refresh_inlay_hints = 118; + InlayHints inlay_hints = 106; + InlayHintsResponse inlay_hints_response = 107; + ResolveInlayHint resolve_inlay_hint = 108; + ResolveInlayHintResponse resolve_inlay_hint_response = 109; + RefreshInlayHints refresh_inlay_hints = 110; - CreateChannel create_channel = 119; - CreateChannelResponse create_channel_response = 120; - InviteChannelMember invite_channel_member = 121; - RemoveChannelMember remove_channel_member = 122; - RespondToChannelInvite respond_to_channel_invite = 123; - UpdateChannels update_channels = 124; - JoinChannel join_channel = 125; - DeleteChannel delete_channel = 126; - GetChannelMembers get_channel_members = 127; - GetChannelMembersResponse get_channel_members_response = 128; - SetChannelMemberAdmin set_channel_member_admin = 129; - RenameChannel rename_channel = 130; - RenameChannelResponse rename_channel_response = 154; + CreateChannel create_channel = 111; + CreateChannelResponse create_channel_response = 112; + InviteChannelMember invite_channel_member = 113; + RemoveChannelMember remove_channel_member = 114; + RespondToChannelInvite respond_to_channel_invite = 115; + UpdateChannels update_channels = 116; + JoinChannel join_channel = 117; + DeleteChannel delete_channel = 118; + GetChannelMembers get_channel_members = 119; + GetChannelMembersResponse get_channel_members_response = 120; + SetChannelMemberAdmin set_channel_member_admin = 121; + RenameChannel rename_channel = 122; + RenameChannelResponse rename_channel_response = 123; - JoinChannelBuffer join_channel_buffer = 131; - JoinChannelBufferResponse join_channel_buffer_response = 132; - UpdateChannelBuffer update_channel_buffer = 133; - LeaveChannelBuffer leave_channel_buffer = 134; - AddChannelBufferCollaborator add_channel_buffer_collaborator = 135; - RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136; - UpdateChannelBufferCollaborator update_channel_buffer_collaborator = 139; - RejoinChannelBuffers rejoin_channel_buffers = 140; - RejoinChannelBuffersResponse rejoin_channel_buffers_response = 141; + JoinChannelBuffer join_channel_buffer = 124; + JoinChannelBufferResponse join_channel_buffer_response = 125; + UpdateChannelBuffer update_channel_buffer = 126; + LeaveChannelBuffer leave_channel_buffer = 127; + UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128; + RejoinChannelBuffers rejoin_channel_buffers = 129; + RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130; - JoinChannelChat join_channel_chat = 142; - JoinChannelChatResponse join_channel_chat_response = 143; - LeaveChannelChat leave_channel_chat = 144; - SendChannelMessage send_channel_message = 145; - SendChannelMessageResponse send_channel_message_response = 146; - ChannelMessageSent channel_message_sent = 147; - GetChannelMessages get_channel_messages = 148; - GetChannelMessagesResponse get_channel_messages_response = 149; - RemoveChannelMessage remove_channel_message = 150; + JoinChannelChat join_channel_chat = 131; + JoinChannelChatResponse join_channel_chat_response = 132; + LeaveChannelChat leave_channel_chat = 133; + SendChannelMessage send_channel_message = 134; + SendChannelMessageResponse send_channel_message_response = 135; + ChannelMessageSent channel_message_sent = 136; + GetChannelMessages get_channel_messages = 137; + GetChannelMessagesResponse get_channel_messages_response = 138; + RemoveChannelMessage remove_channel_message = 139; - LinkChannel link_channel = 151; - UnlinkChannel unlink_channel = 152; - MoveChannel move_channel = 153; // Current max: 154 + LinkChannel link_channel = 140; + UnlinkChannel unlink_channel = 141; + MoveChannel move_channel = 142; } } @@ -258,6 +256,7 @@ message Participant { PeerId peer_id = 2; repeated ParticipantProject projects = 3; ParticipantLocation location = 4; + uint32 color_index = 5; } message PendingParticipant { @@ -440,20 +439,9 @@ message RemoveProjectCollaborator { PeerId peer_id = 2; } -message AddChannelBufferCollaborator { +message UpdateChannelBufferCollaborators { uint64 channel_id = 1; - Collaborator collaborator = 2; -} - -message RemoveChannelBufferCollaborator { - uint64 channel_id = 1; - PeerId peer_id = 2; -} - -message UpdateChannelBufferCollaborator { - uint64 channel_id = 1; - PeerId old_peer_id = 2; - PeerId new_peer_id = 3; + repeated Collaborator collaborators = 2; } message GetDefinition { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 48e9eef710..6d0a0f85d1 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -270,9 +270,7 @@ messages!( (JoinChannelBufferResponse, Foreground), (LeaveChannelBuffer, Background), (UpdateChannelBuffer, Foreground), - (RemoveChannelBufferCollaborator, Foreground), - (AddChannelBufferCollaborator, Foreground), - (UpdateChannelBufferCollaborator, Foreground), + (UpdateChannelBufferCollaborators, Foreground), ); request_messages!( @@ -407,10 +405,8 @@ entity_messages!( channel_id, ChannelMessageSent, UpdateChannelBuffer, - RemoveChannelBufferCollaborator, RemoveChannelMessage, - AddChannelBufferCollaborator, - UpdateChannelBufferCollaborator + UpdateChannelBufferCollaborators ); const KIB: usize = 1024; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 5ea5ce8778..a96a3d9c7c 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1064,14 +1064,16 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ColorIndex(pub u32); + impl Editor { - pub fn replica_selection_style(&self, replica_id: u16) -> &SelectionStyle { - let style_ix = replica_id as usize % (self.guest_selections.len() + 1); - if style_ix == 0 { - &self.selection - } else { - &self.guest_selections[style_ix - 1] + pub fn replica_selection_style(&self, color_index: ColorIndex) -> SelectionStyle { + if self.guest_selections.is_empty() { + return SelectionStyle::default(); } + let style_ix = color_index.0 as usize % self.guest_selections.len(); + self.guest_selections[style_ix] } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e2fa6e989a..c223726422 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -168,7 +168,7 @@ impl Vim { self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event { Event::SelectionsChanged { local: true } => { let editor = editor.read(cx); - if editor.leader_replica_id().is_none() { + if editor.leader_peer_id().is_none() { let newest = editor.selections.newest::(cx); local_selections_changed(newest, cx); } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index de19e82c8b..a489dfd9a4 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -4,7 +4,10 @@ use crate::{ }; use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings}; use anyhow::Result; -use client::{proto, Client}; +use client::{ + proto::{self, PeerId}, + Client, +}; use gpui::geometry::vector::Vector2F; use gpui::AnyWindowHandle; use gpui::{ @@ -698,13 +701,13 @@ pub trait FollowableItem: Item { ) -> Task>; fn is_project_item(&self, cx: &AppContext) -> bool; - fn set_leader_replica_id(&mut self, leader_replica_id: Option, cx: &mut ViewContext); + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext); fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; } pub trait FollowableItemHandle: ItemHandle { fn remote_id(&self, client: &Arc, cx: &AppContext) -> Option; - fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut WindowContext); + fn set_leader_peer_id(&self, leader_peer_id: Option, cx: &mut WindowContext); fn to_state_proto(&self, cx: &AppContext) -> Option; fn add_event_to_update_proto( &self, @@ -732,10 +735,8 @@ impl FollowableItemHandle for ViewHandle { }) } - fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut WindowContext) { - self.update(cx, |this, cx| { - this.set_leader_replica_id(leader_replica_id, cx) - }) + fn set_leader_peer_id(&self, leader_peer_id: Option, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx)) } fn to_state_proto(&self, cx: &AppContext) -> Option { diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 29068ce923..425fd00b5a 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -183,25 +183,23 @@ impl Member { }) .and_then(|leader_id| { let room = active_call?.read(cx).room()?.read(cx); - let collaborator = project.read(cx).collaborators().get(leader_id)?; - let participant = room.remote_participant_for_peer_id(*leader_id)?; - Some((collaborator.replica_id, participant)) + room.remote_participant_for_peer_id(*leader_id) }); - let border = if let Some((replica_id, _)) = leader.as_ref() { - let leader_color = theme.editor.replica_selection_style(*replica_id).cursor; - let mut border = Border::all(theme.workspace.leader_border_width, leader_color); - border + let mut leader_border = Border::default(); + let mut leader_status_box = None; + if let Some(leader) = &leader { + let leader_color = theme + .editor + .replica_selection_style(leader.color_index) + .cursor; + leader_border = Border::all(theme.workspace.leader_border_width, leader_color); + leader_border .color .fade_out(1. - theme.workspace.leader_border_opacity); - border.overlay = true; - border - } else { - Border::default() - }; + leader_border.overlay = true; - let leader_status_box = if let Some((_, leader)) = leader { - match leader.location { + leader_status_box = match leader.location { ParticipantLocation::SharedProject { project_id: leader_project_id, } => { @@ -279,13 +277,11 @@ impl Member { .right() .into_any(), ), - } - } else { - None - }; + }; + } Stack::new() - .with_child(pane_element.contained().with_border(border)) + .with_child(pane_element.contained().with_border(leader_border)) .with_children(leader_status_box) .into_any() } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f874f5ee16..9d0d276e49 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2423,7 +2423,7 @@ impl Workspace { if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) { for state in states_by_pane.into_values() { for item in state.items_by_leader_view_id.into_values() { - item.set_leader_replica_id(None, cx); + item.set_leader_peer_id(None, cx); } } } @@ -2527,7 +2527,7 @@ impl Workspace { let leader_id = *leader_id; if let Some(state) = states_by_pane.remove(pane) { for (_, item) in state.items_by_leader_view_id { - item.set_leader_replica_id(None, cx); + item.set_leader_peer_id(None, cx); } if states_by_pane.is_empty() { @@ -2828,16 +2828,6 @@ impl Workspace { let this = this .upgrade(cx) .ok_or_else(|| anyhow!("workspace dropped"))?; - let project = this - .read_with(cx, |this, _| this.project.clone()) - .ok_or_else(|| anyhow!("window dropped"))?; - - let replica_id = project.read_with(cx, |project, _| { - project - .collaborators() - .get(&leader_id) - .map(|c| c.replica_id) - }); let item_builders = cx.update(|cx| { cx.default_global::() @@ -2882,7 +2872,7 @@ impl Workspace { .get_mut(&pane)?; for (id, item) in leader_view_ids.into_iter().zip(items) { - item.set_leader_replica_id(replica_id, cx); + item.set_leader_peer_id(Some(leader_id), cx); state.items_by_leader_view_id.insert(id, item); } diff --git a/selection-color-notes.txt b/selection-color-notes.txt new file mode 100644 index 0000000000..6186adcac1 --- /dev/null +++ b/selection-color-notes.txt @@ -0,0 +1,14 @@ +Assign selection colors to users. goals: + * current user is always main color + * every other user has the same color in every context + * users don't need to be in a shared project to have a color. they can either be in the call, or in a channel notes. + +Places colors are used: + * editor element, driven by the buffer's `remote_selections` + * pane border (access to more state) + * collab titlebar (access to more state) + +Currently, editor holds an optional "replica id map". + +Most challenging part is in the editor, because the editor should be fairly self-contained, not depend on e.g. the user store. + From 0f39b638019d0826e8f7502a31eafcbf0e3955ac Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2023 11:18:29 -0700 Subject: [PATCH 08/45] Rename color_index to participant_index Co-authored-by: Conrad --- crates/call/src/participant.rs | 4 +-- crates/call/src/room.rs | 13 +++++----- crates/client/src/user.rs | 24 +++++++++-------- .../20221109000000_test_schema.sql | 2 +- ...0_add_color_index_to_room_participants.sql | 1 - ...participant_index_to_room_participants.sql | 1 + crates/collab/src/db/queries/rooms.rs | 24 ++++++++--------- .../collab/src/db/tables/room_participant.rs | 2 +- .../collab/src/tests/channel_buffer_tests.rs | 24 +++++++++++------ crates/collab_ui/src/channel_view.rs | 9 ++++--- crates/collab_ui/src/collab_titlebar_item.rs | 26 ++++++++++++------- crates/editor/src/editor.rs | 24 ++++++++++------- crates/editor/src/element.rs | 4 +-- crates/project/src/project.rs | 8 ------ crates/rpc/proto/zed.proto | 2 +- crates/theme/src/theme.rs | 7 ++--- crates/workspace/src/pane_group.rs | 2 +- 17 files changed, 97 insertions(+), 80 deletions(-) delete mode 100644 crates/collab/migrations/20230926102500_add_color_index_to_room_participants.sql create mode 100644 crates/collab/migrations/20230926102500_add_participant_index_to_room_participants.sql diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index b0751be919..ab796e56b0 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, Result}; +use client::ParticipantIndex; use client::{proto, User}; use collections::HashMap; use gpui::WeakModelHandle; @@ -6,7 +7,6 @@ pub use live_kit_client::Frame; use live_kit_client::RemoteAudioTrack; use project::Project; use std::{fmt, sync::Arc}; -use theme::ColorIndex; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ParticipantLocation { @@ -44,7 +44,7 @@ pub struct RemoteParticipant { pub peer_id: proto::PeerId, pub projects: Vec, pub location: ParticipantLocation, - pub color_index: ColorIndex, + pub participant_index: ParticipantIndex, pub muted: bool, pub speaking: bool, pub video_tracks: HashMap>, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index e6759d87ca..bf30e31a98 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -7,7 +7,7 @@ use anyhow::{anyhow, Result}; use audio::{Audio, Sound}; use client::{ proto::{self, PeerId}, - Client, TypedEnvelope, User, UserStore, + Client, ParticipantIndex, TypedEnvelope, User, UserStore, }; use collections::{BTreeMap, HashMap, HashSet}; use fs::Fs; @@ -21,7 +21,6 @@ use live_kit_client::{ use postage::stream::Stream; use project::Project; use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration}; -use theme::ColorIndex; use util::{post_inc, ResultExt, TryFutureExt}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -715,7 +714,9 @@ impl Room { participant.user_id, RemoteParticipant { user: user.clone(), - color_index: ColorIndex(participant.color_index), + participant_index: ParticipantIndex( + participant.participant_index, + ), peer_id, projects: participant.projects, location, @@ -810,12 +811,12 @@ impl Room { } this.user_store.update(cx, |user_store, cx| { - let color_indices_by_user_id = this + let participant_indices_by_user_id = this .remote_participants .iter() - .map(|(user_id, participant)| (*user_id, participant.color_index)) + .map(|(user_id, participant)| (*user_id, participant.participant_index)) .collect(); - user_store.set_color_indices(color_indices_by_user_id, cx); + user_store.set_participant_indices(participant_indices_by_user_id, cx); }); this.check_invariants(); diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 0522545587..b8cc8fb1b8 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -8,12 +8,14 @@ use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; use text::ReplicaId; -use theme::ColorIndex; use util::http::HttpClient; use util::TryFutureExt as _; pub type UserId = u64; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ParticipantIndex(pub u32); + #[derive(Default, Debug)] pub struct User { pub id: UserId, @@ -65,7 +67,7 @@ pub enum ContactRequestStatus { pub struct UserStore { users: HashMap>, - color_indices: HashMap, + participant_indices: HashMap, update_contacts_tx: mpsc::UnboundedSender, current_user: watch::Receiver>>, contacts: Vec>, @@ -91,7 +93,7 @@ pub enum Event { kind: ContactEventKind, }, ShowContacts, - ColorIndicesChanged, + ParticipantIndicesChanged, } #[derive(Clone, Copy)] @@ -129,7 +131,7 @@ impl UserStore { current_user: current_user_rx, contacts: Default::default(), incoming_contact_requests: Default::default(), - color_indices: Default::default(), + participant_indices: Default::default(), outgoing_contact_requests: Default::default(), invite_info: None, client: Arc::downgrade(&client), @@ -654,19 +656,19 @@ impl UserStore { }) } - pub fn set_color_indices( + pub fn set_participant_indices( &mut self, - color_indices: HashMap, + participant_indices: HashMap, cx: &mut ModelContext, ) { - if color_indices != self.color_indices { - self.color_indices = color_indices; - cx.emit(Event::ColorIndicesChanged); + if participant_indices != self.participant_indices { + self.participant_indices = participant_indices; + cx.emit(Event::ParticipantIndicesChanged); } } - pub fn color_indices(&self) -> &HashMap { - &self.color_indices + pub fn participant_indices(&self) -> &HashMap { + &self.participant_indices } } diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 5f5484679f..8a153949c2 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -159,7 +159,7 @@ CREATE TABLE "room_participants" ( "calling_user_id" INTEGER NOT NULL REFERENCES users (id), "calling_connection_id" INTEGER NOT NULL, "calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL, - "color_index" INTEGER + "participant_index" INTEGER ); CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id"); CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id"); diff --git a/crates/collab/migrations/20230926102500_add_color_index_to_room_participants.sql b/crates/collab/migrations/20230926102500_add_color_index_to_room_participants.sql deleted file mode 100644 index 626268bd5c..0000000000 --- a/crates/collab/migrations/20230926102500_add_color_index_to_room_participants.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE room_participants ADD COLUMN color_index INTEGER; diff --git a/crates/collab/migrations/20230926102500_add_participant_index_to_room_participants.sql b/crates/collab/migrations/20230926102500_add_participant_index_to_room_participants.sql new file mode 100644 index 0000000000..1493119e2a --- /dev/null +++ b/crates/collab/migrations/20230926102500_add_participant_index_to_room_participants.sql @@ -0,0 +1 @@ +ALTER TABLE room_participants ADD COLUMN participant_index INTEGER; diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index fca4c67690..70e39c91b3 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -152,7 +152,7 @@ impl Database { room_id: ActiveValue::set(room_id), user_id: ActiveValue::set(called_user_id), answering_connection_lost: ActiveValue::set(false), - color_index: ActiveValue::NotSet, + participant_index: ActiveValue::NotSet, calling_user_id: ActiveValue::set(calling_user_id), calling_connection_id: ActiveValue::set(calling_connection.id as i32), calling_connection_server_id: ActiveValue::set(Some(ServerId( @@ -285,19 +285,19 @@ impl Database { .ok_or_else(|| anyhow!("no such room"))?; #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryColorIndices { - ColorIndex, + enum QueryParticipantIndices { + ParticipantIndex, } - let existing_color_indices: Vec = room_participant::Entity::find() + let existing_participant_indices: Vec = room_participant::Entity::find() .filter(room_participant::Column::RoomId.eq(room_id)) .select_only() - .column(room_participant::Column::ColorIndex) - .into_values::<_, QueryColorIndices>() + .column(room_participant::Column::ParticipantIndex) + .into_values::<_, QueryParticipantIndices>() .all(&*tx) .await?; - let mut color_index = 0; - while existing_color_indices.contains(&color_index) { - color_index += 1; + let mut participant_index = 0; + while existing_participant_indices.contains(&participant_index) { + participant_index += 1; } if let Some(channel_id) = channel_id { @@ -317,7 +317,7 @@ impl Database { calling_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, ))), - color_index: ActiveValue::Set(color_index), + participant_index: ActiveValue::Set(participant_index), ..Default::default() }]) .on_conflict( @@ -340,7 +340,7 @@ impl Database { .add(room_participant::Column::AnsweringConnectionId.is_null()), ) .set(room_participant::ActiveModel { - color_index: ActiveValue::Set(color_index), + participant_index: ActiveValue::Set(participant_index), answering_connection_id: ActiveValue::set(Some(connection.id as i32)), answering_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, @@ -1090,7 +1090,7 @@ impl Database { peer_id: Some(answering_connection.into()), projects: Default::default(), location: Some(proto::ParticipantLocation { variant: location }), - color_index: db_participant.color_index as u32, + participant_index: db_participant.participant_index as u32, }, ); } else { diff --git a/crates/collab/src/db/tables/room_participant.rs b/crates/collab/src/db/tables/room_participant.rs index 8072fed69c..d93667b3fc 100644 --- a/crates/collab/src/db/tables/room_participant.rs +++ b/crates/collab/src/db/tables/room_participant.rs @@ -18,7 +18,7 @@ pub struct Model { pub calling_user_id: UserId, pub calling_connection_id: i32, pub calling_connection_server_id: Option, - pub color_index: i32, + pub participant_index: i32, } impl Model { diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index e403ed6f94..7033c71e8c 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -4,6 +4,7 @@ use crate::{ }; use call::ActiveCall; use channel::Channel; +use client::ParticipantIndex; use client::{Collaborator, UserId}; use collab_ui::channel_view::ChannelView; use collections::HashMap; @@ -13,7 +14,6 @@ use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext}; use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; use serde_json::json; use std::{ops::Range, sync::Arc}; -use theme::ColorIndex; #[gpui::test] async fn test_core_channel_buffers( @@ -122,7 +122,7 @@ async fn test_core_channel_buffers( } #[gpui::test] -async fn test_channel_notes_color_indices( +async fn test_channel_notes_participant_indices( deterministic: Arc, mut cx_a: &mut TestAppContext, mut cx_b: &mut TestAppContext, @@ -226,12 +226,20 @@ async fn test_channel_notes_color_indices( deterministic.run_until_parked(); channel_view_a.update(cx_a, |notes, cx| { notes.editor.update(cx, |editor, cx| { - assert_remote_selections(editor, &[(Some(ColorIndex(1)), 1..2), (None, 2..3)], cx); + assert_remote_selections( + editor, + &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)], + cx, + ); }); }); channel_view_b.update(cx_b, |notes, cx| { notes.editor.update(cx, |editor, cx| { - assert_remote_selections(editor, &[(Some(ColorIndex(0)), 0..1), (None, 2..3)], cx); + assert_remote_selections( + editor, + &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)], + cx, + ); }); }); @@ -275,17 +283,17 @@ async fn test_channel_notes_color_indices( // Clients A and B see each other with the same colors as in the channel notes. editor_a.update(cx_a, |editor, cx| { - assert_remote_selections(editor, &[(Some(ColorIndex(1)), 2..3)], cx); + assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx); }); editor_b.update(cx_b, |editor, cx| { - assert_remote_selections(editor, &[(Some(ColorIndex(0)), 0..1)], cx); + assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx); }); } #[track_caller] fn assert_remote_selections( editor: &mut Editor, - expected_selections: &[(Option, Range)], + expected_selections: &[(Option, Range)], cx: &mut ViewContext, ) { let snapshot = editor.snapshot(cx); @@ -295,7 +303,7 @@ fn assert_remote_selections( .map(|s| { let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); - (s.color_index, start..end) + (s.participant_index, start..end) }) .collect::>(); assert_eq!( diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 1d103350de..22e7d6637c 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -3,7 +3,7 @@ use call::ActiveCall; use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId}; use client::{ proto::{self, PeerId}, - Collaborator, + Collaborator, ParticipantIndex, }; use collections::HashMap; use editor::{CollaborationHub, Editor}; @@ -359,7 +359,10 @@ impl CollaborationHub for ChannelBufferCollaborationHub { self.0.read(cx).collaborators() } - fn user_color_indices<'a>(&self, cx: &'a AppContext) -> &'a HashMap { - self.0.read(cx).user_store().read(cx).color_indices() + fn user_participant_indices<'a>( + &self, + cx: &'a AppContext, + ) -> &'a HashMap { + self.0.read(cx).user_store().read(cx).participant_indices() } } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 5dafae0cd5..c1b3689aad 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -923,15 +923,18 @@ impl CollabTitlebarItem { .background_color .unwrap_or_default(); - let color_index = self + let participant_index = self .user_store .read(cx) - .color_indices() + .participant_indices() .get(&user_id) .copied(); - if let Some(color_index) = color_index { + if let Some(participant_index) = participant_index { if followed_by_self { - let selection = theme.editor.replica_selection_style(color_index).selection; + let selection = theme + .editor + .selection_style_for_room_participant(participant_index.0) + .selection; background_color = Color::blend(selection, background_color); background_color.a = 255; } @@ -996,10 +999,12 @@ impl CollabTitlebarItem { .contained() .with_style(theme.titlebar.leader_selection); - if let Some(color_index) = color_index { + if let Some(participant_index) = participant_index { if followed_by_self { - let color = - theme.editor.replica_selection_style(color_index).selection; + let color = theme + .editor + .selection_style_for_room_participant(participant_index.0) + .selection; container = container.with_background_color(color); } } @@ -1007,8 +1012,11 @@ impl CollabTitlebarItem { container })) .with_children((|| { - let color_index = color_index?; - let color = theme.editor.replica_selection_style(color_index).cursor; + let participant_index = participant_index?; + let color = theme + .editor + .selection_style_for_room_participant(participant_index.0) + .cursor; Some( AvatarRibbon::new(color) .constrained() diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7692a54b01..2e15dd3a92 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -25,7 +25,7 @@ use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context, Result}; use blink_manager::BlinkManager; -use client::{ClickhouseEvent, Collaborator, TelemetrySettings}; +use client::{ClickhouseEvent, Collaborator, ParticipantIndex, TelemetrySettings}; use clock::{Global, ReplicaId}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; @@ -102,7 +102,7 @@ use std::{ pub use sum_tree::Bias; use sum_tree::TreeMap; use text::Rope; -use theme::{ColorIndex, DiagnosticStyle, Theme, ThemeSettings}; +use theme::{DiagnosticStyle, Theme, ThemeSettings}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ItemNavHistory, ViewId, Workspace}; @@ -637,7 +637,7 @@ pub struct RemoteSelection { pub cursor_shape: CursorShape, pub peer_id: PeerId, pub line_mode: bool, - pub color_index: Option, + pub participant_index: Option, } #[derive(Clone, Debug)] @@ -8570,7 +8570,10 @@ impl Editor { pub trait CollaborationHub { fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap; - fn user_color_indices<'a>(&self, cx: &'a AppContext) -> &'a HashMap; + fn user_participant_indices<'a>( + &self, + cx: &'a AppContext, + ) -> &'a HashMap; } impl CollaborationHub for ModelHandle { @@ -8578,8 +8581,11 @@ impl CollaborationHub for ModelHandle { self.read(cx).collaborators() } - fn user_color_indices<'a>(&self, cx: &'a AppContext) -> &'a HashMap { - self.read(cx).user_store().read(cx).color_indices() + fn user_participant_indices<'a>( + &self, + cx: &'a AppContext, + ) -> &'a HashMap { + self.read(cx).user_store().read(cx).participant_indices() } } @@ -8632,7 +8638,7 @@ impl EditorSnapshot { collaboration_hub: &dyn CollaborationHub, cx: &'a AppContext, ) -> impl 'a + Iterator { - let color_indices = collaboration_hub.user_color_indices(cx); + let participant_indices = collaboration_hub.user_participant_indices(cx); let collaborators_by_peer_id = collaboration_hub.collaborators(cx); let collaborators_by_replica_id = collaborators_by_peer_id .iter() @@ -8642,13 +8648,13 @@ impl EditorSnapshot { .remote_selections_in_range(range) .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| { let collaborator = collaborators_by_replica_id.get(&replica_id)?; - let color_index = color_indices.get(&collaborator.user_id).copied(); + let participant_index = participant_indices.get(&collaborator.user_id).copied(); Some(RemoteSelection { replica_id, selection, cursor_shape, line_mode, - color_index, + participant_index, peer_id: collaborator.peer_id, }) }) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index dad5b06626..0be395729f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2256,8 +2256,8 @@ impl Element for EditorElement { collaboration_hub.as_ref(), cx, ) { - let selection_style = if let Some(color_index) = selection.color_index { - style.replica_selection_style(color_index) + let selection_style = if let Some(participant_index) = selection.participant_index { + style.selection_style_for_room_participant(participant_index.0) } else { style.absent_selection }; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 62df8425c4..86303555dd 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -76,7 +76,6 @@ use std::{ }; use terminals::Terminals; use text::Anchor; -use theme::ColorIndex; use util::{ debug_panic, defer, http::HttpClient, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, @@ -120,7 +119,6 @@ pub struct Project { join_project_response_message_id: u32, next_diagnostic_group_id: usize, user_store: ModelHandle, - user_color_indices: HashMap, fs: Arc, client_state: Option, collaborators: HashMap, @@ -644,7 +642,6 @@ impl Project { languages, client, user_store, - user_color_indices: Default::default(), fs, next_entry_id: Default::default(), next_diagnostic_group_id: Default::default(), @@ -717,7 +714,6 @@ impl Project { _maintain_workspace_config: Self::maintain_workspace_config(cx), languages, user_store: user_store.clone(), - user_color_indices: Default::default(), fs, next_entry_id: Default::default(), next_diagnostic_group_id: Default::default(), @@ -922,10 +918,6 @@ impl Project { self.user_store.clone() } - pub fn user_color_indices(&self) -> &HashMap { - &self.user_color_indices - } - pub fn opened_buffers(&self, cx: &AppContext) -> Vec> { self.opened_buffers .values() diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index da97cd35c7..523ef1d763 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -256,7 +256,7 @@ message Participant { PeerId peer_id = 2; repeated ParticipantProject projects = 3; ParticipantLocation location = 4; - uint32 color_index = 5; + uint32 participant_index = 5; } message PendingParticipant { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a96a3d9c7c..1ca2d839c0 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1064,15 +1064,12 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ColorIndex(pub u32); - impl Editor { - pub fn replica_selection_style(&self, color_index: ColorIndex) -> SelectionStyle { + pub fn selection_style_for_room_participant(&self, participant_index: u32) -> SelectionStyle { if self.guest_selections.is_empty() { return SelectionStyle::default(); } - let style_ix = color_index.0 as usize % self.guest_selections.len(); + let style_ix = participant_index as usize % self.guest_selections.len(); self.guest_selections[style_ix] } } diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 425fd00b5a..7c7a7db7ea 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -191,7 +191,7 @@ impl Member { if let Some(leader) = &leader { let leader_color = theme .editor - .replica_selection_style(leader.color_index) + .selection_style_for_room_participant(leader.participant_index.0) .cursor; leader_border = Border::all(theme.workspace.leader_border_width, leader_color); leader_border From 0c95e5a6ca01e7c1d4d2e7d772dd8b4aa5e40fa3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2023 11:37:44 -0700 Subject: [PATCH 09/45] Fix coloring of local selections when following Co-authored-by: Conrad --- crates/editor/src/element.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 0be395729f..924d66c21c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2250,6 +2250,21 @@ impl Element for EditorElement { } if let Some(collaboration_hub) = &editor.collaboration_hub { + // When following someone, render the local selections in their color. + if let Some(leader_id) = editor.leader_peer_id { + if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) { + if let Some(participant_index) = collaboration_hub + .user_participant_indices(cx) + .get(&collaborator.user_id) + { + if let Some((local_selection_style, _)) = selections.first_mut() { + *local_selection_style = + style.selection_style_for_room_participant(participant_index.0); + } + } + } + } + let mut remote_selections = HashMap::default(); for selection in snapshot.remote_selections_in_range( &(start_anchor..end_anchor), @@ -2262,11 +2277,9 @@ impl Element for EditorElement { style.absent_selection }; - // The local selections match the leader's selections. + // Don't re-render the leader's selections, since the local selections + // match theirs. if Some(selection.peer_id) == editor.leader_peer_id { - if let Some((local_selection_style, _)) = selections.first_mut() { - *local_selection_style = selection_style; - } continue; } From ce940da8e92361692cf42bcad307e97edf487994 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2023 12:03:53 -0700 Subject: [PATCH 10/45] Fix errors from assuming all room_participant rows had a non-null participant_index Rows representing pending participants have a null participant_index. Co-authored-by: Conrad --- crates/collab/src/db/queries/rooms.rs | 27 +++++++++++++------ .../collab/src/db/tables/room_participant.rs | 2 +- .../collab/src/tests/channel_buffer_tests.rs | 12 +++++---- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 70e39c91b3..de1e8a1ce7 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -128,6 +128,7 @@ impl Database { calling_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, ))), + participant_index: ActiveValue::set(Some(0)), ..Default::default() } .insert(&*tx) @@ -289,7 +290,11 @@ impl Database { ParticipantIndex, } let existing_participant_indices: Vec = room_participant::Entity::find() - .filter(room_participant::Column::RoomId.eq(room_id)) + .filter( + room_participant::Column::RoomId + .eq(room_id) + .and(room_participant::Column::ParticipantIndex.is_not_null()), + ) .select_only() .column(room_participant::Column::ParticipantIndex) .into_values::<_, QueryParticipantIndices>() @@ -317,7 +322,7 @@ impl Database { calling_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, ))), - participant_index: ActiveValue::Set(participant_index), + participant_index: ActiveValue::Set(Some(participant_index)), ..Default::default() }]) .on_conflict( @@ -326,6 +331,7 @@ impl Database { room_participant::Column::AnsweringConnectionId, room_participant::Column::AnsweringConnectionServerId, room_participant::Column::AnsweringConnectionLost, + room_participant::Column::ParticipantIndex, ]) .to_owned(), ) @@ -340,7 +346,7 @@ impl Database { .add(room_participant::Column::AnsweringConnectionId.is_null()), ) .set(room_participant::ActiveModel { - participant_index: ActiveValue::Set(participant_index), + participant_index: ActiveValue::Set(Some(participant_index)), answering_connection_id: ActiveValue::set(Some(connection.id as i32)), answering_connection_server_id: ActiveValue::set(Some(ServerId( connection.owner_id as i32, @@ -1056,10 +1062,15 @@ impl Database { let mut pending_participants = Vec::new(); while let Some(db_participant) = db_participants.next().await { let db_participant = db_participant?; - if let Some((answering_connection_id, answering_connection_server_id)) = db_participant - .answering_connection_id - .zip(db_participant.answering_connection_server_id) - { + if let ( + Some(answering_connection_id), + Some(answering_connection_server_id), + Some(participant_index), + ) = ( + db_participant.answering_connection_id, + db_participant.answering_connection_server_id, + db_participant.participant_index, + ) { let location = match ( db_participant.location_kind, db_participant.location_project_id, @@ -1090,7 +1101,7 @@ impl Database { peer_id: Some(answering_connection.into()), projects: Default::default(), location: Some(proto::ParticipantLocation { variant: location }), - participant_index: db_participant.participant_index as u32, + participant_index: participant_index as u32, }, ); } else { diff --git a/crates/collab/src/db/tables/room_participant.rs b/crates/collab/src/db/tables/room_participant.rs index d93667b3fc..4c5b8cc11c 100644 --- a/crates/collab/src/db/tables/room_participant.rs +++ b/crates/collab/src/db/tables/room_participant.rs @@ -18,7 +18,7 @@ pub struct Model { pub calling_user_id: UserId, pub calling_connection_id: i32, pub calling_connection_server_id: Option, - pub participant_index: i32, + pub participant_index: Option, } impl Model { diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 7033c71e8c..05abda5af3 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -102,7 +102,7 @@ async fn test_core_channel_buffers( channel_buffer_b.read_with(cx_b, |buffer, _| { assert_collaborators( &buffer.collaborators(), - &[client_b.user_id(), client_a.user_id()], + &[client_a.user_id(), client_b.user_id()], ); }); @@ -759,11 +759,13 @@ async fn test_following_to_channel_notes_without_a_shared_project( #[track_caller] fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { + let mut user_ids = collaborators + .values() + .map(|collaborator| collaborator.user_id) + .collect::>(); + user_ids.sort(); assert_eq!( - collaborators - .values() - .map(|collaborator| collaborator.user_id) - .collect::>(), + user_ids, ids.into_iter().map(|id| id.unwrap()).collect::>() ); } From e9c1ad6acd9b1ae6750ef958be237e6d05b1f6d7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2023 14:21:44 -0700 Subject: [PATCH 11/45] Undo making project optional on stored follower states Following works without a project, but following in unshared projects does not need to be replicated to other participants. --- crates/call/src/call.rs | 2 +- crates/call/src/room.rs | 4 +- .../20221109000000_test_schema.sql | 2 +- ...142700_allow_following_without_project.sql | 1 - crates/collab/src/db/queries/projects.rs | 93 +++++-------------- crates/collab/src/db/queries/rooms.rs | 2 +- crates/collab/src/db/tables/follower.rs | 2 +- crates/collab/src/rpc.rs | 32 ++++--- crates/rpc/proto/zed.proto | 2 +- 9 files changed, 47 insertions(+), 93 deletions(-) delete mode 100644 crates/collab/migrations/20230918142700_allow_following_without_project.sql diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 8c570c7165..6756c2aa53 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -58,7 +58,7 @@ pub struct ActiveCall { _subscriptions: Vec, } -#[derive(PartialEq, Eq, PartialOrd, Ord)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] struct Follower { project_id: Option, peer_id: PeerId, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index bf30e31a98..26a531cc31 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -62,7 +62,7 @@ pub struct Room { leave_when_empty: bool, client: Arc, user_store: ModelHandle, - follows_by_leader_id_project_id: HashMap<(PeerId, Option), Vec>, + follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec>, subscriptions: Vec, pending_room_update: Option>, maintain_connection: Option>>, @@ -584,7 +584,7 @@ impl Room { pub fn followers_for(&self, leader_id: PeerId, project_id: u64) -> &[PeerId] { self.follows_by_leader_id_project_id - .get(&(leader_id, Some(project_id))) + .get(&(leader_id, project_id)) .map_or(&[], |v| v.as_slice()) } diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 8a153949c2..d8325755f8 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -176,7 +176,7 @@ CREATE TABLE "servers" ( CREATE TABLE "followers" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE, - "project_id" INTEGER REFERENCES projects (id) ON DELETE CASCADE, + "project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE, "leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, "leader_connection_id" INTEGER NOT NULL, "follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, diff --git a/crates/collab/migrations/20230918142700_allow_following_without_project.sql b/crates/collab/migrations/20230918142700_allow_following_without_project.sql deleted file mode 100644 index e0cc0141ec..0000000000 --- a/crates/collab/migrations/20230918142700_allow_following_without_project.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE followers ALTER COLUMN project_id DROP NOT NULL; diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 80e71eb1eb..3e2c003378 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -862,83 +862,34 @@ impl Database { .await } - pub async fn check_can_follow( + pub async fn check_room_participants( &self, room_id: RoomId, - project_id: Option, - leader_id: ConnectionId, - follower_id: ConnectionId, - ) -> Result<()> { - let mut found_leader = false; - let mut found_follower = false; - self.transaction(|tx| async move { - if let Some(project_id) = project_id { - let mut rows = project_collaborator::Entity::find() - .filter(project_collaborator::Column::ProjectId.eq(project_id)) - .stream(&*tx) - .await?; - while let Some(row) = rows.next().await { - let row = row?; - let connection = row.connection(); - if connection == leader_id { - found_leader = true; - } else if connection == follower_id { - found_follower = true; - } - } - } else { - let mut rows = room_participant::Entity::find() - .filter(room_participant::Column::RoomId.eq(room_id)) - .stream(&*tx) - .await?; - while let Some(row) = rows.next().await { - let row = row?; - if let Some(connection) = row.answering_connection() { - if connection == leader_id { - found_leader = true; - } else if connection == follower_id { - found_follower = true; - } - } - } - } - - if !found_leader || !found_follower { - Err(anyhow!("not a room participant"))?; - } - - Ok(()) - }) - .await - } - - pub async fn check_can_unfollow( - &self, - room_id: RoomId, - project_id: Option, leader_id: ConnectionId, follower_id: ConnectionId, ) -> Result<()> { self.transaction(|tx| async move { - follower::Entity::find() + use room_participant::Column; + + let count = room_participant::Entity::find() .filter( - Condition::all() - .add(follower::Column::RoomId.eq(room_id)) - .add(follower::Column::ProjectId.eq(project_id)) - .add(follower::Column::LeaderConnectionId.eq(leader_id.id as i32)) - .add(follower::Column::FollowerConnectionId.eq(follower_id.id as i32)) - .add( - follower::Column::LeaderConnectionServerId - .eq(leader_id.owner_id as i32), - ) - .add( - follower::Column::FollowerConnectionServerId - .eq(follower_id.owner_id as i32), - ), + Condition::all().add(Column::RoomId.eq(room_id)).add( + Condition::any() + .add(Column::AnsweringConnectionId.eq(leader_id.id as i32).and( + Column::AnsweringConnectionServerId.eq(leader_id.owner_id as i32), + )) + .add(Column::AnsweringConnectionId.eq(follower_id.id as i32).and( + Column::AnsweringConnectionServerId.eq(follower_id.owner_id as i32), + )), + ), ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("not a follower"))?; + .count(&*tx) + .await?; + + if count < 2 { + Err(anyhow!("not room participants"))?; + } + Ok(()) }) .await @@ -947,7 +898,7 @@ impl Database { pub async fn follow( &self, room_id: RoomId, - project_id: Option, + project_id: ProjectId, leader_connection: ConnectionId, follower_connection: ConnectionId, ) -> Result> { @@ -977,7 +928,7 @@ impl Database { pub async fn unfollow( &self, room_id: RoomId, - project_id: Option, + project_id: ProjectId, leader_connection: ConnectionId, follower_connection: ConnectionId, ) -> Result> { diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index de1e8a1ce7..41f9755872 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -1154,7 +1154,7 @@ impl Database { followers.push(proto::Follower { leader_id: Some(db_follower.leader_connection().into()), follower_id: Some(db_follower.follower_connection().into()), - project_id: db_follower.project_id.map(|id| id.to_proto()), + project_id: db_follower.project_id.to_proto(), }); } diff --git a/crates/collab/src/db/tables/follower.rs b/crates/collab/src/db/tables/follower.rs index b5bc163b21..ffd45434e9 100644 --- a/crates/collab/src/db/tables/follower.rs +++ b/crates/collab/src/db/tables/follower.rs @@ -8,7 +8,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: FollowerId, pub room_id: RoomId, - pub project_id: Option, + pub project_id: ProjectId, pub leader_connection_server_id: ServerId, pub leader_connection_id: i32, pub follower_connection_server_id: ServerId, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 56cecb2e74..9e14c48473 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1901,7 +1901,7 @@ async fn follow( session .db() .await - .check_can_follow(room_id, project_id, leader_id, session.connection_id) + .check_room_participants(room_id, leader_id, session.connection_id) .await?; let mut response_payload = session @@ -1913,12 +1913,14 @@ async fn follow( .retain(|view| view.leader_id != Some(follower_id.into())); response.send(response_payload)?; - let room = session - .db() - .await - .follow(room_id, project_id, leader_id, follower_id) - .await?; - room_updated(&room, &session.peer); + if let Some(project_id) = project_id { + let room = session + .db() + .await + .follow(room_id, project_id, leader_id, follower_id) + .await?; + room_updated(&room, &session.peer); + } Ok(()) } @@ -1935,19 +1937,21 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { session .db() .await - .check_can_unfollow(room_id, project_id, leader_id, session.connection_id) + .check_room_participants(room_id, leader_id, session.connection_id) .await?; session .peer .forward_send(session.connection_id, leader_id, request)?; - let room = session - .db() - .await - .unfollow(room_id, project_id, leader_id, follower_id) - .await?; - room_updated(&room, &session.peer); + if let Some(project_id) = project_id { + let room = session + .db() + .await + .unfollow(room_id, project_id, leader_id, follower_id) + .await?; + room_updated(&room, &session.peer); + } Ok(()) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 523ef1d763..a62a9f06c3 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -273,7 +273,7 @@ message ParticipantProject { message Follower { PeerId leader_id = 1; PeerId follower_id = 2; - optional uint64 project_id = 3; + uint64 project_id = 3; } message ParticipantLocation { From 38a9e6fde1a971b2bf41fe7770fba5691e7f764d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2023 16:46:43 -0700 Subject: [PATCH 12/45] Fix removal of followers on Unfollow --- crates/call/src/call.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 6756c2aa53..f5bc05e37a 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -95,7 +95,7 @@ impl ActiveCall { client.add_message_handler(cx.handle(), Self::handle_call_canceled), client.add_request_handler(cx.handle(), Self::handle_follow), client.add_message_handler(cx.handle(), Self::handle_unfollow), - client.add_message_handler(cx.handle(), Self::handle_update_followers), + client.add_message_handler(cx.handle(), Self::handle_update_from_leader), ], client, user_store, @@ -259,14 +259,14 @@ impl ActiveCall { project_id: envelope.payload.project_id, peer_id: envelope.original_sender_id()?, }; - if let Err(ix) = this.followers.binary_search(&follower) { + if let Ok(ix) = this.followers.binary_search(&follower) { this.followers.remove(ix); } Ok(()) }) } - async fn handle_update_followers( + async fn handle_update_from_leader( this: ModelHandle, envelope: TypedEnvelope, _: Arc, From e34ebbc665ff2b10bfb686c9b79db7d9b9bb11e4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2023 16:56:39 -0700 Subject: [PATCH 13/45] Remove unused dependencies on theme --- Cargo.lock | 4 ---- crates/call/Cargo.toml | 1 - crates/channel/Cargo.toml | 1 - crates/client/Cargo.toml | 1 - crates/project/Cargo.toml | 1 - 5 files changed, 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b675614376..983f6946ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1106,7 +1106,6 @@ dependencies = [ "serde_derive", "serde_json", "settings", - "theme", "util", ] @@ -1247,7 +1246,6 @@ dependencies = [ "sum_tree", "tempfile", "text", - "theme", "thiserror", "time", "tiny_http", @@ -1419,7 +1417,6 @@ dependencies = [ "sum_tree", "tempfile", "text", - "theme", "thiserror", "time", "tiny_http", @@ -5552,7 +5549,6 @@ dependencies = [ "tempdir", "terminal", "text", - "theme", "thiserror", "toml 0.5.11", "unindent", diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 716bc3c27b..b4e94fe56c 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -31,7 +31,6 @@ language = { path = "../language" } media = { path = "../media" } project = { path = "../project" } settings = { path = "../settings" } -theme = { path = "../theme" } util = { path = "../util" } anyhow.workspace = true diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index e3a74ecbe6..16a1d418d5 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -23,7 +23,6 @@ language = { path = "../language" } settings = { path = "../settings" } feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } -theme = { path = "../theme" } anyhow.workspace = true futures.workspace = true diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 2bd03d789f..e3038e5bcc 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -21,7 +21,6 @@ text = { path = "../text" } settings = { path = "../settings" } feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } -theme = { path = "../theme" } anyhow.workspace = true async-recursion = "0.3" diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 07f1bfa43e..ffea6646e9 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -35,7 +35,6 @@ rpc = { path = "../rpc" } settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } terminal = { path = "../terminal" } -theme = { path = "../theme" } util = { path = "../util" } aho-corasick = "1.1" From 00587027499f3d5a2bb8b70027be34a0eecd2bde Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2023 16:56:51 -0700 Subject: [PATCH 14/45] Remove unused db query method --- crates/collab/src/db/queries/rooms.rs | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 41f9755872..b103ae1c73 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -985,32 +985,6 @@ impl Database { Ok(room) } - pub async fn room_id_for_connection(&self, connection_id: ConnectionId) -> Result { - #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] - enum QueryRoomId { - RoomId, - } - - self.transaction(|tx| async move { - Ok(room_participant::Entity::find() - .select_only() - .column(room_participant::Column::RoomId) - .filter( - Condition::all() - .add(room_participant::Column::AnsweringConnectionId.eq(connection_id.id)) - .add( - room_participant::Column::AnsweringConnectionServerId - .eq(ServerId(connection_id.owner_id as i32)), - ), - ) - .into_values::<_, QueryRoomId>() - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no room for connection {:?}", connection_id))?) - }) - .await - } - pub async fn room_connection_ids( &self, room_id: RoomId, From 5a15692589139484cac9703001743ab2c571a98d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2023 17:04:05 -0700 Subject: [PATCH 15/45] :art: Workspace::leader_updated --- crates/workspace/src/workspace.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 404d974c52..89dff882c3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3042,20 +3042,18 @@ impl Workspace { }; for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { - let item = state - .active_view_id - .and_then(|id| state.items_by_leader_view_id.get(&id)); - let shared_screen = self.shared_screen_for_peer(leader_id, pane, cx); - if leader_in_this_app { + let item = state + .active_view_id + .and_then(|id| state.items_by_leader_view_id.get(&id)); if let Some(item) = item { if leader_in_this_project || !item.is_project_item(cx) { items_to_activate.push((pane.clone(), item.boxed_clone())); } - } else if let Some(shared_screen) = shared_screen { - items_to_activate.push((pane.clone(), Box::new(shared_screen))); + continue; } - } else if let Some(shared_screen) = shared_screen { + } + if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { items_to_activate.push((pane.clone(), Box::new(shared_screen))); } } From 837ec5a27c69ebbb4bd3cf0feb956558ad5a2ce7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2023 17:13:39 -0700 Subject: [PATCH 16/45] Remove stray file --- selection-color-notes.txt | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 selection-color-notes.txt diff --git a/selection-color-notes.txt b/selection-color-notes.txt deleted file mode 100644 index 6186adcac1..0000000000 --- a/selection-color-notes.txt +++ /dev/null @@ -1,14 +0,0 @@ -Assign selection colors to users. goals: - * current user is always main color - * every other user has the same color in every context - * users don't need to be in a shared project to have a color. they can either be in the call, or in a channel notes. - -Places colors are used: - * editor element, driven by the buffer's `remote_selections` - * pane border (access to more state) - * collab titlebar (access to more state) - -Currently, editor holds an optional "replica id map". - -Most challenging part is in the editor, because the editor should be fairly self-contained, not depend on e.g. the user store. - From ca0a4bdf8e24d64039ba2ae1bfad2b2fbbf48b07 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2023 18:58:52 -0700 Subject: [PATCH 17/45] Introduce a WorkspaceStore for handling following --- crates/call/src/call.rs | 205 ++---------------------- crates/collab/src/tests/test_server.rs | 4 +- crates/workspace/src/workspace.rs | 212 +++++++++++++++++++++---- crates/zed/src/main.rs | 4 +- 4 files changed, 201 insertions(+), 224 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index f5bc05e37a..bdc402ee77 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -2,29 +2,23 @@ pub mod call_settings; pub mod participant; pub mod room; -use std::sync::Arc; - use anyhow::{anyhow, Result}; use audio::Audio; use call_settings::CallSettings; use channel::ChannelId; -use client::{ - proto::{self, PeerId}, - ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore, -}; +use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore}; use collections::HashSet; use futures::{future::Shared, FutureExt}; -use postage::watch; - use gpui::{ - AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, - ModelHandle, Subscription, Task, ViewContext, WeakModelHandle, + AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task, + WeakModelHandle, }; +use postage::watch; use project::Project; +use std::sync::Arc; pub use participant::ParticipantLocation; pub use room::Room; -use util::ResultExt; pub fn init(client: Arc, user_store: ModelHandle, cx: &mut AppContext) { settings::register::(cx); @@ -53,25 +47,9 @@ pub struct ActiveCall { ), client: Arc, user_store: ModelHandle, - follow_handlers: Vec, - followers: Vec, _subscriptions: Vec, } -#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] -struct Follower { - project_id: Option, - peer_id: PeerId, -} - -struct FollowHandler { - project_id: Option, - root_view: AnyWeakViewHandle, - get_views: - Box, &mut AppContext) -> Option>, - update_view: Box, -} - impl Entity for ActiveCall { type Event = room::Event; } @@ -88,14 +66,10 @@ impl ActiveCall { location: None, pending_invites: Default::default(), incoming_call: watch::channel(), - follow_handlers: Default::default(), - followers: Default::default(), + _subscriptions: vec![ client.add_request_handler(cx.handle(), Self::handle_incoming_call), client.add_message_handler(cx.handle(), Self::handle_call_canceled), - client.add_request_handler(cx.handle(), Self::handle_follow), - client.add_message_handler(cx.handle(), Self::handle_unfollow), - client.add_message_handler(cx.handle(), Self::handle_update_from_leader), ], client, user_store, @@ -106,48 +80,6 @@ impl ActiveCall { self.room()?.read(cx).channel_id() } - pub fn add_follow_handler( - &mut self, - root_view: gpui::ViewHandle, - project_id: Option, - get_views: GetViews, - update_view: UpdateView, - _cx: &mut ModelContext, - ) where - GetViews: 'static - + Fn(&mut V, Option, &mut gpui::ViewContext) -> Result, - UpdateView: - 'static + Fn(&mut V, PeerId, proto::UpdateFollowers, &mut ViewContext) -> Result<()>, - { - self.follow_handlers - .retain(|h| h.root_view.id() != root_view.id()); - if let Err(ix) = self - .follow_handlers - .binary_search_by_key(&(project_id, root_view.id()), |f| { - (f.project_id, f.root_view.id()) - }) - { - self.follow_handlers.insert( - ix, - FollowHandler { - project_id, - root_view: root_view.into_any().downgrade(), - get_views: Box::new(move |view, project_id, cx| { - let view = view.clone().downcast::().unwrap(); - view.update(cx, |view, cx| get_views(view, project_id, cx).log_err()) - .flatten() - }), - update_view: Box::new(move |view, leader_id, message, cx| { - let view = view.clone().downcast::().unwrap(); - view.update(cx, |view, cx| { - update_view(view, leader_id, message, cx).log_err() - }); - }), - }, - ); - } - } - async fn handle_incoming_call( this: ModelHandle, envelope: TypedEnvelope, @@ -194,127 +126,6 @@ impl ActiveCall { Ok(()) } - async fn handle_follow( - this: ModelHandle, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result { - this.update(&mut cx, |this, cx| { - let follower = Follower { - project_id: envelope.payload.project_id, - peer_id: envelope.original_sender_id()?, - }; - let active_project_id = this - .location - .as_ref() - .and_then(|project| project.upgrade(cx)?.read(cx).remote_id()); - - let mut response = proto::FollowResponse::default(); - for handler in &this.follow_handlers { - if follower.project_id != handler.project_id && follower.project_id.is_some() { - continue; - } - - let Some(root_view) = handler.root_view.upgrade(cx) else { - continue; - }; - - let Some(handler_response) = - (handler.get_views)(&root_view, follower.project_id, cx) - else { - continue; - }; - - if response.views.is_empty() { - response.views = handler_response.views; - } else { - response.views.extend_from_slice(&handler_response.views); - } - - if let Some(active_view_id) = handler_response.active_view_id.clone() { - if response.active_view_id.is_none() || handler.project_id == active_project_id - { - response.active_view_id = Some(active_view_id); - } - } - } - - if let Err(ix) = this.followers.binary_search(&follower) { - this.followers.insert(ix, follower); - } - - Ok(response) - }) - } - - async fn handle_unfollow( - this: ModelHandle, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, _| { - let follower = Follower { - project_id: envelope.payload.project_id, - peer_id: envelope.original_sender_id()?, - }; - if let Ok(ix) = this.followers.binary_search(&follower) { - this.followers.remove(ix); - } - Ok(()) - }) - } - - async fn handle_update_from_leader( - this: ModelHandle, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - let leader_id = envelope.original_sender_id()?; - let update = envelope.payload; - this.update(&mut cx, |this, cx| { - for handler in &this.follow_handlers { - if update.project_id != handler.project_id && update.project_id.is_some() { - continue; - } - let Some(root_view) = handler.root_view.upgrade(cx) else { - continue; - }; - (handler.update_view)(&root_view, leader_id, update.clone(), cx); - } - Ok(()) - }) - } - - pub fn update_followers( - &self, - project_id: Option, - update: proto::update_followers::Variant, - cx: &AppContext, - ) -> Option<()> { - let room_id = self.room()?.read(cx).id(); - let follower_ids: Vec<_> = self - .followers - .iter() - .filter_map(|follower| { - (follower.project_id == project_id).then_some(follower.peer_id.into()) - }) - .collect(); - if follower_ids.is_empty() { - return None; - } - self.client - .send(proto::UpdateFollowers { - room_id, - project_id, - follower_ids, - variant: Some(update), - }) - .log_err() - } - pub fn global(cx: &AppContext) -> ModelHandle { cx.global::>().clone() } @@ -536,6 +347,10 @@ impl ActiveCall { } } + pub fn location(&self) -> Option<&WeakModelHandle> { + self.location.as_ref() + } + pub fn set_location( &mut self, project: Option<&ModelHandle>, diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 71537f069f..a56df311bd 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -29,7 +29,7 @@ use std::{ }, }; use util::http::FakeHttpClient; -use workspace::Workspace; +use workspace::{Workspace, WorkspaceStore}; pub struct TestServer { pub app_state: Arc, @@ -204,6 +204,7 @@ 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(); @@ -211,6 +212,7 @@ impl TestServer { 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(), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 89dff882c3..256ecfd3e9 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -15,7 +15,7 @@ use call::ActiveCall; use channel::ChannelStore; use client::{ proto::{self, PeerId}, - Client, UserStore, + Client, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use drag_and_drop::DragAndDrop; @@ -451,6 +451,7 @@ pub struct AppState { pub client: Arc, pub user_store: ModelHandle, pub channel_store: ModelHandle, + pub workspace_store: ModelHandle, pub fs: Arc, pub build_window_options: fn(Option, Option, &dyn Platform) -> WindowOptions<'static>, @@ -459,6 +460,19 @@ pub struct AppState { pub background_actions: BackgroundActions, } +pub struct WorkspaceStore { + workspaces: HashSet>, + followers: Vec, + client: Arc, + _subscriptions: Vec, +} + +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] +struct Follower { + project_id: Option, + peer_id: PeerId, +} + impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Arc { @@ -475,6 +489,7 @@ impl AppState { let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let channel_store = cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); + let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); theme::init((), cx); client::init(&client, cx); @@ -486,6 +501,7 @@ impl AppState { languages, user_store, channel_store, + workspace_store, initialize_workspace: |_, _, _, _| Task::ready(Ok(())), build_window_options: |_, _, _| Default::default(), background_actions: || &[], @@ -663,6 +679,10 @@ impl Workspace { cx.focus(¢er_pane); cx.emit(Event::PaneAdded(center_pane.clone())); + app_state.workspace_store.update(cx, |store, _| { + store.workspaces.insert(weak_handle.clone()); + }); + let mut current_user = app_state.user_store.read(cx).watch_current_user(); let mut connection_status = app_state.client.status(); let _observe_current_user = cx.spawn(|this, mut cx| async move { @@ -2492,19 +2512,8 @@ impl Workspace { &self.active_pane } - fn project_remote_id_changed(&mut self, remote_id: Option, cx: &mut ViewContext) { - let handle = cx.handle(); - if let Some(call) = self.active_call() { - call.update(cx, |call, cx| { - call.add_follow_handler( - handle, - remote_id, - Self::get_views_for_followers, - Self::handle_update_followers, - cx, - ); - }); - } + fn project_remote_id_changed(&mut self, _project_id: Option, _cx: &mut ViewContext) { + // TODO } fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { @@ -2793,11 +2802,7 @@ impl Workspace { // RPC handlers - fn get_views_for_followers( - &mut self, - _project_id: Option, - cx: &mut ViewContext, - ) -> Result { + fn handle_follow(&mut self, cx: &mut ViewContext) -> proto::FollowResponse { let client = &self.app_state.client; let active_view_id = self.active_item(cx).and_then(|i| { @@ -2810,7 +2815,7 @@ impl Workspace { cx.notify(); - Ok(proto::FollowResponse { + proto::FollowResponse { active_view_id, views: self .panes() @@ -2832,7 +2837,7 @@ impl Workspace { }) }) .collect(), - }) + } } fn handle_update_followers( @@ -2840,10 +2845,10 @@ impl Workspace { leader_id: PeerId, message: proto::UpdateFollowers, _cx: &mut ViewContext, - ) -> Result<()> { + ) { self.leader_updates_tx - .unbounded_send((leader_id, message))?; - Ok(()) + .unbounded_send((leader_id, message)) + .ok(); } async fn process_leader_update( @@ -2999,9 +3004,9 @@ impl Workspace { update: proto::update_followers::Variant, cx: &AppContext, ) -> Option<()> { - self.active_call()? - .read(cx) - .update_followers(self.project.read(cx).remote_id(), update, cx) + self.app_state().workspace_store.read_with(cx, |store, cx| { + store.update_followers(self.project.read(cx).remote_id(), update, cx) + }) } pub fn leader_for_pane(&self, pane: &ViewHandle) -> Option { @@ -3472,8 +3477,10 @@ impl Workspace { let channel_store = cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); + let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); let app_state = Arc::new(AppState { languages: project.read(cx).languages().clone(), + workspace_store, client, user_store, channel_store, @@ -3717,6 +3724,12 @@ fn notify_if_database_failed(workspace: &WeakViewHandle, cx: &mut Asy impl Entity for Workspace { type Event = Event; + + fn release(&mut self, cx: &mut AppContext) { + self.app_state.workspace_store.update(cx, |store, _| { + store.workspaces.remove(&self.weak_self); + }) + } } impl View for Workspace { @@ -3859,6 +3872,151 @@ impl View for Workspace { } } +impl WorkspaceStore { + pub fn new(client: Arc, cx: &mut ModelContext) -> Self { + Self { + workspaces: Default::default(), + followers: Default::default(), + _subscriptions: vec![ + client.add_request_handler(cx.handle(), Self::handle_follow), + client.add_message_handler(cx.handle(), Self::handle_unfollow), + client.add_message_handler(cx.handle(), Self::handle_update_from_leader), + ], + client, + } + } + + pub fn update_followers( + &self, + project_id: Option, + update: proto::update_followers::Variant, + cx: &AppContext, + ) -> Option<()> { + if !cx.has_global::>() { + return None; + } + + let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id(); + let follower_ids: Vec<_> = self + .followers + .iter() + .filter_map(|follower| { + (follower.project_id == project_id).then_some(follower.peer_id.into()) + }) + .collect(); + if follower_ids.is_empty() { + return None; + } + self.client + .send(proto::UpdateFollowers { + room_id, + project_id, + follower_ids, + variant: Some(update), + }) + .log_err() + } + + async fn handle_follow( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + this.update(&mut cx, |this, cx| { + let follower = Follower { + project_id: envelope.payload.project_id, + peer_id: envelope.original_sender_id()?, + }; + let active_project_id = ActiveCall::global(cx) + .read(cx) + .location() + .as_ref() + .and_then(|project| project.upgrade(cx)?.read(cx).remote_id()); + + let mut response = proto::FollowResponse::default(); + for workspace in &this.workspaces { + let Some(workspace) = workspace.upgrade(cx) else { + continue; + }; + + workspace.update(cx.as_mut(), |workspace, cx| { + let project_id = workspace.project.read(cx).remote_id(); + if follower.project_id != project_id && follower.project_id.is_some() { + return; + } + + let handler_response = workspace.handle_follow(cx); + if response.views.is_empty() { + response.views = handler_response.views; + } else { + response.views.extend_from_slice(&handler_response.views); + } + + if let Some(active_view_id) = handler_response.active_view_id.clone() { + if response.active_view_id.is_none() || project_id == active_project_id { + response.active_view_id = Some(active_view_id); + } + } + }); + } + + if let Err(ix) = this.followers.binary_search(&follower) { + this.followers.insert(ix, follower); + } + + Ok(response) + }) + } + + async fn handle_unfollow( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, _| { + let follower = Follower { + project_id: envelope.payload.project_id, + peer_id: envelope.original_sender_id()?, + }; + if let Ok(ix) = this.followers.binary_search(&follower) { + this.followers.remove(ix); + } + Ok(()) + }) + } + + async fn handle_update_from_leader( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let leader_id = envelope.original_sender_id()?; + let update = envelope.payload; + this.update(&mut cx, |this, cx| { + for workspace in &this.workspaces { + let Some(workspace) = workspace.upgrade(cx) else { + continue; + }; + workspace.update(cx.as_mut(), |workspace, cx| { + let project_id = workspace.project.read(cx).remote_id(); + if update.project_id != project_id && update.project_id.is_some() { + return; + } + workspace.handle_update_followers(leader_id, update.clone(), cx); + }); + } + Ok(()) + }) + } +} + +impl Entity for WorkspaceStore { + type Event = (); +} + impl ViewId { pub(crate) fn from_proto(message: proto::ViewId) -> Result { Ok(Self { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index bb44f67841..7991cabde2 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -54,7 +54,7 @@ use welcome::{show_welcome_experience, FIRST_OPEN}; use fs::RealFs; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; -use workspace::AppState; +use workspace::{AppState, WorkspaceStore}; use zed::{ assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, @@ -139,6 +139,7 @@ fn main() { let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); let channel_store = cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx)); + let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx)); cx.set_global(client.clone()); @@ -187,6 +188,7 @@ fn main() { build_window_options, initialize_workspace, background_actions, + workspace_store, }); cx.set_global(Arc::downgrade(&app_state)); From 026b3a1d0f87d97206d54228e7b8ae58b4080eab Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Sep 2023 08:54:23 -0700 Subject: [PATCH 18/45] Remove uneeded Workspace::project_remote_id_changed method --- crates/workspace/src/workspace.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 256ecfd3e9..9834f47370 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -622,9 +622,8 @@ impl Workspace { cx.observe(&project, |_, _, cx| cx.notify()).detach(); cx.subscribe(&project, move |this, _, event, cx| { match event { - project::Event::RemoteIdChanged(remote_id) => { + project::Event::RemoteIdChanged(_) => { this.update_window_title(cx); - this.project_remote_id_changed(*remote_id, cx); } project::Event::CollaboratorLeft(peer_id) => { @@ -776,7 +775,8 @@ impl Workspace { }), ]; - let mut this = Workspace { + cx.defer(|this, cx| this.update_window_title(cx)); + Workspace { weak_self: weak_handle.clone(), modal: None, zoomed: None, @@ -805,10 +805,7 @@ impl Workspace { leader_updates_tx, subscriptions, pane_history_timestamp, - }; - this.project_remote_id_changed(project.read(cx).remote_id(), cx); - cx.defer(|this, cx| this.update_window_title(cx)); - this + } } fn new_local( @@ -2512,10 +2509,6 @@ impl Workspace { &self.active_pane } - fn project_remote_id_changed(&mut self, _project_id: Option, _cx: &mut ViewContext) { - // TODO - } - fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) { for state in states_by_pane.into_values() { From 9b7bd4e9ae10238606b09a44b0d71904caee8784 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 29 Sep 2023 12:08:25 -0600 Subject: [PATCH 19/45] vim: Fix accidental visual selection when following --- crates/vim/src/vim.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 6ff997a161..65410a460e 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -195,6 +195,9 @@ impl Vim { if editor_mode == EditorMode::Full && !newest_selection_empty && self.state().mode == Mode::Normal + // if leader_replica_id is set, then you're following someone else's cursor + // don't switch vim mode. + && editor.leader_replica_id().is_none() { self.switch_mode(Mode::Visual, true, cx); } From 3c12e711a40d97a759edd9b1e4ea7e17f263f476 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Fri, 29 Sep 2023 14:35:02 -0400 Subject: [PATCH 20/45] add scheme for full parseable files in semantic index --- crates/semantic_index/src/parsing.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/semantic_index/src/parsing.rs b/crates/semantic_index/src/parsing.rs index 498ad6187e..f9b8bac9a4 100644 --- a/crates/semantic_index/src/parsing.rs +++ b/crates/semantic_index/src/parsing.rs @@ -61,8 +61,9 @@ const CODE_CONTEXT_TEMPLATE: &str = const ENTIRE_FILE_TEMPLATE: &str = "The below snippet is from file ''\n\n```\n\n```"; const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file ''\n\n"; -pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = - &["TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML"]; +pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = &[ + "TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML", "Scheme", +]; pub struct CodeContextRetriever { pub parser: Parser, From 4887ea3563df756a2a25ce36eeb2800d7758721b Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 29 Sep 2023 12:05:21 -0700 Subject: [PATCH 21/45] Add support for the TextDocumentSyncKind LSP options --- crates/project/src/project.rs | 58 ++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a551b985bf..1db00b387c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2231,26 +2231,48 @@ impl Project { .get_mut(&buffer.remote_id()) .and_then(|m| m.get_mut(&language_server.server_id()))?; let previous_snapshot = buffer_snapshots.last()?; - let next_version = previous_snapshot.version + 1; - let content_changes = buffer - .edits_since::<(PointUtf16, usize)>(previous_snapshot.snapshot.version()) - .map(|edit| { - let edit_start = edit.new.start.0; - let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); - let new_text = next_snapshot - .text_for_range(edit.new.start.1..edit.new.end.1) - .collect(); - lsp::TextDocumentContentChangeEvent { - range: Some(lsp::Range::new( - point_to_lsp(edit_start), - point_to_lsp(edit_end), - )), + let document_sync_kind = language_server + .capabilities() + .text_document_sync + .as_ref() + .and_then(|sync| match sync { + lsp::TextDocumentSyncCapability::Kind(kind) => Some(*kind), + lsp::TextDocumentSyncCapability::Options(options) => options.change, + }); + + let content_changes: Vec<_> = match document_sync_kind { + Some(lsp::TextDocumentSyncKind::FULL) => { + vec![lsp::TextDocumentContentChangeEvent { + range: None, range_length: None, - text: new_text, - } - }) - .collect(); + text: next_snapshot.text(), + }] + } + Some(lsp::TextDocumentSyncKind::INCREMENTAL) => buffer + .edits_since::<(PointUtf16, usize)>( + previous_snapshot.snapshot.version(), + ) + .map(|edit| { + let edit_start = edit.new.start.0; + let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); + let new_text = next_snapshot + .text_for_range(edit.new.start.1..edit.new.end.1) + .collect(); + lsp::TextDocumentContentChangeEvent { + range: Some(lsp::Range::new( + point_to_lsp(edit_start), + point_to_lsp(edit_end), + )), + range_length: None, + text: new_text, + } + }) + .collect(), + _ => continue, + }; + + let next_version = previous_snapshot.version + 1; buffer_snapshots.push(LspBufferSnapshot { version: next_version, From 31ff5bffd632947d4962582defc203b2a0ee3e55 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 29 Sep 2023 12:19:58 -0700 Subject: [PATCH 22/45] Fix tests relying on off-spec behavior --- crates/project/src/project.rs | 54 ++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1db00b387c..94180bc023 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2232,24 +2232,8 @@ impl Project { .and_then(|m| m.get_mut(&language_server.server_id()))?; let previous_snapshot = buffer_snapshots.last()?; - let document_sync_kind = language_server - .capabilities() - .text_document_sync - .as_ref() - .and_then(|sync| match sync { - lsp::TextDocumentSyncCapability::Kind(kind) => Some(*kind), - lsp::TextDocumentSyncCapability::Options(options) => options.change, - }); - - let content_changes: Vec<_> = match document_sync_kind { - Some(lsp::TextDocumentSyncKind::FULL) => { - vec![lsp::TextDocumentContentChangeEvent { - range: None, - range_length: None, - text: next_snapshot.text(), - }] - } - Some(lsp::TextDocumentSyncKind::INCREMENTAL) => buffer + let build_incremental_change = || { + buffer .edits_since::<(PointUtf16, usize)>( previous_snapshot.snapshot.version(), ) @@ -2268,8 +2252,38 @@ impl Project { text: new_text, } }) - .collect(), - _ => continue, + .collect() + }; + + let document_sync_kind = language_server + .capabilities() + .text_document_sync + .as_ref() + .and_then(|sync| match sync { + lsp::TextDocumentSyncCapability::Kind(kind) => Some(*kind), + lsp::TextDocumentSyncCapability::Options(options) => options.change, + }); + + let content_changes: Vec<_> = match document_sync_kind { + Some(lsp::TextDocumentSyncKind::FULL) => { + vec![lsp::TextDocumentContentChangeEvent { + range: None, + range_length: None, + text: next_snapshot.text(), + }] + } + Some(lsp::TextDocumentSyncKind::INCREMENTAL) => build_incremental_change(), + _ => { + #[cfg(any(test, feature = "test-support"))] + { + build_incremental_change() + } + + #[cfg(not(any(test, feature = "test-support")))] + { + continue; + } + } }; let next_version = previous_snapshot.version + 1; From 555c9847d44854f0c43cf140fcf5b493a03b162c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Sep 2023 13:43:43 -0700 Subject: [PATCH 23/45] Add ZED_ALWAYS_ACTIVE env var, use it in local collaboration script This makes zed always behave as if the app is active, even if no window is focused. It prevents the 'viewing a window outside of zed' state during collaboration. --- crates/call/src/call.rs | 16 ++++++++++------ crates/client/src/client.rs | 2 ++ script/start-local-collaboration | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index bdc402ee77..ca0d06beb6 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -6,7 +6,10 @@ use anyhow::{anyhow, Result}; use audio::Audio; use call_settings::CallSettings; use channel::ChannelId; -use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore}; +use client::{ + proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore, + ZED_ALWAYS_ACTIVE, +}; use collections::HashSet; use futures::{future::Shared, FutureExt}; use gpui::{ @@ -356,12 +359,13 @@ impl ActiveCall { project: Option<&ModelHandle>, cx: &mut ModelContext, ) -> Task> { - self.location = project.map(|project| project.downgrade()); - if let Some((room, _)) = self.room.as_ref() { - room.update(cx, |room, cx| room.set_location(project, cx)) - } else { - Task::ready(Ok(())) + if project.is_some() || !*ZED_ALWAYS_ACTIVE { + self.location = project.map(|project| project.downgrade()); + if let Some((room, _)) = self.room.as_ref() { + return room.update(cx, |room, cx| room.set_location(project, cx)); + } } + Task::ready(Ok(())) } fn set_room( diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index d28c1ab1a9..5eae700404 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -62,6 +62,8 @@ lazy_static! { .and_then(|v| v.parse().ok()); pub static ref ZED_APP_PATH: Option = std::env::var("ZED_APP_PATH").ok().map(PathBuf::from); + pub static ref ZED_ALWAYS_ACTIVE: bool = + std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| e.len() > 0); } pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894"; diff --git a/script/start-local-collaboration b/script/start-local-collaboration index a5836ff776..0c4e60f9c3 100755 --- a/script/start-local-collaboration +++ b/script/start-local-collaboration @@ -44,6 +44,7 @@ position_2=${half_width},${y} # Authenticate using the collab server's admin secret. export ZED_STATELESS=1 +export ZED_ALWAYS_ACTIVE=1 export ZED_ADMIN_API_TOKEN=secret export ZED_SERVER_URL=http://localhost:8080 export ZED_WINDOW_SIZE=${half_width},${height} From 973f03e73eb095ec7c34c1c2118aebf305ef5bb4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Sep 2023 14:09:14 -0700 Subject: [PATCH 24/45] Fix bug in follower updates for non-project items --- crates/workspace/src/item.rs | 3 ++ crates/workspace/src/workspace.rs | 62 ++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 277dde55bd..f96c19c9ac 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -404,6 +404,7 @@ impl ItemHandle for ViewHandle { if let Some(followed_item) = self.to_followable_item_handle(cx) { if let Some(message) = followed_item.to_state_proto(cx) { workspace.update_followers( + followed_item.is_project_item(cx), proto::update_followers::Variant::CreateView(proto::View { id: followed_item .remote_id(&workspace.app_state.client, cx) @@ -439,6 +440,7 @@ impl ItemHandle for ViewHandle { }; if let Some(item) = item.to_followable_item_handle(cx) { + let is_project_item = item.is_project_item(cx); let leader_id = workspace.leader_for_pane(&pane); if leader_id.is_some() && item.should_unfollow_on_event(event, cx) { @@ -458,6 +460,7 @@ impl ItemHandle for ViewHandle { move |this, cx| { pending_update_scheduled.store(false, Ordering::SeqCst); this.update_followers( + is_project_item, proto::update_followers::Variant::UpdateView( proto::UpdateView { id: item diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9834f47370..f7cee6a7db 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2795,8 +2795,13 @@ impl Workspace { // RPC handlers - fn handle_follow(&mut self, cx: &mut ViewContext) -> proto::FollowResponse { + fn handle_follow( + &mut self, + follower_project_id: Option, + cx: &mut ViewContext, + ) -> proto::FollowResponse { let client = &self.app_state.client; + let project_id = self.project.read(cx).remote_id(); let active_view_id = self.active_item(cx).and_then(|i| { Some( @@ -2819,6 +2824,12 @@ impl Workspace { let cx = &cx; move |item| { let item = item.to_followable_item_handle(cx)?; + if project_id.is_some() + && project_id != follower_project_id + && item.is_project_item(cx) + { + return None; + } let id = item.remote_id(client, cx)?.to_proto(); let variant = item.to_state_proto(cx)?; Some(proto::View { @@ -2969,20 +2980,23 @@ impl Workspace { } fn update_active_view_for_followers(&self, cx: &AppContext) { - if self.active_pane.read(cx).has_focus() { + let item = self + .active_item(cx) + .and_then(|item| item.to_followable_item_handle(cx)); + if let Some(item) = item { self.update_followers( + item.is_project_item(cx), proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { - id: self.active_item(cx).and_then(|item| { - item.to_followable_item_handle(cx)? - .remote_id(&self.app_state.client, cx) - .map(|id| id.to_proto()) - }), + id: item + .remote_id(&self.app_state.client, cx) + .map(|id| id.to_proto()), leader_id: self.leader_for_pane(&self.active_pane), }), cx, ); } else { self.update_followers( + true, proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { id: None, leader_id: None, @@ -2994,11 +3008,17 @@ impl Workspace { fn update_followers( &self, + project_only: bool, update: proto::update_followers::Variant, cx: &AppContext, ) -> Option<()> { + let project_id = if project_only { + self.project.read(cx).remote_id() + } else { + None + }; self.app_state().workspace_store.read_with(cx, |store, cx| { - store.update_followers(self.project.read(cx).remote_id(), update, cx) + store.update_followers(project_id, update, cx) }) } @@ -3873,7 +3893,7 @@ impl WorkspaceStore { _subscriptions: vec![ client.add_request_handler(cx.handle(), Self::handle_follow), client.add_message_handler(cx.handle(), Self::handle_unfollow), - client.add_message_handler(cx.handle(), Self::handle_update_from_leader), + client.add_message_handler(cx.handle(), Self::handle_update_followers), ], client, } @@ -3894,7 +3914,11 @@ impl WorkspaceStore { .followers .iter() .filter_map(|follower| { - (follower.project_id == project_id).then_some(follower.peer_id.into()) + if follower.project_id == project_id || project_id.is_none() { + Some(follower.peer_id.into()) + } else { + None + } }) .collect(); if follower_ids.is_empty() { @@ -3921,11 +3945,10 @@ impl WorkspaceStore { project_id: envelope.payload.project_id, peer_id: envelope.original_sender_id()?, }; - let active_project_id = ActiveCall::global(cx) + let active_project = ActiveCall::global(cx) .read(cx) .location() - .as_ref() - .and_then(|project| project.upgrade(cx)?.read(cx).remote_id()); + .map(|project| project.id()); let mut response = proto::FollowResponse::default(); for workspace in &this.workspaces { @@ -3934,12 +3957,7 @@ impl WorkspaceStore { }; workspace.update(cx.as_mut(), |workspace, cx| { - let project_id = workspace.project.read(cx).remote_id(); - if follower.project_id != project_id && follower.project_id.is_some() { - return; - } - - let handler_response = workspace.handle_follow(cx); + let handler_response = workspace.handle_follow(follower.project_id, cx); if response.views.is_empty() { response.views = handler_response.views; } else { @@ -3947,7 +3965,9 @@ impl WorkspaceStore { } if let Some(active_view_id) = handler_response.active_view_id.clone() { - if response.active_view_id.is_none() || project_id == active_project_id { + if response.active_view_id.is_none() + || Some(workspace.project.id()) == active_project + { response.active_view_id = Some(active_view_id); } } @@ -3980,7 +4000,7 @@ impl WorkspaceStore { }) } - async fn handle_update_from_leader( + async fn handle_update_followers( this: ModelHandle, envelope: TypedEnvelope, _: Arc, From 752bc5dcddabd99e5e64ba8e582f02b4f8a3830c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 29 Sep 2023 14:06:32 -0700 Subject: [PATCH 25/45] Refactor elixir LSP settings --- assets/settings/default.json | 16 +- crates/zed/src/languages.rs | 17 +- crates/zed/src/languages/elixir.rs | 298 +++++++++++++++++++++++- crates/zed/src/languages/elixir_next.rs | 266 --------------------- 4 files changed, 310 insertions(+), 287 deletions(-) delete mode 100644 crates/zed/src/languages/elixir_next.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 95f99a78e9..7785f5dd44 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -379,24 +379,24 @@ }, // Settings specific to our elixir integration "elixir": { - // Set Zed to use the experimental Next LS LSP server. + // Change the LSP zed uses for elixir. // Note that changing this setting requires a restart of Zed // to take effect. // // May take 3 values: - // 1. Use the standard elixir-ls LSP server - // "next": "off" - // 2. Use a bundled version of the next Next LS LSP server - // "next": "on", - // 3. Use a local build of the next Next LS LSP server: - // "next": { + // 1. Use the standard ElixirLS, this is the default + // "lsp": "elixir_ls" + // 2. Use the experimental NextLs + // "lsp": "next_ls", + // 3. Use a language server installed locally on your machine: + // "lsp": { // "local": { // "path": "~/next-ls/bin/start", // "arguments": ["--stdio"] // } // }, // - "next": "off" + "lsp": "elixir_ls" }, // Different settings for specific languages. "languages": { diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index be8d05256a..04e5292a7d 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -6,12 +6,11 @@ use rust_embed::RustEmbed; use std::{borrow::Cow, str, sync::Arc}; use util::asset_str; -use self::elixir_next::ElixirSettings; +use self::elixir::ElixirSettings; mod c; mod css; mod elixir; -mod elixir_next; mod go; mod html; mod json; @@ -46,7 +45,7 @@ pub fn init( node_runtime: Arc, cx: &mut AppContext, ) { - settings::register::(cx); + settings::register::(cx); let language = |name, grammar, adapters| { languages.register(name, load_config(name), grammar, adapters, load_queries) @@ -72,21 +71,21 @@ pub fn init( ], ); - match &settings::get::(cx).next { - elixir_next::ElixirNextSetting::Off => language( + match &settings::get::(cx).lsp { + elixir::ElixirLspSetting::ElixirLs => language( "elixir", tree_sitter_elixir::language(), vec![Arc::new(elixir::ElixirLspAdapter)], ), - elixir_next::ElixirNextSetting::On => language( + elixir::ElixirLspSetting::NextLs => language( "elixir", tree_sitter_elixir::language(), - vec![Arc::new(elixir_next::NextLspAdapter)], + vec![Arc::new(elixir::NextLspAdapter)], ), - elixir_next::ElixirNextSetting::Local { path, arguments } => language( + elixir::ElixirLspSetting::Local { path, arguments } => language( "elixir", tree_sitter_elixir::language(), - vec![Arc::new(elixir_next::LocalNextLspAdapter { + vec![Arc::new(elixir::LocalLspAdapter { path: path.clone(), arguments: arguments.clone(), })], diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index b166feda76..9d2ebb7f47 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -1,12 +1,17 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; use futures::StreamExt; use gpui::{AsyncAppContext, Task}; pub use language::*; use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind}; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; +use settings::Setting; use smol::fs::{self, File}; use std::{ any::Any, + env::consts, + ops::Deref, path::PathBuf, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, @@ -14,11 +19,50 @@ use std::{ }, }; use util::{ + async_iife, fs::remove_matching, github::{latest_github_release, GitHubLspBinaryVersion}, ResultExt, }; +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct ElixirSettings { + pub lsp: ElixirLspSetting, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ElixirLspSetting { + ElixirLs, + NextLs, + Local { + path: String, + arguments: Vec, + }, +} + +#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)] +pub struct ElixirSettingsContent { + lsp: Option, +} + +impl Setting for ElixirSettings { + const KEY: Option<&'static str> = Some("elixir"); + + type FileContent = ElixirSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> Result + where + Self: Sized, + { + Self::load_via_json_merge(default_value, user_values) + } +} + pub struct ElixirLspAdapter; #[async_trait] @@ -144,14 +188,14 @@ impl LspAdapter for ElixirLspAdapter { container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - get_cached_server_binary(container_dir).await + get_cached_server_binary_elixir_ls(container_dir).await } async fn installation_test_binary( &self, container_dir: PathBuf, ) -> Option { - get_cached_server_binary(container_dir).await + get_cached_server_binary_elixir_ls(container_dir).await } async fn label_for_completion( @@ -238,7 +282,9 @@ impl LspAdapter for ElixirLspAdapter { } } -async fn get_cached_server_binary(container_dir: PathBuf) -> Option { +async fn get_cached_server_binary_elixir_ls( + container_dir: PathBuf, +) -> Option { (|| async move { let mut last = None; let mut entries = fs::read_dir(&container_dir).await?; @@ -254,3 +300,247 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option LanguageServerName { + LanguageServerName("next-ls".into()) + } + + fn short_name(&self) -> &'static str { + "next-ls" + } + + async fn fetch_latest_server_version( + &self, + delegate: &dyn LspAdapterDelegate, + ) -> Result> { + let release = + latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?; + let version = release.name.clone(); + let platform = match consts::ARCH { + "x86_64" => "darwin_arm64", + "aarch64" => "darwin_amd64", + other => bail!("Running on unsupported platform: {other}"), + }; + let asset_name = format!("next_ls_{}", platform); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: version, + url: asset.browser_download_url.clone(), + }; + Ok(Box::new(version) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let version = version.downcast::().unwrap(); + + let binary_path = container_dir.join("next-ls"); + + if fs::metadata(&binary_path).await.is_err() { + let mut response = delegate + .http_client() + .get(&version.url, Default::default(), true) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + + let mut file = smol::fs::File::create(&binary_path).await?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response.body_mut(), &mut file).await?; + + fs::set_permissions( + &binary_path, + ::from_mode(0o755), + ) + .await?; + } + + Ok(LanguageServerBinary { + path: binary_path, + arguments: vec!["--stdio".into()], + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary_next(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--stdio".into()]; + binary + }) + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary_next(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--help".into()]; + binary + }) + } + + async fn label_for_completion( + &self, + completion: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + label_for_completion_elixir(completion, language) + } + + async fn label_for_symbol( + &self, + name: &str, + symbol_kind: SymbolKind, + language: &Arc, + ) -> Option { + label_for_symbol_elixir(name, symbol_kind, language) + } +} + +async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option { + async_iife!({ + let mut last_binary_path = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_file() + && entry + .file_name() + .to_str() + .map_or(false, |name| name == "next-ls") + { + last_binary_path = Some(entry.path()); + } + } + + if let Some(path) = last_binary_path { + Ok(LanguageServerBinary { + path, + arguments: Vec::new(), + }) + } else { + Err(anyhow!("no cached binary")) + } + }) + .await + .log_err() +} + +pub struct LocalLspAdapter { + pub path: String, + pub arguments: Vec, +} + +#[async_trait] +impl LspAdapter for LocalLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("local-ls".into()) + } + + fn short_name(&self) -> &'static str { + "local-ls" + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(()) as Box<_>) + } + + async fn fetch_server_binary( + &self, + _: Box, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + let path = shellexpand::full(&self.path)?; + Ok(LanguageServerBinary { + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), + }) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + let path = shellexpand::full(&self.path).ok()?; + Some(LanguageServerBinary { + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), + }) + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + let path = shellexpand::full(&self.path).ok()?; + Some(LanguageServerBinary { + path: PathBuf::from(path.deref()), + arguments: self.arguments.iter().map(|arg| arg.into()).collect(), + }) + } + + async fn label_for_completion( + &self, + completion: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + label_for_completion_elixir(completion, language) + } + + async fn label_for_symbol( + &self, + name: &str, + symbol: SymbolKind, + language: &Arc, + ) -> Option { + label_for_symbol_elixir(name, symbol, language) + } +} + +fn label_for_completion_elixir( + completion: &lsp::CompletionItem, + language: &Arc, +) -> Option { + return Some(CodeLabel { + runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()), + text: completion.label.clone(), + filter_range: 0..completion.label.len(), + }); +} + +fn label_for_symbol_elixir( + name: &str, + _: SymbolKind, + language: &Arc, +) -> Option { + Some(CodeLabel { + runs: language.highlight_text(&name.into(), 0..name.len()), + text: name.to_string(), + filter_range: 0..name.len(), + }) +} diff --git a/crates/zed/src/languages/elixir_next.rs b/crates/zed/src/languages/elixir_next.rs deleted file mode 100644 index f5a77c7568..0000000000 --- a/crates/zed/src/languages/elixir_next.rs +++ /dev/null @@ -1,266 +0,0 @@ -use anyhow::{anyhow, bail, Result}; - -use async_trait::async_trait; -pub use language::*; -use lsp::{LanguageServerBinary, SymbolKind}; -use schemars::JsonSchema; -use serde_derive::{Deserialize, Serialize}; -use settings::Setting; -use smol::{fs, stream::StreamExt}; -use std::{any::Any, env::consts, ops::Deref, path::PathBuf, sync::Arc}; -use util::{ - async_iife, - github::{latest_github_release, GitHubLspBinaryVersion}, - ResultExt, -}; - -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -pub struct ElixirSettings { - pub next: ElixirNextSetting, -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ElixirNextSetting { - Off, - On, - Local { - path: String, - arguments: Vec, - }, -} - -#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)] -pub struct ElixirSettingsContent { - next: Option, -} - -impl Setting for ElixirSettings { - const KEY: Option<&'static str> = Some("elixir"); - - type FileContent = ElixirSettingsContent; - - fn load( - default_value: &Self::FileContent, - user_values: &[&Self::FileContent], - _: &gpui::AppContext, - ) -> Result - where - Self: Sized, - { - Self::load_via_json_merge(default_value, user_values) - } -} - -pub struct NextLspAdapter; - -#[async_trait] -impl LspAdapter for NextLspAdapter { - async fn name(&self) -> LanguageServerName { - LanguageServerName("next-ls".into()) - } - - fn short_name(&self) -> &'static str { - "next-ls" - } - - async fn fetch_latest_server_version( - &self, - delegate: &dyn LspAdapterDelegate, - ) -> Result> { - let release = - latest_github_release("elixir-tools/next-ls", false, delegate.http_client()).await?; - let version = release.name.clone(); - let platform = match consts::ARCH { - "x86_64" => "darwin_arm64", - "aarch64" => "darwin_amd64", - other => bail!("Running on unsupported platform: {other}"), - }; - let asset_name = format!("next_ls_{}", platform); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; - let version = GitHubLspBinaryVersion { - name: version, - url: asset.browser_download_url.clone(), - }; - Ok(Box::new(version) as Box<_>) - } - - async fn fetch_server_binary( - &self, - version: Box, - container_dir: PathBuf, - delegate: &dyn LspAdapterDelegate, - ) -> Result { - let version = version.downcast::().unwrap(); - - let binary_path = container_dir.join("next-ls"); - - if fs::metadata(&binary_path).await.is_err() { - let mut response = delegate - .http_client() - .get(&version.url, Default::default(), true) - .await - .map_err(|err| anyhow!("error downloading release: {}", err))?; - - let mut file = smol::fs::File::create(&binary_path).await?; - if !response.status().is_success() { - Err(anyhow!( - "download failed with status {}", - response.status().to_string() - ))?; - } - futures::io::copy(response.body_mut(), &mut file).await?; - - fs::set_permissions( - &binary_path, - ::from_mode(0o755), - ) - .await?; - } - - Ok(LanguageServerBinary { - path: binary_path, - arguments: vec!["--stdio".into()], - }) - } - - async fn cached_server_binary( - &self, - container_dir: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - get_cached_server_binary(container_dir) - .await - .map(|mut binary| { - binary.arguments = vec!["--stdio".into()]; - binary - }) - } - - async fn installation_test_binary( - &self, - container_dir: PathBuf, - ) -> Option { - get_cached_server_binary(container_dir) - .await - .map(|mut binary| { - binary.arguments = vec!["--help".into()]; - binary - }) - } - - async fn label_for_symbol( - &self, - name: &str, - symbol_kind: SymbolKind, - language: &Arc, - ) -> Option { - label_for_symbol_next(name, symbol_kind, language) - } -} - -async fn get_cached_server_binary(container_dir: PathBuf) -> Option { - async_iife!({ - let mut last_binary_path = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_file() - && entry - .file_name() - .to_str() - .map_or(false, |name| name == "next-ls") - { - last_binary_path = Some(entry.path()); - } - } - - if let Some(path) = last_binary_path { - Ok(LanguageServerBinary { - path, - arguments: Vec::new(), - }) - } else { - Err(anyhow!("no cached binary")) - } - }) - .await - .log_err() -} - -pub struct LocalNextLspAdapter { - pub path: String, - pub arguments: Vec, -} - -#[async_trait] -impl LspAdapter for LocalNextLspAdapter { - async fn name(&self) -> LanguageServerName { - LanguageServerName("local-next-ls".into()) - } - - fn short_name(&self) -> &'static str { - "next-ls" - } - - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new(()) as Box<_>) - } - - async fn fetch_server_binary( - &self, - _: Box, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Result { - let path = shellexpand::full(&self.path)?; - Ok(LanguageServerBinary { - path: PathBuf::from(path.deref()), - arguments: self.arguments.iter().map(|arg| arg.into()).collect(), - }) - } - - async fn cached_server_binary( - &self, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - let path = shellexpand::full(&self.path).ok()?; - Some(LanguageServerBinary { - path: PathBuf::from(path.deref()), - arguments: self.arguments.iter().map(|arg| arg.into()).collect(), - }) - } - - async fn installation_test_binary(&self, _: PathBuf) -> Option { - let path = shellexpand::full(&self.path).ok()?; - Some(LanguageServerBinary { - path: PathBuf::from(path.deref()), - arguments: self.arguments.iter().map(|arg| arg.into()).collect(), - }) - } - - async fn label_for_symbol( - &self, - name: &str, - symbol: SymbolKind, - language: &Arc, - ) -> Option { - label_for_symbol_next(name, symbol, language) - } -} - -fn label_for_symbol_next(name: &str, _: SymbolKind, language: &Arc) -> Option { - Some(CodeLabel { - runs: language.highlight_text(&name.into(), 0..name.len()), - text: name.to_string(), - filter_range: 0..name.len(), - }) -} From afd293ee879fb456c6f44802fcbe97a6ae15620e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Sep 2023 14:12:51 -0700 Subject: [PATCH 26/45] Update active view when activating a window --- crates/workspace/src/workspace.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f7cee6a7db..3b50157774 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3117,6 +3117,7 @@ impl Workspace { pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { if active { + self.update_active_view_for_followers(cx); cx.background() .spawn(persistence::DB.update_timestamp(self.database_id())) .detach(); From 55da5bc25d7d675c7a28bbe74e99f99636d4c87f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Sep 2023 14:16:38 -0700 Subject: [PATCH 27/45] Switch .leader_replica_id -> .leader_peer_id --- crates/vim/src/vim.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 68999b96b2..d27be2c54b 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -195,9 +195,8 @@ impl Vim { if editor_mode == EditorMode::Full && !newest_selection_empty && self.state().mode == Mode::Normal - // if leader_replica_id is set, then you're following someone else's cursor - // don't switch vim mode. - && editor.leader_replica_id().is_none() + // When following someone, don't switch vim mode. + && editor.leader_peer_id().is_none() { self.switch_mode(Mode::Visual, true, cx); } From 948871969fd9603c69c85412c63a8f746566b6b3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Sep 2023 14:35:21 -0700 Subject: [PATCH 28/45] Fix active view update when center pane is not focused --- crates/workspace/src/workspace.rs | 36 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3b50157774..44a70f9a08 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2980,30 +2980,28 @@ impl Workspace { } fn update_active_view_for_followers(&self, cx: &AppContext) { - let item = self - .active_item(cx) - .and_then(|item| item.to_followable_item_handle(cx)); - if let Some(item) = item { - self.update_followers( - item.is_project_item(cx), - proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { + let mut is_project_item = true; + let mut update = proto::UpdateActiveView::default(); + if self.active_pane.read(cx).has_focus() { + let item = self + .active_item(cx) + .and_then(|item| item.to_followable_item_handle(cx)); + if let Some(item) = item { + is_project_item = item.is_project_item(cx); + update = proto::UpdateActiveView { id: item .remote_id(&self.app_state.client, cx) .map(|id| id.to_proto()), leader_id: self.leader_for_pane(&self.active_pane), - }), - cx, - ); - } else { - self.update_followers( - true, - proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { - id: None, - leader_id: None, - }), - cx, - ); + }; + } } + + self.update_followers( + is_project_item, + proto::update_followers::Variant::UpdateActiveView(update), + cx, + ); } fn update_followers( From 7adaa2046d84bf6460a8550cb03c5b0f92d23a2a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Sep 2023 15:08:25 -0700 Subject: [PATCH 29/45] Show current user as follower when following in unshared projects --- crates/collab_ui/src/collab_titlebar_item.rs | 76 +++++++++----------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index c1b3689aad..9f3b7d2f30 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -889,22 +889,17 @@ impl CollabTitlebarItem { let user_id = user.id; let project_id = workspace.read(cx).project().read(cx).remote_id(); let room = ActiveCall::global(cx).read(cx).room().cloned(); - let is_being_followed = workspace.read(cx).is_being_followed(peer_id); - let followed_by_self = room - .as_ref() - .and_then(|room| { - Some( - is_being_followed - && room - .read(cx) - .followers_for(peer_id, project_id?) - .iter() - .any(|&follower| { - Some(follower) == workspace.read(cx).client().peer_id() - }), - ) - }) - .unwrap_or(false); + let self_peer_id = workspace.read(cx).client().peer_id(); + let self_following = workspace.read(cx).is_being_followed(peer_id); + let self_following_initialized = self_following + && room.as_ref().map_or(false, |room| match project_id { + None => true, + Some(project_id) => room + .read(cx) + .followers_for(peer_id, project_id) + .iter() + .any(|&follower| Some(follower) == self_peer_id), + }); let leader_style = theme.titlebar.leader_avatar; let follower_style = theme.titlebar.follower_avatar; @@ -930,7 +925,7 @@ impl CollabTitlebarItem { .get(&user_id) .copied(); if let Some(participant_index) = participant_index { - if followed_by_self { + if self_following_initialized { let selection = theme .editor .selection_style_for_room_participant(participant_index.0) @@ -960,31 +955,14 @@ impl CollabTitlebarItem { let project_id = project_id?; let room = room?.read(cx); let followers = room.followers_for(peer_id, project_id); - - Some(followers.into_iter().flat_map(|&follower| { - let remote_participant = - room.remote_participant_for_peer_id(follower); - - let avatar = remote_participant - .and_then(|p| p.user.avatar.clone()) - .or_else(|| { - if follower - == workspace.read(cx).client().peer_id()? - { - workspace - .read(cx) - .user_store() - .read(cx) - .current_user()? - .avatar - .clone() - } else { - None - } - })?; - + Some(followers.into_iter().filter_map(|&follower| { + if Some(follower) == self_peer_id { + return None; + } + let participant = + room.remote_participant_for_peer_id(follower)?; Some(Self::render_face( - avatar.clone(), + participant.user.avatar.clone()?, follower_style, background_color, None, @@ -993,6 +971,18 @@ impl CollabTitlebarItem { })() .into_iter() .flatten(), + ) + .with_children( + self_following_initialized + .then(|| self.user_store.read(cx).current_user()) + .and_then(|user| { + Some(Self::render_face( + user?.avatar.clone()?, + follower_style, + background_color, + None, + )) + }), ); let mut container = face_pile @@ -1000,7 +990,7 @@ impl CollabTitlebarItem { .with_style(theme.titlebar.leader_selection); if let Some(participant_index) = participant_index { - if followed_by_self { + if self_following_initialized { let color = theme .editor .selection_style_for_room_participant(participant_index.0) @@ -1067,7 +1057,7 @@ impl CollabTitlebarItem { }) .with_tooltip::( peer_id.as_u64() as usize, - if is_being_followed { + if self_following { format!("Unfollow {}", user.github_login) } else { format!("Follow {}", user.github_login) From 219715449d406df651174ef85ec391ae4ed83795 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 29 Sep 2023 12:36:17 -0600 Subject: [PATCH 30/45] More logging on collab by default --- Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Procfile b/Procfile index f6fde3cd92..2eb7de20fb 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,4 @@ web: cd ../zed.dev && PORT=3000 npm run dev -collab: cd crates/collab && cargo run serve +collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve livekit: livekit-server --dev postgrest: postgrest crates/collab/admin_api.conf From 1cfc2f0c0796b891e32225ad3067c1db3e35d18c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 29 Sep 2023 16:02:36 -0600 Subject: [PATCH 31/45] Show host in titlebar Co-Authored-By: Max Brunsfeld --- crates/client/src/client.rs | 4 ++ crates/client/src/user.rs | 4 ++ crates/collab_ui/src/collab_titlebar_item.rs | 73 +++++++++++++++++++- crates/project/src/project.rs | 4 ++ crates/theme/src/theme.rs | 1 + crates/workspace/src/workspace.rs | 18 +++++ styles/src/style_tree/titlebar.ts | 8 ++- 7 files changed, 110 insertions(+), 2 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 5eae700404..4ddfbc5a34 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -453,6 +453,10 @@ impl Client { self.state.read().status.1.clone() } + pub fn is_connected(&self) -> bool { + matches!(&*self.status().borrow(), Status::Connected { .. }) + } + fn set_status(self: &Arc, status: Status, cx: &AsyncAppContext) { log::info!("set status on client {}: {:?}", self.id, status); let mut state = self.state.write(); diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index b8cc8fb1b8..6aa41708e3 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -595,6 +595,10 @@ impl UserStore { self.load_users(proto::FuzzySearchUsers { query }, cx) } + pub fn get_cached_user(&self, user_id: u64) -> Option> { + self.users.get(&user_id).cloned() + } + pub fn get_user( &mut self, user_id: u64, diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 9f3b7d2f30..546b8ef407 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -215,7 +215,13 @@ impl CollabTitlebarItem { let git_style = theme.titlebar.git_menu_button.clone(); let item_spacing = theme.titlebar.item_spacing; - let mut ret = Flex::row().with_child( + let mut ret = Flex::row(); + + if let Some(project_host) = self.collect_project_host(theme.clone(), cx) { + ret = ret.with_child(project_host) + } + + ret = ret.with_child( Stack::new() .with_child( MouseEventHandler::new::(0, cx, |mouse_state, cx| { @@ -283,6 +289,71 @@ impl CollabTitlebarItem { ret.into_any() } + fn collect_project_host( + &self, + theme: Arc, + cx: &mut ViewContext, + ) -> Option> { + if ActiveCall::global(cx).read(cx).room().is_none() { + return None; + } + let project = self.project.read(cx); + let user_store = self.user_store.read(cx); + + if project.is_local() { + return None; + } + + let Some(host) = project.host() else { + return None; + }; + let (Some(host_user), Some(participant_index)) = ( + user_store.get_cached_user(host.user_id), + user_store.participant_indices().get(&host.user_id), + ) else { + return None; + }; + + enum ProjectHost {} + enum ProjectHostTooltip {} + + let host_style = theme.titlebar.project_host.clone(); + let selection_style = theme + .editor + .selection_style_for_room_participant(participant_index.0); + let peer_id = host.peer_id.clone(); + + Some( + MouseEventHandler::new::(0, cx, |mouse_state, _| { + let mut host_style = host_style.style_for(mouse_state).clone(); + host_style.text.color = selection_style.cursor; + Label::new(host_user.github_login.clone(), host_style.text) + .contained() + .with_style(host_style.container) + .aligned() + .left() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + if let Some(task) = + workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx)) + { + task.detach_and_log_err(cx); + } + } + }) + .with_tooltip::( + 0, + host_user.github_login.clone() + " is sharing this project. Click to follow.", + None, + theme.tooltip.clone(), + cx, + ) + .into_any_named("project-host"), + ) + } + fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { let project = if active { Some(self.project.clone()) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ee8690ea70..1ddf1a1f66 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -975,6 +975,10 @@ impl Project { &self.collaborators } + pub fn host(&self) -> Option<&Collaborator> { + self.collaborators.values().find(|c| c.replica_id == 0) + } + /// Collect all worktrees, including ones that don't appear in the project panel pub fn worktrees<'a>( &'a self, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 1ca2d839c0..b1595fb0d9 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -131,6 +131,7 @@ pub struct Titlebar { pub menu: TitlebarMenu, pub project_menu_button: Toggleable>, pub git_menu_button: Toggleable>, + pub project_host: Interactive, pub item_spacing: f32, pub face_pile_spacing: f32, pub avatar_ribbon: AvatarRibbon, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 44a70f9a08..8d9a4c1550 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2608,6 +2608,24 @@ impl Workspace { .and_then(|leader_id| self.toggle_follow(leader_id, cx)) } + pub fn follow( + &mut self, + leader_id: PeerId, + cx: &mut ViewContext, + ) -> Option>> { + 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; + } + } + } + + // not currently following, so follow. + self.toggle_follow(leader_id, cx) + } + pub fn unfollow( &mut self, pane: &ViewHandle, diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index 672907b22c..63c057a8eb 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -1,4 +1,4 @@ -import { icon_button, toggleable_icon_button, toggleable_text_button } from "../component" +import { icon_button, text_button, toggleable_icon_button, toggleable_text_button } from "../component" import { interactive, toggleable } from "../element" import { useTheme, with_opacity } from "../theme" import { background, border, foreground, text } from "./components" @@ -191,6 +191,12 @@ export function titlebar(): any { color: "variant", }), + project_host: text_button({ + text_properties: { + weight: "bold" + } + }), + // Collaborators leader_avatar: { width: avatar_width, From 92bb9a5fdccb63df807362ccde8654b39dd69dca Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 29 Sep 2023 17:59:19 -0600 Subject: [PATCH 32/45] Make following more good Co-Authored-By: Max Brunsfeld --- crates/collab/src/tests.rs | 1 + crates/collab/src/tests/following_tests.rs | 1189 ++++++++++++++++++ crates/collab/src/tests/integration_tests.rs | 1075 +--------------- crates/collab_ui/src/collab_titlebar_item.rs | 71 +- crates/workspace/src/pane_group.rs | 2 +- crates/workspace/src/workspace.rs | 52 +- 6 files changed, 1263 insertions(+), 1127 deletions(-) create mode 100644 crates/collab/src/tests/following_tests.rs diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index b0f5b96fde..e78bbe3466 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -4,6 +4,7 @@ use gpui::{ModelHandle, TestAppContext}; mod channel_buffer_tests; mod channel_message_tests; mod channel_tests; +mod following_tests; mod integration_tests; mod random_channel_buffer_tests; mod random_project_collaboration_tests; diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs new file mode 100644 index 0000000000..d7acae3995 --- /dev/null +++ b/crates/collab/src/tests/following_tests.rs @@ -0,0 +1,1189 @@ +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; +use call::ActiveCall; +use editor::{Editor, ExcerptRange, MultiBuffer}; +use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle}; +use live_kit_client::MacOSDisplay; +use serde_json::json; +use std::sync::Arc; +use workspace::{ + dock::{test::TestPanel, DockPosition}, + item::{test::TestItem, ItemHandle as _}, + shared_screen::SharedScreen, + SplitDirection, Workspace, +}; + +#[gpui::test(iterations = 10)] +async fn test_basic_following( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, + cx_d: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + let client_d = server.create_client(cx_d, "user_d").await; + server + .create_room(&mut [ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + (&client_d, cx_d), + ]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one\none\none", + "2.txt": "two\ntwo\ntwo", + "3.txt": "three\nthree\nthree", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let window_a = client_a.build_workspace(&project_a, cx_a); + let workspace_a = window_a.root(cx_a); + let window_b = client_b.build_workspace(&project_b, cx_b); + let workspace_b = window_b.root(cx_b); + + // Client A opens some editors. + let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); + let editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_a2 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B opens an editor. + let editor_b1 = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let peer_id_a = client_a.peer_id().unwrap(); + let peer_id_b = client_b.peer_id().unwrap(); + let peer_id_c = client_c.peer_id().unwrap(); + let peer_id_d = client_d.peer_id().unwrap(); + + // Client A updates their selections in those editors + editor_a1.update(cx_a, |editor, cx| { + editor.handle_input("a", cx); + editor.handle_input("b", cx); + editor.handle_input("c", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![3..2]); + }); + editor_a2.update(cx_a, |editor, cx| { + editor.handle_input("d", cx); + editor.handle_input("e", cx); + editor.select_left(&Default::default(), cx); + assert_eq!(editor.selections.ranges(cx), vec![2..1]); + }); + + // When client B starts following client A, all visible view states are replicated to client B. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(peer_id_a, cx).unwrap() + }) + .await + .unwrap(); + + cx_c.foreground().run_until_parked(); + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + cx_b.read(|cx| editor_b2.project_path(cx)), + Some((worktree_id, "2.txt").into()) + ); + assert_eq!( + editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![2..1] + ); + assert_eq!( + editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), + vec![3..2] + ); + + cx_c.foreground().run_until_parked(); + let active_call_c = cx_c.read(ActiveCall::global); + let project_c = client_c.build_remote_project(project_id, cx_c).await; + let window_c = client_c.build_workspace(&project_c, cx_c); + let workspace_c = window_c.root(cx_c); + active_call_c + .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) + .await + .unwrap(); + drop(project_c); + + // Client C also follows client A. + workspace_c + .update(cx_c, |workspace, cx| { + workspace.toggle_follow(peer_id_a, cx).unwrap() + }) + .await + .unwrap(); + + cx_d.foreground().run_until_parked(); + let active_call_d = cx_d.read(ActiveCall::global); + let project_d = client_d.build_remote_project(project_id, cx_d).await; + let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d); + active_call_d + .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) + .await + .unwrap(); + drop(project_d); + + // All clients see that clients B and C are following client A. + cx_c.foreground().run_until_parked(); + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_a, project_id), + &[peer_id_b, peer_id_c], + "checking followers for A as {name}" + ); + }); + } + + // Client C unfollows client A. + workspace_c.update(cx_c, |workspace, cx| { + workspace.toggle_follow(peer_id_a, cx); + }); + + // All clients see that clients B is following client A. + cx_c.foreground().run_until_parked(); + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_a, project_id), + &[peer_id_b], + "checking followers for A as {name}" + ); + }); + } + + // Client C re-follows client A. + workspace_c.update(cx_c, |workspace, cx| { + workspace.toggle_follow(peer_id_a, cx); + }); + + // All clients see that clients B and C are following client A. + cx_c.foreground().run_until_parked(); + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_a, project_id), + &[peer_id_b, peer_id_c], + "checking followers for A as {name}" + ); + }); + } + + // Client D follows client C. + workspace_d + .update(cx_d, |workspace, cx| { + workspace.toggle_follow(peer_id_c, cx).unwrap() + }) + .await + .unwrap(); + + // All clients see that D is following C + cx_d.foreground().run_until_parked(); + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_c, project_id), + &[peer_id_d], + "checking followers for C as {name}" + ); + }); + } + + // Client C closes the project. + window_c.remove(cx_c); + cx_c.drop_last(workspace_c); + + // Clients A and B see that client B is following A, and client C is not present in the followers. + cx_c.foreground().run_until_parked(); + for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_a, project_id), + &[peer_id_b], + "checking followers for A as {name}" + ); + }); + } + + // All clients see that no-one is following C + for (name, active_call, cx) in [ + ("A", &active_call_a, &cx_a), + ("B", &active_call_b, &cx_b), + ("C", &active_call_c, &cx_c), + ("D", &active_call_d, &cx_d), + ] { + active_call.read_with(*cx, |call, cx| { + let room = call.room().unwrap().read(cx); + assert_eq!( + room.followers_for(peer_id_c, project_id), + &[], + "checking followers for C as {name}" + ); + }); + } + + // When client A activates a different editor, client B does so as well. + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(&editor_a1, cx) + }); + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); + }); + + // When client A opens a multibuffer, client B does so as well. + let multibuffer_a = cx_a.add_model(|cx| { + let buffer_a1 = project_a.update(cx, |project, cx| { + project + .get_open_buffer(&(worktree_id, "1.txt").into(), cx) + .unwrap() + }); + let buffer_a2 = project_a.update(cx, |project, cx| { + project + .get_open_buffer(&(worktree_id, "2.txt").into(), cx) + .unwrap() + }); + let mut result = MultiBuffer::new(0); + result.push_excerpts( + buffer_a1, + [ExcerptRange { + context: 0..3, + primary: None, + }], + cx, + ); + result.push_excerpts( + buffer_a2, + [ExcerptRange { + context: 4..7, + primary: None, + }], + cx, + ); + result + }); + let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { + let editor = + cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); + workspace.add_item(Box::new(editor.clone()), cx); + editor + }); + deterministic.run_until_parked(); + let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + assert_eq!( + multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)), + multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)), + ); + + // When client A navigates back and forth, client B does so as well. + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); + }); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_back(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id()); + }); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.go_forward(workspace.active_pane().downgrade(), cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); + }); + + // Changes to client A's editor are reflected on client B. + editor_a1.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); + }); + deterministic.run_until_parked(); + editor_b1.read_with(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); + }); + + editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); + deterministic.run_until_parked(); + editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); + + editor_a1.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([3..3])); + editor.set_scroll_position(vec2f(0., 100.), cx); + }); + deterministic.run_until_parked(); + editor_b1.read_with(cx_b, |editor, cx| { + assert_eq!(editor.selections.ranges(cx), &[3..3]); + }); + + // After unfollowing, client B stops receiving updates from client A. + workspace_b.update(cx_b, |workspace, cx| { + workspace.unfollow(&workspace.active_pane().clone(), cx) + }); + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(&editor_a2, cx) + }); + deterministic.run_until_parked(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + editor_b1.id() + ); + + // Client A starts following client B. + workspace_a + .update(cx_a, |workspace, cx| { + workspace.toggle_follow(peer_id_b, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + Some(peer_id_b) + ); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + editor_a1.id() + ); + + // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. + let display = MacOSDisplay::new(); + active_call_b + .update(cx_b, |call, cx| call.set_location(None, cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| { + call.room().unwrap().update(cx, |room, cx| { + room.set_display_sources(vec![display.clone()]); + room.share_screen(cx) + }) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| { + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item isn't a shared screen") + }); + + // Client B activates Zed again, which causes the previous editor to become focused again. + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + workspace_a.read_with(cx_a, |workspace, cx| { + assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id()) + }); + + // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.activate_item(&multibuffer_editor_b, cx) + }); + deterministic.run_until_parked(); + workspace_a.read_with(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().id(), + multibuffer_editor_a.id() + ) + }); + + // Client B activates a panel, and the previously-opened screen-sharing item gets activated. + let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left)); + workspace_b.update(cx_b, |workspace, cx| { + workspace.add_panel(panel, cx); + workspace.toggle_panel_focus::(cx); + }); + deterministic.run_until_parked(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + shared_screen.id() + ); + + // Toggling the focus back to the pane causes client A to return to the multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + deterministic.run_until_parked(); + workspace_a.read_with(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().id(), + multibuffer_editor_a.id() + ) + }); + + // Client B activates an item that doesn't implement following, + // so the previously-opened screen-sharing item gets activated. + let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new()); + workspace_b.update(cx_b, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item(Box::new(unfollowable_item), true, true, None, cx) + }) + }); + deterministic.run_until_parked(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + shared_screen.id() + ); + + // Following interrupts when client B disconnects. + client_b.disconnect(&cx_b.to_async()); + deterministic.advance_clock(RECONNECT_TIMEOUT); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + None + ); +} + +#[gpui::test] +async fn test_following_tab_order( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); + + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); + + let client_b_id = project_a.read_with(cx_a, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + + //Open 1, 3 in that order on client A + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap(); + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "3.txt"), None, true, cx) + }) + .await + .unwrap(); + + let pane_paths = |pane: &ViewHandle, cx: &mut TestAppContext| { + pane.update(cx, |pane, cx| { + pane.items() + .map(|item| { + item.project_path(cx) + .unwrap() + .path + .to_str() + .unwrap() + .to_owned() + }) + .collect::>() + }) + }; + + //Verify that the tabs opened in the order we expect + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]); + + //Follow client B as client A + workspace_a + .update(cx_a, |workspace, cx| { + workspace.toggle_follow(client_b_id, cx).unwrap() + }) + .await + .unwrap(); + + //Open just 2 on client B + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Verify that newly opened followed file is at the end + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); + + //Open just 1 on client B + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap(); + assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]); + deterministic.run_until_parked(); + + // Verify that following into 1 did not reorder + assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); +} + +#[gpui::test(iterations = 10)] +async fn test_peers_following_each_other( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + // Client A shares a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + "4.txt": "four", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + // Client B joins the project. + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + // Client A opens some editors. + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); + let _editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B opens an editor. + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); + let _editor_b1 = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Clients A and B follow each other in split panes + workspace_a.update(cx_a, |workspace, cx| { + workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); + }); + workspace_a + .update(cx_a, |workspace, cx| { + assert_ne!(*workspace.active_pane(), pane_a1); + let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); + workspace.toggle_follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); + }); + workspace_b + .update(cx_b, |workspace, cx| { + assert_ne!(*workspace.active_pane(), pane_b1); + let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); + workspace.toggle_follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_next_pane(cx); + }); + // Wait for focus effects to be fully flushed + workspace_a.update(cx_a, |workspace, _| { + assert_eq!(*workspace.active_pane(), pane_a1); + }); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "3.txt"), None, true, cx) + }) + .await + .unwrap(); + workspace_b.update(cx_b, |workspace, cx| { + workspace.activate_next_pane(cx); + }); + + workspace_b + .update(cx_b, |workspace, cx| { + assert_eq!(*workspace.active_pane(), pane_b1); + workspace.open_path((worktree_id, "4.txt"), None, true, cx) + }) + .await + .unwrap(); + cx_a.foreground().run_until_parked(); + + // Ensure leader updates don't change the active pane of followers + workspace_a.read_with(cx_a, |workspace, _| { + assert_eq!(*workspace.active_pane(), pane_a1); + }); + workspace_b.read_with(cx_b, |workspace, _| { + assert_eq!(*workspace.active_pane(), pane_b1); + }); + + // Ensure peers following each other doesn't cause an infinite loop. + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .project_path(cx)), + Some((worktree_id, "3.txt").into()) + ); + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "3.txt").into()) + ); + workspace.activate_next_pane(cx); + }); + + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "4.txt").into()) + ); + }); + + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "4.txt").into()) + ); + workspace.activate_next_pane(cx); + }); + + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "3.txt").into()) + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_auto_unfollowing( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + // 2 clients connect to a server. + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + // Client A shares a project. + client_a + .fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + // Client A opens some editors. + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let _editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B starts following client A. + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); + let leader_id = project_b.read_with(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + + // When client B moves, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B edits, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B scrolls, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| { + editor.set_scroll_position(vec2f(0., 3.), cx) + }); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(leader_id, cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B activates a different pane, it continues following client A in the original pane. + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx) + }); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B activates a different item in the original pane, it automatically stops following client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), None, true, cx) + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); +} + +#[gpui::test(iterations = 10)] +async fn test_peers_simultaneously_following_each_other( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a.fs().insert_tree("/a", json!({})).await; + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + + deterministic.run_until_parked(); + let client_a_id = project_b.read_with(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + let client_b_id = project_a.read_with(cx_a, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + + let a_follow_b = workspace_a.update(cx_a, |workspace, cx| { + workspace.toggle_follow(client_b_id, cx).unwrap() + }); + let b_follow_a = workspace_b.update(cx_b, |workspace, cx| { + workspace.toggle_follow(client_a_id, cx).unwrap() + }); + + futures::try_join!(a_follow_b, b_follow_a).unwrap(); + workspace_a.read_with(cx_a, |workspace, _| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_b_id) + ); + }); + workspace_b.read_with(cx_b, |workspace, _| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a_id) + ); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_following_across_workspaces( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + // a and b join a channel/call + // a shares project 1 + // b shares project 2 + // + // + // b joins project 1 + // + // test: when a is in project 2 and b clicks follow (from unshared project), b should open project 2 and follow a + // test: when a is in project 1 and b clicks follow, b should open project 1 and follow a + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + cx_a.update(editor::init); + cx_b.update(editor::init); + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "w.rs": "", + "x.rs": "", + }), + ) + .await; + + client_b + .fs() + .insert_tree( + "/b", + json!({ + "y.rs": "", + "z.rs": "", + }), + ) + .await; + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await; + let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await; + + let project_a_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + .await + .unwrap(); + + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id_a, "w.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + deterministic.run_until_parked(); + assert_eq!(cx_b.windows().len(), 1); + + workspace_b.update(cx_b, |workspace, cx| { + workspace + .follow(client_a.peer_id().unwrap(), cx) + .unwrap() + .detach() + }); + + deterministic.run_until_parked(); + let workspace_b_project_a = cx_b + .windows() + .iter() + .max_by_key(|window| window.id()) + .unwrap() + .downcast::() + .unwrap() + .root(cx_b); + + // assert that b is following a in project a in w.rs + workspace_b_project_a.update(cx_b, |workspace, _| { + assert!(workspace.is_being_followed(client_a.peer_id().unwrap())); + assert_eq!( + client_a.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + }); + + // assert that there are no share notifications open +} diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index b17b7b3fc2..4008a941dd 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -7,14 +7,11 @@ use client::{User, RECEIVE_TIMEOUT}; use collections::{HashMap, HashSet}; use editor::{ test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, - ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo, + ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo, }; use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions}; use futures::StreamExt as _; -use gpui::{ - executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle, - TestAppContext, ViewHandle, -}; +use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, TestAppContext}; use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, Formatter, InlayHintSettings}, @@ -38,12 +35,7 @@ use std::{ }, }; use unindent::Unindent as _; -use workspace::{ - dock::{test::TestPanel, DockPosition}, - item::{test::TestItem, ItemHandle as _}, - shared_screen::SharedScreen, - SplitDirection, Workspace, -}; +use workspace::Workspace; #[ctor::ctor] fn init_logger() { @@ -6387,547 +6379,6 @@ async fn test_contact_requests( } } -#[gpui::test(iterations = 10)] -async fn test_basic_following( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, - cx_c: &mut TestAppContext, - cx_d: &mut TestAppContext, -) { - deterministic.forbid_parking(); - - let mut server = TestServer::start(&deterministic).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; - let client_d = server.create_client(cx_d, "user_d").await; - server - .create_room(&mut [ - (&client_a, cx_a), - (&client_b, cx_b), - (&client_c, cx_c), - (&client_d, cx_d), - ]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "1.txt": "one\none\none", - "2.txt": "two\ntwo\ntwo", - "3.txt": "three\nthree\nthree", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - let window_a = client_a.build_workspace(&project_a, cx_a); - let workspace_a = window_a.root(cx_a); - let window_b = client_b.build_workspace(&project_b, cx_b); - let workspace_b = window_b.root(cx_b); - - // Client A opens some editors. - let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); - let editor_a1 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - let editor_a2 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Client B opens an editor. - let editor_b1 = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - let peer_id_a = client_a.peer_id().unwrap(); - let peer_id_b = client_b.peer_id().unwrap(); - let peer_id_c = client_c.peer_id().unwrap(); - let peer_id_d = client_d.peer_id().unwrap(); - - // Client A updates their selections in those editors - editor_a1.update(cx_a, |editor, cx| { - editor.handle_input("a", cx); - editor.handle_input("b", cx); - editor.handle_input("c", cx); - editor.select_left(&Default::default(), cx); - assert_eq!(editor.selections.ranges(cx), vec![3..2]); - }); - editor_a2.update(cx_a, |editor, cx| { - editor.handle_input("d", cx); - editor.handle_input("e", cx); - editor.select_left(&Default::default(), cx); - assert_eq!(editor.selections.ranges(cx), vec![2..1]); - }); - - // When client B starts following client A, all visible view states are replicated to client B. - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx).unwrap() - }) - .await - .unwrap(); - - cx_c.foreground().run_until_parked(); - let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }); - assert_eq!( - cx_b.read(|cx| editor_b2.project_path(cx)), - Some((worktree_id, "2.txt").into()) - ); - assert_eq!( - editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), - vec![2..1] - ); - assert_eq!( - editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)), - vec![3..2] - ); - - cx_c.foreground().run_until_parked(); - let active_call_c = cx_c.read(ActiveCall::global); - let project_c = client_c.build_remote_project(project_id, cx_c).await; - let window_c = client_c.build_workspace(&project_c, cx_c); - let workspace_c = window_c.root(cx_c); - active_call_c - .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) - .await - .unwrap(); - drop(project_c); - - // Client C also follows client A. - workspace_c - .update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx).unwrap() - }) - .await - .unwrap(); - - cx_d.foreground().run_until_parked(); - let active_call_d = cx_d.read(ActiveCall::global); - let project_d = client_d.build_remote_project(project_id, cx_d).await; - let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d); - active_call_d - .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) - .await - .unwrap(); - drop(project_d); - - // All clients see that clients B and C are following client A. - cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b, peer_id_c], - "checking followers for A as {name}" - ); - }); - } - - // Client C unfollows client A. - workspace_c.update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx); - }); - - // All clients see that clients B is following client A. - cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b], - "checking followers for A as {name}" - ); - }); - } - - // Client C re-follows client A. - workspace_c.update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx); - }); - - // All clients see that clients B and C are following client A. - cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b, peer_id_c], - "checking followers for A as {name}" - ); - }); - } - - // Client D follows client C. - workspace_d - .update(cx_d, |workspace, cx| { - workspace.toggle_follow(peer_id_c, cx).unwrap() - }) - .await - .unwrap(); - - // All clients see that D is following C - cx_d.foreground().run_until_parked(); - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_c, project_id), - &[peer_id_d], - "checking followers for C as {name}" - ); - }); - } - - // Client C closes the project. - window_c.remove(cx_c); - cx_c.drop_last(workspace_c); - - // Clients A and B see that client B is following A, and client C is not present in the followers. - cx_c.foreground().run_until_parked(); - for (name, active_call, cx) in [("A", &active_call_a, &cx_a), ("B", &active_call_b, &cx_b)] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_a, project_id), - &[peer_id_b], - "checking followers for A as {name}" - ); - }); - } - - // All clients see that no-one is following C - for (name, active_call, cx) in [ - ("A", &active_call_a, &cx_a), - ("B", &active_call_b, &cx_b), - ("C", &active_call_c, &cx_c), - ("D", &active_call_d, &cx_d), - ] { - active_call.read_with(*cx, |call, cx| { - let room = call.room().unwrap().read(cx); - assert_eq!( - room.followers_for(peer_id_c, project_id), - &[], - "checking followers for C as {name}" - ); - }); - } - - // When client A activates a different editor, client B does so as well. - workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_item(&editor_a1, cx) - }); - deterministic.run_until_parked(); - workspace_b.read_with(cx_b, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); - }); - - // When client A opens a multibuffer, client B does so as well. - let multibuffer_a = cx_a.add_model(|cx| { - let buffer_a1 = project_a.update(cx, |project, cx| { - project - .get_open_buffer(&(worktree_id, "1.txt").into(), cx) - .unwrap() - }); - let buffer_a2 = project_a.update(cx, |project, cx| { - project - .get_open_buffer(&(worktree_id, "2.txt").into(), cx) - .unwrap() - }); - let mut result = MultiBuffer::new(0); - result.push_excerpts( - buffer_a1, - [ExcerptRange { - context: 0..3, - primary: None, - }], - cx, - ); - result.push_excerpts( - buffer_a2, - [ExcerptRange { - context: 4..7, - primary: None, - }], - cx, - ); - result - }); - let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| { - let editor = - cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx)); - workspace.add_item(Box::new(editor.clone()), cx); - editor - }); - deterministic.run_until_parked(); - let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }); - assert_eq!( - multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)), - multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)), - ); - - // When client A navigates back and forth, client B does so as well. - workspace_a - .update(cx_a, |workspace, cx| { - workspace.go_back(workspace.active_pane().downgrade(), cx) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - workspace_b.read_with(cx_b, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); - }); - - workspace_a - .update(cx_a, |workspace, cx| { - workspace.go_back(workspace.active_pane().downgrade(), cx) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - workspace_b.read_with(cx_b, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id()); - }); - - workspace_a - .update(cx_a, |workspace, cx| { - workspace.go_forward(workspace.active_pane().downgrade(), cx) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - workspace_b.read_with(cx_b, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id()); - }); - - // Changes to client A's editor are reflected on client B. - editor_a1.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2])); - }); - deterministic.run_until_parked(); - editor_b1.read_with(cx_b, |editor, cx| { - assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); - }); - - editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); - deterministic.run_until_parked(); - editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO")); - - editor_a1.update(cx_a, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([3..3])); - editor.set_scroll_position(vec2f(0., 100.), cx); - }); - deterministic.run_until_parked(); - editor_b1.read_with(cx_b, |editor, cx| { - assert_eq!(editor.selections.ranges(cx), &[3..3]); - }); - - // After unfollowing, client B stops receiving updates from client A. - workspace_b.update(cx_b, |workspace, cx| { - workspace.unfollow(&workspace.active_pane().clone(), cx) - }); - workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_item(&editor_a2, cx) - }); - deterministic.run_until_parked(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .id()), - editor_b1.id() - ); - - // Client A starts following client B. - workspace_a - .update(cx_a, |workspace, cx| { - workspace.toggle_follow(peer_id_b, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), - Some(peer_id_b) - ); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .id()), - editor_a1.id() - ); - - // Client B activates an external window, which causes a new screen-sharing item to be added to the pane. - let display = MacOSDisplay::new(); - active_call_b - .update(cx_b, |call, cx| call.set_location(None, cx)) - .await - .unwrap(); - active_call_b - .update(cx_b, |call, cx| { - call.room().unwrap().update(cx, |room, cx| { - room.set_display_sources(vec![display.clone()]); - room.share_screen(cx) - }) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| { - workspace - .active_item(cx) - .expect("no active item") - .downcast::() - .expect("active item isn't a shared screen") - }); - - // Client B activates Zed again, which causes the previous editor to become focused again. - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - deterministic.run_until_parked(); - workspace_a.read_with(cx_a, |workspace, cx| { - assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id()) - }); - - // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer. - workspace_b.update(cx_b, |workspace, cx| { - workspace.activate_item(&multibuffer_editor_b, cx) - }); - deterministic.run_until_parked(); - workspace_a.read_with(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().id(), - multibuffer_editor_a.id() - ) - }); - - // Client B activates a panel, and the previously-opened screen-sharing item gets activated. - let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left)); - workspace_b.update(cx_b, |workspace, cx| { - workspace.add_panel(panel, cx); - workspace.toggle_panel_focus::(cx); - }); - deterministic.run_until_parked(); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .id()), - shared_screen.id() - ); - - // Toggling the focus back to the pane causes client A to return to the multibuffer. - workspace_b.update(cx_b, |workspace, cx| { - workspace.toggle_panel_focus::(cx); - }); - deterministic.run_until_parked(); - workspace_a.read_with(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().id(), - multibuffer_editor_a.id() - ) - }); - - // Client B activates an item that doesn't implement following, - // so the previously-opened screen-sharing item gets activated. - let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new()); - workspace_b.update(cx_b, |workspace, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item(Box::new(unfollowable_item), true, true, None, cx) - }) - }); - deterministic.run_until_parked(); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .id()), - shared_screen.id() - ); - - // Following interrupts when client B disconnects. - client_b.disconnect(&cx_b.to_async()); - deterministic.advance_clock(RECONNECT_TIMEOUT); - assert_eq!( - workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), - None - ); -} - #[gpui::test(iterations = 10)] async fn test_join_call_after_screen_was_shared( deterministic: Arc, @@ -7021,526 +6472,6 @@ async fn test_join_call_after_screen_was_shared( }); } -#[gpui::test] -async fn test_following_tab_order( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - let mut server = TestServer::start(&deterministic).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - client_a - .fs() - .insert_tree( - "/a", - json!({ - "1.txt": "one", - "2.txt": "two", - "3.txt": "three", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); - - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); - - let client_b_id = project_a.read_with(cx_a, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); - - //Open 1, 3 in that order on client A - workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap(); - workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "3.txt"), None, true, cx) - }) - .await - .unwrap(); - - let pane_paths = |pane: &ViewHandle, cx: &mut TestAppContext| { - pane.update(cx, |pane, cx| { - pane.items() - .map(|item| { - item.project_path(cx) - .unwrap() - .path - .to_str() - .unwrap() - .to_owned() - }) - .collect::>() - }) - }; - - //Verify that the tabs opened in the order we expect - assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]); - - //Follow client B as client A - workspace_a - .update(cx_a, |workspace, cx| { - workspace.toggle_follow(client_b_id, cx).unwrap() - }) - .await - .unwrap(); - - //Open just 2 on client B - workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) - }) - .await - .unwrap(); - deterministic.run_until_parked(); - - // Verify that newly opened followed file is at the end - assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); - - //Open just 1 on client B - workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap(); - assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]); - deterministic.run_until_parked(); - - // Verify that following into 1 did not reorder - assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]); -} - -#[gpui::test(iterations = 10)] -async fn test_peers_following_each_other( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - deterministic.forbid_parking(); - let mut server = TestServer::start(&deterministic).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - // Client A shares a project. - client_a - .fs() - .insert_tree( - "/a", - json!({ - "1.txt": "one", - "2.txt": "two", - "3.txt": "three", - "4.txt": "four", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - // Client B joins the project. - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - // Client A opens some editors. - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); - let _editor_a1 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Client B opens an editor. - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); - let _editor_b1 = workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Clients A and B follow each other in split panes - workspace_a.update(cx_a, |workspace, cx| { - workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); - }); - workspace_a - .update(cx_a, |workspace, cx| { - assert_ne!(*workspace.active_pane(), pane_a1); - let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - workspace_b.update(cx_b, |workspace, cx| { - workspace.split_and_clone(workspace.active_pane().clone(), SplitDirection::Right, cx); - }); - workspace_b - .update(cx_b, |workspace, cx| { - assert_ne!(*workspace.active_pane(), pane_b1); - let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - - workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_next_pane(cx); - }); - // Wait for focus effects to be fully flushed - workspace_a.update(cx_a, |workspace, _| { - assert_eq!(*workspace.active_pane(), pane_a1); - }); - - workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "3.txt"), None, true, cx) - }) - .await - .unwrap(); - workspace_b.update(cx_b, |workspace, cx| { - workspace.activate_next_pane(cx); - }); - - workspace_b - .update(cx_b, |workspace, cx| { - assert_eq!(*workspace.active_pane(), pane_b1); - workspace.open_path((worktree_id, "4.txt"), None, true, cx) - }) - .await - .unwrap(); - cx_a.foreground().run_until_parked(); - - // Ensure leader updates don't change the active pane of followers - workspace_a.read_with(cx_a, |workspace, _| { - assert_eq!(*workspace.active_pane(), pane_a1); - }); - workspace_b.read_with(cx_b, |workspace, _| { - assert_eq!(*workspace.active_pane(), pane_b1); - }); - - // Ensure peers following each other doesn't cause an infinite loop. - assert_eq!( - workspace_a.read_with(cx_a, |workspace, cx| workspace - .active_item(cx) - .unwrap() - .project_path(cx)), - Some((worktree_id, "3.txt").into()) - ); - workspace_a.update(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "3.txt").into()) - ); - workspace.activate_next_pane(cx); - }); - - workspace_a.update(cx_a, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "4.txt").into()) - ); - }); - - workspace_b.update(cx_b, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "4.txt").into()) - ); - workspace.activate_next_pane(cx); - }); - - workspace_b.update(cx_b, |workspace, cx| { - assert_eq!( - workspace.active_item(cx).unwrap().project_path(cx), - Some((worktree_id, "3.txt").into()) - ); - }); -} - -#[gpui::test(iterations = 10)] -async fn test_auto_unfollowing( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - deterministic.forbid_parking(); - - // 2 clients connect to a server. - let mut server = TestServer::start(&deterministic).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - // Client A shares a project. - client_a - .fs() - .insert_tree( - "/a", - json!({ - "1.txt": "one", - "2.txt": "two", - "3.txt": "three", - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - active_call_a - .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) - .await - .unwrap(); - - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; - active_call_b - .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) - .await - .unwrap(); - - // Client A opens some editors. - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let _editor_a1 = workspace_a - .update(cx_a, |workspace, cx| { - workspace.open_path((worktree_id, "1.txt"), None, true, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Client B starts following client A. - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); - let leader_id = project_b.read_with(cx_b, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }); - - // When client B moves, it automatically stops following client A. - editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - None - ); - - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - // When client B edits, it automatically stops following client A. - editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - None - ); - - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - // When client B scrolls, it automatically stops following client A. - editor_b2.update(cx_b, |editor, cx| { - editor.set_scroll_position(vec2f(0., 3.), cx) - }); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - None - ); - - workspace_b - .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - // When client B activates a different pane, it continues following client A in the original pane. - workspace_b.update(cx_b, |workspace, cx| { - workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx) - }); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - Some(leader_id) - ); - - // When client B activates a different item in the original pane, it automatically stops following client A. - workspace_b - .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "2.txt"), None, true, cx) - }) - .await - .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), - None - ); -} - -#[gpui::test(iterations = 10)] -async fn test_peers_simultaneously_following_each_other( - deterministic: Arc, - cx_a: &mut TestAppContext, - cx_b: &mut TestAppContext, -) { - deterministic.forbid_parking(); - - let mut server = TestServer::start(&deterministic).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - - cx_a.update(editor::init); - cx_b.update(editor::init); - - client_a.fs().insert_tree("/a", json!({})).await; - let (project_a, _) = client_a.build_local_project("/a", cx_a).await; - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - - let project_b = client_b.build_remote_project(project_id, cx_b).await; - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - - deterministic.run_until_parked(); - let client_a_id = project_b.read_with(cx_b, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); - let client_b_id = project_a.read_with(cx_a, |project, _| { - project.collaborators().values().next().unwrap().peer_id - }); - - let a_follow_b = workspace_a.update(cx_a, |workspace, cx| { - workspace.toggle_follow(client_b_id, cx).unwrap() - }); - let b_follow_a = workspace_b.update(cx_b, |workspace, cx| { - workspace.toggle_follow(client_a_id, cx).unwrap() - }); - - futures::try_join!(a_follow_b, b_follow_a).unwrap(); - workspace_a.read_with(cx_a, |workspace, _| { - assert_eq!( - workspace.leader_for_pane(workspace.active_pane()), - Some(client_b_id) - ); - }); - workspace_b.read_with(cx_b, |workspace, _| { - assert_eq!( - workspace.leader_for_pane(workspace.active_pane()), - Some(client_a_id) - ); - }); -} - #[gpui::test(iterations = 10)] async fn test_on_input_format_from_host_to_guest( deterministic: Arc, diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 546b8ef407..879b375cd4 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1090,55 +1090,30 @@ impl CollabTitlebarItem { }, ); - match (replica_id, location) { - // If the user's location isn't known, do nothing. - (_, None) => content.into_any(), - - // If the user is not in this project, but is in another share project, - // join that project. - (None, Some(ParticipantLocation::SharedProject { project_id })) => content - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - let app_state = workspace.read(cx).app_state().clone(); - workspace::join_remote_project(project_id, user_id, app_state, cx) - .detach_and_log_err(cx); - } - }) - .with_tooltip::( - peer_id.as_u64() as usize, - format!("Follow {} into external project", user.github_login), - Some(Box::new(FollowNextCollaborator)), - theme.tooltip.clone(), - cx, - ) - .into_any(), - - // Otherwise, follow the user in the current window. - _ => content - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, item, cx| { - if let Some(workspace) = item.workspace.upgrade(cx) { - if let Some(task) = workspace - .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx)) - { - task.detach_and_log_err(cx); - } - } - }) - .with_tooltip::( - peer_id.as_u64() as usize, - if self_following { - format!("Unfollow {}", user.github_login) - } else { - format!("Follow {}", user.github_login) - }, - Some(Box::new(FollowNextCollaborator)), - theme.tooltip.clone(), - cx, - ) - .into_any(), + if Some(peer_id) == self_peer_id { + return content.into_any(); } + + content + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + let Some(workspace) = this.workspace.upgrade(cx) else { + return; + }; + if let Some(task) = + workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx)) + { + task.detach_and_log_err(cx); + } + }) + .with_tooltip::( + peer_id.as_u64() as usize, + format!("Follow {}", user.github_login), + Some(Box::new(FollowNextCollaborator)), + theme.tooltip.clone(), + cx, + ) + .into_any() } fn location_style( diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 40adeccd11..c12cb261c8 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -222,7 +222,7 @@ impl Member { |_, _| { Label::new( format!( - "Follow {} on their active project", + "Follow {} to their active project", leader_user.github_login, ), theme diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8d9a4c1550..38773fb8cc 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2529,6 +2529,7 @@ impl Workspace { if let Some(prev_leader_id) = self.unfollow(&pane, cx) { if leader_id == prev_leader_id { + dbg!("oh no!"); return None; } } @@ -2613,16 +2614,50 @@ impl Workspace { leader_id: PeerId, cx: &mut ViewContext, ) -> Option>> { + let room = ActiveCall::global(cx).read(cx).room()?.read(cx); + let project = self.project.read(cx); + + let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else { + dbg!("no remote participant yet..."); + return None; + }; + + let other_project_id = match remote_participant.location { + call::ParticipantLocation::External => None, + call::ParticipantLocation::UnsharedProject => None, + call::ParticipantLocation::SharedProject { project_id } => { + if Some(project_id) == project.remote_id() { + None + } else { + Some(project_id) + } + } + }; + dbg!(other_project_id); + + // if they are active in another project, follow there. + if let Some(project_id) = other_project_id { + let app_state = self.app_state.clone(); + return Some(crate::join_remote_project( + project_id, + remote_participant.user.id, + app_state, + cx, + )); + } + + // if you're already following, find the right pane and focus it. for (existing_leader_id, states_by_pane) in &mut self.follower_states_by_leader { if leader_id == *existing_leader_id { for (pane, _) in states_by_pane { + dbg!("focusing pane"); cx.focus(pane); return None; } } } - // not currently following, so follow. + // Otherwise, follow. self.toggle_follow(leader_id, cx) } @@ -4214,6 +4249,7 @@ pub fn join_remote_project( app_state: Arc, cx: &mut AppContext, ) -> Task> { + dbg!("huh??"); cx.spawn(|mut cx| async move { let existing_workspace = cx .windows() @@ -4232,8 +4268,10 @@ pub fn join_remote_project( .flatten(); let workspace = if let Some(existing_workspace) = existing_workspace { + dbg!("huh"); existing_workspace } else { + dbg!("huh/"); let active_call = cx.read(ActiveCall::global); let room = active_call .read_with(&cx, |call, _| call.room().cloned()) @@ -4249,6 +4287,7 @@ pub fn join_remote_project( }) .await?; + dbg!("huh//"); let window_bounds_override = window_bounds_env_override(&cx); let window = cx.add_window( (app_state.build_window_options)( @@ -4271,6 +4310,7 @@ pub fn join_remote_project( workspace.downgrade() }; + dbg!("huh///"); workspace.window().activate(&mut cx); cx.platform().activate(true); @@ -4293,12 +4333,12 @@ pub fn join_remote_project( Some(collaborator.peer_id) }); + dbg!(follow_peer_id); + if let Some(follow_peer_id) = follow_peer_id { - if !workspace.is_being_followed(follow_peer_id) { - workspace - .toggle_follow(follow_peer_id, cx) - .map(|follow| follow.detach_and_log_err(cx)); - } + workspace + .follow(follow_peer_id, cx) + .map(|follow| follow.detach_and_log_err(cx)); } } })?; From e5e63ed20152a1326f453f013ca6b2e6802c5156 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sun, 1 Oct 2023 23:38:30 -0400 Subject: [PATCH 33/45] Add Nushell support to venv activation --- assets/settings/default.json | 2 +- crates/project/src/terminals.rs | 1 + crates/terminal/src/terminal_settings.rs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 7785f5dd44..8fb73a2ecb 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -361,7 +361,7 @@ ".venv", "venv" ], - // Can also be 'csh' and 'fish' + // Can also be 'csh', 'fish', and `nushell` "activate_script": "default" } } diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 68a0431316..a47fb39105 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -84,6 +84,7 @@ impl Project { terminal_settings::ActivateScript::Default => "activate", terminal_settings::ActivateScript::Csh => "activate.csh", terminal_settings::ActivateScript::Fish => "activate.fish", + terminal_settings::ActivateScript::Nushell => "activate.nu", }; for virtual_environment_name in settings.directories { diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index e0649ebf65..0e8599e647 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -69,6 +69,7 @@ pub enum ActivateScript { Default, Csh, Fish, + Nushell, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] From a785eb914140bd83e1d0780929261d9f46157848 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 2 Oct 2023 15:24:09 +0200 Subject: [PATCH 34/45] auto-update: Link to the current release's changelog, not the latest one (#3076) An user complained in zed-industries/community#2093 that we always link to the latest release changelog, not the one that they've just updated to. Release Notes: - Fixed changelog link in update notification always leading to the latest release changelog, not the one that was updated to. Fixes zed-industries/community#2093. --- crates/auto_update/src/auto_update.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 822886b580..0d537b882a 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -115,13 +115,15 @@ pub fn check(_: &Check, cx: &mut AppContext) { fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { if let Some(auto_updater) = AutoUpdater::get(cx) { - let server_url = &auto_updater.read(cx).server_url; + let auto_updater = auto_updater.read(cx); + let server_url = &auto_updater.server_url; + let current_version = auto_updater.current_version; let latest_release_url = if cx.has_global::() && *cx.global::() == ReleaseChannel::Preview { - format!("{server_url}/releases/preview/latest") + format!("{server_url}/releases/preview/{current_version}") } else { - format!("{server_url}/releases/stable/latest") + format!("{server_url}/releases/stable/{current_version}") }; cx.platform().open_url(&latest_release_url); } From bf5d9e32240e5752630988fc99df5f7c82031660 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 2 Oct 2023 17:50:52 +0200 Subject: [PATCH 35/45] Sort matches before processing them --- crates/assistant/src/prompts.rs | 65 ++++++++++----------- crates/zed/src/languages/rust/embedding.scm | 3 + 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 2451369a18..bf041dff52 100644 --- a/crates/assistant/src/prompts.rs +++ b/crates/assistant/src/prompts.rs @@ -1,8 +1,8 @@ use crate::codegen::CodegenKind; use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; -use std::cmp; +use std::cmp::{self, Reverse}; +use std::fmt::Write; use std::ops::Range; -use std::{fmt::Write, iter}; fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> String { #[derive(Debug)] @@ -12,59 +12,58 @@ fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> S } let selected_range = selected_range.to_offset(buffer); - let mut matches = buffer.matches(0..buffer.len(), |grammar| { + let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| { Some(&grammar.embedding_config.as_ref()?.query) }); - let configs = matches + let configs = ts_matches .grammars() .iter() .map(|g| g.embedding_config.as_ref().unwrap()) .collect::>(); - let mut matches = iter::from_fn(move || { - while let Some(mat) = matches.peek() { - let config = &configs[mat.grammar_index]; - if let Some(collapse) = mat.captures.iter().find_map(|cap| { - if Some(cap.index) == config.collapse_capture_ix { - Some(cap.node.byte_range()) - } else { - None - } - }) { - let mut keep = Vec::new(); - for capture in mat.captures.iter() { - if Some(capture.index) == config.keep_capture_ix { - keep.push(capture.node.byte_range()); - } else { - continue; - } - } - matches.advance(); - return Some(Match { collapse, keep }); + let mut matches = Vec::new(); + while let Some(mat) = ts_matches.peek() { + let config = &configs[mat.grammar_index]; + if let Some(collapse) = mat.captures.iter().find_map(|cap| { + if Some(cap.index) == config.collapse_capture_ix { + Some(cap.node.byte_range()) } else { - matches.advance(); + None } + }) { + let mut keep = Vec::new(); + for capture in mat.captures.iter() { + if Some(capture.index) == config.keep_capture_ix { + keep.push(capture.node.byte_range()); + } else { + continue; + } + } + ts_matches.advance(); + matches.push(Match { collapse, keep }); + } else { + ts_matches.advance(); } - None - }) - .peekable(); + } + matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end))); + let mut matches = matches.into_iter().peekable(); let mut summary = String::new(); let mut offset = 0; let mut flushed_selection = false; - while let Some(mut mat) = matches.next() { + while let Some(mat) = matches.next() { // Keep extending the collapsed range if the next match surrounds // the current one. while let Some(next_mat) = matches.peek() { - if next_mat.collapse.start <= mat.collapse.start - && next_mat.collapse.end >= mat.collapse.end + if mat.collapse.start <= next_mat.collapse.start + && mat.collapse.end >= next_mat.collapse.end { - mat = matches.next().unwrap(); + matches.next().unwrap(); } else { break; } } - if offset >= mat.collapse.start { + if offset > mat.collapse.start { // Skip collapsed nodes that have already been summarized. offset = cmp::max(offset, mat.collapse.end); continue; diff --git a/crates/zed/src/languages/rust/embedding.scm b/crates/zed/src/languages/rust/embedding.scm index e4218382a9..c4ed7d2097 100644 --- a/crates/zed/src/languages/rust/embedding.scm +++ b/crates/zed/src/languages/rust/embedding.scm @@ -2,6 +2,9 @@ [(line_comment) (attribute_item)]* @context . [ + (attribute_item) @collapse + (use_declaration) @collapse + (struct_item name: (_) @name) From 9dc292772af147d27ef0b75d228543f3e818408b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 09:53:30 -0600 Subject: [PATCH 36/45] Add a screen for gpui tests Allows me to test notifications --- crates/gpui/src/platform/test.rs | 35 +++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index e8579a0006..7b4813ffa9 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -103,6 +103,7 @@ pub struct Platform { current_clipboard_item: Mutex>, cursor: Mutex, active_window: Arc>>, + active_screen: Screen, } impl Platform { @@ -113,6 +114,7 @@ impl Platform { current_clipboard_item: Default::default(), cursor: Mutex::new(CursorStyle::Arrow), active_window: Default::default(), + active_screen: Screen::new(), } } } @@ -136,12 +138,16 @@ impl super::Platform for Platform { fn quit(&self) {} - fn screen_by_id(&self, _id: uuid::Uuid) -> Option> { - None + fn screen_by_id(&self, uuid: uuid::Uuid) -> Option> { + if self.active_screen.uuid == uuid { + Some(Rc::new(self.active_screen.clone())) + } else { + None + } } fn screens(&self) -> Vec> { - Default::default() + vec![Rc::new(self.active_screen.clone())] } fn open_window( @@ -158,6 +164,7 @@ impl super::Platform for Platform { WindowBounds::Fixed(rect) => rect.size(), }, self.active_window.clone(), + Rc::new(self.active_screen.clone()), )) } @@ -170,6 +177,7 @@ impl super::Platform for Platform { handle, vec2f(24., 24.), self.active_window.clone(), + Rc::new(self.active_screen.clone()), )) } @@ -238,8 +246,18 @@ impl super::Platform for Platform { fn restart(&self) {} } -#[derive(Debug)] -pub struct Screen; +#[derive(Debug, Clone)] +pub struct Screen { + uuid: uuid::Uuid, +} + +impl Screen { + fn new() -> Self { + Self { + uuid: uuid::Uuid::new_v4(), + } + } +} impl super::Screen for Screen { fn as_any(&self) -> &dyn Any { @@ -255,7 +273,7 @@ impl super::Screen for Screen { } fn display_uuid(&self) -> Option { - Some(uuid::Uuid::new_v4()) + Some(self.uuid) } } @@ -275,6 +293,7 @@ pub struct Window { pub(crate) edited: bool, pub(crate) pending_prompts: RefCell>>, active_window: Arc>>, + screen: Rc, } impl Window { @@ -282,6 +301,7 @@ impl Window { handle: AnyWindowHandle, size: Vector2F, active_window: Arc>>, + screen: Rc, ) -> Self { Self { handle, @@ -299,6 +319,7 @@ impl Window { edited: false, pending_prompts: Default::default(), active_window, + screen, } } @@ -329,7 +350,7 @@ impl super::Window for Window { } fn screen(&self) -> Rc { - Rc::new(Screen) + self.screen.clone() } fn mouse_position(&self) -> Vector2F { From 39af2bb0a45e1ad272e16ac92b8cf90ec3e776a1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 10:56:32 -0600 Subject: [PATCH 37/45] Ensure notifications are dismissed Before this change if you joined a project without clicking on the notification it would never disappear. Fix a related bug where if you have more than one monitor, the notification was only dismissed from one of them. --- crates/call/src/room.rs | 7 ++++ crates/collab/src/tests/following_tests.rs | 39 ++++++++++++++++--- crates/collab_ui/src/collab_titlebar_item.rs | 2 +- crates/collab_ui/src/collab_ui.rs | 2 +- .../src/project_shared_notification.rs | 21 ++++++++-- crates/workspace/src/workspace.rs | 11 ------ 6 files changed, 61 insertions(+), 21 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 26a531cc31..130a7a64f0 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -44,6 +44,12 @@ pub enum Event { RemoteProjectUnshared { project_id: u64, }, + RemoteProjectJoined { + project_id: u64, + }, + RemoteProjectInvitationDiscarded { + project_id: u64, + }, Left, } @@ -1015,6 +1021,7 @@ impl Room { ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); + cx.emit(Event::RemoteProjectJoined { project_id: id }); cx.spawn(|this, mut cx| async move { let project = Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?; diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index d7acae3995..696923e505 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1,7 +1,10 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use call::ActiveCall; +use collab_ui::project_shared_notification::ProjectSharedNotification; use editor::{Editor, ExcerptRange, MultiBuffer}; -use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle}; +use gpui::{ + executor::Deterministic, geometry::vector::vec2f, AppContext, TestAppContext, ViewHandle, +}; use live_kit_client::MacOSDisplay; use serde_json::json; use std::sync::Arc; @@ -1073,6 +1076,24 @@ async fn test_peers_simultaneously_following_each_other( }); } +fn visible_push_notifications( + cx: &mut TestAppContext, +) -> Vec> { + let mut ret = Vec::new(); + for window in cx.windows() { + window.read_with(cx, |window| { + if let Some(handle) = window + .root_view() + .clone() + .downcast::() + { + ret.push(handle) + } + }); + } + ret +} + #[gpui::test(iterations = 10)] async fn test_following_across_workspaces( deterministic: Arc, @@ -1126,17 +1147,22 @@ async fn test_following_across_workspaces( let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await; let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await; + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + + cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx)); + cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx)); + let project_a_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); + /* let project_b_id = active_call_b .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) .await .unwrap(); - - let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); - let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); + */ active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) @@ -1157,7 +1183,9 @@ async fn test_following_across_workspaces( .unwrap(); deterministic.run_until_parked(); - assert_eq!(cx_b.windows().len(), 1); + assert_eq!(cx_b.windows().len(), 2); + + assert_eq!(visible_push_notifications(cx_b).len(), 1); workspace_b.update(cx_b, |workspace, cx| { workspace @@ -1186,4 +1214,5 @@ async fn test_following_across_workspaces( }); // assert that there are no share notifications open + assert_eq!(visible_push_notifications(cx_b).len(), 0); } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 879b375cd4..d85aca164a 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -948,7 +948,7 @@ impl CollabTitlebarItem { fn render_face_pile( &self, user: &User, - replica_id: Option, + _replica_id: Option, peer_id: PeerId, location: Option, muted: bool, diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 84a9b3b6b6..57d6f7b4f6 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -7,7 +7,7 @@ mod face_pile; mod incoming_call_notification; mod notifications; mod panel_settings; -mod project_shared_notification; +pub mod project_shared_notification; mod sharing_status_indicator; use call::{report_call_event_for_room, ActiveCall, Room}; diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 21fa7d4ee6..5e362403f0 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -40,7 +40,8 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { .push(window); } } - room::Event::RemoteProjectUnshared { project_id } => { + room::Event::RemoteProjectUnshared { project_id } + | room::Event::RemoteProjectInvitationDiscarded { project_id } => { if let Some(windows) = notification_windows.remove(&project_id) { for window in windows { window.remove(cx); @@ -54,6 +55,13 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { } } } + room::Event::RemoteProjectJoined { project_id } => { + if let Some(windows) = notification_windows.remove(&project_id) { + for window in windows { + window.remove(cx); + } + } + } _ => {} }) .detach(); @@ -82,7 +90,6 @@ impl ProjectSharedNotification { } fn join(&mut self, cx: &mut ViewContext) { - cx.remove_window(); if let Some(app_state) = self.app_state.upgrade() { workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx) .detach_and_log_err(cx); @@ -90,7 +97,15 @@ impl ProjectSharedNotification { } fn dismiss(&mut self, cx: &mut ViewContext) { - cx.remove_window(); + if let Some(active_room) = + ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned()) + { + active_room.update(cx, |_, cx| { + cx.emit(room::Event::RemoteProjectInvitationDiscarded { + project_id: self.project_id, + }); + }); + } } fn render_owner(&self, cx: &mut ViewContext) -> AnyElement { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 38773fb8cc..c90b175320 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2529,7 +2529,6 @@ impl Workspace { if let Some(prev_leader_id) = self.unfollow(&pane, cx) { if leader_id == prev_leader_id { - dbg!("oh no!"); return None; } } @@ -2618,7 +2617,6 @@ impl Workspace { let project = self.project.read(cx); let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else { - dbg!("no remote participant yet..."); return None; }; @@ -2633,7 +2631,6 @@ impl Workspace { } } }; - dbg!(other_project_id); // if they are active in another project, follow there. if let Some(project_id) = other_project_id { @@ -2650,7 +2647,6 @@ impl Workspace { 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 { - dbg!("focusing pane"); cx.focus(pane); return None; } @@ -4249,7 +4245,6 @@ pub fn join_remote_project( app_state: Arc, cx: &mut AppContext, ) -> Task> { - dbg!("huh??"); cx.spawn(|mut cx| async move { let existing_workspace = cx .windows() @@ -4268,10 +4263,8 @@ pub fn join_remote_project( .flatten(); let workspace = if let Some(existing_workspace) = existing_workspace { - dbg!("huh"); existing_workspace } else { - dbg!("huh/"); let active_call = cx.read(ActiveCall::global); let room = active_call .read_with(&cx, |call, _| call.room().cloned()) @@ -4287,7 +4280,6 @@ pub fn join_remote_project( }) .await?; - dbg!("huh//"); let window_bounds_override = window_bounds_env_override(&cx); let window = cx.add_window( (app_state.build_window_options)( @@ -4310,7 +4302,6 @@ pub fn join_remote_project( workspace.downgrade() }; - dbg!("huh///"); workspace.window().activate(&mut cx); cx.platform().activate(true); @@ -4333,8 +4324,6 @@ pub fn join_remote_project( Some(collaborator.peer_id) }); - dbg!(follow_peer_id); - if let Some(follow_peer_id) = follow_peer_id { workspace .follow(follow_peer_id, cx) From 7f44083a969aa0232b42353bcb06a88462d41be5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 11:03:55 -0600 Subject: [PATCH 38/45] Remove unused function --- crates/client/src/client.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 4ddfbc5a34..5eae700404 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -453,10 +453,6 @@ impl Client { self.state.read().status.1.clone() } - pub fn is_connected(&self) -> bool { - matches!(&*self.status().borrow(), Status::Connected { .. }) - } - fn set_status(self: &Arc, status: Status, cx: &AsyncAppContext) { log::info!("set status on client {}: {:?}", self.id, status); let mut state = self.state.write(); From 3d68fcad0bed07e4ce151bb2c3d41ccdea450c0b Mon Sep 17 00:00:00 2001 From: Julia Date: Mon, 2 Oct 2023 13:18:49 -0400 Subject: [PATCH 39/45] Detach completion confirmation task when selecting with mouse Otherwise the spawn to resolve the additional edits never runs causing autocomplete to never add imports automatically when clicking with the mouse --- crates/editor/src/editor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ca19ad24cb..e0b8af1c71 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1057,7 +1057,8 @@ impl CompletionsMenu { item_ix: Some(item_ix), }, cx, - ); + ) + .map(|task| task.detach()); }) .into_any(), ); From 9e1f7c4c18b53d98bcf9927bdf1144d6ff78c397 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 2 Oct 2023 18:20:47 -0400 Subject: [PATCH 40/45] Mainline GPUI2 UI work (#3079) This PR mainlines the current state of new GPUI2-based UI from the `gpui2-ui` branch. Release Notes: - N/A --------- Co-authored-by: Nate Butler Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Nate --- Cargo.lock | 4 +- crates/fs/Cargo.toml | 6 +- crates/fs/src/fs.rs | 27 - crates/gpui/Cargo.toml | 2 +- crates/project/src/project.rs | 27 +- crates/storybook/Cargo.toml | 2 + .../src/stories/components/breadcrumb.rs | 33 +- .../src/stories/components/buffer.rs | 8 +- .../src/stories/components/chat_panel.rs | 48 +- .../src/stories/components/facepile.rs | 2 +- .../storybook/src/stories/components/panel.rs | 7 +- .../src/stories/components/project_panel.rs | 8 +- .../src/stories/components/tab_bar.rs | 34 +- .../src/stories/components/toolbar.rs | 60 ++- .../storybook/src/stories/elements/avatar.rs | 2 +- crates/storybook/src/stories/elements/icon.rs | 2 +- crates/storybook/src/stories/kitchen_sink.rs | 3 + crates/storybook/src/storybook.rs | 39 +- crates/ui/Cargo.toml | 1 + crates/ui/src/components.rs | 4 +- crates/ui/src/components/breadcrumb.rs | 60 ++- crates/ui/src/components/buffer.rs | 40 +- crates/ui/src/components/chat_panel.rs | 75 +-- crates/ui/src/components/editor.rs | 25 - crates/ui/src/components/editor_pane.rs | 60 +++ crates/ui/src/components/panel.rs | 16 +- crates/ui/src/components/player_stack.rs | 9 +- crates/ui/src/components/project_panel.rs | 85 +-- crates/ui/src/components/tab.rs | 2 +- crates/ui/src/components/tab_bar.rs | 57 +- crates/ui/src/components/terminal.rs | 11 +- crates/ui/src/components/title_bar.rs | 29 +- crates/ui/src/components/toolbar.rs | 40 +- crates/ui/src/components/workspace.rs | 101 ++-- crates/ui/src/elements/icon.rs | 2 + crates/ui/src/elements/input.rs | 1 + crates/ui/src/elements/player.rs | 3 +- crates/ui/src/prelude.rs | 19 +- crates/ui/src/static_data.rs | 492 ++++++++++++++++-- 39 files changed, 1047 insertions(+), 399 deletions(-) delete mode 100644 crates/ui/src/components/editor.rs create mode 100644 crates/ui/src/components/editor_pane.rs diff --git a/Cargo.lock b/Cargo.lock index 76de671620..f146c4b8d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2790,7 +2790,6 @@ dependencies = [ "lazy_static", "libc", "log", - "lsp", "parking_lot 0.11.2", "regex", "rope", @@ -7403,6 +7402,8 @@ dependencies = [ "anyhow", "chrono", "clap 4.4.4", + "fs", + "futures 0.3.28", "gpui2", "itertools 0.11.0", "log", @@ -8638,6 +8639,7 @@ dependencies = [ "anyhow", "chrono", "gpui2", + "rand 0.8.5", "serde", "settings", "smallvec", diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 78146c3a9d..441ce6f9c7 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -9,8 +9,6 @@ path = "src/fs.rs" [dependencies] collections = { path = "../collections" } -gpui = { path = "../gpui" } -lsp = { path = "../lsp" } rope = { path = "../rope" } text = { path = "../text" } util = { path = "../util" } @@ -34,8 +32,10 @@ log.workspace = true libc = "0.2" time.workspace = true +gpui = { path = "../gpui", optional = true} + [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } [features] -test-support = [] +test-support = ["gpui/test-support"] diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 97175cb55e..1d95db9b6c 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -93,33 +93,6 @@ pub struct Metadata { pub is_dir: bool, } -impl From for CreateOptions { - fn from(options: lsp::CreateFileOptions) -> Self { - Self { - overwrite: options.overwrite.unwrap_or(false), - ignore_if_exists: options.ignore_if_exists.unwrap_or(false), - } - } -} - -impl From for RenameOptions { - fn from(options: lsp::RenameFileOptions) -> Self { - Self { - overwrite: options.overwrite.unwrap_or(false), - ignore_if_exists: options.ignore_if_exists.unwrap_or(false), - } - } -} - -impl From for RemoveOptions { - fn from(options: lsp::DeleteFileOptions) -> Self { - Self { - recursive: options.recursive.unwrap_or(false), - ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false), - } - } -} - pub struct RealFs; #[async_trait::async_trait] diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 95b7ccb559..6aeef558c0 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -11,7 +11,7 @@ path = "src/gpui.rs" doctest = false [features] -test-support = ["backtrace", "dhat", "env_logger", "collections/test-support"] +test-support = ["backtrace", "dhat", "env_logger", "collections/test-support", "util/test-support"] [dependencies] collections = { path = "../collections" } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ee8690ea70..1194593157 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4957,8 +4957,16 @@ impl Project { if abs_path.ends_with("/") { fs.create_dir(&abs_path).await?; } else { - fs.create_file(&abs_path, op.options.map(Into::into).unwrap_or_default()) - .await?; + fs.create_file( + &abs_path, + op.options + .map(|options| fs::CreateOptions { + overwrite: options.overwrite.unwrap_or(false), + ignore_if_exists: options.ignore_if_exists.unwrap_or(false), + }) + .unwrap_or_default(), + ) + .await?; } } @@ -4974,7 +4982,12 @@ impl Project { fs.rename( &source_abs_path, &target_abs_path, - op.options.map(Into::into).unwrap_or_default(), + op.options + .map(|options| fs::RenameOptions { + overwrite: options.overwrite.unwrap_or(false), + ignore_if_exists: options.ignore_if_exists.unwrap_or(false), + }) + .unwrap_or_default(), ) .await?; } @@ -4984,7 +4997,13 @@ impl Project { .uri .to_file_path() .map_err(|_| anyhow!("can't convert URI to path"))?; - let options = op.options.map(Into::into).unwrap_or_default(); + let options = op + .options + .map(|options| fs::RemoveOptions { + recursive: options.recursive.unwrap_or(false), + ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false), + }) + .unwrap_or_default(); if abs_path.ends_with("/") { fs.remove_dir(&abs_path, options).await?; } else { diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index 73c8361613..43890dd01a 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -12,6 +12,8 @@ path = "src/storybook.rs" anyhow.workspace = true clap = { version = "4.4", features = ["derive", "string"] } chrono = "0.4" +fs = { path = "../fs" } +futures.workspace = true gpui2 = { path = "../gpui2" } itertools = "0.11.0" log.workspace = true diff --git a/crates/storybook/src/stories/components/breadcrumb.rs b/crates/storybook/src/stories/components/breadcrumb.rs index 8d144c0174..002b6140e1 100644 --- a/crates/storybook/src/stories/components/breadcrumb.rs +++ b/crates/storybook/src/stories/components/breadcrumb.rs @@ -1,5 +1,8 @@ +use std::path::PathBuf; +use std::str::FromStr; + use ui::prelude::*; -use ui::Breadcrumb; +use ui::{Breadcrumb, HighlightedText, Symbol}; use crate::story::Story; @@ -8,9 +11,35 @@ pub struct BreadcrumbStory {} impl BreadcrumbStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + Story::container(cx) .child(Story::title_for::<_, Breadcrumb>(cx)) .child(Story::label(cx, "Default")) - .child(Breadcrumb::new()) + .child(Breadcrumb::new( + PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(), + vec![ + Symbol(vec![ + HighlightedText { + text: "impl ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "BreadcrumbStory".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + ]), + Symbol(vec![ + HighlightedText { + text: "fn ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "render".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + ]), + ], + )) } } diff --git a/crates/storybook/src/stories/components/buffer.rs b/crates/storybook/src/stories/components/buffer.rs index 8d9e70a282..0b3268421b 100644 --- a/crates/storybook/src/stories/components/buffer.rs +++ b/crates/storybook/src/stories/components/buffer.rs @@ -12,8 +12,10 @@ pub struct BufferStory {} impl BufferStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + Story::container(cx) - .child(Story::title_for::<_, Buffer>(cx)) + .child(Story::title_for::<_, Buffer>(cx)) .child(Story::label(cx, "Default")) .child(div().w(rems(64.)).h_96().child(empty_buffer_example())) .child(Story::label(cx, "Hello World (Rust)")) @@ -21,14 +23,14 @@ impl BufferStory { div() .w(rems(64.)) .h_96() - .child(hello_world_rust_buffer_example(cx)), + .child(hello_world_rust_buffer_example(&theme)), ) .child(Story::label(cx, "Hello World (Rust) with Status")) .child( div() .w(rems(64.)) .h_96() - .child(hello_world_rust_buffer_with_status_example(cx)), + .child(hello_world_rust_buffer_with_status_example(&theme)), ) } } diff --git a/crates/storybook/src/stories/components/chat_panel.rs b/crates/storybook/src/stories/components/chat_panel.rs index 804290b7ca..e87ac0afa2 100644 --- a/crates/storybook/src/stories/components/chat_panel.rs +++ b/crates/storybook/src/stories/components/chat_panel.rs @@ -1,6 +1,6 @@ use chrono::DateTime; use ui::prelude::*; -use ui::{ChatMessage, ChatPanel}; +use ui::{ChatMessage, ChatPanel, Panel}; use crate::story::Story; @@ -12,23 +12,35 @@ impl ChatPanelStory { Story::container(cx) .child(Story::title_for::<_, ChatPanel>(cx)) .child(Story::label(cx, "Default")) - .child(ChatPanel::new(ScrollState::default())) + .child(Panel::new( + ScrollState::default(), + |_, _| vec![ChatPanel::new(ScrollState::default()).into_any()], + Box::new(()), + )) .child(Story::label(cx, "With Mesages")) - .child(ChatPanel::new(ScrollState::default()).with_messages(vec![ - ChatMessage::new( - "osiewicz".to_string(), - "is this thing on?".to_string(), - DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z") - .unwrap() - .naive_local(), - ), - ChatMessage::new( - "maxdeviant".to_string(), - "Reading you loud and clear!".to_string(), - DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z") - .unwrap() - .naive_local(), - ), - ])) + .child(Panel::new( + ScrollState::default(), + |_, _| { + vec![ChatPanel::new(ScrollState::default()) + .with_messages(vec![ + ChatMessage::new( + "osiewicz".to_string(), + "is this thing on?".to_string(), + DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z") + .unwrap() + .naive_local(), + ), + ChatMessage::new( + "maxdeviant".to_string(), + "Reading you loud and clear!".to_string(), + DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z") + .unwrap() + .naive_local(), + ), + ]) + .into_any()] + }, + Box::new(()), + )) } } diff --git a/crates/storybook/src/stories/components/facepile.rs b/crates/storybook/src/stories/components/facepile.rs index a32ffa3693..bbd08ae984 100644 --- a/crates/storybook/src/stories/components/facepile.rs +++ b/crates/storybook/src/stories/components/facepile.rs @@ -11,7 +11,7 @@ impl FacepileStory { let players = static_players(); Story::container(cx) - .child(Story::title_for::<_, ui::Facepile>(cx)) + .child(Story::title_for::<_, Facepile>(cx)) .child(Story::label(cx, "Default")) .child( div() diff --git a/crates/storybook/src/stories/components/panel.rs b/crates/storybook/src/stories/components/panel.rs index 38e7033d44..39a5ceafa2 100644 --- a/crates/storybook/src/stories/components/panel.rs +++ b/crates/storybook/src/stories/components/panel.rs @@ -14,9 +14,10 @@ impl PanelStory { .child(Panel::new( ScrollState::default(), |_, _| { - (0..100) - .map(|ix| Label::new(format!("Item {}", ix + 1)).into_any()) - .collect() + vec![div() + .overflow_y_scroll(ScrollState::default()) + .children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1)))) + .into_any()] }, Box::new(()), )) diff --git a/crates/storybook/src/stories/components/project_panel.rs b/crates/storybook/src/stories/components/project_panel.rs index ff4eb6099b..cba71cd21a 100644 --- a/crates/storybook/src/stories/components/project_panel.rs +++ b/crates/storybook/src/stories/components/project_panel.rs @@ -1,5 +1,5 @@ use ui::prelude::*; -use ui::ProjectPanel; +use ui::{Panel, ProjectPanel}; use crate::story::Story; @@ -11,6 +11,10 @@ impl ProjectPanelStory { Story::container(cx) .child(Story::title_for::<_, ProjectPanel>(cx)) .child(Story::label(cx, "Default")) - .child(ProjectPanel::new(ScrollState::default())) + .child(Panel::new( + ScrollState::default(), + |_, _| vec![ProjectPanel::new(ScrollState::default()).into_any()], + Box::new(()), + )) } } diff --git a/crates/storybook/src/stories/components/tab_bar.rs b/crates/storybook/src/stories/components/tab_bar.rs index 4c116caf7b..b5fa45dfd6 100644 --- a/crates/storybook/src/stories/components/tab_bar.rs +++ b/crates/storybook/src/stories/components/tab_bar.rs @@ -1,5 +1,5 @@ use ui::prelude::*; -use ui::TabBar; +use ui::{Tab, TabBar}; use crate::story::Story; @@ -11,6 +11,36 @@ impl TabBarStory { Story::container(cx) .child(Story::title_for::<_, TabBar>(cx)) .child(Story::label(cx, "Default")) - .child(TabBar::new(ScrollState::default())) + .child(TabBar::new(vec![ + Tab::new() + .title("Cargo.toml".to_string()) + .current(false) + .git_status(GitStatus::Modified), + Tab::new() + .title("Channels Panel".to_string()) + .current(false), + Tab::new() + .title("channels_panel.rs".to_string()) + .current(true) + .git_status(GitStatus::Modified), + Tab::new() + .title("workspace.rs".to_string()) + .current(false) + .git_status(GitStatus::Modified), + Tab::new() + .title("icon_button.rs".to_string()) + .current(false), + Tab::new() + .title("storybook.rs".to_string()) + .current(false) + .git_status(GitStatus::Created), + Tab::new().title("theme.rs".to_string()).current(false), + Tab::new() + .title("theme_registry.rs".to_string()) + .current(false), + Tab::new() + .title("styleable_helpers.rs".to_string()) + .current(false), + ])) } } diff --git a/crates/storybook/src/stories/components/toolbar.rs b/crates/storybook/src/stories/components/toolbar.rs index cfe3c97840..1413c463b4 100644 --- a/crates/storybook/src/stories/components/toolbar.rs +++ b/crates/storybook/src/stories/components/toolbar.rs @@ -1,5 +1,9 @@ +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; + use ui::prelude::*; -use ui::Toolbar; +use ui::{theme, Breadcrumb, HighlightColor, HighlightedText, Icon, IconButton, Symbol, Toolbar}; use crate::story::Story; @@ -8,9 +12,59 @@ pub struct ToolbarStory {} impl ToolbarStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx); + + struct LeftItemsPayload { + pub theme: Arc, + } + Story::container(cx) - .child(Story::title_for::<_, Toolbar>(cx)) + .child(Story::title_for::<_, Toolbar>(cx)) .child(Story::label(cx, "Default")) - .child(Toolbar::new()) + .child(Toolbar::new( + |_, payload| { + let payload = payload.downcast_ref::().unwrap(); + + let theme = payload.theme.clone(); + + vec![Breadcrumb::new( + PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(), + vec![ + Symbol(vec![ + HighlightedText { + text: "impl ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "ToolbarStory".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + ]), + Symbol(vec![ + HighlightedText { + text: "fn ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "render".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + ]), + ], + ) + .into_any()] + }, + Box::new(LeftItemsPayload { + theme: theme.clone(), + }), + |_, _| { + vec![ + IconButton::new(Icon::InlayHint).into_any(), + IconButton::new(Icon::MagnifyingGlass).into_any(), + IconButton::new(Icon::MagicWand).into_any(), + ] + }, + Box::new(()), + )) } } diff --git a/crates/storybook/src/stories/elements/avatar.rs b/crates/storybook/src/stories/elements/avatar.rs index d47c667f61..a277fa6a1e 100644 --- a/crates/storybook/src/stories/elements/avatar.rs +++ b/crates/storybook/src/stories/elements/avatar.rs @@ -9,7 +9,7 @@ pub struct AvatarStory {} impl AvatarStory { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { Story::container(cx) - .child(Story::title_for::<_, ui::Avatar>(cx)) + .child(Story::title_for::<_, Avatar>(cx)) .child(Story::label(cx, "Default")) .child(Avatar::new( "https://avatars.githubusercontent.com/u/1714999?v=4", diff --git a/crates/storybook/src/stories/elements/icon.rs b/crates/storybook/src/stories/elements/icon.rs index 21838bd839..66d3abc0b3 100644 --- a/crates/storybook/src/stories/elements/icon.rs +++ b/crates/storybook/src/stories/elements/icon.rs @@ -12,7 +12,7 @@ impl IconStory { let icons = Icon::iter(); Story::container(cx) - .child(Story::title_for::<_, ui::IconElement>(cx)) + .child(Story::title_for::<_, IconElement>(cx)) .child(Story::label(cx, "All Icons")) .child(div().flex().gap_3().children(icons.map(IconElement::new))) } diff --git a/crates/storybook/src/stories/kitchen_sink.rs b/crates/storybook/src/stories/kitchen_sink.rs index 3bb902b0db..ae826f934e 100644 --- a/crates/storybook/src/stories/kitchen_sink.rs +++ b/crates/storybook/src/stories/kitchen_sink.rs @@ -19,5 +19,8 @@ impl KitchenSinkStory { .child(div().flex().flex_col().children_any(element_stories)) .child(Story::label(cx, "Components")) .child(div().flex().flex_col().children_any(component_stories)) + // Add a bit of space at the bottom of the kitchen sink so elements + // don't end up squished right up against the bottom of the screen. + .child(div().p_4()) } } diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index f4a2f69704..afae0d5ebe 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -4,7 +4,7 @@ mod stories; mod story; mod story_selector; -use std::sync::Arc; +use std::{process::Command, sync::Arc}; use ::theme as legacy_theme; use clap::Parser; @@ -38,11 +38,44 @@ struct Args { theme: Option, } +async fn watch_zed_changes(fs: Arc) -> Option<()> { + if std::env::var("ZED_HOT_RELOAD").is_err() { + return None; + } + use futures::StreamExt; + let mut events = fs + .watch(".".as_ref(), std::time::Duration::from_millis(100)) + .await; + let mut current_child: Option = None; + while let Some(events) = events.next().await { + if !events.iter().any(|event| { + event + .path + .to_str() + .map(|path| path.contains("/crates/")) + .unwrap_or_default() + }) { + continue; + } + let child = current_child.take().map(|mut child| child.kill()); + log::info!("Storybook changed, rebuilding..."); + current_child = Some( + Command::new("cargo") + .args(["run", "-p", "storybook"]) + .spawn() + .ok()?, + ); + } + Some(()) +} + fn main() { SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); let args = Args::parse(); + let fs = Arc::new(fs::RealFs); + gpui2::App::new(Assets).unwrap().run(move |cx| { let mut store = SettingsStore::default(); store @@ -63,6 +96,10 @@ fn main() { }) .and_then(|theme_name| theme_registry.get(&theme_name).ok()); + cx.spawn(|_| async move { + watch_zed_changes(fs).await; + }) + .detach(); cx.add_window( gpui2::WindowOptions { bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1700., 980.))), diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 821e93a340..7bd9d912a0 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -13,3 +13,4 @@ settings = { path = "../settings" } smallvec.workspace = true strum = { version = "0.25.0", features = ["derive"] } theme = { path = "../theme" } +rand = "0.8" diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index f96964bd27..0af13040f7 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -5,7 +5,7 @@ mod chat_panel; mod collab_panel; mod command_palette; mod context_menu; -mod editor; +mod editor_pane; mod facepile; mod icon_button; mod keybinding; @@ -31,7 +31,7 @@ pub use chat_panel::*; pub use collab_panel::*; pub use command_palette::*; pub use context_menu::*; -pub use editor::*; +pub use editor_pane::*; pub use facepile::*; pub use icon_button::*; pub use keybinding::*; diff --git a/crates/ui/src/components/breadcrumb.rs b/crates/ui/src/components/breadcrumb.rs index 30b40011a5..c14e89ee7b 100644 --- a/crates/ui/src/components/breadcrumb.rs +++ b/crates/ui/src/components/breadcrumb.rs @@ -1,17 +1,35 @@ -use crate::prelude::*; +use std::path::PathBuf; + +use gpui2::elements::div::Div; + use crate::{h_stack, theme}; +use crate::{prelude::*, HighlightedText}; + +#[derive(Clone)] +pub struct Symbol(pub Vec); #[derive(Element)] -pub struct Breadcrumb {} +pub struct Breadcrumb { + path: PathBuf, + symbols: Vec, +} impl Breadcrumb { - pub fn new() -> Self { - Self {} + pub fn new(path: PathBuf, symbols: Vec) -> Self { + Self { path, symbols } + } + + fn render_separator(&self, theme: &Theme) -> Div { + div() + .child(" › ") + .text_color(HighlightColor::Default.hsla(theme)) } fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); + let symbols_len = self.symbols.len(); + h_stack() .px_1() // TODO: Read font from theme (or settings?). @@ -21,11 +39,33 @@ impl Breadcrumb { .rounded_md() .hover() .fill(theme.highest.base.hovered.background) - // TODO: Replace hardcoded breadcrumbs. - .child("crates/ui/src/components/toolbar.rs") - .child(" › ") - .child("impl Breadcrumb") - .child(" › ") - .child("fn render") + .child(self.path.clone().to_str().unwrap().to_string()) + .child(if !self.symbols.is_empty() { + self.render_separator(&theme) + } else { + div() + }) + .child( + div().flex().children( + self.symbols + .iter() + .enumerate() + // TODO: Could use something like `intersperse` here instead. + .flat_map(|(ix, symbol)| { + let mut items = + vec![div().flex().children(symbol.0.iter().map(|segment| { + div().child(segment.text.clone()).text_color(segment.color) + }))]; + + let is_last_segment = ix == symbols_len - 1; + if !is_last_segment { + items.push(self.render_separator(&theme)); + } + + items + }) + .collect::>(), + ), + ) } } diff --git a/crates/ui/src/components/buffer.rs b/crates/ui/src/components/buffer.rs index 88c5a59563..00e5daee55 100644 --- a/crates/ui/src/components/buffer.rs +++ b/crates/ui/src/components/buffer.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use gpui2::{Hsla, WindowContext}; use crate::prelude::*; @@ -33,6 +31,7 @@ pub struct BufferRow { pub show_line_number: bool, } +#[derive(Clone)] pub struct BufferRows { pub show_line_numbers: bool, pub rows: Vec, @@ -108,9 +107,8 @@ impl BufferRow { } } -#[derive(Element)] -pub struct Buffer { - view_type: PhantomData, +#[derive(Element, Clone)] +pub struct Buffer { scroll_state: ScrollState, rows: Option, readonly: bool, @@ -119,10 +117,9 @@ pub struct Buffer { path: Option, } -impl Buffer { +impl Buffer { pub fn new() -> Self { Self { - view_type: PhantomData, scroll_state: ScrollState::default(), rows: Some(BufferRows::default()), readonly: false, @@ -161,7 +158,7 @@ impl Buffer { self } - fn render_row(row: BufferRow, cx: &WindowContext) -> impl IntoElement { + fn render_row(row: BufferRow, cx: &WindowContext) -> impl IntoElement { let theme = theme(cx); let system_color = SystemColor::new(); @@ -172,28 +169,35 @@ impl Buffer { }; let line_number_color = if row.current { - HighlightColor::Default.hsla(cx) + HighlightColor::Default.hsla(&theme) } else { - HighlightColor::Comment.hsla(cx) + HighlightColor::Comment.hsla(&theme) }; h_stack() .fill(line_background) + .w_full() .gap_2() - .px_2() - .child(h_stack().w_4().h_full().px_1().when(row.code_action, |c| { - div().child(IconElement::new(Icon::Bolt)) - })) + .px_1() + .child( + h_stack() + .w_4() + .h_full() + .px_0p5() + .when(row.code_action, |c| { + div().child(IconElement::new(Icon::Bolt)) + }), + ) .when(row.show_line_number, |this| { this.child( - h_stack().justify_end().px_1().w_4().child( + h_stack().justify_end().px_0p5().w_3().child( div() .text_color(line_number_color) .child(row.line_number.to_string()), ), ) }) - .child(div().mx_1().w_1().h_full().fill(row.status.hsla(cx))) + .child(div().mx_0p5().w_1().h_full().fill(row.status.hsla(cx))) .children(row.line.map(|line| { div() .flex() @@ -205,7 +209,7 @@ impl Buffer { })) } - fn render_rows(&self, cx: &WindowContext) -> Vec> { + fn render_rows(&self, cx: &WindowContext) -> Vec> { match &self.rows { Some(rows) => rows .rows @@ -216,7 +220,7 @@ impl Buffer { } } - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); let rows = self.render_rows(cx); v_stack() diff --git a/crates/ui/src/components/chat_panel.rs b/crates/ui/src/components/chat_panel.rs index e5a2d6a556..5ae66967b6 100644 --- a/crates/ui/src/components/chat_panel.rs +++ b/crates/ui/src/components/chat_panel.rs @@ -4,13 +4,12 @@ use chrono::NaiveDateTime; use crate::prelude::*; use crate::theme::theme; -use crate::{Icon, IconButton, Input, Label, LabelColor, Panel, PanelSide}; +use crate::{Icon, IconButton, Input, Label, LabelColor}; #[derive(Element)] pub struct ChatPanel { view_type: PhantomData, scroll_state: ScrollState, - current_side: PanelSide, messages: Vec, } @@ -19,16 +18,10 @@ impl ChatPanel { Self { view_type: PhantomData, scroll_state, - current_side: PanelSide::default(), messages: Vec::new(), } } - pub fn side(mut self, side: PanelSide) -> Self { - self.current_side = side; - self - } - pub fn with_messages(mut self, messages: Vec) -> Self { self.messages = messages; self @@ -37,38 +30,33 @@ impl ChatPanel { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); - struct PanelPayload { - pub scroll_state: ScrollState, - pub messages: Vec, - } - - Panel::new( - self.scroll_state.clone(), - |_, payload| { - let payload = payload.downcast_ref::().unwrap(); - - vec![div() + div() + .flex() + .flex_col() + .justify_between() + .h_full() + .px_2() + .gap_2() + // Header + .child( + div() .flex() - .flex_col() - .h_full() - .px_2() - .gap_2() - // Header + .justify_between() + .py_2() + .child(div().flex().child(Label::new("#design"))) .child( div() .flex() - .justify_between() - .gap_2() - .child(div().flex().child(Label::new("#design"))) - .child( - div() - .flex() - .items_center() - .gap_px() - .child(IconButton::new(Icon::File)) - .child(IconButton::new(Icon::AudioOn)), - ), - ) + .items_center() + .gap_px() + .child(IconButton::new(Icon::File)) + .child(IconButton::new(Icon::AudioOn)), + ), + ) + .child( + div() + .flex() + .flex_col() // Chat Body .child( div() @@ -76,19 +64,12 @@ impl ChatPanel { .flex() .flex_col() .gap_3() - .overflow_y_scroll(payload.scroll_state.clone()) - .children(payload.messages.clone()), + .overflow_y_scroll(self.scroll_state.clone()) + .children(self.messages.clone()), ) // Composer - .child(div().flex().gap_2().child(Input::new("Message #design"))) - .into_any()] - }, - Box::new(PanelPayload { - scroll_state: self.scroll_state.clone(), - messages: self.messages.clone(), - }), - ) - .side(self.current_side) + .child(div().flex().my_2().child(Input::new("Message #design"))), + ) } } diff --git a/crates/ui/src/components/editor.rs b/crates/ui/src/components/editor.rs deleted file mode 100644 index 105ed86c40..0000000000 --- a/crates/ui/src/components/editor.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::marker::PhantomData; - -use crate::prelude::*; -use crate::{Buffer, Toolbar}; - -#[derive(Element)] -struct Editor { - view_type: PhantomData, - toolbar: Toolbar, - buffer: Buffer, -} - -impl Editor { - pub fn new(toolbar: Toolbar, buffer: Buffer) -> Self { - Self { - view_type: PhantomData, - toolbar, - buffer, - } - } - - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - div().child(self.toolbar.clone()) - } -} diff --git a/crates/ui/src/components/editor_pane.rs b/crates/ui/src/components/editor_pane.rs new file mode 100644 index 0000000000..561081164c --- /dev/null +++ b/crates/ui/src/components/editor_pane.rs @@ -0,0 +1,60 @@ +use std::marker::PhantomData; +use std::path::PathBuf; + +use crate::prelude::*; +use crate::{v_stack, Breadcrumb, Buffer, Icon, IconButton, Symbol, Tab, TabBar, Toolbar}; + +pub struct Editor { + pub tabs: Vec, + pub path: PathBuf, + pub symbols: Vec, + pub buffer: Buffer, +} + +#[derive(Element)] +pub struct EditorPane { + view_type: PhantomData, + editor: Editor, +} + +impl EditorPane { + pub fn new(editor: Editor) -> Self { + Self { + view_type: PhantomData, + editor, + } + } + + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + struct LeftItemsPayload { + path: PathBuf, + symbols: Vec, + } + + v_stack() + .w_full() + .h_full() + .flex_1() + .child(TabBar::new(self.editor.tabs.clone())) + .child(Toolbar::new( + |_, payload| { + let payload = payload.downcast_ref::().unwrap(); + + vec![Breadcrumb::new(payload.path.clone(), payload.symbols.clone()).into_any()] + }, + Box::new(LeftItemsPayload { + path: self.editor.path.clone(), + symbols: self.editor.symbols.clone(), + }), + |_, _| { + vec![ + IconButton::new(Icon::InlayHint).into_any(), + IconButton::new(Icon::MagnifyingGlass).into_any(), + IconButton::new(Icon::MagicWand).into_any(), + ] + }, + Box::new(()), + )) + .child(self.editor.buffer.clone()) + } +} diff --git a/crates/ui/src/components/panel.rs b/crates/ui/src/components/panel.rs index 9d64945cc1..cbcf502670 100644 --- a/crates/ui/src/components/panel.rs +++ b/crates/ui/src/components/panel.rs @@ -105,16 +105,12 @@ impl Panel { let theme = theme(cx); let panel_base; - let current_width = if let Some(width) = self.width { - width - } else { - self.initial_width - }; + let current_width = self.width.unwrap_or(self.initial_width); match self.current_side { PanelSide::Left => { panel_base = v_stack() - .overflow_y_scroll(self.scroll_state.clone()) + .flex_initial() .h_full() .w(current_width) .fill(theme.middle.base.default.background) @@ -123,20 +119,20 @@ impl Panel { } PanelSide::Right => { panel_base = v_stack() - .overflow_y_scroll(self.scroll_state.clone()) + .flex_initial() .h_full() .w(current_width) .fill(theme.middle.base.default.background) - .border_r() + .border_l() .border_color(theme.middle.base.default.border); } PanelSide::Bottom => { panel_base = v_stack() - .overflow_y_scroll(self.scroll_state.clone()) + .flex_initial() .w_full() .h(current_width) .fill(theme.middle.base.default.background) - .border_r() + .border_t() .border_color(theme.middle.base.default.border); } } diff --git a/crates/ui/src/components/player_stack.rs b/crates/ui/src/components/player_stack.rs index 4c00aaf2cf..7df6f065fb 100644 --- a/crates/ui/src/components/player_stack.rs +++ b/crates/ui/src/components/player_stack.rs @@ -38,9 +38,8 @@ impl PlayerStack { div().flex().justify_center().w_full().child( div() .w_4() - .h_1() - .rounded_bl_sm() - .rounded_br_sm() + .h_0p5() + .rounded_sm() .fill(player.cursor_color(cx)), ), ) @@ -50,7 +49,7 @@ impl PlayerStack { .items_center() .justify_center() .h_6() - .px_1() + .pl_1() .rounded_lg() .fill(if followers.is_none() { system_color.transparent @@ -59,7 +58,7 @@ impl PlayerStack { }) .child(Avatar::new(player.avatar_src().to_string())) .children(followers.map(|followers| { - div().neg_mr_1().child(Facepile::new(followers.into_iter())) + div().neg_ml_2().child(Facepile::new(followers.into_iter())) })), ) } diff --git a/crates/ui/src/components/project_panel.rs b/crates/ui/src/components/project_panel.rs index cf6f080b1c..1f32c698e5 100644 --- a/crates/ui/src/components/project_panel.rs +++ b/crates/ui/src/components/project_panel.rs @@ -1,17 +1,15 @@ use std::marker::PhantomData; -use std::sync::Arc; use crate::prelude::*; use crate::{ static_project_panel_project_items, static_project_panel_single_items, theme, Input, List, - ListHeader, Panel, PanelSide, Theme, + ListHeader, }; #[derive(Element)] pub struct ProjectPanel { view_type: PhantomData, scroll_state: ScrollState, - current_side: PanelSide, } impl ProjectPanel { @@ -19,69 +17,42 @@ impl ProjectPanel { Self { view_type: PhantomData, scroll_state, - current_side: PanelSide::default(), } } - pub fn side(mut self, side: PanelSide) -> Self { - self.current_side = side; - self - } - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { - struct PanelPayload { - pub theme: Arc, - pub scroll_state: ScrollState, - } + let theme = theme(cx); - Panel::new( - self.scroll_state.clone(), - |_, payload| { - let payload = payload.downcast_ref::().unwrap(); - - let theme = payload.theme.clone(); - - vec![div() + div() + .flex() + .flex_col() + .w_full() + .h_full() + .px_2() + .fill(theme.middle.base.default.background) + .child( + div() + .w_56() .flex() .flex_col() - .w_56() - .h_full() - .px_2() - .fill(theme.middle.base.default.background) + .overflow_y_scroll(ScrollState::default()) .child( - div() - .w_56() - .flex() - .flex_col() - .overflow_y_scroll(payload.scroll_state.clone()) - .child( - List::new(static_project_panel_single_items()) - .header( - ListHeader::new("FILES").set_toggle(ToggleState::Toggled), - ) - .empty_message("No files in directory") - .set_toggle(ToggleState::Toggled), - ) - .child( - List::new(static_project_panel_project_items()) - .header( - ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled), - ) - .empty_message("No folders in directory") - .set_toggle(ToggleState::Toggled), - ), + List::new(static_project_panel_single_items()) + .header(ListHeader::new("FILES").set_toggle(ToggleState::Toggled)) + .empty_message("No files in directory") + .set_toggle(ToggleState::Toggled), ) .child( - Input::new("Find something...") - .value("buffe".to_string()) - .state(InteractionState::Focused), - ) - .into_any()] - }, - Box::new(PanelPayload { - theme: theme(cx), - scroll_state: self.scroll_state.clone(), - }), - ) + List::new(static_project_panel_project_items()) + .header(ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled)) + .empty_message("No folders in directory") + .set_toggle(ToggleState::Toggled), + ), + ) + .child( + Input::new("Find something...") + .value("buffe".to_string()) + .state(InteractionState::Focused), + ) } } diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index 9c034d2535..9eb1122775 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -1,7 +1,7 @@ use crate::prelude::*; use crate::{theme, Icon, IconColor, IconElement, Label, LabelColor}; -#[derive(Element)] +#[derive(Element, Clone)] pub struct Tab { title: String, icon: Option, diff --git a/crates/ui/src/components/tab_bar.rs b/crates/ui/src/components/tab_bar.rs index 43fef77e2c..8addcb87b1 100644 --- a/crates/ui/src/components/tab_bar.rs +++ b/crates/ui/src/components/tab_bar.rs @@ -7,20 +7,27 @@ use crate::{theme, Icon, IconButton, Tab}; pub struct TabBar { view_type: PhantomData, scroll_state: ScrollState, + tabs: Vec, } impl TabBar { - pub fn new(scroll_state: ScrollState) -> Self { + pub fn new(tabs: Vec) -> Self { Self { view_type: PhantomData, - scroll_state, + scroll_state: ScrollState::default(), + tabs, } } + pub fn bind_scroll_state(&mut self, scroll_state: ScrollState) { + self.scroll_state = scroll_state; + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); let can_navigate_back = true; let can_navigate_forward = false; + div() .w_full() .flex() @@ -54,51 +61,7 @@ impl TabBar { div() .flex() .overflow_x_scroll(self.scroll_state.clone()) - .child( - Tab::new() - .title("Cargo.toml".to_string()) - .current(false) - .git_status(GitStatus::Modified), - ) - .child( - Tab::new() - .title("Channels Panel".to_string()) - .current(false), - ) - .child( - Tab::new() - .title("channels_panel.rs".to_string()) - .current(true) - .git_status(GitStatus::Modified), - ) - .child( - Tab::new() - .title("workspace.rs".to_string()) - .current(false) - .git_status(GitStatus::Modified), - ) - .child( - Tab::new() - .title("icon_button.rs".to_string()) - .current(false), - ) - .child( - Tab::new() - .title("storybook.rs".to_string()) - .current(false) - .git_status(GitStatus::Created), - ) - .child(Tab::new().title("theme.rs".to_string()).current(false)) - .child( - Tab::new() - .title("theme_registry.rs".to_string()) - .current(false), - ) - .child( - Tab::new() - .title("styleable_helpers.rs".to_string()) - .current(false), - ), + .children(self.tabs.clone()), ), ) // Right Side diff --git a/crates/ui/src/components/terminal.rs b/crates/ui/src/components/terminal.rs index f5c3a42a64..909cb886ce 100644 --- a/crates/ui/src/components/terminal.rs +++ b/crates/ui/src/components/terminal.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use gpui2::geometry::{relative, rems, Size}; use crate::prelude::*; @@ -20,6 +22,7 @@ impl Terminal { div() .flex() .flex_col() + .w_full() .child( // Terminal Tabs. div() @@ -70,8 +73,12 @@ impl Terminal { width: relative(1.).into(), height: rems(36.).into(), }, - |_, _| vec![], - Box::new(()), + |_, payload| { + let theme = payload.downcast_ref::>().unwrap(); + + vec![crate::static_data::terminal_buffer(&theme).into_any()] + }, + Box::new(theme), )) } } diff --git a/crates/ui/src/components/title_bar.rs b/crates/ui/src/components/title_bar.rs index 196b896396..dd3d6c1fba 100644 --- a/crates/ui/src/components/title_bar.rs +++ b/crates/ui/src/components/title_bar.rs @@ -2,16 +2,24 @@ use std::marker::PhantomData; use std::sync::atomic::AtomicBool; use std::sync::Arc; -use crate::prelude::*; +use crate::{prelude::*, PlayerWithCallStatus}; use crate::{ - static_players_with_call_status, theme, Avatar, Button, Icon, IconButton, IconColor, - PlayerStack, ToolDivider, TrafficLights, + theme, Avatar, Button, Icon, IconButton, IconColor, PlayerStack, ToolDivider, TrafficLights, }; +#[derive(Clone)] +pub struct Livestream { + pub players: Vec, + pub channel: Option, // projects + // windows +} + #[derive(Element)] pub struct TitleBar { view_type: PhantomData, + /// If the window is active from the OS's perspective. is_active: Arc, + livestream: Option, } impl TitleBar { @@ -28,14 +36,24 @@ impl TitleBar { Self { view_type: PhantomData, is_active, + livestream: None, } } + pub fn set_livestream(mut self, livestream: Option) -> Self { + self.livestream = livestream; + self + } + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); let has_focus = cx.window_is_active(); - let player_list = static_players_with_call_status().into_iter(); + let player_list = if let Some(livestream) = &self.livestream { + livestream.players.clone().into_iter() + } else { + vec![].into_iter() + }; div() .flex() @@ -61,7 +79,8 @@ impl TitleBar { .child(Button::new("zed")) .child(Button::new("nate/gpui2-ui-components")), ) - .children(player_list.map(|p| PlayerStack::new(p))), + .children(player_list.map(|p| PlayerStack::new(p))) + .child(IconButton::new(Icon::Plus)), ) .child( div() diff --git a/crates/ui/src/components/toolbar.rs b/crates/ui/src/components/toolbar.rs index aedd634743..e0953bf3b2 100644 --- a/crates/ui/src/components/toolbar.rs +++ b/crates/ui/src/components/toolbar.rs @@ -1,33 +1,49 @@ use crate::prelude::*; -use crate::{theme, Breadcrumb, Icon, IconButton}; +use crate::theme; #[derive(Clone)] pub struct ToolbarItem {} -#[derive(Element, Clone)] -pub struct Toolbar { - items: Vec, +#[derive(Element)] +pub struct Toolbar { + left_items: HackyChildren, + left_items_payload: HackyChildrenPayload, + right_items: HackyChildren, + right_items_payload: HackyChildrenPayload, } -impl Toolbar { - pub fn new() -> Self { - Self { items: Vec::new() } +impl Toolbar { + pub fn new( + left_items: HackyChildren, + left_items_payload: HackyChildrenPayload, + right_items: HackyChildren, + right_items_payload: HackyChildrenPayload, + ) -> Self { + Self { + left_items, + left_items_payload, + right_items, + right_items_payload, + } } - fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { let theme = theme(cx); div() + .fill(theme.highest.base.default.background) .p_2() .flex() .justify_between() - .child(Breadcrumb::new()) .child( div() .flex() - .child(IconButton::new(Icon::InlayHint)) - .child(IconButton::new(Icon::MagnifyingGlass)) - .child(IconButton::new(Icon::MagicWand)), + .children_any((self.left_items)(cx, self.left_items_payload.as_ref())), + ) + .child( + div() + .flex() + .children_any((self.right_items)(cx, self.right_items_payload.as_ref())), ) } } diff --git a/crates/ui/src/components/workspace.rs b/crates/ui/src/components/workspace.rs index 0c6331dc9b..b609546f7f 100644 --- a/crates/ui/src/components/workspace.rs +++ b/crates/ui/src/components/workspace.rs @@ -1,10 +1,15 @@ +use std::sync::Arc; + use chrono::DateTime; use gpui2::geometry::{relative, rems, Size}; -use crate::prelude::*; use crate::{ - theme, v_stack, ChatMessage, ChatPanel, Pane, PaneGroup, Panel, PanelAllowedSides, PanelSide, - ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar, + hello_world_rust_editor_with_status_example, prelude::*, random_players_with_call_status, + Livestream, +}; +use crate::{ + theme, v_stack, ChatMessage, ChatPanel, EditorPane, Pane, PaneGroup, Panel, PanelAllowedSides, + PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar, }; #[derive(Element, Default)] @@ -17,6 +22,8 @@ pub struct WorkspaceElement { impl WorkspaceElement { fn render(&mut self, _: &mut V, cx: &mut ViewContext) -> impl IntoElement { + let theme = theme(cx).clone(); + let temp_size = rems(36.).into(); let root_group = PaneGroup::new_groups( @@ -29,8 +36,15 @@ impl WorkspaceElement { width: relative(1.).into(), height: temp_size, }, - |_, _| vec![Terminal::new().into_any()], - Box::new(()), + |_, payload| { + let theme = payload.downcast_ref::>().unwrap(); + + vec![EditorPane::new(hello_world_rust_editor_with_status_example( + &theme, + )) + .into_any()] + }, + Box::new(theme.clone()), ), Pane::new( ScrollState::default(), @@ -51,8 +65,15 @@ impl WorkspaceElement { width: relative(1.).into(), height: relative(1.).into(), }, - |_, _| vec![Terminal::new().into_any()], - Box::new(()), + |_, payload| { + let theme = payload.downcast_ref::>().unwrap(); + + vec![EditorPane::new(hello_world_rust_editor_with_status_example( + &theme, + )) + .into_any()] + }, + Box::new(theme.clone()), )], SplitDirection::Vertical, ), @@ -60,8 +81,6 @@ impl WorkspaceElement { SplitDirection::Horizontal, ); - let theme = theme(cx).clone(); - div() .size_full() .flex() @@ -72,7 +91,10 @@ impl WorkspaceElement { .items_start() .text_color(theme.lowest.base.default.foreground) .fill(theme.lowest.base.default.background) - .child(TitleBar::new(cx)) + .child(TitleBar::new(cx).set_livestream(Some(Livestream { + players: random_players_with_call_status(7), + channel: Some("gpui2-ui".to_string()), + }))) .child( div() .flex_1() @@ -84,8 +106,12 @@ impl WorkspaceElement { .border_b() .border_color(theme.lowest.base.default.border) .child( - ProjectPanel::new(self.left_panel_scroll_state.clone()) - .side(PanelSide::Left), + Panel::new( + self.left_panel_scroll_state.clone(), + |_, payload| vec![ProjectPanel::new(ScrollState::default()).into_any()], + Box::new(()), + ) + .side(PanelSide::Left), ) .child( v_stack() @@ -110,26 +136,37 @@ impl WorkspaceElement { .side(PanelSide::Bottom), ), ) - .child(ChatPanel::new(ScrollState::default()).with_messages(vec![ - ChatMessage::new( - "osiewicz".to_string(), - "is this thing on?".to_string(), - DateTime::parse_from_rfc3339( - "2023-09-27T15:40:52.707Z", - ) - .unwrap() - .naive_local(), - ), - ChatMessage::new( - "maxdeviant".to_string(), - "Reading you loud and clear!".to_string(), - DateTime::parse_from_rfc3339( - "2023-09-28T15:40:52.707Z", - ) - .unwrap() - .naive_local(), - ), - ])), + .child( + Panel::new( + self.right_panel_scroll_state.clone(), + |_, payload| { + vec![ChatPanel::new(ScrollState::default()) + .with_messages(vec![ + ChatMessage::new( + "osiewicz".to_string(), + "is this thing on?".to_string(), + DateTime::parse_from_rfc3339( + "2023-09-27T15:40:52.707Z", + ) + .unwrap() + .naive_local(), + ), + ChatMessage::new( + "maxdeviant".to_string(), + "Reading you loud and clear!".to_string(), + DateTime::parse_from_rfc3339( + "2023-09-28T15:40:52.707Z", + ) + .unwrap() + .naive_local(), + ), + ]) + .into_any()] + }, + Box::new(()), + ) + .side(PanelSide::Right), + ), ) .child(StatusBar::new()) } diff --git a/crates/ui/src/elements/icon.rs b/crates/ui/src/elements/icon.rs index ca357b4f02..6d4053a4ae 100644 --- a/crates/ui/src/elements/icon.rs +++ b/crates/ui/src/elements/icon.rs @@ -84,6 +84,7 @@ pub enum Icon { Plus, Quote, Screen, + SelectAll, Split, SplitMessage, Terminal, @@ -131,6 +132,7 @@ impl Icon { Icon::Plus => "icons/plus.svg", Icon::Quote => "icons/quote.svg", Icon::Screen => "icons/desktop.svg", + Icon::SelectAll => "icons/select-all.svg", Icon::Split => "icons/split.svg", Icon::SplitMessage => "icons/split_message.svg", Icon::Terminal => "icons/terminal.svg", diff --git a/crates/ui/src/elements/input.rs b/crates/ui/src/elements/input.rs index 1a860028d2..fd860f30c2 100644 --- a/crates/ui/src/elements/input.rs +++ b/crates/ui/src/elements/input.rs @@ -81,6 +81,7 @@ impl Input { div() .h_7() + .w_full() .px_2() .border() .border_color(border_color_default) diff --git a/crates/ui/src/elements/player.rs b/crates/ui/src/elements/player.rs index e9e269a2cb..465542dc7f 100644 --- a/crates/ui/src/elements/player.rs +++ b/crates/ui/src/elements/player.rs @@ -65,7 +65,7 @@ impl PlayerCallStatus { } } -#[derive(Clone)] +#[derive(PartialEq, Clone)] pub struct Player { index: usize, avatar_src: String, @@ -73,6 +73,7 @@ pub struct Player { status: PlayerStatus, } +#[derive(Clone)] pub struct PlayerWithCallStatus { player: Player, call_status: PlayerCallStatus, diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index c3cecbfc61..b19b2becd1 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -2,7 +2,7 @@ pub use gpui2::elements::div::{div, ScrollState}; pub use gpui2::style::{StyleHelpers, Styleable}; pub use gpui2::{Element, IntoElement, ParentElement, ViewContext}; -pub use crate::{theme, ButtonVariant, HackyChildren, HackyChildrenPayload, InputVariant}; +pub use crate::{theme, ButtonVariant, HackyChildren, HackyChildrenPayload, InputVariant, Theme}; use gpui2::{hsla, rgb, Hsla, WindowContext}; use strum::EnumIter; @@ -40,8 +40,7 @@ pub enum HighlightColor { } impl HighlightColor { - pub fn hsla(&self, cx: &WindowContext) -> Hsla { - let theme = theme(cx); + pub fn hsla(&self, theme: &Theme) -> Hsla { let system_color = SystemColor::new(); match self { @@ -74,7 +73,7 @@ impl HighlightColor { } } -#[derive(Default, PartialEq, EnumIter)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum FileSystemStatus { #[default] None, @@ -92,7 +91,7 @@ impl FileSystemStatus { } } -#[derive(Default, PartialEq, EnumIter, Clone, Copy)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum GitStatus { #[default] None, @@ -130,7 +129,7 @@ impl GitStatus { } } -#[derive(Default, PartialEq)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum DiagnosticStatus { #[default] None, @@ -139,14 +138,14 @@ pub enum DiagnosticStatus { Info, } -#[derive(Default, PartialEq)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum IconSide { #[default] Left, Right, } -#[derive(Default, PartialEq)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum OrderMethod { #[default] Ascending, @@ -154,14 +153,14 @@ pub enum OrderMethod { MostRecent, } -#[derive(Default, PartialEq, Clone, Copy)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum Shape { #[default] Circle, RoundedRectangle, } -#[derive(Default, PartialEq, Clone, Copy)] +#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] pub enum DisclosureControlVisibility { #[default] OnHover, diff --git a/crates/ui/src/static_data.rs b/crates/ui/src/static_data.rs index fed2d40a73..b8c4e18f14 100644 --- a/crates/ui/src/static_data.rs +++ b/crates/ui/src/static_data.rs @@ -1,12 +1,109 @@ -use gpui2::WindowContext; +use std::path::PathBuf; +use std::str::FromStr; + +use rand::Rng; use crate::{ - Buffer, BufferRow, BufferRows, GitStatus, HighlightColor, HighlightedLine, HighlightedText, - Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem, MicStatus, - ModifierKeys, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, - ToggleState, + Buffer, BufferRow, BufferRows, Editor, FileSystemStatus, GitStatus, HighlightColor, + HighlightedLine, HighlightedText, Icon, Keybinding, Label, LabelColor, ListEntry, + ListEntrySize, ListItem, Livestream, MicStatus, ModifierKeys, PaletteItem, Player, + PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, Theme, ToggleState, + VideoStatus, }; +pub fn static_tabs_example() -> Vec { + vec![ + Tab::new() + .title("wip.rs".to_string()) + .icon(Icon::FileRust) + .current(false) + .fs_status(FileSystemStatus::Deleted), + Tab::new() + .title("Cargo.toml".to_string()) + .icon(Icon::FileToml) + .current(false) + .git_status(GitStatus::Modified), + Tab::new() + .title("Channels Panel".to_string()) + .icon(Icon::Hash) + .current(false), + Tab::new() + .title("channels_panel.rs".to_string()) + .icon(Icon::FileRust) + .current(true) + .git_status(GitStatus::Modified), + Tab::new() + .title("workspace.rs".to_string()) + .current(false) + .icon(Icon::FileRust) + .git_status(GitStatus::Modified), + Tab::new() + .title("icon_button.rs".to_string()) + .icon(Icon::FileRust) + .current(false), + Tab::new() + .title("storybook.rs".to_string()) + .icon(Icon::FileRust) + .current(false) + .git_status(GitStatus::Created), + Tab::new() + .title("theme.rs".to_string()) + .icon(Icon::FileRust) + .current(false), + Tab::new() + .title("theme_registry.rs".to_string()) + .icon(Icon::FileRust) + .current(false), + Tab::new() + .title("styleable_helpers.rs".to_string()) + .icon(Icon::FileRust) + .current(false), + ] +} + +pub fn static_tabs_1() -> Vec { + vec![ + Tab::new() + .title("project_panel.rs".to_string()) + .icon(Icon::FileRust) + .current(false) + .fs_status(FileSystemStatus::Deleted), + Tab::new() + .title("tab_bar.rs".to_string()) + .icon(Icon::FileRust) + .current(false) + .git_status(GitStatus::Modified), + Tab::new() + .title("workspace.rs".to_string()) + .icon(Icon::FileRust) + .current(false), + Tab::new() + .title("tab.rs".to_string()) + .icon(Icon::FileRust) + .current(true) + .git_status(GitStatus::Modified), + ] +} + +pub fn static_tabs_2() -> Vec { + vec![ + Tab::new() + .title("tab_bar.rs".to_string()) + .icon(Icon::FileRust) + .current(false) + .fs_status(FileSystemStatus::Deleted), + Tab::new() + .title("static_data.rs".to_string()) + .icon(Icon::FileRust) + .current(true) + .git_status(GitStatus::Modified), + ] +} + +pub fn static_tabs_3() -> Vec { + vec![Tab::new().git_status(GitStatus::Created).current(true)] +} + pub fn static_players() -> Vec { vec![ Player::new( @@ -37,6 +134,154 @@ pub fn static_players() -> Vec { ] } +#[derive(Debug)] +pub struct PlayerData { + pub url: String, + pub name: String, +} +pub fn static_player_data() -> Vec { + vec![ + PlayerData { + url: "https://avatars.githubusercontent.com/u/1714999?v=4".into(), + name: "iamnbutler".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/326587?v=4".into(), + name: "maxbrunsfeld".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/482957?v=4".into(), + name: "as-cii".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/1789?v=4".into(), + name: "nathansobo".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(), + name: "ForLoveOfCats".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/2690773?v=4".into(), + name: "SomeoneToIgnore".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/19867440?v=4".into(), + name: "JosephTLyons".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/24362066?v=4".into(), + name: "osiewicz".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/22121886?v=4".into(), + name: "KCaverly".into(), + }, + PlayerData { + url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(), + name: "maxdeviant".into(), + }, + ] +} +pub fn create_static_players(player_data: Vec) -> Vec { + let mut players = Vec::new(); + for data in player_data { + players.push(Player::new(players.len(), data.url, data.name)); + } + players +} +pub fn static_player_1(data: &Vec) -> Player { + Player::new(1, data[0].url.clone(), data[0].name.clone()) +} +pub fn static_player_2(data: &Vec) -> Player { + Player::new(2, data[1].url.clone(), data[1].name.clone()) +} +pub fn static_player_3(data: &Vec) -> Player { + Player::new(3, data[2].url.clone(), data[2].name.clone()) +} +pub fn static_player_4(data: &Vec) -> Player { + Player::new(4, data[3].url.clone(), data[3].name.clone()) +} +pub fn static_player_5(data: &Vec) -> Player { + Player::new(5, data[4].url.clone(), data[4].name.clone()) +} +pub fn static_player_6(data: &Vec) -> Player { + Player::new(6, data[5].url.clone(), data[5].name.clone()) +} +pub fn static_player_7(data: &Vec) -> Player { + Player::new(7, data[6].url.clone(), data[6].name.clone()) +} +pub fn static_player_8(data: &Vec) -> Player { + Player::new(8, data[7].url.clone(), data[7].name.clone()) +} +pub fn static_player_9(data: &Vec) -> Player { + Player::new(9, data[8].url.clone(), data[8].name.clone()) +} +pub fn static_player_10(data: &Vec) -> Player { + Player::new(10, data[9].url.clone(), data[9].name.clone()) +} +pub fn static_livestream() -> Livestream { + Livestream { + players: random_players_with_call_status(7), + channel: Some("gpui2-ui".to_string()), + } +} +pub fn populate_player_call_status( + player: Player, + followers: Option>, +) -> PlayerCallStatus { + let mut rng = rand::thread_rng(); + let in_current_project: bool = rng.gen(); + let disconnected: bool = rng.gen(); + let voice_activity: f32 = rng.gen(); + let mic_status = if rng.gen_bool(0.5) { + MicStatus::Muted + } else { + MicStatus::Unmuted + }; + let video_status = if rng.gen_bool(0.5) { + VideoStatus::On + } else { + VideoStatus::Off + }; + let screen_share_status = if rng.gen_bool(0.5) { + ScreenShareStatus::Shared + } else { + ScreenShareStatus::NotShared + }; + PlayerCallStatus { + mic_status, + voice_activity, + video_status, + screen_share_status, + in_current_project, + disconnected, + following: None, + followers, + } +} +pub fn random_players_with_call_status(number_of_players: usize) -> Vec { + let players = create_static_players(static_player_data()); + let mut player_status = vec![]; + for i in 0..number_of_players { + let followers = if i == 0 { + Some(vec![ + players[1].clone(), + players[3].clone(), + players[5].clone(), + players[6].clone(), + ]) + } else if i == 1 { + Some(vec![players[2].clone(), players[6].clone()]) + } else { + None + }; + let call_status = populate_player_call_status(players[i].clone(), followers); + player_status.push(PlayerWithCallStatus::new(players[i].clone(), call_status)); + } + player_status +} + pub fn static_players_with_call_status() -> Vec { let players = static_players(); let mut player_0_status = PlayerCallStatus::new(); @@ -123,7 +368,7 @@ pub fn static_project_panel_project_items() -> Vec { .left_icon(Icon::FolderOpen.into()) .indent_level(3) .set_toggle(ToggleState::Toggled), - ListEntry::new(Label::new("derrive_element.rs")) + ListEntry::new(Label::new("derive_element.rs")) .left_icon(Icon::FileRust.into()) .indent_level(4), ListEntry::new(Label::new("storybook").color(LabelColor::Modified)) @@ -337,33 +582,49 @@ pub fn example_editor_actions() -> Vec { ] } -pub fn empty_buffer_example() -> Buffer { +pub fn empty_editor_example() -> Editor { + Editor { + tabs: static_tabs_example(), + path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(), + symbols: vec![], + buffer: empty_buffer_example(), + } +} + +pub fn empty_buffer_example() -> Buffer { Buffer::new().set_rows(Some(BufferRows::default())) } -pub fn hello_world_rust_buffer_example(cx: &WindowContext) -> Buffer { +pub fn hello_world_rust_editor_example(theme: &Theme) -> Editor { + Editor { + tabs: static_tabs_example(), + path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(), + symbols: vec![Symbol(vec![ + HighlightedText { + text: "fn ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "main".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + ])], + buffer: hello_world_rust_buffer_example(theme), + } +} + +pub fn hello_world_rust_buffer_example(theme: &Theme) -> Buffer { Buffer::new() .set_title("hello_world.rs".to_string()) .set_path("src/hello_world.rs".to_string()) .set_language("rust".to_string()) .set_rows(Some(BufferRows { show_line_numbers: true, - rows: hello_world_rust_buffer_rows(cx), + rows: hello_world_rust_buffer_rows(theme), })) } -pub fn hello_world_rust_buffer_with_status_example(cx: &WindowContext) -> Buffer { - Buffer::new() - .set_title("hello_world.rs".to_string()) - .set_path("src/hello_world.rs".to_string()) - .set_language("rust".to_string()) - .set_rows(Some(BufferRows { - show_line_numbers: true, - rows: hello_world_rust_with_status_buffer_rows(cx), - })) -} - -pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec { +pub fn hello_world_rust_buffer_rows(theme: &Theme) -> Vec { let show_line_number = true; vec![ @@ -375,15 +636,15 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec { highlighted_texts: vec![ HighlightedText { text: "fn ".to_string(), - color: HighlightColor::Keyword.hsla(cx), + color: HighlightColor::Keyword.hsla(&theme), }, HighlightedText { text: "main".to_string(), - color: HighlightColor::Function.hsla(cx), + color: HighlightColor::Function.hsla(&theme), }, HighlightedText { text: "() {".to_string(), - color: HighlightColor::Default.hsla(cx), + color: HighlightColor::Default.hsla(&theme), }, ], }), @@ -399,7 +660,7 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec { highlighted_texts: vec![HighlightedText { text: " // Statements here are executed when the compiled binary is called." .to_string(), - color: HighlightColor::Comment.hsla(cx), + color: HighlightColor::Comment.hsla(&theme), }], }), cursors: None, @@ -422,7 +683,7 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec { line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { text: " // Print text to the console.".to_string(), - color: HighlightColor::Comment.hsla(cx), + color: HighlightColor::Comment.hsla(&theme), }], }), cursors: None, @@ -433,10 +694,34 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec { line_number: 5, code_action: false, current: false, + line: Some(HighlightedLine { + highlighted_texts: vec![ + HighlightedText { + text: " println!(".to_string(), + color: HighlightColor::Default.hsla(&theme), + }, + HighlightedText { + text: "\"Hello, world!\"".to_string(), + color: HighlightColor::String.hsla(&theme), + }, + HighlightedText { + text: ");".to_string(), + color: HighlightColor::Default.hsla(&theme), + }, + ], + }), + cursors: None, + status: GitStatus::None, + show_line_number, + }, + BufferRow { + line_number: 6, + code_action: false, + current: false, line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { text: "}".to_string(), - color: HighlightColor::Default.hsla(cx), + color: HighlightColor::Default.hsla(&theme), }], }), cursors: None, @@ -446,7 +731,36 @@ pub fn hello_world_rust_buffer_rows(cx: &WindowContext) -> Vec { ] } -pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec { +pub fn hello_world_rust_editor_with_status_example(theme: &Theme) -> Editor { + Editor { + tabs: static_tabs_example(), + path: PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(), + symbols: vec![Symbol(vec![ + HighlightedText { + text: "fn ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "main".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + ])], + buffer: hello_world_rust_buffer_with_status_example(theme), + } +} + +pub fn hello_world_rust_buffer_with_status_example(theme: &Theme) -> Buffer { + Buffer::new() + .set_title("hello_world.rs".to_string()) + .set_path("src/hello_world.rs".to_string()) + .set_language("rust".to_string()) + .set_rows(Some(BufferRows { + show_line_numbers: true, + rows: hello_world_rust_with_status_buffer_rows(theme), + })) +} + +pub fn hello_world_rust_with_status_buffer_rows(theme: &Theme) -> Vec { let show_line_number = true; vec![ @@ -458,15 +772,15 @@ pub fn hello_world_rust_with_status_buffer_rows(cx: &WindowContext) -> Vec Vec Vec Vec Vec Vec Vec Buffer { + Buffer::new() + .set_title("zed — fish".to_string()) + .set_rows(Some(BufferRows { + show_line_numbers: false, + rows: terminal_buffer_rows(theme), + })) +} + +pub fn terminal_buffer_rows(theme: &Theme) -> Vec { + let show_line_number = false; + + vec![ + BufferRow { + line_number: 1, + code_action: false, + current: false, + line: Some(HighlightedLine { + highlighted_texts: vec![ + HighlightedText { + text: "maxdeviant ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + HighlightedText { + text: "in ".to_string(), + color: HighlightColor::Default.hsla(&theme), + }, + HighlightedText { + text: "profaned-capital ".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + HighlightedText { + text: "in ".to_string(), + color: HighlightColor::Default.hsla(&theme), + }, + HighlightedText { + text: "~/p/zed ".to_string(), + color: HighlightColor::Function.hsla(&theme), + }, + HighlightedText { + text: "on ".to_string(), + color: HighlightColor::Default.hsla(&theme), + }, + HighlightedText { + text: " gpui2-ui ".to_string(), + color: HighlightColor::Keyword.hsla(&theme), + }, + ], + }), + cursors: None, + status: GitStatus::None, + show_line_number, + }, + BufferRow { + line_number: 2, + code_action: false, + current: false, + line: Some(HighlightedLine { + highlighted_texts: vec![HighlightedText { + text: "λ ".to_string(), + color: HighlightColor::String.hsla(&theme), + }], + }), + cursors: None, + status: GitStatus::None, + show_line_number, + }, + ] +} From 27d784b23e81a8f763587ebb3cb6fa09f4327e5f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 16:27:12 -0600 Subject: [PATCH 41/45] Fix bug in following Prior to this change you could only follow across workspaces when you were heading to the first window. --- crates/collab/src/tests/following_tests.rs | 132 +++++++++++++++--- .../src/project_shared_notification.rs | 8 +- crates/workspace/src/workspace.rs | 15 +- 3 files changed, 118 insertions(+), 37 deletions(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 696923e505..657d71afd4 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -2,12 +2,10 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use call::ActiveCall; use collab_ui::project_shared_notification::ProjectSharedNotification; use editor::{Editor, ExcerptRange, MultiBuffer}; -use gpui::{ - executor::Deterministic, geometry::vector::vec2f, AppContext, TestAppContext, ViewHandle, -}; +use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle}; use live_kit_client::MacOSDisplay; use serde_json::json; -use std::sync::Arc; +use std::{borrow::Cow, sync::Arc}; use workspace::{ dock::{test::TestPanel, DockPosition}, item::{test::TestItem, ItemHandle as _}, @@ -1104,11 +1102,10 @@ async fn test_following_across_workspaces( // a shares project 1 // b shares project 2 // - // - // b joins project 1 - // - // test: when a is in project 2 and b clicks follow (from unshared project), b should open project 2 and follow a - // test: when a is in project 1 and b clicks follow, b should open project 1 and follow a + // b follows a: causes project 2 to be joined, and b to follow a. + // b opens a different file in project 2, a follows b + // b opens a different file in project 1, a cannot follow b + // b shares the project, a joins the project and follows b deterministic.forbid_parking(); let mut server = TestServer::start(&deterministic).await; let client_a = server.create_client(cx_a, "user_a").await; @@ -1153,16 +1150,10 @@ async fn test_following_across_workspaces( cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx)); cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx)); - let project_a_id = active_call_a + active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - /* - let project_b_id = active_call_b - .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) - .await - .unwrap(); - */ active_call_a .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) @@ -1173,18 +1164,14 @@ async fn test_following_across_workspaces( .await .unwrap(); - let editor_a = workspace_a + workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id_a, "w.rs"), None, true, cx) }) .await - .unwrap() - .downcast::() .unwrap(); deterministic.run_until_parked(); - assert_eq!(cx_b.windows().len(), 2); - assert_eq!(visible_push_notifications(cx_b).len(), 1); workspace_b.update(cx_b, |workspace, cx| { @@ -1205,14 +1192,115 @@ async fn test_following_across_workspaces( .root(cx_b); // assert that b is following a in project a in w.rs - workspace_b_project_a.update(cx_b, |workspace, _| { + workspace_b_project_a.update(cx_b, |workspace, cx| { assert!(workspace.is_being_followed(client_a.peer_id().unwrap())); assert_eq!( client_a.peer_id(), workspace.leader_for_pane(workspace.active_pane()) ); + let item = workspace.active_item(cx).unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("w.rs")); }); + // TODO: in app code, this would be done by the collab_ui. + active_call_b + .update(cx_b, |call, cx| { + let project = workspace_b_project_a.read(cx).project().clone(); + call.set_location(Some(&project), cx) + }) + .await + .unwrap(); + // assert that there are no share notifications open assert_eq!(visible_push_notifications(cx_b).len(), 0); + + // b moves to x.rs in a's project, and a follows + workspace_b_project_a + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_a, "x.rs"), None, true, cx) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + workspace_b_project_a.update(cx_b, |workspace, cx| { + let item = workspace.active_item(cx).unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs")); + }); + + workspace_a.update(cx_a, |workspace, cx| { + workspace + .follow(client_b.peer_id().unwrap(), cx) + .unwrap() + .detach() + }); + + deterministic.run_until_parked(); + workspace_a.update(cx_a, |workspace, cx| { + assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); + assert_eq!( + client_b.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + let item = workspace.active_pane().read(cx).active_item().unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs")); + }); + + // b moves to y.rs in b's project, a is still following but can't yet see + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_b, "y.rs"), None, true, cx) + }) + .await + .unwrap(); + + // TODO: in app code, this would be done by the collab_ui. + active_call_b + .update(cx_b, |call, cx| { + let project = workspace_b.read(cx).project().clone(); + call.set_location(Some(&project), cx) + }) + .await + .unwrap(); + + let project_b_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + .await + .unwrap(); + + deterministic.run_until_parked(); + assert_eq!(visible_push_notifications(cx_a).len(), 1); + cx_a.update(|cx| { + workspace::join_remote_project( + project_b_id, + client_b.user_id().unwrap(), + client_a.app_state.clone(), + cx, + ) + }) + .await + .unwrap(); + + deterministic.run_until_parked(); + + assert_eq!(visible_push_notifications(cx_a).len(), 0); + let workspace_a_project_b = cx_a + .windows() + .iter() + .max_by_key(|window| window.id()) + .unwrap() + .downcast::() + .unwrap() + .root(cx_a); + + workspace_a_project_b.update(cx_a, |workspace, cx| { + assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id)); + assert!(workspace.is_being_followed(client_b.peer_id().unwrap())); + assert_eq!( + client_b.peer_id(), + workspace.leader_for_pane(workspace.active_pane()) + ); + let item = workspace.active_item(cx).unwrap(); + assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("y.rs")); + }); } diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 5e362403f0..28ccee768b 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -41,6 +41,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { } } room::Event::RemoteProjectUnshared { project_id } + | room::Event::RemoteProjectJoined { project_id } | room::Event::RemoteProjectInvitationDiscarded { project_id } => { if let Some(windows) = notification_windows.remove(&project_id) { for window in windows { @@ -55,13 +56,6 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { } } } - room::Event::RemoteProjectJoined { project_id } => { - if let Some(windows) = notification_windows.remove(&project_id) { - for window in windows { - window.remove(cx); - } - } - } _ => {} }) .detach(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c90b175320..6e62a9bf16 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4246,21 +4246,20 @@ pub fn join_remote_project( cx: &mut AppContext, ) -> Task> { cx.spawn(|mut cx| async move { - let existing_workspace = cx - .windows() - .into_iter() - .find_map(|window| { - window.downcast::().and_then(|window| { - window.read_root_with(&cx, |workspace, cx| { + let windows = cx.windows(); + let existing_workspace = windows.into_iter().find_map(|window| { + window.downcast::().and_then(|window| { + window + .read_root_with(&cx, |workspace, cx| { if workspace.project().read(cx).remote_id() == Some(project_id) { Some(cx.handle().downgrade()) } else { None } }) - }) + .unwrap_or(None) }) - .flatten(); + }); let workspace = if let Some(existing_workspace) = existing_workspace { existing_workspace From 528fa5c57b69a6987c734aa8ce0a1dfcd9617b1f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 16:51:02 -0600 Subject: [PATCH 42/45] Refactor to remove toggle_follow --- .../collab/src/tests/channel_buffer_tests.rs | 4 +-- crates/collab/src/tests/following_tests.rs | 30 +++++++++---------- crates/workspace/src/workspace.rs | 22 +++++++------- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 05abda5af3..46005244c1 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -702,9 +702,7 @@ async fn test_following_to_channel_notes_without_a_shared_project( // Client B follows client A. workspace_b .update(cx_b, |workspace, cx| { - workspace - .toggle_follow(client_a.peer_id().unwrap(), cx) - .unwrap() + workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() }) .await .unwrap(); diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 657d71afd4..6d374b7920 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -126,7 +126,7 @@ async fn test_basic_following( // When client B starts following client A, all visible view states are replicated to client B. workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx).unwrap() + workspace.follow(peer_id_a, cx).unwrap() }) .await .unwrap(); @@ -166,7 +166,7 @@ async fn test_basic_following( // Client C also follows client A. workspace_c .update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx).unwrap() + workspace.follow(peer_id_a, cx).unwrap() }) .await .unwrap(); @@ -201,7 +201,7 @@ async fn test_basic_following( // Client C unfollows client A. workspace_c.update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx); + workspace.unfollow(&workspace.active_pane().clone(), cx); }); // All clients see that clients B is following client A. @@ -224,7 +224,7 @@ async fn test_basic_following( // Client C re-follows client A. workspace_c.update(cx_c, |workspace, cx| { - workspace.toggle_follow(peer_id_a, cx); + workspace.follow(peer_id_a, cx); }); // All clients see that clients B and C are following client A. @@ -248,7 +248,7 @@ async fn test_basic_following( // Client D follows client C. workspace_d .update(cx_d, |workspace, cx| { - workspace.toggle_follow(peer_id_c, cx).unwrap() + workspace.follow(peer_id_c, cx).unwrap() }) .await .unwrap(); @@ -439,7 +439,7 @@ async fn test_basic_following( // Client A starts following client B. workspace_a .update(cx_a, |workspace, cx| { - workspace.toggle_follow(peer_id_b, cx).unwrap() + workspace.follow(peer_id_b, cx).unwrap() }) .await .unwrap(); @@ -644,7 +644,7 @@ async fn test_following_tab_order( //Follow client B as client A workspace_a .update(cx_a, |workspace, cx| { - workspace.toggle_follow(client_b_id, cx).unwrap() + workspace.follow(client_b_id, cx).unwrap() }) .await .unwrap(); @@ -756,7 +756,7 @@ async fn test_peers_following_each_other( .update(cx_a, |workspace, cx| { assert_ne!(*workspace.active_pane(), pane_a1); let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); - workspace.toggle_follow(leader_id, cx).unwrap() + workspace.follow(leader_id, cx).unwrap() }) .await .unwrap(); @@ -767,7 +767,7 @@ async fn test_peers_following_each_other( .update(cx_b, |workspace, cx| { assert_ne!(*workspace.active_pane(), pane_b1); let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); - workspace.toggle_follow(leader_id, cx).unwrap() + workspace.follow(leader_id, cx).unwrap() }) .await .unwrap(); @@ -914,7 +914,7 @@ async fn test_auto_unfollowing( }); workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() + workspace.follow(leader_id, cx).unwrap() }) .await .unwrap(); @@ -939,7 +939,7 @@ async fn test_auto_unfollowing( workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() + workspace.follow(leader_id, cx).unwrap() }) .await .unwrap(); @@ -957,7 +957,7 @@ async fn test_auto_unfollowing( workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() + workspace.follow(leader_id, cx).unwrap() }) .await .unwrap(); @@ -977,7 +977,7 @@ async fn test_auto_unfollowing( workspace_b .update(cx_b, |workspace, cx| { - workspace.toggle_follow(leader_id, cx).unwrap() + workspace.follow(leader_id, cx).unwrap() }) .await .unwrap(); @@ -1053,10 +1053,10 @@ async fn test_peers_simultaneously_following_each_other( }); let a_follow_b = workspace_a.update(cx_a, |workspace, cx| { - workspace.toggle_follow(client_b_id, cx).unwrap() + workspace.follow(client_b_id, cx).unwrap() }); let b_follow_a = workspace_b.update(cx_b, |workspace, cx| { - workspace.toggle_follow(client_a_id, cx).unwrap() + workspace.follow(client_a_id, cx).unwrap() }); futures::try_join!(a_follow_b, b_follow_a).unwrap(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6e62a9bf16..f7bb409229 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2520,19 +2520,13 @@ impl Workspace { cx.notify(); } - pub fn toggle_follow( + fn start_following( &mut self, leader_id: PeerId, cx: &mut ViewContext, ) -> Option>> { let pane = self.active_pane().clone(); - if let Some(prev_leader_id) = self.unfollow(&pane, cx) { - if leader_id == prev_leader_id { - return None; - } - } - self.last_leaders_by_pane .insert(pane.downgrade(), leader_id); self.follower_states_by_leader @@ -2603,9 +2597,15 @@ impl Workspace { None }; - next_leader_id - .or_else(|| collaborators.keys().copied().next()) - .and_then(|leader_id| self.toggle_follow(leader_id, cx)) + let pane = self.active_pane.clone(); + let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next()) + else { + return None; + }; + if Some(leader_id) == self.unfollow(&pane, cx) { + return None; + } + self.follow(leader_id, cx) } pub fn follow( @@ -2654,7 +2654,7 @@ impl Workspace { } // Otherwise, follow. - self.toggle_follow(leader_id, cx) + self.start_following(leader_id, cx) } pub fn unfollow( From 892350fa2d6894290edd68ab2520da1d9e21636f Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 2 Oct 2023 19:35:31 -0400 Subject: [PATCH 43/45] Add memory and cpu events Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- Cargo.lock | 5 ++-- Cargo.toml | 1 + crates/client/Cargo.toml | 9 +++--- crates/client/src/telemetry.rs | 55 +++++++++++++++++++++++++++++++++- crates/feedback/Cargo.toml | 2 +- crates/zed/src/main.rs | 2 +- 6 files changed, 65 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76de671620..3b714455ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1415,6 +1415,7 @@ dependencies = [ "settings", "smol", "sum_tree", + "sysinfo", "tempfile", "text", "thiserror", @@ -7606,9 +7607,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.27.8" +version = "0.29.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a902e9050fca0a5d6877550b769abd2bd1ce8c04634b941dbe2809735e1a1e33" +checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys 0.8.3", diff --git a/Cargo.toml b/Cargo.toml index f09d44e8da..801435ee2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,6 +111,7 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] } serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } smallvec = { version = "1.6", features = ["union"] } smol = { version = "1.2" } +sysinfo = "0.29.10" tempdir = { version = "0.3.7" } thiserror = { version = "1.0.29" } time = { version = "0.3", features = ["serde", "serde-well-known"] } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index e3038e5bcc..9e371ec8bd 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -33,15 +33,16 @@ parking_lot.workspace = true postage.workspace = true rand.workspace = true schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true smol.workspace = true +sysinfo.workspace = true +tempfile = "3" thiserror.workspace = true time.workspace = true tiny_http = "0.8" -uuid = { version = "1.1.2", features = ["v4"] } url = "2.2" -serde.workspace = true -serde_derive.workspace = true -tempfile = "3" +uuid = { version = "1.1.2", features = ["v4"] } [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 1596e6d850..8d51a3d1fe 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -4,6 +4,7 @@ use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration}; +use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt}; use tempfile::NamedTempFile; use util::http::HttpClient; use util::{channel::ReleaseChannel, TryFutureExt}; @@ -88,6 +89,16 @@ pub enum ClickhouseEvent { kind: AssistantKind, model: &'static str, }, + Cpu { + usage_as_percent: f32, + core_count: u32, + }, + Memory { + memory_in_bytes: u64, + virtual_memory_in_bytes: u64, + start_time_in_seconds: u64, + run_time_in_seconds: u64, + }, } #[cfg(debug_assertions)] @@ -136,7 +147,7 @@ impl Telemetry { Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) } - pub fn start(self: &Arc, installation_id: Option) { + pub fn start(self: &Arc, installation_id: Option, cx: &mut AppContext) { let mut state = self.state.lock(); state.installation_id = installation_id.map(|id| id.into()); let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); @@ -145,6 +156,48 @@ impl Telemetry { if has_clickhouse_events { self.flush_clickhouse_events(); } + + let this = self.clone(); + cx.spawn(|mut cx| async move { + let mut system = System::new_all(); + system.refresh_all(); + + loop { + // Waiting some amount of time before the first query is important to get a reasonable value + // https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage + const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60); + smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await; + + let telemetry_settings = cx.update(|cx| *settings::get::(cx)); + + system.refresh_memory(); + system.refresh_processes(); + + let current_process = Pid::from_u32(std::process::id()); + let Some(process) = system.processes().get(¤t_process) else { + let process = current_process; + log::error!("Failed to find own process {process:?} in system process table"); + // TODO: Fire an error telemetry event + return; + }; + + let memory_event = ClickhouseEvent::Memory { + memory_in_bytes: process.memory(), + virtual_memory_in_bytes: process.virtual_memory(), + start_time_in_seconds: process.start_time(), + run_time_in_seconds: process.run_time(), + }; + + let cpu_event = ClickhouseEvent::Cpu { + usage_as_percent: process.cpu_usage(), + core_count: system.cpus().len() as u32, + }; + + this.report_clickhouse_event(memory_event, telemetry_settings); + this.report_clickhouse_event(cpu_event, telemetry_settings); + } + }) + .detach(); } pub fn set_authenticated_user_info( diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index 07b6ad790c..651d32ba91 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -33,7 +33,7 @@ lazy_static.workspace = true postage.workspace = true serde.workspace = true serde_derive.workspace = true -sysinfo = "0.27.1" +sysinfo.workspace = true tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } urlencoding = "2.1.2" diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 7991cabde2..d6f3be2b46 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -177,7 +177,7 @@ fn main() { }) .detach(); - client.telemetry().start(installation_id); + client.telemetry().start(installation_id, cx); let app_state = Arc::new(AppState { languages, From d7867cd1e283080788117e61b5246478ccd8469d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 2 Oct 2023 19:38:45 -0600 Subject: [PATCH 44/45] Add/fix mouse interactions in current call sidebar --- crates/collab_ui/src/collab_panel.rs | 265 +++++++++++++++++---------- 1 file changed, 171 insertions(+), 94 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 16a9ec563b..22ab573974 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -47,7 +47,7 @@ use util::{iife, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, item::ItemHandle, - Workspace, + FollowNextCollaborator, Workspace, }; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -404,6 +404,7 @@ enum ListEntry { Header(Section), CallParticipant { user: Arc, + peer_id: Option, is_pending: bool, }, ParticipantProject { @@ -508,14 +509,19 @@ impl CollabPanel { let is_collapsed = this.collapsed_sections.contains(section); this.render_header(*section, &theme, is_selected, is_collapsed, cx) } - ListEntry::CallParticipant { user, is_pending } => { - Self::render_call_participant( - user, - *is_pending, - is_selected, - &theme.collab_panel, - ) - } + ListEntry::CallParticipant { + user, + peer_id, + is_pending, + } => Self::render_call_participant( + user, + *peer_id, + this.user_store.clone(), + *is_pending, + is_selected, + &theme, + cx, + ), ListEntry::ParticipantProject { project_id, worktree_root_names, @@ -528,7 +534,7 @@ impl CollabPanel { Some(*project_id) == current_project_id, *is_last, is_selected, - &theme.collab_panel, + &theme, cx, ), ListEntry::ParticipantScreen { peer_id, is_last } => { @@ -793,6 +799,7 @@ impl CollabPanel { let user_id = user.id; self.entries.push(ListEntry::CallParticipant { user, + peer_id: None, is_pending: false, }); let mut projects = room.local_participant().projects.iter().peekable(); @@ -830,6 +837,7 @@ impl CollabPanel { let participant = &room.remote_participants()[&user_id]; self.entries.push(ListEntry::CallParticipant { user: participant.user.clone(), + peer_id: Some(participant.peer_id), is_pending: false, }); let mut projects = participant.projects.iter().peekable(); @@ -871,6 +879,7 @@ impl CollabPanel { self.entries .extend(matches.iter().map(|mat| ListEntry::CallParticipant { user: room.pending_participants()[mat.candidate_id].clone(), + peer_id: None, is_pending: true, })); } @@ -1174,46 +1183,97 @@ impl CollabPanel { fn render_call_participant( user: &User, + peer_id: Option, + user_store: ModelHandle, is_pending: bool, is_selected: bool, - theme: &theme::CollabPanel, + theme: &theme::Theme, + cx: &mut ViewContext, ) -> AnyElement { - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::from_data(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - })) - .with_child( - Label::new( - user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true), - ) - .with_children(if is_pending { - Some( - Label::new("Calling", theme.calling_indicator.text.clone()) + enum CallParticipant {} + enum CallParticipantTooltip {} + + let collab_theme = &theme.collab_panel; + + let is_current_user = + user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); + + let content = + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + let style = if is_current_user { + *collab_theme + .contact_row + .in_state(is_selected) + .style_for(&mut Default::default()) + } else { + *collab_theme + .contact_row + .in_state(is_selected) + .style_for(mouse_state) + }; + + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::from_data(avatar) + .with_style(collab_theme.contact_avatar) + .aligned() + .left() + })) + .with_child( + Label::new( + user.github_login.clone(), + collab_theme.contact_username.text.clone(), + ) .contained() - .with_style(theme.calling_indicator.container) - .aligned(), - ) - } else { - None + .with_style(collab_theme.contact_username.container) + .aligned() + .left() + .flex(1., true), + ) + .with_children(if is_pending { + Some( + Label::new("Calling", collab_theme.calling_indicator.text.clone()) + .contained() + .with_style(collab_theme.calling_indicator.container) + .aligned(), + ) + } else if is_current_user { + Some( + Label::new("You", collab_theme.calling_indicator.text.clone()) + .contained() + .with_style(collab_theme.calling_indicator.container) + .aligned(), + ) + } else { + None + }) + .constrained() + .with_height(collab_theme.row_height) + .contained() + .with_style(style) + }); + + if is_current_user || is_pending || peer_id.is_none() { + return content.into_any(); + } + + let tooltip = format!("Follow {}", user.github_login); + + content + .on_click(MouseButton::Left, move |_, this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + workspace + .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx)) + .map(|task| task.detach_and_log_err(cx)); + } }) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style( - *theme - .contact_row - .in_state(is_selected) - .style_for(&mut Default::default()), + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + user.id as usize, + tooltip, + Some(Box::new(FollowNextCollaborator)), + theme.tooltip.clone(), + cx, ) .into_any() } @@ -1225,74 +1285,91 @@ impl CollabPanel { is_current: bool, is_last: bool, is_selected: bool, - theme: &theme::CollabPanel, + theme: &theme::Theme, cx: &mut ViewContext, ) -> AnyElement { enum JoinProject {} + enum JoinProjectTooltip {} - let host_avatar_width = theme + let collab_theme = &theme.collab_panel; + let host_avatar_width = collab_theme .contact_avatar .width - .or(theme.contact_avatar.height) + .or(collab_theme.contact_avatar.height) .unwrap_or(0.); - let tree_branch = theme.tree_branch; + let tree_branch = collab_theme.tree_branch; let project_name = if worktree_root_names.is_empty() { "untitled".to_string() } else { worktree_root_names.join(", ") }; - MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { - let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); - let row = theme - .project_row - .in_state(is_selected) - .style_for(mouse_state); + let content = + MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { + let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); + let row = if is_current { + collab_theme + .project_row + .in_state(true) + .style_for(&mut Default::default()) + } else { + collab_theme + .project_row + .in_state(is_selected) + .style_for(mouse_state) + }; - Flex::row() - .with_child(render_tree_branch( - tree_branch, - &row.name.text, - is_last, - vec2f(host_avatar_width, theme.row_height), - cx.font_cache(), - )) - .with_child( - Svg::new("icons/file_icons/folder.svg") - .with_color(theme.channel_hash.color) - .constrained() - .with_width(theme.channel_hash.width) - .aligned() - .left(), - ) - .with_child( - Label::new(project_name, row.name.text.clone()) - .aligned() - .left() - .contained() - .with_style(row.name.container) - .flex(1., false), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(row.container) - }) - .with_cursor_style(if !is_current { - CursorStyle::PointingHand - } else { - CursorStyle::Arrow - }) - .on_click(MouseButton::Left, move |_, this, cx| { - if !is_current { + Flex::row() + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + is_last, + vec2f(host_avatar_width, collab_theme.row_height), + cx.font_cache(), + )) + .with_child( + Svg::new("icons/file_icons/folder.svg") + .with_color(collab_theme.channel_hash.color) + .constrained() + .with_width(collab_theme.channel_hash.width) + .aligned() + .left(), + ) + .with_child( + Label::new(project_name.clone(), row.name.text.clone()) + .aligned() + .left() + .contained() + .with_style(row.name.container) + .flex(1., false), + ) + .constrained() + .with_height(collab_theme.row_height) + .contained() + .with_style(row.container) + }); + + if is_current { + return content.into_any(); + } + + content + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { if let Some(workspace) = this.workspace.upgrade(cx) { let app_state = workspace.read(cx).app_state().clone(); workspace::join_remote_project(project_id, host_user_id, app_state, cx) .detach_and_log_err(cx); } - } - }) - .into_any() + }) + .with_tooltip::( + project_id as usize, + format!("Open {}", project_name), + None, + theme.tooltip.clone(), + cx, + ) + .into_any() } fn render_participant_screen( From 9f160537ef4ea8fec1a82c45c7c70e62973b24f3 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 3 Oct 2023 11:56:45 +0300 Subject: [PATCH 45/45] move collapsed only matches outside item parent in embedding.scm --- .../semantic_index/src/semantic_index_tests.rs | 17 +++++++++++++++++ crates/zed/src/languages/rust/embedding.scm | 5 +++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index f2cae8a557..182010ca83 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -305,6 +305,11 @@ async fn test_code_context_retrieval_rust() { todo!(); } } + + #[derive(Clone)] + struct D { + name: String + } " .unindent(); @@ -361,6 +366,15 @@ async fn test_code_context_retrieval_rust() { .unindent(), text.find("fn function_2").unwrap(), ), + ( + " + #[derive(Clone)] + struct D { + name: String + }" + .unindent(), + text.find("struct D").unwrap(), + ), ], ); } @@ -1422,6 +1436,9 @@ fn rust_lang() -> Arc { name: (_) @name) ] @item ) + + (attribute_item) @collapse + (use_declaration) @collapse "#, ) .unwrap(), diff --git a/crates/zed/src/languages/rust/embedding.scm b/crates/zed/src/languages/rust/embedding.scm index c4ed7d2097..286b1d1357 100644 --- a/crates/zed/src/languages/rust/embedding.scm +++ b/crates/zed/src/languages/rust/embedding.scm @@ -2,8 +2,6 @@ [(line_comment) (attribute_item)]* @context . [ - (attribute_item) @collapse - (use_declaration) @collapse (struct_item name: (_) @name) @@ -29,3 +27,6 @@ name: (_) @name) ] @item ) + +(attribute_item) @collapse +(use_declaration) @collapse