Detailed changes
@@ -594,9 +594,17 @@ async fn test_following_to_channel_notes_without_a_shared_project(
let client_b = server.create_client(cx_b, "user_b").await;
let client_c = server.create_client(cx_c, "user_c").await;
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+ cx_c.update(editor::init);
+ cx_a.update(collab_ui::channel_view::init);
+ cx_b.update(collab_ui::channel_view::init);
+ cx_c.update(collab_ui::channel_view::init);
+
let channel_1_id = server
.make_channel(
"channel-1",
+ None,
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
@@ -604,6 +612,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
let channel_2_id = server
.make_channel(
"channel-2",
+ None,
(&client_a, cx_a),
&mut [(&client_b, cx_b), (&client_c, cx_c)],
)
@@ -633,20 +642,27 @@ async fn test_following_to_channel_notes_without_a_shared_project(
let (project_c, _) = client_b.build_local_project("/c", cx_c).await;
let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
- let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
+ let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
+
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
// Client A opens the notes for channel 1.
let channel_view_1_a = cx_a
- .update(|cx| {
- ChannelView::open(
- channel_1_id,
- workspace_a.read(cx).active_pane().clone(),
- workspace_a.clone(),
- cx,
- )
- })
+ .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
.await
.unwrap();
+ channel_view_1_a.update(cx_a, |notes, cx| {
+ assert_eq!(notes.channel(cx).name, "channel-1");
+ notes.editor.update(cx, |editor, cx| {
+ editor.insert("Hello from A.", cx);
+ editor.change_selections(None, cx, |selections| {
+ selections.select_ranges(vec![3..4]);
+ });
+ });
+ });
// Client B follows client A.
workspace_b
@@ -658,12 +674,51 @@ async fn test_following_to_channel_notes_without_a_shared_project(
.await
.unwrap();
+ // Client B is taken to the notes for channel 1, with the same
+ // text selected as client A.
+ deterministic.run_until_parked();
+ let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| {
+ assert_eq!(
+ workspace.leader_for_pane(workspace.active_pane()),
+ Some(client_a.peer_id().unwrap())
+ );
+ workspace
+ .active_item(cx)
+ .expect("no active item")
+ .downcast::<ChannelView>()
+ .expect("active item is not a channel view")
+ });
+ channel_view_1_b.read_with(cx_b, |notes, cx| {
+ assert_eq!(notes.channel(cx).name, "channel-1");
+ let editor = notes.editor.read(cx);
+ assert_eq!(editor.text(cx), "Hello from A.");
+ assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
+ });
+
+ // Client A opens the notes for channel 2.
+ let channel_view_2_a = cx_a
+ .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
+ .await
+ .unwrap();
+ channel_view_2_a.read_with(cx_a, |notes, cx| {
+ assert_eq!(notes.channel(cx).name, "channel-2");
+ });
+
+ // Client B is taken to the notes for channel 2.
deterministic.run_until_parked();
- workspace_b.read_with(cx_b, |workspace, _| {
+ let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| {
assert_eq!(
workspace.leader_for_pane(workspace.active_pane()),
Some(client_a.peer_id().unwrap())
);
+ workspace
+ .active_item(cx)
+ .expect("no active item")
+ .downcast::<ChannelView>()
+ .expect("active item is not a channel view")
+ });
+ channel_view_2_b.read_with(cx_b, |notes, cx| {
+ assert_eq!(notes.channel(cx).name, "channel-2");
});
}
@@ -6848,9 +6848,9 @@ async fn test_basic_following(
let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
workspace
.active_item(cx)
- .unwrap()
+ .expect("no active item")
.downcast::<SharedScreen>()
- .unwrap()
+ .expect("active item isn't a shared screen")
});
// Client B activates Zed again, which causes the previous editor to become focused again.
@@ -1,6 +1,6 @@
use anyhow::{anyhow, Result};
use call::ActiveCall;
-use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId};
+use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId};
use client::proto;
use clock::ReplicaId;
use collections::HashMap;
@@ -13,7 +13,10 @@ use gpui::{
ViewContext, ViewHandle,
};
use project::Project;
-use std::any::{Any, TypeId};
+use std::{
+ any::{Any, TypeId},
+ sync::Arc,
+};
use util::ResultExt;
use workspace::{
item::{FollowableItem, Item, ItemHandle},
@@ -24,7 +27,7 @@ use workspace::{
actions!(channel_view, [Deploy]);
-pub(crate) fn init(cx: &mut AppContext) {
+pub fn init(cx: &mut AppContext) {
register_followable_item::<ChannelView>(cx)
}
@@ -37,9 +40,13 @@ pub struct ChannelView {
}
impl ChannelView {
- pub fn deploy(channel_id: ChannelId, workspace: ViewHandle<Workspace>, cx: &mut AppContext) {
+ pub fn open(
+ channel_id: ChannelId,
+ workspace: ViewHandle<Workspace>,
+ cx: &mut AppContext,
+ ) -> Task<Result<ViewHandle<Self>>> {
let pane = workspace.read(cx).active_pane().clone();
- let channel_view = Self::open(channel_id, pane.clone(), workspace.clone(), cx);
+ let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
cx.spawn(|mut cx| async move {
let channel_view = channel_view.await?;
pane.update(&mut cx, |pane, cx| {
@@ -56,12 +63,11 @@ impl ChannelView {
);
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
});
- anyhow::Ok(())
+ anyhow::Ok(channel_view)
})
- .detach();
}
- pub fn open(
+ pub fn open_in_pane(
channel_id: ChannelId,
pane: ViewHandle<Pane>,
workspace: ViewHandle<Workspace>,
@@ -121,6 +127,10 @@ impl ChannelView {
this
}
+ pub fn channel(&self, cx: &AppContext) -> Arc<Channel> {
+ self.channel_buffer.read(cx).channel()
+ }
+
fn handle_project_event(
&mut self,
_: ModelHandle<Project>,
@@ -318,7 +328,7 @@ impl FollowableItem for ChannelView {
unreachable!()
};
- let open = ChannelView::open(state.channel_id, pane, workspace, cx);
+ let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
Some(cx.spawn(|mut cx| async move {
let this = open.await?;
@@ -391,4 +401,8 @@ impl FollowableItem for ChannelView {
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
Editor::should_unfollow_on_event(event, cx)
}
+
+ fn is_project_item(&self, _cx: &AppContext) -> bool {
+ false
+ }
}
@@ -409,7 +409,7 @@ impl ChatPanel {
})
.on_click(MouseButton::Left, move |_, _, cx| {
if let Some(workspace) = workspace.upgrade(cx) {
- ChannelView::deploy(channel_id, workspace, cx);
+ ChannelView::open(channel_id, workspace, cx).detach();
}
})
.with_tooltip::<OpenChannelNotes>(
@@ -546,7 +546,7 @@ impl ChatPanel {
if let Some((chat, _)) = &self.active_chat {
let channel_id = chat.read(cx).channel().id;
if let Some(workspace) = self.workspace.upgrade(cx) {
- ChannelView::deploy(channel_id, workspace, cx);
+ ChannelView::open(channel_id, workspace, cx).detach();
}
}
}
@@ -2745,7 +2745,7 @@ impl CollabPanel {
fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade(cx) {
- ChannelView::deploy(action.channel_id, workspace, cx);
+ ChannelView::open(action.channel_id, workspace, cx).detach();
}
}
@@ -1004,61 +1004,64 @@ impl CollabTitlebarItem {
.into_any();
if let Some(location) = location {
- if let Some(replica_id) = replica_id {
- enum ToggleFollow {}
+ match (replica_id, location) {
+ (None, ParticipantLocation::SharedProject { project_id }) => {
+ enum JoinProject {}
- content = MouseEventHandler::new::<ToggleFollow, _>(
- replica_id.into(),
- cx,
- move |_, _| content,
- )
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, item, cx| {
- if let Some(workspace) = item.workspace.upgrade(cx) {
- if let Some(task) = workspace
- .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
- {
- task.detach_and_log_err(cx);
+ let user_id = user.id;
+ content = MouseEventHandler::new::<JoinProject, _>(
+ peer_id.as_u64() as usize,
+ cx,
+ move |_, _| content,
+ )
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, this, cx| {
+ if let Some(workspace) = this.workspace.upgrade(cx) {
+ let app_state = workspace.read(cx).app_state().clone();
+ workspace::join_remote_project(project_id, user_id, app_state, cx)
+ .detach_and_log_err(cx);
}
- }
- })
- .with_tooltip::<ToggleFollow>(
- peer_id.as_u64() as usize,
- if is_being_followed {
- format!("Unfollow {}", user.github_login)
- } else {
- format!("Follow {}", user.github_login)
- },
- Some(Box::new(FollowNextCollaborator)),
- theme.tooltip.clone(),
- cx,
- )
- .into_any();
- } else if let ParticipantLocation::SharedProject { project_id } = location {
- enum JoinProject {}
+ })
+ .with_tooltip::<JoinProject>(
+ peer_id.as_u64() as usize,
+ format!("Follow {} into external project", user.github_login),
+ Some(Box::new(FollowNextCollaborator)),
+ theme.tooltip.clone(),
+ cx,
+ )
+ .into_any();
+ }
+ _ => {
+ enum ToggleFollow {}
- let user_id = user.id;
- content = MouseEventHandler::new::<JoinProject, _>(
- peer_id.as_u64() as usize,
- cx,
- move |_, _| content,
- )
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(workspace) = this.workspace.upgrade(cx) {
- let app_state = workspace.read(cx).app_state().clone();
- workspace::join_remote_project(project_id, user_id, app_state, cx)
- .detach_and_log_err(cx);
- }
- })
- .with_tooltip::<JoinProject>(
- peer_id.as_u64() as usize,
- format!("Follow {} into external project", user.github_login),
- Some(Box::new(FollowNextCollaborator)),
- theme.tooltip.clone(),
- cx,
- )
- .into_any();
+ content = MouseEventHandler::new::<ToggleFollow, _>(
+ user.id as usize,
+ cx,
+ move |_, _| content,
+ )
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, item, cx| {
+ if let Some(workspace) = item.workspace.upgrade(cx) {
+ if let Some(task) = workspace
+ .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
+ {
+ task.detach_and_log_err(cx);
+ }
+ }
+ })
+ .with_tooltip::<ToggleFollow>(
+ peer_id.as_u64() as usize,
+ if is_being_followed {
+ format!("Unfollow {}", user.github_login)
+ } else {
+ format!("Follow {}", user.github_login)
+ },
+ Some(Box::new(FollowNextCollaborator)),
+ theme.tooltip.clone(),
+ cx,
+ )
+ .into_any();
+ }
}
}
content
@@ -309,6 +309,10 @@ impl FollowableItem for Editor {
_ => false,
}
}
+
+ fn is_project_item(&self, _cx: &AppContext) -> bool {
+ true
+ }
}
async fn update_editor_from_message(
@@ -696,6 +696,7 @@ pub trait FollowableItem: Item {
message: proto::update_view::Variant,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>>;
+ fn is_project_item(&self, cx: &AppContext) -> bool;
fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
@@ -718,6 +719,7 @@ pub trait FollowableItemHandle: ItemHandle {
cx: &mut WindowContext,
) -> Task<Result<()>>;
fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
+ fn is_project_item(&self, cx: &AppContext) -> bool;
}
impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
@@ -769,6 +771,10 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
false
}
}
+
+ fn is_project_item(&self, cx: &AppContext) -> bool {
+ self.read(cx).is_project_item(cx)
+ }
}
#[cfg(any(test, feature = "test-support"))]
@@ -210,7 +210,6 @@ impl Member {
} else {
let leader_user = leader.user.clone();
let leader_user_id = leader.user.id;
- let app_state = Arc::downgrade(app_state);
Some(
MouseEventHandler::new::<FollowIntoExternalProject, _>(
pane.id(),
@@ -234,16 +233,14 @@ impl Member {
},
)
.with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, _, cx| {
- if let Some(app_state) = app_state.upgrade() {
- crate::join_remote_project(
- leader_project_id,
- leader_user_id,
- app_state,
- cx,
- )
- .detach_and_log_err(cx);
- }
+ .on_click(MouseButton::Left, move |_, this, cx| {
+ crate::join_remote_project(
+ leader_project_id,
+ leader_user_id,
+ this.app_state().clone(),
+ cx,
+ )
+ .detach_and_log_err(cx);
})
.aligned()
.bottom()
@@ -2832,14 +2832,12 @@ impl Workspace {
.read_with(cx, |this, _| this.project.clone())
.ok_or_else(|| anyhow!("window dropped"))?;
- let replica_id = project
- .read_with(cx, |project, _| {
- project
- .collaborators()
- .get(&leader_id)
- .map(|c| c.replica_id)
- })
- .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
+ let replica_id = project.read_with(cx, |project, _| {
+ project
+ .collaborators()
+ .get(&leader_id)
+ .map(|c| c.replica_id)
+ });
let item_builders = cx.update(|cx| {
cx.default_global::<FollowableItemBuilders>()
@@ -2884,7 +2882,7 @@ impl Workspace {
.get_mut(&pane)?;
for (id, item) in leader_view_ids.into_iter().zip(items) {
- item.set_leader_replica_id(Some(replica_id), cx);
+ item.set_leader_replica_id(replica_id, cx);
state.items_by_leader_view_id.insert(id, item);
}
@@ -2947,30 +2945,40 @@ impl Workspace {
let room = call.read(cx).room()?.read(cx);
let participant = room.remote_participant_for_peer_id(leader_id)?;
let mut items_to_activate = Vec::new();
+
+ let leader_in_this_app;
+ let leader_in_this_project;
match participant.location {
call::ParticipantLocation::SharedProject { project_id } => {
- if Some(project_id) == self.project.read(cx).remote_id() {
- for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
- if let Some(item) = state
- .active_view_id
- .and_then(|id| state.items_by_leader_view_id.get(&id))
- {
- items_to_activate.push((pane.clone(), item.boxed_clone()));
- } else if let Some(shared_screen) =
- self.shared_screen_for_peer(leader_id, pane, cx)
- {
- items_to_activate.push((pane.clone(), Box::new(shared_screen)));
- }
- }
- }
+ leader_in_this_app = true;
+ leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
+ }
+ call::ParticipantLocation::UnsharedProject => {
+ leader_in_this_app = true;
+ leader_in_this_project = false;
}
- call::ParticipantLocation::UnsharedProject => {}
call::ParticipantLocation::External => {
- for (pane, _) in self.follower_states_by_leader.get(&leader_id)? {
- if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
- items_to_activate.push((pane.clone(), Box::new(shared_screen)));
+ leader_in_this_app = false;
+ leader_in_this_project = false;
+ }
+ };
+
+ for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
+ let item = state
+ .active_view_id
+ .and_then(|id| state.items_by_leader_view_id.get(&id));
+ let shared_screen = self.shared_screen_for_peer(leader_id, pane, cx);
+
+ if leader_in_this_app {
+ if let Some(item) = item {
+ if leader_in_this_project || !item.is_project_item(cx) {
+ items_to_activate.push((pane.clone(), item.boxed_clone()));
}
+ } else if let Some(shared_screen) = shared_screen {
+ items_to_activate.push((pane.clone(), Box::new(shared_screen)));
}
+ } else if let Some(shared_screen) = shared_screen {
+ items_to_activate.push((pane.clone(), Box::new(shared_screen)));
}
}