diff --git a/Cargo.lock b/Cargo.lock index 958d13510343fb557afb29392853c9b00015d76d..c12a15ec574b76ba7b16c8fc18f04beaf7e0fbd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -768,6 +768,7 @@ dependencies = [ "anyhow", "async-recursion", "async-tungstenite", + "collections", "futures", "gpui", "image", @@ -3404,6 +3405,7 @@ dependencies = [ "sum_tree", "tempdir", "text", + "thiserror", "toml", "unindent", "util", diff --git a/assets/themes/cave-dark.json b/assets/themes/cave-dark.json index bbb7130f754e2e1afc76947c7803e5a73d59b978..f551bf7d010a26fabdb6306fe670af86acb55c8e 100644 --- a/assets/themes/cave-dark.json +++ b/assets/themes/cave-dark.json @@ -90,6 +90,16 @@ }, "workspace": { "background": "#26232a", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#e2dfe7", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { @@ -1359,41 +1369,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#8b8792", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#26232a", - "hover": { - "background": "#332f38" - }, - "active": { - "background": "#3f3b45" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/cave-light.json b/assets/themes/cave-light.json index 96ceb3b1a99357e49b13caff69bbfdaccd2d77c5..0f46cafdfc75a616f3abaaad2a0c9e38bd4b0ff1 100644 --- a/assets/themes/cave-light.json +++ b/assets/themes/cave-light.json @@ -90,6 +90,16 @@ }, "workspace": { "background": "#e2dfe7", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#26232a", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { @@ -1359,41 +1369,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#585260", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#e2dfe7", - "hover": { - "background": "#ccc9d2" - }, - "active": { - "background": "#b7b3bd" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/solarized-dark.json b/assets/themes/solarized-dark.json index 87f4df2ccbd0ba2e2f740d7e9f4dc46eac257f06..6ee7e689c85453c969fbfac4cecc24916cb70ca4 100644 --- a/assets/themes/solarized-dark.json +++ b/assets/themes/solarized-dark.json @@ -90,6 +90,16 @@ }, "workspace": { "background": "#073642", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#eee8d5", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { @@ -1359,41 +1369,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#93a1a1", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#073642", - "hover": { - "background": "#1b444f" - }, - "active": { - "background": "#30525c" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/solarized-light.json b/assets/themes/solarized-light.json index 6d74234a6a62e626b6e6012d98ad6c9e87d5f7b1..f0a69dcf3cb685ab1f6c06a7d05a6c403a48a17f 100644 --- a/assets/themes/solarized-light.json +++ b/assets/themes/solarized-light.json @@ -90,6 +90,16 @@ }, "workspace": { "background": "#eee8d5", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#073642", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { @@ -1359,41 +1369,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#586e75", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#eee8d5", - "hover": { - "background": "#d7d6c8" - }, - "active": { - "background": "#c1c5bb" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/sulphurpool-dark.json b/assets/themes/sulphurpool-dark.json index d18e97dc5574bb0bb5a45530c9415f01433b9f9c..433b68c520af5df8a2eb331833bc100535aab1ef 100644 --- a/assets/themes/sulphurpool-dark.json +++ b/assets/themes/sulphurpool-dark.json @@ -90,6 +90,16 @@ }, "workspace": { "background": "#293256", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#dfe2f1", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { @@ -1359,41 +1369,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#979db4", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#293256", - "hover": { - "background": "#363f62" - }, - "active": { - "background": "#444c6f" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/assets/themes/sulphurpool-light.json b/assets/themes/sulphurpool-light.json index 157200701259325e6018a621e195dfdc4945d47f..9ec1da7c6c0fbbf9ff13a12e8a563ba6b0732e22 100644 --- a/assets/themes/sulphurpool-light.json +++ b/assets/themes/sulphurpool-light.json @@ -90,6 +90,16 @@ }, "workspace": { "background": "#dfe2f1", + "joining_project_avatar": { + "corner_radius": 40, + "width": 80 + }, + "joining_project_message": { + "padding": 12, + "family": "Zed Sans", + "color": "#293256", + "size": 18 + }, "leader_border_opacity": 0.7, "leader_border_width": 2, "tab": { @@ -1359,41 +1369,7 @@ "button_width": 16, "corner_radius": 8 }, - "shared_project_row": { - "guest_avatar_spacing": 4, - "height": 24, - "guest_avatar": { - "corner_radius": 8, - "width": 14 - }, - "name": { - "family": "Zed Mono", - "color": "#5e6687", - "size": 14, - "margin": { - "left": 8, - "right": 6 - } - }, - "guests": { - "margin": { - "left": 8, - "right": 8 - } - }, - "padding": { - "left": 12, - "right": 12 - }, - "background": "#dfe2f1", - "hover": { - "background": "#cdd1e2" - }, - "active": { - "background": "#bbc0d3" - } - }, - "unshared_project_row": { + "project_row": { "guest_avatar_spacing": 4, "height": 24, "guest_avatar": { diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index f4f85b64d07799a85711dcb6ffee382f8beeff5d..45e8547c2c0bd40705e6694c8c4af45ec67ed5c9 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -8,9 +8,10 @@ path = "src/client.rs" doctest = false [features] -test-support = ["gpui/test-support", "rpc/test-support"] +test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"] [dependencies] +collections = { path = "../collections" } gpui = { path = "../gpui" } util = { path = "../util" } rpc = { path = "../rpc" } @@ -33,5 +34,6 @@ tiny_http = "0.8" url = "2.2" [dev-dependencies] +collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 75d5b459e134d97b61d01e949e164ab80baf9d89..0fc0f97949bfdc73d1921ab23c514ef11b29a18b 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1106,7 +1106,7 @@ mod tests { let (done_tx1, mut done_rx1) = smol::channel::unbounded(); let (done_tx2, mut done_rx2) = smol::channel::unbounded(); client.add_model_message_handler( - move |model: ModelHandle, _: TypedEnvelope, _, cx| { + move |model: ModelHandle, _: TypedEnvelope, _, cx| { match model.read_with(&cx, |model, _| model.id) { 1 => done_tx1.try_send(()).unwrap(), 2 => done_tx2.try_send(()).unwrap(), @@ -1135,8 +1135,8 @@ mod tests { let subscription3 = model3.update(cx, |_, cx| client.add_model_for_remote_entity(3, cx)); drop(subscription3); - server.send(proto::UnshareProject { project_id: 1 }); - server.send(proto::UnshareProject { project_id: 2 }); + server.send(proto::JoinProject { project_id: 1 }); + server.send(proto::JoinProject { project_id: 2 }); done_rx1.next().await.unwrap(); done_rx2.next().await.unwrap(); } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 84254da73a5bb0815acf3b66bf0cef112088f030..08092f46f1951f5ab717f65e4946a80c566c12cb 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,13 +1,11 @@ use super::{http::HttpClient, proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; +use collections::{hash_map::Entry, BTreeSet, HashMap, HashSet}; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{prelude::Stream, sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; -use std::{ - collections::{hash_map::Entry, HashMap, HashSet}, - sync::{Arc, Weak}, -}; +use std::sync::{Arc, Weak}; use util::TryFutureExt as _; #[derive(Debug)] @@ -17,6 +15,26 @@ pub struct User { pub avatar: Option>, } +impl PartialOrd for User { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(&other)) + } +} + +impl Ord for User { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.github_login.cmp(&other.github_login) + } +} + +impl PartialEq for User { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.github_login == other.github_login + } +} + +impl Eq for User {} + #[derive(Debug)] pub struct Contact { pub user: Arc, @@ -27,9 +45,8 @@ pub struct Contact { #[derive(Debug)] pub struct ProjectMetadata { pub id: u64, - pub is_shared: bool, pub worktree_root_names: Vec, - pub guests: Vec>, + pub guests: BTreeSet>, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -170,7 +187,7 @@ impl UserStore { self.client.upgrade().unwrap().id, message ); - let mut user_ids = HashSet::new(); + let mut user_ids = HashSet::default(); for contact in &message.contacts { user_ids.insert(contact.user_id); user_ids.extend(contact.projects.iter().flat_map(|w| &w.guests).copied()); @@ -547,9 +564,9 @@ impl Contact { .await?; let mut projects = Vec::new(); for project in contact.projects { - let mut guests = Vec::new(); + let mut guests = BTreeSet::new(); for participant_id in project.guests { - guests.push( + guests.insert( user_store .update(cx, |user_store, cx| { user_store.fetch_user(participant_id, cx) @@ -560,7 +577,6 @@ impl Contact { projects.push(ProjectMetadata { id: project.id, worktree_root_names: project.worktree_root_names.clone(), - is_shared: project.is_shared, guests, }); } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b2fce39616d0b868a1b4e6f30713f4f8cfae0736..f33441a2b84178ab7f433ada8ad06bf2781b8174 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -66,6 +66,11 @@ impl Response { self.server.peer.respond(self.receipt, payload)?; Ok(()) } + + fn into_receipt(self) -> Receipt { + self.responded.store(true, SeqCst); + self.receipt + } } pub struct Server { @@ -115,10 +120,9 @@ impl Server { .add_request_handler(Server::ping) .add_request_handler(Server::register_project) .add_message_handler(Server::unregister_project) - .add_request_handler(Server::share_project) - .add_message_handler(Server::unshare_project) .add_request_handler(Server::join_project) .add_message_handler(Server::leave_project) + .add_message_handler(Server::respond_to_join_project_request) .add_request_handler(Server::register_worktree) .add_message_handler(Server::unregister_worktree) .add_request_handler(Server::update_worktree) @@ -337,31 +341,59 @@ impl Server { #[instrument(skip(self), err)] async fn sign_out(self: &mut Arc, connection_id: ConnectionId) -> Result<()> { self.peer.disconnect(connection_id); - let removed_connection = self.store_mut().await.remove_connection(connection_id)?; - for (project_id, project) in removed_connection.hosted_projects { - if let Some(share) = project.share { - broadcast(connection_id, share.guests.keys().copied(), |conn_id| { + let removed_user_id = { + let mut store = self.store_mut().await; + let removed_connection = store.remove_connection(connection_id)?; + + for (project_id, project) in removed_connection.hosted_projects { + broadcast(connection_id, project.guests.keys().copied(), |conn_id| { self.peer - .send(conn_id, proto::UnshareProject { project_id }) + .send(conn_id, proto::UnregisterProject { project_id }) }); + + for (_, receipts) in project.join_requests { + for receipt in receipts { + self.peer.respond( + receipt, + proto::JoinProjectResponse { + variant: Some(proto::join_project_response::Variant::Decline( + proto::join_project_response::Decline { + reason: proto::join_project_response::decline::Reason::WentOffline as i32 + }, + )), + }, + )?; + } + } } - } - for (project_id, peer_ids) in removed_connection.guest_project_ids { - broadcast(connection_id, peer_ids, |conn_id| { - self.peer.send( - conn_id, - proto::RemoveProjectCollaborator { - project_id, - peer_id: connection_id.0, - }, - ) - }); - } + for project_id in removed_connection.guest_project_ids { + if let Some(project) = store.project(project_id).trace_err() { + broadcast(connection_id, project.connection_ids(), |conn_id| { + self.peer.send( + conn_id, + proto::RemoveProjectCollaborator { + project_id, + peer_id: connection_id.0, + }, + ) + }); + if project.guests.is_empty() { + self.peer + .send( + project.host_connection_id, + proto::ProjectUnshared { project_id }, + ) + .trace_err(); + } + } + } - self.update_user_contacts(removed_connection.user_id) - .await?; + removed_connection.user_id + }; + + self.update_user_contacts(removed_user_id).await?; Ok(()) } @@ -396,31 +428,32 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> Result<()> { - let user_id = { + let (user_id, project) = { let mut state = self.store_mut().await; - state.unregister_project(request.payload.project_id, request.sender_id)?; - state.user_id_for_connection(request.sender_id)? + let project = + state.unregister_project(request.payload.project_id, request.sender_id)?; + (state.user_id_for_connection(request.sender_id)?, project) }; + for (_, receipts) in project.join_requests { + for receipt in receipts { + self.peer.respond( + receipt, + proto::JoinProjectResponse { + variant: Some(proto::join_project_response::Variant::Decline( + proto::join_project_response::Decline { + reason: proto::join_project_response::decline::Reason::Closed + as i32, + }, + )), + }, + )?; + } + } self.update_user_contacts(user_id).await?; Ok(()) } - async fn share_project( - self: Arc, - request: TypedEnvelope, - response: Response, - ) -> Result<()> { - let user_id = { - let mut state = self.store_mut().await; - state.share_project(request.payload.project_id, request.sender_id)?; - state.user_id_for_connection(request.sender_id)? - }; - self.update_user_contacts(user_id).await?; - response.send(proto::Ack {})?; - Ok(()) - } - async fn update_user_contacts(self: &Arc, user_id: UserId) -> Result<()> { let contacts = self.app_state.db.get_contacts(user_id).await?; let store = self.store().await; @@ -451,24 +484,6 @@ impl Server { Ok(()) } - async fn unshare_project( - self: Arc, - request: TypedEnvelope, - ) -> Result<()> { - let project_id = request.payload.project_id; - let project; - { - let mut state = self.store_mut().await; - project = state.unshare_project(project_id, request.sender_id)?; - broadcast(request.sender_id, project.connection_ids, |conn_id| { - self.peer - .send(conn_id, proto::UnshareProject { project_id }) - }); - } - self.update_user_contacts(project.host_user_id).await?; - Ok(()) - } - async fn join_project( self: Arc, request: TypedEnvelope, @@ -477,9 +492,12 @@ impl Server { let project_id = request.payload.project_id; let host_user_id; let guest_user_id; + let host_connection_id; { let state = self.store().await; - host_user_id = state.project(project_id)?.host_user_id; + let project = state.project(project_id)?; + host_user_id = project.host_user_id; + host_connection_id = project.host_connection_id; guest_user_id = state.user_id_for_connection(request.sender_id)?; }; @@ -492,22 +510,74 @@ impl Server { return Err(anyhow!("no such project"))?; } + self.store_mut().await.request_join_project( + guest_user_id, + project_id, + response.into_receipt(), + )?; + self.peer.send( + host_connection_id, + proto::RequestJoinProject { + project_id, + requester_id: guest_user_id.to_proto(), + }, + )?; + Ok(()) + } + + async fn respond_to_join_project_request( + self: Arc, + request: TypedEnvelope, + ) -> Result<()> { + let host_user_id; + { - let state = &mut *self.store_mut().await; - let joined = state.join_project(request.sender_id, guest_user_id, project_id)?; - let share = joined.project.share()?; - let peer_count = share.guests.len(); + let mut state = self.store_mut().await; + let project_id = request.payload.project_id; + let project = state.project(project_id)?; + if project.host_connection_id != request.sender_id { + Err(anyhow!("no such connection"))?; + } + + host_user_id = project.host_user_id; + let guest_user_id = UserId::from_proto(request.payload.requester_id); + + if !request.payload.allow { + let receipts = state + .deny_join_project_request(request.sender_id, guest_user_id, project_id) + .ok_or_else(|| anyhow!("no such request"))?; + for receipt in receipts { + self.peer.respond( + receipt, + proto::JoinProjectResponse { + variant: Some(proto::join_project_response::Variant::Decline( + proto::join_project_response::Decline { + reason: proto::join_project_response::decline::Reason::Declined + as i32, + }, + )), + }, + )?; + } + return Ok(()); + } + + let (receipts_with_replica_ids, project) = state + .accept_join_project_request(request.sender_id, guest_user_id, project_id) + .ok_or_else(|| anyhow!("no such request"))?; + + let peer_count = project.guests.len(); let mut collaborators = Vec::with_capacity(peer_count); collaborators.push(proto::Collaborator { - peer_id: joined.project.host_connection_id.0, + peer_id: project.host_connection_id.0, replica_id: 0, - user_id: joined.project.host_user_id.to_proto(), + user_id: project.host_user_id.to_proto(), }); - let worktrees = share + let worktrees = project .worktrees .iter() .filter_map(|(id, shared_worktree)| { - let worktree = joined.project.worktrees.get(&id)?; + let worktree = project.worktrees.get(&id)?; Some(proto::Worktree { id: *id, root_name: worktree.root_name.clone(), @@ -521,9 +591,14 @@ impl Server { scan_id: shared_worktree.scan_id, }) }) - .collect(); - for (peer_conn_id, (peer_replica_id, peer_user_id)) in &share.guests { - if *peer_conn_id != request.sender_id { + .collect::>(); + + // Add all guests other than the requesting user's own connections as collaborators + for (peer_conn_id, (peer_replica_id, peer_user_id)) in &project.guests { + if receipts_with_replica_ids + .iter() + .all(|(receipt, _)| receipt.sender_id != *peer_conn_id) + { collaborators.push(proto::Collaborator { peer_id: peer_conn_id.0, replica_id: *peer_replica_id as u32, @@ -531,30 +606,42 @@ impl Server { }); } } - broadcast( - request.sender_id, - joined.project.connection_ids(), - |conn_id| { - self.peer.send( - conn_id, - proto::AddProjectCollaborator { - project_id, - collaborator: Some(proto::Collaborator { - peer_id: request.sender_id.0, - replica_id: joined.replica_id as u32, - user_id: guest_user_id.to_proto(), - }), - }, - ) - }, - ); - response.send(proto::JoinProjectResponse { - worktrees, - replica_id: joined.replica_id as u32, - collaborators, - language_servers: joined.project.language_servers.clone(), - })?; + + for conn_id in project.connection_ids() { + for (receipt, replica_id) in &receipts_with_replica_ids { + if conn_id != receipt.sender_id { + self.peer.send( + conn_id, + proto::AddProjectCollaborator { + project_id, + collaborator: Some(proto::Collaborator { + peer_id: receipt.sender_id.0, + replica_id: *replica_id as u32, + user_id: guest_user_id.to_proto(), + }), + }, + )?; + } + } + } + + for (receipt, replica_id) in receipts_with_replica_ids { + self.peer.respond( + receipt, + proto::JoinProjectResponse { + variant: Some(proto::join_project_response::Variant::Accept( + proto::join_project_response::Accept { + worktrees: worktrees.clone(), + replica_id: replica_id as u32, + collaborators: collaborators.clone(), + language_servers: project.language_servers.clone(), + }, + )), + }, + )?; + } } + self.update_user_contacts(host_user_id).await?; Ok(()) } @@ -567,17 +654,37 @@ impl Server { let project_id = request.payload.project_id; let project; { - let mut state = self.store_mut().await; - project = state.leave_project(sender_id, project_id)?; - broadcast(sender_id, project.connection_ids, |conn_id| { + let mut store = self.store_mut().await; + project = store.leave_project(sender_id, project_id)?; + + if project.remove_collaborator { + broadcast(sender_id, project.connection_ids, |conn_id| { + self.peer.send( + conn_id, + proto::RemoveProjectCollaborator { + project_id, + peer_id: sender_id.0, + }, + ) + }); + } + + if let Some(requester_id) = project.cancel_request { self.peer.send( - conn_id, - proto::RemoveProjectCollaborator { + project.host_connection_id, + proto::JoinProjectRequestCancelled { project_id, - peer_id: sender_id.0, + requester_id: requester_id.to_proto(), }, - ) - }); + )?; + } + + if project.unshare { + self.peer.send( + project.host_connection_id, + proto::ProjectUnshared { project_id }, + )?; + } } self.update_user_contacts(project.host_user_id).await?; Ok(()) @@ -603,6 +710,7 @@ impl Server { Worktree { root_name: request.payload.root_name.clone(), visible: request.payload.visible, + ..Default::default() }, )?; @@ -1542,6 +1650,7 @@ mod tests { use settings::Settings; use sqlx::types::time::OffsetDateTime; use std::{ + cell::RefCell, env, ops::Deref, path::{Path, PathBuf}, @@ -1564,7 +1673,12 @@ mod tests { } #[gpui::test(iterations = 10)] - async fn test_share_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + async fn test_share_project( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_b2: &mut TestAppContext, + ) { let (window_b, _) = cx_b.add_window(|_| EmptyView); let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); @@ -1573,7 +1687,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1596,6 +1710,9 @@ mod tests { cx, ) }); + let project_id = project_a + .read_with(cx_a, |project, _| project.next_remote_id()) + .await; let (worktree_a, _) = project_a .update(cx_a, |p, cx| { p.find_or_create_local_worktree("/a", true, cx) @@ -1606,20 +1723,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let client_b_peer_id = client_b.peer_id; + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let replica_id_b = project_b.read_with(cx_b, |project, _| { assert_eq!( @@ -1636,7 +1743,7 @@ mod tests { project_a .condition(&cx_a, |tree, _| { tree.collaborators() - .get(&client_b.peer_id) + .get(&client_b_peer_id) .map_or(false, |collaborator| { collaborator.replica_id == replica_id_b && collaborator.user.github_login == "user_b" @@ -1681,15 +1788,49 @@ mod tests { // .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0) // .await; - // Dropping the client B's project removes client B from client A's collaborators. - cx_b.update(move |_| drop(project_b)); - project_a - .condition(&cx_a, |project, _| project.collaborators().is_empty()) - .await; + // Client B can join again on a different window because they are already a participant. + let client_b2 = server.create_client(cx_b2, "user_b").await; + let project_b2 = Project::remote( + project_id, + client_b2.client.clone(), + client_b2.user_store.clone(), + lang_registry.clone(), + FakeFs::new(cx_b2.background()), + &mut cx_b2.to_async(), + ) + .await + .unwrap(); + deterministic.run_until_parked(); + project_a.read_with(cx_a, |project, _| { + assert_eq!(project.collaborators().len(), 2); + }); + project_b.read_with(cx_b, |project, _| { + assert_eq!(project.collaborators().len(), 2); + }); + project_b2.read_with(cx_b2, |project, _| { + assert_eq!(project.collaborators().len(), 2); + }); + + // Dropping client B's first project removes only that from client A's collaborators. + cx_b.update(move |_| { + drop(client_b.project.take()); + drop(project_b); + }); + deterministic.run_until_parked(); + project_a.read_with(cx_a, |project, _| { + assert_eq!(project.collaborators().len(), 1); + }); + project_b2.read_with(cx_b2, |project, _| { + assert_eq!(project.collaborators().len(), 1); + }); } #[gpui::test(iterations = 10)] - async fn test_unshare_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + async fn test_unshare_project( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + ) { let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); cx_a.foreground().forbid_parking(); @@ -1697,7 +1838,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -1729,54 +1870,27 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); project_b .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); - // Unshare the project as client A - project_a.update(cx_a, |project, cx| project.unshare(cx)); - project_b - .condition(cx_b, |project, _| project.is_read_only()) - .await; - assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); + // When client B leaves the project, it gets automatically unshared. cx_b.update(|_| { + drop(client_b.project.take()); drop(project_b); }); + deterministic.run_until_parked(); + assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); - // Share the project again and ensure guests can still join. - project_a - .update(cx_a, |project, cx| project.share(cx)) - .await - .unwrap(); + // When client B joins again, the project gets re-shared. + let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await; assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - - let project_b2 = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); project_b2 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await @@ -1784,17 +1898,27 @@ mod tests { } #[gpui::test(iterations = 10)] - async fn test_host_disconnect(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + async fn test_host_disconnect( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, + ) { let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); cx_a.foreground().forbid_parking(); - // Connect to a server as 2 clients. + // Connect to a server as 3 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .make_contacts(vec![ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + ]) .await; // Share a project as client A @@ -1815,6 +1939,9 @@ mod tests { cx, ) }); + let project_id = project_a + .read_with(cx_a, |project, _| project.next_remote_id()) + .await; let (worktree_a, _) = project_a .update(cx_a, |p, cx| { p.find_or_create_local_worktree("/a", true, cx) @@ -1824,27 +1951,35 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); project_b .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); + // Request to join that project as client C + let project_c = cx_c.spawn(|mut cx| { + let client = client_c.client.clone(); + let user_store = client_c.user_store.clone(); + let lang_registry = lang_registry.clone(); + async move { + Project::remote( + project_id, + client, + user_store, + lang_registry.clone(), + FakeFs::new(cx.background()), + &mut cx, + ) + .await + } + }); + deterministic.run_until_parked(); + // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. server.disconnect_client(client_a.current_user_id(cx_a)); cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); @@ -1859,31 +1994,213 @@ mod tests { cx_b.update(|_| { drop(project_b); }); + assert!(matches!( + project_c.await.unwrap_err(), + project::JoinProjectError::HostWentOffline + )); - // Await reconnection - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; + // Ensure guests can still join. + let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); + project_b2 + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .await + .unwrap(); + } - // Share the project again and ensure guests can still join. - project_a - .update(cx_a, |project, cx| project.share(cx)) + #[gpui::test(iterations = 10)] + async fn test_decline_join_request( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + ) { + let lang_registry = Arc::new(LanguageRegistry::test()); + let fs = FakeFs::new(cx_a.background()); + cx_a.foreground().forbid_parking(); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + // Share a project as client A + fs.insert_tree("/a", json!({})).await; + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let project_id = project_a + .read_with(cx_a, |project, _| project.next_remote_id()) + .await; + let (worktree_a, _) = project_a + .update(cx_a, |p, cx| { + p.find_or_create_local_worktree("/a", true, cx) + }) .await .unwrap(); - assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); + worktree_a + .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; - let project_b2 = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - project_b2 - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + // Request to join that project as client B + let project_b = cx_b.spawn(|mut cx| { + let client = client_b.client.clone(); + let user_store = client_b.user_store.clone(); + let lang_registry = lang_registry.clone(); + async move { + Project::remote( + project_id, + client, + user_store, + lang_registry.clone(), + FakeFs::new(cx.background()), + &mut cx, + ) + .await + } + }); + deterministic.run_until_parked(); + project_a.update(cx_a, |project, cx| { + project.respond_to_join_request(client_b.user_id().unwrap(), false, cx) + }); + assert!(matches!( + project_b.await.unwrap_err(), + project::JoinProjectError::HostDeclined + )); + + // Request to join the project again as client B + let project_b = cx_b.spawn(|mut cx| { + let client = client_b.client.clone(); + let user_store = client_b.user_store.clone(); + let lang_registry = lang_registry.clone(); + async move { + Project::remote( + project_id, + client, + user_store, + lang_registry.clone(), + FakeFs::new(cx.background()), + &mut cx, + ) + .await + } + }); + + // Close the project on the host + deterministic.run_until_parked(); + cx_a.update(|_| drop(project_a)); + deterministic.run_until_parked(); + assert!(matches!( + project_b.await.unwrap_err(), + project::JoinProjectError::HostClosedProject + )); + } + + #[gpui::test(iterations = 10)] + async fn test_cancel_join_request( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + ) { + let lang_registry = Arc::new(LanguageRegistry::test()); + let fs = FakeFs::new(cx_a.background()); + cx_a.foreground().forbid_parking(); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .await; + + // Share a project as client A + fs.insert_tree("/a", json!({})).await; + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let project_id = project_a + .read_with(cx_a, |project, _| project.next_remote_id()) + .await; + + let project_a_events = Rc::new(RefCell::new(Vec::new())); + let user_b = client_a + .user_store + .update(cx_a, |store, cx| { + store.fetch_user(client_b.user_id().unwrap(), cx) + }) .await .unwrap(); + project_a.update(cx_a, { + let project_a_events = project_a_events.clone(); + move |_, cx| { + cx.subscribe(&cx.handle(), move |_, _, event, _| { + project_a_events.borrow_mut().push(event.clone()); + }) + .detach(); + } + }); + + let (worktree_a, _) = project_a + .update(cx_a, |p, cx| { + p.find_or_create_local_worktree("/a", true, cx) + }) + .await + .unwrap(); + worktree_a + .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + + // Request to join that project as client B + let project_b = cx_b.spawn(|mut cx| { + let client = client_b.client.clone(); + let user_store = client_b.user_store.clone(); + let lang_registry = lang_registry.clone(); + async move { + Project::remote( + project_id, + client, + user_store, + lang_registry.clone(), + FakeFs::new(cx.background()), + &mut cx, + ) + .await + } + }); + deterministic.run_until_parked(); + assert_eq!( + &*project_a_events.borrow(), + &[project::Event::ContactRequestedJoin(user_b.clone())] + ); + project_a_events.borrow_mut().clear(); + + // Cancel the join request by leaving the project + client_b + .client + .send(proto::LeaveProject { project_id }) + .unwrap(); + drop(project_b); + + deterministic.run_until_parked(); + assert_eq!( + &*project_a_events.borrow(), + &[project::Event::ContactCancelledJoinRequest(user_b.clone())] + ); } #[gpui::test(iterations = 10)] @@ -1899,8 +2216,8 @@ mod tests { // Connect to a server as 3 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - let client_c = server.create_client(cx_c, "user_c").await; + let mut client_b = server.create_client(cx_b, "user_b").await; + let mut client_c = server.create_client(cx_c, "user_c").await; server .make_contacts(vec![ (&client_a, cx_a), @@ -1936,31 +2253,11 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join that worktree as clients B and C. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - let project_c = Project::remote( - project_id, - client_c.clone(), - client_c.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_c.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await; let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap()); let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap()); @@ -2099,13 +2396,7 @@ mod tests { .await; let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await; - let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap()); - project_a - .update(cx_a, |project, cx| project.share(cx)) - .await - .unwrap(); - - let project_b = client_b.build_remote_project(project_id, cx_b).await; + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); @@ -2252,7 +2543,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2284,21 +2575,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open a buffer as client B let buffer_b = project_b @@ -2336,7 +2616,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2368,21 +2648,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let _worktree_b = project_b.update(cx_b, |p, cx| p.worktrees(cx).next().unwrap()); // Open a buffer as client B @@ -2420,7 +2689,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2451,21 +2720,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open a buffer as client A let buffer_a = project_a @@ -2501,7 +2759,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2532,21 +2790,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join that project as client B - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // See that a guest has joined as client A. project_a @@ -2557,7 +2804,10 @@ mod tests { let buffer_b = cx_b .background() .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))); - cx_b.update(|_| drop(project_b)); + cx_b.update(|_| { + drop(client_b.project.take()); + drop(project_b); + }); drop(buffer_b); // See that the guest has left. @@ -2575,7 +2825,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2607,25 +2857,9 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a - .update(cx_a, |project, _| project.next_remote_id()) - .await; - project_a - .update(cx_a, |project, cx| project.share(cx)) - .await - .unwrap(); // Join that project as client B - let _project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Client A sees that a guest has joined. project_a @@ -2639,16 +2873,7 @@ mod tests { .await; // Rejoin the project as client B - let _project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Client A sees that a guest has re-joined. project_a @@ -2666,10 +2891,12 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_collaborating_with_diagnostics( + deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, ) { - cx_a.foreground().forbid_parking(); + deterministic.forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); @@ -2688,9 +2915,14 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; + let mut client_c = server.create_client(cx_c, "user_c").await; server - .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) + .make_contacts(vec![ + (&client_a, cx_a), + (&client_b, cx_b), + (&client_c, cx_c), + ]) .await; // Share a project as client A @@ -2722,10 +2954,9 @@ mod tests { .await; let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Cause the language server to start. - let _ = cx_a + let _buffer = cx_a .background() .spawn(project_a.update(cx_a, |project, cx| { project.open_buffer( @@ -2739,6 +2970,9 @@ mod tests { .await .unwrap(); + // Join the worktree as client B. + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; + // Simulate a language server reporting errors for a file. let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server @@ -2755,37 +2989,18 @@ mod tests { ..Default::default() }], }, - ); - - // Wait for server to see the diagnostics update. - server - .condition(|store| { - let worktree = store - .project(project_id) - .unwrap() - .share - .as_ref() - .unwrap() - .worktrees - .get(&worktree_id.to_proto()) - .unwrap(); - - !worktree.diagnostic_summaries.is_empty() - }) - .await; + ); - // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + // Wait for server to see the diagnostics update. + deterministic.run_until_parked(); + { + let store = server.store.read().await; + let project = store.project(project_id).unwrap(); + let worktree = project.worktrees.get(&worktree_id.to_proto()).unwrap(); + assert!(!worktree.diagnostic_summaries.is_empty()); + } + // Ensure client B observes the new diagnostics. project_b.read_with(cx_b, |project, cx| { assert_eq!( project.diagnostic_summaries(cx).collect::>(), @@ -2803,6 +3018,25 @@ mod tests { ) }); + // Join project as client C and observe the diagnostics. + let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await; + project_c.read_with(cx_c, |project, cx| { + assert_eq!( + project.diagnostic_summaries(cx).collect::>(), + &[( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("a.rs")), + }, + DiagnosticSummary { + error_count: 1, + warning_count: 0, + ..Default::default() + }, + )] + ) + }); + // Simulate a language server reporting more errors for a file. fake_language_server.notify::( lsp::PublishDiagnosticsParams { @@ -2828,23 +3062,40 @@ mod tests { }, ); - // Client b gets the updated summaries - project_b - .condition(&cx_b, |project, cx| { - project.diagnostic_summaries(cx).collect::>() - == &[( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("a.rs")), - }, - DiagnosticSummary { - error_count: 1, - warning_count: 1, - ..Default::default() - }, - )] - }) - .await; + // Clients B and C get the updated summaries + deterministic.run_until_parked(); + project_b.read_with(cx_b, |project, cx| { + assert_eq!( + project.diagnostic_summaries(cx).collect::>(), + [( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("a.rs")), + }, + DiagnosticSummary { + error_count: 1, + warning_count: 1, + ..Default::default() + }, + )] + ); + }); + project_c.read_with(cx_c, |project, cx| { + assert_eq!( + project.diagnostic_summaries(cx).collect::>(), + [( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("a.rs")), + }, + DiagnosticSummary { + error_count: 1, + warning_count: 1, + ..Default::default() + }, + )] + ); + }); // Open the file with the errors on client B. They should be present. let buffer_b = cx_b @@ -2893,16 +3144,16 @@ mod tests { diagnostics: vec![], }, ); - project_a - .condition(cx_a, |project, cx| { - project.diagnostic_summaries(cx).collect::>() == &[] - }) - .await; - project_b - .condition(cx_b, |project, cx| { - project.diagnostic_summaries(cx).collect::>() == &[] - }) - .await; + deterministic.run_until_parked(); + project_a.read_with(cx_a, |project, cx| { + assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + }); + project_b.read_with(cx_b, |project, cx| { + assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + }); + project_c.read_with(cx_c, |project, cx| { + assert_eq!(project.diagnostic_summaries(cx).collect::>(), []) + }); } #[gpui::test(iterations = 10)] @@ -2938,7 +3189,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -2970,21 +3221,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open a file in an editor as the guest. let buffer_b = project_b @@ -3121,7 +3361,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3152,25 +3392,14 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); let buffer_a = project_a .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)) .await .unwrap(); // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let buffer_b = cx_b .background() @@ -3252,7 +3481,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3283,21 +3512,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + // Join the project as client B. + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let buffer_b = cx_b .background() @@ -3366,7 +3584,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3390,21 +3608,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + // Join the project as client B. + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open the file on client B. let buffer_b = cx_b @@ -3513,7 +3720,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3537,21 +3744,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open the file on client B. let buffer_b = cx_b @@ -3646,7 +3842,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3661,7 +3857,6 @@ mod tests { cx, ) }); - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let (worktree_1, _) = project_a .update(cx_a, |p, cx| { @@ -3682,20 +3877,8 @@ mod tests { .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); - + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let results = project_b .update(cx_b, |project, cx| { project.search(SearchQuery::text("world", false, false), cx) @@ -3757,7 +3940,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3781,21 +3964,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Open the file on client B. let buffer_b = cx_b @@ -3904,7 +4076,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -3928,21 +4100,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Cause the language server to start. let _buffer = cx_b @@ -4036,7 +4197,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -4061,21 +4222,10 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + // Join the project as client B. + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let buffer_b1 = cx_b .background() @@ -4136,7 +4286,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -4168,26 +4318,15 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); - // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + // Join the project as client B. + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let mut params = cx_b.update(WorkspaceParams::test); params.languages = lang_registry.clone(); + params.project = project_b.clone(); params.client = client_b.client.clone(); params.user_store = client_b.user_store.clone(); - params.project = project_b; let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b @@ -4388,7 +4527,7 @@ mod tests { // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; + let mut client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)]) .await; @@ -4420,26 +4559,15 @@ mod tests { worktree_a .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); - project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); // Join the worktree as client B. - let project_b = Project::remote( - project_id, - client_b.clone(), - client_b.user_store.clone(), - lang_registry.clone(), - fs.clone(), - &mut cx_b.to_async(), - ) - .await - .unwrap(); + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; let mut params = cx_b.update(WorkspaceParams::test); params.languages = lang_registry.clone(); + params.project = project_b.clone(); params.client = client_b.client.clone(); params.user_store = client_b.user_store.clone(); - params.project = project_b; let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b @@ -5022,7 +5150,9 @@ mod tests { ("user_a", true, vec![]), ("user_b", true, vec![]), ("user_c", true, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } @@ -5038,37 +5168,17 @@ mod tests { assert_eq!( contacts(store), [ - ("user_a", true, vec![("a", false, vec![])]), - ("user_b", true, vec![]), - ("user_c", true, vec![]) - ] - ) - }); - } - - let project_id = project_a - .update(cx_a, |project, _| project.next_remote_id()) - .await; - project_a - .update(cx_a, |project, cx| project.share(cx)) - .await - .unwrap(); - - deterministic.run_until_parked(); - for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { - client.user_store.read_with(*cx, |store, _| { - assert_eq!( - contacts(store), - [ - ("user_a", true, vec![("a", true, vec![])]), + ("user_a", true, vec![("a", vec![])]), ("user_b", true, vec![]), ("user_c", true, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } - let _project_b = client_b.build_remote_project(project_id, cx_b).await; + let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; deterministic.run_until_parked(); for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] { @@ -5076,10 +5186,12 @@ mod tests { assert_eq!( contacts(store), [ - ("user_a", true, vec![("a", true, vec!["user_b"])]), + ("user_a", true, vec![("a", vec!["user_b"])]), ("user_b", true, vec![]), ("user_c", true, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } @@ -5095,10 +5207,12 @@ mod tests { assert_eq!( contacts(store), [ - ("user_a", true, vec![("a", true, vec!["user_b"])]), - ("user_b", true, vec![("b", false, vec![])]), + ("user_a", true, vec![("a", vec!["user_b"])]), + ("user_b", true, vec![("b", vec![])]), ("user_c", true, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } @@ -5118,9 +5232,11 @@ mod tests { contacts(store), [ ("user_a", true, vec![]), - ("user_b", true, vec![("b", false, vec![])]), + ("user_b", true, vec![("b", vec![])]), ("user_c", true, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } @@ -5134,9 +5250,11 @@ mod tests { contacts(store), [ ("user_a", true, vec![]), - ("user_b", true, vec![("b", false, vec![])]), + ("user_b", true, vec![("b", vec![])]), ("user_c", false, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } @@ -5157,14 +5275,16 @@ mod tests { contacts(store), [ ("user_a", true, vec![]), - ("user_b", true, vec![("b", false, vec![])]), + ("user_b", true, vec![("b", vec![])]), ("user_c", true, vec![]) - ] + ], + "{} has the wrong contacts", + client.username ) }); } - fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, bool, Vec<&str>)>)> { + fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, Vec<&str>)>)> { user_store .contacts() .iter() @@ -5175,7 +5295,6 @@ mod tests { .map(|p| { ( p.worktree_root_names[0].as_str(), - p.is_shared, p.guests.iter().map(|p| p.github_login.as_str()).collect(), ) }) @@ -5408,20 +5527,9 @@ mod tests { ) .await; let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; - project_a - .update(cx_a, |project, cx| project.share(cx)) - .await - .unwrap(); // Client B joins the project. - let project_b = client_b - .build_remote_project( - project_a - .read_with(cx_a, |project, _| project.remote_id()) - .unwrap(), - cx_b, - ) - .await; + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); @@ -5476,6 +5584,7 @@ mod tests { }) .await .unwrap(); + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { workspace .active_item(cx) @@ -5628,20 +5737,9 @@ mod tests { ) .await; let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; - project_a - .update(cx_a, |project, cx| project.share(cx)) - .await - .unwrap(); // Client B joins the project. - let project_b = client_b - .build_remote_project( - project_a - .read_with(cx_a, |project, _| project.remote_id()) - .unwrap(), - cx_b, - ) - .await; + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); @@ -5775,20 +5873,9 @@ mod tests { ) .await; let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; - project_a - .update(cx_a, |project, cx| project.share(cx)) - .await - .unwrap(); // Client B joins the project. - let project_b = client_b - .build_remote_project( - project_a - .read_with(cx_a, |project, _| project.remote_id()) - .unwrap(), - cx_b, - ) - .await; + let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await; // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); @@ -5997,10 +6084,6 @@ mod tests { collab_worktree .read_with(&host_cx, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - host_project - .update(&mut host_cx, |project, cx| project.share(cx)) - .await - .unwrap(); // Set up fake language servers. let mut language = Language::new( @@ -6161,7 +6244,7 @@ mod tests { let (host, mut host_cx, host_err) = clients.remove(0); if let Some(host_err) = host_err { - log::error!("host error - {}", host_err); + log::error!("host error - {:?}", host_err); } host.project .as_ref() @@ -6169,18 +6252,25 @@ mod tests { .read_with(&host_cx, |project, _| assert!(!project.is_shared())); for (guest, mut guest_cx, guest_err) in clients { if let Some(guest_err) = guest_err { - log::error!("{} error - {}", guest.username, guest_err); + log::error!("{} error - {:?}", guest.username, guest_err); } - // TODO - // let contacts = server - // .store - // .read() - // .await - // .contacts_for_user(guest.current_user_id(&guest_cx)); - // assert!(!contacts - // .iter() - // .flat_map(|contact| &contact.projects) - // .any(|project| project.id == host_project_id)); + + let contacts = server + .app_state + .db + .get_contacts(guest.current_user_id(&guest_cx)) + .await + .unwrap(); + let contacts = server + .store + .read() + .await + .build_initial_contacts_update(contacts) + .contacts; + assert!(!contacts + .iter() + .flat_map(|contact| &contact.projects) + .any(|project| project.id == host_project_id)); guest .project .as_ref() @@ -6240,34 +6330,45 @@ mod tests { let removed_guest_id = user_ids.remove(guest_ix); let guest = clients.remove(guest_ix); op_start_signals.remove(guest_ix); + server.forbid_connections(); server.disconnect_client(removed_guest_id); cx.foreground().advance_clock(RECEIVE_TIMEOUT); let (guest, mut guest_cx, guest_err) = guest.await; + server.allow_connections(); + if let Some(guest_err) = guest_err { - log::error!("{} error - {}", guest.username, guest_err); + log::error!("{} error - {:?}", guest.username, guest_err); } guest .project .as_ref() .unwrap() .read_with(&guest_cx, |project, _| assert!(project.is_read_only())); - // TODO - // for user_id in &user_ids { - // for contact in server.store.read().await.contacts_for_user(*user_id) { - // assert_ne!( - // contact.user_id, removed_guest_id.0 as u64, - // "removed guest is still a contact of another peer" - // ); - // for project in contact.projects { - // for project_guest_id in project.guests { - // assert_ne!( - // project_guest_id, removed_guest_id.0 as u64, - // "removed guest appears as still participating on a project" - // ); - // } - // } - // } - // } + for user_id in &user_ids { + let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap(); + let contacts = server + .store + .read() + .await + .build_initial_contacts_update(contacts) + .contacts; + for contact in contacts { + if contact.online { + assert_ne!( + contact.user_id, removed_guest_id.0 as u64, + "removed guest is still a contact of another peer" + ); + } + for project in contact.projects { + for project_guest_id in project.guests { + assert_ne!( + project_guest_id, removed_guest_id.0 as u64, + "removed guest appears as still participating on a project" + ); + } + } + } + } log::info!("{} removed", guest.username); available_guests.push(guest.username.clone()); @@ -6298,7 +6399,7 @@ mod tests { let (host_client, mut host_cx, host_err) = clients.remove(0); if let Some(host_err) = host_err { - panic!("host error - {}", host_err); + panic!("host error - {:?}", host_err); } let host_project = host_client.project.as_ref().unwrap(); let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| { @@ -6319,7 +6420,7 @@ mod tests { for (guest_client, mut guest_cx, guest_err) in clients.into_iter() { if let Some(guest_err) = guest_err { - panic!("{} error - {}", guest_client.username, guest_err); + panic!("{} error - {:?}", guest_client.username, guest_err); } let worktree_snapshots = guest_client @@ -6501,19 +6602,18 @@ mod tests { }) }); - client - .authenticate_and_connect(false, &cx.to_async()) - .await - .unwrap(); - Channel::init(&client); Project::init(&client); cx.update(|cx| { workspace::init(&client, cx); }); - let peer_id = PeerId(connection_id_rx.next().await.unwrap().0); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); + client + .authenticate_and_connect(false, &cx.to_async()) + .await + .unwrap(); + let peer_id = PeerId(connection_id_rx.next().await.unwrap().0); let client = TestClient { client, @@ -6705,19 +6805,37 @@ mod tests { async fn build_remote_project( &mut self, - project_id: u64, - cx: &mut TestAppContext, + host_project: &ModelHandle, + host_cx: &mut TestAppContext, + guest_cx: &mut TestAppContext, ) -> ModelHandle { - let project = Project::remote( - project_id, - self.client.clone(), - self.user_store.clone(), - self.language_registry.clone(), - FakeFs::new(cx.background()), - &mut cx.to_async(), - ) - .await - .unwrap(); + let host_project_id = host_project + .read_with(host_cx, |project, _| project.next_remote_id()) + .await; + let guest_user_id = self.user_id().unwrap(); + let languages = + host_project.read_with(host_cx, |project, _| project.languages().clone()); + let project_b = guest_cx.spawn(|mut cx| { + let user_store = self.user_store.clone(); + let guest_client = self.client.clone(); + async move { + Project::remote( + host_project_id, + guest_client, + user_store.clone(), + languages, + FakeFs::new(cx.background()), + &mut cx, + ) + .await + .unwrap() + } + }); + host_cx.foreground().run_until_parked(); + host_project.update(host_cx, |project, cx| { + project.respond_to_join_request(guest_user_id, true, cx) + }); + let project = project_b.await; self.project = Some(project.clone()); project } @@ -6763,11 +6881,23 @@ mod tests { ) -> anyhow::Result<()> { let fs = project.read_with(cx, |project, _| project.fs().clone()); + cx.update(|cx| { + cx.subscribe(&project, move |project, event, cx| { + if let project::Event::ContactRequestedJoin(user) = event { + log::info!("Host: accepting join request from {}", user.github_login); + project.update(cx, |project, cx| { + project.respond_to_join_request(user.id, true, cx) + }); + } + }) + .detach(); + }); + while op_start_signal.next().await.is_some() { let distribution = rng.lock().gen_range::(0..100); let files = fs.as_fake().files().await; match distribution { - 0..=20 if !files.is_empty() => { + 0..=19 if !files.is_empty() => { let path = files.choose(&mut *rng.lock()).unwrap(); let mut path = path.as_path(); while let Some(parent_path) = path.parent() { @@ -6787,7 +6917,7 @@ mod tests { find_or_create_worktree.await?; } } - 10..=80 if !files.is_empty() => { + 20..=79 if !files.is_empty() => { let buffer = if client.buffers.is_empty() || rng.lock().gen() { let file = files.choose(&mut *rng.lock()).unwrap(); let (worktree, path) = project diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index 07103204e5553d7f6a19f96209261c706fafb18c..33fa1eb11396fd35fbe6a185593c1625b40a3721 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -1,8 +1,8 @@ use crate::db::{self, ChannelId, UserId}; use anyhow::{anyhow, Result}; -use collections::{BTreeMap, HashMap, HashSet}; -use rpc::{proto, ConnectionId}; -use std::{collections::hash_map, path::PathBuf}; +use collections::{hash_map::Entry, BTreeMap, HashMap, HashSet}; +use rpc::{proto, ConnectionId, Receipt}; +use std::{collections::hash_map, mem, path::PathBuf}; use tracing::instrument; #[derive(Default)] @@ -17,31 +17,24 @@ pub struct Store { struct ConnectionState { user_id: UserId, projects: HashSet, + requested_projects: HashSet, channels: HashSet, } pub struct Project { pub host_connection_id: ConnectionId, pub host_user_id: UserId, - pub share: Option, + pub guests: HashMap, + pub join_requests: HashMap>>, + pub active_replica_ids: HashSet, pub worktrees: HashMap, pub language_servers: Vec, } +#[derive(Default)] pub struct Worktree { pub root_name: String, pub visible: bool, -} - -#[derive(Default)] -pub struct ProjectShare { - pub guests: HashMap, - pub active_replica_ids: HashSet, - pub worktrees: HashMap, -} - -#[derive(Default)] -pub struct WorktreeShare { pub entries: HashMap, pub diagnostic_summaries: BTreeMap, pub scan_id: u64, @@ -58,25 +51,17 @@ pub type ReplicaId = u16; pub struct RemovedConnectionState { pub user_id: UserId, pub hosted_projects: HashMap, - pub guest_project_ids: HashMap>, + pub guest_project_ids: HashSet, pub contact_ids: HashSet, } -pub struct JoinedProject<'a> { - pub replica_id: ReplicaId, - pub project: &'a Project, -} - -pub struct SharedProject {} - -pub struct UnsharedProject { - pub connection_ids: Vec, - pub host_user_id: UserId, -} - pub struct LeftProject { - pub connection_ids: Vec, pub host_user_id: UserId, + pub host_connection_id: ConnectionId, + pub connection_ids: Vec, + pub remove_collaborator: bool, + pub cancel_request: Option, + pub unshare: bool, } #[derive(Copy, Clone)] @@ -93,7 +78,7 @@ impl Store { let mut shared_projects = 0; for project in self.projects.values() { registered_projects += 1; - if project.share.is_some() { + if !project.guests.is_empty() { shared_projects += 1; } } @@ -112,6 +97,7 @@ impl Store { ConnectionState { user_id, projects: Default::default(), + requested_projects: Default::default(), channels: Default::default(), }, ); @@ -126,39 +112,40 @@ impl Store { &mut self, connection_id: ConnectionId, ) -> Result { - let connection = if let Some(connection) = self.connections.remove(&connection_id) { - connection - } else { - return Err(anyhow!("no such connection"))?; - }; + let connection = self + .connections + .get_mut(&connection_id) + .ok_or_else(|| anyhow!("no such connection"))?; - for channel_id in &connection.channels { - if let Some(channel) = self.channels.get_mut(&channel_id) { - channel.connection_ids.remove(&connection_id); - } - } + let user_id = connection.user_id; + let connection_projects = mem::take(&mut connection.projects); + let connection_channels = mem::take(&mut connection.channels); - let user_connections = self - .connections_by_user_id - .get_mut(&connection.user_id) - .unwrap(); - user_connections.remove(&connection_id); - if user_connections.is_empty() { - self.connections_by_user_id.remove(&connection.user_id); + let mut result = RemovedConnectionState::default(); + result.user_id = user_id; + + // Leave all channels. + for channel_id in connection_channels { + self.leave_channel(connection_id, channel_id); } - let mut result = RemovedConnectionState::default(); - result.user_id = connection.user_id; - for project_id in connection.projects.clone() { + // Unregister and leave all projects. + for project_id in connection_projects { if let Ok(project) = self.unregister_project(project_id, connection_id) { result.hosted_projects.insert(project_id, project); - } else if let Ok(project) = self.leave_project(connection_id, project_id) { - result - .guest_project_ids - .insert(project_id, project.connection_ids); + } else if self.leave_project(connection_id, project_id).is_ok() { + result.guest_project_ids.insert(project_id); } } + let user_connections = self.connections_by_user_id.get_mut(&user_id).unwrap(); + user_connections.remove(&connection_id); + if user_connections.is_empty() { + self.connections_by_user_id.remove(&user_id); + } + + self.connections.remove(&connection_id).unwrap(); + Ok(result) } @@ -275,18 +262,15 @@ impl Store { if project.host_user_id == user_id { metadata.push(proto::ProjectMetadata { id: project_id, - is_shared: project.share.is_some(), worktree_root_names: project .worktrees .values() .map(|worktree| worktree.root_name.clone()) .collect(), guests: project - .share - .iter() - .flat_map(|share| { - share.guests.values().map(|(_, user_id)| user_id.to_proto()) - }) + .guests + .values() + .map(|(_, user_id)| user_id.to_proto()) .collect(), }); } @@ -307,7 +291,9 @@ impl Store { Project { host_connection_id, host_user_id, - share: None, + guests: Default::default(), + join_requests: Default::default(), + active_replica_ids: Default::default(), worktrees: Default::default(), language_servers: Default::default(), }, @@ -332,10 +318,6 @@ impl Store { .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id { project.worktrees.insert(worktree_id, worktree); - if let Ok(share) = project.share_mut() { - share.worktrees.insert(worktree_id, Default::default()); - } - Ok(()) } else { Err(anyhow!("no such project"))? @@ -356,10 +338,22 @@ impl Store { host_connection.projects.remove(&project_id); } - if let Some(share) = &project.share { - for guest_connection in share.guests.keys() { - if let Some(connection) = self.connections.get_mut(&guest_connection) { - connection.projects.remove(&project_id); + for guest_connection in project.guests.keys() { + if let Some(connection) = self.connections.get_mut(&guest_connection) { + connection.projects.remove(&project_id); + } + } + + for requester_user_id in project.join_requests.keys() { + if let Some(requester_connection_ids) = + self.connections_by_user_id.get_mut(&requester_user_id) + { + for requester_connection_id in requester_connection_ids.iter() { + if let Some(requester_connection) = + self.connections.get_mut(requester_connection_id) + { + requester_connection.requested_projects.remove(&project_id); + } } } } @@ -391,64 +385,7 @@ impl Store { .worktrees .remove(&worktree_id) .ok_or_else(|| anyhow!("no such worktree"))?; - - let mut guest_connection_ids = Vec::new(); - if let Ok(share) = project.share_mut() { - guest_connection_ids.extend(share.guests.keys()); - share.worktrees.remove(&worktree_id); - } - - Ok((worktree, guest_connection_ids)) - } - - pub fn share_project( - &mut self, - project_id: u64, - connection_id: ConnectionId, - ) -> Result { - if let Some(project) = self.projects.get_mut(&project_id) { - if project.host_connection_id == connection_id { - let mut share = ProjectShare::default(); - for worktree_id in project.worktrees.keys() { - share.worktrees.insert(*worktree_id, Default::default()); - } - project.share = Some(share); - return Ok(SharedProject {}); - } - } - Err(anyhow!("no such project"))? - } - - pub fn unshare_project( - &mut self, - project_id: u64, - acting_connection_id: ConnectionId, - ) -> Result { - let project = if let Some(project) = self.projects.get_mut(&project_id) { - project - } else { - return Err(anyhow!("no such project"))?; - }; - - if project.host_connection_id != acting_connection_id { - return Err(anyhow!("not your project"))?; - } - - let connection_ids = project.connection_ids(); - if let Some(share) = project.share.take() { - for connection_id in share.guests.into_keys() { - if let Some(connection) = self.connections.get_mut(&connection_id) { - connection.projects.remove(&project_id); - } - } - - Ok(UnsharedProject { - connection_ids, - host_user_id: project.host_user_id, - }) - } else { - Err(anyhow!("project is not shared"))? - } + Ok((worktree, project.guest_connection_ids())) } pub fn update_diagnostic_summary( @@ -464,7 +401,6 @@ impl Store { .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id { let worktree = project - .share_mut()? .worktrees .get_mut(&worktree_id) .ok_or_else(|| anyhow!("no such worktree"))?; @@ -495,35 +431,77 @@ impl Store { Err(anyhow!("no such project"))? } - pub fn join_project( + pub fn request_join_project( &mut self, - connection_id: ConnectionId, - user_id: UserId, + requester_id: UserId, project_id: u64, - ) -> Result { + receipt: Receipt, + ) -> Result<()> { let connection = self .connections - .get_mut(&connection_id) + .get_mut(&receipt.sender_id) .ok_or_else(|| anyhow!("no such connection"))?; let project = self .projects .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; + connection.requested_projects.insert(project_id); + project + .join_requests + .entry(requester_id) + .or_default() + .push(receipt); + Ok(()) + } - let share = project.share_mut()?; - connection.projects.insert(project_id); + pub fn deny_join_project_request( + &mut self, + responder_connection_id: ConnectionId, + requester_id: UserId, + project_id: u64, + ) -> Option>> { + let project = self.projects.get_mut(&project_id)?; + if responder_connection_id != project.host_connection_id { + return None; + } - let mut replica_id = 1; - while share.active_replica_ids.contains(&replica_id) { - replica_id += 1; + let receipts = project.join_requests.remove(&requester_id)?; + for receipt in &receipts { + let requester_connection = self.connections.get_mut(&receipt.sender_id)?; + requester_connection.requested_projects.remove(&project_id); } - share.active_replica_ids.insert(replica_id); - share.guests.insert(connection_id, (replica_id, user_id)); + Some(receipts) + } - Ok(JoinedProject { - replica_id, - project: &self.projects[&project_id], - }) + pub fn accept_join_project_request( + &mut self, + responder_connection_id: ConnectionId, + requester_id: UserId, + project_id: u64, + ) -> Option<(Vec<(Receipt, ReplicaId)>, &Project)> { + let project = self.projects.get_mut(&project_id)?; + if responder_connection_id != project.host_connection_id { + return None; + } + + let receipts = project.join_requests.remove(&requester_id)?; + let mut receipts_with_replica_ids = Vec::new(); + for receipt in receipts { + let requester_connection = self.connections.get_mut(&receipt.sender_id)?; + requester_connection.requested_projects.remove(&project_id); + requester_connection.projects.insert(project_id); + let mut replica_id = 1; + while project.active_replica_ids.contains(&replica_id) { + replica_id += 1; + } + project.active_replica_ids.insert(replica_id); + project + .guests + .insert(receipt.sender_id, (replica_id, requester_id)); + receipts_with_replica_ids.push((receipt, replica_id)); + } + + Some((receipts_with_replica_ids, project)) } pub fn leave_project( @@ -531,27 +509,48 @@ impl Store { connection_id: ConnectionId, project_id: u64, ) -> Result { + let user_id = self.user_id_for_connection(connection_id)?; let project = self .projects .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; - let share = project - .share - .as_mut() - .ok_or_else(|| anyhow!("project is not shared"))?; - let (replica_id, _) = share - .guests - .remove(&connection_id) - .ok_or_else(|| anyhow!("cannot leave a project before joining it"))?; - share.active_replica_ids.remove(&replica_id); + + // If the connection leaving the project is a collaborator, remove it. + let remove_collaborator = + if let Some((replica_id, _)) = project.guests.remove(&connection_id) { + project.active_replica_ids.remove(&replica_id); + true + } else { + false + }; + + // If the connection leaving the project has a pending request, remove it. + // If that user has no other pending requests on other connections, indicate that the request should be cancelled. + let mut cancel_request = None; + if let Entry::Occupied(mut entry) = project.join_requests.entry(user_id) { + entry + .get_mut() + .retain(|receipt| receipt.sender_id != connection_id); + if entry.get().is_empty() { + entry.remove(); + cancel_request = Some(user_id); + } + } if let Some(connection) = self.connections.get_mut(&connection_id) { connection.projects.remove(&project_id); } + let connection_ids = project.connection_ids(); + let unshare = connection_ids.len() <= 1 && project.join_requests.is_empty(); + Ok(LeftProject { - connection_ids: project.connection_ids(), + host_connection_id: project.host_connection_id, host_user_id: project.host_user_id, + connection_ids, + cancel_request, + unshare, + remove_collaborator, }) } @@ -566,7 +565,6 @@ impl Store { ) -> Result> { let project = self.write_project(project_id, connection_id)?; let worktree = project - .share_mut()? .worktrees .get_mut(&worktree_id) .ok_or_else(|| anyhow!("no such worktree"))?; @@ -611,12 +609,7 @@ impl Store { .get(&project_id) .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id - || project - .share - .as_ref() - .ok_or_else(|| anyhow!("project is not shared"))? - .guests - .contains_key(&connection_id) + || project.guests.contains_key(&connection_id) { Ok(project) } else { @@ -634,12 +627,7 @@ impl Store { .get_mut(&project_id) .ok_or_else(|| anyhow!("no such project"))?; if project.host_connection_id == connection_id - || project - .share - .as_ref() - .ok_or_else(|| anyhow!("project is not shared"))? - .guests - .contains_key(&connection_id) + || project.guests.contains_key(&connection_id) { Ok(project) } else { @@ -653,28 +641,21 @@ impl Store { for project_id in &connection.projects { let project = &self.projects.get(&project_id).unwrap(); if project.host_connection_id != *connection_id { - assert!(project - .share - .as_ref() - .unwrap() - .guests - .contains_key(connection_id)); + assert!(project.guests.contains_key(connection_id)); } - if let Some(share) = project.share.as_ref() { - for (worktree_id, worktree) in share.worktrees.iter() { - let mut paths = HashMap::default(); - for entry in worktree.entries.values() { - let prev_entry = paths.insert(&entry.path, entry); - assert_eq!( - prev_entry, - None, - "worktree {:?}, duplicate path for entries {:?} and {:?}", - worktree_id, - prev_entry.unwrap(), - entry - ); - } + for (worktree_id, worktree) in project.worktrees.iter() { + let mut paths = HashMap::default(); + for entry in worktree.entries.values() { + let prev_entry = paths.insert(&entry.path, entry); + assert_eq!( + prev_entry, + None, + "worktree {:?}, duplicate path for entries {:?} and {:?}", + worktree_id, + prev_entry.unwrap(), + entry + ); } } } @@ -702,21 +683,19 @@ impl Store { let host_connection = self.connections.get(&project.host_connection_id).unwrap(); assert!(host_connection.projects.contains(project_id)); - if let Some(share) = &project.share { - for guest_connection_id in share.guests.keys() { - let guest_connection = self.connections.get(guest_connection_id).unwrap(); - assert!(guest_connection.projects.contains(project_id)); - } - assert_eq!(share.active_replica_ids.len(), share.guests.len(),); - assert_eq!( - share.active_replica_ids, - share - .guests - .values() - .map(|(replica_id, _)| *replica_id) - .collect::>(), - ); + for guest_connection_id in project.guests.keys() { + let guest_connection = self.connections.get(guest_connection_id).unwrap(); + assert!(guest_connection.projects.contains(project_id)); } + assert_eq!(project.active_replica_ids.len(), project.guests.len(),); + assert_eq!( + project.active_replica_ids, + project + .guests + .values() + .map(|(replica_id, _)| *replica_id) + .collect::>(), + ); } for (channel_id, channel) in &self.channels { @@ -730,38 +709,15 @@ impl Store { impl Project { pub fn guest_connection_ids(&self) -> Vec { - if let Some(share) = &self.share { - share.guests.keys().copied().collect() - } else { - Vec::new() - } + self.guests.keys().copied().collect() } pub fn connection_ids(&self) -> Vec { - if let Some(share) = &self.share { - share - .guests - .keys() - .copied() - .chain(Some(self.host_connection_id)) - .collect() - } else { - vec![self.host_connection_id] - } - } - - pub fn share(&self) -> Result<&ProjectShare> { - Ok(self - .share - .as_ref() - .ok_or_else(|| anyhow!("worktree is not shared"))?) - } - - fn share_mut(&mut self) -> Result<&mut ProjectShare> { - Ok(self - .share - .as_mut() - .ok_or_else(|| anyhow!("worktree is not shared"))?) + self.guests + .keys() + .copied() + .chain(Some(self.host_connection_id)) + .collect() } } diff --git a/crates/contacts_panel/Cargo.toml b/crates/contacts_panel/Cargo.toml index de49f070b94338cd64e5cf29056fd7c1bb2088ec..b6d7bf63fc67897dac2c1fc34281fbed4df04d73 100644 --- a/crates/contacts_panel/Cargo.toml +++ b/crates/contacts_panel/Cargo.toml @@ -13,6 +13,7 @@ editor = { path = "../editor" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } picker = { path = "../picker" } +project = { path = "../project" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } diff --git a/crates/contacts_panel/src/contact_notification.rs b/crates/contacts_panel/src/contact_notification.rs index 6369f70ce0244fdd0a1a8540d4a91a0d86c0acbc..2d408da0c2d9a4e16bdb0f3afe1f53436968f862 100644 --- a/crates/contacts_panel/src/contact_notification.rs +++ b/crates/contacts_panel/src/contact_notification.rs @@ -1,13 +1,11 @@ +use crate::notifications::render_user_notification; use client::{ContactEvent, ContactEventKind, UserStore}; use gpui::{ - elements::*, impl_internal_actions, platform::CursorStyle, Entity, ModelHandle, - MutableAppContext, RenderContext, View, ViewContext, + elements::*, impl_internal_actions, Entity, ModelHandle, MutableAppContext, RenderContext, + View, ViewContext, }; -use settings::Settings; use workspace::Notification; -use crate::render_icon_button; - impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]); pub fn init(cx: &mut MutableAppContext) { @@ -33,9 +31,6 @@ pub enum Event { Dismiss, } -enum Decline {} -enum Accept {} - impl Entity for ContactNotification { type Event = Event; } @@ -47,8 +42,40 @@ impl View for ContactNotification { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { match self.event.kind { - ContactEventKind::Requested => self.render_incoming_request(cx), - ContactEventKind::Accepted => self.render_acceptance(cx), + ContactEventKind::Requested => render_user_notification( + self.event.user.clone(), + "wants to add you as a contact", + Some("They won't know if you decline."), + RespondToContactRequest { + user_id: self.event.user.id, + accept: false, + }, + vec![ + ( + "Decline", + Box::new(RespondToContactRequest { + user_id: self.event.user.id, + accept: false, + }), + ), + ( + "Accept", + Box::new(RespondToContactRequest { + user_id: self.event.user.id, + accept: true, + }), + ), + ], + cx, + ), + ContactEventKind::Accepted => render_user_notification( + self.event.user.clone(), + "accepted your contact request", + None, + Dismiss(self.event.user.id), + vec![], + cx, + ), _ => unreachable!(), } } @@ -82,138 +109,6 @@ impl ContactNotification { Self { event, user_store } } - fn render_incoming_request(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.global::().theme.clone(); - let theme = &theme.contact_notification; - let user = &self.event.user; - let user_id = user.id; - - Flex::column() - .with_child(self.render_header("wants to add you as a contact.", theme, cx)) - .with_child( - Label::new( - "They won't know if you decline.".to_string(), - theme.body_message.text.clone(), - ) - .contained() - .with_style(theme.body_message.container) - .boxed(), - ) - .with_child( - Flex::row() - .with_child( - MouseEventHandler::new::( - self.event.user.id as usize, - cx, - |state, _| { - let button = theme.button.style_for(state, false); - Label::new("Decline".to_string(), button.text.clone()) - .contained() - .with_style(button.container) - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: false, - }); - }) - .boxed(), - ) - .with_child( - MouseEventHandler::new::(user.id as usize, cx, |state, _| { - let button = theme.button.style_for(state, false); - Label::new("Accept".to_string(), button.text.clone()) - .contained() - .with_style(button.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: true, - }); - }) - .boxed(), - ) - .aligned() - .right() - .boxed(), - ) - .contained() - .boxed() - } - - fn render_acceptance(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.global::().theme.clone(); - let theme = &theme.contact_notification; - - self.render_header("accepted your contact request", theme, cx) - } - - fn render_header( - &self, - message: &'static str, - theme: &theme::ContactNotification, - cx: &mut RenderContext, - ) -> ElementBox { - let user = &self.event.user; - let user_id = user.id; - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.header_avatar) - .aligned() - .constrained() - .with_height( - cx.font_cache() - .line_height(theme.header_message.text.font_size), - ) - .aligned() - .top() - .boxed() - })) - .with_child( - Text::new( - format!("{} {}", user.github_login, message), - theme.header_message.text.clone(), - ) - .contained() - .with_style(theme.header_message.container) - .aligned() - .top() - .left() - .flex(1., true) - .boxed(), - ) - .with_child( - MouseEventHandler::new::(user.id as usize, cx, |state, _| { - render_icon_button( - theme.dismiss_button.style_for(state, false), - "icons/decline.svg", - ) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_padding(Padding::uniform(5.)) - .on_click(move |_, cx| cx.dispatch_action(Dismiss(user_id))) - .aligned() - .constrained() - .with_height( - cx.font_cache() - .line_height(theme.header_message.text.font_size), - ) - .aligned() - .top() - .flex_float() - .boxed(), - ) - .named("contact notification header") - } - fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { self.user_store.update(cx, |store, cx| { store diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 03968d5aaaf740552423b4331026a38150d233f1..df6d85712bc0088caa974f97ceda2d2d592298d9 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1,5 +1,7 @@ mod contact_finder; mod contact_notification; +mod join_project_notification; +mod notifications; use client::{Contact, ContactEventKind, User, UserStore}; use contact_notification::ContactNotification; @@ -13,6 +15,7 @@ use gpui::{ AppContext, Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; +use join_project_notification::JoinProjectNotification; use serde::Deserialize; use settings::Settings; use std::sync::Arc; @@ -76,6 +79,7 @@ pub struct RespondToContactRequest { pub fn init(cx: &mut MutableAppContext) { contact_finder::init(cx); contact_notification::init(cx); + join_project_notification::init(cx); cx.add_action(ContactsPanel::request_contact); cx.add_action(ContactsPanel::remove_contact); cx.add_action(ContactsPanel::respond_to_contact_request); @@ -118,6 +122,37 @@ impl ContactsPanel { }) .detach(); + cx.defer({ + let workspace = workspace.clone(); + move |_, cx| { + if let Some(workspace_handle) = workspace.upgrade(cx) { + cx.subscribe(&workspace_handle.read(cx).project().clone(), { + let workspace = workspace.clone(); + move |_, project, event, cx| match event { + project::Event::ContactRequestedJoin(user) => { + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.show_notification( + cx.add_view(|cx| { + JoinProjectNotification::new( + project, + user.clone(), + cx, + ) + }), + cx, + ) + }); + } + } + _ => {} + } + }) + .detach(); + } + } + }); + cx.subscribe(&app_state.user_store, { let user_store = app_state.user_store.downgrade(); move |_, _, event, cx| { @@ -305,22 +340,16 @@ impl ContactsPanel { fn render_contact_project( contact: Arc, current_user_id: Option, - project_ix: usize, + project_index: usize, app_state: Arc, theme: &theme::ContactsPanel, is_last_project: bool, is_selected: bool, cx: &mut LayoutContext, ) -> ElementBox { - let project = &contact.projects[project_ix]; + let project = &contact.projects[project_index]; let project_id = project.id; let is_host = Some(contact.user.id) == current_user_id; - let is_guest = !is_host - && project - .guests - .iter() - .any(|guest| Some(guest.id) == current_user_id); - let is_shared = project.is_shared; let font_cache = cx.font_cache(); let host_avatar_height = theme @@ -328,7 +357,7 @@ impl ContactsPanel { .width .or(theme.contact_avatar.height) .unwrap_or(0.); - let row = &theme.unshared_project_row.default; + let row = &theme.project_row.default; let tree_branch = theme.tree_branch.clone(); let line_height = row.name.text.line_height(font_cache); let cap_height = row.name.text.cap_height(font_cache); @@ -337,12 +366,7 @@ impl ContactsPanel { MouseEventHandler::new::(project_id as usize, cx, |mouse_state, _| { let tree_branch = *tree_branch.style_for(mouse_state, is_selected); - let row = if project.is_shared { - &theme.shared_project_row - } else { - &theme.unshared_project_row - } - .style_for(mouse_state, is_selected); + let row = theme.project_row.style_for(mouse_state, is_selected); Flex::row() .with_child( @@ -412,15 +436,16 @@ impl ContactsPanel { .with_style(row.container) .boxed() }) - .with_cursor_style(if !is_host && is_shared { + .with_cursor_style(if !is_host { CursorStyle::PointingHand } else { CursorStyle::Arrow }) .on_click(move |_, cx| { - if !is_host && !is_guest { + if !is_host { cx.dispatch_global_action(JoinProject { - project_id, + contact: contact.clone(), + project_index, app_state: app_state.clone(), }); } @@ -743,12 +768,12 @@ impl ContactsPanel { let section = *section; self.toggle_expanded(&ToggleExpanded(section), cx); } - ContactEntry::ContactProject(contact, project_ix) => { - cx.dispatch_global_action(JoinProject { - project_id: contact.projects[*project_ix].id, + ContactEntry::ContactProject(contact, project_index) => cx + .dispatch_global_action(JoinProject { + contact: contact.clone(), + project_index: *project_index, app_state: self.app_state.clone(), - }) - } + }), _ => {} } } @@ -947,7 +972,6 @@ mod tests { projects: vec![proto::ProjectMetadata { id: 101, worktree_root_names: vec!["dir1".to_string()], - is_shared: true, guests: vec![2], }], }, @@ -958,7 +982,6 @@ mod tests { projects: vec![proto::ProjectMetadata { id: 102, worktree_root_names: vec!["dir2".to_string()], - is_shared: true, guests: vec![2], }], }, diff --git a/crates/contacts_panel/src/join_project_notification.rs b/crates/contacts_panel/src/join_project_notification.rs new file mode 100644 index 0000000000000000000000000000000000000000..d8e8e670cff02da81a5b6c4afc6754d88661f2f2 --- /dev/null +++ b/crates/contacts_panel/src/join_project_notification.rs @@ -0,0 +1,80 @@ +use client::User; +use gpui::{ + actions, ElementBox, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext, +}; +use project::Project; +use std::sync::Arc; +use workspace::Notification; + +use crate::notifications::render_user_notification; + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(JoinProjectNotification::decline); + cx.add_action(JoinProjectNotification::accept); +} + +pub enum Event { + Dismiss, +} + +actions!(contacts_panel, [Accept, Decline]); + +pub struct JoinProjectNotification { + project: ModelHandle, + user: Arc, +} + +impl JoinProjectNotification { + pub fn new(project: ModelHandle, user: Arc, cx: &mut ViewContext) -> Self { + cx.subscribe(&project, |this, _, event, cx| { + if let project::Event::ContactCancelledJoinRequest(user) = event { + if *user == this.user { + cx.emit(Event::Dismiss); + } + } + }) + .detach(); + Self { project, user } + } + + fn decline(&mut self, _: &Decline, cx: &mut ViewContext) { + self.project.update(cx, |project, cx| { + project.respond_to_join_request(self.user.id, false, cx) + }); + cx.emit(Event::Dismiss) + } + + fn accept(&mut self, _: &Accept, cx: &mut ViewContext) { + self.project.update(cx, |project, cx| { + project.respond_to_join_request(self.user.id, true, cx) + }); + cx.emit(Event::Dismiss) + } +} + +impl Entity for JoinProjectNotification { + type Event = Event; +} + +impl View for JoinProjectNotification { + fn ui_name() -> &'static str { + "JoinProjectNotification" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + render_user_notification( + self.user.clone(), + "wants to join your project", + None, + Decline, + vec![("Decline", Box::new(Decline)), ("Accept", Box::new(Accept))], + cx, + ) + } +} + +impl Notification for JoinProjectNotification { + fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { + matches!(event, Event::Dismiss) + } +} diff --git a/crates/contacts_panel/src/notifications.rs b/crates/contacts_panel/src/notifications.rs new file mode 100644 index 0000000000000000000000000000000000000000..555d8962d3d8003eeaa11ad078625f51844b6dbb --- /dev/null +++ b/crates/contacts_panel/src/notifications.rs @@ -0,0 +1,113 @@ +use crate::render_icon_button; +use client::User; +use gpui::{ + elements::{Flex, Image, Label, MouseEventHandler, Padding, ParentElement, Text}, + platform::CursorStyle, + Action, Element, ElementBox, RenderContext, View, +}; +use settings::Settings; +use std::sync::Arc; + +enum Dismiss {} +enum Button {} + +pub fn render_user_notification( + user: Arc, + title: &str, + body: Option<&str>, + dismiss_action: A, + buttons: Vec<(&'static str, Box)>, + cx: &mut RenderContext, +) -> ElementBox { + let theme = cx.global::().theme.clone(); + let theme = &theme.contact_notification; + + Flex::column() + .with_child( + Flex::row() + .with_children(user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.header_avatar) + .aligned() + .constrained() + .with_height( + cx.font_cache() + .line_height(theme.header_message.text.font_size), + ) + .aligned() + .top() + .boxed() + })) + .with_child( + Text::new( + format!("{} {}", user.github_login, title), + theme.header_message.text.clone(), + ) + .contained() + .with_style(theme.header_message.container) + .aligned() + .top() + .left() + .flex(1., true) + .boxed(), + ) + .with_child( + MouseEventHandler::new::(user.id as usize, cx, |state, _| { + render_icon_button( + theme.dismiss_button.style_for(state, false), + "icons/decline.svg", + ) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_padding(Padding::uniform(5.)) + .on_click(move |_, cx| cx.dispatch_any_action(dismiss_action.boxed_clone())) + .aligned() + .constrained() + .with_height( + cx.font_cache() + .line_height(theme.header_message.text.font_size), + ) + .aligned() + .top() + .flex_float() + .boxed(), + ) + .named("contact notification header"), + ) + .with_children(body.map(|body| { + Label::new( + body.to_string(), + theme.body_message.text.clone(), + ) + .contained() + .with_style(theme.body_message.container) + .boxed() + })) + .with_children(if buttons.is_empty() { + None + } else { + Some( + Flex::row() + .with_children(buttons.into_iter().enumerate().map( + |(ix, (message, action))| { + MouseEventHandler::new::(ix, cx, |state, _| { + let button = theme.button.style_for(state, false); + Label::new(message.to_string(), button.text.clone()) + .contained() + .with_style(button.container) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, cx| cx.dispatch_any_action(action.boxed_clone())) + .boxed() + }, + )) + .aligned() + .right() + .boxed(), + ) + }) + .contained() + .boxed() +} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 4fc5dc564cc0e4c1cc3aaa117a7d7485922c9df5..826592daa0bf9fb615640071ca3ac8b3da5ea055 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1611,6 +1611,20 @@ impl MutableAppContext { }) } + pub fn replace_root_view(&mut self, window_id: usize, build_root_view: F) -> ViewHandle + where + T: View, + F: FnOnce(&mut ViewContext) -> T, + { + self.update(|this| { + let root_view = this.add_view(window_id, build_root_view); + let window = this.cx.windows.get_mut(&window_id).unwrap(); + window.root_view = root_view.clone().into(); + window.focused_view_id = Some(root_view.id()); + root_view + }) + } + pub fn remove_window(&mut self, window_id: usize) { self.cx.windows.remove(&window_id); self.presenters_and_platform_windows.remove(&window_id); @@ -1628,20 +1642,22 @@ impl MutableAppContext { { let mut app = self.upgrade(); - let presenter = presenter.clone(); + let presenter = Rc::downgrade(&presenter); window.on_event(Box::new(move |event| { app.update(|cx| { - if let Event::KeyDown { keystroke, .. } = &event { - if cx.dispatch_keystroke( - window_id, - presenter.borrow().dispatch_path(cx.as_ref()), - keystroke, - ) { - return; + if let Some(presenter) = presenter.upgrade() { + if let Event::KeyDown { keystroke, .. } = &event { + if cx.dispatch_keystroke( + window_id, + presenter.borrow().dispatch_path(cx.as_ref()), + keystroke, + ) { + return; + } } - } - presenter.borrow_mut().dispatch_event(event, cx); + presenter.borrow_mut().dispatch_event(event, cx); + } }) })); } @@ -3224,6 +3240,21 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.add_option_view(self.window_id, build_view) } + pub fn replace_root_view(&mut self, build_root_view: F) -> ViewHandle + where + V: View, + F: FnOnce(&mut ViewContext) -> V, + { + let window_id = self.window_id; + self.update(|this| { + let root_view = this.add_view(window_id, build_root_view); + let window = this.cx.windows.get_mut(&window_id).unwrap(); + window.root_view = root_view.clone().into(); + window.focused_view_id = Some(root_view.id()); + root_view + }) + } + pub fn subscribe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 451f14567a1acc5e1fb69683cc8dc20c02e4624b..fbdd6963e372f711d193c9bde16b34156d6118d5 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -388,13 +388,17 @@ impl<'a> EventContext<'a> { } } - pub fn dispatch_action(&mut self, action: A) { + pub fn dispatch_any_action(&mut self, action: Box) { self.dispatched_actions.push(DispatchDirective { path: self.view_stack.clone(), - action: Box::new(action), + action, }); } + pub fn dispatch_action(&mut self, action: A) { + self.dispatch_any_action(Box::new(action)); + } + pub fn notify(&mut self) { self.notify_count += 1; if let Some(view_id) = self.view_stack.last() { diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index eaae45bcc62d31f68adde4a8492c9d4f2e9a9f7b..7a3c7c5367f1137962e3bf0262b7b5eacfac4193 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -45,6 +45,7 @@ serde_json = { version = "1.0.64", features = ["preserve_order"] } sha2 = "0.10" similar = "1.3" smol = "1.2.5" +thiserror = "1.0.29" toml = "0.5" [dev-dependencies] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d23122f45b337d5bc7152fef6cbec9abd3e4623e..e6eb2dcf771fd4f8ff8924f60145834e6b2a768c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -49,6 +49,7 @@ use std::{ }, time::Instant, }; +use thiserror::Error; use util::{post_inc, ResultExt, TryFutureExt as _}; pub use fs::*; @@ -90,6 +91,18 @@ pub struct Project { nonce: u128, } +#[derive(Error, Debug)] +pub enum JoinProjectError { + #[error("host declined join request")] + HostDeclined, + #[error("host closed the project")] + HostClosedProject, + #[error("host went offline")] + HostWentOffline, + #[error("{0}")] + Other(#[from] anyhow::Error), +} + enum OpenBuffer { Strong(ModelHandle), Weak(WeakModelHandle), @@ -123,7 +136,7 @@ pub struct Collaborator { pub replica_id: ReplicaId, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { ActiveEntryChanged(Option), WorktreeRemoved(WorktreeId), @@ -133,6 +146,8 @@ pub enum Event { DiagnosticsUpdated(ProjectPath), RemoteIdChanged(Option), CollaboratorLeft(PeerId), + ContactRequestedJoin(Arc), + ContactCancelledJoinRequest(Arc), } #[derive(Serialize)] @@ -248,15 +263,18 @@ impl ProjectEntryId { impl Project { pub fn init(client: &Arc) { + client.add_model_message_handler(Self::handle_request_join_project); client.add_model_message_handler(Self::handle_add_collaborator); client.add_model_message_handler(Self::handle_buffer_reloaded); client.add_model_message_handler(Self::handle_buffer_saved); client.add_model_message_handler(Self::handle_start_language_server); client.add_model_message_handler(Self::handle_update_language_server); client.add_model_message_handler(Self::handle_remove_collaborator); + client.add_model_message_handler(Self::handle_join_project_request_cancelled); client.add_model_message_handler(Self::handle_register_worktree); client.add_model_message_handler(Self::handle_unregister_worktree); - client.add_model_message_handler(Self::handle_unshare_project); + client.add_model_message_handler(Self::handle_unregister_project); + client.add_model_message_handler(Self::handle_project_unshared); client.add_model_message_handler(Self::handle_update_buffer_file); client.add_model_message_handler(Self::handle_update_buffer); client.add_model_message_handler(Self::handle_update_diagnostic_summary); @@ -353,7 +371,7 @@ impl Project { languages: Arc, fs: Arc, cx: &mut AsyncAppContext, - ) -> Result> { + ) -> Result, JoinProjectError> { client.authenticate_and_connect(true, &cx).await?; let response = client @@ -362,6 +380,24 @@ impl Project { }) .await?; + let response = match response.variant.ok_or_else(|| anyhow!("missing variant"))? { + proto::join_project_response::Variant::Accept(response) => response, + proto::join_project_response::Variant::Decline(decline) => { + match proto::join_project_response::decline::Reason::from_i32(decline.reason) { + Some(proto::join_project_response::decline::Reason::Declined) => { + Err(JoinProjectError::HostDeclined)? + } + Some(proto::join_project_response::decline::Reason::Closed) => { + Err(JoinProjectError::HostClosedProject)? + } + Some(proto::join_project_response::decline::Reason::WentOffline) => { + Err(JoinProjectError::HostWentOffline)? + } + None => Err(anyhow!("missing decline reason"))?, + } + } + }; + let replica_id = response.replica_id as ReplicaId; let mut worktrees = Vec::new(); @@ -400,7 +436,7 @@ impl Project { // Even if we're initially connected, any future change of the status means we momentarily disconnected. if !is_connected || status.next().await.is_some() { if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| this.project_unshared(cx)) + this.update(&mut cx, |this, cx| this.removed_from_project(cx)) } } Ok(()) @@ -550,7 +586,7 @@ impl Project { } fn unregister(&mut self, cx: &mut ModelContext) { - self.unshare(cx); + self.unshared(cx); for worktree in &self.worktrees { if let Some(worktree) = worktree.upgrade(cx) { worktree.update(cx, |worktree, _| { @@ -810,64 +846,59 @@ impl Project { } } - pub fn can_share(&self, cx: &AppContext) -> bool { - self.is_local() && self.visible_worktrees(cx).next().is_some() - } + fn share(&mut self, cx: &mut ModelContext) -> Task> { + let project_id; + if let ProjectClientState::Local { + remote_id_rx, + is_shared, + .. + } = &mut self.client_state + { + if *is_shared { + return Task::ready(Ok(())); + } + *is_shared = true; + if let Some(id) = *remote_id_rx.borrow() { + project_id = id; + } else { + return Task::ready(Err(anyhow!("project hasn't been registered"))); + } + } else { + return Task::ready(Err(anyhow!("can't share a remote project"))); + }; - pub fn share(&self, cx: &mut ModelContext) -> Task> { - let rpc = self.client.clone(); - cx.spawn(|this, mut cx| async move { - let project_id = this.update(&mut cx, |this, cx| { - if let ProjectClientState::Local { - is_shared, - remote_id_rx, - .. - } = &mut this.client_state - { - *is_shared = true; - - for open_buffer in this.opened_buffers.values_mut() { - match open_buffer { - OpenBuffer::Strong(_) => {} - OpenBuffer::Weak(buffer) => { - if let Some(buffer) = buffer.upgrade(cx) { - *open_buffer = OpenBuffer::Strong(buffer); - } - } - OpenBuffer::Loading(_) => unreachable!(), - } + for open_buffer in self.opened_buffers.values_mut() { + match open_buffer { + OpenBuffer::Strong(_) => {} + OpenBuffer::Weak(buffer) => { + if let Some(buffer) = buffer.upgrade(cx) { + *open_buffer = OpenBuffer::Strong(buffer); } + } + OpenBuffer::Loading(_) => unreachable!(), + } + } - for worktree_handle in this.worktrees.iter_mut() { - match worktree_handle { - WorktreeHandle::Strong(_) => {} - WorktreeHandle::Weak(worktree) => { - if let Some(worktree) = worktree.upgrade(cx) { - *worktree_handle = WorktreeHandle::Strong(worktree); - } - } - } + for worktree_handle in self.worktrees.iter_mut() { + match worktree_handle { + WorktreeHandle::Strong(_) => {} + WorktreeHandle::Weak(worktree) => { + if let Some(worktree) = worktree.upgrade(cx) { + *worktree_handle = WorktreeHandle::Strong(worktree); } - - remote_id_rx - .borrow() - .ok_or_else(|| anyhow!("no project id")) - } else { - Err(anyhow!("can't share a remote project")) } - })?; - - rpc.request(proto::ShareProject { project_id }).await?; + } + } - let mut tasks = Vec::new(); - this.update(&mut cx, |this, cx| { - for worktree in this.worktrees(cx).collect::>() { - worktree.update(cx, |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - tasks.push(worktree.share(project_id, cx)); - }); - } + let mut tasks = Vec::new(); + for worktree in self.worktrees(cx).collect::>() { + worktree.update(cx, |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + tasks.push(worktree.share(project_id, cx)); }); + } + + cx.spawn(|this, mut cx| async move { for task in tasks { task.await?; } @@ -876,15 +907,8 @@ impl Project { }) } - pub fn unshare(&mut self, cx: &mut ModelContext) { - let rpc = self.client.clone(); - - if let ProjectClientState::Local { - is_shared, - remote_id_rx, - .. - } = &mut self.client_state - { + fn unshared(&mut self, cx: &mut ModelContext) { + if let ProjectClientState::Local { is_shared, .. } = &mut self.client_state { if !*is_shared { return; } @@ -913,17 +937,35 @@ impl Project { } } - if let Some(project_id) = *remote_id_rx.borrow() { - rpc.send(proto::UnshareProject { project_id }).log_err(); - } - cx.notify(); } else { log::error!("attempted to unshare a remote project"); } } - fn project_unshared(&mut self, cx: &mut ModelContext) { + pub fn respond_to_join_request( + &mut self, + requester_id: u64, + allow: bool, + cx: &mut ModelContext, + ) { + if let Some(project_id) = self.remote_id() { + let share = self.share(cx); + let client = self.client.clone(); + cx.foreground() + .spawn(async move { + share.await?; + client.send(proto::RespondToJoinProjectRequest { + requester_id, + project_id, + allow, + }) + }) + .detach_and_log_err(cx); + } + } + + fn removed_from_project(&mut self, cx: &mut ModelContext) { if let ProjectClientState::Remote { sharing_has_stopped, .. @@ -3504,25 +3546,36 @@ impl Project { }); let worktree = worktree?; - let (remote_project_id, is_shared) = - project.update(&mut cx, |project, cx| { - project.add_worktree(&worktree, cx); - (project.remote_id(), project.is_shared()) - }); + let remote_project_id = project.update(&mut cx, |project, cx| { + project.add_worktree(&worktree, cx); + project.remote_id() + }); if let Some(project_id) = remote_project_id { - if is_shared { - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_local_mut().unwrap().share(project_id, cx) - }) - .await?; - } else { - worktree - .update(&mut cx, |worktree, cx| { - worktree.as_local_mut().unwrap().register(project_id, cx) - }) - .await?; + // Because sharing is async, we may have *unshared* the project by the time it completes, + // in which case we need to register the worktree instead. + loop { + if project.read_with(&cx, |project, _| project.is_shared()) { + if worktree + .update(&mut cx, |worktree, cx| { + worktree.as_local_mut().unwrap().share(project_id, cx) + }) + .await + .is_ok() + { + break; + } + } else { + worktree + .update(&mut cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .register(project_id, cx) + }) + .await?; + break; + } } } @@ -3745,13 +3798,46 @@ impl Project { // RPC message handlers - async fn handle_unshare_project( + async fn handle_request_join_project( this: ModelHandle, - _: TypedEnvelope, + message: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| this.project_unshared(cx)); + let user_id = message.payload.requester_id; + if this.read_with(&cx, |project, _| { + project.collaborators.values().any(|c| c.user.id == user_id) + }) { + this.update(&mut cx, |this, cx| { + this.respond_to_join_request(user_id, true, cx) + }); + } else { + let user_store = this.read_with(&cx, |this, _| this.user_store.clone()); + let user = user_store + .update(&mut cx, |store, cx| store.fetch_user(user_id, cx)) + .await?; + this.update(&mut cx, |_, cx| cx.emit(Event::ContactRequestedJoin(user))); + } + Ok(()) + } + + async fn handle_unregister_project( + this: ModelHandle, + _: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| this.removed_from_project(cx)); + Ok(()) + } + + async fn handle_project_unshared( + this: ModelHandle, + _: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| this.unshared(cx)); Ok(()) } @@ -3796,12 +3882,34 @@ impl Project { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); } } + cx.emit(Event::CollaboratorLeft(peer_id)); cx.notify(); Ok(()) }) } + async fn handle_join_project_request_cancelled( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let user = this + .update(&mut cx, |this, cx| { + this.user_store.update(cx, |user_store, cx| { + user_store.fetch_user(envelope.payload.requester_id, cx) + }) + }) + .await?; + + this.update(&mut cx, |_, cx| { + cx.emit(Event::ContactCancelledJoinRequest(user)); + }); + + Ok(()) + } + async fn handle_register_worktree( this: ModelHandle, envelope: TypedEnvelope, @@ -4052,6 +4160,7 @@ impl Project { .into_iter() .map(|op| language::proto::deserialize_operation(op)) .collect::, _>>()?; + let is_remote = this.is_remote(); match this.opened_buffers.entry(buffer_id) { hash_map::Entry::Occupied(mut e) => match e.get_mut() { OpenBuffer::Strong(buffer) => { @@ -4061,6 +4170,11 @@ impl Project { OpenBuffer::Weak(_) => {} }, hash_map::Entry::Vacant(e) => { + assert!( + is_remote, + "received buffer update from {:?}", + envelope.original_sender_id + ); e.insert(OpenBuffer::Loading(ops)); } } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 84fedbbde7facc527500c5137a8bdf2a8bf6bbe0..1c77c3add7ca127bc3dfcde330787ac0ac38ce74 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -616,8 +616,10 @@ impl LocalWorktree { let text = fs.load(&abs_path).await?; // Eagerly populate the snapshot with an updated entry for the loaded file let entry = this - .update(&mut cx, |this, _| { - this.as_local().unwrap().refresh_entry(path, abs_path, None) + .update(&mut cx, |this, cx| { + this.as_local() + .unwrap() + .refresh_entry(path, abs_path, None, cx) }) .await?; this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); @@ -753,11 +755,12 @@ impl LocalWorktree { Some(cx.spawn(|this, mut cx| async move { rename.await?; let entry = this - .update(&mut cx, |this, _| { + .update(&mut cx, |this, cx| { this.as_local_mut().unwrap().refresh_entry( new_path.clone(), abs_new_path, Some(old_path), + cx, ) }) .await?; @@ -793,10 +796,10 @@ impl LocalWorktree { cx.spawn(|this, mut cx| async move { write.await?; let entry = this - .update(&mut cx, |this, _| { + .update(&mut cx, |this, cx| { this.as_local_mut() .unwrap() - .refresh_entry(path, abs_path, None) + .refresh_entry(path, abs_path, None, cx) }) .await?; this.update(&mut cx, |this, cx| { @@ -813,18 +816,17 @@ impl LocalWorktree { path: Arc, abs_path: PathBuf, old_path: Option>, - ) -> impl Future> { + cx: &mut ModelContext, + ) -> Task> { + let fs = self.fs.clone(); let root_char_bag; let next_entry_id; - let fs = self.fs.clone(); - let shared_snapshots_tx = self.share.as_ref().map(|share| share.snapshots_tx.clone()); - let snapshot = self.background_snapshot.clone(); { - let snapshot = snapshot.lock(); + let snapshot = self.background_snapshot.lock(); root_char_bag = snapshot.root_char_bag; next_entry_id = snapshot.next_entry_id.clone(); } - async move { + cx.spawn_weak(|this, mut cx| async move { let entry = Entry::new( path, &fs.metadata(&abs_path) @@ -833,16 +835,29 @@ impl LocalWorktree { &next_entry_id, root_char_bag, ); - let mut snapshot = snapshot.lock(); - if let Some(old_path) = old_path { - snapshot.remove_path(&old_path); - } - let entry = snapshot.insert_entry(entry, fs.as_ref()); - if let Some(tx) = shared_snapshots_tx { - tx.send(snapshot.clone()).await.ok(); + + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("worktree was dropped"))?; + let (entry, snapshot, snapshots_tx) = this.read_with(&cx, |this, _| { + let this = this.as_local().unwrap(); + let mut snapshot = this.background_snapshot.lock(); + if let Some(old_path) = old_path { + snapshot.remove_path(&old_path); + } + let entry = snapshot.insert_entry(entry, fs.as_ref()); + snapshot.scan_id += 1; + let snapshots_tx = this.share.as_ref().map(|s| s.snapshots_tx.clone()); + (entry, snapshot.clone(), snapshots_tx) + }); + this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); + + if let Some(snapshots_tx) = snapshots_tx { + snapshots_tx.send(snapshot).await.ok(); } + Ok(entry) - } + }) } pub fn register( @@ -2171,8 +2186,7 @@ impl BackgroundScanner { let root_abs_path; let next_entry_id; { - let mut snapshot = self.snapshot.lock(); - snapshot.scan_id += 1; + let snapshot = self.snapshot.lock(); root_char_bag = snapshot.root_char_bag; root_abs_path = snapshot.abs_path.clone(); next_entry_id = snapshot.next_entry_id.clone(); @@ -2197,6 +2211,7 @@ impl BackgroundScanner { let (scan_queue_tx, scan_queue_rx) = channel::unbounded(); { let mut snapshot = self.snapshot.lock(); + snapshot.scan_id += 1; for event in &events { if let Ok(path) = event.path.strip_prefix(&root_abs_path) { snapshot.remove_path(&path); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 12ff05c7575ec53b1102ab253d4a9db08790b69a..d2ab7d99ac17e4a8fe36b922e714a734cb549e7f 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -14,89 +14,91 @@ message Envelope { RegisterProject register_project = 8; RegisterProjectResponse register_project_response = 9; UnregisterProject unregister_project = 10; - ShareProject share_project = 11; - UnshareProject unshare_project = 12; - JoinProject join_project = 13; - JoinProjectResponse join_project_response = 14; - LeaveProject leave_project = 15; - AddProjectCollaborator add_project_collaborator = 16; - RemoveProjectCollaborator remove_project_collaborator = 17; - - GetDefinition get_definition = 18; - GetDefinitionResponse get_definition_response = 19; - GetReferences get_references = 20; - GetReferencesResponse get_references_response = 21; - GetDocumentHighlights get_document_highlights = 22; - GetDocumentHighlightsResponse get_document_highlights_response = 23; - GetProjectSymbols get_project_symbols = 24; - GetProjectSymbolsResponse get_project_symbols_response = 25; - OpenBufferForSymbol open_buffer_for_symbol = 26; - OpenBufferForSymbolResponse open_buffer_for_symbol_response = 27; - - RegisterWorktree register_worktree = 28; - UnregisterWorktree unregister_worktree = 29; - UpdateWorktree update_worktree = 31; - - CreateProjectEntry create_project_entry = 32; - RenameProjectEntry rename_project_entry = 33; - DeleteProjectEntry delete_project_entry = 34; - ProjectEntryResponse project_entry_response = 35; - - UpdateDiagnosticSummary update_diagnostic_summary = 36; - StartLanguageServer start_language_server = 37; - UpdateLanguageServer update_language_server = 38; - - OpenBufferById open_buffer_by_id = 39; - OpenBufferByPath open_buffer_by_path = 40; - OpenBufferResponse open_buffer_response = 41; - UpdateBuffer update_buffer = 42; - UpdateBufferFile update_buffer_file = 43; - SaveBuffer save_buffer = 44; - BufferSaved buffer_saved = 45; - BufferReloaded buffer_reloaded = 46; - ReloadBuffers reload_buffers = 47; - ReloadBuffersResponse reload_buffers_response = 48; - FormatBuffers format_buffers = 49; - FormatBuffersResponse format_buffers_response = 50; - GetCompletions get_completions = 51; - GetCompletionsResponse get_completions_response = 52; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 53; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 54; - GetCodeActions get_code_actions = 55; - GetCodeActionsResponse get_code_actions_response = 56; - ApplyCodeAction apply_code_action = 57; - ApplyCodeActionResponse apply_code_action_response = 58; - PrepareRename prepare_rename = 59; - PrepareRenameResponse prepare_rename_response = 60; - PerformRename perform_rename = 61; - PerformRenameResponse perform_rename_response = 62; - SearchProject search_project = 63; - SearchProjectResponse search_project_response = 64; - - GetChannels get_channels = 65; - GetChannelsResponse get_channels_response = 66; - JoinChannel join_channel = 67; - JoinChannelResponse join_channel_response = 68; - LeaveChannel leave_channel = 69; - SendChannelMessage send_channel_message = 70; - SendChannelMessageResponse send_channel_message_response = 71; - ChannelMessageSent channel_message_sent = 72; - GetChannelMessages get_channel_messages = 73; - GetChannelMessagesResponse get_channel_messages_response = 74; - - UpdateContacts update_contacts = 75; - - GetUsers get_users = 76; - FuzzySearchUsers fuzzy_search_users = 77; - UsersResponse users_response = 78; - RequestContact request_contact = 79; - RespondToContactRequest respond_to_contact_request = 80; - RemoveContact remove_contact = 81; - - Follow follow = 82; - FollowResponse follow_response = 83; - UpdateFollowers update_followers = 84; - Unfollow unfollow = 85; + RequestJoinProject request_join_project = 11; + RespondToJoinProjectRequest respond_to_join_project_request = 12; + JoinProjectRequestCancelled join_project_request_cancelled = 13; + JoinProject join_project = 14; + JoinProjectResponse join_project_response = 15; + LeaveProject leave_project = 16; + AddProjectCollaborator add_project_collaborator = 17; + RemoveProjectCollaborator remove_project_collaborator = 18; + ProjectUnshared project_unshared = 19; + + GetDefinition get_definition = 20; + GetDefinitionResponse get_definition_response = 21; + GetReferences get_references = 22; + GetReferencesResponse get_references_response = 23; + GetDocumentHighlights get_document_highlights = 24; + GetDocumentHighlightsResponse get_document_highlights_response = 25; + GetProjectSymbols get_project_symbols = 26; + GetProjectSymbolsResponse get_project_symbols_response = 27; + OpenBufferForSymbol open_buffer_for_symbol = 28; + OpenBufferForSymbolResponse open_buffer_for_symbol_response = 29; + + RegisterWorktree register_worktree = 30; + UnregisterWorktree unregister_worktree = 31; + UpdateWorktree update_worktree = 32; + + CreateProjectEntry create_project_entry = 33; + RenameProjectEntry rename_project_entry = 34; + DeleteProjectEntry delete_project_entry = 35; + ProjectEntryResponse project_entry_response = 36; + + UpdateDiagnosticSummary update_diagnostic_summary = 37; + StartLanguageServer start_language_server = 38; + UpdateLanguageServer update_language_server = 39; + + OpenBufferById open_buffer_by_id = 40; + OpenBufferByPath open_buffer_by_path = 41; + OpenBufferResponse open_buffer_response = 42; + UpdateBuffer update_buffer = 43; + UpdateBufferFile update_buffer_file = 44; + SaveBuffer save_buffer = 45; + BufferSaved buffer_saved = 46; + BufferReloaded buffer_reloaded = 47; + ReloadBuffers reload_buffers = 48; + ReloadBuffersResponse reload_buffers_response = 49; + FormatBuffers format_buffers = 50; + FormatBuffersResponse format_buffers_response = 51; + GetCompletions get_completions = 52; + GetCompletionsResponse get_completions_response = 53; + ApplyCompletionAdditionalEdits apply_completion_additional_edits = 54; + ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 55; + GetCodeActions get_code_actions = 56; + GetCodeActionsResponse get_code_actions_response = 57; + ApplyCodeAction apply_code_action = 58; + ApplyCodeActionResponse apply_code_action_response = 59; + PrepareRename prepare_rename = 60; + PrepareRenameResponse prepare_rename_response = 61; + PerformRename perform_rename = 62; + PerformRenameResponse perform_rename_response = 63; + SearchProject search_project = 64; + SearchProjectResponse search_project_response = 65; + + GetChannels get_channels = 66; + GetChannelsResponse get_channels_response = 67; + JoinChannel join_channel = 68; + JoinChannelResponse join_channel_response = 69; + LeaveChannel leave_channel = 70; + SendChannelMessage send_channel_message = 71; + SendChannelMessageResponse send_channel_message_response = 72; + ChannelMessageSent channel_message_sent = 73; + GetChannelMessages get_channel_messages = 74; + GetChannelMessagesResponse get_channel_messages_response = 75; + + UpdateContacts update_contacts = 76; + + GetUsers get_users = 77; + FuzzySearchUsers fuzzy_search_users = 78; + UsersResponse users_response = 79; + RequestContact request_contact = 80; + RespondToContactRequest respond_to_contact_request = 81; + RemoveContact remove_contact = 82; + + Follow follow = 83; + FollowResponse follow_response = 84; + UpdateFollowers update_followers = 85; + Unfollow unfollow = 86; } } @@ -124,12 +126,20 @@ message UnregisterProject { uint64 project_id = 1; } -message ShareProject { - uint64 project_id = 1; +message RequestJoinProject { + uint64 requester_id = 1; + uint64 project_id = 2; } -message UnshareProject { - uint64 project_id = 1; +message RespondToJoinProjectRequest { + uint64 requester_id = 1; + uint64 project_id = 2; + bool allow = 3; +} + +message JoinProjectRequestCancelled { + uint64 requester_id = 1; + uint64 project_id = 2; } message JoinProject { @@ -137,10 +147,27 @@ message JoinProject { } message JoinProjectResponse { - uint32 replica_id = 1; - repeated Worktree worktrees = 2; - repeated Collaborator collaborators = 3; - repeated LanguageServer language_servers = 4; + oneof variant { + Accept accept = 1; + Decline decline = 2; + } + + message Accept { + uint32 replica_id = 1; + repeated Worktree worktrees = 2; + repeated Collaborator collaborators = 3; + repeated LanguageServer language_servers = 4; + } + + message Decline { + Reason reason = 1; + + enum Reason { + Declined = 0; + Closed = 1; + WentOffline = 2; + } + } } message LeaveProject { @@ -201,6 +228,10 @@ message RemoveProjectCollaborator { uint32 peer_id = 2; } +message ProjectUnshared { + uint64 project_id = 1; +} + message GetDefinition { uint64 project_id = 1; uint64 buffer_id = 2; @@ -882,7 +913,6 @@ message Contact { message ProjectMetadata { uint64 id = 1; - bool is_shared = 2; repeated string worktree_root_names = 3; repeated uint64 guests = 4; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 0b7ba21c4a22419d1f13ba909bbaeab7e24c512b..a1b7425b69a724c50209c3bf0bf366987bd4fb94 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -114,6 +114,7 @@ messages!( (JoinChannelResponse, Foreground), (JoinProject, Foreground), (JoinProjectResponse, Foreground), + (JoinProjectRequestCancelled, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), (OpenBufferById, Background), @@ -128,6 +129,7 @@ messages!( (ProjectEntryResponse, Foreground), (RegisterProjectResponse, Foreground), (Ping, Foreground), + (ProjectUnshared, Foreground), (RegisterProject, Foreground), (RegisterWorktree, Foreground), (ReloadBuffers, Foreground), @@ -135,19 +137,19 @@ messages!( (RemoveProjectCollaborator, Foreground), (RenameProjectEntry, Foreground), (RequestContact, Foreground), + (RequestJoinProject, Foreground), (RespondToContactRequest, Foreground), + (RespondToJoinProjectRequest, Foreground), (SaveBuffer, Foreground), (SearchProject, Background), (SearchProjectResponse, Background), (SendChannelMessage, Foreground), (SendChannelMessageResponse, Foreground), - (ShareProject, Foreground), (StartLanguageServer, Foreground), (Test, Foreground), (Unfollow, Foreground), (UnregisterProject, Foreground), (UnregisterWorktree, Foreground), - (UnshareProject, Foreground), (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), @@ -195,7 +197,6 @@ request_messages!( (SaveBuffer, BufferSaved), (SearchProject, SearchProjectResponse), (SendChannelMessage, SendChannelMessageResponse), - (ShareProject, Ack), (Test, Test), (UpdateBuffer, Ack), (UpdateWorktree, Ack), @@ -220,20 +221,23 @@ entity_messages!( GetReferences, GetProjectSymbols, JoinProject, + JoinProjectRequestCancelled, LeaveProject, OpenBufferById, OpenBufferByPath, OpenBufferForSymbol, PerformRename, PrepareRename, + ProjectUnshared, ReloadBuffers, RemoveProjectCollaborator, + RequestJoinProject, SaveBuffer, SearchProject, StartLanguageServer, Unfollow, + UnregisterProject, UnregisterWorktree, - UnshareProject, UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index f238c6792d9ed188984292fe49de18400e308c91..1d4416d77520e5a2f8036679f5d7c85e17d68226 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 17; +pub const PROTOCOL_VERSION: u32 = 18; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c73035cc71d5d21d221d4cb77e493ec39e983e81..a8b2dfbf4e531318e4ef84a23f68a526a94f8436 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -48,6 +48,8 @@ pub struct Workspace { pub modal: ContainerStyle, pub notification: ContainerStyle, pub notifications: Notifications, + pub joining_project_avatar: ImageStyle, + pub joining_project_message: ContainedText, } #[derive(Clone, Deserialize, Default)] @@ -251,8 +253,7 @@ pub struct ContactsPanel { pub add_contact_button: IconButton, pub header_row: Interactive, pub contact_row: Interactive, - pub shared_project_row: Interactive, - pub unshared_project_row: Interactive, + pub project_row: Interactive, pub row_height: f32, pub contact_avatar: ImageStyle, pub contact_username: ContainedText, diff --git a/crates/workspace/src/waiting_room.rs b/crates/workspace/src/waiting_room.rs new file mode 100644 index 0000000000000000000000000000000000000000..fef7bf2e4332ebfe94c96bafd6fefd3706cd7f13 --- /dev/null +++ b/crates/workspace/src/waiting_room.rs @@ -0,0 +1,190 @@ +use crate::{ + sidebar::{Side, ToggleSidebarItem}, + AppState, ToggleFollow, +}; +use anyhow::Result; +use client::{proto, Client, Contact}; +use gpui::{ + elements::*, ElementBox, Entity, ImageData, MutableAppContext, RenderContext, Task, View, + ViewContext, +}; +use project::Project; +use settings::Settings; +use std::sync::Arc; +use util::ResultExt; + +pub struct WaitingRoom { + project_id: u64, + avatar: Option>, + message: String, + waiting: bool, + client: Arc, + _join_task: Task>, +} + +impl Entity for WaitingRoom { + type Event = (); + + fn release(&mut self, _: &mut MutableAppContext) { + if self.waiting { + self.client + .send(proto::LeaveProject { + project_id: self.project_id, + }) + .log_err(); + } + } +} + +impl View for WaitingRoom { + fn ui_name() -> &'static str { + "WaitingRoom" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = &cx.global::().theme.workspace; + + Flex::column() + .with_children(self.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.joining_project_avatar) + .aligned() + .boxed() + })) + .with_child( + Text::new( + self.message.clone(), + theme.joining_project_message.text.clone(), + ) + .contained() + .with_style(theme.joining_project_message.container) + .aligned() + .boxed(), + ) + .aligned() + .contained() + .with_background_color(theme.background) + .boxed() + } +} + +impl WaitingRoom { + pub fn new( + contact: Arc, + project_index: usize, + app_state: Arc, + cx: &mut ViewContext, + ) -> Self { + let project_id = contact.projects[project_index].id; + let client = app_state.client.clone(); + let _join_task = cx.spawn_weak({ + let contact = contact.clone(); + |this, mut cx| async move { + let project = Project::remote( + project_id, + app_state.client.clone(), + app_state.user_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), + &mut cx, + ) + .await; + + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.waiting = false; + match project { + Ok(project) => { + cx.replace_root_view(|cx| { + let mut workspace = (app_state.build_workspace)( + project.clone(), + &app_state, + cx, + ); + workspace.toggle_sidebar_item( + &ToggleSidebarItem { + side: Side::Left, + item_index: 0, + }, + cx, + ); + if let Some((host_peer_id, _)) = project + .read(cx) + .collaborators() + .iter() + .find(|(_, collaborator)| collaborator.replica_id == 0) + { + if let Some(follow) = workspace + .toggle_follow(&ToggleFollow(*host_peer_id), cx) + { + follow.detach_and_log_err(cx); + } + } + workspace + }); + } + Err(error @ _) => { + let login = &contact.user.github_login; + let message = match error { + project::JoinProjectError::HostDeclined => { + format!("@{} declined your request.", login) + } + project::JoinProjectError::HostClosedProject => { + format!( + "@{} closed their copy of {}.", + login, + humanize_list( + &contact.projects[project_index] + .worktree_root_names + ) + ) + } + project::JoinProjectError::HostWentOffline => { + format!("@{} went offline.", login) + } + project::JoinProjectError::Other(error) => { + log::error!("error joining project: {}", error); + "An error occurred.".to_string() + } + }; + this.message = message; + cx.notify(); + } + } + }) + } + + Ok(()) + } + }); + + Self { + project_id, + avatar: contact.user.avatar.clone(), + message: format!( + "Asking to join @{}'s copy of {}...", + contact.user.github_login, + humanize_list(&contact.projects[project_index].worktree_root_names) + ), + waiting: true, + client, + _join_task, + } + } +} + +fn humanize_list<'a>(items: impl IntoIterator) -> String { + let mut list = String::new(); + let mut items = items.into_iter().enumerate().peekable(); + while let Some((ix, item)) = items.next() { + if ix > 0 { + list.push_str(", "); + if items.peek().is_none() { + list.push_str("and "); + } + } + + list.push_str(item); + } + list +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fada690bb52a3fc80fddf0c32b83dee9cd433cea..d27cb9bfbaf9791d64160c4956db22bfccb8e843 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5,10 +5,12 @@ pub mod pane_group; pub mod sidebar; mod status_bar; mod toolbar; +mod waiting_room; use anyhow::{anyhow, Context, Result}; use client::{ - proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore, + proto, Authenticate, ChannelList, Client, Contact, PeerId, Subscription, TypedEnvelope, User, + UserStore, }; use clock::ReplicaId; use collections::{hash_map, HashMap, HashSet}; @@ -49,6 +51,7 @@ use std::{ use theme::{Theme, ThemeRegistry}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use util::ResultExt; +use waiting_room::WaitingRoom; type ProjectItemBuilders = HashMap< TypeId, @@ -72,7 +75,6 @@ type FollowableItemBuilders = HashMap< actions!( workspace, [ - ToggleShare, Unfollow, Save, ActivatePreviousPane, @@ -98,7 +100,8 @@ pub struct ToggleFollow(pub PeerId); #[derive(Clone)] pub struct JoinProject { - pub project_id: u64, + pub contact: Arc, + pub project_index: usize, pub app_state: Arc, } @@ -118,10 +121,14 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { open_new(&action.0, cx) }); cx.add_global_action(move |action: &JoinProject, cx: &mut MutableAppContext| { - join_project(action.project_id, &action.app_state, cx).detach(); + join_project( + action.contact.clone(), + action.project_index, + &action.app_state, + cx, + ); }); - cx.add_action(Workspace::toggle_share); cx.add_async_action(Workspace::toggle_follow); cx.add_async_action(Workspace::follow_next_collaborator); cx.add_action( @@ -692,6 +699,7 @@ impl WorkspaceParams { pub enum Event { PaneAdded(ViewHandle), + ContactRequestedJoin(u64), } pub struct Workspace { @@ -1366,18 +1374,6 @@ impl Workspace { &self.active_pane } - fn toggle_share(&mut self, _: &ToggleShare, cx: &mut ViewContext) { - self.project.update(cx, |project, cx| { - if project.is_local() { - if project.is_shared() { - project.unshare(cx); - } else if project.can_share(cx) { - project.share(cx).detach(); - } - } - }); - } - fn project_remote_id_changed(&mut self, remote_id: Option, cx: &mut ViewContext) { if let Some(remote_id) = remote_id { self.remote_entity_subscription = @@ -1580,7 +1576,6 @@ impl Workspace { cx, )) .with_children(self.render_connection_status(cx)) - .with_children(self.render_share_icon(theme, cx)) .boxed(), ) .right() @@ -1701,39 +1696,6 @@ impl Workspace { } } - fn render_share_icon(&self, theme: &Theme, cx: &mut RenderContext) -> Option { - if self.project().read(cx).is_local() - && self.client.user_id().is_some() - && self.project().read(cx).can_share(cx) - { - Some( - MouseEventHandler::new::(0, cx, |state, cx| { - let style = &theme - .workspace - .titlebar - .share_icon - .style_for(state, self.project().read(cx).is_shared()); - Svg::new("icons/share.svg") - .with_color(style.color) - .constrained() - .with_height(14.) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(24.) - .aligned() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(|_, cx| cx.dispatch_action(ToggleShare)) - .boxed(), - ) - } else { - None - } - } - fn render_disconnected_overlay(&self, cx: &AppContext) -> Option { if self.project.read(cx).is_read_only() { let theme = &cx.global::().theme; @@ -2315,36 +2277,25 @@ pub fn open_paths( } pub fn join_project( - project_id: u64, + contact: Arc, + project_index: usize, app_state: &Arc, cx: &mut MutableAppContext, -) -> Task>> { +) { + let project_id = contact.projects[project_index].id; + for window_id in cx.window_ids().collect::>() { if let Some(workspace) = cx.root_view::(window_id) { if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) { - return Task::ready(Ok(workspace)); + cx.activate_window(window_id); + return; } } } - let app_state = app_state.clone(); - cx.spawn(|mut cx| async move { - let project = Project::remote( - project_id, - app_state.client.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - &mut cx, - ) - .await?; - Ok(cx.update(|cx| { - cx.add_window((app_state.build_window_options)(), |cx| { - (app_state.build_workspace)(project, &app_state, cx) - }) - .1 - })) - }) + cx.add_window((app_state.build_window_options)(), |cx| { + WaitingRoom::new(contact, project_index, app_state.clone(), cx) + }); } fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index fda2c3c1c58df568d8eeceef8018ab18bcc508d8..bfb75d88740ae1eee3928cc7d1f18b9b2cbe3dad 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -123,17 +123,18 @@ pub fn build_workspace( cx.subscribe(&cx.handle(), { let project = project.clone(); move |_, _, event, cx| { - let workspace::Event::PaneAdded(pane) = event; - pane.update(cx, |pane, cx| { - pane.toolbar().update(cx, |toolbar, cx| { - let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(project.clone())); - toolbar.add_item(breadcrumbs, cx); - let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); - toolbar.add_item(buffer_search_bar, cx); - let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); - toolbar.add_item(project_search_bar, cx); - }) - }); + if let workspace::Event::PaneAdded(pane) = event { + pane.update(cx, |pane, cx| { + pane.toolbar().update(cx, |toolbar, cx| { + let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(project.clone())); + toolbar.add_item(breadcrumbs, cx); + let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx)); + toolbar.add_item(buffer_search_bar, cx); + let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + toolbar.add_item(project_search_bar, cx); + }) + }); + } } }) .detach(); diff --git a/script/watch-themes b/script/watch-themes new file mode 100755 index 0000000000000000000000000000000000000000..a3663962f7ca970130c6596246419cdaf91ca219 --- /dev/null +++ b/script/watch-themes @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +cd styles +npm install +npm run watch diff --git a/styles/dist/dark.json b/styles/dist/dark.json deleted file mode 100644 index cf780140dd115f1a85553761c186365420ad2293..0000000000000000000000000000000000000000 --- a/styles/dist/dark.json +++ /dev/null @@ -1,531 +0,0 @@ -{ - "meta": { - "themeName": "cave-dark" - }, - "text": { - "primary": { - "value": "#e2dfe7", - "type": "color" - }, - "secondary": { - "value": "#8b8792", - "type": "color" - }, - "muted": { - "value": "#8b8792", - "type": "color" - }, - "placeholder": { - "value": "#7e7887", - "type": "color" - }, - "active": { - "value": "#efecf4", - "type": "color" - }, - "feature": { - "value": "#576ddb", - "type": "color" - }, - "ok": { - "value": "#2a9292", - "type": "color" - }, - "error": { - "value": "#be4678", - "type": "color" - }, - "warning": { - "value": "#a06e3b", - "type": "color" - }, - "info": { - "value": "#576ddb", - "type": "color" - } - }, - "icon": { - "primary": { - "value": "#e2dfe7", - "type": "color" - }, - "secondary": { - "value": "#8b8792", - "type": "color" - }, - "muted": { - "value": "#8b8792", - "type": "color" - }, - "placeholder": { - "value": "#7e7887", - "type": "color" - }, - "active": { - "value": "#efecf4", - "type": "color" - }, - "feature": { - "value": "#576ddb", - "type": "color" - }, - "ok": { - "value": "#2a9292", - "type": "color" - }, - "error": { - "value": "#be4678", - "type": "color" - }, - "warning": { - "value": "#a06e3b", - "type": "color" - }, - "info": { - "value": "#576ddb", - "type": "color" - } - }, - "background": { - "100": { - "base": { - "value": "#26232a", - "type": "color" - }, - "hovered": { - "value": "#5852603d", - "type": "color" - }, - "active": { - "value": "#5852605c", - "type": "color" - } - }, - "300": { - "base": { - "value": "#26232a", - "type": "color" - }, - "hovered": { - "value": "#5852603d", - "type": "color" - }, - "active": { - "value": "#5852605c", - "type": "color" - } - }, - "500": { - "base": { - "value": "#19171c", - "type": "color" - }, - "hovered": { - "value": "#26232a3d", - "type": "color" - }, - "active": { - "value": "#26232a5c", - "type": "color" - } - }, - "on300": { - "base": { - "value": "#19171c", - "type": "color" - }, - "hovered": { - "value": "#26232a3d", - "type": "color" - }, - "active": { - "value": "#26232a7a", - "type": "color" - } - }, - "on500": { - "base": { - "value": "#26232a", - "type": "color" - }, - "hovered": { - "value": "#5852603d", - "type": "color" - }, - "active": { - "value": "#5852607a", - "type": "color" - } - }, - "ok": { - "base": { - "value": "#2a929226", - "type": "color" - }, - "hovered": { - "value": "#2a929233", - "type": "color" - }, - "active": { - "value": "#2a929240", - "type": "color" - } - }, - "error": { - "base": { - "value": "#be467826", - "type": "color" - }, - "hovered": { - "value": "#be467833", - "type": "color" - }, - "active": { - "value": "#be467840", - "type": "color" - } - }, - "warning": { - "base": { - "value": "#a06e3b26", - "type": "color" - }, - "hovered": { - "value": "#a06e3b33", - "type": "color" - }, - "active": { - "value": "#a06e3b40", - "type": "color" - } - }, - "info": { - "base": { - "value": "#576ddb26", - "type": "color" - }, - "hovered": { - "value": "#576ddb33", - "type": "color" - }, - "active": { - "value": "#576ddb40", - "type": "color" - } - } - }, - "border": { - "primary": { - "value": "#19171c", - "type": "color" - }, - "secondary": { - "value": "#26232a", - "type": "color" - }, - "muted": { - "value": "#655f6d", - "type": "color" - }, - "active": { - "value": "#655f6d", - "type": "color" - }, - "onMedia": { - "value": "#19171c1a", - "type": "color" - }, - "ok": { - "value": "#2a929226", - "type": "color" - }, - "error": { - "value": "#be467826", - "type": "color" - }, - "warning": { - "value": "#a06e3b26", - "type": "color" - }, - "info": { - "value": "#576ddb26", - "type": "color" - } - }, - "editor": { - "background": { - "value": "#19171c", - "type": "color" - }, - "indent_guide": { - "value": "#655f6d", - "type": "color" - }, - "indent_guide_active": { - "value": "#26232a", - "type": "color" - }, - "line": { - "active": { - "value": "#efecf412", - "type": "color" - }, - "highlighted": { - "value": "#efecf41f", - "type": "color" - }, - "inserted": { - "value": "#2a929240", - "type": "color" - }, - "deleted": { - "value": "#be467840", - "type": "color" - }, - "modified": { - "value": "#576ddb40", - "type": "color" - } - }, - "highlight": { - "selection": { - "value": "#576ddb3d", - "type": "color" - }, - "occurrence": { - "value": "#efecf41f", - "type": "color" - }, - "activeOccurrence": { - "value": "#efecf43d", - "type": "color" - }, - "matchingBracket": { - "value": "#26232a5c", - "type": "color" - }, - "match": { - "value": "#955ae77a", - "type": "color" - }, - "activeMatch": { - "value": "#955ae7b8", - "type": "color" - }, - "related": { - "value": "#26232a3d", - "type": "color" - } - }, - "gutter": { - "primary": { - "value": "#7e7887", - "type": "color" - }, - "active": { - "value": "#efecf4", - "type": "color" - } - } - }, - "syntax": { - "primary": { - "value": "#efecf4", - "type": "color" - }, - "comment": { - "value": "#8b8792", - "type": "color" - }, - "keyword": { - "value": "#576ddb", - "type": "color" - }, - "function": { - "value": "#a06e3b", - "type": "color" - }, - "type": { - "value": "#398bc6", - "type": "color" - }, - "variant": { - "value": "#576ddb", - "type": "color" - }, - "property": { - "value": "#576ddb", - "type": "color" - }, - "enum": { - "value": "#aa573c", - "type": "color" - }, - "operator": { - "value": "#aa573c", - "type": "color" - }, - "string": { - "value": "#aa573c", - "type": "color" - }, - "number": { - "value": "#2a9292", - "type": "color" - }, - "boolean": { - "value": "#2a9292", - "type": "color" - } - }, - "player": { - "1": { - "baseColor": { - "value": "#576ddb", - "type": "color" - }, - "cursorColor": { - "value": "#576ddb", - "type": "color" - }, - "selectionColor": { - "value": "#576ddb3d", - "type": "color" - }, - "borderColor": { - "value": "#576ddbcc", - "type": "color" - } - }, - "2": { - "baseColor": { - "value": "#2a9292", - "type": "color" - }, - "cursorColor": { - "value": "#2a9292", - "type": "color" - }, - "selectionColor": { - "value": "#2a92923d", - "type": "color" - }, - "borderColor": { - "value": "#2a9292cc", - "type": "color" - } - }, - "3": { - "baseColor": { - "value": "#bf40bf", - "type": "color" - }, - "cursorColor": { - "value": "#bf40bf", - "type": "color" - }, - "selectionColor": { - "value": "#bf40bf3d", - "type": "color" - }, - "borderColor": { - "value": "#bf40bfcc", - "type": "color" - } - }, - "4": { - "baseColor": { - "value": "#aa573c", - "type": "color" - }, - "cursorColor": { - "value": "#aa573c", - "type": "color" - }, - "selectionColor": { - "value": "#aa573c3d", - "type": "color" - }, - "borderColor": { - "value": "#aa573ccc", - "type": "color" - } - }, - "5": { - "baseColor": { - "value": "#955ae7", - "type": "color" - }, - "cursorColor": { - "value": "#955ae7", - "type": "color" - }, - "selectionColor": { - "value": "#955ae73d", - "type": "color" - }, - "borderColor": { - "value": "#955ae7cc", - "type": "color" - } - }, - "6": { - "baseColor": { - "value": "#398bc6", - "type": "color" - }, - "cursorColor": { - "value": "#398bc6", - "type": "color" - }, - "selectionColor": { - "value": "#398bc63d", - "type": "color" - }, - "borderColor": { - "value": "#398bc6cc", - "type": "color" - } - }, - "7": { - "baseColor": { - "value": "#be4678", - "type": "color" - }, - "cursorColor": { - "value": "#be4678", - "type": "color" - }, - "selectionColor": { - "value": "#be46783d", - "type": "color" - }, - "borderColor": { - "value": "#be4678cc", - "type": "color" - } - }, - "8": { - "baseColor": { - "value": "#a06e3b", - "type": "color" - }, - "cursorColor": { - "value": "#a06e3b", - "type": "color" - }, - "selectionColor": { - "value": "#a06e3b3d", - "type": "color" - }, - "borderColor": { - "value": "#a06e3bcc", - "type": "color" - } - } - }, - "shadowAlpha": { - "value": 0.24, - "type": "number" - } -} \ No newline at end of file diff --git a/styles/dist/light.json b/styles/dist/light.json deleted file mode 100644 index 1287dea481fc0ff6f1bc33e7e1e0ecff065badaa..0000000000000000000000000000000000000000 --- a/styles/dist/light.json +++ /dev/null @@ -1,531 +0,0 @@ -{ - "meta": { - "themeName": "cave-light" - }, - "text": { - "primary": { - "value": "#26232a", - "type": "color" - }, - "secondary": { - "value": "#585260", - "type": "color" - }, - "muted": { - "value": "#585260", - "type": "color" - }, - "placeholder": { - "value": "#655f6d", - "type": "color" - }, - "active": { - "value": "#19171c", - "type": "color" - }, - "feature": { - "value": "#576ddb", - "type": "color" - }, - "ok": { - "value": "#2a9292", - "type": "color" - }, - "error": { - "value": "#be4678", - "type": "color" - }, - "warning": { - "value": "#a06e3b", - "type": "color" - }, - "info": { - "value": "#576ddb", - "type": "color" - } - }, - "icon": { - "primary": { - "value": "#26232a", - "type": "color" - }, - "secondary": { - "value": "#585260", - "type": "color" - }, - "muted": { - "value": "#585260", - "type": "color" - }, - "placeholder": { - "value": "#655f6d", - "type": "color" - }, - "active": { - "value": "#19171c", - "type": "color" - }, - "feature": { - "value": "#576ddb", - "type": "color" - }, - "ok": { - "value": "#2a9292", - "type": "color" - }, - "error": { - "value": "#be4678", - "type": "color" - }, - "warning": { - "value": "#a06e3b", - "type": "color" - }, - "info": { - "value": "#576ddb", - "type": "color" - } - }, - "background": { - "100": { - "base": { - "value": "#e2dfe7", - "type": "color" - }, - "hovered": { - "value": "#8b87921f", - "type": "color" - }, - "active": { - "value": "#8b87922e", - "type": "color" - } - }, - "300": { - "base": { - "value": "#e2dfe7", - "type": "color" - }, - "hovered": { - "value": "#8b87921f", - "type": "color" - }, - "active": { - "value": "#8b87922e", - "type": "color" - } - }, - "500": { - "base": { - "value": "#efecf4", - "type": "color" - }, - "hovered": { - "value": "#e2dfe71f", - "type": "color" - }, - "active": { - "value": "#e2dfe72e", - "type": "color" - } - }, - "on300": { - "base": { - "value": "#efecf4", - "type": "color" - }, - "hovered": { - "value": "#e2dfe71f", - "type": "color" - }, - "active": { - "value": "#e2dfe73d", - "type": "color" - } - }, - "on500": { - "base": { - "value": "#e2dfe7", - "type": "color" - }, - "hovered": { - "value": "#8b87921f", - "type": "color" - }, - "active": { - "value": "#8b87923d", - "type": "color" - } - }, - "ok": { - "base": { - "value": "#2a929226", - "type": "color" - }, - "hovered": { - "value": "#2a929233", - "type": "color" - }, - "active": { - "value": "#2a929240", - "type": "color" - } - }, - "error": { - "base": { - "value": "#be467826", - "type": "color" - }, - "hovered": { - "value": "#be467833", - "type": "color" - }, - "active": { - "value": "#be467840", - "type": "color" - } - }, - "warning": { - "base": { - "value": "#a06e3b26", - "type": "color" - }, - "hovered": { - "value": "#a06e3b33", - "type": "color" - }, - "active": { - "value": "#a06e3b40", - "type": "color" - } - }, - "info": { - "base": { - "value": "#576ddb26", - "type": "color" - }, - "hovered": { - "value": "#576ddb33", - "type": "color" - }, - "active": { - "value": "#576ddb40", - "type": "color" - } - } - }, - "border": { - "primary": { - "value": "#efecf4", - "type": "color" - }, - "secondary": { - "value": "#e2dfe7", - "type": "color" - }, - "muted": { - "value": "#7e7887", - "type": "color" - }, - "active": { - "value": "#7e7887", - "type": "color" - }, - "onMedia": { - "value": "#efecf41a", - "type": "color" - }, - "ok": { - "value": "#2a929226", - "type": "color" - }, - "error": { - "value": "#be467826", - "type": "color" - }, - "warning": { - "value": "#a06e3b26", - "type": "color" - }, - "info": { - "value": "#576ddb26", - "type": "color" - } - }, - "editor": { - "background": { - "value": "#efecf4", - "type": "color" - }, - "indent_guide": { - "value": "#7e7887", - "type": "color" - }, - "indent_guide_active": { - "value": "#e2dfe7", - "type": "color" - }, - "line": { - "active": { - "value": "#19171c12", - "type": "color" - }, - "highlighted": { - "value": "#19171c1f", - "type": "color" - }, - "inserted": { - "value": "#2a929240", - "type": "color" - }, - "deleted": { - "value": "#be467840", - "type": "color" - }, - "modified": { - "value": "#576ddb40", - "type": "color" - } - }, - "highlight": { - "selection": { - "value": "#576ddb3d", - "type": "color" - }, - "occurrence": { - "value": "#19171c0f", - "type": "color" - }, - "activeOccurrence": { - "value": "#19171c1f", - "type": "color" - }, - "matchingBracket": { - "value": "#e2dfe72e", - "type": "color" - }, - "match": { - "value": "#955ae73d", - "type": "color" - }, - "activeMatch": { - "value": "#955ae75c", - "type": "color" - }, - "related": { - "value": "#e2dfe71f", - "type": "color" - } - }, - "gutter": { - "primary": { - "value": "#655f6d", - "type": "color" - }, - "active": { - "value": "#19171c", - "type": "color" - } - } - }, - "syntax": { - "primary": { - "value": "#19171c", - "type": "color" - }, - "comment": { - "value": "#585260", - "type": "color" - }, - "keyword": { - "value": "#576ddb", - "type": "color" - }, - "function": { - "value": "#a06e3b", - "type": "color" - }, - "type": { - "value": "#398bc6", - "type": "color" - }, - "variant": { - "value": "#576ddb", - "type": "color" - }, - "property": { - "value": "#576ddb", - "type": "color" - }, - "enum": { - "value": "#aa573c", - "type": "color" - }, - "operator": { - "value": "#aa573c", - "type": "color" - }, - "string": { - "value": "#aa573c", - "type": "color" - }, - "number": { - "value": "#2a9292", - "type": "color" - }, - "boolean": { - "value": "#2a9292", - "type": "color" - } - }, - "player": { - "1": { - "baseColor": { - "value": "#576ddb", - "type": "color" - }, - "cursorColor": { - "value": "#576ddb", - "type": "color" - }, - "selectionColor": { - "value": "#576ddb3d", - "type": "color" - }, - "borderColor": { - "value": "#576ddbcc", - "type": "color" - } - }, - "2": { - "baseColor": { - "value": "#2a9292", - "type": "color" - }, - "cursorColor": { - "value": "#2a9292", - "type": "color" - }, - "selectionColor": { - "value": "#2a92923d", - "type": "color" - }, - "borderColor": { - "value": "#2a9292cc", - "type": "color" - } - }, - "3": { - "baseColor": { - "value": "#bf40bf", - "type": "color" - }, - "cursorColor": { - "value": "#bf40bf", - "type": "color" - }, - "selectionColor": { - "value": "#bf40bf3d", - "type": "color" - }, - "borderColor": { - "value": "#bf40bfcc", - "type": "color" - } - }, - "4": { - "baseColor": { - "value": "#aa573c", - "type": "color" - }, - "cursorColor": { - "value": "#aa573c", - "type": "color" - }, - "selectionColor": { - "value": "#aa573c3d", - "type": "color" - }, - "borderColor": { - "value": "#aa573ccc", - "type": "color" - } - }, - "5": { - "baseColor": { - "value": "#955ae7", - "type": "color" - }, - "cursorColor": { - "value": "#955ae7", - "type": "color" - }, - "selectionColor": { - "value": "#955ae73d", - "type": "color" - }, - "borderColor": { - "value": "#955ae7cc", - "type": "color" - } - }, - "6": { - "baseColor": { - "value": "#398bc6", - "type": "color" - }, - "cursorColor": { - "value": "#398bc6", - "type": "color" - }, - "selectionColor": { - "value": "#398bc63d", - "type": "color" - }, - "borderColor": { - "value": "#398bc6cc", - "type": "color" - } - }, - "7": { - "baseColor": { - "value": "#be4678", - "type": "color" - }, - "cursorColor": { - "value": "#be4678", - "type": "color" - }, - "selectionColor": { - "value": "#be46783d", - "type": "color" - }, - "borderColor": { - "value": "#be4678cc", - "type": "color" - } - }, - "8": { - "baseColor": { - "value": "#a06e3b", - "type": "color" - }, - "cursorColor": { - "value": "#a06e3b", - "type": "color" - }, - "selectionColor": { - "value": "#a06e3b3d", - "type": "color" - }, - "borderColor": { - "value": "#a06e3bcc", - "type": "color" - } - } - }, - "shadowAlpha": { - "value": 0.12, - "type": "number" - } -} \ No newline at end of file diff --git a/styles/dist/solarized-dark.json b/styles/dist/solarized-dark.json index 11c58dc9442d5a0d6472349f0e267c9156427f0e..68d97906bfeba92f4dd4a482ac4088b9539125a7 100644 --- a/styles/dist/solarized-dark.json +++ b/styles/dist/solarized-dark.json @@ -89,15 +89,15 @@ "background": { "100": { "base": { - "value": "#073642", + "value": "#1b444f", "type": "color" }, "hovered": { - "value": "#586e753d", + "value": "#30525c", "type": "color" }, "active": { - "value": "#586e755c", + "value": "#446068", "type": "color" } }, @@ -107,11 +107,11 @@ "type": "color" }, "hovered": { - "value": "#586e753d", + "value": "#1b444f", "type": "color" }, "active": { - "value": "#586e755c", + "value": "#30525c", "type": "color" } }, @@ -121,11 +121,11 @@ "type": "color" }, "hovered": { - "value": "#0736423d", + "value": "#022e39", "type": "color" }, "active": { - "value": "#0736425c", + "value": "#04313c", "type": "color" } }, @@ -135,25 +135,25 @@ "type": "color" }, "hovered": { - "value": "#0736423d", + "value": "#022e39", "type": "color" }, "active": { - "value": "#0736427a", + "value": "#04313c", "type": "color" } }, "on500": { "base": { - "value": "#073642", + "value": "#1b444f", "type": "color" }, "hovered": { - "value": "#586e753d", + "value": "#30525c", "type": "color" }, "active": { - "value": "#586e757a", + "value": "#446068", "type": "color" } }, @@ -267,23 +267,11 @@ }, "line": { "active": { - "value": "#fdf6e312", + "value": "#073642", "type": "color" }, "highlighted": { - "value": "#fdf6e31f", - "type": "color" - }, - "inserted": { - "value": "#85990040", - "type": "color" - }, - "deleted": { - "value": "#dc322f40", - "type": "color" - }, - "modified": { - "value": "#268bd240", + "value": "#1b444f", "type": "color" } }, @@ -293,27 +281,27 @@ "type": "color" }, "occurrence": { - "value": "#fdf6e31f", + "value": "#586e753d", "type": "color" }, "activeOccurrence": { - "value": "#fdf6e33d", + "value": "#586e757a", "type": "color" }, "matchingBracket": { - "value": "#0736425c", + "value": "#04313c", "type": "color" }, "match": { - "value": "#6c71c47a", + "value": "#1b1f6b", "type": "color" }, "activeMatch": { - "value": "#6c71c4b8", + "value": "#434abc7a", "type": "color" }, "related": { - "value": "#0736423d", + "value": "#022e39", "type": "color" } }, diff --git a/styles/dist/solarized-light.json b/styles/dist/solarized-light.json index 0bd8dada67b333436ddb3eea46e7e311dad1d4db..f21430dfa8771fd95aa42074236901da7e9ded8a 100644 --- a/styles/dist/solarized-light.json +++ b/styles/dist/solarized-light.json @@ -89,15 +89,15 @@ "background": { "100": { "base": { - "value": "#eee8d5", + "value": "#d7d6c8", "type": "color" }, "hovered": { - "value": "#93a1a11f", + "value": "#c1c5bb", "type": "color" }, "active": { - "value": "#93a1a12e", + "value": "#aab3ae", "type": "color" } }, @@ -107,11 +107,11 @@ "type": "color" }, "hovered": { - "value": "#93a1a11f", + "value": "#d7d6c8", "type": "color" }, "active": { - "value": "#93a1a12e", + "value": "#c1c5bb", "type": "color" } }, @@ -121,11 +121,11 @@ "type": "color" }, "hovered": { - "value": "#eee8d51f", + "value": "#f9f3e0", "type": "color" }, "active": { - "value": "#eee8d52e", + "value": "#f6efdc", "type": "color" } }, @@ -135,25 +135,25 @@ "type": "color" }, "hovered": { - "value": "#eee8d51f", + "value": "#f9f3e0", "type": "color" }, "active": { - "value": "#eee8d53d", + "value": "#f6efdc", "type": "color" } }, "on500": { "base": { - "value": "#eee8d5", + "value": "#d7d6c8", "type": "color" }, "hovered": { - "value": "#93a1a11f", + "value": "#c1c5bb", "type": "color" }, "active": { - "value": "#93a1a13d", + "value": "#aab3ae", "type": "color" } }, @@ -216,19 +216,19 @@ }, "border": { "primary": { - "value": "#fdf6e3", + "value": "#93a1a1", "type": "color" }, "secondary": { - "value": "#eee8d5", + "value": "#93a1a1", "type": "color" }, "muted": { - "value": "#839496", + "value": "#657b83", "type": "color" }, "active": { - "value": "#839496", + "value": "#657b83", "type": "color" }, "onMedia": { @@ -258,32 +258,20 @@ "type": "color" }, "indent_guide": { - "value": "#839496", + "value": "#657b83", "type": "color" }, "indent_guide_active": { - "value": "#eee8d5", + "value": "#93a1a1", "type": "color" }, "line": { "active": { - "value": "#002b3612", + "value": "#eee8d5", "type": "color" }, "highlighted": { - "value": "#002b361f", - "type": "color" - }, - "inserted": { - "value": "#85990040", - "type": "color" - }, - "deleted": { - "value": "#dc322f40", - "type": "color" - }, - "modified": { - "value": "#268bd240", + "value": "#d7d6c8", "type": "color" } }, @@ -293,27 +281,27 @@ "type": "color" }, "occurrence": { - "value": "#002b360f", + "value": "#93a1a11f", "type": "color" }, "activeOccurrence": { - "value": "#002b361f", + "value": "#93a1a13d", "type": "color" }, "matchingBracket": { - "value": "#eee8d52e", + "value": "#f6efdc", "type": "color" }, "match": { - "value": "#6c71c43d", + "value": "#bcc0f6", "type": "color" }, "activeMatch": { - "value": "#6c71c45c", + "value": "#7f84d73d", "type": "color" }, "related": { - "value": "#eee8d51f", + "value": "#f9f3e0", "type": "color" } }, diff --git a/styles/dist/tokens.json b/styles/dist/tokens.json index 29a39b1fbe998ab3be4536f2d099d0bb288fef59..5874a9a7c19f2aca2cd83a9da5468415a9648db3 100644 --- a/styles/dist/tokens.json +++ b/styles/dist/tokens.json @@ -1271,15 +1271,15 @@ "background": { "100": { "base": { - "value": "#26232a", + "value": "#332f38", "type": "color" }, "hovered": { - "value": "#5852603d", + "value": "#3f3b45", "type": "color" }, "active": { - "value": "#5852605c", + "value": "#4c4653", "type": "color" } }, @@ -1289,11 +1289,11 @@ "type": "color" }, "hovered": { - "value": "#5852603d", + "value": "#332f38", "type": "color" }, "active": { - "value": "#5852605c", + "value": "#3f3b45", "type": "color" } }, @@ -1303,11 +1303,11 @@ "type": "color" }, "hovered": { - "value": "#26232a3d", + "value": "#1c1a20", "type": "color" }, "active": { - "value": "#26232a5c", + "value": "#201d23", "type": "color" } }, @@ -1317,25 +1317,25 @@ "type": "color" }, "hovered": { - "value": "#26232a3d", + "value": "#1c1a20", "type": "color" }, "active": { - "value": "#26232a7a", + "value": "#201d23", "type": "color" } }, "on500": { "base": { - "value": "#26232a", + "value": "#332f38", "type": "color" }, "hovered": { - "value": "#5852603d", + "value": "#3f3b45", "type": "color" }, "active": { - "value": "#5852607a", + "value": "#4c4653", "type": "color" } }, @@ -1449,23 +1449,11 @@ }, "line": { "active": { - "value": "#efecf412", + "value": "#26232a", "type": "color" }, "highlighted": { - "value": "#efecf41f", - "type": "color" - }, - "inserted": { - "value": "#2a929240", - "type": "color" - }, - "deleted": { - "value": "#be467840", - "type": "color" - }, - "modified": { - "value": "#576ddb40", + "value": "#332f38", "type": "color" } }, @@ -1475,27 +1463,27 @@ "type": "color" }, "occurrence": { - "value": "#efecf41f", + "value": "#5852603d", "type": "color" }, "activeOccurrence": { - "value": "#efecf43d", + "value": "#5852607a", "type": "color" }, "matchingBracket": { - "value": "#26232a5c", + "value": "#201d23", "type": "color" }, "match": { - "value": "#955ae77a", + "value": "#3d1576", "type": "color" }, "activeMatch": { - "value": "#955ae7b8", + "value": "#782edf7a", "type": "color" }, "related": { - "value": "#26232a3d", + "value": "#1c1a20", "type": "color" } }, @@ -1802,15 +1790,15 @@ "background": { "100": { "base": { - "value": "#e2dfe7", + "value": "#ccc9d2", "type": "color" }, "hovered": { - "value": "#8b87921f", + "value": "#b7b3bd", "type": "color" }, "active": { - "value": "#8b87922e", + "value": "#a19da7", "type": "color" } }, @@ -1820,11 +1808,11 @@ "type": "color" }, "hovered": { - "value": "#8b87921f", + "value": "#ccc9d2", "type": "color" }, "active": { - "value": "#8b87922e", + "value": "#b7b3bd", "type": "color" } }, @@ -1834,11 +1822,11 @@ "type": "color" }, "hovered": { - "value": "#e2dfe71f", + "value": "#ece9f1", "type": "color" }, "active": { - "value": "#e2dfe72e", + "value": "#e9e6ee", "type": "color" } }, @@ -1848,25 +1836,25 @@ "type": "color" }, "hovered": { - "value": "#e2dfe71f", + "value": "#ece9f1", "type": "color" }, "active": { - "value": "#e2dfe73d", + "value": "#e9e6ee", "type": "color" } }, "on500": { "base": { - "value": "#e2dfe7", + "value": "#ccc9d2", "type": "color" }, "hovered": { - "value": "#8b87921f", + "value": "#b7b3bd", "type": "color" }, "active": { - "value": "#8b87923d", + "value": "#a19da7", "type": "color" } }, @@ -1929,19 +1917,19 @@ }, "border": { "primary": { - "value": "#efecf4", + "value": "#8b8792", "type": "color" }, "secondary": { - "value": "#e2dfe7", + "value": "#8b8792", "type": "color" }, "muted": { - "value": "#7e7887", + "value": "#655f6d", "type": "color" }, "active": { - "value": "#7e7887", + "value": "#655f6d", "type": "color" }, "onMedia": { @@ -1971,32 +1959,20 @@ "type": "color" }, "indent_guide": { - "value": "#7e7887", + "value": "#655f6d", "type": "color" }, "indent_guide_active": { - "value": "#e2dfe7", + "value": "#8b8792", "type": "color" }, "line": { "active": { - "value": "#19171c12", + "value": "#e2dfe7", "type": "color" }, "highlighted": { - "value": "#19171c1f", - "type": "color" - }, - "inserted": { - "value": "#2a929240", - "type": "color" - }, - "deleted": { - "value": "#be467840", - "type": "color" - }, - "modified": { - "value": "#576ddb40", + "value": "#ccc9d2", "type": "color" } }, @@ -2006,27 +1982,27 @@ "type": "color" }, "occurrence": { - "value": "#19171c0f", + "value": "#8b87921f", "type": "color" }, "activeOccurrence": { - "value": "#19171c1f", + "value": "#8b87923d", "type": "color" }, "matchingBracket": { - "value": "#e2dfe72e", + "value": "#e9e6ee", "type": "color" }, "match": { - "value": "#955ae73d", + "value": "#d5bdfa", "type": "color" }, "activeMatch": { - "value": "#955ae75c", + "value": "#a775ee3d", "type": "color" }, "related": { - "value": "#e2dfe71f", + "value": "#ece9f1", "type": "color" } }, @@ -2333,15 +2309,15 @@ "background": { "100": { "base": { - "value": "#073642", + "value": "#1b444f", "type": "color" }, "hovered": { - "value": "#586e753d", + "value": "#30525c", "type": "color" }, "active": { - "value": "#586e755c", + "value": "#446068", "type": "color" } }, @@ -2351,11 +2327,11 @@ "type": "color" }, "hovered": { - "value": "#586e753d", + "value": "#1b444f", "type": "color" }, "active": { - "value": "#586e755c", + "value": "#30525c", "type": "color" } }, @@ -2365,11 +2341,11 @@ "type": "color" }, "hovered": { - "value": "#0736423d", + "value": "#022e39", "type": "color" }, "active": { - "value": "#0736425c", + "value": "#04313c", "type": "color" } }, @@ -2379,25 +2355,25 @@ "type": "color" }, "hovered": { - "value": "#0736423d", + "value": "#022e39", "type": "color" }, "active": { - "value": "#0736427a", + "value": "#04313c", "type": "color" } }, "on500": { "base": { - "value": "#073642", + "value": "#1b444f", "type": "color" }, "hovered": { - "value": "#586e753d", + "value": "#30525c", "type": "color" }, "active": { - "value": "#586e757a", + "value": "#446068", "type": "color" } }, @@ -2511,23 +2487,11 @@ }, "line": { "active": { - "value": "#fdf6e312", + "value": "#073642", "type": "color" }, "highlighted": { - "value": "#fdf6e31f", - "type": "color" - }, - "inserted": { - "value": "#85990040", - "type": "color" - }, - "deleted": { - "value": "#dc322f40", - "type": "color" - }, - "modified": { - "value": "#268bd240", + "value": "#1b444f", "type": "color" } }, @@ -2537,27 +2501,27 @@ "type": "color" }, "occurrence": { - "value": "#fdf6e31f", + "value": "#586e753d", "type": "color" }, "activeOccurrence": { - "value": "#fdf6e33d", + "value": "#586e757a", "type": "color" }, "matchingBracket": { - "value": "#0736425c", + "value": "#04313c", "type": "color" }, "match": { - "value": "#6c71c47a", + "value": "#1b1f6b", "type": "color" }, "activeMatch": { - "value": "#6c71c4b8", + "value": "#434abc7a", "type": "color" }, "related": { - "value": "#0736423d", + "value": "#022e39", "type": "color" } }, @@ -2864,15 +2828,15 @@ "background": { "100": { "base": { - "value": "#eee8d5", + "value": "#d7d6c8", "type": "color" }, "hovered": { - "value": "#93a1a11f", + "value": "#c1c5bb", "type": "color" }, "active": { - "value": "#93a1a12e", + "value": "#aab3ae", "type": "color" } }, @@ -2882,11 +2846,11 @@ "type": "color" }, "hovered": { - "value": "#93a1a11f", + "value": "#d7d6c8", "type": "color" }, "active": { - "value": "#93a1a12e", + "value": "#c1c5bb", "type": "color" } }, @@ -2896,11 +2860,11 @@ "type": "color" }, "hovered": { - "value": "#eee8d51f", + "value": "#f9f3e0", "type": "color" }, "active": { - "value": "#eee8d52e", + "value": "#f6efdc", "type": "color" } }, @@ -2910,25 +2874,25 @@ "type": "color" }, "hovered": { - "value": "#eee8d51f", + "value": "#f9f3e0", "type": "color" }, "active": { - "value": "#eee8d53d", + "value": "#f6efdc", "type": "color" } }, "on500": { "base": { - "value": "#eee8d5", + "value": "#d7d6c8", "type": "color" }, "hovered": { - "value": "#93a1a11f", + "value": "#c1c5bb", "type": "color" }, "active": { - "value": "#93a1a13d", + "value": "#aab3ae", "type": "color" } }, @@ -2991,19 +2955,19 @@ }, "border": { "primary": { - "value": "#fdf6e3", + "value": "#93a1a1", "type": "color" }, "secondary": { - "value": "#eee8d5", + "value": "#93a1a1", "type": "color" }, "muted": { - "value": "#839496", + "value": "#657b83", "type": "color" }, "active": { - "value": "#839496", + "value": "#657b83", "type": "color" }, "onMedia": { @@ -3033,32 +2997,20 @@ "type": "color" }, "indent_guide": { - "value": "#839496", + "value": "#657b83", "type": "color" }, "indent_guide_active": { - "value": "#eee8d5", + "value": "#93a1a1", "type": "color" }, "line": { "active": { - "value": "#002b3612", + "value": "#eee8d5", "type": "color" }, "highlighted": { - "value": "#002b361f", - "type": "color" - }, - "inserted": { - "value": "#85990040", - "type": "color" - }, - "deleted": { - "value": "#dc322f40", - "type": "color" - }, - "modified": { - "value": "#268bd240", + "value": "#d7d6c8", "type": "color" } }, @@ -3068,27 +3020,27 @@ "type": "color" }, "occurrence": { - "value": "#002b360f", + "value": "#93a1a11f", "type": "color" }, "activeOccurrence": { - "value": "#002b361f", + "value": "#93a1a13d", "type": "color" }, "matchingBracket": { - "value": "#eee8d52e", + "value": "#f6efdc", "type": "color" }, "match": { - "value": "#6c71c43d", + "value": "#bcc0f6", "type": "color" }, "activeMatch": { - "value": "#6c71c45c", + "value": "#7f84d73d", "type": "color" }, "related": { - "value": "#eee8d51f", + "value": "#f9f3e0", "type": "color" } }, @@ -3303,5 +3255,1043 @@ "value": 0.12, "type": "number" } + }, + "sulphurpool-dark": { + "meta": { + "themeName": "sulphurpool-dark" + }, + "text": { + "primary": { + "value": "#dfe2f1", + "type": "color" + }, + "secondary": { + "value": "#979db4", + "type": "color" + }, + "muted": { + "value": "#979db4", + "type": "color" + }, + "placeholder": { + "value": "#898ea4", + "type": "color" + }, + "active": { + "value": "#f5f7ff", + "type": "color" + }, + "feature": { + "value": "#3d8fd1", + "type": "color" + }, + "ok": { + "value": "#ac9739", + "type": "color" + }, + "error": { + "value": "#c94922", + "type": "color" + }, + "warning": { + "value": "#c08b30", + "type": "color" + }, + "info": { + "value": "#3d8fd1", + "type": "color" + } + }, + "icon": { + "primary": { + "value": "#dfe2f1", + "type": "color" + }, + "secondary": { + "value": "#979db4", + "type": "color" + }, + "muted": { + "value": "#979db4", + "type": "color" + }, + "placeholder": { + "value": "#898ea4", + "type": "color" + }, + "active": { + "value": "#f5f7ff", + "type": "color" + }, + "feature": { + "value": "#3d8fd1", + "type": "color" + }, + "ok": { + "value": "#ac9739", + "type": "color" + }, + "error": { + "value": "#c94922", + "type": "color" + }, + "warning": { + "value": "#c08b30", + "type": "color" + }, + "info": { + "value": "#3d8fd1", + "type": "color" + } + }, + "background": { + "100": { + "base": { + "value": "#363f62", + "type": "color" + }, + "hovered": { + "value": "#444c6f", + "type": "color" + }, + "active": { + "value": "#51597b", + "type": "color" + } + }, + "300": { + "base": { + "value": "#293256", + "type": "color" + }, + "hovered": { + "value": "#363f62", + "type": "color" + }, + "active": { + "value": "#444c6f", + "type": "color" + } + }, + "500": { + "base": { + "value": "#202746", + "type": "color" + }, + "hovered": { + "value": "#222a4a", + "type": "color" + }, + "active": { + "value": "#252d4e", + "type": "color" + } + }, + "on300": { + "base": { + "value": "#202746", + "type": "color" + }, + "hovered": { + "value": "#222a4a", + "type": "color" + }, + "active": { + "value": "#252d4e", + "type": "color" + } + }, + "on500": { + "base": { + "value": "#363f62", + "type": "color" + }, + "hovered": { + "value": "#444c6f", + "type": "color" + }, + "active": { + "value": "#51597b", + "type": "color" + } + }, + "ok": { + "base": { + "value": "#ac973926", + "type": "color" + }, + "hovered": { + "value": "#ac973933", + "type": "color" + }, + "active": { + "value": "#ac973940", + "type": "color" + } + }, + "error": { + "base": { + "value": "#c9492226", + "type": "color" + }, + "hovered": { + "value": "#c9492233", + "type": "color" + }, + "active": { + "value": "#c9492240", + "type": "color" + } + }, + "warning": { + "base": { + "value": "#c08b3026", + "type": "color" + }, + "hovered": { + "value": "#c08b3033", + "type": "color" + }, + "active": { + "value": "#c08b3040", + "type": "color" + } + }, + "info": { + "base": { + "value": "#3d8fd126", + "type": "color" + }, + "hovered": { + "value": "#3d8fd133", + "type": "color" + }, + "active": { + "value": "#3d8fd140", + "type": "color" + } + } + }, + "border": { + "primary": { + "value": "#202746", + "type": "color" + }, + "secondary": { + "value": "#293256", + "type": "color" + }, + "muted": { + "value": "#6b7394", + "type": "color" + }, + "active": { + "value": "#6b7394", + "type": "color" + }, + "onMedia": { + "value": "#2027461a", + "type": "color" + }, + "ok": { + "value": "#ac973926", + "type": "color" + }, + "error": { + "value": "#c9492226", + "type": "color" + }, + "warning": { + "value": "#c08b3026", + "type": "color" + }, + "info": { + "value": "#3d8fd126", + "type": "color" + } + }, + "editor": { + "background": { + "value": "#202746", + "type": "color" + }, + "indent_guide": { + "value": "#6b7394", + "type": "color" + }, + "indent_guide_active": { + "value": "#293256", + "type": "color" + }, + "line": { + "active": { + "value": "#293256", + "type": "color" + }, + "highlighted": { + "value": "#363f62", + "type": "color" + } + }, + "highlight": { + "selection": { + "value": "#3d8fd13d", + "type": "color" + }, + "occurrence": { + "value": "#5e66873d", + "type": "color" + }, + "activeOccurrence": { + "value": "#5e66877a", + "type": "color" + }, + "matchingBracket": { + "value": "#252d4e", + "type": "color" + }, + "match": { + "value": "#1a2a6d", + "type": "color" + }, + "activeMatch": { + "value": "#3d56c47a", + "type": "color" + }, + "related": { + "value": "#222a4a", + "type": "color" + } + }, + "gutter": { + "primary": { + "value": "#898ea4", + "type": "color" + }, + "active": { + "value": "#f5f7ff", + "type": "color" + } + } + }, + "syntax": { + "primary": { + "value": "#f5f7ff", + "type": "color" + }, + "comment": { + "value": "#979db4", + "type": "color" + }, + "keyword": { + "value": "#3d8fd1", + "type": "color" + }, + "function": { + "value": "#c08b30", + "type": "color" + }, + "type": { + "value": "#22a2c9", + "type": "color" + }, + "variant": { + "value": "#3d8fd1", + "type": "color" + }, + "property": { + "value": "#3d8fd1", + "type": "color" + }, + "enum": { + "value": "#c76b29", + "type": "color" + }, + "operator": { + "value": "#c76b29", + "type": "color" + }, + "string": { + "value": "#c76b29", + "type": "color" + }, + "number": { + "value": "#ac9739", + "type": "color" + }, + "boolean": { + "value": "#ac9739", + "type": "color" + } + }, + "player": { + "1": { + "baseColor": { + "value": "#3d8fd1", + "type": "color" + }, + "cursorColor": { + "value": "#3d8fd1", + "type": "color" + }, + "selectionColor": { + "value": "#3d8fd13d", + "type": "color" + }, + "borderColor": { + "value": "#3d8fd1cc", + "type": "color" + } + }, + "2": { + "baseColor": { + "value": "#ac9739", + "type": "color" + }, + "cursorColor": { + "value": "#ac9739", + "type": "color" + }, + "selectionColor": { + "value": "#ac97393d", + "type": "color" + }, + "borderColor": { + "value": "#ac9739cc", + "type": "color" + } + }, + "3": { + "baseColor": { + "value": "#9c637a", + "type": "color" + }, + "cursorColor": { + "value": "#9c637a", + "type": "color" + }, + "selectionColor": { + "value": "#9c637a3d", + "type": "color" + }, + "borderColor": { + "value": "#9c637acc", + "type": "color" + } + }, + "4": { + "baseColor": { + "value": "#c76b29", + "type": "color" + }, + "cursorColor": { + "value": "#c76b29", + "type": "color" + }, + "selectionColor": { + "value": "#c76b293d", + "type": "color" + }, + "borderColor": { + "value": "#c76b29cc", + "type": "color" + } + }, + "5": { + "baseColor": { + "value": "#6679cc", + "type": "color" + }, + "cursorColor": { + "value": "#6679cc", + "type": "color" + }, + "selectionColor": { + "value": "#6679cc3d", + "type": "color" + }, + "borderColor": { + "value": "#6679cccc", + "type": "color" + } + }, + "6": { + "baseColor": { + "value": "#22a2c9", + "type": "color" + }, + "cursorColor": { + "value": "#22a2c9", + "type": "color" + }, + "selectionColor": { + "value": "#22a2c93d", + "type": "color" + }, + "borderColor": { + "value": "#22a2c9cc", + "type": "color" + } + }, + "7": { + "baseColor": { + "value": "#c94922", + "type": "color" + }, + "cursorColor": { + "value": "#c94922", + "type": "color" + }, + "selectionColor": { + "value": "#c949223d", + "type": "color" + }, + "borderColor": { + "value": "#c94922cc", + "type": "color" + } + }, + "8": { + "baseColor": { + "value": "#c08b30", + "type": "color" + }, + "cursorColor": { + "value": "#c08b30", + "type": "color" + }, + "selectionColor": { + "value": "#c08b303d", + "type": "color" + }, + "borderColor": { + "value": "#c08b30cc", + "type": "color" + } + } + }, + "shadowAlpha": { + "value": 0.24, + "type": "number" + } + }, + "sulphurpool-light": { + "meta": { + "themeName": "sulphurpool-light" + }, + "text": { + "primary": { + "value": "#293256", + "type": "color" + }, + "secondary": { + "value": "#5e6687", + "type": "color" + }, + "muted": { + "value": "#5e6687", + "type": "color" + }, + "placeholder": { + "value": "#6b7394", + "type": "color" + }, + "active": { + "value": "#202746", + "type": "color" + }, + "feature": { + "value": "#3d8fd1", + "type": "color" + }, + "ok": { + "value": "#ac9739", + "type": "color" + }, + "error": { + "value": "#c94922", + "type": "color" + }, + "warning": { + "value": "#c08b30", + "type": "color" + }, + "info": { + "value": "#3d8fd1", + "type": "color" + } + }, + "icon": { + "primary": { + "value": "#293256", + "type": "color" + }, + "secondary": { + "value": "#5e6687", + "type": "color" + }, + "muted": { + "value": "#5e6687", + "type": "color" + }, + "placeholder": { + "value": "#6b7394", + "type": "color" + }, + "active": { + "value": "#202746", + "type": "color" + }, + "feature": { + "value": "#3d8fd1", + "type": "color" + }, + "ok": { + "value": "#ac9739", + "type": "color" + }, + "error": { + "value": "#c94922", + "type": "color" + }, + "warning": { + "value": "#c08b30", + "type": "color" + }, + "info": { + "value": "#3d8fd1", + "type": "color" + } + }, + "background": { + "100": { + "base": { + "value": "#cdd1e2", + "type": "color" + }, + "hovered": { + "value": "#bbc0d3", + "type": "color" + }, + "active": { + "value": "#a9aec3", + "type": "color" + } + }, + "300": { + "base": { + "value": "#dfe2f1", + "type": "color" + }, + "hovered": { + "value": "#cdd1e2", + "type": "color" + }, + "active": { + "value": "#bbc0d3", + "type": "color" + } + }, + "500": { + "base": { + "value": "#f5f7ff", + "type": "color" + }, + "hovered": { + "value": "#f0f2fc", + "type": "color" + }, + "active": { + "value": "#eaedf8", + "type": "color" + } + }, + "on300": { + "base": { + "value": "#f5f7ff", + "type": "color" + }, + "hovered": { + "value": "#f0f2fc", + "type": "color" + }, + "active": { + "value": "#eaedf8", + "type": "color" + } + }, + "on500": { + "base": { + "value": "#cdd1e2", + "type": "color" + }, + "hovered": { + "value": "#bbc0d3", + "type": "color" + }, + "active": { + "value": "#a9aec3", + "type": "color" + } + }, + "ok": { + "base": { + "value": "#ac973926", + "type": "color" + }, + "hovered": { + "value": "#ac973933", + "type": "color" + }, + "active": { + "value": "#ac973940", + "type": "color" + } + }, + "error": { + "base": { + "value": "#c9492226", + "type": "color" + }, + "hovered": { + "value": "#c9492233", + "type": "color" + }, + "active": { + "value": "#c9492240", + "type": "color" + } + }, + "warning": { + "base": { + "value": "#c08b3026", + "type": "color" + }, + "hovered": { + "value": "#c08b3033", + "type": "color" + }, + "active": { + "value": "#c08b3040", + "type": "color" + } + }, + "info": { + "base": { + "value": "#3d8fd126", + "type": "color" + }, + "hovered": { + "value": "#3d8fd133", + "type": "color" + }, + "active": { + "value": "#3d8fd140", + "type": "color" + } + } + }, + "border": { + "primary": { + "value": "#979db4", + "type": "color" + }, + "secondary": { + "value": "#979db4", + "type": "color" + }, + "muted": { + "value": "#6b7394", + "type": "color" + }, + "active": { + "value": "#6b7394", + "type": "color" + }, + "onMedia": { + "value": "#f5f7ff1a", + "type": "color" + }, + "ok": { + "value": "#ac973926", + "type": "color" + }, + "error": { + "value": "#c9492226", + "type": "color" + }, + "warning": { + "value": "#c08b3026", + "type": "color" + }, + "info": { + "value": "#3d8fd126", + "type": "color" + } + }, + "editor": { + "background": { + "value": "#f5f7ff", + "type": "color" + }, + "indent_guide": { + "value": "#6b7394", + "type": "color" + }, + "indent_guide_active": { + "value": "#979db4", + "type": "color" + }, + "line": { + "active": { + "value": "#dfe2f1", + "type": "color" + }, + "highlighted": { + "value": "#cdd1e2", + "type": "color" + } + }, + "highlight": { + "selection": { + "value": "#3d8fd13d", + "type": "color" + }, + "occurrence": { + "value": "#979db41f", + "type": "color" + }, + "activeOccurrence": { + "value": "#979db43d", + "type": "color" + }, + "matchingBracket": { + "value": "#eaedf8", + "type": "color" + }, + "match": { + "value": "#bcc6f7", + "type": "color" + }, + "activeMatch": { + "value": "#7b8ddc3d", + "type": "color" + }, + "related": { + "value": "#f0f2fc", + "type": "color" + } + }, + "gutter": { + "primary": { + "value": "#6b7394", + "type": "color" + }, + "active": { + "value": "#202746", + "type": "color" + } + } + }, + "syntax": { + "primary": { + "value": "#202746", + "type": "color" + }, + "comment": { + "value": "#5e6687", + "type": "color" + }, + "keyword": { + "value": "#3d8fd1", + "type": "color" + }, + "function": { + "value": "#c08b30", + "type": "color" + }, + "type": { + "value": "#22a2c9", + "type": "color" + }, + "variant": { + "value": "#3d8fd1", + "type": "color" + }, + "property": { + "value": "#3d8fd1", + "type": "color" + }, + "enum": { + "value": "#c76b29", + "type": "color" + }, + "operator": { + "value": "#c76b29", + "type": "color" + }, + "string": { + "value": "#c76b29", + "type": "color" + }, + "number": { + "value": "#ac9739", + "type": "color" + }, + "boolean": { + "value": "#ac9739", + "type": "color" + } + }, + "player": { + "1": { + "baseColor": { + "value": "#3d8fd1", + "type": "color" + }, + "cursorColor": { + "value": "#3d8fd1", + "type": "color" + }, + "selectionColor": { + "value": "#3d8fd13d", + "type": "color" + }, + "borderColor": { + "value": "#3d8fd1cc", + "type": "color" + } + }, + "2": { + "baseColor": { + "value": "#ac9739", + "type": "color" + }, + "cursorColor": { + "value": "#ac9739", + "type": "color" + }, + "selectionColor": { + "value": "#ac97393d", + "type": "color" + }, + "borderColor": { + "value": "#ac9739cc", + "type": "color" + } + }, + "3": { + "baseColor": { + "value": "#9c637a", + "type": "color" + }, + "cursorColor": { + "value": "#9c637a", + "type": "color" + }, + "selectionColor": { + "value": "#9c637a3d", + "type": "color" + }, + "borderColor": { + "value": "#9c637acc", + "type": "color" + } + }, + "4": { + "baseColor": { + "value": "#c76b29", + "type": "color" + }, + "cursorColor": { + "value": "#c76b29", + "type": "color" + }, + "selectionColor": { + "value": "#c76b293d", + "type": "color" + }, + "borderColor": { + "value": "#c76b29cc", + "type": "color" + } + }, + "5": { + "baseColor": { + "value": "#6679cc", + "type": "color" + }, + "cursorColor": { + "value": "#6679cc", + "type": "color" + }, + "selectionColor": { + "value": "#6679cc3d", + "type": "color" + }, + "borderColor": { + "value": "#6679cccc", + "type": "color" + } + }, + "6": { + "baseColor": { + "value": "#22a2c9", + "type": "color" + }, + "cursorColor": { + "value": "#22a2c9", + "type": "color" + }, + "selectionColor": { + "value": "#22a2c93d", + "type": "color" + }, + "borderColor": { + "value": "#22a2c9cc", + "type": "color" + } + }, + "7": { + "baseColor": { + "value": "#c94922", + "type": "color" + }, + "cursorColor": { + "value": "#c94922", + "type": "color" + }, + "selectionColor": { + "value": "#c949223d", + "type": "color" + }, + "borderColor": { + "value": "#c94922cc", + "type": "color" + } + }, + "8": { + "baseColor": { + "value": "#c08b30", + "type": "color" + }, + "cursorColor": { + "value": "#c08b30", + "type": "color" + }, + "selectionColor": { + "value": "#c08b303d", + "type": "color" + }, + "borderColor": { + "value": "#c08b30cc", + "type": "color" + } + } + }, + "shadowAlpha": { + "value": 0.12, + "type": "number" + } } } \ No newline at end of file diff --git a/styles/src/buildThemes.ts b/styles/src/buildThemes.ts index 69bd96f2c5d5c0726f9f1b0d472981f111c753fa..520a37395d5f1775ac37c465ec4f81ac5121b0ef 100644 --- a/styles/src/buildThemes.ts +++ b/styles/src/buildThemes.ts @@ -1,17 +1,9 @@ import * as fs from "fs"; import * as path from "path"; import app from "./styleTree/app"; -import { dark as caveDark, light as caveLight } from "./themes/cave"; -import { dark as solarizedDark, light as solarizedLight } from "./themes/solarized"; -import { dark as sulphurpoolDark, light as sulphurpoolLight } from "./themes/sulphurpool"; +import themes from "./themes"; import snakeCase from "./utils/snakeCase"; -const themes = [ - caveDark, caveLight, - solarizedDark, solarizedLight, - sulphurpoolDark, sulphurpoolLight -]; - const themeDirectory = `${__dirname}/../../assets/themes/`; // Clear existing themes diff --git a/styles/src/buildTokens.ts b/styles/src/buildTokens.ts index 27ea50d814ecfa6618ab4006eb02cb65c083fc85..04b4a6b7523f50dede166f539c9d5fb83e86af67 100644 --- a/styles/src/buildTokens.ts +++ b/styles/src/buildTokens.ts @@ -1,9 +1,7 @@ import * as fs from "fs"; import * as path from "path"; -import { light as solarizedLight, dark as solarizedDark } from "./themes/solarized"; -// Use cave as "light" and "dark" themes -import { light, dark } from "./themes/cave"; -import Theme from "./themes/theme"; +import themes from "./themes"; +import Theme from "./themes/common/theme"; import { colors, fontFamilies, fontSizes, fontWeights, sizes } from "./tokens"; // Organize theme tokens @@ -50,6 +48,9 @@ const coreTokens = { const combinedTokens: any = {}; const distPath = path.resolve(`${__dirname}/../dist`); +for (const file of fs.readdirSync(distPath)) { + fs.unlinkSync(path.join(distPath, file)); +} // Add core tokens to the combined tokens and write `core.json`. // We write `core.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly. @@ -60,7 +61,6 @@ combinedTokens.core = coreTokens; // Add each theme to the combined tokens and write ${theme}.json. // We write `${theme}.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly. -let themes = [dark, light, solarizedDark, solarizedLight]; themes.forEach((theme) => { const themePath = `${distPath}/${theme.name}.json` fs.writeFileSync(themePath, JSON.stringify(themeTokens(theme), null, 2)); diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 9822f6766e3e2a99731dfca0b34494892ec68ea9..230bd3e57f8eb31b5b602785026c6abe927d1ffe 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import chatPanel from "./chatPanel"; import { text } from "./components"; import contactFinder from "./contactFinder"; diff --git a/styles/src/styleTree/chatPanel.ts b/styles/src/styleTree/chatPanel.ts index 69b5f3baa0d74c03e4ff5e0a81ff4fc6616afdfc..778cd6758480b337a9f3dd8d87caa0ad822687cc 100644 --- a/styles/src/styleTree/chatPanel.ts +++ b/styles/src/styleTree/chatPanel.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { panel } from "./app"; import { backgroundColor, diff --git a/styles/src/styleTree/commandPalette.ts b/styles/src/styleTree/commandPalette.ts index 0dd290a91c8eeb379c07a2abd0a191ad8a755de1..7b94f1cfaee2a05908e3385c66b5355170fe0f45 100644 --- a/styles/src/styleTree/commandPalette.ts +++ b/styles/src/styleTree/commandPalette.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { text, backgroundColor, border } from "./components"; export default function commandPalette(theme: Theme) { diff --git a/styles/src/styleTree/components.ts b/styles/src/styleTree/components.ts index d0412af02e6b7e3a11ef9aae978d846c4e6da551..2a265377508e81738a105af8a44ea3baf1c096c7 100644 --- a/styles/src/styleTree/components.ts +++ b/styles/src/styleTree/components.ts @@ -1,5 +1,5 @@ import chroma from "chroma-js"; -import Theme, { BackgroundColorSet } from "../themes/theme"; +import Theme, { BackgroundColorSet } from "../themes/common/theme"; import { fontFamilies, fontSizes, FontWeight } from "../tokens"; import { Color } from "../utils/color"; diff --git a/styles/src/styleTree/contactFinder.ts b/styles/src/styleTree/contactFinder.ts index 853f87ca5e654d8824309158b0e97360e1ebc1fc..fdefa34b8ad629a7fa5de5426e6f54e23b4b8160 100644 --- a/styles/src/styleTree/contactFinder.ts +++ b/styles/src/styleTree/contactFinder.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import picker from "./picker"; import { backgroundColor, iconColor } from "./components"; diff --git a/styles/src/styleTree/contactNotification.ts b/styles/src/styleTree/contactNotification.ts index 09360f2f9118876070a0700392f6fc2756b9e5c4..0ed9636399e2385c4ae2d3864c0b0d2f975f9ef3 100644 --- a/styles/src/styleTree/contactNotification.ts +++ b/styles/src/styleTree/contactNotification.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, iconColor, text } from "./components"; const avatarSize = 12; diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index a2caafadec633aeda5ecd7fc3334cecb784a2b6a..d55487a97b5ffe8a1973686b96c7d5809b0ec3cb 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { panel } from "./app"; import { backgroundColor, border, borderColor, iconColor, player, text } from "./components"; @@ -122,7 +122,7 @@ export default function contactsPanel(theme: Theme) { background: backgroundColor(theme, 100), color: iconColor(theme, "muted"), }, - sharedProjectRow: { + projectRow: { ...projectRow, background: backgroundColor(theme, 300), name: { @@ -136,19 +136,5 @@ export default function contactsPanel(theme: Theme) { background: backgroundColor(theme, 300, "active"), } }, - unsharedProjectRow: { - ...projectRow, - background: backgroundColor(theme, 300), - name: { - ...projectRow.name, - ...text(theme, "mono", "secondary", { size: "sm" }), - }, - hover: { - background: backgroundColor(theme, 300, "hovered"), - }, - active: { - background: backgroundColor(theme, 300, "active"), - } - } } } diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 88e630b357f87c93b064f630671be227476295e0..06f0d98d70fab9ac3096591fc5a0769fdeac5007 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, border, diff --git a/styles/src/styleTree/picker.ts b/styles/src/styleTree/picker.ts index e783945dd1366023a9bff9d2dacf266586f86628..adb53458d3457e5e1e38776016c7e58a8da5bdca 100644 --- a/styles/src/styleTree/picker.ts +++ b/styles/src/styleTree/picker.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, border, player, shadow, text } from "./components"; export default function picker(theme: Theme) { diff --git a/styles/src/styleTree/projectDiagnostics.ts b/styles/src/styleTree/projectDiagnostics.ts index 7331df1f98e45480d5032a81f21fa80cdd2feaea..fe2d3e5f3b24da2d7120c4fe491817853741b831 100644 --- a/styles/src/styleTree/projectDiagnostics.ts +++ b/styles/src/styleTree/projectDiagnostics.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, text, diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts index bacc3590e552c5900e113825229574af6c82e98d..6892b666d9b37cab5c126ecfedb91feea9c335db 100644 --- a/styles/src/styleTree/projectPanel.ts +++ b/styles/src/styleTree/projectPanel.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { panel } from "./app"; import { backgroundColor, iconColor, player, text } from "./components"; diff --git a/styles/src/styleTree/search.ts b/styles/src/styleTree/search.ts index b48dccc156149c2269ae45640cf66426cd760625..7febfb98b19cee4da8ebc17d32e2974bd4dda708 100644 --- a/styles/src/styleTree/search.ts +++ b/styles/src/styleTree/search.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, border, player, text } from "./components"; export default function search(theme: Theme) { diff --git a/styles/src/styleTree/statusBar.ts b/styles/src/styleTree/statusBar.ts index c7b7c6a0a35b3138e77d648b042344f98c507ff2..4b3fbaafeab1f71a083d3f3d3c689d5151f56690 100644 --- a/styles/src/styleTree/statusBar.ts +++ b/styles/src/styleTree/statusBar.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, border, iconColor, text } from "./components"; import { workspaceBackground } from "./workspace"; diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 65564f5cbc35299fd49e39656e97370798312084..560049b4947d6eddff6f54815112f4615de480a1 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -1,4 +1,4 @@ -import Theme from "../themes/theme"; +import Theme from "../themes/common/theme"; import { backgroundColor, border, iconColor, shadow, text } from "./components"; import statusBar from "./statusBar"; @@ -41,6 +41,14 @@ export default function workspace(theme: Theme) { return { background: backgroundColor(theme, 300), + joiningProjectAvatar: { + cornerRadius: 40, + width: 80, + }, + joiningProjectMessage: { + padding: 12, + ...text(theme, "sans", "primary", { size: "lg" }) + }, leaderBorderOpacity: 0.7, leaderBorderWidth: 2.0, tab, diff --git a/styles/src/themes.ts b/styles/src/themes.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef02fab8219fc3aa748436461fe08e916192dd5d --- /dev/null +++ b/styles/src/themes.ts @@ -0,0 +1,16 @@ +import fs from "fs"; +import path from "path"; +import Theme from "./themes/common/theme"; + +const themes: Theme[] = []; +export default themes; + +const themesPath = path.resolve(`${__dirname}/themes`); +for (const fileName of fs.readdirSync(themesPath)) { + const filePath = path.join(themesPath, fileName); + if (fs.statSync(filePath).isFile()) { + const theme = require(filePath); + themes.push(theme.dark); + themes.push(theme.light); + } +} diff --git a/styles/src/themes/cave.ts b/styles/src/themes/cave.ts index aa443dc83734dd92356b5b7cc778022d0f5e580b..f3c8102ac9ecc5d7b267a7f201cf73e47275cbc1 100644 --- a/styles/src/themes/cave.ts +++ b/styles/src/themes/cave.ts @@ -1,5 +1,5 @@ import chroma from "chroma-js"; -import { colorRamp, createTheme } from "./base16"; +import { colorRamp, createTheme } from "./common/base16"; const name = "cave"; diff --git a/styles/src/themes/base16.ts b/styles/src/themes/common/base16.ts similarity index 99% rename from styles/src/themes/base16.ts rename to styles/src/themes/common/base16.ts index 812235072725e323a03ef4352539823a45dfc46d..7fd485b0c3cb38d8322c4e37550d5b54429cac94 100644 --- a/styles/src/themes/base16.ts +++ b/styles/src/themes/common/base16.ts @@ -1,7 +1,7 @@ import chroma from "chroma-js"; import { Scale, Color } from "chroma-js"; -import { color, ColorToken, fontWeights, NumberToken } from "../tokens"; -import { withOpacity } from "../utils/color"; +import { color, ColorToken, fontWeights, NumberToken } from "../../tokens"; +import { withOpacity } from "../../utils/color"; import Theme, { buildPlayer, Syntax } from "./theme"; export function colorRamp(color: Color): Scale { diff --git a/styles/src/themes/theme.ts b/styles/src/themes/common/theme.ts similarity index 96% rename from styles/src/themes/theme.ts rename to styles/src/themes/common/theme.ts index 21eda88268841222e3d92d88a639152ea551dca5..2c648e87ec530d639e3bd77167b01632219c38ea 100644 --- a/styles/src/themes/theme.ts +++ b/styles/src/themes/common/theme.ts @@ -1,5 +1,5 @@ -import { ColorToken, FontWeightToken, NumberToken } from "../tokens"; -import { withOpacity } from "../utils/color"; +import { ColorToken, FontWeightToken, NumberToken } from "../../tokens"; +import { withOpacity } from "../../utils/color"; export interface SyntaxHighlightStyle { color: ColorToken; diff --git a/styles/src/themes/gruvbox.ts b/styles/src/themes/gruvbox.ts deleted file mode 100644 index f0100fa3272135a36120b6c6a494ae0b8af6416b..0000000000000000000000000000000000000000 --- a/styles/src/themes/gruvbox.ts +++ /dev/null @@ -1,30 +0,0 @@ -import chroma from "chroma-js"; -import { createTheme } from "./base16"; - -const name = "cave"; - -const colors = { - "red": chroma("#be4678"), - "orange": chroma("#aa573c"), - "yellow": chroma("#a06e3b"), - "green": chroma("#2a9292"), - "cyan": chroma("#398bc6"), - "blue": chroma("#576ddb"), - "violet": chroma("#955ae7"), - "magenta": chroma("#bf40bf"), -}; - -const ramps = { - neutral: chroma.scale(["#19171c", "#26232a", "#585260", "#655f6d", "#7e7887", "#8b8792", "#e2dfe7", "#efecf4"]), - red: chroma.scale([colors.red.darken(3), colors.red, colors.red.brighten(3)]), - orange: chroma.scale([colors.orange.darken(3), colors.orange, colors.orange.brighten(3)]), - yellow: chroma.scale([colors.yellow.darken(3), colors.yellow, colors.yellow.brighten(3)]), - green: chroma.scale([colors.green.darken(3), colors.green, colors.green.brighten(3)]), - cyan: chroma.scale([colors.cyan.darken(3), colors.cyan, colors.cyan.brighten(3)]), - blue: chroma.scale([colors.blue.darken(3), colors.blue, colors.blue.brighten(3)]), - violet: chroma.scale([colors.violet.darken(3), colors.violet, colors.violet.brighten(3)]), - magenta: chroma.scale([colors.magenta.darken(3), colors.magenta, colors.magenta.brighten(3)]), -} - -export const dark = createTheme(`${name}-dark`, false, ramps); -export const light = createTheme(`${name}-light`, true, ramps); \ No newline at end of file diff --git a/styles/src/themes/solarized.ts b/styles/src/themes/solarized.ts index 2b9db833115e9c026c6b0be35bf08916a1011613..6992dc5bf8e86f30a67a9fde5c95b5b31ee489f2 100644 --- a/styles/src/themes/solarized.ts +++ b/styles/src/themes/solarized.ts @@ -1,5 +1,5 @@ import chroma from "chroma-js"; -import { colorRamp, createTheme } from "./base16"; +import { colorRamp, createTheme } from "./common/base16"; const name = "solarized"; diff --git a/styles/src/themes/sulphurpool.ts b/styles/src/themes/sulphurpool.ts index 727f1ce38ab90b4267ab7b18e5604d6b929ddde5..202d52bc68532cae2e109cedaf8c0b91c7edfcd3 100644 --- a/styles/src/themes/sulphurpool.ts +++ b/styles/src/themes/sulphurpool.ts @@ -1,5 +1,5 @@ import chroma from "chroma-js"; -import { colorRamp, createTheme } from "./base16"; +import { colorRamp, createTheme } from "./common/base16"; const name = "sulphurpool";