diff --git a/.zed.toml b/.zed.toml new file mode 100644 index 0000000000000000000000000000000000000000..6e8c8fe4282e941901c1c4aaaa30b2569be7c173 --- /dev/null +++ b/.zed.toml @@ -0,0 +1 @@ +collaborators = ["nathansobo", "as-cii", "maxbrunsfeld", "iamnbutler"] diff --git a/gpui/src/color.rs b/gpui/src/color.rs index 9e31530b27f8b9f93c42df203aa88f99d9b9eff0..5adf03daef73e3a9449255bb425d197427ebfba5 100644 --- a/gpui/src/color.rs +++ b/gpui/src/color.rs @@ -33,6 +33,14 @@ impl Color { Self(ColorU::from_u32(0xff0000ff)) } + pub fn green() -> Self { + Self(ColorU::from_u32(0x00ff00ff)) + } + + pub fn blue() -> Self { + Self(ColorU::from_u32(0x0000ffff)) + } + pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { Self(ColorU::new(r, g, b, a)) } diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index 42e9810cfbff21ea9c96017b54689cb5a77f48cd..c8048ef3fa2f002656fb2a6943292e613f655aa0 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -8,7 +8,6 @@ mod flex; mod hook; mod image; mod label; -mod line_box; mod list; mod mouse_event_handler; mod overlay; @@ -19,8 +18,8 @@ mod uniform_list; pub use self::{ align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*, - hook::*, image::*, label::*, line_box::*, list::*, mouse_event_handler::*, overlay::*, - stack::*, svg::*, text::*, uniform_list::*, + hook::*, image::*, label::*, list::*, mouse_event_handler::*, overlay::*, stack::*, svg::*, + text::*, uniform_list::*, }; pub use crate::presenter::ChildView; use crate::{ @@ -109,6 +108,34 @@ pub trait Element { element: Rc::new(RefCell::new(Lifecycle::Init { element: self })), }) } + + fn constrained(self) -> ConstrainedBox + where + Self: 'static + Sized, + { + ConstrainedBox::new(self.boxed()) + } + + fn aligned(self) -> Align + where + Self: 'static + Sized, + { + Align::new(self.boxed()) + } + + fn contained(self) -> Container + where + Self: 'static + Sized, + { + Container::new(self.boxed()) + } + + fn expanded(self, flex: f32) -> Expanded + where + Self: 'static + Sized, + { + Expanded::new(flex, self.boxed()) + } } pub enum Lifecycle { diff --git a/gpui/src/elements/align.rs b/gpui/src/elements/align.rs index 652a014ddad275faa7e509be73f6bfe98099e096..7f065e2b5357856a96d515224679d1d9f4c46525 100644 --- a/gpui/src/elements/align.rs +++ b/gpui/src/elements/align.rs @@ -25,6 +25,11 @@ impl Align { self } + pub fn left(mut self) -> Self { + self.alignment.set_x(-1.0); + self + } + pub fn right(mut self) -> Self { self.alignment.set_x(1.0); self diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index abbafe4d034e76926ff79f794a286a24a727d927..dd58cf07398ed001f2b05e28c9a7398a4a581c4a 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -57,6 +57,11 @@ impl Container { self } + pub fn with_margin_right(mut self, margin: f32) -> Self { + self.style.margin.right = margin; + self + } + pub fn with_horizontal_padding(mut self, padding: f32) -> Self { self.style.padding.left = padding; self.style.padding.right = padding; diff --git a/gpui/src/elements/image.rs b/gpui/src/elements/image.rs index 421e18ec95ce7bc64df77d675797a10c41ff3541..5d36828d0cd57c1605dd6c7db9246268bb868078 100644 --- a/gpui/src/elements/image.rs +++ b/gpui/src/elements/image.rs @@ -1,6 +1,9 @@ use super::constrain_size_preserving_aspect_ratio; use crate::{ - geometry::{rect::RectF, vector::Vector2F}, + geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, + }, json::{json, ToJson}, scene, Border, DebugContext, Element, Event, EventContext, ImageData, LayoutContext, PaintContext, SizeConstraint, @@ -16,9 +19,13 @@ pub struct Image { #[derive(Copy, Clone, Default, Deserialize)] pub struct ImageStyle { #[serde(default)] - border: Border, + pub border: Border, #[serde(default)] - corner_radius: f32, + pub corner_radius: f32, + #[serde(default)] + pub height: Option, + #[serde(default)] + pub width: Option, } impl Image { @@ -44,8 +51,14 @@ impl Element for Image { constraint: SizeConstraint, _: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - let size = - constrain_size_preserving_aspect_ratio(constraint.max, self.data.size().to_f32()); + let desired_size = vec2f( + self.style.width.unwrap_or(constraint.max.x()), + self.style.height.unwrap_or(constraint.max.y()), + ); + let size = constrain_size_preserving_aspect_ratio( + constraint.constrain(desired_size), + self.data.size().to_f32(), + ); (size, ()) } diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index acfbb5abd9e7dfa9c33004bc34522b16a7bb59a7..c1e048eb93e0c945db450f2112e7b20a1d735cda 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -137,8 +137,7 @@ impl Element for Label { let size = vec2f( line.width().max(constraint.min.x()).min(constraint.max.x()), cx.font_cache - .line_height(self.style.text.font_id, self.style.text.font_size) - .ceil(), + .line_height(self.style.text.font_id, self.style.text.font_size), ); (size, line) diff --git a/gpui/src/elements/line_box.rs b/gpui/src/elements/line_box.rs deleted file mode 100644 index 33fd2510c8869fce725cf4ab7482e5468b142c9a..0000000000000000000000000000000000000000 --- a/gpui/src/elements/line_box.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::{ - fonts::TextStyle, - geometry::{ - rect::RectF, - vector::{vec2f, Vector2F}, - }, - json::{json, ToJson}, - DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, - SizeConstraint, -}; - -pub struct LineBox { - child: ElementBox, - style: TextStyle, -} - -impl LineBox { - pub fn new(child: ElementBox, style: TextStyle) -> Self { - Self { child, style } - } -} - -impl Element for LineBox { - type LayoutState = f32; - type PaintState = (); - - fn layout( - &mut self, - constraint: SizeConstraint, - cx: &mut LayoutContext, - ) -> (Vector2F, Self::LayoutState) { - let line_height = cx - .font_cache - .line_height(self.style.font_id, self.style.font_size); - let character_height = cx - .font_cache - .ascent(self.style.font_id, self.style.font_size) - + cx.font_cache - .descent(self.style.font_id, self.style.font_size); - let child_max = vec2f(constraint.max.x(), character_height); - let child_size = self.child.layout( - SizeConstraint::new(constraint.min.min(child_max), child_max), - cx, - ); - let size = vec2f(child_size.x(), line_height); - (size, (line_height - character_height) / 2.) - } - - fn paint( - &mut self, - bounds: RectF, - visible_bounds: RectF, - padding_top: &mut f32, - cx: &mut PaintContext, - ) -> Self::PaintState { - self.child.paint( - bounds.origin() + vec2f(0., *padding_top), - visible_bounds, - cx, - ); - } - - fn dispatch_event( - &mut self, - event: &Event, - _: RectF, - _: &mut Self::LayoutState, - _: &mut Self::PaintState, - cx: &mut EventContext, - ) -> bool { - self.child.dispatch_event(event, cx) - } - - fn debug( - &self, - bounds: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - cx: &DebugContext, - ) -> serde_json::Value { - json!({ - "bounds": bounds.to_json(), - "style": self.style.to_json(), - "child": self.child.debug(cx), - }) - } -} diff --git a/gpui/src/elements/list.rs b/gpui/src/elements/list.rs index 3864bf3c80daf4f9e2a6c119351838c2aabb2bb3..9fecfd6e61ed06d9859c6b62b3645866b4ce39b6 100644 --- a/gpui/src/elements/list.rs +++ b/gpui/src/elements/list.rs @@ -12,6 +12,7 @@ use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc}; pub struct List { state: ListState, + invalidated_elements: Vec, } #[derive(Clone)] @@ -79,7 +80,10 @@ struct Height(f32); impl List { pub fn new(state: ListState) -> Self { - Self { state } + Self { + state, + invalidated_elements: Default::default(), + } } } @@ -258,10 +262,35 @@ impl Element for List { let mut handled = false; let mut state = self.state.0.borrow_mut(); - for (mut element, _) in state.visible_elements(bounds, scroll_top) { - handled = element.dispatch_event(event, cx) || handled; + let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item); + let mut cursor = state.items.cursor::(); + let mut new_items = cursor.slice(&Count(scroll_top.item_ix), Bias::Right, &()); + while let Some(item) = cursor.item() { + if item_origin.y() > bounds.max_y() { + break; + } + + if let ListItem::Rendered(element) = item { + let prev_notify_count = cx.notify_count(); + let mut element = element.clone(); + handled = element.dispatch_event(event, cx) || handled; + item_origin.set_y(item_origin.y() + element.size().y()); + if cx.notify_count() > prev_notify_count { + new_items.push(ListItem::Unrendered, &()); + self.invalidated_elements.push(element); + } else { + new_items.push(item.clone(), &()); + } + cursor.next(&()); + } else { + unreachable!(); + } } + new_items.push_tree(cursor.suffix(&()), &()); + drop(cursor); + state.items = new_items; + match event { Event::ScrollWheel { position, diff --git a/gpui/src/font_cache.rs b/gpui/src/font_cache.rs index c0255a7af5f251b9828e4788dba6443f30efcc5b..0509ecd437d134bd05f3f3c22cf9063286ae1140 100644 --- a/gpui/src/font_cache.rs +++ b/gpui/src/font_cache.rs @@ -166,6 +166,10 @@ impl FontCache { self.metric(font_id, |m| m.cap_height) * self.em_scale(font_id, font_size) } + pub fn x_height(&self, font_id: FontId, font_size: f32) -> f32 { + self.metric(font_id, |m| m.x_height) * self.em_scale(font_id, font_size) + } + pub fn ascent(&self, font_id: FontId, font_size: f32) -> f32 { self.metric(font_id, |m| m.ascent) * self.em_scale(font_id, font_size) } @@ -178,6 +182,14 @@ impl FontCache { font_size / self.metric(font_id, |m| m.units_per_em as f32) } + pub fn baseline_offset(&self, font_id: FontId, font_size: f32) -> f32 { + let line_height = self.line_height(font_id, font_size); + let ascent = self.ascent(font_id, font_size); + let descent = self.descent(font_id, font_size); + let padding_top = (line_height - ascent - descent) / 2.; + padding_top + ascent + } + pub fn line_wrapper(self: &Arc, font_id: FontId, font_size: f32) -> LineWrapperHandle { let mut state = self.0.write(); let wrappers = state diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index 3ec8aad9626bf78e4beb79709b89e525be679ad9..f2bbd04477bdfb630196d06abf23fa258fce2951 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -132,6 +132,14 @@ impl TextStyle { font_cache.line_height(self.font_id, self.font_size) } + pub fn cap_height(&self, font_cache: &FontCache) -> f32 { + font_cache.cap_height(self.font_id, self.font_size) + } + + pub fn x_height(&self, font_cache: &FontCache) -> f32 { + font_cache.x_height(self.font_id, self.font_size) + } + pub fn em_width(&self, font_cache: &FontCache) -> f32 { font_cache.em_width(self.font_id, self.font_size) } @@ -140,6 +148,10 @@ impl TextStyle { font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache) } + pub fn baseline_offset(&self, font_cache: &FontCache) -> f32 { + font_cache.baseline_offset(self.font_id, self.font_size) + } + fn em_scale(&self, font_cache: &FontCache) -> f32 { font_cache.em_scale(self.font_id, self.font_size) } diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 2062397e9e6547dbe4ea9083485a8b8c0a519475..354f0a0f821af81040581a7afedb2eb4652acbc2 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -195,6 +195,7 @@ impl Presenter { text_layout_cache: &self.text_layout_cache, view_stack: Default::default(), invalidated_views: Default::default(), + notify_count: 0, app: cx, } } @@ -300,6 +301,7 @@ pub struct EventContext<'a> { pub font_cache: &'a FontCache, pub text_layout_cache: &'a TextLayoutCache, pub app: &'a mut MutableAppContext, + pub notify_count: usize, view_stack: Vec, invalidated_views: HashSet, } @@ -325,10 +327,15 @@ impl<'a> EventContext<'a> { } pub fn notify(&mut self) { + self.notify_count += 1; if let Some(view_id) = self.view_stack.last() { self.invalidated_views.insert(*view_id); } } + + pub fn notify_count(&self) -> usize { + self.notify_count + } } impl<'a> Deref for EventContext<'a> { @@ -432,6 +439,13 @@ impl SizeConstraint { Axis::Vertical => self.min.y(), } } + + pub fn constrain(&self, size: Vector2F) -> Vector2F { + vec2f( + size.x().min(self.max.x()).max(self.min.x()), + size.y().min(self.max.y()).max(self.min.y()), + ) + } } impl ToJson for SizeConstraint { diff --git a/server/src/auth.rs b/server/src/auth.rs index 1f6ec5f1db176638ffc52106d129cc6793f75c6e..e60802285ec602a058fb0c74c2b4038603d6312a 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -18,7 +18,7 @@ use scrypt::{ use serde::{Deserialize, Serialize}; use std::{borrow::Cow, convert::TryFrom, sync::Arc}; use surf::{StatusCode, Url}; -use tide::Server; +use tide::{log, Server}; use zrpc::auth as zed_auth; static CURRENT_GITHUB_USER: &'static str = "current_github_user"; @@ -121,6 +121,7 @@ pub fn add_routes(app: &mut Server>) { struct NativeAppSignInParams { native_app_port: String, native_app_public_key: String, + impersonate: Option, } async fn get_sign_in(mut request: Request) -> tide::Result { @@ -142,11 +143,15 @@ async fn get_sign_in(mut request: Request) -> tide::Result { let app_sign_in_params: Option = request.query().ok(); if let Some(query) = app_sign_in_params { - redirect_url - .query_pairs_mut() + let mut redirect_query = redirect_url.query_pairs_mut(); + redirect_query .clear() .append_pair("native_app_port", &query.native_app_port) .append_pair("native_app_public_key", &query.native_app_public_key); + + if let Some(impersonate) = &query.impersonate { + redirect_query.append_pair("impersonate", impersonate); + } } let (auth_url, csrf_token) = request @@ -222,7 +227,20 @@ async fn get_auth_callback(mut request: Request) -> tide::Result { // When signing in from the native app, generate a new access token for the current user. Return // a redirect so that the user's browser sends this access token to the locally-running app. if let Some((user, app_sign_in_params)) = user.zip(query.native_app_sign_in_params) { - let access_token = create_access_token(request.db(), user.id).await?; + let mut user_id = user.id; + if let Some(impersonated_login) = app_sign_in_params.impersonate { + log::info!("attempting to impersonate user @{}", impersonated_login); + if let Some(user) = request.db().get_users_by_ids([user_id]).await?.first() { + if user.admin { + user_id = request.db().create_user(&impersonated_login, false).await?; + log::info!("impersonating user {}", user_id.0); + } else { + log::info!("refusing to impersonate user"); + } + } + } + + let access_token = create_access_token(request.db(), user_id).await?; let native_app_public_key = zed_auth::PublicKey::try_from(app_sign_in_params.native_app_public_key.clone()) .context("failed to parse app public key")?; @@ -232,7 +250,7 @@ async fn get_auth_callback(mut request: Request) -> tide::Result { return Ok(tide::Redirect::new(&format!( "http://127.0.0.1:{}?user_id={}&access_token={}", - app_sign_in_params.native_app_port, user.id.0, encrypted_access_token, + app_sign_in_params.native_app_port, user_id.0, encrypted_access_token, )) .into()); } diff --git a/server/src/bin/seed.rs b/server/src/bin/seed.rs index d2427d495c451497df0644dc0fc4d36e7ecaa4ea..4d3fb978dbd7db0a08c1c84fa816dece3d6f86ff 100644 --- a/server/src/bin/seed.rs +++ b/server/src/bin/seed.rs @@ -27,8 +27,12 @@ async fn main() { let zed_users = ["nathansobo", "maxbrunsfeld", "as-cii", "iamnbutler"]; let mut zed_user_ids = Vec::::new(); for zed_user in zed_users { - if let Some(user_id) = db.get_user(zed_user).await.expect("failed to fetch user") { - zed_user_ids.push(user_id); + if let Some(user) = db + .get_user_by_github_login(zed_user) + .await + .expect("failed to fetch user") + { + zed_user_ids.push(user.id); } else { zed_user_ids.push( db.create_user(zed_user, true) diff --git a/server/src/db.rs b/server/src/db.rs index 14ad85b68af2e06148c02d12dc74790fa2b5b0c9..002b82741c6de924cf9319753b3be31730613c5c 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -84,27 +84,12 @@ impl Db { // users - #[allow(unused)] // Help rust-analyzer - #[cfg(any(test, feature = "seed-support"))] - pub async fn get_user(&self, github_login: &str) -> Result> { - test_support!(self, { - let query = " - SELECT id - FROM users - WHERE github_login = $1 - "; - sqlx::query_scalar(query) - .bind(github_login) - .fetch_optional(&self.pool) - .await - }) - } - pub async fn create_user(&self, github_login: &str, admin: bool) -> Result { test_support!(self, { let query = " INSERT INTO users (github_login, admin) VALUES ($1, $2) + ON CONFLICT (github_login) DO UPDATE SET github_login = excluded.github_login RETURNING id "; sqlx::query_scalar(query) @@ -125,51 +110,17 @@ impl Db { pub async fn get_users_by_ids( &self, - requester_id: UserId, - ids: impl Iterator, + ids: impl IntoIterator, ) -> Result> { - let mut include_requester = false; - let ids = ids - .map(|id| { - if id == requester_id { - include_requester = true; - } - id.0 - }) - .collect::>(); - + let ids = ids.into_iter().map(|id| id.0).collect::>(); test_support!(self, { - // Only return users that are in a common channel with the requesting user. - // Also allow the requesting user to return their own data, even if they aren't - // in any channels. let query = " - SELECT - users.* - FROM - users, channel_memberships - WHERE - users.id = ANY ($1) AND - channel_memberships.user_id = users.id AND - channel_memberships.channel_id IN ( - SELECT channel_id - FROM channel_memberships - WHERE channel_memberships.user_id = $2 - ) - UNION - SELECT - users.* - FROM - users - WHERE - $3 AND users.id = $2 + SELECT users.* + FROM users + WHERE users.id = ANY ($1) "; - sqlx::query_as(query) - .bind(&ids) - .bind(requester_id) - .bind(include_requester) - .fetch_all(&self.pool) - .await + sqlx::query_as(query).bind(&ids).fetch_all(&self.pool).await }) } @@ -597,45 +548,11 @@ pub mod tests { let friend1 = db.create_user("friend-1", false).await.unwrap(); let friend2 = db.create_user("friend-2", false).await.unwrap(); let friend3 = db.create_user("friend-3", false).await.unwrap(); - let stranger = db.create_user("stranger", false).await.unwrap(); - // A user can read their own info, even if they aren't in any channels. assert_eq!( - db.get_users_by_ids( - user, - [user, friend1, friend2, friend3, stranger].iter().copied() - ) - .await - .unwrap(), - vec![User { - id: user, - github_login: "user".to_string(), - admin: false, - },], - ); - - // A user can read the info of any other user who is in a shared channel - // with them. - let org = db.create_org("test org", "test-org").await.unwrap(); - let chan1 = db.create_org_channel(org, "channel-1").await.unwrap(); - let chan2 = db.create_org_channel(org, "channel-2").await.unwrap(); - let chan3 = db.create_org_channel(org, "channel-3").await.unwrap(); - - db.add_channel_member(chan1, user, false).await.unwrap(); - db.add_channel_member(chan2, user, false).await.unwrap(); - db.add_channel_member(chan1, friend1, false).await.unwrap(); - db.add_channel_member(chan1, friend2, false).await.unwrap(); - db.add_channel_member(chan2, friend2, false).await.unwrap(); - db.add_channel_member(chan2, friend3, false).await.unwrap(); - db.add_channel_member(chan3, stranger, false).await.unwrap(); - - assert_eq!( - db.get_users_by_ids( - user, - [user, friend1, friend2, friend3, stranger].iter().copied() - ) - .await - .unwrap(), + db.get_users_by_ids([user, friend1, friend2, friend3]) + .await + .unwrap(), vec![ User { id: user, @@ -659,18 +576,6 @@ pub mod tests { } ] ); - - // The user's own info is only returned if they request it. - assert_eq!( - db.get_users_by_ids(user, [friend1].iter().copied()) - .await - .unwrap(), - vec![User { - id: friend1, - github_login: "friend-1".to_string(), - admin: false, - },] - ) } #[gpui::test] diff --git a/server/src/rpc.rs b/server/src/rpc.rs index 698414851e60fadd301b60cfc19ac31ad425ef6f..fec6182fcc1ff02cf696fed5cfcba32f41564af4 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1,3 +1,5 @@ +mod store; + use super::{ auth, db::{ChannelId, MessageId, UserId}, @@ -11,12 +13,13 @@ use postage::{mpsc, prelude::Sink as _, prelude::Stream as _}; use sha1::{Digest as _, Sha1}; use std::{ any::TypeId, - collections::{hash_map, HashMap, HashSet}, + collections::{HashMap, HashSet}, future::Future, mem, sync::Arc, time::Instant, }; +use store::{JoinedWorktree, Store, Worktree}; use surf::StatusCode; use tide::log; use tide::{ @@ -25,13 +28,10 @@ use tide::{ }; use time::OffsetDateTime; use zrpc::{ - auth::random_token, proto::{self, AnyTypedEnvelope, EnvelopedMessage}, Connection, ConnectionId, Peer, TypedEnvelope, }; -type ReplicaId = u16; - type MessageHandler = Box< dyn Send + Sync @@ -40,40 +40,12 @@ type MessageHandler = Box< pub struct Server { peer: Arc, - state: RwLock, + store: RwLock, app_state: Arc, handlers: HashMap, notifications: Option>, } -#[derive(Default)] -struct ServerState { - connections: HashMap, - pub worktrees: HashMap, - channels: HashMap, - next_worktree_id: u64, -} - -struct ConnectionState { - user_id: UserId, - worktrees: HashSet, - channels: HashSet, -} - -struct Worktree { - host_connection_id: Option, - guest_connection_ids: HashMap, - active_replica_ids: HashSet, - access_token: String, - root_name: String, - entries: HashMap, -} - -#[derive(Default)] -struct Channel { - connection_ids: HashSet, -} - const MESSAGE_COUNT_PER_PAGE: usize = 100; const MAX_MESSAGE_LEN: usize = 1024; @@ -86,17 +58,20 @@ impl Server { let mut server = Self { peer, app_state, - state: Default::default(), + store: Default::default(), handlers: Default::default(), notifications, }; server .add_handler(Server::ping) + .add_handler(Server::open_worktree) + .add_handler(Server::close_worktree) .add_handler(Server::share_worktree) + .add_handler(Server::unshare_worktree) .add_handler(Server::join_worktree) + .add_handler(Server::leave_worktree) .add_handler(Server::update_worktree) - .add_handler(Server::close_worktree) .add_handler(Server::open_buffer) .add_handler(Server::close_buffer) .add_handler(Server::update_buffer) @@ -137,11 +112,16 @@ impl Server { addr: String, user_id: UserId, ) -> impl Future { - let this = self.clone(); + let mut this = self.clone(); async move { let (connection_id, handle_io, mut incoming_rx) = this.peer.add_connection(connection).await; - this.add_connection(connection_id, user_id).await; + this.state_mut() + .await + .add_connection(connection_id, user_id); + if let Err(err) = this.update_collaborators_for_users(&[user_id]).await { + log::error!("error updating collaborators for {:?}: {}", user_id, err); + } let handle_io = handle_io.fuse(); futures::pin_mut!(handle_io); @@ -186,78 +166,122 @@ impl Server { } } - async fn sign_out(self: &Arc, connection_id: zrpc::ConnectionId) -> tide::Result<()> { + async fn sign_out(self: &mut Arc, connection_id: ConnectionId) -> tide::Result<()> { self.peer.disconnect(connection_id).await; - let worktree_ids = self.remove_connection(connection_id).await; - for worktree_id in worktree_ids { - let state = self.state.read().await; - if let Some(worktree) = state.worktrees.get(&worktree_id) { - broadcast(connection_id, worktree.connection_ids(), |conn_id| { - self.peer.send( - conn_id, - proto::RemovePeer { - worktree_id, - peer_id: connection_id.0, - }, - ) - }) + let removed_connection = self.state_mut().await.remove_connection(connection_id)?; + + for (worktree_id, worktree) in removed_connection.hosted_worktrees { + if let Some(share) = worktree.share { + broadcast( + connection_id, + share.guest_connection_ids.keys().copied().collect(), + |conn_id| { + self.peer + .send(conn_id, proto::UnshareWorktree { worktree_id }) + }, + ) .await?; } } + + for (worktree_id, peer_ids) in removed_connection.guest_worktree_ids { + broadcast(connection_id, peer_ids, |conn_id| { + self.peer.send( + conn_id, + proto::RemovePeer { + worktree_id, + peer_id: connection_id.0, + }, + ) + }) + .await?; + } + + self.update_collaborators_for_users(removed_connection.collaborator_ids.iter()) + .await?; + Ok(()) } - // Add a new connection associated with a given user. - async fn add_connection(&self, connection_id: ConnectionId, user_id: UserId) { - self.state.write().await.connections.insert( - connection_id, - ConnectionState { - user_id, - worktrees: Default::default(), - channels: Default::default(), - }, - ); + async fn ping(self: Arc, request: TypedEnvelope) -> tide::Result<()> { + self.peer.respond(request.receipt(), proto::Ack {}).await?; + Ok(()) } - // Remove the given connection and its association with any worktrees. - async fn remove_connection(&self, connection_id: ConnectionId) -> Vec { - let mut worktree_ids = Vec::new(); - let mut state = self.state.write().await; - if let Some(connection) = state.connections.remove(&connection_id) { - for channel_id in connection.channels { - if let Some(channel) = state.channels.get_mut(&channel_id) { - channel.connection_ids.remove(&connection_id); + async fn open_worktree( + mut self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let receipt = request.receipt(); + let host_user_id = self + .state() + .await + .user_id_for_connection(request.sender_id)?; + + let mut collaborator_user_ids = HashSet::new(); + collaborator_user_ids.insert(host_user_id); + for github_login in request.payload.collaborator_logins { + match self.app_state.db.create_user(&github_login, false).await { + Ok(collaborator_user_id) => { + collaborator_user_ids.insert(collaborator_user_id); } - } - for worktree_id in connection.worktrees { - if let Some(worktree) = state.worktrees.get_mut(&worktree_id) { - if worktree.host_connection_id == Some(connection_id) { - worktree_ids.push(worktree_id); - } else if let Some(replica_id) = - worktree.guest_connection_ids.remove(&connection_id) - { - worktree.active_replica_ids.remove(&replica_id); - worktree_ids.push(worktree_id); - } + Err(err) => { + let message = err.to_string(); + self.peer + .respond_with_error(receipt, proto::Error { message }) + .await?; + return Ok(()); } } } - worktree_ids + + let collaborator_user_ids = collaborator_user_ids.into_iter().collect::>(); + let worktree_id = self.state_mut().await.add_worktree(Worktree { + host_connection_id: request.sender_id, + collaborator_user_ids: collaborator_user_ids.clone(), + root_name: request.payload.root_name, + share: None, + }); + + self.peer + .respond(receipt, proto::OpenWorktreeResponse { worktree_id }) + .await?; + self.update_collaborators_for_users(&collaborator_user_ids) + .await?; + + Ok(()) } - async fn ping(self: Arc, request: TypedEnvelope) -> tide::Result<()> { - self.peer.respond(request.receipt(), proto::Ack {}).await?; + async fn close_worktree( + mut self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let worktree_id = request.payload.worktree_id; + let worktree = self + .state_mut() + .await + .remove_worktree(worktree_id, request.sender_id)?; + + if let Some(share) = worktree.share { + broadcast( + request.sender_id, + share.guest_connection_ids.keys().copied().collect(), + |conn_id| { + self.peer + .send(conn_id, proto::UnshareWorktree { worktree_id }) + }, + ) + .await?; + } + self.update_collaborators_for_users(&worktree.collaborator_user_ids) + .await?; Ok(()) } async fn share_worktree( - self: Arc, + mut self: Arc, mut request: TypedEnvelope, ) -> tide::Result<()> { - let mut state = self.state.write().await; - let worktree_id = state.next_worktree_id; - state.next_worktree_id += 1; - let access_token = random_token(); let worktree = request .payload .worktree @@ -267,148 +291,169 @@ impl Server { .into_iter() .map(|entry| (entry.id, entry)) .collect(); - state.worktrees.insert( - worktree_id, - Worktree { - host_connection_id: Some(request.sender_id), - guest_connection_ids: Default::default(), - active_replica_ids: Default::default(), - access_token: access_token.clone(), - root_name: mem::take(&mut worktree.root_name), - entries, - }, - ); - self.peer - .respond( - request.receipt(), - proto::ShareWorktreeResponse { - worktree_id, - access_token, - }, - ) + let collaborator_user_ids = + self.state_mut() + .await + .share_worktree(worktree.id, request.sender_id, entries); + if let Some(collaborator_user_ids) = collaborator_user_ids { + self.peer + .respond(request.receipt(), proto::ShareWorktreeResponse {}) + .await?; + self.update_collaborators_for_users(&collaborator_user_ids) + .await?; + } else { + self.peer + .respond_with_error( + request.receipt(), + proto::Error { + message: "no such worktree".to_string(), + }, + ) + .await?; + } + Ok(()) + } + + async fn unshare_worktree( + mut self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let worktree_id = request.payload.worktree_id; + let worktree = self + .state_mut() + .await + .unshare_worktree(worktree_id, request.sender_id)?; + + broadcast(request.sender_id, worktree.connection_ids, |conn_id| { + self.peer + .send(conn_id, proto::UnshareWorktree { worktree_id }) + }) + .await?; + self.update_collaborators_for_users(&worktree.collaborator_ids) .await?; + Ok(()) } async fn join_worktree( - self: Arc, - request: TypedEnvelope, + mut self: Arc, + request: TypedEnvelope, ) -> tide::Result<()> { let worktree_id = request.payload.worktree_id; - let access_token = &request.payload.access_token; + let user_id = self + .state() + .await + .user_id_for_connection(request.sender_id)?; - let mut state = self.state.write().await; - if let Some((peer_replica_id, worktree)) = - state.join_worktree(request.sender_id, worktree_id, access_token) - { - let mut peers = Vec::new(); - if let Some(host_connection_id) = worktree.host_connection_id { + let mut state = self.state_mut().await; + match state.join_worktree(request.sender_id, user_id, worktree_id) { + Ok(JoinedWorktree { + replica_id, + worktree, + }) => { + let share = worktree.share()?; + let peer_count = share.guest_connection_ids.len(); + let mut peers = Vec::with_capacity(peer_count); peers.push(proto::Peer { - peer_id: host_connection_id.0, + peer_id: worktree.host_connection_id.0, replica_id: 0, }); - } - for (peer_conn_id, peer_replica_id) in &worktree.guest_connection_ids { - if *peer_conn_id != request.sender_id { - peers.push(proto::Peer { - peer_id: peer_conn_id.0, - replica_id: *peer_replica_id as u32, - }); + for (peer_conn_id, peer_replica_id) in &share.guest_connection_ids { + if *peer_conn_id != request.sender_id { + peers.push(proto::Peer { + peer_id: peer_conn_id.0, + replica_id: *peer_replica_id as u32, + }); + } } + let response = proto::JoinWorktreeResponse { + worktree: Some(proto::Worktree { + id: worktree_id, + root_name: worktree.root_name.clone(), + entries: share.entries.values().cloned().collect(), + }), + replica_id: replica_id as u32, + peers, + }; + let connection_ids = worktree.connection_ids(); + let collaborator_user_ids = worktree.collaborator_user_ids.clone(); + drop(state); + + broadcast(request.sender_id, connection_ids, |conn_id| { + self.peer.send( + conn_id, + proto::AddPeer { + worktree_id, + peer: Some(proto::Peer { + peer_id: request.sender_id.0, + replica_id: response.replica_id, + }), + }, + ) + }) + .await?; + self.peer.respond(request.receipt(), response).await?; + self.update_collaborators_for_users(&collaborator_user_ids) + .await?; + } + Err(error) => { + drop(state); + self.peer + .respond_with_error( + request.receipt(), + proto::Error { + message: error.to_string(), + }, + ) + .await?; } + } - broadcast(request.sender_id, worktree.connection_ids(), |conn_id| { + Ok(()) + } + + async fn leave_worktree( + mut self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let sender_id = request.sender_id; + let worktree_id = request.payload.worktree_id; + let worktree = self + .state_mut() + .await + .leave_worktree(sender_id, worktree_id); + if let Some(worktree) = worktree { + broadcast(sender_id, worktree.connection_ids, |conn_id| { self.peer.send( conn_id, - proto::AddPeer { + proto::RemovePeer { worktree_id, - peer: Some(proto::Peer { - peer_id: request.sender_id.0, - replica_id: peer_replica_id as u32, - }), + peer_id: sender_id.0, }, ) }) .await?; - self.peer - .respond( - request.receipt(), - proto::OpenWorktreeResponse { - worktree_id, - worktree: Some(proto::Worktree { - root_name: worktree.root_name.clone(), - entries: worktree.entries.values().cloned().collect(), - }), - replica_id: peer_replica_id as u32, - peers, - }, - ) - .await?; - } else { - self.peer - .respond( - request.receipt(), - proto::OpenWorktreeResponse { - worktree_id, - worktree: None, - replica_id: 0, - peers: Vec::new(), - }, - ) + self.update_collaborators_for_users(&worktree.collaborator_ids) .await?; } - Ok(()) } async fn update_worktree( - self: Arc, + mut self: Arc, request: TypedEnvelope, ) -> tide::Result<()> { - { - let mut state = self.state.write().await; - let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; - for entry_id in &request.payload.removed_entries { - worktree.entries.remove(&entry_id); - } - - for entry in &request.payload.updated_entries { - worktree.entries.insert(entry.id, entry.clone()); - } - } - - self.broadcast_in_worktree(request.payload.worktree_id, &request) - .await?; - Ok(()) - } - - async fn close_worktree( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result<()> { - let connection_ids; - { - let mut state = self.state.write().await; - let worktree = state.write_worktree(request.payload.worktree_id, request.sender_id)?; - connection_ids = worktree.connection_ids(); - if worktree.host_connection_id == Some(request.sender_id) { - worktree.host_connection_id = None; - } else if let Some(replica_id) = - worktree.guest_connection_ids.remove(&request.sender_id) - { - worktree.active_replica_ids.remove(&replica_id); - } - } - - broadcast(request.sender_id, connection_ids, |conn_id| { - self.peer.send( - conn_id, - proto::RemovePeer { - worktree_id: request.payload.worktree_id, - peer_id: request.sender_id.0, - }, - ) + let connection_ids = self.state_mut().await.update_worktree( + request.sender_id, + request.payload.worktree_id, + &request.payload.removed_entries, + &request.payload.updated_entries, + )?; + + broadcast(request.sender_id, connection_ids, |connection_id| { + self.peer + .forward_send(request.sender_id, connection_id, request.payload.clone()) }) .await?; @@ -420,14 +465,10 @@ impl Server { request: TypedEnvelope, ) -> tide::Result<()> { let receipt = request.receipt(); - let worktree_id = request.payload.worktree_id; let host_connection_id = self - .state - .read() + .state() .await - .read_worktree(worktree_id, request.sender_id)? - .host_connection_id()?; - + .worktree_host_connection_id(request.sender_id, request.payload.worktree_id)?; let response = self .peer .forward_request(request.sender_id, host_connection_id, request.payload) @@ -441,16 +482,12 @@ impl Server { request: TypedEnvelope, ) -> tide::Result<()> { let host_connection_id = self - .state - .read() + .state() .await - .read_worktree(request.payload.worktree_id, request.sender_id)? - .host_connection_id()?; - + .worktree_host_connection_id(request.sender_id, request.payload.worktree_id)?; self.peer .forward_send(request.sender_id, host_connection_id, request.payload) .await?; - Ok(()) } @@ -461,14 +498,11 @@ impl Server { let host; let guests; { - let state = self.state.read().await; - let worktree = state.read_worktree(request.payload.worktree_id, request.sender_id)?; - host = worktree.host_connection_id()?; - guests = worktree - .guest_connection_ids - .keys() - .copied() - .collect::>(); + let state = self.state().await; + host = state + .worktree_host_connection_id(request.sender_id, request.payload.worktree_id)?; + guests = state + .worktree_guest_connection_ids(request.sender_id, request.payload.worktree_id)?; } let sender = request.sender_id; @@ -498,8 +532,17 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> tide::Result<()> { - self.broadcast_in_worktree(request.payload.worktree_id, &request) - .await?; + broadcast( + request.sender_id, + self.state() + .await + .worktree_connection_ids(request.sender_id, request.payload.worktree_id)?, + |connection_id| { + self.peer + .forward_send(request.sender_id, connection_id, request.payload.clone()) + }, + ) + .await?; self.peer.respond(request.receipt(), proto::Ack {}).await?; Ok(()) } @@ -508,8 +551,19 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> tide::Result<()> { - self.broadcast_in_worktree(request.payload.worktree_id, &request) - .await + broadcast( + request.sender_id, + self.store + .read() + .await + .worktree_connection_ids(request.sender_id, request.payload.worktree_id)?, + |connection_id| { + self.peer + .forward_send(request.sender_id, connection_id, request.payload.clone()) + }, + ) + .await?; + Ok(()) } async fn get_channels( @@ -517,8 +571,7 @@ impl Server { request: TypedEnvelope, ) -> tide::Result<()> { let user_id = self - .state - .read() + .state() .await .user_id_for_connection(request.sender_id)?; let channels = self.app_state.db.get_accessible_channels(user_id).await?; @@ -543,17 +596,12 @@ impl Server { self: Arc, request: TypedEnvelope, ) -> tide::Result<()> { - let user_id = self - .state - .read() - .await - .user_id_for_connection(request.sender_id)?; let receipt = request.receipt(); let user_ids = request.payload.user_ids.into_iter().map(UserId::from_proto); let users = self .app_state .db - .get_users_by_ids(user_id, user_ids) + .get_users_by_ids(user_ids) .await? .into_iter() .map(|user| proto::User { @@ -568,13 +616,37 @@ impl Server { Ok(()) } + async fn update_collaborators_for_users<'a>( + self: &Arc, + user_ids: impl IntoIterator, + ) -> tide::Result<()> { + let mut send_futures = Vec::new(); + + let state = self.state().await; + for user_id in user_ids { + let collaborators = state.collaborators_for_user(*user_id); + for connection_id in state.connection_ids_for_user(*user_id) { + send_futures.push(self.peer.send( + connection_id, + proto::UpdateCollaborators { + collaborators: collaborators.clone(), + }, + )); + } + } + + drop(state); + futures::future::try_join_all(send_futures).await?; + + Ok(()) + } + async fn join_channel( - self: Arc, + mut self: Arc, request: TypedEnvelope, ) -> tide::Result<()> { let user_id = self - .state - .read() + .state() .await .user_id_for_connection(request.sender_id)?; let channel_id = ChannelId::from_proto(request.payload.channel_id); @@ -587,8 +659,7 @@ impl Server { Err(anyhow!("access denied"))?; } - self.state - .write() + self.state_mut() .await .join_channel(request.sender_id, channel_id); let messages = self @@ -618,12 +689,11 @@ impl Server { } async fn leave_channel( - self: Arc, + mut self: Arc, request: TypedEnvelope, ) -> tide::Result<()> { let user_id = self - .state - .read() + .state() .await .user_id_for_connection(request.sender_id)?; let channel_id = ChannelId::from_proto(request.payload.channel_id); @@ -636,8 +706,7 @@ impl Server { Err(anyhow!("access denied"))?; } - self.state - .write() + self.state_mut() .await .leave_channel(request.sender_id, channel_id); @@ -653,10 +722,10 @@ impl Server { let user_id; let connection_ids; { - let state = self.state.read().await; + let state = self.state().await; user_id = state.user_id_for_connection(request.sender_id)?; - if let Some(channel) = state.channels.get(&channel_id) { - connection_ids = channel.connection_ids(); + if let Some(ids) = state.channel_connection_ids(channel_id) { + connection_ids = ids; } else { return Ok(()); } @@ -741,8 +810,7 @@ impl Server { request: TypedEnvelope, ) -> tide::Result<()> { let user_id = self - .state - .read() + .state() .await .user_id_for_connection(request.sender_id)?; let channel_id = ChannelId::from_proto(request.payload.channel_id); @@ -785,25 +853,16 @@ impl Server { Ok(()) } - async fn broadcast_in_worktree( - &self, - worktree_id: u64, - message: &TypedEnvelope, - ) -> tide::Result<()> { - let connection_ids = self - .state - .read() - .await - .read_worktree(worktree_id, message.sender_id)? - .connection_ids(); - - broadcast(message.sender_id, connection_ids, |conn_id| { - self.peer - .forward_send(message.sender_id, conn_id, message.payload.clone()) - }) - .await?; + fn state<'a>( + self: &'a Arc, + ) -> impl Future> { + self.store.read() + } - Ok(()) + fn state_mut<'a>( + self: &'a mut Arc, + ) -> impl Future> { + self.store.write() } } @@ -824,137 +883,6 @@ where Ok(()) } -impl ServerState { - fn join_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) { - if let Some(connection) = self.connections.get_mut(&connection_id) { - connection.channels.insert(channel_id); - self.channels - .entry(channel_id) - .or_default() - .connection_ids - .insert(connection_id); - } - } - - fn leave_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) { - if let Some(connection) = self.connections.get_mut(&connection_id) { - connection.channels.remove(&channel_id); - if let hash_map::Entry::Occupied(mut entry) = self.channels.entry(channel_id) { - entry.get_mut().connection_ids.remove(&connection_id); - if entry.get_mut().connection_ids.is_empty() { - entry.remove(); - } - } - } - } - - fn user_id_for_connection(&self, connection_id: ConnectionId) -> tide::Result { - Ok(self - .connections - .get(&connection_id) - .ok_or_else(|| anyhow!("unknown connection"))? - .user_id) - } - - // Add the given connection as a guest of the given worktree - fn join_worktree( - &mut self, - connection_id: ConnectionId, - worktree_id: u64, - access_token: &str, - ) -> Option<(ReplicaId, &Worktree)> { - if let Some(worktree) = self.worktrees.get_mut(&worktree_id) { - if access_token == worktree.access_token { - if let Some(connection) = self.connections.get_mut(&connection_id) { - connection.worktrees.insert(worktree_id); - } - - let mut replica_id = 1; - while worktree.active_replica_ids.contains(&replica_id) { - replica_id += 1; - } - worktree.active_replica_ids.insert(replica_id); - worktree - .guest_connection_ids - .insert(connection_id, replica_id); - Some((replica_id, worktree)) - } else { - None - } - } else { - None - } - } - - fn read_worktree( - &self, - worktree_id: u64, - connection_id: ConnectionId, - ) -> tide::Result<&Worktree> { - let worktree = self - .worktrees - .get(&worktree_id) - .ok_or_else(|| anyhow!("worktree not found"))?; - - if worktree.host_connection_id == Some(connection_id) - || worktree.guest_connection_ids.contains_key(&connection_id) - { - Ok(worktree) - } else { - Err(anyhow!( - "{} is not a member of worktree {}", - connection_id, - worktree_id - ))? - } - } - - fn write_worktree( - &mut self, - worktree_id: u64, - connection_id: ConnectionId, - ) -> tide::Result<&mut Worktree> { - let worktree = self - .worktrees - .get_mut(&worktree_id) - .ok_or_else(|| anyhow!("worktree not found"))?; - - if worktree.host_connection_id == Some(connection_id) - || worktree.guest_connection_ids.contains_key(&connection_id) - { - Ok(worktree) - } else { - Err(anyhow!( - "{} is not a member of worktree {}", - connection_id, - worktree_id - ))? - } - } -} - -impl Worktree { - pub fn connection_ids(&self) -> Vec { - self.guest_connection_ids - .keys() - .copied() - .chain(self.host_connection_id) - .collect() - } - - fn host_connection_id(&self) -> tide::Result { - Ok(self - .host_connection_id - .ok_or_else(|| anyhow!("host disconnected from worktree"))?) - } -} - -impl Channel { - fn connection_ids(&self) -> Vec { - self.connection_ids.iter().copied().collect() - } -} - pub fn add_routes(app: &mut tide::Server>, rpc: &Arc) { let server = Server::new(app.state().clone(), rpc.clone(), None); app.at("/rpc").with(auth::VerifyToken).get(move |request: Request>| { @@ -1022,7 +950,7 @@ mod tests { github, AppState, Config, }; use async_std::{sync::RwLockReadGuard, task}; - use gpui::TestAppContext; + use gpui::{ModelHandle, TestAppContext}; use parking_lot::Mutex; use postage::{mpsc, watch}; use serde_json::json; @@ -1040,10 +968,12 @@ mod tests { editor::{Editor, EditorStyle, Insert}, fs::{FakeFs, Fs as _}, language::LanguageRegistry, + people_panel::JoinWorktree, rpc::{self, Client, Credentials, EstablishConnectionError}, settings, test::FakeHttpClient, user::UserStore, + workspace::Workspace, worktree::Worktree, }; use zrpc::Peer; @@ -1066,15 +996,17 @@ mod tests { fs.insert_tree( "/a", json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", "b.txt": "b-contents", }), ) .await; let worktree_a = Worktree::open_local( + client_a.clone(), "/a".as_ref(), - lang_registry.clone(), fs, + lang_registry.clone(), &mut cx_a.to_async(), ) .await @@ -1082,10 +1014,8 @@ mod tests { worktree_a .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) + let worktree_id = worktree_a + .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) .await .unwrap(); @@ -1093,7 +1023,6 @@ mod tests { let worktree_b = Worktree::open_remote( client_b.clone(), worktree_id, - worktree_token, lang_registry.clone(), &mut cx_b.to_async(), ) @@ -1160,6 +1089,94 @@ mod tests { .await; } + #[gpui::test] + async fn test_unshare_worktree(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + cx_b.update(zed::workspace::init); + let lang_registry = Arc::new(LanguageRegistry::new()); + + // Connect to a server as 2 clients. + let mut server = TestServer::start().await; + let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; + let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; + let app_state_b = zed::AppState { + rpc: client_b, + user_store: user_store_b, + ..Arc::try_unwrap(cx_b.update(zed::test::test_app_state)) + .ok() + .unwrap() + }; + + cx_a.foreground().forbid_parking(); + + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + let worktree_a = Worktree::open_local( + client_a.clone(), + "/a".as_ref(), + fs, + lang_registry.clone(), + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + + let remote_worktree_id = worktree_a + .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) + .await + .unwrap(); + + let (window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&app_state_b, cx)); + cx_b.update(|cx| { + cx.dispatch_action( + window_b, + vec![workspace_b.id()], + &JoinWorktree(remote_worktree_id), + ); + }); + workspace_b + .condition(&cx_b, |workspace, _| workspace.worktrees().len() == 1) + .await; + + let local_worktree_id_b = workspace_b.read_with(&cx_b, |workspace, cx| { + let active_pane = workspace.active_pane().read(cx); + assert!(active_pane.active_item().is_none()); + workspace.worktrees().iter().next().unwrap().id() + }); + workspace_b + .update(&mut cx_b, |worktree, cx| { + worktree.open_entry((local_worktree_id_b, Path::new("a.txt").into()), cx) + }) + .unwrap() + .await; + workspace_b.read_with(&cx_b, |workspace, cx| { + let active_pane = workspace.active_pane().read(cx); + assert!(active_pane.active_item().is_some()); + }); + + worktree_a.update(&mut cx_a, |tree, cx| { + tree.as_local_mut().unwrap().unshare(cx); + }); + workspace_b + .condition(&cx_b, |workspace, _| workspace.worktrees().len() == 0) + .await; + workspace_b.read_with(&cx_b, |workspace, cx| { + let active_pane = workspace.active_pane().read(cx); + assert!(active_pane.active_item().is_none()); + }); + } + #[gpui::test] async fn test_propagate_saves_and_fs_changes_in_shared_worktree( mut cx_a: TestAppContext, @@ -1181,6 +1198,7 @@ mod tests { fs.insert_tree( "/a", json!({ + ".zed.toml": r#"collaborators = ["user_b", "user_c"]"#, "file1": "", "file2": "" }), @@ -1188,9 +1206,10 @@ mod tests { .await; let worktree_a = Worktree::open_local( + client_a.clone(), "/a".as_ref(), - lang_registry.clone(), fs.clone(), + lang_registry.clone(), &mut cx_a.to_async(), ) .await @@ -1198,10 +1217,8 @@ mod tests { worktree_a .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) + let worktree_id = worktree_a + .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) .await .unwrap(); @@ -1209,7 +1226,6 @@ mod tests { let worktree_b = Worktree::open_remote( client_b.clone(), worktree_id, - worktree_token.clone(), lang_registry.clone(), &mut cx_b.to_async(), ) @@ -1218,7 +1234,6 @@ mod tests { let worktree_c = Worktree::open_remote( client_c.clone(), worktree_id, - worktree_token, lang_registry.clone(), &mut cx_c.to_async(), ) @@ -1282,17 +1297,17 @@ mod tests { .unwrap(); worktree_b - .condition(&cx_b, |tree, _| tree.file_count() == 3) + .condition(&cx_b, |tree, _| tree.file_count() == 4) .await; worktree_c - .condition(&cx_c, |tree, _| tree.file_count() == 3) + .condition(&cx_c, |tree, _| tree.file_count() == 4) .await; worktree_b.read_with(&cx_b, |tree, _| { assert_eq!( tree.paths() .map(|p| p.to_string_lossy()) .collect::>(), - &["file1", "file3", "file4"] + &[".zed.toml", "file1", "file3", "file4"] ) }); worktree_c.read_with(&cx_c, |tree, _| { @@ -1300,7 +1315,7 @@ mod tests { tree.paths() .map(|p| p.to_string_lossy()) .collect::>(), - &["file1", "file3", "file4"] + &[".zed.toml", "file1", "file3", "file4"] ) }); } @@ -1317,13 +1332,20 @@ mod tests { // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); - fs.save(Path::new("/a.txt"), &"a-contents".into()) - .await - .unwrap(); + fs.insert_tree( + "/dir", + json!({ + ".zed.toml": r#"collaborators = ["user_b", "user_c"]"#, + "a.txt": "a-contents", + }), + ) + .await; + let worktree_a = Worktree::open_local( - "/".as_ref(), - lang_registry.clone(), + client_a.clone(), + "/dir".as_ref(), fs, + lang_registry.clone(), &mut cx_a.to_async(), ) .await @@ -1331,10 +1353,8 @@ mod tests { worktree_a .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) + let worktree_id = worktree_a + .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) .await .unwrap(); @@ -1342,7 +1362,6 @@ mod tests { let worktree_b = Worktree::open_remote( client_b.clone(), worktree_id, - worktree_token, lang_registry.clone(), &mut cx_b.to_async(), ) @@ -1398,13 +1417,19 @@ mod tests { // Share a local worktree as client A let fs = Arc::new(FakeFs::new()); - fs.save(Path::new("/a.txt"), &"a-contents".into()) - .await - .unwrap(); + fs.insert_tree( + "/dir", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "a.txt": "a-contents", + }), + ) + .await; let worktree_a = Worktree::open_local( - "/".as_ref(), - lang_registry.clone(), + client_a.clone(), + "/dir".as_ref(), fs, + lang_registry.clone(), &mut cx_a.to_async(), ) .await @@ -1412,10 +1437,8 @@ mod tests { worktree_a .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) + let worktree_id = worktree_a + .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) .await .unwrap(); @@ -1423,7 +1446,6 @@ mod tests { let worktree_b = Worktree::open_remote( client_b.clone(), worktree_id, - worktree_token, lang_registry.clone(), &mut cx_b.to_async(), ) @@ -1446,6 +1468,69 @@ mod tests { buffer_b.condition(&cx_b, |buf, _| buf.text() == text).await; } + #[gpui::test] + async fn test_leaving_worktree_while_opening_buffer( + mut cx_a: TestAppContext, + mut cx_b: TestAppContext, + ) { + cx_a.foreground().forbid_parking(); + let lang_registry = Arc::new(LanguageRegistry::new()); + + // Connect to a server as 2 clients. + let mut server = TestServer::start().await; + let (client_a, _) = server.create_client(&mut cx_a, "user_a").await; + let (client_b, _) = server.create_client(&mut cx_b, "user_b").await; + + // Share a local worktree as client A + let fs = Arc::new(FakeFs::new()); + fs.insert_tree( + "/dir", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "a.txt": "a-contents", + }), + ) + .await; + let worktree_a = Worktree::open_local( + client_a.clone(), + "/dir".as_ref(), + fs, + lang_registry.clone(), + &mut cx_a.to_async(), + ) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let worktree_id = worktree_a + .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) + .await + .unwrap(); + + // Join that worktree as client B, and see that a guest has joined as client A. + let worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + worktree_a + .condition(&cx_a, |tree, _| tree.peers().len() == 1) + .await; + + let buffer_b = cx_b + .background() + .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx))); + cx_b.update(|_| drop(worktree_b)); + drop(buffer_b); + worktree_a + .condition(&cx_a, |tree, _| tree.peers().len() == 0) + .await; + } + #[gpui::test] async fn test_peer_disconnection(mut cx_a: TestAppContext, cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); @@ -1461,15 +1546,17 @@ mod tests { fs.insert_tree( "/a", json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, "a.txt": "a-contents", "b.txt": "b-contents", }), ) .await; let worktree_a = Worktree::open_local( + client_a.clone(), "/a".as_ref(), - lang_registry.clone(), fs, + lang_registry.clone(), &mut cx_a.to_async(), ) .await @@ -1477,10 +1564,8 @@ mod tests { worktree_a .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) .await; - let (worktree_id, worktree_token) = worktree_a - .update(&mut cx_a, |tree, cx| { - tree.as_local_mut().unwrap().share(client_a.clone(), cx) - }) + let worktree_id = worktree_a + .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) .await .unwrap(); @@ -1488,7 +1573,6 @@ mod tests { let _worktree_b = Worktree::open_remote( client_b.clone(), worktree_id, - worktree_token, lang_registry.clone(), &mut cx_b.to_async(), ) @@ -1517,24 +1601,24 @@ mod tests { // Create an org that includes these 2 users. let db = &server.app_state.db; let org_id = db.create_org("Test Org", "test-org").await.unwrap(); - db.add_org_member(org_id, current_user_id(&user_store_a), false) + db.add_org_member(org_id, current_user_id(&user_store_a, &cx_a), false) .await .unwrap(); - db.add_org_member(org_id, current_user_id(&user_store_b), false) + db.add_org_member(org_id, current_user_id(&user_store_b, &cx_b), false) .await .unwrap(); // Create a channel that includes all the users. let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap(); - db.add_channel_member(channel_id, current_user_id(&user_store_a), false) + db.add_channel_member(channel_id, current_user_id(&user_store_a, &cx_a), false) .await .unwrap(); - db.add_channel_member(channel_id, current_user_id(&user_store_b), false) + db.add_channel_member(channel_id, current_user_id(&user_store_b, &cx_b), false) .await .unwrap(); db.create_channel_message( channel_id, - current_user_id(&user_store_b), + current_user_id(&user_store_b, &cx_b), "hello A, it's B.", OffsetDateTime::now_utc(), 1, @@ -1623,19 +1707,23 @@ mod tests { .await; assert_eq!( - server.state().await.channels[&channel_id] + server + .state() + .await + .channel(channel_id) + .unwrap() .connection_ids .len(), 2 ); cx_b.update(|_| drop(channel_b)); server - .condition(|state| state.channels[&channel_id].connection_ids.len() == 1) + .condition(|state| state.channel(channel_id).unwrap().connection_ids.len() == 1) .await; cx_a.update(|_| drop(channel_a)); server - .condition(|state| !state.channels.contains_key(&channel_id)) + .condition(|state| state.channel(channel_id).is_none()) .await; } @@ -1649,10 +1737,10 @@ mod tests { let db = &server.app_state.db; let org_id = db.create_org("Test Org", "test-org").await.unwrap(); let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap(); - db.add_org_member(org_id, current_user_id(&user_store_a), false) + db.add_org_member(org_id, current_user_id(&user_store_a, &cx_a), false) .await .unwrap(); - db.add_channel_member(channel_id, current_user_id(&user_store_a), false) + db.add_channel_member(channel_id, current_user_id(&user_store_a, &cx_a), false) .await .unwrap(); @@ -1701,7 +1789,6 @@ mod tests { #[gpui::test] async fn test_chat_reconnection(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); - let http = FakeHttpClient::new(|_| async move { Ok(surf::http::Response::new(404)) }); // Connect to a server as 2 clients. let mut server = TestServer::start().await; @@ -1712,24 +1799,24 @@ mod tests { // Create an org that includes these 2 users. let db = &server.app_state.db; let org_id = db.create_org("Test Org", "test-org").await.unwrap(); - db.add_org_member(org_id, current_user_id(&user_store_a), false) + db.add_org_member(org_id, current_user_id(&user_store_a, &cx_a), false) .await .unwrap(); - db.add_org_member(org_id, current_user_id(&user_store_b), false) + db.add_org_member(org_id, current_user_id(&user_store_b, &cx_b), false) .await .unwrap(); // Create a channel that includes all the users. let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap(); - db.add_channel_member(channel_id, current_user_id(&user_store_a), false) + db.add_channel_member(channel_id, current_user_id(&user_store_a, &cx_a), false) .await .unwrap(); - db.add_channel_member(channel_id, current_user_id(&user_store_b), false) + db.add_channel_member(channel_id, current_user_id(&user_store_b, &cx_b), false) .await .unwrap(); db.create_channel_message( channel_id, - current_user_id(&user_store_b), + current_user_id(&user_store_b, &cx_b), "hello A, it's B.", OffsetDateTime::now_utc(), 2, @@ -1737,8 +1824,6 @@ mod tests { .await .unwrap(); - let user_store_a = - UserStore::new(client_a.clone(), http.clone(), cx_a.background().as_ref()); let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx)); channels_a .condition(&mut cx_a, |list, _| list.available_channels().is_some()) @@ -1791,7 +1876,7 @@ mod tests { // Disconnect client B, ensuring we can still access its cached channel data. server.forbid_connections(); - server.disconnect_client(current_user_id(&user_store_b)); + server.disconnect_client(current_user_id(&user_store_b, &cx_b)); while !matches!( status_b.recv().await, Some(rpc::Status::ReconnectionError { .. }) @@ -1911,6 +1996,120 @@ mod tests { .await; } + #[gpui::test] + async fn test_collaborators( + mut cx_a: TestAppContext, + mut cx_b: TestAppContext, + mut cx_c: TestAppContext, + ) { + cx_a.foreground().forbid_parking(); + let lang_registry = Arc::new(LanguageRegistry::new()); + + // Connect to a server as 3 clients. + let mut server = TestServer::start().await; + let (client_a, user_store_a) = server.create_client(&mut cx_a, "user_a").await; + let (client_b, user_store_b) = server.create_client(&mut cx_b, "user_b").await; + let (_client_c, user_store_c) = server.create_client(&mut cx_c, "user_c").await; + + let fs = Arc::new(FakeFs::new()); + + // Share a worktree as client A. + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b", "user_c"]"#, + }), + ) + .await; + + let worktree_a = Worktree::open_local( + client_a.clone(), + "/a".as_ref(), + fs.clone(), + lang_registry.clone(), + &mut cx_a.to_async(), + ) + .await + .unwrap(); + + user_store_a + .condition(&cx_a, |user_store, _| { + collaborators(user_store) == vec![("user_a", vec![("a", vec![])])] + }) + .await; + user_store_b + .condition(&cx_b, |user_store, _| { + collaborators(user_store) == vec![("user_a", vec![("a", vec![])])] + }) + .await; + user_store_c + .condition(&cx_c, |user_store, _| { + collaborators(user_store) == vec![("user_a", vec![("a", vec![])])] + }) + .await; + + let worktree_id = worktree_a + .update(&mut cx_a, |tree, cx| tree.as_local_mut().unwrap().share(cx)) + .await + .unwrap(); + + let _worktree_b = Worktree::open_remote( + client_b.clone(), + worktree_id, + lang_registry.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + + user_store_a + .condition(&cx_a, |user_store, _| { + collaborators(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])] + }) + .await; + user_store_b + .condition(&cx_b, |user_store, _| { + collaborators(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])] + }) + .await; + user_store_c + .condition(&cx_c, |user_store, _| { + collaborators(user_store) == vec![("user_a", vec![("a", vec!["user_b"])])] + }) + .await; + + cx_a.update(move |_| drop(worktree_a)); + user_store_a + .condition(&cx_a, |user_store, _| collaborators(user_store) == vec![]) + .await; + user_store_b + .condition(&cx_b, |user_store, _| collaborators(user_store) == vec![]) + .await; + user_store_c + .condition(&cx_c, |user_store, _| collaborators(user_store) == vec![]) + .await; + + fn collaborators(user_store: &UserStore) -> Vec<(&str, Vec<(&str, Vec<&str>)>)> { + user_store + .collaborators() + .iter() + .map(|collaborator| { + let worktrees = collaborator + .worktrees + .iter() + .map(|w| { + ( + w.root_name.as_str(), + w.guests.iter().map(|p| p.github_login.as_str()).collect(), + ) + }) + .collect(); + (collaborator.user.github_login.as_str(), worktrees) + }) + .collect() + } + } + struct TestServer { peer: Arc, app_state: Arc, @@ -1943,7 +2142,7 @@ mod tests { &mut self, cx: &mut TestAppContext, name: &str, - ) -> (Arc, Arc) { + ) -> (Arc, ModelHandle) { let user_id = self.app_state.db.create_user(name, false).await.unwrap(); let client_name = name.to_string(); let mut client = Client::new(); @@ -1991,8 +2190,9 @@ mod tests { .await .unwrap(); - let user_store = UserStore::new(client.clone(), http, &cx.background()); - let mut authed_user = user_store.watch_current_user(); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); + let mut authed_user = + user_store.read_with(cx, |user_store, _| user_store.watch_current_user()); while authed_user.recv().await.unwrap().is_none() {} (client, user_store) @@ -2027,16 +2227,16 @@ mod tests { }) } - async fn state<'a>(&'a self) -> RwLockReadGuard<'a, ServerState> { - self.server.state.read().await + async fn state<'a>(&'a self) -> RwLockReadGuard<'a, Store> { + self.server.store.read().await } async fn condition(&mut self, mut predicate: F) where - F: FnMut(&ServerState) -> bool, + F: FnMut(&Store) -> bool, { async_std::future::timeout(Duration::from_millis(500), async { - while !(predicate)(&*self.server.state.read().await) { + while !(predicate)(&*self.server.store.read().await) { self.notifications.recv().await; } }) @@ -2051,8 +2251,10 @@ mod tests { } } - fn current_user_id(user_store: &Arc) -> UserId { - UserId::from_proto(user_store.current_user().unwrap().id) + fn current_user_id(user_store: &ModelHandle, cx: &TestAppContext) -> UserId { + UserId::from_proto( + user_store.read_with(cx, |user_store, _| user_store.current_user().unwrap().id), + ) } fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> { diff --git a/server/src/rpc/store.rs b/server/src/rpc/store.rs new file mode 100644 index 0000000000000000000000000000000000000000..b33cd135589d57de9aaf2602f25c2a58a0f6dfd8 --- /dev/null +++ b/server/src/rpc/store.rs @@ -0,0 +1,615 @@ +use crate::db::{ChannelId, UserId}; +use anyhow::anyhow; +use std::collections::{hash_map, HashMap, HashSet}; +use zrpc::{proto, ConnectionId}; + +#[derive(Default)] +pub struct Store { + connections: HashMap, + connections_by_user_id: HashMap>, + worktrees: HashMap, + visible_worktrees_by_user_id: HashMap>, + channels: HashMap, + next_worktree_id: u64, +} + +struct ConnectionState { + user_id: UserId, + worktrees: HashSet, + channels: HashSet, +} + +pub struct Worktree { + pub host_connection_id: ConnectionId, + pub collaborator_user_ids: Vec, + pub root_name: String, + pub share: Option, +} + +pub struct WorktreeShare { + pub guest_connection_ids: HashMap, + pub active_replica_ids: HashSet, + pub entries: HashMap, +} + +#[derive(Default)] +pub struct Channel { + pub connection_ids: HashSet, +} + +pub type ReplicaId = u16; + +#[derive(Default)] +pub struct RemovedConnectionState { + pub hosted_worktrees: HashMap, + pub guest_worktree_ids: HashMap>, + pub collaborator_ids: HashSet, +} + +pub struct JoinedWorktree<'a> { + pub replica_id: ReplicaId, + pub worktree: &'a Worktree, +} + +pub struct UnsharedWorktree { + pub connection_ids: Vec, + pub collaborator_ids: Vec, +} + +pub struct LeftWorktree { + pub connection_ids: Vec, + pub collaborator_ids: Vec, +} + +impl Store { + pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId) { + self.connections.insert( + connection_id, + ConnectionState { + user_id, + worktrees: Default::default(), + channels: Default::default(), + }, + ); + self.connections_by_user_id + .entry(user_id) + .or_default() + .insert(connection_id); + } + + pub fn remove_connection( + &mut self, + connection_id: ConnectionId, + ) -> tide::Result { + let connection = if let Some(connection) = self.connections.remove(&connection_id) { + connection + } else { + return Err(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_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(); + for worktree_id in connection.worktrees.clone() { + if let Ok(worktree) = self.remove_worktree(worktree_id, connection_id) { + result + .collaborator_ids + .extend(worktree.collaborator_user_ids.iter().copied()); + result.hosted_worktrees.insert(worktree_id, worktree); + } else if let Some(worktree) = self.leave_worktree(connection_id, worktree_id) { + result + .guest_worktree_ids + .insert(worktree_id, worktree.connection_ids); + result.collaborator_ids.extend(worktree.collaborator_ids); + } + } + + #[cfg(test)] + self.check_invariants(); + + Ok(result) + } + + #[cfg(test)] + pub fn channel(&self, id: ChannelId) -> Option<&Channel> { + self.channels.get(&id) + } + + pub fn join_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) { + if let Some(connection) = self.connections.get_mut(&connection_id) { + connection.channels.insert(channel_id); + self.channels + .entry(channel_id) + .or_default() + .connection_ids + .insert(connection_id); + } + } + + pub fn leave_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) { + if let Some(connection) = self.connections.get_mut(&connection_id) { + connection.channels.remove(&channel_id); + if let hash_map::Entry::Occupied(mut entry) = self.channels.entry(channel_id) { + entry.get_mut().connection_ids.remove(&connection_id); + if entry.get_mut().connection_ids.is_empty() { + entry.remove(); + } + } + } + } + + pub fn user_id_for_connection(&self, connection_id: ConnectionId) -> tide::Result { + Ok(self + .connections + .get(&connection_id) + .ok_or_else(|| anyhow!("unknown connection"))? + .user_id) + } + + pub fn connection_ids_for_user<'a>( + &'a self, + user_id: UserId, + ) -> impl 'a + Iterator { + self.connections_by_user_id + .get(&user_id) + .into_iter() + .flatten() + .copied() + } + + pub fn collaborators_for_user(&self, user_id: UserId) -> Vec { + let mut collaborators = HashMap::new(); + for worktree_id in self + .visible_worktrees_by_user_id + .get(&user_id) + .unwrap_or(&HashSet::new()) + { + let worktree = &self.worktrees[worktree_id]; + + let mut guests = HashSet::new(); + if let Ok(share) = worktree.share() { + for guest_connection_id in share.guest_connection_ids.keys() { + if let Ok(user_id) = self.user_id_for_connection(*guest_connection_id) { + guests.insert(user_id.to_proto()); + } + } + } + + if let Ok(host_user_id) = self.user_id_for_connection(worktree.host_connection_id) { + collaborators + .entry(host_user_id) + .or_insert_with(|| proto::Collaborator { + user_id: host_user_id.to_proto(), + worktrees: Vec::new(), + }) + .worktrees + .push(proto::WorktreeMetadata { + id: *worktree_id, + root_name: worktree.root_name.clone(), + is_shared: worktree.share.is_some(), + guests: guests.into_iter().collect(), + }); + } + } + + collaborators.into_values().collect() + } + + pub fn add_worktree(&mut self, worktree: Worktree) -> u64 { + let worktree_id = self.next_worktree_id; + for collaborator_user_id in &worktree.collaborator_user_ids { + self.visible_worktrees_by_user_id + .entry(*collaborator_user_id) + .or_default() + .insert(worktree_id); + } + self.next_worktree_id += 1; + if let Some(connection) = self.connections.get_mut(&worktree.host_connection_id) { + connection.worktrees.insert(worktree_id); + } + self.worktrees.insert(worktree_id, worktree); + + #[cfg(test)] + self.check_invariants(); + + worktree_id + } + + pub fn remove_worktree( + &mut self, + worktree_id: u64, + acting_connection_id: ConnectionId, + ) -> tide::Result { + let worktree = if let hash_map::Entry::Occupied(e) = self.worktrees.entry(worktree_id) { + if e.get().host_connection_id != acting_connection_id { + Err(anyhow!("not your worktree"))?; + } + e.remove() + } else { + return Err(anyhow!("no such worktree"))?; + }; + + if let Some(connection) = self.connections.get_mut(&worktree.host_connection_id) { + connection.worktrees.remove(&worktree_id); + } + + if let Some(share) = &worktree.share { + for connection_id in share.guest_connection_ids.keys() { + if let Some(connection) = self.connections.get_mut(connection_id) { + connection.worktrees.remove(&worktree_id); + } + } + } + + for collaborator_user_id in &worktree.collaborator_user_ids { + if let Some(visible_worktrees) = self + .visible_worktrees_by_user_id + .get_mut(&collaborator_user_id) + { + visible_worktrees.remove(&worktree_id); + } + } + + #[cfg(test)] + self.check_invariants(); + + Ok(worktree) + } + + pub fn share_worktree( + &mut self, + worktree_id: u64, + connection_id: ConnectionId, + entries: HashMap, + ) -> Option> { + if let Some(worktree) = self.worktrees.get_mut(&worktree_id) { + if worktree.host_connection_id == connection_id { + worktree.share = Some(WorktreeShare { + guest_connection_ids: Default::default(), + active_replica_ids: Default::default(), + entries, + }); + return Some(worktree.collaborator_user_ids.clone()); + } + } + None + } + + pub fn unshare_worktree( + &mut self, + worktree_id: u64, + acting_connection_id: ConnectionId, + ) -> tide::Result { + let worktree = if let Some(worktree) = self.worktrees.get_mut(&worktree_id) { + worktree + } else { + return Err(anyhow!("no such worktree"))?; + }; + + if worktree.host_connection_id != acting_connection_id { + return Err(anyhow!("not your worktree"))?; + } + + let connection_ids = worktree.connection_ids(); + let collaborator_ids = worktree.collaborator_user_ids.clone(); + if let Some(share) = worktree.share.take() { + for connection_id in share.guest_connection_ids.into_keys() { + if let Some(connection) = self.connections.get_mut(&connection_id) { + connection.worktrees.remove(&worktree_id); + } + } + + #[cfg(test)] + self.check_invariants(); + + Ok(UnsharedWorktree { + connection_ids, + collaborator_ids, + }) + } else { + Err(anyhow!("worktree is not shared"))? + } + } + + pub fn join_worktree( + &mut self, + connection_id: ConnectionId, + user_id: UserId, + worktree_id: u64, + ) -> tide::Result { + let connection = self + .connections + .get_mut(&connection_id) + .ok_or_else(|| anyhow!("no such connection"))?; + let worktree = self + .worktrees + .get_mut(&worktree_id) + .and_then(|worktree| { + if worktree.collaborator_user_ids.contains(&user_id) { + Some(worktree) + } else { + None + } + }) + .ok_or_else(|| anyhow!("no such worktree"))?; + + let share = worktree.share_mut()?; + connection.worktrees.insert(worktree_id); + + let mut replica_id = 1; + while share.active_replica_ids.contains(&replica_id) { + replica_id += 1; + } + share.active_replica_ids.insert(replica_id); + share.guest_connection_ids.insert(connection_id, replica_id); + + #[cfg(test)] + self.check_invariants(); + + Ok(JoinedWorktree { + replica_id, + worktree: &self.worktrees[&worktree_id], + }) + } + + pub fn leave_worktree( + &mut self, + connection_id: ConnectionId, + worktree_id: u64, + ) -> Option { + let worktree = self.worktrees.get_mut(&worktree_id)?; + let share = worktree.share.as_mut()?; + let replica_id = share.guest_connection_ids.remove(&connection_id)?; + share.active_replica_ids.remove(&replica_id); + + if let Some(connection) = self.connections.get_mut(&connection_id) { + connection.worktrees.remove(&worktree_id); + } + + let connection_ids = worktree.connection_ids(); + let collaborator_ids = worktree.collaborator_user_ids.clone(); + + #[cfg(test)] + self.check_invariants(); + + Some(LeftWorktree { + connection_ids, + collaborator_ids, + }) + } + + pub fn update_worktree( + &mut self, + connection_id: ConnectionId, + worktree_id: u64, + removed_entries: &[u64], + updated_entries: &[proto::Entry], + ) -> tide::Result> { + let worktree = self.write_worktree(worktree_id, connection_id)?; + let share = worktree.share_mut()?; + for entry_id in removed_entries { + share.entries.remove(&entry_id); + } + for entry in updated_entries { + share.entries.insert(entry.id, entry.clone()); + } + Ok(worktree.connection_ids()) + } + + pub fn worktree_host_connection_id( + &self, + connection_id: ConnectionId, + worktree_id: u64, + ) -> tide::Result { + Ok(self + .read_worktree(worktree_id, connection_id)? + .host_connection_id) + } + + pub fn worktree_guest_connection_ids( + &self, + connection_id: ConnectionId, + worktree_id: u64, + ) -> tide::Result> { + Ok(self + .read_worktree(worktree_id, connection_id)? + .share()? + .guest_connection_ids + .keys() + .copied() + .collect()) + } + + pub fn worktree_connection_ids( + &self, + connection_id: ConnectionId, + worktree_id: u64, + ) -> tide::Result> { + Ok(self + .read_worktree(worktree_id, connection_id)? + .connection_ids()) + } + + pub fn channel_connection_ids(&self, channel_id: ChannelId) -> Option> { + Some(self.channels.get(&channel_id)?.connection_ids()) + } + + fn read_worktree( + &self, + worktree_id: u64, + connection_id: ConnectionId, + ) -> tide::Result<&Worktree> { + let worktree = self + .worktrees + .get(&worktree_id) + .ok_or_else(|| anyhow!("worktree not found"))?; + + if worktree.host_connection_id == connection_id + || worktree + .share()? + .guest_connection_ids + .contains_key(&connection_id) + { + Ok(worktree) + } else { + Err(anyhow!( + "{} is not a member of worktree {}", + connection_id, + worktree_id + ))? + } + } + + fn write_worktree( + &mut self, + worktree_id: u64, + connection_id: ConnectionId, + ) -> tide::Result<&mut Worktree> { + let worktree = self + .worktrees + .get_mut(&worktree_id) + .ok_or_else(|| anyhow!("worktree not found"))?; + + if worktree.host_connection_id == connection_id + || worktree.share.as_ref().map_or(false, |share| { + share.guest_connection_ids.contains_key(&connection_id) + }) + { + Ok(worktree) + } else { + Err(anyhow!( + "{} is not a member of worktree {}", + connection_id, + worktree_id + ))? + } + } + + #[cfg(test)] + fn check_invariants(&self) { + for (connection_id, connection) in &self.connections { + for worktree_id in &connection.worktrees { + let worktree = &self.worktrees.get(&worktree_id).unwrap(); + if worktree.host_connection_id != *connection_id { + assert!(worktree + .share() + .unwrap() + .guest_connection_ids + .contains_key(connection_id)); + } + } + for channel_id in &connection.channels { + let channel = self.channels.get(channel_id).unwrap(); + assert!(channel.connection_ids.contains(connection_id)); + } + assert!(self + .connections_by_user_id + .get(&connection.user_id) + .unwrap() + .contains(connection_id)); + } + + for (user_id, connection_ids) in &self.connections_by_user_id { + for connection_id in connection_ids { + assert_eq!( + self.connections.get(connection_id).unwrap().user_id, + *user_id + ); + } + } + + for (worktree_id, worktree) in &self.worktrees { + let host_connection = self.connections.get(&worktree.host_connection_id).unwrap(); + assert!(host_connection.worktrees.contains(worktree_id)); + + for collaborator_id in &worktree.collaborator_user_ids { + let visible_worktree_ids = self + .visible_worktrees_by_user_id + .get(collaborator_id) + .unwrap(); + assert!(visible_worktree_ids.contains(worktree_id)); + } + + if let Some(share) = &worktree.share { + for guest_connection_id in share.guest_connection_ids.keys() { + let guest_connection = self.connections.get(guest_connection_id).unwrap(); + assert!(guest_connection.worktrees.contains(worktree_id)); + } + assert_eq!( + share.active_replica_ids.len(), + share.guest_connection_ids.len(), + ); + assert_eq!( + share.active_replica_ids, + share + .guest_connection_ids + .values() + .copied() + .collect::>(), + ); + } + } + + for (user_id, visible_worktree_ids) in &self.visible_worktrees_by_user_id { + for worktree_id in visible_worktree_ids { + let worktree = self.worktrees.get(worktree_id).unwrap(); + assert!(worktree.collaborator_user_ids.contains(user_id)); + } + } + + for (channel_id, channel) in &self.channels { + for connection_id in &channel.connection_ids { + let connection = self.connections.get(connection_id).unwrap(); + assert!(connection.channels.contains(channel_id)); + } + } + } +} + +impl Worktree { + pub fn connection_ids(&self) -> Vec { + if let Some(share) = &self.share { + share + .guest_connection_ids + .keys() + .copied() + .chain(Some(self.host_connection_id)) + .collect() + } else { + vec![self.host_connection_id] + } + } + + pub fn share(&self) -> tide::Result<&WorktreeShare> { + Ok(self + .share + .as_ref() + .ok_or_else(|| anyhow!("worktree is not shared"))?) + } + + fn share_mut(&mut self) -> tide::Result<&mut WorktreeShare> { + Ok(self + .share + .as_mut() + .ok_or_else(|| anyhow!("worktree is not shared"))?) + } +} + +impl Channel { + fn connection_ids(&self) -> Vec { + self.connection_ids.iter().copied().collect() + } +} diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 57f25eb26fc331311a2dc93536554471ec549602..5938032c2c8c099f2840460f3f5a640dd64196d6 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -56,10 +56,13 @@ border = { width = 1, color = "$border.0", right = true } extends = "$workspace.sidebar" border = { width = 1, color = "$border.0", left = true } +[panel] +padding = 12 + [chat_panel] +extends = "$panel" channel_name = { extends = "$text.0", weight = "bold" } channel_name_hash = { text = "$text.2", padding.right = 8 } -padding = 12 [chat_panel.message] body = "$text.1" @@ -120,6 +123,41 @@ underline = true extends = "$chat_panel.sign_in_prompt" color = "$text.1.color" +[people_panel] +extends = "$panel" +host_row_height = 28 +host_avatar = { corner_radius = 10, width = 20 } +host_username = { extends = "$text.0", padding.left = 8 } +tree_branch_width = 1 +tree_branch_color = "$surface.2" + +[people_panel.worktree] +height = 24 +padding = { left = 8 } +guest_avatar = { corner_radius = 8, width = 16 } +guest_avatar_spacing = 4 + +[people_panel.worktree.name] +extends = "$text.1" +margin = { right = 6 } + +[people_panel.unshared_worktree] +extends = "$people_panel.worktree" + +[people_panel.hovered_unshared_worktree] +extends = "$people_panel.unshared_worktree" +background = "$state.hover" +corner_radius = 6 + +[people_panel.shared_worktree] +extends = "$people_panel.worktree" +name.color = "$text.0.color" + +[people_panel.hovered_shared_worktree] +extends = "$people_panel.shared_worktree" +background = "$state.hover" +corner_radius = 6 + [selector] background = "$surface.0" padding = 8 diff --git a/zed/src/channel.rs b/zed/src/channel.rs index c43cf2e6f7b28a45e5a69dfa67c0383e065f6143..f0e7a90c9aed29367df60a388febdac0f47b130e 100644 --- a/zed/src/channel.rs +++ b/zed/src/channel.rs @@ -6,7 +6,7 @@ use crate::{ use anyhow::{anyhow, Context, Result}; use gpui::{ sum_tree::{self, Bias, SumTree}, - Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle, + AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle, }; use postage::prelude::Stream; use rand::prelude::*; @@ -26,7 +26,7 @@ pub struct ChannelList { available_channels: Option>, channels: HashMap>, rpc: Arc, - user_store: Arc, + user_store: ModelHandle, _task: Task>, } @@ -41,7 +41,7 @@ pub struct Channel { messages: SumTree, loaded_all_messages: bool, next_pending_message_id: usize, - user_store: Arc, + user_store: ModelHandle, rpc: Arc, rng: StdRng, _subscription: rpc::Subscription, @@ -87,7 +87,7 @@ impl Entity for ChannelList { impl ChannelList { pub fn new( - user_store: Arc, + user_store: ModelHandle, rpc: Arc, cx: &mut ModelContext, ) -> Self { @@ -186,11 +186,11 @@ impl Entity for Channel { impl Channel { pub fn new( details: ChannelDetails, - user_store: Arc, + user_store: ModelHandle, rpc: Arc, cx: &mut ModelContext, ) -> Self { - let _subscription = rpc.subscribe_from_model(details.id, cx, Self::handle_message_sent); + let _subscription = rpc.subscribe_to_entity(details.id, cx, Self::handle_message_sent); { let user_store = user_store.clone(); @@ -199,7 +199,8 @@ impl Channel { cx.spawn(|channel, mut cx| { async move { let response = rpc.request(proto::JoinChannel { channel_id }).await?; - let messages = messages_from_proto(response.messages, &user_store).await?; + let messages = + messages_from_proto(response.messages, &user_store, &mut cx).await?; let loaded_all_messages = response.done; channel.update(&mut cx, |channel, cx| { @@ -241,6 +242,7 @@ impl Channel { let current_user = self .user_store + .read(cx) .current_user() .ok_or_else(|| anyhow!("current_user is not present"))?; @@ -272,6 +274,7 @@ impl Channel { let message = ChannelMessage::from_proto( response.message.ok_or_else(|| anyhow!("invalid message"))?, &user_store, + &mut cx, ) .await?; this.update(&mut cx, |this, cx| { @@ -301,7 +304,8 @@ impl Channel { }) .await?; let loaded_all_messages = response.done; - let messages = messages_from_proto(response.messages, &user_store).await?; + let messages = + messages_from_proto(response.messages, &user_store, &mut cx).await?; this.update(&mut cx, |this, cx| { this.loaded_all_messages = loaded_all_messages; this.insert_messages(messages, cx); @@ -324,7 +328,7 @@ impl Channel { cx.spawn(|this, mut cx| { async move { let response = rpc.request(proto::JoinChannel { channel_id }).await?; - let messages = messages_from_proto(response.messages, &user_store).await?; + let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?; let loaded_all_messages = response.done; let pending_messages = this.update(&mut cx, |this, cx| { @@ -359,6 +363,7 @@ impl Channel { let message = ChannelMessage::from_proto( response.message.ok_or_else(|| anyhow!("invalid message"))?, &user_store, + &mut cx, ) .await?; this.update(&mut cx, |this, cx| { @@ -413,7 +418,7 @@ impl Channel { cx.spawn(|this, mut cx| { async move { - let message = ChannelMessage::from_proto(message, &user_store).await?; + let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?; this.update(&mut cx, |this, cx| { this.insert_messages(SumTree::from_item(message, &()), cx) }); @@ -486,7 +491,8 @@ impl Channel { async fn messages_from_proto( proto_messages: Vec, - user_store: &UserStore, + user_store: &ModelHandle, + cx: &mut AsyncAppContext, ) -> Result> { let unique_user_ids = proto_messages .iter() @@ -494,11 +500,15 @@ async fn messages_from_proto( .collect::>() .into_iter() .collect(); - user_store.load_users(unique_user_ids).await?; + user_store + .update(cx, |user_store, cx| { + user_store.load_users(unique_user_ids, cx) + }) + .await?; let mut messages = Vec::with_capacity(proto_messages.len()); for message in proto_messages { - messages.push(ChannelMessage::from_proto(message, &user_store).await?); + messages.push(ChannelMessage::from_proto(message, user_store, cx).await?); } let mut result = SumTree::new(); result.extend(messages, &()); @@ -517,9 +527,14 @@ impl From for ChannelDetails { impl ChannelMessage { pub async fn from_proto( message: proto::ChannelMessage, - user_store: &UserStore, + user_store: &ModelHandle, + cx: &mut AsyncAppContext, ) -> Result { - let sender = user_store.fetch_user(message.sender_id).await?; + let sender = user_store + .update(cx, |user_store, cx| { + user_store.fetch_user(message.sender_id, cx) + }) + .await?; Ok(ChannelMessage { id: ChannelMessageId::Saved(message.id), body: message.body, @@ -595,26 +610,11 @@ mod tests { let mut client = Client::new(); let http_client = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) }); let server = FakeServer::for_client(user_id, &mut client, &cx).await; - let user_store = UserStore::new(client.clone(), http_client, cx.background().as_ref()); + let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let channel_list = cx.add_model(|cx| ChannelList::new(user_store, client.clone(), cx)); channel_list.read_with(&cx, |list, _| assert_eq!(list.available_channels(), None)); - let get_users = server.receive::().await.unwrap(); - assert_eq!(get_users.payload.user_ids, vec![5]); - server - .respond( - get_users.receipt(), - proto::GetUsersResponse { - users: vec![proto::User { - id: 5, - github_login: "nathansobo".into(), - avatar_url: "http://avatar.com/nathansobo".into(), - }], - }, - ) - .await; - // Get the available channels. let get_channels = server.receive::().await.unwrap(); server @@ -639,6 +639,21 @@ mod tests { ) }); + let get_users = server.receive::().await.unwrap(); + assert_eq!(get_users.payload.user_ids, vec![5]); + server + .respond( + get_users.receipt(), + proto::GetUsersResponse { + users: vec![proto::User { + id: 5, + github_login: "nathansobo".into(), + avatar_url: "http://avatar.com/nathansobo".into(), + }], + }, + ) + .await; + // Join a channel and populate its existing messages. let channel = channel_list .update(&mut cx, |list, cx| { diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 25403d3aae44c3ef22c735700f80bfd1ea46f0dd..a06b7ee013787591152894b2d0f911adf803f388 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -2320,6 +2320,7 @@ impl Editor { buffer::Event::Saved => cx.emit(Event::Saved), buffer::Event::FileHandleChanged => cx.emit(Event::FileHandleChanged), buffer::Event::Reloaded => cx.emit(Event::FileHandleChanged), + buffer::Event::Closed => cx.emit(Event::Closed), buffer::Event::Reparsed => {} } } @@ -2449,6 +2450,7 @@ pub enum Event { Dirtied, Saved, FileHandleChanged, + Closed, } impl Entity for Editor { @@ -2556,6 +2558,10 @@ impl workspace::ItemView for Editor { matches!(event, Event::Activate) } + fn should_close_item_on_event(event: &Self::Event) -> bool { + matches!(event, Event::Closed) + } + fn should_update_tab_on_event(event: &Self::Event) -> bool { matches!( event, diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index 0ff7f9c148d418ee617c432b753d7201a5ea4312..8186b422aec89df277eb3c612df4922fa50dbd14 100644 --- a/zed/src/editor/buffer.rs +++ b/zed/src/editor/buffer.rs @@ -801,6 +801,10 @@ impl Buffer { cx.emit(Event::FileHandleChanged); } + pub fn close(&mut self, cx: &mut ModelContext) { + cx.emit(Event::Closed); + } + pub fn language(&self) -> Option<&Arc> { self.language.as_ref() } @@ -2264,6 +2268,7 @@ pub enum Event { FileHandleChanged, Reloaded, Reparsed, + Closed, } impl Entity for Buffer { @@ -2928,6 +2933,7 @@ mod tests { use crate::{ fs::RealFs, language::LanguageRegistry, + rpc, test::temp_tree, util::RandomCharIter, worktree::{Worktree, WorktreeHandle as _}, @@ -3394,9 +3400,10 @@ mod tests { "file3": "ghi", })); let tree = Worktree::open_local( + rpc::Client::new(), dir.path(), - Default::default(), Arc::new(RealFs), + Default::default(), &mut cx.to_async(), ) .await @@ -3516,9 +3523,10 @@ mod tests { let initial_contents = "aaa\nbbbbb\nc\n"; let dir = temp_tree(json!({ "the-file": initial_contents })); let tree = Worktree::open_local( + rpc::Client::new(), dir.path(), - Default::default(), Arc::new(RealFs), + Default::default(), &mut cx.to_async(), ) .await diff --git a/zed/src/lib.rs b/zed/src/lib.rs index c9cec56f46da05851471cd20f9e4adb9133dc18c..b7fa3f83d06b00f730945cccd40f2f7a7869a077 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -8,6 +8,7 @@ mod fuzzy; pub mod http; pub mod language; pub mod menus; +pub mod people_panel; pub mod project_browser; pub mod rpc; pub mod settings; @@ -43,7 +44,7 @@ pub struct AppState { pub languages: Arc, pub themes: Arc, pub rpc: Arc, - pub user_store: Arc, + pub user_store: ModelHandle, pub fs: Arc, pub channel_list: ModelHandle, } diff --git a/zed/src/main.rs b/zed/src/main.rs index 87426c1ca7bac448aa9b0f2d49ee341687f5f525..c88b1465d14742e69341ee7a68513555fa937d08 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -38,7 +38,7 @@ fn main() { app.run(move |cx| { let rpc = rpc::Client::new(); let http = http::client(); - let user_store = UserStore::new(rpc.clone(), http.clone(), cx.background()); + let user_store = cx.add_model(|cx| UserStore::new(rpc.clone(), http.clone(), cx)); let app_state = Arc::new(AppState { languages: languages.clone(), settings_tx: Arc::new(Mutex::new(settings_tx)), diff --git a/zed/src/menus.rs b/zed/src/menus.rs index 9ba28f9cef875b699bdc9ab5cc401c2f0c87332e..e885d3120955930966708565d417aa26298f9740 100644 --- a/zed/src/menus.rs +++ b/zed/src/menus.rs @@ -16,21 +16,6 @@ pub fn menus(state: &Arc) -> Vec> { action: Box::new(super::About), }, MenuItem::Separator, - MenuItem::Action { - name: "Sign In", - keystroke: None, - action: Box::new(super::Authenticate), - }, - MenuItem::Action { - name: "Share", - keystroke: None, - action: Box::new(workspace::ShareWorktree), - }, - MenuItem::Action { - name: "Join", - keystroke: None, - action: Box::new(workspace::JoinWorktree(state.clone())), - }, MenuItem::Action { name: "Quit", keystroke: Some("cmd-q"), diff --git a/zed/src/people_panel.rs b/zed/src/people_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..10d3bdaa0274cd883cf3504014d7ae22f44d2fdd --- /dev/null +++ b/zed/src/people_panel.rs @@ -0,0 +1,267 @@ +use crate::{ + theme::Theme, + user::{Collaborator, UserStore}, + Settings, +}; +use gpui::{ + action, + elements::*, + geometry::{rect::RectF, vector::vec2f}, + platform::CursorStyle, + Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View, + ViewContext, +}; +use postage::watch; + +action!(JoinWorktree, u64); +action!(LeaveWorktree, u64); +action!(ShareWorktree, u64); +action!(UnshareWorktree, u64); + +pub struct PeoplePanel { + collaborators: ListState, + user_store: ModelHandle, + settings: watch::Receiver, + _maintain_collaborators: Subscription, +} + +impl PeoplePanel { + pub fn new( + user_store: ModelHandle, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + Self { + collaborators: ListState::new( + user_store.read(cx).collaborators().len(), + Orientation::Top, + 1000., + { + let user_store = user_store.clone(); + let settings = settings.clone(); + move |ix, cx| { + let user_store = user_store.read(cx); + let collaborators = user_store.collaborators().clone(); + let current_user_id = user_store.current_user().map(|user| user.id); + Self::render_collaborator( + &collaborators[ix], + current_user_id, + &settings.borrow().theme, + cx, + ) + } + }, + ), + _maintain_collaborators: cx.observe(&user_store, Self::update_collaborators), + user_store, + settings, + } + } + + fn update_collaborators(&mut self, _: ModelHandle, cx: &mut ViewContext) { + self.collaborators + .reset(self.user_store.read(cx).collaborators().len()); + cx.notify(); + } + + fn render_collaborator( + collaborator: &Collaborator, + current_user_id: Option, + theme: &Theme, + cx: &mut LayoutContext, + ) -> ElementBox { + let theme = &theme.people_panel; + let worktree_count = collaborator.worktrees.len(); + let font_cache = cx.font_cache(); + let line_height = theme.unshared_worktree.name.text.line_height(font_cache); + let cap_height = theme.unshared_worktree.name.text.cap_height(font_cache); + let baseline_offset = theme + .unshared_worktree + .name + .text + .baseline_offset(font_cache) + + (theme.unshared_worktree.height - line_height) / 2.; + let tree_branch_width = theme.tree_branch_width; + let tree_branch_color = theme.tree_branch_color; + let host_avatar_height = theme + .host_avatar + .width + .or(theme.host_avatar.height) + .unwrap_or(0.); + + Flex::column() + .with_child( + Flex::row() + .with_children(collaborator.user.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(theme.host_avatar) + .aligned() + .left() + .boxed() + })) + .with_child( + Label::new( + collaborator.user.github_login.clone(), + theme.host_username.text.clone(), + ) + .contained() + .with_style(theme.host_username.container) + .aligned() + .left() + .boxed(), + ) + .constrained() + .with_height(theme.host_row_height) + .boxed(), + ) + .with_children( + collaborator + .worktrees + .iter() + .enumerate() + .map(|(ix, worktree)| { + let worktree_id = worktree.id; + + Flex::row() + .with_child( + Canvas::new(move |bounds, _, cx| { + let start_x = bounds.min_x() + (bounds.width() / 2.) + - (tree_branch_width / 2.); + let end_x = bounds.max_x(); + let start_y = bounds.min_y(); + let end_y = + bounds.min_y() + baseline_offset - (cap_height / 2.); + + cx.scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + tree_branch_width, + if ix + 1 == worktree_count { + end_y + } else { + bounds.max_y() + }, + ), + ), + background: Some(tree_branch_color), + border: gpui::Border::default(), + corner_radius: 0., + }); + cx.scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, end_y), + vec2f(end_x, end_y + tree_branch_width), + ), + background: Some(tree_branch_color), + border: gpui::Border::default(), + corner_radius: 0., + }); + }) + .constrained() + .with_width(host_avatar_height) + .boxed(), + ) + .with_child({ + let is_host = Some(collaborator.user.id) == current_user_id; + let is_guest = !is_host + && worktree + .guests + .iter() + .any(|guest| Some(guest.id) == current_user_id); + let is_shared = worktree.is_shared; + + MouseEventHandler::new::( + worktree_id as usize, + cx, + |mouse_state, _| { + let style = match (worktree.is_shared, mouse_state.hovered) + { + (false, false) => &theme.unshared_worktree, + (false, true) => &theme.hovered_unshared_worktree, + (true, false) => &theme.shared_worktree, + (true, true) => &theme.hovered_shared_worktree, + }; + + Flex::row() + .with_child( + Label::new( + worktree.root_name.clone(), + style.name.text.clone(), + ) + .aligned() + .left() + .contained() + .with_style(style.name.container) + .boxed(), + ) + .with_children(worktree.guests.iter().filter_map( + |participant| { + participant.avatar.clone().map(|avatar| { + Image::new(avatar) + .with_style(style.guest_avatar) + .aligned() + .left() + .contained() + .with_margin_right( + style.guest_avatar_spacing, + ) + .boxed() + }) + }, + )) + .contained() + .with_style(style.container) + .constrained() + .with_height(style.height) + .boxed() + }, + ) + .with_cursor_style(if is_host || is_shared { + CursorStyle::PointingHand + } else { + CursorStyle::Arrow + }) + .on_click(move |cx| { + if is_shared { + if is_host { + cx.dispatch_action(UnshareWorktree(worktree_id)); + } else if is_guest { + cx.dispatch_action(LeaveWorktree(worktree_id)); + } else { + cx.dispatch_action(JoinWorktree(worktree_id)) + } + } else if is_host { + cx.dispatch_action(ShareWorktree(worktree_id)); + } + }) + .expanded(1.0) + .boxed() + }) + .constrained() + .with_height(theme.unshared_worktree.height) + .boxed() + }), + ) + .boxed() + } +} + +pub enum Event {} + +impl Entity for PeoplePanel { + type Event = Event; +} + +impl View for PeoplePanel { + fn ui_name() -> &'static str { + "PeoplePanel" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + let theme = &self.settings.borrow().theme.people_panel; + Container::new(List::new(self.collaborators.clone()).boxed()) + .with_style(theme.container) + .boxed() + } +} diff --git a/zed/src/rpc.rs b/zed/src/rpc.rs index d4315a44d42f7de2b40a07eeaabefd7d1994f7c8..5dc2b49b76d9ac8ca958f6dc3903a8da3603dbcd 100644 --- a/zed/src/rpc.rs +++ b/zed/src/rpc.rs @@ -14,6 +14,7 @@ use std::{ any::TypeId, collections::HashMap, convert::TryFrom, + fmt::Write as _, future::Future, sync::{Arc, Weak}, time::{Duration, Instant}, @@ -29,6 +30,9 @@ use zrpc::{ lazy_static! { static ref ZED_SERVER_URL: String = std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev:443".to_string()); + static ref IMPERSONATE_LOGIN: Option = std::env::var("ZED_IMPERSONATE") + .ok() + .and_then(|s| if s.is_empty() { None } else { Some(s) }); } pub struct Client { @@ -230,7 +234,51 @@ impl Client { } } - pub fn subscribe_from_model( + pub fn subscribe( + self: &Arc, + cx: &mut ModelContext, + mut handler: F, + ) -> Subscription + where + T: EnvelopedMessage, + M: Entity, + F: 'static + + Send + + Sync + + FnMut(&mut M, TypedEnvelope, Arc, &mut ModelContext) -> Result<()>, + { + let subscription_id = (TypeId::of::(), Default::default()); + let client = self.clone(); + let mut state = self.state.write(); + let model = cx.handle().downgrade(); + let prev_extractor = state + .entity_id_extractors + .insert(subscription_id.0, Box::new(|_| Default::default())); + if prev_extractor.is_some() { + panic!("registered a handler for the same entity twice") + } + + state.model_handlers.insert( + subscription_id, + Box::new(move |envelope, cx| { + if let Some(model) = model.upgrade(cx) { + let envelope = envelope.into_any().downcast::>().unwrap(); + model.update(cx, |model, cx| { + if let Err(error) = handler(model, *envelope, client.clone(), cx) { + log::error!("error handling message: {}", error) + } + }); + } + }), + ); + + Subscription { + client: Arc::downgrade(self), + id: subscription_id, + } + } + + pub fn subscribe_to_entity( self: &Arc, remote_id: u64, cx: &mut ModelContext, @@ -306,12 +354,12 @@ impl Client { self.set_status(Status::Reauthenticating, cx) } - let mut read_from_keychain = false; + let mut used_keychain = false; let credentials = self.state.read().credentials.clone(); let credentials = if let Some(credentials) = credentials { credentials } else if let Some(credentials) = read_credentials_from_keychain(cx) { - read_from_keychain = true; + used_keychain = true; credentials } else { let credentials = match self.authenticate(&cx).await { @@ -334,7 +382,7 @@ impl Client { Ok(conn) => { log::info!("connected to rpc address {}", *ZED_SERVER_URL); self.state.write().credentials = Some(credentials.clone()); - if !read_from_keychain { + if !used_keychain && IMPERSONATE_LOGIN.is_none() { write_credentials_to_keychain(&credentials, cx).log_err(); } self.set_connection(conn, cx).await; @@ -343,8 +391,8 @@ impl Client { Err(err) => { if matches!(err, EstablishConnectionError::Unauthorized) { self.state.write().credentials.take(); - cx.platform().delete_credentials(&ZED_SERVER_URL).log_err(); - if read_from_keychain { + if used_keychain { + cx.platform().delete_credentials(&ZED_SERVER_URL).log_err(); self.set_status(Status::SignedOut, cx); self.authenticate_and_connect(cx).await } else { @@ -484,10 +532,17 @@ impl Client { // Open the Zed sign-in page in the user's browser, with query parameters that indicate // that the user is signing in from a Zed app running on the same device. - platform.open_url(&format!( + let mut url = format!( "{}/sign_in?native_app_port={}&native_app_public_key={}", *ZED_SERVER_URL, port, public_key_string - )); + ); + + if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() { + log::info!("impersonating user @{}", impersonate_login); + write!(&mut url, "&impersonate={}", impersonate_login).unwrap(); + } + + platform.open_url(&url); // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted // access token from the query params. @@ -571,6 +626,10 @@ impl Client { } fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option { + if IMPERSONATE_LOGIN.is_some() { + return None; + } + let (user_id, access_token) = cx .platform() .read_credentials(&ZED_SERVER_URL) diff --git a/zed/src/test.rs b/zed/src/test.rs index 7d027a8a1771b41fd2b68844802a2f6939b6448b..eb6bf20f456236a5158c3a5e8d6a9311a7eddc40 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -168,7 +168,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); let rpc = rpc::Client::new(); let http = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) }); - let user_store = UserStore::new(rpc.clone(), http, cx.background()); + let user_store = cx.add_model(|cx| UserStore::new(rpc.clone(), http, cx)); Arc::new(AppState { settings_tx: Arc::new(Mutex::new(settings_tx)), settings, diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 4458183e6dde3ef563cdd3fac1c60d56ce996553..8b43b09f13d28fdd1cafdf0871a12123efa12f96 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -24,6 +24,7 @@ pub struct Theme { pub name: String, pub workspace: Workspace, pub chat_panel: ChatPanel, + pub people_panel: PeoplePanel, pub selector: Selector, pub editor: EditorStyle, pub syntax: SyntaxTheme, @@ -104,6 +105,31 @@ pub struct ChatPanel { pub hovered_sign_in_prompt: TextStyle, } +#[derive(Deserialize)] +pub struct PeoplePanel { + #[serde(flatten)] + pub container: ContainerStyle, + pub host_row_height: f32, + pub host_avatar: ImageStyle, + pub host_username: ContainedText, + pub tree_branch_width: f32, + pub tree_branch_color: Color, + pub shared_worktree: WorktreeRow, + pub hovered_shared_worktree: WorktreeRow, + pub unshared_worktree: WorktreeRow, + pub hovered_unshared_worktree: WorktreeRow, +} + +#[derive(Deserialize)] +pub struct WorktreeRow { + #[serde(flatten)] + pub container: ContainerStyle, + pub height: f32, + pub name: ContainedText, + pub guest_avatar: ImageStyle, + pub guest_avatar_spacing: f32, +} + #[derive(Deserialize)] pub struct ChatMessage { #[serde(flatten)] @@ -143,7 +169,7 @@ pub struct Selector { pub active_item: ContainedLabel, } -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] pub struct ContainedText { #[serde(flatten)] pub container: ContainerStyle, diff --git a/zed/src/theme/resolution.rs b/zed/src/theme/resolution.rs index fd3864e274af20f75fbfe4da54f43fcdcdecc6c3..acebf72b86120065cb8587d05c2a8db92a00cced 100644 --- a/zed/src/theme/resolution.rs +++ b/zed/src/theme/resolution.rs @@ -237,9 +237,12 @@ impl Tree { fn update_resolved(&self) { match &mut *self.0.borrow_mut() { Node::Object { - resolved, children, .. + resolved, + base, + children, + .. } => { - *resolved = children.values().all(|c| c.is_resolved()); + *resolved = base.is_none() && children.values().all(|c| c.is_resolved()); } Node::Array { resolved, children, .. @@ -261,6 +264,9 @@ impl Tree { if tree.is_resolved() { while let Some(parent) = tree.parent() { parent.update_resolved(); + if !parent.is_resolved() { + break; + } tree = parent; } } @@ -330,9 +336,10 @@ impl Tree { made_progress = true; } - if let Node::Object { resolved, .. } = &mut *self.0.borrow_mut() { + if let Node::Object { resolved, base, .. } = &mut *self.0.borrow_mut() { if has_base { if resolved_base.is_some() { + base.take(); *resolved = true; } else { unresolved.push(self.clone()); @@ -341,6 +348,8 @@ impl Tree { *resolved = true; } } + } else if base.is_some() { + unresolved.push(self.clone()); } Ok(made_progress) @@ -427,6 +436,7 @@ mod test { fn test_references() { let json = serde_json::json!({ "a": { + "extends": "$g", "x": "$b.d" }, "b": { @@ -436,6 +446,9 @@ mod test { "e": { "extends": "$a", "f": "1" + }, + "g": { + "h": 2 } }); @@ -443,19 +456,27 @@ mod test { resolve_references(json).unwrap(), serde_json::json!({ "a": { - "x": "1" + "extends": "$g", + "x": "1", + "h": 2 }, "b": { "c": { - "x": "1" + "extends": "$g", + "x": "1", + "h": 2 }, "d": "1" }, "e": { "extends": "$a", "f": "1", - "x": "1" + "x": "1", + "h": 2 }, + "g": { + "h": 2 + } }) ) } diff --git a/zed/src/user.rs b/zed/src/user.rs index 54e84d756ff81229a44dcb5291e12fec1618da27..7467b16f9b524bb72f6964cbfa694681f25151da 100644 --- a/zed/src/user.rs +++ b/zed/src/user.rs @@ -5,14 +5,13 @@ use crate::{ }; use anyhow::{anyhow, Context, Result}; use futures::future; -use gpui::{executor, ImageData, Task}; -use parking_lot::Mutex; -use postage::{oneshot, prelude::Stream, sink::Sink, watch}; +use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; +use postage::{prelude::Stream, sink::Sink, watch}; use std::{ - collections::HashMap, - sync::{Arc, Weak}, + collections::{HashMap, HashSet}, + sync::Arc, }; -use zrpc::proto; +use zrpc::{proto, TypedEnvelope}; #[derive(Debug)] pub struct User { @@ -21,42 +20,75 @@ pub struct User { pub avatar: Option>, } +#[derive(Debug)] +pub struct Collaborator { + pub user: Arc, + pub worktrees: Vec, +} + +#[derive(Debug)] +pub struct WorktreeMetadata { + pub id: u64, + pub root_name: String, + pub is_shared: bool, + pub guests: Vec>, +} + pub struct UserStore { - users: Mutex>>, + users: HashMap>, current_user: watch::Receiver>>, + collaborators: Arc<[Collaborator]>, rpc: Arc, http: Arc, + _maintain_collaborators: Task<()>, _maintain_current_user: Task<()>, } +pub enum Event {} + +impl Entity for UserStore { + type Event = Event; +} + impl UserStore { - pub fn new( - rpc: Arc, - http: Arc, - executor: &executor::Background, - ) -> Arc { + pub fn new(rpc: Arc, http: Arc, cx: &mut ModelContext) -> Self { let (mut current_user_tx, current_user_rx) = watch::channel(); - let (mut this_tx, mut this_rx) = oneshot::channel::>(); - let this = Arc::new(Self { + let (mut update_collaborators_tx, mut update_collaborators_rx) = + watch::channel::>(); + let update_collaborators_subscription = rpc.subscribe( + cx, + move |_: &mut Self, msg: TypedEnvelope, _, _| { + let _ = update_collaborators_tx.blocking_send(Some(msg.payload)); + Ok(()) + }, + ); + Self { users: Default::default(), current_user: current_user_rx, + collaborators: Arc::from([]), rpc: rpc.clone(), http, - _maintain_current_user: executor.spawn(async move { - let this = if let Some(this) = this_rx.recv().await { - this - } else { - return; - }; + _maintain_collaborators: cx.spawn_weak(|this, mut cx| async move { + let _subscription = update_collaborators_subscription; + while let Some(message) = update_collaborators_rx.recv().await { + if let Some((message, this)) = message.zip(this.upgrade(&cx)) { + this.update(&mut cx, |this, cx| this.update_collaborators(message, cx)) + .log_err() + .await; + } + } + }), + _maintain_current_user: cx.spawn_weak(|this, mut cx| async move { let mut status = rpc.status(); while let Some(status) = status.recv().await { match status { Status::Connected { .. } => { - if let Some((this, user_id)) = this.upgrade().zip(rpc.user_id()) { - current_user_tx - .send(this.fetch_user(user_id).log_err().await) - .await - .ok(); + if let Some((this, user_id)) = this.upgrade(&cx).zip(rpc.user_id()) { + let user = this + .update(&mut cx, |this, cx| this.fetch_user(user_id, cx)) + .log_err() + .await; + current_user_tx.send(user).await.ok(); } } Status::SignedOut => { @@ -66,49 +98,100 @@ impl UserStore { } } }), - }); - let weak = Arc::downgrade(&this); - executor - .spawn(async move { this_tx.send(weak).await }) - .detach(); - this + } } - pub async fn load_users(&self, mut user_ids: Vec) -> Result<()> { - { - let users = self.users.lock(); - user_ids.retain(|id| !users.contains_key(id)); + fn update_collaborators( + &mut self, + message: proto::UpdateCollaborators, + cx: &mut ModelContext, + ) -> Task> { + let mut user_ids = HashSet::new(); + for collaborator in &message.collaborators { + user_ids.insert(collaborator.user_id); + user_ids.extend( + collaborator + .worktrees + .iter() + .flat_map(|w| &w.guests) + .copied(), + ); } - if !user_ids.is_empty() { - let response = self.rpc.request(proto::GetUsers { user_ids }).await?; - let new_users = future::join_all( - response - .users - .into_iter() - .map(|user| User::new(user, self.http.as_ref())), - ) - .await; - let mut users = self.users.lock(); - for user in new_users { - users.insert(user.id, Arc::new(user)); + let load_users = self.load_users(user_ids.into_iter().collect(), cx); + cx.spawn(|this, mut cx| async move { + load_users.await?; + + let mut collaborators = Vec::new(); + for collaborator in message.collaborators { + collaborators.push(Collaborator::from_proto(collaborator, &this, &mut cx).await?); } - } - Ok(()) + this.update(&mut cx, |this, cx| { + collaborators.sort_by(|a, b| a.user.github_login.cmp(&b.user.github_login)); + this.collaborators = collaborators.into(); + cx.notify(); + }); + + Ok(()) + }) + } + + pub fn collaborators(&self) -> &Arc<[Collaborator]> { + &self.collaborators } - pub async fn fetch_user(&self, user_id: u64) -> Result> { - if let Some(user) = self.users.lock().get(&user_id).cloned() { - return Ok(user); + pub fn load_users( + &mut self, + mut user_ids: Vec, + cx: &mut ModelContext, + ) -> Task> { + let rpc = self.rpc.clone(); + let http = self.http.clone(); + user_ids.retain(|id| !self.users.contains_key(id)); + cx.spawn_weak(|this, mut cx| async move { + if !user_ids.is_empty() { + let response = rpc.request(proto::GetUsers { user_ids }).await?; + let new_users = future::join_all( + response + .users + .into_iter() + .map(|user| User::new(user, http.as_ref())), + ) + .await; + + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, _| { + for user in new_users { + this.users.insert(user.id, Arc::new(user)); + } + }); + } + } + + Ok(()) + }) + } + + pub fn fetch_user( + &mut self, + user_id: u64, + cx: &mut ModelContext, + ) -> Task>> { + if let Some(user) = self.users.get(&user_id).cloned() { + return cx.spawn_weak(|_, _| async move { Ok(user) }); } - self.load_users(vec![user_id]).await?; - self.users - .lock() - .get(&user_id) - .cloned() - .ok_or_else(|| anyhow!("server responded with no users")) + let load_users = self.load_users(vec![user_id], cx); + cx.spawn(|this, mut cx| async move { + load_users.await?; + this.update(&mut cx, |this, _| { + this.users + .get(&user_id) + .cloned() + .ok_or_else(|| anyhow!("server responded with no users")) + }) + }) } pub fn current_user(&self) -> Option> { @@ -130,6 +213,40 @@ impl User { } } +impl Collaborator { + async fn from_proto( + collaborator: proto::Collaborator, + user_store: &ModelHandle, + cx: &mut AsyncAppContext, + ) -> Result { + let user = user_store + .update(cx, |user_store, cx| { + user_store.fetch_user(collaborator.user_id, cx) + }) + .await?; + let mut worktrees = Vec::new(); + for worktree in collaborator.worktrees { + let mut guests = Vec::new(); + for participant_id in worktree.guests { + guests.push( + user_store + .update(cx, |user_store, cx| { + user_store.fetch_user(participant_id, cx) + }) + .await?, + ); + } + worktrees.push(WorktreeMetadata { + id: worktree.id, + root_name: worktree.root_name, + is_shared: worktree.is_shared, + guests, + }); + } + Ok(Self { user, worktrees }) + } +} + async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result> { let url = Url::parse(url).with_context(|| format!("failed to parse avatar url {:?}", url))?; let mut request = Request::new(Method::Get, url); diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index e69d9b2bb4f25b199c12f539771111e4a1c5c055..df4afb020e3246d09f462fb7ad85cd7fe72d13a2 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -7,14 +7,16 @@ use crate::{ editor::Buffer, fs::Fs, language::LanguageRegistry, + people_panel::{JoinWorktree, LeaveWorktree, PeoplePanel, ShareWorktree, UnshareWorktree}, project_browser::ProjectBrowser, rpc, settings::Settings, user, - worktree::{File, Worktree}, + util::TryFutureExt as _, + worktree::{self, File, Worktree}, AppState, Authenticate, }; -use anyhow::{anyhow, Result}; +use anyhow::Result; use gpui::{ action, elements::*, @@ -41,8 +43,6 @@ use std::{ action!(Open, Arc); action!(OpenPaths, OpenParams); action!(OpenNew, Arc); -action!(ShareWorktree); -action!(JoinWorktree, Arc); action!(Save); action!(DebugElements); @@ -52,13 +52,14 @@ pub fn init(cx: &mut MutableAppContext) { open_paths(action, cx).detach() }); cx.add_global_action(open_new); - cx.add_global_action(join_worktree); cx.add_action(Workspace::save_active_item); cx.add_action(Workspace::debug_elements); cx.add_action(Workspace::open_new_file); + cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::share_worktree); + cx.add_action(Workspace::unshare_worktree); cx.add_action(Workspace::join_worktree); - cx.add_action(Workspace::toggle_sidebar_item); + cx.add_action(Workspace::leave_worktree); cx.add_bindings(vec![ Binding::new("cmd-s", Save, None), Binding::new("cmd-alt-i", DebugElements, None), @@ -129,14 +130,6 @@ fn open_new(action: &OpenNew, cx: &mut MutableAppContext) { }); } -fn join_worktree(action: &JoinWorktree, cx: &mut MutableAppContext) { - cx.add_window(window_options(), |cx| { - let mut view = Workspace::new(action.0.as_ref(), cx); - view.join_worktree(action, cx); - view - }); -} - fn window_options() -> WindowOptions<'static> { WindowOptions { bounds: RectF::new(vec2f(0., 0.), vec2f(1024., 768.)), @@ -183,6 +176,9 @@ pub trait ItemView: View { fn should_activate_item_on_event(_: &Self::Event) -> bool { false } + fn should_close_item_on_event(_: &Self::Event) -> bool { + false + } fn should_update_tab_on_event(_: &Self::Event) -> bool { false } @@ -281,6 +277,10 @@ impl ItemViewHandle for ViewHandle { fn set_parent_pane(&self, pane: &ViewHandle, cx: &mut MutableAppContext) { pane.update(cx, |_, cx| { cx.subscribe(self, |pane, item, event, cx| { + if T::should_close_item_on_event(event) { + pane.close_item(item.id(), cx); + return; + } if T::should_activate_item_on_event(event) { if let Some(ix) = pane.item_index(&item) { pane.activate_item(ix, cx); @@ -341,7 +341,7 @@ pub struct Workspace { pub settings: watch::Receiver, languages: Arc, rpc: Arc, - user_store: Arc, + user_store: ModelHandle, fs: Arc, modal: Option, center: PaneGroup, @@ -375,6 +375,13 @@ impl Workspace { ); let mut right_sidebar = Sidebar::new(Side::Right); + right_sidebar.add_item( + "icons/user-16.svg", + cx.add_view(|cx| { + PeoplePanel::new(app_state.user_store.clone(), app_state.settings.clone(), cx) + }) + .into(), + ); right_sidebar.add_item( "icons/comment-16.svg", cx.add_view(|cx| { @@ -387,9 +394,8 @@ impl Workspace { }) .into(), ); - right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into()); - let mut current_user = app_state.user_store.watch_current_user().clone(); + let mut current_user = app_state.user_store.read(cx).watch_current_user().clone(); let mut connection_status = app_state.rpc.status().clone(); let _observe_current_user = cx.spawn_weak(|this, mut cx| async move { current_user.recv().await; @@ -546,10 +552,11 @@ impl Workspace { cx: &mut ViewContext, ) -> Task>> { let languages = self.languages.clone(); + let rpc = self.rpc.clone(); let fs = self.fs.clone(); let path = Arc::from(path); cx.spawn(|this, mut cx| async move { - let worktree = Worktree::open_local(path, languages, fs, &mut cx).await?; + let worktree = Worktree::open_local(rpc, path, fs, languages, &mut cx).await?; this.update(&mut cx, |this, cx| { cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); this.worktrees.insert(worktree.clone()); @@ -815,69 +822,122 @@ impl Workspace { }; } - fn share_worktree(&mut self, _: &ShareWorktree, cx: &mut ViewContext) { + fn share_worktree(&mut self, action: &ShareWorktree, cx: &mut ViewContext) { let rpc = self.rpc.clone(); - let platform = cx.platform(); - - let task = cx.spawn(|this, mut cx| async move { - rpc.authenticate_and_connect(&cx).await?; + let remote_id = action.0; + cx.spawn(|this, mut cx| { + async move { + rpc.authenticate_and_connect(&cx).await?; + + let task = this.update(&mut cx, |this, cx| { + for worktree in &this.worktrees { + let task = worktree.update(cx, |worktree, cx| { + worktree.as_local_mut().and_then(|worktree| { + if worktree.remote_id() == Some(remote_id) { + Some(worktree.share(cx)) + } else { + None + } + }) + }); - let share_task = this.update(&mut cx, |this, cx| { - let worktree = this.worktrees.iter().next()?; - worktree.update(cx, |worktree, cx| { - let worktree = worktree.as_local_mut()?; - Some(worktree.share(rpc, cx)) - }) - }); + if task.is_some() { + return task; + } + } + None + }); - if let Some(share_task) = share_task { - let (worktree_id, access_token) = share_task.await?; - let worktree_url = rpc::encode_worktree_url(worktree_id, &access_token); - log::info!("wrote worktree url to clipboard: {}", worktree_url); - platform.write_to_clipboard(ClipboardItem::new(worktree_url)); - } - surf::Result::Ok(()) - }); + if let Some(share_task) = task { + share_task.await?; + } - cx.spawn(|_, _| async move { - if let Err(e) = task.await { - log::error!("sharing failed: {:?}", e); + Ok(()) } + .log_err() }) .detach(); } - fn join_worktree(&mut self, _: &JoinWorktree, cx: &mut ViewContext) { + fn unshare_worktree(&mut self, action: &UnshareWorktree, cx: &mut ViewContext) { + let remote_id = action.0; + for worktree in &self.worktrees { + if worktree.update(cx, |worktree, cx| { + if let Some(worktree) = worktree.as_local_mut() { + if worktree.remote_id() == Some(remote_id) { + worktree.unshare(cx); + return true; + } + } + false + }) { + break; + } + } + } + + fn join_worktree(&mut self, action: &JoinWorktree, cx: &mut ViewContext) { let rpc = self.rpc.clone(); let languages = self.languages.clone(); + let worktree_id = action.0; + + cx.spawn(|this, mut cx| { + async move { + rpc.authenticate_and_connect(&cx).await?; + let worktree = + Worktree::open_remote(rpc.clone(), worktree_id, languages, &mut cx).await?; + this.update(&mut cx, |this, cx| { + cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); + cx.subscribe(&worktree, move |this, _, event, cx| match event { + worktree::Event::Closed => { + this.worktrees.retain(|worktree| { + worktree.update(cx, |worktree, cx| { + if let Some(worktree) = worktree.as_remote_mut() { + if worktree.remote_id() == worktree_id { + worktree.close_all_buffers(cx); + return false; + } + } + true + }) + }); - let task = cx.spawn(|this, mut cx| async move { - rpc.authenticate_and_connect(&cx).await?; - - let worktree_url = cx - .platform() - .read_from_clipboard() - .ok_or_else(|| anyhow!("failed to read url from clipboard"))?; - let (worktree_id, access_token) = rpc::decode_worktree_url(worktree_url.text()) - .ok_or_else(|| anyhow!("failed to decode worktree url"))?; - log::info!("read worktree url from clipboard: {}", worktree_url.text()); + cx.notify(); + } + }) + .detach(); + this.worktrees.insert(worktree); + cx.notify(); + }); - let worktree = - Worktree::open_remote(rpc.clone(), worktree_id, access_token, languages, &mut cx) - .await?; - this.update(&mut cx, |workspace, cx| { - cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); - workspace.worktrees.insert(worktree); - cx.notify(); - }); + Ok(()) + } + .log_err() + }) + .detach(); + } - surf::Result::Ok(()) - }); + fn leave_worktree(&mut self, action: &LeaveWorktree, cx: &mut ViewContext) { + let remote_id = action.0; + cx.spawn(|this, mut cx| { + async move { + this.update(&mut cx, |this, cx| { + this.worktrees.retain(|worktree| { + worktree.update(cx, |worktree, cx| { + if let Some(worktree) = worktree.as_remote_mut() { + if worktree.remote_id() == remote_id { + worktree.close_all_buffers(cx); + return false; + } + } + true + }) + }) + }); - cx.spawn(|_, _| async move { - if let Err(e) = task.await { - log::error!("joining failed: {}", e); + Ok(()) } + .log_err() }) .detach(); } @@ -989,6 +1049,7 @@ impl Workspace { let theme = &self.settings.borrow().theme; let avatar = if let Some(avatar) = self .user_store + .read(cx) .current_user() .and_then(|user| user.avatar.clone()) { diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index f94269c6aaca0c366b277f3b3ccdf6491d83b862..de819af1411ca3abaef9604f9e4b4ad6c09d41e1 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -7,7 +7,7 @@ use crate::{ fuzzy, fuzzy::CharBag, language::{Language, LanguageRegistry}, - rpc::{self, proto}, + rpc::{self, proto, Status}, time::{self, ReplicaId}, util::{Bias, TryFutureExt}, }; @@ -27,6 +27,7 @@ use postage::{ prelude::{Sink as _, Stream as _}, watch, }; +use serde::Deserialize; use smol::channel::{self, Sender}; use std::{ cmp::{self, Ordering}, @@ -61,37 +62,50 @@ pub enum Worktree { Remote(RemoteWorktree), } +pub enum Event { + Closed, +} + impl Entity for Worktree { - type Event = (); + type Event = Event; fn release(&mut self, cx: &mut MutableAppContext) { - let rpc = match self { - Self::Local(tree) => tree - .share - .as_ref() - .map(|share| (share.rpc.clone(), share.remote_id)), - Self::Remote(tree) => Some((tree.rpc.clone(), tree.remote_id)), - }; - - if let Some((rpc, worktree_id)) = rpc { - cx.spawn(|_| async move { - if let Err(err) = rpc.send(proto::CloseWorktree { worktree_id }).await { - log::error!("error closing worktree {}: {}", worktree_id, err); + match self { + Self::Local(tree) => { + if let Some(worktree_id) = *tree.remote_id.borrow() { + let rpc = tree.rpc.clone(); + cx.spawn(|_| async move { + if let Err(err) = rpc.send(proto::CloseWorktree { worktree_id }).await { + log::error!("error closing worktree: {}", err); + } + }) + .detach(); } - }) - .detach(); + } + Self::Remote(tree) => { + let rpc = tree.rpc.clone(); + let worktree_id = tree.remote_id; + cx.spawn(|_| async move { + if let Err(err) = rpc.send(proto::LeaveWorktree { worktree_id }).await { + log::error!("error closing worktree: {}", err); + } + }) + .detach(); + } } } } impl Worktree { pub async fn open_local( + rpc: Arc, path: impl Into>, - languages: Arc, fs: Arc, + languages: Arc, cx: &mut AsyncAppContext, ) -> Result> { - let (tree, scan_states_tx) = LocalWorktree::new(path, languages, fs.clone(), cx).await?; + let (tree, scan_states_tx) = + LocalWorktree::new(rpc, path, fs.clone(), languages, cx).await?; tree.update(cx, |tree, cx| { let tree = tree.as_local_mut().unwrap(); let abs_path = tree.snapshot.abs_path.clone(); @@ -110,33 +124,26 @@ impl Worktree { pub async fn open_remote( rpc: Arc, id: u64, - access_token: String, languages: Arc, cx: &mut AsyncAppContext, ) -> Result> { - let response = rpc - .request(proto::OpenWorktree { - worktree_id: id, - access_token, - }) - .await?; - + let response = rpc.request(proto::JoinWorktree { worktree_id: id }).await?; Worktree::remote(response, rpc, languages, cx).await } async fn remote( - open_response: proto::OpenWorktreeResponse, + join_response: proto::JoinWorktreeResponse, rpc: Arc, languages: Arc, cx: &mut AsyncAppContext, ) -> Result> { - let worktree = open_response + let worktree = join_response .worktree .ok_or_else(|| anyhow!("empty worktree"))?; - let remote_id = open_response.worktree_id; - let replica_id = open_response.replica_id as ReplicaId; - let peers = open_response.peers; + let remote_id = worktree.id; + let replica_id = join_response.replica_id as ReplicaId; + let peers = join_response.peers; let root_char_bag: CharBag = worktree .root_name .chars() @@ -215,11 +222,12 @@ impl Worktree { } let _subscriptions = vec![ - rpc.subscribe_from_model(remote_id, cx, Self::handle_add_peer), - rpc.subscribe_from_model(remote_id, cx, Self::handle_remove_peer), - rpc.subscribe_from_model(remote_id, cx, Self::handle_update), - rpc.subscribe_from_model(remote_id, cx, Self::handle_update_buffer), - rpc.subscribe_from_model(remote_id, cx, Self::handle_buffer_saved), + rpc.subscribe_to_entity(remote_id, cx, Self::handle_add_peer), + rpc.subscribe_to_entity(remote_id, cx, Self::handle_remove_peer), + rpc.subscribe_to_entity(remote_id, cx, Self::handle_update), + rpc.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer), + rpc.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved), + rpc.subscribe_to_entity(remote_id, cx, Self::handle_unshare), ]; Worktree::Remote(RemoteWorktree { @@ -519,6 +527,16 @@ impl Worktree { Ok(()) } + pub fn handle_unshare( + &mut self, + _: TypedEnvelope, + _: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + cx.emit(Event::Closed); + Ok(()) + } + fn poll_snapshot(&mut self, cx: &mut ModelContext) { match self { Self::Local(worktree) => { @@ -648,24 +666,34 @@ impl Deref for Worktree { pub struct LocalWorktree { snapshot: Snapshot, + config: WorktreeConfig, background_snapshot: Arc>, last_scan_state_rx: watch::Receiver, _background_scanner_task: Option>, + _maintain_remote_id_task: Task>, poll_task: Option>, + remote_id: watch::Receiver>, share: Option, open_buffers: HashMap>, shared_buffers: HashMap>>, peers: HashMap, languages: Arc, queued_operations: Vec<(u64, Operation)>, + rpc: Arc, fs: Arc, } +#[derive(Default, Deserialize)] +struct WorktreeConfig { + collaborators: Vec, +} + impl LocalWorktree { async fn new( + rpc: Arc, path: impl Into>, - languages: Arc, fs: Arc, + languages: Arc, cx: &mut AsyncAppContext, ) -> Result<(ModelHandle, Sender)> { let abs_path = path.into(); @@ -680,6 +708,13 @@ impl LocalWorktree { let root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect(); let metadata = fs.metadata(&abs_path).await?; + let mut config = WorktreeConfig::default(); + if let Ok(zed_toml) = fs.load(&abs_path.join(".zed.toml")).await { + if let Ok(parsed) = toml::from_str(&zed_toml) { + config = parsed; + } + } + let (scan_states_tx, scan_states_rx) = smol::channel::unbounded(); let (mut last_scan_state_tx, last_scan_state_rx) = watch::channel_with(ScanState::Scanning); let tree = cx.add_model(move |cx: &mut ModelContext| { @@ -687,7 +722,7 @@ impl LocalWorktree { id: cx.model_id(), scan_id: 0, abs_path, - root_name, + root_name: root_name.clone(), root_char_bag, ignores: Default::default(), entries_by_path: Default::default(), @@ -704,11 +739,48 @@ impl LocalWorktree { )); } + let (mut remote_id_tx, remote_id_rx) = watch::channel(); + let _maintain_remote_id_task = cx.spawn_weak({ + let rpc = rpc.clone(); + move |this, cx| { + async move { + let mut status = rpc.status(); + while let Some(status) = status.recv().await { + if let Some(this) = this.upgrade(&cx) { + let remote_id = if let Status::Connected { .. } = status { + let collaborator_logins = this.read_with(&cx, |this, _| { + this.as_local().unwrap().config.collaborators.clone() + }); + let response = rpc + .request(proto::OpenWorktree { + root_name: root_name.clone(), + collaborator_logins, + }) + .await?; + + Some(response.worktree_id) + } else { + None + }; + if remote_id_tx.send(remote_id).await.is_err() { + break; + } + } + } + Ok(()) + } + .log_err() + } + }); + let tree = Self { snapshot: snapshot.clone(), + config, + remote_id: remote_id_rx, background_snapshot: Arc::new(Mutex::new(snapshot)), last_scan_state_rx, _background_scanner_task: None, + _maintain_remote_id_task, share: None, poll_task: None, open_buffers: Default::default(), @@ -716,6 +788,7 @@ impl LocalWorktree { queued_operations: Default::default(), peers: Default::default(), languages, + rpc, fs, }; @@ -728,13 +801,10 @@ impl LocalWorktree { let tree = this.as_local_mut().unwrap(); if !tree.is_scanning() { if let Some(share) = tree.share.as_ref() { - Some((tree.snapshot(), share.snapshots_tx.clone())) - } else { - None + return Some((tree.snapshot(), share.snapshots_tx.clone())); } - } else { - None } + None }); if let Some((snapshot, snapshots_to_send_tx)) = to_send { @@ -888,6 +958,22 @@ impl LocalWorktree { } } + pub fn remote_id(&self) -> Option { + *self.remote_id.borrow() + } + + pub fn next_remote_id(&self) -> impl Future> { + let mut remote_id = self.remote_id.clone(); + async move { + while let Some(remote_id) = remote_id.recv().await { + if remote_id.is_some() { + return remote_id; + } + } + None + } + } + fn is_scanning(&self) -> bool { if let ScanState::Scanning = *self.last_scan_state_rx.borrow() { true @@ -973,17 +1059,19 @@ impl LocalWorktree { }) } - pub fn share( - &mut self, - rpc: Arc, - cx: &mut ModelContext, - ) -> Task> { + pub fn share(&mut self, cx: &mut ModelContext) -> Task> { let snapshot = self.snapshot(); let share_request = self.share_request(cx); + let rpc = self.rpc.clone(); cx.spawn(|this, mut cx| async move { - let share_request = share_request.await; + let share_request = if let Some(request) = share_request.await { + request + } else { + return Err(anyhow!("failed to open worktree on the server")); + }; + + let remote_id = share_request.worktree.as_ref().unwrap().id; let share_response = rpc.request(share_request).await?; - let remote_id = share_response.worktree_id; log::info!("sharing worktree {:?}", share_response); let (snapshots_to_send_tx, snapshots_to_send_rx) = @@ -1007,39 +1095,61 @@ impl LocalWorktree { this.update(&mut cx, |worktree, cx| { let _subscriptions = vec![ - rpc.subscribe_from_model(remote_id, cx, Worktree::handle_add_peer), - rpc.subscribe_from_model(remote_id, cx, Worktree::handle_remove_peer), - rpc.subscribe_from_model(remote_id, cx, Worktree::handle_open_buffer), - rpc.subscribe_from_model(remote_id, cx, Worktree::handle_close_buffer), - rpc.subscribe_from_model(remote_id, cx, Worktree::handle_update_buffer), - rpc.subscribe_from_model(remote_id, cx, Worktree::handle_save_buffer), + rpc.subscribe_to_entity(remote_id, cx, Worktree::handle_add_peer), + rpc.subscribe_to_entity(remote_id, cx, Worktree::handle_remove_peer), + rpc.subscribe_to_entity(remote_id, cx, Worktree::handle_open_buffer), + rpc.subscribe_to_entity(remote_id, cx, Worktree::handle_close_buffer), + rpc.subscribe_to_entity(remote_id, cx, Worktree::handle_update_buffer), + rpc.subscribe_to_entity(remote_id, cx, Worktree::handle_save_buffer), ]; let worktree = worktree.as_local_mut().unwrap(); worktree.share = Some(ShareState { - rpc, - remote_id: share_response.worktree_id, snapshots_tx: snapshots_to_send_tx, _subscriptions, }); }); - Ok((remote_id, share_response.access_token)) + Ok(remote_id) }) } - fn share_request(&self, cx: &mut ModelContext) -> Task { + pub fn unshare(&mut self, cx: &mut ModelContext) { + self.share.take(); + let rpc = self.rpc.clone(); + let remote_id = self.remote_id(); + cx.foreground() + .spawn( + async move { + if let Some(worktree_id) = remote_id { + rpc.send(proto::UnshareWorktree { worktree_id }).await?; + } + Ok(()) + } + .log_err(), + ) + .detach() + } + + fn share_request(&self, cx: &mut ModelContext) -> Task> { + let remote_id = self.next_remote_id(); let snapshot = self.snapshot(); let root_name = self.root_name.clone(); cx.background().spawn(async move { - let entries = snapshot - .entries_by_path - .cursor::<(), ()>() - .map(Into::into) - .collect(); - proto::ShareWorktree { - worktree: Some(proto::Worktree { root_name, entries }), - } + remote_id.await.map(|id| { + let entries = snapshot + .entries_by_path + .cursor::<(), ()>() + .map(Into::into) + .collect(); + proto::ShareWorktree { + worktree: Some(proto::Worktree { + id, + root_name, + entries, + }), + } + }) }) } } @@ -1077,8 +1187,6 @@ impl fmt::Debug for LocalWorktree { } struct ShareState { - rpc: Arc, - remote_id: u64, snapshots_tx: Sender, _subscriptions: Vec, } @@ -1103,12 +1211,11 @@ impl RemoteWorktree { path: &Path, cx: &mut ModelContext, ) -> Task>> { - let handle = cx.handle(); let mut existing_buffer = None; self.open_buffers.retain(|_buffer_id, buffer| { if let Some(buffer) = buffer.upgrade(cx.as_ref()) { if let Some(file) = buffer.read(cx.as_ref()).file() { - if file.worktree_id() == handle.id() && file.path.as_ref() == path { + if file.worktree_id() == cx.model_id() && file.path.as_ref() == path { existing_buffer = Some(buffer); } } @@ -1122,21 +1229,27 @@ impl RemoteWorktree { let replica_id = self.replica_id; let remote_worktree_id = self.remote_id; let path = path.to_string_lossy().to_string(); - cx.spawn(|this, mut cx| async move { + cx.spawn_weak(|this, mut cx| async move { if let Some(existing_buffer) = existing_buffer { Ok(existing_buffer) } else { let entry = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("worktree was closed"))? .read_with(&cx, |tree, _| tree.entry_for_path(&path).cloned()) .ok_or_else(|| anyhow!("file does not exist"))?; - let file = File::new(entry.id, handle, entry.path, entry.mtime); - let language = cx.read(|cx| file.select_language(cx)); let response = rpc .request(proto::OpenBuffer { worktree_id: remote_worktree_id as u64, path, }) .await?; + + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("worktree was closed"))?; + let file = File::new(entry.id, this.clone(), entry.path, entry.mtime); + let language = cx.read(|cx| file.select_language(cx)); let remote_buffer = response.buffer.ok_or_else(|| anyhow!("empty buffer"))?; let buffer_id = remote_buffer.id as usize; let buffer = cx.add_model(|cx| { @@ -1157,6 +1270,20 @@ impl RemoteWorktree { }) } + pub fn remote_id(&self) -> u64 { + self.remote_id + } + + pub fn close_all_buffers(&mut self, cx: &mut MutableAppContext) { + for (_, buffer) in self.open_buffers.drain() { + if let RemoteBuffer::Loaded(buffer) = buffer { + if let Some(buffer) = buffer.upgrade(cx) { + buffer.update(cx, |buffer, cx| buffer.close(cx)) + } + } + } + } + fn snapshot(&self) -> Snapshot { self.snapshot.clone() } @@ -1547,9 +1674,9 @@ impl File { self.worktree.update(cx, |worktree, cx| { if let Some((rpc, remote_id)) = match worktree { Worktree::Local(worktree) => worktree - .share - .as_ref() - .map(|share| (share.rpc.clone(), share.remote_id)), + .remote_id + .borrow() + .map(|id| (worktree.rpc.clone(), id)), Worktree::Remote(worktree) => Some((worktree.rpc.clone(), worktree.remote_id)), } { cx.spawn(|worktree, mut cx| async move { @@ -1645,14 +1772,12 @@ impl File { ) -> Task> { self.worktree.update(cx, |worktree, cx| match worktree { Worktree::Local(worktree) => { - let rpc = worktree - .share - .as_ref() - .map(|share| (share.rpc.clone(), share.remote_id)); + let rpc = worktree.rpc.clone(); + let worktree_id = *worktree.remote_id.borrow(); let save = worktree.save(self.path.clone(), text, cx); cx.background().spawn(async move { let entry = save.await?; - if let Some((rpc, worktree_id)) = rpc { + if let Some(worktree_id) = worktree_id { rpc.send(proto::BufferSaved { worktree_id, buffer_id, @@ -2537,6 +2662,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { #[cfg(test)] mod tests { use super::*; + use crate::fs::FakeFs; use crate::test::*; use anyhow::Result; use fs::RealFs; @@ -2571,9 +2697,10 @@ mod tests { .unwrap(); let tree = Worktree::open_local( + rpc::Client::new(), root_link_path, - Default::default(), Arc::new(RealFs), + Default::default(), &mut cx.to_async(), ) .await @@ -2627,9 +2754,10 @@ mod tests { } })); let tree = Worktree::open_local( + rpc::Client::new(), dir.path(), - Default::default(), Arc::new(RealFs), + Default::default(), &mut cx.to_async(), ) .await @@ -2671,9 +2799,10 @@ mod tests { "file1": "the old contents", })); let tree = Worktree::open_local( + rpc::Client::new(), dir.path(), - Arc::new(LanguageRegistry::new()), Arc::new(RealFs), + Default::default(), &mut cx.to_async(), ) .await @@ -2700,9 +2829,10 @@ mod tests { let file_path = dir.path().join("file1"); let tree = Worktree::open_local( + rpc::Client::new(), file_path.clone(), - Arc::new(LanguageRegistry::new()), Arc::new(RealFs), + Default::default(), &mut cx.to_async(), ) .await @@ -2741,10 +2871,14 @@ mod tests { } })); + let user_id = 5; + let mut client = rpc::Client::new(); + let server = FakeServer::for_client(user_id, &mut client, &cx).await; let tree = Worktree::open_local( + client, dir.path(), - Default::default(), Arc::new(RealFs), + Default::default(), &mut cx.to_async(), ) .await @@ -2778,15 +2912,20 @@ mod tests { // Create a remote copy of this worktree. let initial_snapshot = tree.read_with(&cx, |tree, _| tree.snapshot()); let worktree_id = 1; - let share_request = tree - .update(&mut cx, |tree, cx| { - tree.as_local().unwrap().share_request(cx) - }) + let share_request = tree.update(&mut cx, |tree, cx| { + tree.as_local().unwrap().share_request(cx) + }); + let open_worktree = server.receive::().await.unwrap(); + server + .respond( + open_worktree.receipt(), + proto::OpenWorktreeResponse { worktree_id: 1 }, + ) .await; + let remote = Worktree::remote( - proto::OpenWorktreeResponse { - worktree_id, - worktree: share_request.worktree, + proto::JoinWorktreeResponse { + worktree: share_request.await.unwrap().worktree, replica_id: 1, peers: Vec::new(), }, @@ -2896,9 +3035,10 @@ mod tests { })); let tree = Worktree::open_local( + rpc::Client::new(), dir.path(), - Default::default(), Arc::new(RealFs), + Default::default(), &mut cx.to_async(), ) .await @@ -2928,6 +3068,65 @@ mod tests { }); } + #[gpui::test] + async fn test_open_and_share_worktree(mut cx: gpui::TestAppContext) { + let user_id = 100; + let mut client = rpc::Client::new(); + let server = FakeServer::for_client(user_id, &mut client, &cx).await; + + let fs = Arc::new(FakeFs::new()); + fs.insert_tree( + "/path", + json!({ + "to": { + "the-dir": { + ".zed.toml": r#"collaborators = ["friend-1", "friend-2"]"#, + "a.txt": "a-contents", + }, + }, + }), + ) + .await; + + let worktree = Worktree::open_local( + client.clone(), + "/path/to/the-dir".as_ref(), + fs, + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + { + let cx = cx.to_async(); + client.authenticate_and_connect(&cx).await.unwrap(); + } + + let open_worktree = server.receive::().await.unwrap(); + assert_eq!( + open_worktree.payload, + proto::OpenWorktree { + root_name: "the-dir".to_string(), + collaborator_logins: vec!["friend-1".to_string(), "friend-2".to_string()], + } + ); + + server + .respond( + open_worktree.receipt(), + proto::OpenWorktreeResponse { worktree_id: 5 }, + ) + .await; + let remote_id = worktree + .update(&mut cx, |tree, _| tree.as_local().unwrap().next_remote_id()) + .await; + assert_eq!(remote_id, Some(5)); + + cx.update(move |_| drop(worktree)); + server.receive::().await.unwrap(); + } + #[gpui::test(iterations = 100)] fn test_random(mut rng: StdRng) { let operations = env::var("OPERATIONS") diff --git a/zrpc/proto/zed.proto b/zrpc/proto/zed.proto index 4e42441eb276ad36e71938946f7c229cc9799e5f..0f0ea69261c07edbf9357959a5b0c39345631994 100644 --- a/zrpc/proto/zed.proto +++ b/zrpc/proto/zed.proto @@ -11,8 +11,8 @@ message Envelope { Ping ping = 6; ShareWorktree share_worktree = 7; ShareWorktreeResponse share_worktree_response = 8; - OpenWorktree open_worktree = 9; - OpenWorktreeResponse open_worktree_response = 10; + JoinWorktree join_worktree = 9; + JoinWorktreeResponse join_worktree_response = 10; UpdateWorktree update_worktree = 11; CloseWorktree close_worktree = 12; OpenBuffer open_buffer = 13; @@ -35,6 +35,11 @@ message Envelope { ChannelMessageSent channel_message_sent = 30; GetChannelMessages get_channel_messages = 31; GetChannelMessagesResponse get_channel_messages_response = 32; + OpenWorktree open_worktree = 33; + OpenWorktreeResponse open_worktree_response = 34; + UnshareWorktree unshare_worktree = 35; + UpdateCollaborators update_collaborators = 36; + LeaveWorktree leave_worktree = 37; } } @@ -48,22 +53,34 @@ message Error { string message = 1; } +message OpenWorktree { + string root_name = 1; + repeated string collaborator_logins = 2; +} + +message OpenWorktreeResponse { + uint64 worktree_id = 1; +} + message ShareWorktree { Worktree worktree = 1; } -message ShareWorktreeResponse { +message ShareWorktreeResponse {} + +message UnshareWorktree { uint64 worktree_id = 1; - string access_token = 2; } -message OpenWorktree { +message JoinWorktree { uint64 worktree_id = 1; - string access_token = 2; } -message OpenWorktreeResponse { +message LeaveWorktree { uint64 worktree_id = 1; +} + +message JoinWorktreeResponse { Worktree worktree = 2; uint32 replica_id = 3; repeated Peer peers = 4; @@ -173,6 +190,10 @@ message GetChannelMessagesResponse { bool done = 2; } +message UpdateCollaborators { + repeated Collaborator collaborators = 1; +} + // Entities message Peer { @@ -187,8 +208,9 @@ message User { } message Worktree { - string root_name = 1; - repeated Entry entries = 2; + uint64 id = 1; + string root_name = 2; + repeated Entry entries = 3; } message Entry { @@ -314,3 +336,15 @@ message ChannelMessage { uint64 sender_id = 4; Nonce nonce = 5; } + +message Collaborator { + uint64 user_id = 1; + repeated WorktreeMetadata worktrees = 2; +} + +message WorktreeMetadata { + uint64 id = 1; + string root_name = 2; + bool is_shared = 3; + repeated uint64 guests = 4; +} diff --git a/zrpc/src/proto.rs b/zrpc/src/proto.rs index b2d4de3bbf501c2ce5c7e28fb0c7f7355171a790..92fca53e28335680f2cf7227e0eea32a68a54e8b 100644 --- a/zrpc/src/proto.rs +++ b/zrpc/src/proto.rs @@ -131,11 +131,15 @@ messages!( GetChannelMessagesResponse, GetChannels, GetChannelsResponse, + UpdateCollaborators, GetUsers, GetUsersResponse, JoinChannel, JoinChannelResponse, + JoinWorktree, + JoinWorktreeResponse, LeaveChannel, + LeaveWorktree, OpenBuffer, OpenBufferResponse, OpenWorktree, @@ -147,6 +151,7 @@ messages!( SendChannelMessageResponse, ShareWorktree, ShareWorktreeResponse, + UnshareWorktree, UpdateBuffer, UpdateWorktree, ); @@ -156,11 +161,13 @@ request_messages!( (GetUsers, GetUsersResponse), (JoinChannel, JoinChannelResponse), (OpenBuffer, OpenBufferResponse), + (JoinWorktree, JoinWorktreeResponse), (OpenWorktree, OpenWorktreeResponse), (Ping, Ack), (SaveBuffer, BufferSaved), (UpdateBuffer, Ack), (ShareWorktree, ShareWorktreeResponse), + (UnshareWorktree, Ack), (SendChannelMessage, SendChannelMessageResponse), (GetChannelMessages, GetChannelMessagesResponse), ); @@ -172,9 +179,10 @@ entity_messages!( CloseBuffer, CloseWorktree, OpenBuffer, - OpenWorktree, + JoinWorktree, RemovePeer, SaveBuffer, + UnshareWorktree, UpdateBuffer, UpdateWorktree, );