Merge pull request #170 from zed-industries/people-panel

People panel
This commit is contained in:
Antonio Scandurra 2021-09-22 17:10:50 +02:00 committed by GitHub
commit 796139e4ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 2618 additions and 991 deletions

1
.zed.toml Normal file
View file

@ -0,0 +1 @@
collaborators = ["nathansobo", "as-cii", "maxbrunsfeld", "iamnbutler"]

View file

@ -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))
}

View file

@ -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<T: Element> {

View file

@ -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

View file

@ -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;

View file

@ -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<f32>,
#[serde(default)]
pub width: Option<f32>,
}
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, ())
}

View file

@ -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)

View file

@ -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),
})
}
}

View file

@ -12,6 +12,7 @@ use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
pub struct List {
state: ListState,
invalidated_elements: Vec<ElementRc>,
}
#[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::<Count, ()>();
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,

View file

@ -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<Self>, font_id: FontId, font_size: f32) -> LineWrapperHandle {
let mut state = self.0.write();
let wrappers = state

View file

@ -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)
}

View file

@ -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<usize>,
invalidated_views: HashSet<usize>,
}
@ -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 {

View file

@ -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<Arc<AppState>>) {
struct NativeAppSignInParams {
native_app_port: String,
native_app_public_key: String,
impersonate: Option<String>,
}
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<NativeAppSignInParams> = 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());
}

View file

@ -27,8 +27,12 @@ async fn main() {
let zed_users = ["nathansobo", "maxbrunsfeld", "as-cii", "iamnbutler"];
let mut zed_user_ids = Vec::<UserId>::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)

View file

