From 3ea061a11e13c517f48bfe0dd357adce9bd4cd68 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 31 May 2022 16:47:06 -0700 Subject: [PATCH] Allow making projects private --- crates/collab/src/integration_tests.rs | 2 +- crates/contacts_panel/src/contacts_panel.rs | 119 +++++++++++++------- crates/project/src/project.rs | 47 +++++--- crates/project/src/worktree.rs | 18 +-- crates/theme/src/theme.rs | 2 +- crates/workspace/src/workspace.rs | 22 ++-- styles/src/styleTree/contactsPanel.ts | 3 +- 7 files changed, 131 insertions(+), 82 deletions(-) diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index ce3a1df16e..96ed7714c2 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -532,7 +532,7 @@ async fn test_private_projects( .read_with(cx_b, |store, _| { store.contacts()[0].projects.is_empty() })); // The project is registered when it is made public. - project_a.update(cx_a, |project, _| project.set_public(true)); + project_a.update(cx_a, |project, cx| project.set_public(true, cx)); deterministic.run_until_parked(); assert!(project_a.read_with(cx_a, |project, _| project.remote_id().is_some())); assert!(!client_b diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index ffa3300e70..575639643c 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -207,7 +207,7 @@ impl ContactsPanel { ContactEntry::Contact(contact) => { Self::render_contact(&contact.user, theme, is_selected) } - ContactEntry::ContactProject(contact, project_ix, _) => { + ContactEntry::ContactProject(contact, project_ix, open_project) => { let is_last_project_for_contact = this.entries.get(ix + 1).map_or(true, |next| { if let ContactEntry::ContactProject(next_contact, _, _) = next { @@ -216,10 +216,11 @@ impl ContactsPanel { true } }); - Self::render_contact_project( + Self::render_project( contact.clone(), current_user_id, *project_ix, + open_project.clone(), theme, is_last_project_for_contact, is_selected, @@ -328,10 +329,11 @@ impl ContactsPanel { .boxed() } - fn render_contact_project( + fn render_project( contact: Arc, current_user_id: Option, project_index: usize, + open_project: Option>, theme: &theme::ContactsPanel, is_last_project: bool, is_selected: bool, @@ -340,6 +342,7 @@ impl ContactsPanel { let project = &contact.projects[project_index]; let project_id = project.id; let is_host = Some(contact.user.id) == current_user_id; + let open_project = open_project.and_then(|p| p.upgrade(cx.deref_mut())); let font_cache = cx.font_cache(); let host_avatar_height = theme @@ -354,48 +357,78 @@ impl ContactsPanel { let baseline_offset = row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; - MouseEventHandler::new::(project_id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { let tree_branch = *tree_branch.style_for(mouse_state, is_selected); let row = theme.project_row.style_for(mouse_state, is_selected); 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.); + Stack::new() + .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 is_last_project { - end_y - } else { - bounds.max_y() + cx.scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + tree_branch.width, + if is_last_project { + 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., + }); + }) + .boxed(), + ) + .with_children(if mouse_state.hovered && open_project.is_some() { + Some( + MouseEventHandler::new::( + project_id as usize, + cx, + |state, _| { + let mut icon_style = + *theme.private_button.style_for(state, false); + icon_style.container.background_color = + row.container.background_color; + render_icon_button(&icon_style, "icons/lock-8.svg") + .aligned() + .boxed() }, - ), - ), - 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_cursor_style(CursorStyle::PointingHand) + .on_click(move |_, _, cx| { + cx.dispatch_action(ToggleProjectPublic { + project: open_project.clone(), + }) + }) + .boxed(), + ) + } else { + None + }) + .constrained() + .with_width(host_avatar_height) + .boxed(), ) .with_child( Label::new( @@ -467,9 +500,9 @@ impl ContactsPanel { MouseEventHandler::new::(project_id, cx, |state, cx| { let row = theme.project_row.style_for(state, is_selected); let mut worktree_root_names = String::new(); - let project = project.read(cx); - let is_public = project.is_public(); - for tree in project.visible_worktrees(cx) { + let project_ = project.read(cx); + let is_public = project_.is_public(); + for tree in project_.visible_worktrees(cx) { if !worktree_root_names.is_empty() { worktree_root_names.push_str(", "); } @@ -498,7 +531,9 @@ impl ContactsPanel { CursorStyle::PointingHand }) .on_click(move |_, _, cx| { - cx.dispatch_action(ToggleProjectPublic { project: None }) + cx.dispatch_action(ToggleProjectPublic { + project: Some(project.clone()), + }) }) .boxed(), ) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9f8ba97ea8..6a8adbb2ae 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -8,7 +8,7 @@ use anyhow::{anyhow, Context, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; -use futures::{future::Shared, select_biased, Future, FutureExt, StreamExt, TryFutureExt}; +use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{ AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, @@ -25,6 +25,7 @@ use language::{ use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer}; use lsp_command::*; use parking_lot::Mutex; +use postage::stream::Stream; use postage::watch; use rand::prelude::*; use search::SearchQuery; @@ -325,14 +326,12 @@ impl Project { let (public_tx, public_rx) = watch::channel_with(public); let (remote_id_tx, remote_id_rx) = watch::channel(); let _maintain_remote_id_task = cx.spawn_weak({ - let mut status_rx = client.clone().status(); - let mut public_rx = public_rx.clone(); + let status_rx = client.clone().status(); + let public_rx = public_rx.clone(); move |this, mut cx| async move { - loop { - select_biased! { - value = status_rx.next().fuse() => { value?; } - value = public_rx.next().fuse() => { value?; } - }; + let mut stream = Stream::map(status_rx.clone(), drop) + .merge(Stream::map(public_rx.clone(), drop)); + while stream.recv().await.is_some() { let this = this.upgrade(&cx)?; if status_rx.borrow().is_connected() && *public_rx.borrow() { this.update(&mut cx, |this, cx| this.register(cx)) @@ -342,11 +341,12 @@ impl Project { this.update(&mut cx, |this, cx| this.unregister(cx)); } } + None } }); let handle = cx.weak_handle(); - project_store.update(cx, |store, cx| store.add(handle, cx)); + project_store.update(cx, |store, cx| store.add_project(handle, cx)); let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); Self { @@ -434,7 +434,7 @@ impl Project { let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); let this = cx.add_model(|cx: &mut ModelContext| { let handle = cx.weak_handle(); - project_store.update(cx, |store, cx| store.add(handle, cx)); + project_store.update(cx, |store, cx| store.add_project(handle, cx)); let mut this = Self { worktrees: Vec::new(), @@ -624,9 +624,10 @@ impl Project { &self.fs } - pub fn set_public(&mut self, is_public: bool) { + pub fn set_public(&mut self, is_public: bool, cx: &mut ModelContext) { if let ProjectClientState::Local { public_tx, .. } = &mut self.client_state { *public_tx.borrow_mut() = is_public; + self.metadata_changed(cx); } } @@ -648,10 +649,19 @@ impl Project { } if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state { - *remote_id_tx.borrow_mut() = None; + let mut remote_id = remote_id_tx.borrow_mut(); + if let Some(remote_id) = *remote_id { + self.client + .send(proto::UnregisterProject { + project_id: remote_id, + }) + .log_err(); + } + *remote_id = None; } self.subscriptions.clear(); + self.metadata_changed(cx); } fn register(&mut self, cx: &mut ModelContext) -> Task> { @@ -671,6 +681,7 @@ impl Project { *remote_id_tx.borrow_mut() = Some(remote_id); } + this.metadata_changed(cx); cx.emit(Event::RemoteIdChanged(Some(remote_id))); this.subscriptions @@ -745,6 +756,10 @@ impl Project { } } + fn metadata_changed(&mut self, cx: &mut ModelContext) { + self.project_store.update(cx, |_, cx| cx.notify()); + } + pub fn collaborators(&self) -> &HashMap { &self.collaborators } @@ -3743,6 +3758,7 @@ impl Project { false } }); + self.metadata_changed(cx); cx.notify(); } @@ -3772,6 +3788,7 @@ impl Project { self.worktrees .push(WorktreeHandle::Weak(worktree.downgrade())); } + self.metadata_changed(cx); cx.emit(Event::WorktreeAdded); cx.notify(); } @@ -5204,7 +5221,7 @@ impl ProjectStore { .filter_map(|project| project.upgrade(cx)) } - fn add(&mut self, project: WeakModelHandle, cx: &mut ModelContext) { + fn add_project(&mut self, project: WeakModelHandle, cx: &mut ModelContext) { if let Err(ix) = self .projects .binary_search_by_key(&project.id(), WeakModelHandle::id) @@ -5214,7 +5231,7 @@ impl ProjectStore { cx.notify(); } - fn prune(&mut self, cx: &mut ModelContext) { + fn prune_projects(&mut self, cx: &mut ModelContext) { let mut did_change = false; self.projects.retain(|project| { if project.is_upgradable(cx) { @@ -5316,7 +5333,7 @@ impl Entity for Project { type Event = Event; fn release(&mut self, cx: &mut gpui::MutableAppContext) { - self.project_store.update(cx, ProjectStore::prune); + self.project_store.update(cx, ProjectStore::prune_projects); match &self.client_state { ProjectClientState::Local { remote_id_rx, .. } => { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index cadfaa520d..05eaecbc97 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -151,14 +151,7 @@ impl Entity for Worktree { fn release(&mut self, _: &mut MutableAppContext) { if let Some(worktree) = self.as_local_mut() { - if let Registration::Done { project_id } = worktree.registration { - let client = worktree.client.clone(); - let unregister_message = proto::UnregisterWorktree { - project_id, - worktree_id: worktree.id().to_proto(), - }; - client.send(unregister_message).log_err(); - } + worktree.unregister(); } } } @@ -1063,6 +1056,15 @@ impl LocalWorktree { pub fn unregister(&mut self) { self.unshare(); + if let Registration::Done { project_id } = self.registration { + self.client + .clone() + .send(proto::UnregisterWorktree { + project_id, + worktree_id: self.id().to_proto(), + }) + .log_err(); + } self.registration = Registration::None; } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index a9bd2b2b48..52b5e8df36 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -319,7 +319,7 @@ pub struct Icon { pub path: String, } -#[derive(Clone, Deserialize, Default)] +#[derive(Deserialize, Clone, Copy, Default)] pub struct IconButton { #[serde(flatten)] pub container: ContainerStyle, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0889077a1e..f6b8c5db09 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -22,7 +22,7 @@ use gpui::{ platform::{CursorStyle, WindowOptions}, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData, ModelContext, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, - Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, + Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; @@ -102,7 +102,7 @@ pub struct OpenPaths { #[derive(Clone, Deserialize)] pub struct ToggleProjectPublic { #[serde(skip_deserializing)] - pub project: Option>, + pub project: Option>, } #[derive(Clone)] @@ -1050,19 +1050,13 @@ impl Workspace { } fn toggle_project_public(&mut self, action: &ToggleProjectPublic, cx: &mut ViewContext) { - let project = if let Some(project) = action.project { - if let Some(project) = project.upgrade(cx) { - project - } else { - return; - } - } else { - self.project.clone() - }; - - project.update(cx, |project, _| { + let project = action + .project + .clone() + .unwrap_or_else(|| self.project.clone()); + project.update(cx, |project, cx| { let is_public = project.is_public(); - project.set_public(!is_public); + project.set_public(!is_public, cx); }); } diff --git a/styles/src/styleTree/contactsPanel.ts b/styles/src/styleTree/contactsPanel.ts index 5253a1185c..cf08e770ab 100644 --- a/styles/src/styleTree/contactsPanel.ts +++ b/styles/src/styleTree/contactsPanel.ts @@ -71,7 +71,8 @@ export default function contactsPanel(theme: Theme) { privateButton: { iconWidth: 8, color: iconColor(theme, "primary"), - buttonWidth: 8, + cornerRadius: 5, + buttonWidth: 12, }, rowHeight: 28, sectionIconSize: 8,