From 28c39aae17c7e027e07b2198aa4c786f5ae64ea6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 2 Jan 2024 11:44:51 -0800 Subject: [PATCH 01/14] Start work on read-only project access for channel guests Co-authored-by: Conrad Co-authored-by: Mikayla --- crates/call/src/room.rs | 2 +- crates/collab/src/db/queries/channels.rs | 52 +++++------ crates/collab/src/tests.rs | 1 + .../collab/src/tests/channel_guest_tests.rs | 88 +++++++++++++++++++ crates/collab/src/tests/integration_tests.rs | 24 ++--- .../random_project_collaboration_tests.rs | 4 +- .../src/tests/randomized_test_helpers.rs | 2 +- crates/gpui/src/element.rs | 5 +- crates/gpui/src/platform/test/platform.rs | 5 +- crates/gpui/src/platform/test/window.rs | 16 +++- crates/project/src/project.rs | 10 ++- crates/workspace/src/workspace.rs | 6 +- 12 files changed, 159 insertions(+), 56 deletions(-) create mode 100644 crates/collab/src/tests/channel_guest_tests.rs diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 0dfdb50fc7201e1cfd9e03b68741d99b3b14bdc7..14d9a55ef03c92f13e1ecfb398963cf57db7710c 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1099,7 +1099,7 @@ impl Room { this.update(&mut cx, |this, cx| { this.joined_projects.retain(|project| { if let Some(project) = project.upgrade() { - !project.read(cx).is_read_only() + !project.read(cx).is_disconnected() } else { false } diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 780fb783bc84c9b0f741117122fc737689e98ab2..9a14aabfda3ba5d8449fea47397d92be5f217690 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -132,34 +132,34 @@ impl Database { debug_assert!( self.channel_role_for_user(&channel, user_id, &*tx).await? == role ); - } - } - - if channel.visibility == ChannelVisibility::Public { - role = Some(ChannelRole::Guest); - let channel_to_join = self - .public_ancestors_including_self(&channel, &*tx) - .await? - .first() - .cloned() - .unwrap_or(channel.clone()); - - channel_member::Entity::insert(channel_member::ActiveModel { - id: ActiveValue::NotSet, - channel_id: ActiveValue::Set(channel_to_join.id), - user_id: ActiveValue::Set(user_id), - accepted: ActiveValue::Set(true), - role: ActiveValue::Set(ChannelRole::Guest), - }) - .exec(&*tx) - .await?; + } else if channel.visibility == ChannelVisibility::Public { + role = Some(ChannelRole::Guest); + let channel_to_join = self + .public_ancestors_including_self(&channel, &*tx) + .await? + .first() + .cloned() + .unwrap_or(channel.clone()); + + channel_member::Entity::insert(channel_member::ActiveModel { + id: ActiveValue::NotSet, + channel_id: ActiveValue::Set(channel_to_join.id), + user_id: ActiveValue::Set(user_id), + accepted: ActiveValue::Set(true), + role: ActiveValue::Set(ChannelRole::Guest), + }) + .exec(&*tx) + .await?; - accept_invite_result = Some( - self.calculate_membership_updated(&channel_to_join, user_id, &*tx) - .await?, - ); + accept_invite_result = Some( + self.calculate_membership_updated(&channel_to_join, user_id, &*tx) + .await?, + ); - debug_assert!(self.channel_role_for_user(&channel, user_id, &*tx).await? == role); + debug_assert!( + self.channel_role_for_user(&channel, user_id, &*tx).await? == role + ); + } } if role.is_none() || role == Some(ChannelRole::Banned) { diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 53d42505bdc3babe023c4a8feb4dbbf5c5e24ab6..aca9329d5ad87a477614803de5143b86f1fe169b 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -2,6 +2,7 @@ use call::Room; use gpui::{Model, TestAppContext}; mod channel_buffer_tests; +mod channel_guest_tests; mod channel_message_tests; mod channel_tests; mod editor_tests; diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..78dd57be026913b3299efe2e1c35a94a5ea798ad --- /dev/null +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -0,0 +1,88 @@ +use crate::tests::TestServer; +use call::ActiveCall; +use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use rpc::proto; +use workspace::Workspace; + +#[gpui::test] +async fn test_channel_guests( + executor: BackgroundExecutor, + mut cx_a: &mut TestAppContext, + mut cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel("the-channel", None, (&client_a, cx_a), &mut []) + .await; + + client_a + .channel_store() + .update(cx_a, |channel_store, cx| { + channel_store.set_channel_visibility(channel_id, proto::ChannelVisibility::Public, cx) + }) + .await + .unwrap(); + + client_a + .fs() + .insert_tree( + "/a", + serde_json::json!({ + "a.txt": "a-contents", + }), + ) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + // Client A shares a project in the channel + active_call_a + .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap()); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + cx_a.executor().run_until_parked(); + + // Client B joins channel A as a guest + cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx)) + .await + .unwrap(); + + // b should be following a in the shared project. + // B is a guest, + cx_a.executor().run_until_parked(); + + // todo!() the test window does not call activation handlers + // correctly yet, so this API does not work. + // let project_b = active_call_b.read_with(cx_b, |call, _| { + // call.location() + // .unwrap() + // .upgrade() + // .expect("should not be weak") + // }); + + let window_b = cx_b.update(|cx| cx.active_window().unwrap()); + let cx_b = &mut VisualTestContext::from_window(window_b, cx_b); + + let workspace_b = window_b + .downcast::() + .unwrap() + .root_view(cx_b) + .unwrap(); + let project_b = workspace_b.update(cx_b, |workspace, _| workspace.project().clone()); + + assert_eq!( + project_b.read_with(cx_b, |project, _| project.remote_id()), + Some(project_id), + ); + assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())) +} diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 201ba07dbb319663cb2c6f1810c50faa87a077ab..e64fdbec832a07d1dc3753c059653f6ea954fb00 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1380,7 +1380,7 @@ async fn test_unshare_project( .unwrap(); executor.run_until_parked(); - assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); + assert!(project_b.read_with(cx_b, |project, _| project.is_disconnected())); // Client C opens the project. let project_c = client_c.build_remote_project(project_id, cx_c).await; @@ -1393,7 +1393,7 @@ async fn test_unshare_project( assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); - assert!(project_c.read_with(cx_c, |project, _| project.is_read_only())); + assert!(project_c.read_with(cx_c, |project, _| project.is_disconnected())); // Client C can open the project again after client A re-shares. let project_id = active_call_a @@ -1419,7 +1419,7 @@ async fn test_unshare_project( project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); project_c2.read_with(cx_c, |project, _| { - assert!(project.is_read_only()); + assert!(project.is_disconnected()); assert!(project.collaborators().is_empty()); }); } @@ -1551,7 +1551,7 @@ async fn test_project_reconnect( }); project_b1.read_with(cx_b, |project, _| { - assert!(!project.is_read_only()); + assert!(!project.is_disconnected()); assert_eq!(project.collaborators().len(), 1); }); @@ -1653,7 +1653,7 @@ async fn test_project_reconnect( }); project_b1.read_with(cx_b, |project, cx| { - assert!(!project.is_read_only()); + assert!(!project.is_disconnected()); assert_eq!( project .worktree_for_id(worktree1_id, cx) @@ -1687,9 +1687,9 @@ async fn test_project_reconnect( ); }); - project_b2.read_with(cx_b, |project, _| assert!(project.is_read_only())); + project_b2.read_with(cx_b, |project, _| assert!(project.is_disconnected())); - project_b3.read_with(cx_b, |project, _| assert!(!project.is_read_only())); + project_b3.read_with(cx_b, |project, _| assert!(!project.is_disconnected())); buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WaZ")); @@ -1746,7 +1746,7 @@ async fn test_project_reconnect( executor.run_until_parked(); project_b1.read_with(cx_b, |project, cx| { - assert!(!project.is_read_only()); + assert!(!project.is_disconnected()); assert_eq!( project .worktree_for_id(worktree1_id, cx) @@ -1780,7 +1780,7 @@ async fn test_project_reconnect( ); }); - project_b3.read_with(cx_b, |project, _| assert!(project.is_read_only())); + project_b3.read_with(cx_b, |project, _| assert!(project.is_disconnected())); buffer_a1.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "WXaYZ")); @@ -3535,7 +3535,7 @@ async fn test_leaving_project( }); project_b2.read_with(cx_b, |project, _| { - assert!(project.is_read_only()); + assert!(project.is_disconnected()); }); project_c.read_with(cx_c, |project, _| { @@ -3568,11 +3568,11 @@ async fn test_leaving_project( }); project_b2.read_with(cx_b, |project, _| { - assert!(project.is_read_only()); + assert!(project.is_disconnected()); }); project_c.read_with(cx_c, |project, _| { - assert!(project.is_read_only()); + assert!(project.is_disconnected()); }); } diff --git a/crates/collab/src/tests/random_project_collaboration_tests.rs b/crates/collab/src/tests/random_project_collaboration_tests.rs index f4194b98e8adbf41742a5aa279d766cf09c2477d..53d47eb6b5b44e9a5e34518bcc305c9e27ed399f 100644 --- a/crates/collab/src/tests/random_project_collaboration_tests.rs +++ b/crates/collab/src/tests/random_project_collaboration_tests.rs @@ -1149,7 +1149,7 @@ impl RandomizedTest for ProjectCollaborationTest { Some((project, cx)) }); - if !guest_project.is_read_only() { + if !guest_project.is_disconnected() { if let Some((host_project, host_cx)) = host_project { let host_worktree_snapshots = host_project.read_with(host_cx, |host_project, cx| { @@ -1236,7 +1236,7 @@ impl RandomizedTest for ProjectCollaborationTest { let buffers = client.buffers().clone(); for (guest_project, guest_buffers) in &buffers { let project_id = if guest_project.read_with(client_cx, |project, _| { - project.is_local() || project.is_read_only() + project.is_local() || project.is_disconnected() }) { continue; } else { diff --git a/crates/collab/src/tests/randomized_test_helpers.rs b/crates/collab/src/tests/randomized_test_helpers.rs index 91bd9cf6f698b3a8c6d436b99e35eaefc771e89e..69bec62460bbd2469fe497ce9418442b0f58ba92 100644 --- a/crates/collab/src/tests/randomized_test_helpers.rs +++ b/crates/collab/src/tests/randomized_test_helpers.rs @@ -518,7 +518,7 @@ impl TestPlan { for project in client.remote_projects().iter() { project.read_with(&client_cx, |project, _| { assert!( - project.is_read_only(), + project.is_disconnected(), "project {:?} should be read only", project.remote_id() ) diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index 30456f14a7ffeb35df54442e15304dcc68ed1958..987b91b791a93b9fb72672a4608c1b6665fc20e2 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -44,8 +44,9 @@ pub trait IntoElement: Sized { } /// Convert into an element, then draw in the current window at the given origin. - /// The provided available space is provided to the layout engine to determine the size of the root element. - /// Once the element is drawn, its associated element staet is yielded to the given callback. + /// The available space argument is provided to the layout engine to determine the size of the + // root element. Once the element is drawn, its associated element state is yielded to the + // given callback. fn draw_and_update_state( self, origin: Point, diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index cc683cacb68f0e471ef9b1fa5596581d81d3d0f3..142498ce4be88f05595d52109c12238eb8ef021f 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -19,7 +19,7 @@ pub struct TestPlatform { background_executor: BackgroundExecutor, foreground_executor: ForegroundExecutor, - active_window: Arc>>, + pub(crate) active_window: Arc>>, active_display: Rc, active_cursor: Mutex, current_clipboard_item: Mutex>, @@ -106,7 +106,7 @@ impl Platform for TestPlatform { } fn activate(&self, _ignoring_other_apps: bool) { - unimplemented!() + // } fn hide(&self) { @@ -142,6 +142,7 @@ impl Platform for TestPlatform { *self.active_window.lock() = Some(handle); Box::new(TestWindow::new( options, + handle, self.weak.clone(), self.active_display.clone(), )) diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 9df513d1f7ea46b0dffb452d27dbd82f5aba2176..dab53e46d9f10593104a08a0ea0d9517e7d2b911 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -1,7 +1,7 @@ use crate::{ - px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay, - PlatformInputHandler, PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance, - WindowBounds, WindowOptions, + px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, + PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, Size, TestPlatform, TileId, + WindowAppearance, WindowBounds, WindowOptions, }; use collections::HashMap; use parking_lot::Mutex; @@ -20,6 +20,7 @@ pub(crate) struct TestWindowHandlers { pub struct TestWindow { pub(crate) bounds: WindowBounds, + pub(crate) handle: AnyWindowHandle, display: Rc, pub(crate) title: Option, pub(crate) edited: bool, @@ -32,6 +33,7 @@ pub struct TestWindow { impl TestWindow { pub fn new( options: WindowOptions, + handle: AnyWindowHandle, platform: Weak, display: Rc, ) -> Self { @@ -39,6 +41,7 @@ impl TestWindow { bounds: options.bounds, display, platform, + handle, input_handler: None, sprite_atlas: Arc::new(TestAtlas::new()), handlers: Default::default(), @@ -107,7 +110,12 @@ impl PlatformWindow for TestWindow { } fn activate(&self) { - unimplemented!() + *self + .platform + .upgrade() + .expect("platform dropped") + .active_window + .lock() = Some(self.handle); } fn set_title(&mut self, title: &str) { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b9c73ae67785d48d7912414113329bb4a6d2e0da..a58a7b804f1120c07c4cabc5c8a5b7e3126acbf9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1659,7 +1659,7 @@ impl Project { cx.emit(Event::Closed); } - pub fn is_read_only(&self) -> bool { + pub fn is_disconnected(&self) -> bool { match &self.client_state { Some(ProjectClientState::Remote { sharing_has_stopped, @@ -1669,6 +1669,10 @@ impl Project { } } + pub fn is_read_only(&self) -> bool { + self.is_disconnected() + } + pub fn is_local(&self) -> bool { match &self.client_state { Some(ProjectClientState::Remote { .. }) => false, @@ -6015,7 +6019,7 @@ impl Project { this.upgrade().context("project dropped")?; let response = rpc.request(message).await?; let this = this.upgrade().context("project dropped")?; - if this.update(&mut cx, |this, _| this.is_read_only())? { + if this.update(&mut cx, |this, _| this.is_disconnected())? { Err(anyhow!("disconnected before completing request")) } else { request @@ -7942,7 +7946,7 @@ impl Project { if let Some(buffer) = buffer { break buffer; - } else if this.update(&mut cx, |this, _| this.is_read_only())? { + } else if this.update(&mut cx, |this, _| this.is_disconnected())? { return Err(anyhow!("disconnected before buffer {} could be opened", id)); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 76715f69bef9ffb5e7f4ced25d00372df897b7e5..7c2ca8a1f79c857270a30c7f98f3ada4507d32e8 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1184,7 +1184,7 @@ impl Workspace { mut save_intent: SaveIntent, cx: &mut ViewContext, ) -> Task> { - if self.project.read(cx).is_read_only() { + if self.project.read(cx).is_disconnected() { return Task::ready(Ok(true)); } let dirty_items = self @@ -2508,7 +2508,7 @@ impl Workspace { } fn update_window_edited(&mut self, cx: &mut ViewContext) { - let is_edited = !self.project.read(cx).is_read_only() + let is_edited = !self.project.read(cx).is_disconnected() && self .items(cx) .any(|item| item.has_conflict(cx) || item.is_dirty(cx)); @@ -3632,7 +3632,7 @@ impl Render for Workspace { })), ) .child(self.status_bar.clone()) - .children(if self.project.read(cx).is_read_only() { + .children(if self.project.read(cx).is_disconnected() { Some(DisconnectedOverlay) } else { None From a801c85a1ba6f54fb077e936f4cb4fea063339a5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 2 Jan 2024 15:51:36 -0700 Subject: [PATCH 02/14] TEMP --- crates/rpc/proto/zed.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 5ae127e2775af712895353d76e9a5c474f02b509..21fe81ca1b16cf32061ffa6b5e88593bf79fc25d 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -368,7 +368,7 @@ message JoinProject { } message JoinProjectResponse { - uint32 replica_id = 1; + optional uint32 replica_id = 1; repeated WorktreeMetadata worktrees = 2; repeated Collaborator collaborators = 3; repeated LanguageServer language_servers = 4; From 88ed5f7290c19c10c18143e988289ac262307a94 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 2 Jan 2024 20:14:30 -0700 Subject: [PATCH 03/14] Plumbing to pass `role` for room participants --- crates/collab/migrations.sqlite/20221109000000_test_schema.sql | 3 ++- .../20240103025509_add_role_to_room_participants.sql | 1 + crates/collab/src/db/queries/rooms.rs | 2 ++ crates/collab/src/db/tables/room_participant.rs | 3 ++- crates/collab/src/rpc.rs | 2 +- crates/project/src/project.rs | 3 ++- crates/rpc/proto/zed.proto | 1 + 7 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 crates/collab/migrations/20240103025509_add_role_to_room_participants.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 775a4c1bbe23be4ca05043d06567889a3a8c87cb..9bbbf88dac9879bf12dee3c99c35c8b18ca8d527 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -161,7 +161,8 @@ 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, - "participant_index" INTEGER + "participant_index" INTEGER, + "role" TEXT ); 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/20240103025509_add_role_to_room_participants.sql b/crates/collab/migrations/20240103025509_add_role_to_room_participants.sql new file mode 100644 index 0000000000000000000000000000000000000000..2748e00ebaa18ec375111c648a7accafe90c5dbb --- /dev/null +++ b/crates/collab/migrations/20240103025509_add_role_to_room_participants.sql @@ -0,0 +1 @@ +ALTER TABLE room_participants ADD COLUMN role TEXT; diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 40fdf5d58f184a0444f0c82938ab8fcd2a7bbb69..12d8940d7c3179e0f7c19881dbfea970a64b910f 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -1126,6 +1126,7 @@ impl Database { projects: Default::default(), location: Some(proto::ParticipantLocation { variant: location }), participant_index: participant_index as u32, + role: db_participant.role.unwrap_or(ChannelRole::Member).into(), }, ); } else { @@ -1137,6 +1138,7 @@ impl Database { } } drop(db_participants); + dbg!(&participants); let mut db_projects = db_room .find_related(project::Entity) diff --git a/crates/collab/src/db/tables/room_participant.rs b/crates/collab/src/db/tables/room_participant.rs index 4c5b8cc11c7a23532de3e7d0ea61f55fe3a4077f..c562111e96957c2457e421f8d7d2a95b9c6c2385 100644 --- a/crates/collab/src/db/tables/room_participant.rs +++ b/crates/collab/src/db/tables/room_participant.rs @@ -1,4 +1,4 @@ -use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId}; +use crate::db::{ChannelRole, ProjectId, RoomId, RoomParticipantId, ServerId, UserId}; use rpc::ConnectionId; use sea_orm::entity::prelude::*; @@ -19,6 +19,7 @@ pub struct Model { pub calling_connection_id: i32, pub calling_connection_server_id: Option, pub participant_index: Option, + pub role: Option, } impl Model { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 835b48809da94dc60cd872d473e564a7456da81e..8bb33cae296016dc241c03304154e14266c02f23 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1504,7 +1504,7 @@ async fn join_project( // First, we send the metadata associated with each worktree. response.send(proto::JoinProjectResponse { worktrees: worktrees.clone(), - replica_id: replica_id.0 as u32, + replica_id: Some(replica_id.0 as u32), collaborators: collaborators.clone(), language_servers: project.language_servers.clone(), })?; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a58a7b804f1120c07c4cabc5c8a5b7e3126acbf9..8b3abff0538f970c72555915d831f090c664f82a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -713,7 +713,8 @@ impl Project { }) .await?; let this = cx.new_model(|cx| { - let replica_id = response.payload.replica_id as ReplicaId; + // todo!() + let replica_id = response.payload.replica_id.unwrap() as ReplicaId; let mut worktrees = Vec::new(); for worktree in response.payload.worktrees { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 21fe81ca1b16cf32061ffa6b5e88593bf79fc25d..84d03727c0c59f61d54c59c4f688aeb71e392e87 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -269,6 +269,7 @@ message Participant { repeated ParticipantProject projects = 3; ParticipantLocation location = 4; uint32 participant_index = 5; + ChannelRole role = 6; } message PendingParticipant { From bf304b3fe7c345000ec06946416a3f26d3a87930 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 2 Jan 2024 21:07:46 -0700 Subject: [PATCH 04/14] Track room participant role (Also wire that through to project collaboration rules for now) --- crates/call/src/participant.rs | 1 + crates/call/src/room.rs | 35 +++++++++++++++++--- crates/collab/src/db/queries/channels.rs | 5 +-- crates/collab/src/db/queries/rooms.rs | 23 +++++++++++-- crates/collab/src/tests/integration_tests.rs | 2 ++ crates/project/src/project.rs | 16 +++++++++ 6 files changed, 73 insertions(+), 9 deletions(-) diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index 11a58b4b098cc6a255f8c1b061d76cf44c64684b..5f3d2827f821ec36b5cdfb63d3b8cd8247fb455e 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -36,6 +36,7 @@ impl ParticipantLocation { pub struct LocalParticipant { pub projects: Vec, pub active_project: Option>, + pub role: proto::ChannelRole, } #[derive(Clone, Debug)] diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 14d9a55ef03c92f13e1ecfb398963cf57db7710c..78e609c73f2f7c83ac701c1bf7795d8992c911c0 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -247,14 +247,18 @@ impl Room { let response = client.request(proto::CreateRoom {}).await?; let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?; let room = cx.new_model(|cx| { - Self::new( + let mut room = Self::new( room_proto.id, None, response.live_kit_connection_info, client, user_store, cx, - ) + ); + if let Some(participant) = room_proto.participants.first() { + room.local_participant.role = participant.role() + } + room })?; let initial_project_id = if let Some(initial_project) = initial_project { @@ -710,7 +714,21 @@ impl Room { this.participant_user_ids.clear(); if let Some(participant) = local_participant { + let role = participant.role(); this.local_participant.projects = participant.projects; + if this.local_participant.role != role { + this.local_participant.role = role; + // TODO!() this may be better done using optional replica ids instead. + // (though need to figure out how to handle promotion? join and leave the project?) + this.joined_projects.retain(|project| { + if let Some(project) = project.upgrade() { + project.update(cx, |project, _| project.set_role(role)); + true + } else { + false + } + }); + } } else { this.local_participant.projects.clear(); } @@ -1091,10 +1109,19 @@ impl Room { ) -> Task>> { let client = self.client.clone(); let user_store = self.user_store.clone(); + let role = self.local_participant.role; cx.emit(Event::RemoteProjectJoined { project_id: id }); cx.spawn(move |this, mut cx| async move { - let project = - Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?; + let project = Project::remote( + id, + client, + user_store, + language_registry, + fs, + role, + cx.clone(), + ) + .await?; this.update(&mut cx, |this, cx| { this.joined_projects.retain(|project| { diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 9a14aabfda3ba5d8449fea47397d92be5f217690..9c28e998c95426bc1026bcc5df86e8c528c4da8b 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -165,15 +165,16 @@ impl Database { if role.is_none() || role == Some(ChannelRole::Banned) { Err(anyhow!("not allowed"))? } + let role = role.unwrap(); let live_kit_room = format!("channel-{}", nanoid::nanoid!(30)); let room_id = self .get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx) .await?; - self.join_channel_room_internal(room_id, user_id, connection, &*tx) + self.join_channel_room_internal(room_id, user_id, connection, role, &*tx) .await - .map(|jr| (jr, accept_invite_result, role.unwrap())) + .map(|jr| (jr, accept_invite_result, role)) }) .await } diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 12d8940d7c3179e0f7c19881dbfea970a64b910f..ee2b0519e30f0c56f41b89dc4012ab7968babda6 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -131,7 +131,12 @@ impl Database { connection.owner_id as i32, ))), participant_index: ActiveValue::set(Some(0)), - ..Default::default() + role: ActiveValue::set(Some(ChannelRole::Admin)), + + id: ActiveValue::NotSet, + location_kind: ActiveValue::NotSet, + location_project_id: ActiveValue::NotSet, + initial_project_id: ActiveValue::NotSet, } .insert(&*tx) .await?; @@ -162,7 +167,13 @@ impl Database { calling_connection.owner_id as i32, ))), initial_project_id: ActiveValue::set(initial_project_id), - ..Default::default() + role: ActiveValue::set(Some(ChannelRole::Member)), + + id: ActiveValue::NotSet, + answering_connection_id: ActiveValue::NotSet, + answering_connection_server_id: ActiveValue::NotSet, + location_kind: ActiveValue::NotSet, + location_project_id: ActiveValue::NotSet, } .insert(&*tx) .await?; @@ -384,6 +395,7 @@ impl Database { room_id: RoomId, user_id: UserId, connection: ConnectionId, + role: ChannelRole, tx: &DatabaseTransaction, ) -> Result { let participant_index = self @@ -404,7 +416,11 @@ impl Database { connection.owner_id as i32, ))), participant_index: ActiveValue::Set(Some(participant_index)), - ..Default::default() + role: ActiveValue::set(Some(role)), + id: ActiveValue::NotSet, + location_kind: ActiveValue::NotSet, + location_project_id: ActiveValue::NotSet, + initial_project_id: ActiveValue::NotSet, }]) .on_conflict( OnConflict::columns([room_participant::Column::UserId]) @@ -413,6 +429,7 @@ impl Database { room_participant::Column::AnsweringConnectionServerId, room_participant::Column::AnsweringConnectionLost, room_participant::Column::ParticipantIndex, + room_participant::Column::Role, ]) .to_owned(), ) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index e64fdbec832a07d1dc3753c059653f6ea954fb00..457f085f8fe9a1d6de8df497fbff435277f6cfef 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -19,6 +19,7 @@ use project::{ search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath, }; use rand::prelude::*; +use rpc::proto::ChannelRole; use serde_json::json; use settings::SettingsStore; use std::{ @@ -3550,6 +3551,7 @@ async fn test_leaving_project( client_b.user_store().clone(), client_b.language_registry().clone(), FakeFs::new(cx.background_executor().clone()), + ChannelRole::Member, cx, ) }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8b3abff0538f970c72555915d831f090c664f82a..28ce04f0fc4022e26d60829046276b31677f6836 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -262,6 +262,8 @@ enum ProjectClientState { }, Remote { sharing_has_stopped: bool, + // todo!() this should be represented differently! + is_read_only: bool, remote_id: u64, replica_id: ReplicaId, }, @@ -702,6 +704,7 @@ impl Project { user_store: Model, languages: Arc, fs: Arc, + role: proto::ChannelRole, mut cx: AsyncAppContext, ) -> Result> { client.authenticate_and_connect(true, &cx).await?; @@ -757,6 +760,7 @@ impl Project { client: client.clone(), client_state: Some(ProjectClientState::Remote { sharing_has_stopped: false, + is_read_only: false, remote_id, replica_id, }), @@ -797,6 +801,7 @@ impl Project { prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), }; + this.set_role(role); for worktree in worktrees { let _ = this.add_worktree(&worktree, cx); } @@ -1619,6 +1624,13 @@ impl Project { cx.notify(); } + pub fn set_role(&mut self, role: proto::ChannelRole) { + if let Some(ProjectClientState::Remote { is_read_only, .. }) = &mut self.client_state { + *is_read_only = + !(role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin) + } + } + fn disconnected_from_host_internal(&mut self, cx: &mut AppContext) { if let Some(ProjectClientState::Remote { sharing_has_stopped, @@ -1672,6 +1684,10 @@ impl Project { pub fn is_read_only(&self) -> bool { self.is_disconnected() + || match &self.client_state { + Some(ProjectClientState::Remote { is_read_only, .. }) => *is_read_only, + _ => false, + } } pub fn is_local(&self) -> bool { From 84171787a5806c66801be4bda60c76ebefdd583b Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 3 Jan 2024 12:08:07 -0700 Subject: [PATCH 05/14] Track read_only per project and buffer This uses a new enum to avoid confusing booleans --- crates/assistant/src/assistant_panel.rs | 6 ++-- crates/channel/src/channel_buffer.rs | 7 ++++- crates/channel/src/channel_store.rs | 9 ++++-- crates/collab_ui/src/channel_view.rs | 13 ++------ crates/diagnostics/src/diagnostics.rs | 7 ++++- crates/editor/src/editor.rs | 28 +++++++++-------- crates/editor/src/editor_tests.rs | 21 +++++++------ crates/editor/src/git.rs | 3 +- crates/editor/src/inlay_hint_cache.rs | 7 +++-- crates/editor/src/items.rs | 3 +- crates/editor/src/movement.rs | 3 +- crates/language/src/buffer.rs | 28 ++++++++++++++++- crates/language/src/buffer_tests.rs | 14 ++++++--- crates/multi_buffer/src/multi_buffer.rs | 39 +++++++++++++---------- crates/project/src/project.rs | 41 +++++++++++++++---------- crates/project/src/worktree.rs | 12 ++++++-- crates/search/src/buffer_search.rs | 2 +- crates/search/src/project_search.rs | 6 ++-- script/sqlx | 2 +- 19 files changed, 161 insertions(+), 90 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 7b19ad130c4c42316f6ca65c8063be9c3842b42b..d225463b056ec13b7955d19106891d2769caca06 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2818,8 +2818,8 @@ impl InlineAssistant { fn handle_codegen_changed(&mut self, _: Model, cx: &mut ViewContext) { let is_read_only = !self.codegen.read(cx).idle(); - self.prompt_editor.update(cx, |editor, _cx| { - let was_read_only = editor.read_only(); + self.prompt_editor.update(cx, |editor, cx| { + let was_read_only = editor.read_only(cx); if was_read_only != is_read_only { if is_read_only { editor.set_read_only(true); @@ -3054,7 +3054,7 @@ impl InlineAssistant { fn render_prompt_editor(&self, cx: &mut ViewContext) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { - color: if self.prompt_editor.read(cx).read_only() { + color: if self.prompt_editor.read(cx).read_only(cx) { cx.theme().colors().text_disabled } else { cx.theme().colors().text diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 62daad0a62f35fcadec7d0839d8fe6f810f18e02..1aca05ec867a05e2125d451ef1b42266b765fd02 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -62,7 +62,12 @@ impl ChannelBuffer { .collect::, _>>()?; let buffer = cx.new_model(|_| { - language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) + language::Buffer::remote( + response.buffer_id, + response.replica_id as u16, + channel.channel_buffer_capability(), + base_text, + ) })?; buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))??; diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 44f527bdfd0d553b8345bc50b9782d53abbb2629..59b69405a5c8dd5160e9d61d6fd8d64fb1370a94 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -11,6 +11,7 @@ use gpui::{ AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task, WeakModel, }; +use language::Capability; use rpc::{ proto::{self, ChannelVisibility}, TypedEnvelope, @@ -74,8 +75,12 @@ impl Channel { slug.trim_matches(|c| c == '-').to_string() } - pub fn can_edit_notes(&self) -> bool { - self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin + pub fn channel_buffer_capability(&self) -> Capability { + if self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin { + Capability::ReadWrite + } else { + Capability::ReadOnly + } } } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index df2adbaabe962192b97c1c9ea456328a5e58ec7b..27873b1067637e8d6c001bc50b0b4c99dde782f9 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -138,12 +138,6 @@ impl ChannelView { editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( channel_buffer.clone(), ))); - editor.set_read_only( - !channel_buffer - .read(cx) - .channel(cx) - .is_some_and(|c| c.can_edit_notes()), - ); editor }); let _editor_event_subscription = @@ -179,7 +173,6 @@ impl ChannelView { }), ChannelBufferEvent::ChannelChanged => { self.editor.update(cx, |editor, cx| { - editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes())); cx.emit(editor::EditorEvent::TitleChanged); cx.notify() }); @@ -254,11 +247,11 @@ impl Item for ChannelView { fn tab_content(&self, _: Option, selected: bool, cx: &WindowContext) -> AnyElement { let label = if let Some(channel) = self.channel(cx) { match ( - channel.can_edit_notes(), + self.channel_buffer.read(cx).buffer().read(cx).read_only(), self.channel_buffer.read(cx).is_connected(), ) { - (true, true) => format!("#{}", channel.name), - (false, true) => format!("#{} (read-only)", channel.name), + (false, true) => format!("#{}", channel.name), + (true, true) => format!("#{} (read-only)", channel.name), (_, false) => format!("#{} (disconnected)", channel.name), } } else { diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 77e6a7673ff838f3f4a5ac3029b9d5bddacbb9ee..d31d6249c6358d05b6e018b0fea227e62e225c86 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -151,7 +151,12 @@ impl ProjectDiagnosticsEditor { let focus_in_subscription = cx.on_focus_in(&focus_handle, |diagnostics, cx| diagnostics.focus_in(cx)); - let excerpts = cx.new_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id())); + let excerpts = cx.new_model(|cx| { + MultiBuffer::new( + project_handle.read(cx).replica_id(), + project_handle.read(cx).capability(), + ) + }); let editor = cx.new_view(|cx| { let mut editor = Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 85a156a8eb9014728a4a2ad1598082e990f45ef7..1a0ccba03ce97a9a1943d08aeb788f2978134c07 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -54,10 +54,10 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, - Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, - LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, - TransactionId, + markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction, + CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, + Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, + SelectionGoal, TransactionId, }; use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState}; @@ -2050,8 +2050,8 @@ impl Editor { } } - pub fn read_only(&self) -> bool { - self.read_only + pub fn read_only(&self, cx: &AppContext) -> bool { + self.read_only || self.buffer.read(cx).read_only() } pub fn set_read_only(&mut self, read_only: bool) { @@ -2200,7 +2200,7 @@ impl Editor { S: ToOffset, T: Into>, { - if self.read_only { + if self.read_only(cx) { return; } @@ -2214,7 +2214,7 @@ impl Editor { S: ToOffset, T: Into>, { - if self.read_only { + if self.read_only(cx) { return; } @@ -2233,7 +2233,7 @@ impl Editor { S: ToOffset, T: Into>, { - if self.read_only { + if self.read_only(cx) { return; } @@ -2597,7 +2597,7 @@ impl Editor { pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext) { let text: Arc = text.into(); - if self.read_only { + if self.read_only(cx) { return; } @@ -3050,7 +3050,7 @@ impl Editor { autoindent_mode: Option, cx: &mut ViewContext, ) { - if self.read_only { + if self.read_only(cx) { return; } @@ -3787,7 +3787,8 @@ impl Editor { let mut ranges_to_highlight = Vec::new(); let excerpt_buffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); + let mut multibuffer = + MultiBuffer::new(replica_id, Capability::ReadWrite).with_title(title); for (buffer_handle, transaction) in &entries { let buffer = buffer_handle.read(cx); ranges_to_highlight.extend( @@ -7492,9 +7493,10 @@ impl Editor { locations.sort_by_key(|location| location.buffer.read(cx).remote_id()); let mut locations = locations.into_iter().peekable(); let mut ranges_to_highlight = Vec::new(); + let capability = workspace.project().read(cx).capability(); let excerpt_buffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(replica_id); + let mut multibuffer = MultiBuffer::new(replica_id, capability); while let Some(location) = locations.next() { let buffer = location.buffer.read(cx); let mut ranges_for_buffer = Vec::new(); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 4d507e0d3795e06ee8b9e07d8ee8c5b2345a4bb6..a84b866e1f8139a8ca558ccd54ac57c1d6e08bdd 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -17,8 +17,9 @@ use gpui::{ use indoc::indoc; use language::{ language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, - BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, - Override, Point, + BracketPairConfig, + Capability::ReadWrite, + FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, Override, Point, }; use parking_lot::Mutex; use project::project_settings::{LspSettings, ProjectSettings}; @@ -2355,7 +2356,7 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { .with_language(rust_language, cx) }); let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); + let mut multibuffer = MultiBuffer::new(0, ReadWrite); multibuffer.push_excerpts( toml_buffer.clone(), [ExcerptRange { @@ -6019,7 +6020,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); + let mut multibuffer = MultiBuffer::new(0, ReadWrite); multibuffer.push_excerpts( buffer.clone(), [ @@ -6103,7 +6104,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { }); let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), initial_text)); let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); + let mut multibuffer = MultiBuffer::new(0, ReadWrite); multibuffer.push_excerpts(buffer, excerpt_ranges, cx); multibuffer }); @@ -6162,7 +6163,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); let mut excerpt1_id = None; let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); + let mut multibuffer = MultiBuffer::new(0, ReadWrite); excerpt1_id = multibuffer .push_excerpts( buffer.clone(), @@ -6247,7 +6248,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); let mut excerpt1_id = None; let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); + let mut multibuffer = MultiBuffer::new(0, ReadWrite); excerpt1_id = multibuffer .push_excerpts( buffer.clone(), @@ -6636,7 +6637,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); let leader = pane.update(cx, |_, cx| { - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0, ReadWrite)); cx.new_view(|cx| build_editor(multibuffer.clone(), cx)) }); @@ -7425,7 +7426,7 @@ async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::T let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n")); let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n")); let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); + let mut multibuffer = MultiBuffer::new(0, ReadWrite); multibuffer.push_excerpts( buffer_1.clone(), [ExcerptRange { @@ -7552,7 +7553,7 @@ async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui .unwrap(); let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); + let mut multibuffer = MultiBuffer::new(0, ReadWrite); multibuffer.push_excerpts( private_buffer.clone(), [ExcerptRange { diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index e1715aa3b2f97f83e01c9907b4e5eeea9bc802ef..6eb80b99fc2248b8540c9eee4da33b5adaa620e6 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -93,6 +93,7 @@ mod tests { use crate::editor_tests::init_test; use crate::Point; use gpui::{Context, TestAppContext}; + use language::Capability::ReadWrite; use multi_buffer::{ExcerptRange, MultiBuffer}; use project::{FakeFs, Project}; use unindent::Unindent; @@ -183,7 +184,7 @@ mod tests { cx.background_executor.run_until_parked(); let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); + let mut multibuffer = MultiBuffer::new(0, ReadWrite); multibuffer.push_excerpts( buffer_1.clone(), [ diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index d7dfa01b219275c29362255c8476449ced77f07d..59c6b8605c1001440999e3f6909db300cf33e392 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -1206,7 +1206,8 @@ pub mod tests { use gpui::{Context, TestAppContext, WindowHandle}; use itertools::Itertools; use language::{ - language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, + language_settings::AllLanguageSettingsContent, Capability, FakeLspAdapter, Language, + LanguageConfig, }; use lsp::FakeLanguageServer; use parking_lot::Mutex; @@ -2459,7 +2460,7 @@ pub mod tests { .await .unwrap(); let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); + let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite); multibuffer.push_excerpts( buffer_1.clone(), [ @@ -2798,7 +2799,7 @@ pub mod tests { }) .await .unwrap(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite)); let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| { let buffer_1_excerpts = multibuffer.push_excerpts( buffer_1.clone(), diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 31c4e24659fb5a691f379e0979ce4cc5a7546aa1..f358a672537b5f4be32b9bd30d4ed50cc474bdcd 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -101,7 +101,8 @@ impl FollowableItem for Editor { if state.singleton && buffers.len() == 1 { multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx) } else { - multibuffer = MultiBuffer::new(replica_id); + multibuffer = + MultiBuffer::new(replica_id, project.read(cx).capability()); let mut excerpts = state.excerpts.into_iter().peekable(); while let Some(excerpt) = excerpts.peek() { let buffer_id = excerpt.buffer_id; diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index cfccec253fe4e5a08b19c5f1e43edd0fd40ae867..0b13e25d5dd621f9d62fcf05e7d75657a3902656 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -461,6 +461,7 @@ mod tests { Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer, }; use gpui::{font, Context as _}; + use language::Capability; use project::Project; use settings::SettingsStore; use util::post_inc; @@ -766,7 +767,7 @@ mod tests { let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abc\ndefg\nhijkl\nmn")); let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); + let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite); multibuffer.push_excerpts( buffer.clone(), [ diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index f56b24bb6a1df586405ff1a84f6439463cc1b563..d9472e8a77afc2dc7222d003aa23f513448ed661 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -57,6 +57,12 @@ lazy_static! { pub static ref BUFFER_DIFF_TASK: TaskLabel = TaskLabel::new(); } +#[derive(PartialEq, Clone, Copy, Debug)] +pub enum Capability { + ReadWrite, + ReadOnly, +} + pub struct Buffer { text: TextBuffer, diff_base: Option, @@ -90,6 +96,7 @@ pub struct Buffer { completion_triggers: Vec, completion_triggers_timestamp: clock::Lamport, deferred_ops: OperationQueue, + capability: Capability, } pub struct BufferSnapshot { @@ -405,19 +412,27 @@ impl Buffer { TextBuffer::new(replica_id, id, base_text.into()), None, None, + Capability::ReadWrite, ) } - pub fn remote(remote_id: u64, replica_id: ReplicaId, base_text: String) -> Self { + pub fn remote( + remote_id: u64, + replica_id: ReplicaId, + capability: Capability, + base_text: String, + ) -> Self { Self::build( TextBuffer::new(replica_id, remote_id, base_text), None, None, + capability, ) } pub fn from_proto( replica_id: ReplicaId, + capability: Capability, message: proto::BufferState, file: Option>, ) -> Result { @@ -426,6 +441,7 @@ impl Buffer { buffer, message.diff_base.map(|text| text.into_boxed_str().into()), file, + capability, ); this.text.set_line_ending(proto::deserialize_line_ending( rpc::proto::LineEnding::from_i32(message.line_ending) @@ -504,10 +520,19 @@ impl Buffer { self } + pub fn capability(&self) -> Capability { + self.capability + } + + pub fn read_only(&self) -> bool { + self.capability == Capability::ReadOnly + } + pub fn build( buffer: TextBuffer, diff_base: Option, file: Option>, + capability: Capability, ) -> Self { let saved_mtime = if let Some(file) = file.as_ref() { file.mtime() @@ -526,6 +551,7 @@ impl Buffer { diff_base, git_diff: git::diff::BufferDiff::new(), file, + capability, syntax_map: Mutex::new(SyntaxMap::new()), parsing_in_background: false, parse_count: 0, diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index af959b13e53949e08224f8188edd571a46c25818..780483c5ca24fbb5ec43f9652b54f6c9ba5b0c30 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1926,7 +1926,7 @@ fn test_serialization(cx: &mut gpui::AppContext) { .background_executor() .block(buffer1.read(cx).serialize_ops(None, cx)); let buffer2 = cx.new_model(|cx| { - let mut buffer = Buffer::from_proto(1, state, None).unwrap(); + let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap(); buffer .apply_ops( ops.into_iter() @@ -1967,7 +1967,8 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { let ops = cx .background_executor() .block(base_buffer.read(cx).serialize_ops(None, cx)); - let mut buffer = Buffer::from_proto(i as ReplicaId, state, None).unwrap(); + let mut buffer = + Buffer::from_proto(i as ReplicaId, Capability::ReadWrite, state, None).unwrap(); buffer .apply_ops( ops.into_iter() @@ -2083,8 +2084,13 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { replica_id ); new_buffer = Some(cx.new_model(|cx| { - let mut new_buffer = - Buffer::from_proto(new_replica_id, old_buffer_state, None).unwrap(); + let mut new_buffer = Buffer::from_proto( + new_replica_id, + Capability::ReadWrite, + old_buffer_state, + None, + ) + .unwrap(); new_buffer .apply_ops( old_buffer_ops diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 49ec284a99e217a0d62961316fa6737ca8d25dfc..946e6af5ab5fd9a97439edb5acb296972068cf44 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -11,7 +11,7 @@ pub use language::Completion; use language::{ char_kind, language_settings::{language_settings, LanguageSettings}, - AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape, + AutoindentMode, Buffer, BufferChunks, BufferSnapshot, Capability, CharKind, Chunk, CursorShape, DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, @@ -55,6 +55,7 @@ pub struct MultiBuffer { replica_id: ReplicaId, history: History, title: Option, + capability: Capability, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -225,13 +226,14 @@ struct ExcerptBytes<'a> { } impl MultiBuffer { - pub fn new(replica_id: ReplicaId) -> Self { + pub fn new(replica_id: ReplicaId, capability: Capability) -> Self { Self { snapshot: Default::default(), buffers: Default::default(), next_excerpt_id: 1, subscriptions: Default::default(), singleton: false, + capability, replica_id, history: History { next_transaction_id: Default::default(), @@ -271,6 +273,7 @@ impl MultiBuffer { next_excerpt_id: 1, subscriptions: Default::default(), singleton: self.singleton, + capability: self.capability, replica_id: self.replica_id, history: self.history.clone(), title: self.title.clone(), @@ -282,8 +285,12 @@ impl MultiBuffer { self } + pub fn read_only(&self) -> bool { + self.capability == Capability::ReadOnly + } + pub fn singleton(buffer: Model, cx: &mut ModelContext) -> Self { - let mut this = Self::new(buffer.read(cx).replica_id()); + let mut this = Self::new(buffer.read(cx).replica_id(), buffer.read(cx).capability()); this.singleton = true; this.push_excerpts( buffer, @@ -1657,7 +1664,7 @@ impl MultiBuffer { excerpts: [(&str, Vec>); COUNT], cx: &mut gpui::AppContext, ) -> Model { - let multi = cx.new_model(|_| Self::new(0)); + let multi = cx.new_model(|_| Self::new(0, Capability::ReadWrite)); for (text, ranges) in excerpts { let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange { @@ -1678,7 +1685,7 @@ impl MultiBuffer { pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> Model { cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); + let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite); let mutation_count = rng.gen_range(1..=5); multibuffer.randomly_edit_excerpts(rng, mutation_count, cx); multibuffer @@ -4176,7 +4183,7 @@ mod tests { let ops = cx .background_executor() .block(host_buffer.read(cx).serialize_ops(None, cx)); - let mut buffer = Buffer::from_proto(1, state, None).unwrap(); + let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap(); buffer .apply_ops( ops.into_iter() @@ -4205,7 +4212,7 @@ mod tests { cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'a'))); let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'g'))); - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite)); let events = Arc::new(RwLock::new(Vec::::new())); multibuffer.update(cx, |_, cx| { @@ -4442,8 +4449,8 @@ mod tests { let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(10, 3, 'm'))); - let leader_multibuffer = cx.new_model(|_| MultiBuffer::new(0)); - let follower_multibuffer = cx.new_model(|_| MultiBuffer::new(0)); + let leader_multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite)); + let follower_multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite)); let follower_edit_event_count = Arc::new(RwLock::new(0)); follower_multibuffer.update(cx, |_, cx| { @@ -4547,7 +4554,7 @@ mod tests { fn test_push_excerpts_with_context_lines(cx: &mut AppContext) { let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a'))); - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite)); let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { multibuffer.push_excerpts_with_context_lines( buffer.clone(), @@ -4584,7 +4591,7 @@ mod tests { async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) { let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a'))); - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite)); let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { let snapshot = buffer.read(cx); let ranges = vec![ @@ -4619,7 +4626,7 @@ mod tests { #[gpui::test] fn test_empty_multibuffer(cx: &mut AppContext) { - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), ""); @@ -4652,7 +4659,7 @@ mod tests { let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd")); let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "efghi")); let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); + let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite); multibuffer.push_excerpts( buffer_1.clone(), [ExcerptRange { @@ -4710,7 +4717,7 @@ mod tests { let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd")); let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "ABCDEFGHIJKLMNOP")); - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite)); // Create an insertion id in buffer 1 that doesn't exist in buffer 2. // Add an excerpt from buffer 1 that spans this new insertion. @@ -4844,7 +4851,7 @@ mod tests { .unwrap_or(10); let mut buffers: Vec> = Vec::new(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite)); let mut excerpt_ids = Vec::::new(); let mut expected_excerpts = Vec::<(Model, Range)>::new(); let mut anchors = Vec::new(); @@ -5266,7 +5273,7 @@ mod tests { let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "1234")); let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "5678")); - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite)); let group_interval = multibuffer.read(cx).history.group_interval; multibuffer.update(cx, |multibuffer, cx| { multibuffer.push_excerpts( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 28ce04f0fc4022e26d60829046276b31677f6836..8b83dc445570365f1def06e0f4ad71b5eb1d8289 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -39,11 +39,11 @@ use language::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, serialize_anchor, serialize_version, split_operations, }, - range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction, - CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, - File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, - OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, - ToOffset, ToPointUtf16, Transaction, Unclipped, + range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, + CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, + Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, + LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, + TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, }; use log::error; use lsp::{ @@ -262,8 +262,7 @@ enum ProjectClientState { }, Remote { sharing_has_stopped: bool, - // todo!() this should be represented differently! - is_read_only: bool, + capability: Capability, remote_id: u64, replica_id: ReplicaId, }, @@ -760,7 +759,7 @@ impl Project { client: client.clone(), client_state: Some(ProjectClientState::Remote { sharing_has_stopped: false, - is_read_only: false, + capability: Capability::ReadWrite, remote_id, replica_id, }), @@ -1625,9 +1624,13 @@ impl Project { } pub fn set_role(&mut self, role: proto::ChannelRole) { - if let Some(ProjectClientState::Remote { is_read_only, .. }) = &mut self.client_state { - *is_read_only = - !(role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin) + if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state { + *capability = if role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin + { + Capability::ReadWrite + } else { + Capability::ReadOnly + }; } } @@ -1682,12 +1685,15 @@ impl Project { } } + pub fn capability(&self) -> Capability { + match &self.client_state { + Some(ProjectClientState::Remote { capability, .. }) => *capability, + Some(ProjectClientState::Local { .. }) | None => Capability::ReadWrite, + } + } + pub fn is_read_only(&self) -> bool { - self.is_disconnected() - || match &self.client_state { - Some(ProjectClientState::Remote { is_read_only, .. }) => *is_read_only, - _ => false, - } + self.is_disconnected() || self.capability() == Capability::ReadOnly } pub fn is_local(&self) -> bool { @@ -7215,7 +7221,8 @@ impl Project { let buffer_id = state.id; let buffer = cx.new_model(|_| { - Buffer::from_proto(this.replica_id(), state, buffer_file).unwrap() + Buffer::from_proto(this.replica_id(), this.capability(), state, buffer_file) + .unwrap() }); this.incomplete_remote_buffers .insert(buffer_id, Some(buffer)); diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 6f7d2046d645157d9aa902b7253110af5273ad87..ae0c074188274b95fbed3b078f68378ce715e570 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -32,7 +32,8 @@ use language::{ deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending, serialize_version, }, - Buffer, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, Unclipped, + Buffer, Capability, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, + Unclipped, }; use lsp::LanguageServerId; use parking_lot::Mutex; @@ -682,7 +683,14 @@ impl LocalWorktree { .background_executor() .spawn(async move { text::Buffer::new(0, id, contents) }) .await; - cx.new_model(|_| Buffer::build(text_buffer, diff_base, Some(Arc::new(file)))) + cx.new_model(|_| { + Buffer::build( + text_buffer, + diff_base, + Some(Arc::new(file)), + Capability::ReadWrite, + ) + }) }) } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 67aa4955bc692483faafb9b6291e9e8550dac859..351558b6bbcd533d80ded576806bf3883cd0ee23 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -70,7 +70,7 @@ impl BufferSearchBar { fn render_text_input(&self, editor: &View, cx: &ViewContext) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { - color: if editor.read(cx).read_only() { + color: if editor.read(cx).read_only(cx) { cx.theme().colors().text_disabled } else { cx.theme().colors().text diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 9a91d619a43cf2cd4e9c024006494f2aa9b19989..94435e8b76a61f9e1e4078a4cf253ee3c5ef6b7c 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -132,9 +132,11 @@ pub struct ProjectSearchBar { impl ProjectSearch { fn new(project: Model, cx: &mut ModelContext) -> Self { let replica_id = project.read(cx).replica_id(); + let capability = project.read(cx).capability(); + Self { project, - excerpts: cx.new_model(|_| MultiBuffer::new(replica_id)), + excerpts: cx.new_model(|_| MultiBuffer::new(replica_id, capability)), pending_search: Default::default(), match_ranges: Default::default(), active_query: None, @@ -1519,7 +1521,7 @@ impl ProjectSearchBar { fn render_text_input(&self, editor: &View, cx: &ViewContext) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let text_style = TextStyle { - color: if editor.read(cx).read_only() { + color: if editor.read(cx).read_only(cx) { cx.theme().colors().text_disabled } else { cx.theme().colors().text diff --git a/script/sqlx b/script/sqlx index cf2fa8d405f0b108c7131533257a859b842fdfd4..a575efbcb619bced63c5d29b29f4d69c7e1221ae 100755 --- a/script/sqlx +++ b/script/sqlx @@ -8,7 +8,7 @@ set -e cd crates/collab # Export contents of .env.toml -eval "$(cargo run --quiet --bin dotenv)" +eval "$(cargo run --quiet --bin dotenv2)" # Run sqlx command sqlx $@ From 6877bd4969d2b496b08ca80a57040bea69e63c77 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 3 Jan 2024 13:10:59 -0700 Subject: [PATCH 06/14] Make read only buffers feel more read only --- crates/editor/src/editor.rs | 3 ++- crates/editor/src/element.rs | 8 +++++++- crates/gpui/src/color.rs | 9 +++++++++ crates/theme/src/styles/players.rs | 13 +++++++++++-- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1a0ccba03ce97a9a1943d08aeb788f2978134c07..e3b0e9874bfdc1cadab7abc3f928d997ed46fab3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8605,7 +8605,8 @@ impl Editor { } pub fn show_local_cursors(&self, cx: &WindowContext) -> bool { - self.blink_manager.read(cx).visible() && self.focus_handle.is_focused(cx) + (self.read_only(cx) || self.blink_manager.read(cx).visible()) + && self.focus_handle.is_focused(cx) } fn on_buffer_changed(&mut self, _: Model, cx: &mut ViewContext) { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2b5c97bac5b1e067db5ff1640752c0bc8a9a489d..368fc4b7ca8f88a26cb1840728c917404c0ef521 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1901,7 +1901,13 @@ impl EditorElement { layouts.push(layout); } - selections.push((style.local_player, layouts)); + let player = if editor.read_only(cx) { + cx.theme().players().read_only() + } else { + style.local_player + }; + + selections.push((player, layouts)); } if let Some(collaboration_hub) = &editor.collaboration_hub { diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index df16d2c0b5664f7354cbde90e088be3cf1aee589..dc0f5055e70a123b99a7cc8b746ee69bd23652a3 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -339,6 +339,15 @@ impl Hsla { } } + pub fn grayscale(&self) -> Self { + Hsla { + h: self.h, + s: 0., + l: self.l, + a: self.a, + } + } + /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0. /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color. pub fn fade_out(&mut self, factor: f32) { diff --git a/crates/theme/src/styles/players.rs b/crates/theme/src/styles/players.rs index 9f9b837e47b8c7e2ccf8b0d870214c382bb9cf28..089de247230341bf89fd40da38a13091e7024368 100644 --- a/crates/theme/src/styles/players.rs +++ b/crates/theme/src/styles/players.rs @@ -1,7 +1,7 @@ -use gpui::Hsla; +use gpui::{hsla, Hsla}; use serde_derive::Deserialize; -use crate::{amber, blue, jade, lime, orange, pink, purple, red}; +use crate::{amber, blue, gray, jade, lime, orange, pink, purple, red}; #[derive(Debug, Clone, Copy, Deserialize, Default)] pub struct PlayerColor { @@ -131,6 +131,15 @@ impl PlayerColors { *self.0.last().unwrap() } + pub fn read_only(&self) -> PlayerColor { + let local = self.local(); + PlayerColor { + cursor: local.cursor.grayscale(), + background: local.background.grayscale(), + selection: local.selection.grayscale(), + } + } + pub fn color_for_participant(&self, participant_index: u32) -> PlayerColor { let len = self.0.len() - 1; self.0[(participant_index as usize % len) + 1] From 9fe17a1d1dd4700679dcb6a753dbca286d8faadb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 3 Jan 2024 13:48:50 -0700 Subject: [PATCH 07/14] Prevent guests from screen-sharing, unmuting or screen sharing --- crates/call/src/room.rs | 5 ++ crates/collab/src/db/ids.rs | 8 +++ crates/collab/src/db/queries/projects.rs | 7 +++ crates/collab_ui/src/collab_titlebar_item.rs | 64 ++++++++++++-------- crates/live_kit_server/src/token.rs | 1 + 5 files changed, 60 insertions(+), 25 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 78e609c73f2f7c83ac701c1bf7795d8992c911c0..03a6b942b212c6766db38a55d75784bae52861ae 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1251,6 +1251,11 @@ impl Room { .unwrap_or(false) } + pub fn can_publish(&self) -> bool { + self.local_participant().role == proto::ChannelRole::Member + || self.local_participant().role == proto::ChannelRole::Admin + } + pub fn is_speaking(&self) -> bool { self.live_kit .as_ref() diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 5f0df90811cce78167643d176f41cedc7a2d7d9c..9bb766147f5b0b2084b76665262962179a62e6eb 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -132,6 +132,14 @@ impl ChannelRole { Admin | Member | Banned => false, } } + + pub fn can_share_projects(&self) -> bool { + use ChannelRole::*; + match self { + Admin | Member => true, + Guest | Banned => false, + } + } } impl From for ChannelRole { diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 3e2c00337823a91badeedf183a5598f94f8f2c2a..5b8d54f8d36a746b2c09a77cbf9e75b77b557543 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -46,6 +46,13 @@ impl Database { if participant.room_id != room_id { return Err(anyhow!("shared project on unexpected room"))?; } + if !participant + .role + .unwrap_or(ChannelRole::Member) + .can_share_projects() + { + return Err(anyhow!("guests cannot share projects"))?; + } let project = project::ActiveModel { room_id: ActiveValue::set(participant.room_id), diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 16d8be89af78db45bdcc27b317bffcbfdf66e98e..a3f82b1f5f83944f051e3ea93e46e8fa761dcb82 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -173,8 +173,9 @@ impl Render for CollabTitlebarItem { let is_muted = room.is_muted(cx); let is_deafened = room.is_deafened().unwrap_or(false); let is_screen_sharing = room.is_screen_sharing(); + let can_publish = room.can_publish(); - this.when(is_local, |this| { + this.when(is_local && can_publish, |this| { this.child( Button::new( "toggle_sharing", @@ -203,20 +204,22 @@ impl Render for CollabTitlebarItem { .detach_and_log_err(cx); }), ) - .child( - IconButton::new( - "mute-microphone", - if is_muted { - ui::Icon::MicMute - } else { - ui::Icon::Mic - }, + .when(can_publish, |this| { + this.child( + IconButton::new( + "mute-microphone", + if is_muted { + ui::Icon::MicMute + } else { + ui::Icon::Mic + }, + ) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .selected(is_muted) + .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)), ) - .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small) - .selected(is_muted) - .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)), - ) + }) .child( IconButton::new( "mute-sound", @@ -230,19 +233,30 @@ impl Render for CollabTitlebarItem { .icon_size(IconSize::Small) .selected(is_deafened) .tooltip(move |cx| { - Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx) + if can_publish { + Tooltip::with_meta( + "Deafen Audio", + None, + "Mic will be muted", + cx, + ) + } else { + Tooltip::text("Deafen Audio", cx) + } }) - .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)), - ) - .child( - IconButton::new("screen-share", ui::Icon::Screen) - .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small) - .selected(is_screen_sharing) - .on_click(move |_, cx| { - crate::toggle_screen_sharing(&Default::default(), cx) - }), + .on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)), ) + .when(can_publish, |this| { + this.child( + IconButton::new("screen-share", ui::Icon::Screen) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .selected(is_screen_sharing) + .on_click(move |_, cx| { + crate::toggle_screen_sharing(&Default::default(), cx) + }), + ) + }) }) .map(|el| { let status = self.client.status(); diff --git a/crates/live_kit_server/src/token.rs b/crates/live_kit_server/src/token.rs index b98f5892aea9ead8472b614da8491bdf7f2a38b4..a2ca19ad20e42b1ad8ef0dc783cfaf4ac42269d0 100644 --- a/crates/live_kit_server/src/token.rs +++ b/crates/live_kit_server/src/token.rs @@ -62,6 +62,7 @@ impl<'a> VideoGrant<'a> { Self { room: Some(Cow::Borrowed(room)), room_join: Some(true), + can_publish: Some(false), can_subscribe: Some(true), ..Default::default() } From c3402024bca4737ce101b75c4ff371170b81281d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 3 Jan 2024 14:05:22 -0700 Subject: [PATCH 08/14] Fix privilege escalation when guests invite people --- crates/collab/src/db/queries/rooms.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index ee2b0519e30f0c56f41b89dc4012ab7968babda6..af554955a1d5de745df0c000b47c0a8230107689 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -156,6 +156,24 @@ impl Database { initial_project_id: Option, ) -> Result> { self.room_transaction(room_id, |tx| async move { + let room = self.get_room(room_id, &tx).await?; + + let caller = room_participant::Entity::find() + .filter( + room_participant::Column::UserId + .eq(calling_user_id) + .and(room_participant::Column::RoomId.eq(room_id)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("user is not in the room"))?; + + let called_user_role = match caller.role.unwrap_or(ChannelRole::Member) { + ChannelRole::Admin | ChannelRole::Member => ChannelRole::Member, + ChannelRole::Guest => ChannelRole::Guest, + ChannelRole::Banned => return Err(anyhow!("banned users cannot invite").into()), + }; + room_participant::ActiveModel { room_id: ActiveValue::set(room_id), user_id: ActiveValue::set(called_user_id), @@ -167,7 +185,7 @@ impl Database { calling_connection.owner_id as i32, ))), initial_project_id: ActiveValue::set(initial_project_id), - role: ActiveValue::set(Some(ChannelRole::Member)), + role: ActiveValue::set(Some(called_user_role)), id: ActiveValue::NotSet, answering_connection_id: ActiveValue::NotSet, @@ -178,7 +196,6 @@ impl Database { .insert(&*tx) .await?; - let room = self.get_room(room_id, &tx).await?; let incoming_call = Self::build_incoming_call(&room, called_user_id) .ok_or_else(|| anyhow!("failed to build incoming call"))?; Ok((room, incoming_call)) From 1930258d397acc516a6ba6a2edd2e08b49b13fc1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 3 Jan 2024 14:44:09 -0700 Subject: [PATCH 09/14] Show guests in fewer places --- crates/call/src/participant.rs | 1 + crates/call/src/room.rs | 19 ++++- .../collab/src/tests/channel_guest_tests.rs | 8 +- crates/collab_ui/src/channel_view.rs | 2 +- crates/collab_ui/src/collab_panel.rs | 77 ++++++++++++++++--- crates/collab_ui/src/collab_titlebar_item.rs | 15 ++-- crates/theme/src/styles/players.rs | 4 +- 7 files changed, 100 insertions(+), 26 deletions(-) diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index 5f3d2827f821ec36b5cdfb63d3b8cd8247fb455e..9faefc63c36975757f2ef45006bfebe7394b986a 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -43,6 +43,7 @@ pub struct LocalParticipant { pub struct RemoteParticipant { pub user: Arc, pub peer_id: proto::PeerId, + pub role: proto::ChannelRole, pub projects: Vec, pub location: ParticipantLocation, pub participant_index: ParticipantIndex, diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 03a6b942b212c6766db38a55d75784bae52861ae..4561e6d807fa3047f9a2e0c0f7663d7e72a4c1ce 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -610,6 +610,12 @@ impl Room { .find(|p| p.peer_id == peer_id) } + pub fn role_for_user(&self, user_id: u64) -> Option { + self.remote_participants + .get(&user_id) + .map(|participant| participant.role) + } + pub fn pending_participants(&self) -> &[Arc] { &self.pending_participants } @@ -784,6 +790,7 @@ impl Room { }); } + let role = participant.role(); let location = ParticipantLocation::from_proto(participant.location) .unwrap_or(ParticipantLocation::External); if let Some(remote_participant) = @@ -792,8 +799,11 @@ impl Room { remote_participant.peer_id = peer_id; remote_participant.projects = participant.projects; remote_participant.participant_index = participant_index; - if location != remote_participant.location { + if location != remote_participant.location + || role != remote_participant.role + { remote_participant.location = location; + remote_participant.role = role; cx.emit(Event::ParticipantLocationChanged { participant_id: peer_id, }); @@ -807,6 +817,7 @@ impl Room { peer_id, projects: participant.projects, location, + role, muted: true, speaking: false, video_tracks: Default::default(), @@ -1251,9 +1262,9 @@ impl Room { .unwrap_or(false) } - pub fn can_publish(&self) -> bool { - self.local_participant().role == proto::ChannelRole::Member - || self.local_participant().role == proto::ChannelRole::Admin + pub fn read_only(&self) -> bool { + !(self.local_participant().role == proto::ChannelRole::Member + || self.local_participant().role == proto::ChannelRole::Admin) } pub fn is_speaking(&self) -> bool { diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs index 78dd57be026913b3299efe2e1c35a94a5ea798ad..e2051c44a038ced0724b616ac4e3b4cd851b4508 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -7,8 +7,8 @@ use workspace::Workspace; #[gpui::test] async fn test_channel_guests( executor: BackgroundExecutor, - mut cx_a: &mut TestAppContext, - mut cx_b: &mut TestAppContext, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; @@ -37,15 +37,13 @@ async fn test_channel_guests( .await; let active_call_a = cx_a.read(ActiveCall::global); - let active_call_b = cx_b.read(ActiveCall::global); // Client A shares a project in the channel active_call_a .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) .await .unwrap(); - let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap()); + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 27873b1067637e8d6c001bc50b0b4c99dde782f9..ce68acfbd83379e77c298152aa95b51280883e0c 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -172,7 +172,7 @@ impl ChannelView { cx.notify(); }), ChannelBufferEvent::ChannelChanged => { - self.editor.update(cx, |editor, cx| { + self.editor.update(cx, |_, cx| { cx.emit(editor::EditorEvent::TitleChanged); cx.notify() }); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 3ed07e5bd1b8b3985f58d8ca89dd04e676f5425a..457a31ff5d01bc3838ba0ae5f252dcc1b89b823f 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -151,6 +151,9 @@ enum ListEntry { peer_id: Option, is_last: bool, }, + GuestCount { + count: usize, + }, IncomingRequest(Arc), OutgoingRequest(Arc), ChannelInvite(Arc), @@ -380,10 +383,13 @@ impl CollabPanel { if !self.collapsed_sections.contains(&Section::ActiveCall) { let room = room.read(cx); + let mut guest_count_ix = 0; + let mut guest_count = if room.read_only() { 1 } else { 0 }; if let Some(channel_id) = room.channel_id() { self.entries.push(ListEntry::ChannelNotes { channel_id }); - self.entries.push(ListEntry::ChannelChat { channel_id }) + self.entries.push(ListEntry::ChannelChat { channel_id }); + guest_count_ix = self.entries.len(); } // Populate the active user. @@ -402,7 +408,7 @@ impl CollabPanel { &Default::default(), executor.clone(), )); - if !matches.is_empty() { + if !matches.is_empty() && !room.read_only() { let user_id = user.id; self.entries.push(ListEntry::CallParticipant { user, @@ -430,13 +436,21 @@ impl CollabPanel { // Populate remote participants. self.match_candidates.clear(); self.match_candidates - .extend(room.remote_participants().iter().map(|(_, participant)| { - StringMatchCandidate { - id: participant.user.id as usize, - string: participant.user.github_login.clone(), - char_bag: participant.user.github_login.chars().collect(), - } - })); + .extend( + room.remote_participants() + .iter() + .filter_map(|(_, participant)| { + if participant.role == proto::ChannelRole::Guest { + guest_count += 1; + return None; + } + Some(StringMatchCandidate { + id: participant.user.id as usize, + string: participant.user.github_login.clone(), + char_bag: participant.user.github_login.chars().collect(), + }) + }), + ); let matches = executor.block(match_strings( &self.match_candidates, &query, @@ -470,6 +484,10 @@ impl CollabPanel { }); } } + if guest_count > 0 { + self.entries + .insert(guest_count_ix, ListEntry::GuestCount { count: guest_count }); + } // Populate pending participants. self.match_candidates.clear(); @@ -959,6 +977,34 @@ impl CollabPanel { .tooltip(move |cx| Tooltip::text("Open Chat", cx)) } + fn render_guest_count( + &self, + count: usize, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + // TODO! disable manage_members for guests. + ListItem::new("guest_count") + .selected(is_selected) + .on_click(cx.listener(move |this, _, cx| { + if let Some(channel_id) = ActiveCall::global(cx).read(cx).channel_id(cx) { + this.manage_members(channel_id, cx) + } + })) + .start_slot( + h_stack() + .gap_1() + .child(render_tree_branch(false, cx)) + .child(""), + ) + .child(Label::new(if count == 1 { + format!("{} guest", count) + } else { + format!("{} guests", count) + })) + .tooltip(move |cx| Tooltip::text("Manage Members", cx)) + } + fn has_subchannels(&self, ix: usize) -> bool { self.entries.get(ix).map_or(false, |entry| { if let ListEntry::Channel { has_children, .. } = entry { @@ -1180,6 +1226,11 @@ impl CollabPanel { }); } } + ListEntry::GuestCount { .. } => { + if let Some(channel_id) = ActiveCall::global(cx).read(cx).channel_id(cx) { + self.manage_members(channel_id, cx) + } + } ListEntry::Channel { channel, .. } => { let is_active = maybe!({ let call_channel = ActiveCall::global(cx) @@ -1735,6 +1786,9 @@ impl CollabPanel { ListEntry::ParticipantScreen { peer_id, is_last } => self .render_participant_screen(*peer_id, *is_last, is_selected, cx) .into_any_element(), + ListEntry::GuestCount { count } => self + .render_guest_count(*count, is_selected, cx) + .into_any_element(), ListEntry::ChannelNotes { channel_id } => self .render_channel_notes(*channel_id, is_selected, cx) .into_any_element(), @@ -2504,6 +2558,11 @@ impl PartialEq for ListEntry { return true; } } + ListEntry::GuestCount { .. } => { + if let ListEntry::GuestCount { .. } = other { + return true; + } + } } false } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index a3f82b1f5f83944f051e3ea93e46e8fa761dcb82..81a71da27ed4dfd4f38dc227c6609a1a063f15a5 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -10,6 +10,7 @@ use gpui::{ }; use project::{Project, RepositoryEntry}; use recent_projects::RecentProjects; +use rpc::proto; use std::sync::Arc; use theme::{ActiveTheme, PlayerColors}; use ui::{ @@ -173,9 +174,9 @@ impl Render for CollabTitlebarItem { let is_muted = room.is_muted(cx); let is_deafened = room.is_deafened().unwrap_or(false); let is_screen_sharing = room.is_screen_sharing(); - let can_publish = room.can_publish(); + let read_only = room.read_only(); - this.when(is_local && can_publish, |this| { + this.when(is_local && !read_only, |this| { this.child( Button::new( "toggle_sharing", @@ -204,7 +205,7 @@ impl Render for CollabTitlebarItem { .detach_and_log_err(cx); }), ) - .when(can_publish, |this| { + .when(!read_only, |this| { this.child( IconButton::new( "mute-microphone", @@ -233,7 +234,7 @@ impl Render for CollabTitlebarItem { .icon_size(IconSize::Small) .selected(is_deafened) .tooltip(move |cx| { - if can_publish { + if !read_only { Tooltip::with_meta( "Deafen Audio", None, @@ -246,7 +247,7 @@ impl Render for CollabTitlebarItem { }) .on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)), ) - .when(can_publish, |this| { + .when(!read_only, |this| { this.child( IconButton::new("screen-share", ui::Icon::Screen) .style(ButtonStyle::Subtle) @@ -420,6 +421,10 @@ impl CollabTitlebarItem { project_id: Option, current_user: &Arc, ) -> Option { + if room.role_for_user(user.id) == Some(proto::ChannelRole::Guest) { + return None; + } + let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id)); let pile = FacePile::default() diff --git a/crates/theme/src/styles/players.rs b/crates/theme/src/styles/players.rs index 089de247230341bf89fd40da38a13091e7024368..d4d27e71237b0968d4642a92399dca754dada77e 100644 --- a/crates/theme/src/styles/players.rs +++ b/crates/theme/src/styles/players.rs @@ -1,7 +1,7 @@ -use gpui::{hsla, Hsla}; +use gpui::Hsla; use serde_derive::Deserialize; -use crate::{amber, blue, gray, jade, lime, orange, pink, purple, red}; +use crate::{amber, blue, jade, lime, orange, pink, purple, red}; #[derive(Debug, Clone, Copy, Deserialize, Default)] pub struct PlayerColor { From 427e7f6b4f55e96628c1c079227f587fb9df58e8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 4 Jan 2024 09:40:12 -0700 Subject: [PATCH 10/14] Read only permissions for project panel too --- crates/project_panel/src/project_panel.rs | 30 +++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 6f438098b718a72602e69b81339129e00fa8a3ba..cb3538601fd7932e7a0cce7a7b7f1f9071f4c176 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -388,8 +388,13 @@ impl ProjectPanel { let is_dir = entry.is_dir(); let worktree_id = worktree.id(); let is_local = project.is_local(); + let is_read_only = project.is_read_only(); let context_menu = ContextMenu::build(cx, |mut menu, cx| { + if is_read_only { + return menu.action("Copy Relative Path", Box::new(CopyRelativePath)); + } + if is_local { menu = menu.action( "Add Folder to Project", @@ -1482,6 +1487,7 @@ impl ProjectPanel { impl Render for ProjectPanel { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { let has_worktree = self.visible_entries.len() != 0; + let project = self.project.read(cx); if has_worktree { div() @@ -1494,21 +1500,25 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::expand_selected_entry)) .on_action(cx.listener(Self::collapse_selected_entry)) .on_action(cx.listener(Self::collapse_all_entries)) - .on_action(cx.listener(Self::new_file)) - .on_action(cx.listener(Self::new_directory)) - .on_action(cx.listener(Self::rename)) - .on_action(cx.listener(Self::delete)) - .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::open_file)) + .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::cut)) - .on_action(cx.listener(Self::copy)) .on_action(cx.listener(Self::copy_path)) .on_action(cx.listener(Self::copy_relative_path)) - .on_action(cx.listener(Self::paste)) - .on_action(cx.listener(Self::reveal_in_finder)) - .on_action(cx.listener(Self::open_in_terminal)) .on_action(cx.listener(Self::new_search_in_directory)) + .when(!project.is_read_only(), |el| { + el.on_action(cx.listener(Self::new_file)) + .on_action(cx.listener(Self::new_directory)) + .on_action(cx.listener(Self::rename)) + .on_action(cx.listener(Self::delete)) + .on_action(cx.listener(Self::cut)) + .on_action(cx.listener(Self::copy)) + .on_action(cx.listener(Self::paste)) + }) + .when(project.is_local(), |el| { + el.on_action(cx.listener(Self::reveal_in_finder)) + .on_action(cx.listener(Self::open_in_terminal)) + }) .track_focus(&self.focus_handle) .child( uniform_list( From fcf7007e0bdc84d62308ba33097512b93e635013 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 4 Jan 2024 09:48:47 -0700 Subject: [PATCH 11/14] let search happen too --- crates/project_panel/src/project_panel.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index cb3538601fd7932e7a0cce7a7b7f1f9071f4c176..a685a82b073395bca0ee812e45a4ff2c62cac4a1 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -392,7 +392,12 @@ impl ProjectPanel { let context_menu = ContextMenu::build(cx, |mut menu, cx| { if is_read_only { - return menu.action("Copy Relative Path", Box::new(CopyRelativePath)); + menu = menu.action("Copy Relative Path", Box::new(CopyRelativePath)); + if is_dir { + menu = menu.action("Search Inside", Box::new(NewSearchInDirectory)) + } + + return menu; } if is_local { From d2afc97b53263a9215e80422d81f604f62efe288 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 4 Jan 2024 11:55:14 -0700 Subject: [PATCH 12/14] Tidy up branch --- crates/collab/src/db/queries/rooms.rs | 4 +--- crates/collab/src/rpc.rs | 2 +- crates/project/src/project.rs | 3 +-- crates/rpc/proto/zed.proto | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index af554955a1d5de745df0c000b47c0a8230107689..878f0d67cfaf6ec03cf51880c2dff67566454ec0 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -156,8 +156,6 @@ impl Database { initial_project_id: Option, ) -> Result> { self.room_transaction(room_id, |tx| async move { - let room = self.get_room(room_id, &tx).await?; - let caller = room_participant::Entity::find() .filter( room_participant::Column::UserId @@ -196,6 +194,7 @@ impl Database { .insert(&*tx) .await?; + let room = self.get_room(room_id, &tx).await?; let incoming_call = Self::build_incoming_call(&room, called_user_id) .ok_or_else(|| anyhow!("failed to build incoming call"))?; Ok((room, incoming_call)) @@ -1172,7 +1171,6 @@ impl Database { } } drop(db_participants); - dbg!(&participants); let mut db_projects = db_room .find_related(project::Entity) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 8bb33cae296016dc241c03304154e14266c02f23..835b48809da94dc60cd872d473e564a7456da81e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1504,7 +1504,7 @@ async fn join_project( // First, we send the metadata associated with each worktree. response.send(proto::JoinProjectResponse { worktrees: worktrees.clone(), - replica_id: Some(replica_id.0 as u32), + replica_id: replica_id.0 as u32, collaborators: collaborators.clone(), language_servers: project.language_servers.clone(), })?; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8b83dc445570365f1def06e0f4ad71b5eb1d8289..ad2e1bb2679bf0764e2a57fdfb52571edf1144a0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -715,8 +715,7 @@ impl Project { }) .await?; let this = cx.new_model(|cx| { - // todo!() - let replica_id = response.payload.replica_id.unwrap() as ReplicaId; + let replica_id = response.payload.replica_id as ReplicaId; let mut worktrees = Vec::new(); for worktree in response.payload.worktrees { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 84d03727c0c59f61d54c59c4f688aeb71e392e87..e423441a30218674c4d6b68098a8f724c8a32654 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -369,7 +369,7 @@ message JoinProject { } message JoinProjectResponse { - optional uint32 replica_id = 1; + uint32 replica_id = 1; repeated WorktreeMetadata worktrees = 2; repeated Collaborator collaborators = 3; repeated LanguageServer language_servers = 4; From be426e67cca91b7525b1a32b95e43ac888a5b4d7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 4 Jan 2024 12:13:22 -0700 Subject: [PATCH 13/14] Tidy up guest count --- crates/call/src/room.rs | 7 ++-- crates/collab_ui/src/collab_panel.rs | 52 +++++++++++++++++++++------- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 4561e6d807fa3047f9a2e0c0f7663d7e72a4c1ce..e2c9bf5886faca374334e463b8d0ac4711ce863b 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -616,6 +616,10 @@ impl Room { .map(|participant| participant.role) } + pub fn local_participant_is_admin(&self) -> bool { + self.local_participant.role == proto::ChannelRole::Admin + } + pub fn pending_participants(&self) -> &[Arc] { &self.pending_participants } @@ -724,8 +728,7 @@ impl Room { this.local_participant.projects = participant.projects; if this.local_participant.role != role { this.local_participant.role = role; - // TODO!() this may be better done using optional replica ids instead. - // (though need to figure out how to handle promotion? join and leave the project?) + this.joined_projects.retain(|project| { if let Some(project) = project.upgrade() { project.update(cx, |project, _| project.set_role(role)); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 457a31ff5d01bc3838ba0ae5f252dcc1b89b823f..f8dd4bf0b76d1f186d5befa1715df251ac4d43b7 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -153,6 +153,7 @@ enum ListEntry { }, GuestCount { count: usize, + has_visible_participants: bool, }, IncomingRequest(Arc), OutgoingRequest(Arc), @@ -385,6 +386,7 @@ impl CollabPanel { let room = room.read(cx); let mut guest_count_ix = 0; let mut guest_count = if room.read_only() { 1 } else { 0 }; + let mut non_guest_count = if room.read_only() { 0 } else { 1 }; if let Some(channel_id) = room.channel_id() { self.entries.push(ListEntry::ChannelNotes { channel_id }); @@ -443,6 +445,8 @@ impl CollabPanel { if participant.role == proto::ChannelRole::Guest { guest_count += 1; return None; + } else { + non_guest_count += 1; } Some(StringMatchCandidate { id: participant.user.id as usize, @@ -485,8 +489,13 @@ impl CollabPanel { } } if guest_count > 0 { - self.entries - .insert(guest_count_ix, ListEntry::GuestCount { count: guest_count }); + self.entries.insert( + guest_count_ix, + ListEntry::GuestCount { + count: guest_count, + has_visible_participants: non_guest_count > 0, + }, + ); } // Populate pending participants. @@ -980,21 +989,25 @@ impl CollabPanel { fn render_guest_count( &self, count: usize, + has_visible_participants: bool, is_selected: bool, cx: &mut ViewContext, ) -> impl IntoElement { - // TODO! disable manage_members for guests. + let manageable_channel_id = ActiveCall::global(cx).read(cx).room().and_then(|room| { + let room = room.read(cx); + if room.local_participant_is_admin() { + room.channel_id() + } else { + None + } + }); + ListItem::new("guest_count") .selected(is_selected) - .on_click(cx.listener(move |this, _, cx| { - if let Some(channel_id) = ActiveCall::global(cx).read(cx).channel_id(cx) { - this.manage_members(channel_id, cx) - } - })) .start_slot( h_stack() .gap_1() - .child(render_tree_branch(false, cx)) + .child(render_tree_branch(!has_visible_participants, cx)) .child(""), ) .child(Label::new(if count == 1 { @@ -1002,7 +1015,10 @@ impl CollabPanel { } else { format!("{} guests", count) })) - .tooltip(move |cx| Tooltip::text("Manage Members", cx)) + .when_some(manageable_channel_id, |el, channel_id| { + el.tooltip(move |cx| Tooltip::text("Manage Members", cx)) + .on_click(cx.listener(move |this, _, cx| this.manage_members(channel_id, cx))) + }) } fn has_subchannels(&self, ix: usize) -> bool { @@ -1227,7 +1243,14 @@ impl CollabPanel { } } ListEntry::GuestCount { .. } => { - if let Some(channel_id) = ActiveCall::global(cx).read(cx).channel_id(cx) { + let Some(room) = ActiveCall::global(cx).read(cx).room() else { + return; + }; + let room = room.read(cx); + let Some(channel_id) = room.channel_id() else { + return; + }; + if room.local_participant_is_admin() { self.manage_members(channel_id, cx) } } @@ -1786,8 +1809,11 @@ impl CollabPanel { ListEntry::ParticipantScreen { peer_id, is_last } => self .render_participant_screen(*peer_id, *is_last, is_selected, cx) .into_any_element(), - ListEntry::GuestCount { count } => self - .render_guest_count(*count, is_selected, cx) + ListEntry::GuestCount { + count, + has_visible_participants, + } => self + .render_guest_count(*count, *has_visible_participants, is_selected, cx) .into_any_element(), ListEntry::ChannelNotes { channel_id } => self .render_channel_notes(*channel_id, is_selected, cx) From 1f09f98c9be5f9739267444b4d2695a3d2fe042d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 4 Jan 2024 22:06:12 -0700 Subject: [PATCH 14/14] Remove un-needed change --- script/sqlx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/sqlx b/script/sqlx index a575efbcb619bced63c5d29b29f4d69c7e1221ae..cf2fa8d405f0b108c7131533257a859b842fdfd4 100755 --- a/script/sqlx +++ b/script/sqlx @@ -8,7 +8,7 @@ set -e cd crates/collab # Export contents of .env.toml -eval "$(cargo run --quiet --bin dotenv2)" +eval "$(cargo run --quiet --bin dotenv)" # Run sqlx command sqlx $@