@ -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<Option<UserId>> {
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<UserId> {
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<Item = UserId>,
ids: impl IntoIterator<Item = UserId>,
) -> Result<Vec<User>> {
let mut include_requester = false;
let ids = ids
.map(|id| {
if id == requester_id {
include_requester = true;
}
id.0
})
.collect::<Vec<_>>();
let ids = ids.into_iter().map(|id| id.0).collect::<Vec<_>>();
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]

File diff suppressed because it is too large Load diff

615
server/src/rpc/store.rs Normal file
View file

@ -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<ConnectionId, ConnectionState>,
connections_by_user_id: HashMap<UserId, HashSet<ConnectionId>>,
worktrees: HashMap<u64, Worktree>,
visible_worktrees_by_user_id: HashMap<UserId, HashSet<u64>>,
channels: HashMap<ChannelId, Channel>,
next_worktree_id: u64,
}
struct ConnectionState {
user_id: UserId,
worktrees: HashSet<u64>,
channels: HashSet<ChannelId>,
}
pub struct Worktree {
pub host_connection_id: ConnectionId,
pub collaborator_user_ids: Vec<UserId>,
pub root_name: String,
pub share: Option<WorktreeShare>,
}
pub struct WorktreeShare {
pub guest_connection_ids: HashMap<ConnectionId, ReplicaId>,
pub active_replica_ids: HashSet<ReplicaId>,
pub entries: HashMap<u64, proto::Entry>,
}
#[derive(Default)]
pub struct Channel {
pub connection_ids: HashSet<ConnectionId>,
}
pub type ReplicaId = u16;
#[derive(Default)]
pub struct RemovedConnectionState {
pub hosted_worktrees: HashMap<u64, Worktree>,
pub guest_worktree_ids: HashMap<u64, Vec<ConnectionId>>,
pub collaborator_ids: HashSet<UserId>,
}
pub struct JoinedWorktree<'a> {
pub replica_id: ReplicaId,
pub worktree: &'a Worktree,
}
pub struct UnsharedWorktree {
pub connection_ids: Vec<ConnectionId>,
pub collaborator_ids: Vec<UserId>,
}
pub struct LeftWorktree {
pub connection_ids: Vec<ConnectionId>,
pub collaborator_ids: Vec<UserId>,
}
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<RemovedConnectionState> {
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<UserId> {
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<Item = ConnectionId> {
self.connections_by_user_id
.get(&user_id)
.into_iter()
.flatten()
.copied()
}
pub fn collaborators_for_user(&self, user_id: UserId) -> Vec<proto::Collaborator> {
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<Worktree> {
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<u64, proto::Entry>,
) -> Option<Vec<UserId>> {
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<UnsharedWorktree> {
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<JoinedWorktree> {
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<LeftWorktree> {
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<Vec<ConnectionId>> {
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<ConnectionId> {
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<Vec<ConnectionId>> {
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<Vec<ConnectionId>> {
Ok(self
.read_worktree(worktree_id, connection_id)?
.connection_ids())
}
pub fn channel_connection_ids(&self, channel_id: ChannelId) -> Option<Vec<ConnectionId>> {
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::<HashSet<_>>(),
);
}
}
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<ConnectionId> {
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<ConnectionId> {
self.connection_ids.iter().copied().collect()
}
}

View file

@ -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

View file

@ -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<Vec<ChannelDetails>>,
channels: HashMap<u64, WeakModelHandle<Channel>>,
rpc: Arc<Client>,
user_store: Arc<UserStore>,
user_store: ModelHandle<UserStore>,
_task: Task<Option<()>>,
}
@ -41,7 +41,7 @@ pub struct Channel {
messages: SumTree<ChannelMessage>,
loaded_all_messages: bool,
next_pending_message_id: usize,
user_store: Arc<UserStore>,
user_store: ModelHandle<UserStore>,
rpc: Arc<Client>,
rng: StdRng,
_subscription: rpc::Subscription,
@ -87,7 +87,7 @@ impl Entity for ChannelList {
impl ChannelList {
pub fn new(
user_store: Arc<UserStore>,
user_store: ModelHandle<UserStore>,
rpc: Arc<rpc::Client>,
cx: &mut ModelContext<Self>,
) -> Self {
@ -186,11 +186,11 @@ impl Entity for Channel {
impl Channel {
pub fn new(
details: ChannelDetails,
user_store: Arc<UserStore>,
user_store: ModelHandle<UserStore>,
rpc: Arc<Client>,
cx: &mut ModelContext<Self>,
) -> 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<proto::ChannelMessage>,
user_store: &UserStore,
user_store: &ModelHandle<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<SumTree<ChannelMessage>> {
let unique_user_ids = proto_messages
.iter()
@ -494,11 +500,15 @@ async fn messages_from_proto(
.collect::<HashSet<_>>()
.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<proto::Channel> for ChannelDetails {
impl ChannelMessage {
pub async fn from_proto(
message: proto::ChannelMessage,
user_store: &UserStore,
user_store: &ModelHandle<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<Self> {
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::<proto::GetUsers>().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::<proto::GetChannels>().await.unwrap();
server
@ -639,6 +639,21 @@ mod tests {
)
});
let get_users = server.receive::<proto::GetUsers>().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| {

View file

@ -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,

View file

@ -801,6 +801,10 @@ impl Buffer {
cx.emit(Event::FileHandleChanged);
}
pub fn close(&mut self, cx: &mut ModelContext<Self>) {
cx.emit(Event::Closed);
}
pub fn language(&self) -> Option<&Arc<Language>> {
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

View file

@ -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<language::LanguageRegistry>,
pub themes: Arc<settings::ThemeRegistry>,
pub rpc: Arc<rpc::Client>,
pub user_store: Arc<user::UserStore>,
pub user_store: ModelHandle<user::UserStore>,
pub fs: Arc<dyn fs::Fs>,
pub channel_list: ModelHandle<ChannelList>,
}

View file

@ -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)),

View file

@ -16,21 +16,6 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
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"),

267
zed/src/people_panel.rs Normal file
View file

@ -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<UserStore>,
settings: watch::Receiver<Settings>,
_maintain_collaborators: Subscription,
}
impl PeoplePanel {
pub fn new(
user_store: ModelHandle<UserStore>,
settings: watch::Receiver<Settings>,
cx: &mut ViewContext<Self>,
) -> 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<UserStore>, cx: &mut ViewContext<Self>) {
self.collaborators
.reset(self.user_store.read(cx).collaborators().len());
cx.notify();
}
fn render_collaborator(
collaborator: &Collaborator,
current_user_id: Option<u64>,
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::<PeoplePanel, _, _, _>(
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<Self>) -> ElementBox {
let theme = &self.settings.borrow().theme.people_panel;
Container::new(List::new(self.collaborators.clone()).boxed())
.with_style(theme.container)
.boxed()
}
}

View file

@ -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<String> = 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<T, M, F>(
pub fn subscribe<T, M, F>(
self: &Arc<Self>,
cx: &mut ModelContext<M>,
mut handler: F,
) -> Subscription
where
T: EnvelopedMessage,
M: Entity,
F: 'static
+ Send
+ Sync
+ FnMut(&mut M, TypedEnvelope<T>, Arc<Self>, &mut ModelContext<M>) -> Result<()>,
{
let subscription_id = (TypeId::of::<T>(), 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::<TypedEnvelope<T>>().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<T, M, F>(
self: &Arc<Self>,
remote_id: u64,
cx: &mut ModelContext<M>,
@ -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<Credentials> {
if IMPERSONATE_LOGIN.is_some() {
return None;
}
let (user_id, access_token) = cx
.platform()
.read_credentials(&ZED_SERVER_URL)

View file

@ -168,7 +168,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
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,

View file

@ -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,

View file

@ -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
}
})
)
}

View file

@ -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<Arc<ImageData>>,
}
#[derive(Debug)]
pub struct Collaborator {
pub user: Arc<User>,
pub worktrees: Vec<WorktreeMetadata>,
}
#[derive(Debug)]
pub struct WorktreeMetadata {
pub id: u64,
pub root_name: String,
pub is_shared: bool,
pub guests: Vec<Arc<User>>,
}
pub struct UserStore {
users: Mutex<HashMap<u64, Arc<User>>>,
users: HashMap<u64, Arc<User>>,
current_user: watch::Receiver<Option<Arc<User>>>,
collaborators: Arc<[Collaborator]>,
rpc: Arc<Client>,
http: Arc<dyn HttpClient>,
_maintain_collaborators: Task<()>,
_maintain_current_user: Task<()>,
}
pub enum Event {}
impl Entity for UserStore {
type Event = Event;
}
impl UserStore {
pub fn new(
rpc: Arc<Client>,
http: Arc<dyn HttpClient>,
executor: &executor::Background,
) -> Arc<Self> {
pub fn new(rpc: Arc<Client>, http: Arc<dyn HttpClient>, cx: &mut ModelContext<Self>) -> Self {
let (mut current_user_tx, current_user_rx) = watch::channel();
let (mut this_tx, mut this_rx) = oneshot::channel::<Weak<Self>>();
let this = Arc::new(Self {
let (mut update_collaborators_tx, mut update_collaborators_rx) =
watch::channel::<Option<proto::UpdateCollaborators>>();
let update_collaborators_subscription = rpc.subscribe(
cx,
move |_: &mut Self, msg: TypedEnvelope<proto::UpdateCollaborators>, _, _| {
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<u64>) -> 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<Self>,
) -> Task<Result<()>> {
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 async fn fetch_user(&self, user_id: u64) -> Result<Arc<User>> {
if let Some(user) = self.users.lock().get(&user_id).cloned() {
return Ok(user);
pub fn collaborators(&self) -> &Arc<[Collaborator]> {
&self.collaborators
}
pub fn load_users(
&mut self,
mut user_ids: Vec<u64>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
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<Self>,
) -> Task<Result<Arc<User>>> {
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<Arc<User>> {
@ -130,6 +213,40 @@ impl User {
}
}
impl Collaborator {
async fn from_proto(
collaborator: proto::Collaborator,
user_store: &ModelHandle<UserStore>,
cx: &mut AsyncAppContext,
) -> Result<Self> {
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<Arc<ImageData>> {
let url = Url::parse(url).with_context(|| format!("failed to parse avatar url {:?}", url))?;
let mut request = Request::new(Method::Get, url);

View file

@ -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<AppState>);
action!(OpenPaths, OpenParams);
action!(OpenNew, Arc<AppState>);
action!(ShareWorktree);
action!(JoinWorktree, Arc<AppState>);
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::share_worktree);
cx.add_action(Workspace::join_worktree);
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::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<T: ItemView> ItemViewHandle for ViewHandle<T> {
fn set_parent_pane(&self, pane: &ViewHandle<Pane>, 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<Settings>,
languages: Arc<LanguageRegistry>,
rpc: Arc<rpc::Client>,
user_store: Arc<user::UserStore>,
user_store: ModelHandle<user::UserStore>,
fs: Arc<dyn Fs>,
modal: Option<AnyViewHandle>,
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<Self>,
) -> Task<Result<ModelHandle<Worktree>>> {
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<Self>) {
fn share_worktree(&mut self, action: &ShareWorktree, cx: &mut ViewContext<Self>) {
let rpc = self.rpc.clone();
let platform = cx.platform();
let remote_id = action.0;
cx.spawn(|this, mut cx| {
async move {
rpc.authenticate_and_connect(&cx).await?;
let task = 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(())
});
cx.spawn(|_, _| async move {
if let Err(e) = task.await {
log::error!("sharing failed: {:?}", e);
if let Some(share_task) = task {
share_task.await?;
}
Ok(())
}
.log_err()
})
.detach();
}
fn join_worktree(&mut self, _: &JoinWorktree, cx: &mut ViewContext<Self>) {
fn unshare_worktree(&mut self, action: &UnshareWorktree, cx: &mut ViewContext<Self>) {
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<Self>) {
let rpc = self.rpc.clone();
let languages = self.languages.clone();
let worktree_id = action.0;
let task = cx.spawn(|this, mut cx| async move {
rpc.authenticate_and_connect(&cx).await?;
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 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();
});
surf::Result::Ok(())
});
cx.spawn(|_, _| async move {
if let Err(e) = task.await {
log::error!("joining failed: {}", e);
Ok(())
}
.log_err()
})
.detach();
}
fn leave_worktree(&mut self, action: &LeaveWorktree, cx: &mut ViewContext<Self>) {
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
})
})
});
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())
{

View file

@ -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<rpc::Client>,
path: impl Into<Arc<Path>>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
languages: Arc<LanguageRegistry>,
cx: &mut AsyncAppContext,
) -> Result<ModelHandle<Self>> {
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<rpc::Client>,
id: u64,
access_token: String,
languages: Arc<LanguageRegistry>,
cx: &mut AsyncAppContext,
) -> Result<ModelHandle<Self>> {
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<rpc::Client>,
languages: Arc<LanguageRegistry>,
cx: &mut AsyncAppContext,
) -> Result<ModelHandle<Self>> {
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<proto::UnshareWorktree>,
_: Arc<rpc::Client>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
cx.emit(Event::Closed);
Ok(())
}
fn poll_snapshot(&mut self, cx: &mut ModelContext<Self>) {
match self {
Self::Local(worktree) => {
@ -648,24 +666,34 @@ impl Deref for Worktree {
pub struct LocalWorktree {
snapshot: Snapshot,
config: WorktreeConfig,
background_snapshot: Arc<Mutex<Snapshot>>,
last_scan_state_rx: watch::Receiver<ScanState>,
_background_scanner_task: Option<Task<()>>,
_maintain_remote_id_task: Task<Option<()>>,
poll_task: Option<Task<()>>,
remote_id: watch::Receiver<Option<u64>>,
share: Option<ShareState>,
open_buffers: HashMap<usize, WeakModelHandle<Buffer>>,
shared_buffers: HashMap<PeerId, HashMap<u64, ModelHandle<Buffer>>>,
peers: HashMap<PeerId, ReplicaId>,
languages: Arc<LanguageRegistry>,
queued_operations: Vec<(u64, Operation)>,
rpc: Arc<rpc::Client>,
fs: Arc<dyn Fs>,
}
#[derive(Default, Deserialize)]
struct WorktreeConfig {
collaborators: Vec<String>,
}
impl LocalWorktree {
async fn new(
rpc: Arc<rpc::Client>,
path: impl Into<Arc<Path>>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
languages: Arc<LanguageRegistry>,
cx: &mut AsyncAppContext,
) -> Result<(ModelHandle<Worktree>, Sender<ScanState>)> {
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<Worktree>| {
@ -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<u64> {
*self.remote_id.borrow()
}
pub fn next_remote_id(&self) -> impl Future<Output = Option<u64>> {
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<rpc::Client>,
cx: &mut ModelContext<Worktree>,
) -> Task<anyhow::Result<(u64, String)>> {
pub fn share(&mut self, cx: &mut ModelContext<Worktree>) -> Task<anyhow::Result<u64>> {
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<Worktree>) -> Task<proto::ShareWorktree> {
pub fn unshare(&mut self, cx: &mut ModelContext<Worktree>) {
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<Worktree>) -> Task<Option<proto::ShareWorktree>> {
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<rpc::Client>,
remote_id: u64,
snapshots_tx: Sender<Snapshot>,
_subscriptions: Vec<rpc::Subscription>,
}
@ -1103,12 +1211,11 @@ impl RemoteWorktree {
path: &Path,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<ModelHandle<Buffer>>> {
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<Result<(time::Global, SystemTime)>> {
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::<proto::OpenWorktree>().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::<proto::OpenWorktree>().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::<proto::CloseWorktree>().await.unwrap();
}
#[gpui::test(iterations = 100)]
fn test_random(mut rng: StdRng) {
let operations = env::var("OPERATIONS")

View file

@ -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 ShareWorktree {
Worktree worktree = 1;
}
message ShareWorktreeResponse {
uint64 worktree_id = 1;
string access_token = 2;
}
message OpenWorktree {
uint64 worktree_id = 1;
string access_token = 2;
string root_name = 1;
repeated string collaborator_logins = 2;
}
message OpenWorktreeResponse {
uint64 worktree_id = 1;
}
message ShareWorktree {
Worktree worktree = 1;
}
message ShareWorktreeResponse {}
message UnshareWorktree {
uint64 worktree_id = 1;
}
message JoinWorktree {
uint64 worktree_id = 1;
}
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;
}

View file

@ -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,
);