Add the ability to follow the agent as it makes edits (#29839)
Nathan here: I also tacked on a bunch of UI refinement. Release Notes: - Introduced the ability to follow the agent around as it reads and edits files. --------- Co-authored-by: Nathan Sobo <nathan@zed.dev> Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
parent
425f32e068
commit
545ae27079
37 changed files with 1255 additions and 567 deletions
|
@ -36,7 +36,6 @@ clock.workspace = true
|
|||
collections.workspace = true
|
||||
component.workspace = true
|
||||
db.workspace = true
|
||||
derive_more.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
use crate::{
|
||||
DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, SerializableItemRegistry,
|
||||
ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory,
|
||||
SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
pane::{self, Pane},
|
||||
persistence::model::ItemId,
|
||||
searchable::SearchableItemHandle,
|
||||
workspace_settings::{AutosaveSetting, WorkspaceSettings},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use client::{
|
||||
Client,
|
||||
proto::{self, PeerId},
|
||||
};
|
||||
use client::{Client, proto};
|
||||
use futures::{StreamExt, channel::mpsc};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, Context, Entity, EntityId, EventEmitter, FocusHandle,
|
||||
|
@ -770,7 +767,7 @@ impl<T: Item> ItemHandle for Entity<T> {
|
|||
proto::UpdateView {
|
||||
id: item
|
||||
.remote_id(workspace.client(), window, cx)
|
||||
.map(|id| id.to_proto()),
|
||||
.and_then(|id| id.to_proto()),
|
||||
variant: pending_update.borrow_mut().take(),
|
||||
leader_id,
|
||||
},
|
||||
|
@ -810,13 +807,27 @@ impl<T: Item> ItemHandle for Entity<T> {
|
|||
}
|
||||
|
||||
if item.item_focus_handle(cx).contains_focused(window, cx) {
|
||||
item.add_event_to_update_proto(
|
||||
event,
|
||||
&mut pending_update.borrow_mut(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
pending_update_tx.unbounded_send(leader_id).ok();
|
||||
match leader_id {
|
||||
Some(CollaboratorId::Agent) => {}
|
||||
Some(CollaboratorId::PeerId(leader_peer_id)) => {
|
||||
item.add_event_to_update_proto(
|
||||
event,
|
||||
&mut pending_update.borrow_mut(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
pending_update_tx.unbounded_send(Some(leader_peer_id)).ok();
|
||||
}
|
||||
None => {
|
||||
item.add_event_to_update_proto(
|
||||
event,
|
||||
&mut pending_update.borrow_mut(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
pending_update_tx.unbounded_send(None).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1081,7 +1092,7 @@ pub trait ProjectItem: Item {
|
|||
|
||||
fn for_project_item(
|
||||
project: Entity<Project>,
|
||||
pane: &Pane,
|
||||
pane: Option<&Pane>,
|
||||
item: Entity<Self::Item>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
@ -1126,19 +1137,31 @@ pub trait FollowableItem: Item {
|
|||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
fn is_project_item(&self, window: &Window, cx: &App) -> bool;
|
||||
fn set_leader_peer_id(
|
||||
fn set_leader_id(
|
||||
&mut self,
|
||||
leader_peer_id: Option<PeerId>,
|
||||
leader_peer_id: Option<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
);
|
||||
fn dedup(&self, existing: &Self, window: &Window, cx: &App) -> Option<Dedup>;
|
||||
fn update_agent_location(
|
||||
&mut self,
|
||||
_location: language::Anchor,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FollowableItemHandle: ItemHandle {
|
||||
fn remote_id(&self, client: &Arc<Client>, window: &mut Window, cx: &mut App) -> Option<ViewId>;
|
||||
fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle>;
|
||||
fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, window: &mut Window, cx: &mut App);
|
||||
fn set_leader_id(
|
||||
&self,
|
||||
leader_peer_id: Option<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
);
|
||||
fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant>;
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
|
@ -1162,13 +1185,14 @@ pub trait FollowableItemHandle: ItemHandle {
|
|||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Dedup>;
|
||||
fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App);
|
||||
}
|
||||
|
||||
impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
|
||||
fn remote_id(&self, client: &Arc<Client>, _: &mut Window, cx: &mut App) -> Option<ViewId> {
|
||||
self.read(cx).remote_id().or_else(|| {
|
||||
client.peer_id().map(|creator| ViewId {
|
||||
creator,
|
||||
creator: CollaboratorId::PeerId(creator),
|
||||
id: self.item_id().as_u64(),
|
||||
})
|
||||
})
|
||||
|
@ -1178,15 +1202,8 @@ impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
|
|||
Box::new(self.downgrade())
|
||||
}
|
||||
|
||||
fn set_leader_peer_id(
|
||||
&self,
|
||||
leader_peer_id: Option<PeerId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.set_leader_peer_id(leader_peer_id, window, cx)
|
||||
})
|
||||
fn set_leader_id(&self, leader_id: Option<CollaboratorId>, window: &mut Window, cx: &mut App) {
|
||||
self.update(cx, |this, cx| this.set_leader_id(leader_id, window, cx))
|
||||
}
|
||||
|
||||
fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
|
||||
|
@ -1237,6 +1254,12 @@ impl<T: FollowableItem> FollowableItemHandle for Entity<T> {
|
|||
let existing = existing.to_any().downcast::<T>().ok()?;
|
||||
self.read(cx).dedup(existing.read(cx), window, cx)
|
||||
}
|
||||
|
||||
fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.update_agent_location(location, window, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WeakFollowableItemHandle: Send + Sync {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
use crate::{
|
||||
AppState, FollowerState, Pane, Workspace, WorkspaceSettings,
|
||||
AppState, CollaboratorId, FollowerState, Pane, Workspace, WorkspaceSettings,
|
||||
pane_group::element::pane_axis,
|
||||
workspace_settings::{PaneSplitDirectionHorizontal, PaneSplitDirectionVertical},
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use call::{ActiveCall, ParticipantLocation};
|
||||
use client::proto::PeerId;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels,
|
||||
|
@ -188,7 +187,7 @@ pub enum Member {
|
|||
#[derive(Clone, Copy)]
|
||||
pub struct PaneRenderContext<'a> {
|
||||
pub project: &'a Entity<Project>,
|
||||
pub follower_states: &'a HashMap<PeerId, FollowerState>,
|
||||
pub follower_states: &'a HashMap<CollaboratorId, FollowerState>,
|
||||
pub active_call: Option<&'a Entity<ActiveCall>>,
|
||||
pub active_pane: &'a Entity<Pane>,
|
||||
pub app_state: &'a Arc<AppState>,
|
||||
|
@ -243,88 +242,104 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> {
|
|||
None
|
||||
}
|
||||
});
|
||||
let leader = follower_state.as_ref().and_then(|(leader_id, _)| {
|
||||
let room = self.active_call?.read(cx).room()?.read(cx);
|
||||
room.remote_participant_for_peer_id(*leader_id)
|
||||
});
|
||||
let Some(leader) = leader else {
|
||||
let Some((leader_id, follower_state)) = follower_state else {
|
||||
return LeaderDecoration::default();
|
||||
};
|
||||
let is_in_unshared_view = follower_state.as_ref().map_or(false, |(_, state)| {
|
||||
state
|
||||
.active_view_id
|
||||
.is_some_and(|view_id| !state.items_by_leader_view_id.contains_key(&view_id))
|
||||
});
|
||||
let is_in_panel = follower_state
|
||||
.as_ref()
|
||||
.map_or(false, |(_, state)| state.dock_pane.is_some());
|
||||
|
||||
let mut leader_join_data = None;
|
||||
let leader_status_box = match leader.location {
|
||||
ParticipantLocation::SharedProject {
|
||||
project_id: leader_project_id,
|
||||
} => {
|
||||
if Some(leader_project_id) == self.project.read(cx).remote_id() {
|
||||
is_in_unshared_view.then(|| {
|
||||
Label::new(format!(
|
||||
"{} is in an unshared pane",
|
||||
leader.user.github_login
|
||||
))
|
||||
})
|
||||
} else {
|
||||
leader_join_data = Some((leader_project_id, leader.user.id));
|
||||
Some(Label::new(format!(
|
||||
"Follow {} to their active project",
|
||||
leader.user.github_login,
|
||||
)))
|
||||
}
|
||||
let mut leader_color;
|
||||
let status_box;
|
||||
match leader_id {
|
||||
CollaboratorId::PeerId(peer_id) => {
|
||||
let Some(leader) = self.active_call.as_ref().and_then(|call| {
|
||||
let room = call.read(cx).room()?.read(cx);
|
||||
room.remote_participant_for_peer_id(peer_id)
|
||||
}) else {
|
||||
return LeaderDecoration::default();
|
||||
};
|
||||
|
||||
let is_in_unshared_view = follower_state.active_view_id.is_some_and(|view_id| {
|
||||
!follower_state
|
||||
.items_by_leader_view_id
|
||||
.contains_key(&view_id)
|
||||
});
|
||||
|
||||
let mut leader_join_data = None;
|
||||
let leader_status_box = match leader.location {
|
||||
ParticipantLocation::SharedProject {
|
||||
project_id: leader_project_id,
|
||||
} => {
|
||||
if Some(leader_project_id) == self.project.read(cx).remote_id() {
|
||||
is_in_unshared_view.then(|| {
|
||||
Label::new(format!(
|
||||
"{} is in an unshared pane",
|
||||
leader.user.github_login
|
||||
))
|
||||
})
|
||||
} else {
|
||||
leader_join_data = Some((leader_project_id, leader.user.id));
|
||||
Some(Label::new(format!(
|
||||
"Follow {} to their active project",
|
||||
leader.user.github_login,
|
||||
)))
|
||||
}
|
||||
}
|
||||
ParticipantLocation::UnsharedProject => Some(Label::new(format!(
|
||||
"{} is viewing an unshared Zed project",
|
||||
leader.user.github_login
|
||||
))),
|
||||
ParticipantLocation::External => Some(Label::new(format!(
|
||||
"{} is viewing a window outside of Zed",
|
||||
leader.user.github_login
|
||||
))),
|
||||
};
|
||||
status_box = leader_status_box.map(|status| {
|
||||
div()
|
||||
.absolute()
|
||||
.w_96()
|
||||
.bottom_3()
|
||||
.right_3()
|
||||
.elevation_2(cx)
|
||||
.p_1()
|
||||
.child(status)
|
||||
.when_some(
|
||||
leader_join_data,
|
||||
|this, (leader_project_id, leader_user_id)| {
|
||||
let app_state = self.app_state.clone();
|
||||
this.cursor_pointer().on_mouse_down(
|
||||
MouseButton::Left,
|
||||
move |_, _, cx| {
|
||||
crate::join_in_room_project(
|
||||
leader_project_id,
|
||||
leader_user_id,
|
||||
app_state.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
});
|
||||
leader_color = cx
|
||||
.theme()
|
||||
.players()
|
||||
.color_for_participant(leader.participant_index.0)
|
||||
.cursor;
|
||||
}
|
||||
ParticipantLocation::UnsharedProject => Some(Label::new(format!(
|
||||
"{} is viewing an unshared Zed project",
|
||||
leader.user.github_login
|
||||
))),
|
||||
ParticipantLocation::External => Some(Label::new(format!(
|
||||
"{} is viewing a window outside of Zed",
|
||||
leader.user.github_login
|
||||
))),
|
||||
};
|
||||
let mut leader_color = cx
|
||||
.theme()
|
||||
.players()
|
||||
.color_for_participant(leader.participant_index.0)
|
||||
.cursor;
|
||||
CollaboratorId::Agent => {
|
||||
status_box = None;
|
||||
leader_color = cx.theme().players().agent().cursor;
|
||||
}
|
||||
}
|
||||
|
||||
let is_in_panel = follower_state.dock_pane.is_some();
|
||||
if is_in_panel {
|
||||
leader_color.fade_out(0.75);
|
||||
} else {
|
||||
leader_color.fade_out(0.3);
|
||||
}
|
||||
let status_box = leader_status_box.map(|status| {
|
||||
div()
|
||||
.absolute()
|
||||
.w_96()
|
||||
.bottom_3()
|
||||
.right_3()
|
||||
.elevation_2(cx)
|
||||
.p_1()
|
||||
.child(status)
|
||||
.when_some(
|
||||
leader_join_data,
|
||||
|this, (leader_project_id, leader_user_id)| {
|
||||
let app_state = self.app_state.clone();
|
||||
this.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, move |_, _, cx| {
|
||||
crate::join_in_room_project(
|
||||
leader_project_id,
|
||||
leader_user_id,
|
||||
app_state.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
});
|
||||
|
||||
LeaderDecoration {
|
||||
status_box,
|
||||
border: Some(leader_color),
|
||||
|
@ -339,6 +354,7 @@ impl PaneLeaderDecorator for PaneRenderContext<'_> {
|
|||
self.workspace
|
||||
}
|
||||
}
|
||||
|
||||
impl Member {
|
||||
fn new_axis(old_pane: Entity<Pane>, new_pane: Entity<Pane>, direction: SplitDirection) -> Self {
|
||||
use Axis::*;
|
||||
|
|
|
@ -24,7 +24,6 @@ use client::{
|
|||
proto::{self, ErrorCode, PanelId, PeerId},
|
||||
};
|
||||
use collections::{HashMap, HashSet, hash_map};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
pub use dock::Panel;
|
||||
use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
|
||||
use futures::{
|
||||
|
@ -36,12 +35,12 @@ use futures::{
|
|||
future::try_join_all,
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context, CursorStyle,
|
||||
Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Global,
|
||||
Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions, Point, PromptLevel,
|
||||
Render, ResizeEdge, Size, Stateful, Subscription, Task, Tiling, WeakEntity, WindowBounds,
|
||||
WindowHandle, WindowId, WindowOptions, action_as, actions, canvas, impl_action_as,
|
||||
impl_actions, point, relative, size, transparent_black,
|
||||
Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
|
||||
CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
|
||||
Focusable, Global, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions,
|
||||
Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task, Tiling, WeakEntity,
|
||||
WindowBounds, WindowHandle, WindowId, WindowOptions, action_as, actions, canvas,
|
||||
impl_action_as, impl_actions, point, relative, size, transparent_black,
|
||||
};
|
||||
pub use history_manager::*;
|
||||
pub use item::{
|
||||
|
@ -451,44 +450,97 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
|||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deref, DerefMut)]
|
||||
struct ProjectItemOpeners(Vec<ProjectItemOpener>);
|
||||
type BuildProjectItemFn =
|
||||
fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
|
||||
|
||||
type ProjectItemOpener = fn(
|
||||
&Entity<Project>,
|
||||
&ProjectPath,
|
||||
&mut Window,
|
||||
&mut App,
|
||||
)
|
||||
-> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
|
||||
type BuildProjectItemForPathFn =
|
||||
fn(
|
||||
&Entity<Project>,
|
||||
&ProjectPath,
|
||||
&mut Window,
|
||||
&mut App,
|
||||
) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct ProjectItemRegistry {
|
||||
build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
|
||||
build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
|
||||
}
|
||||
|
||||
impl ProjectItemRegistry {
|
||||
fn register<T: ProjectItem>(&mut self) {
|
||||
self.build_project_item_fns_by_type.insert(
|
||||
TypeId::of::<T::Item>(),
|
||||
|item, project, pane, window, cx| {
|
||||
let item = item.downcast().unwrap();
|
||||
Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
|
||||
as Box<dyn ItemHandle>
|
||||
},
|
||||
);
|
||||
self.build_project_item_for_path_fns
|
||||
.push(|project, project_path, window, cx| {
|
||||
let project_item =
|
||||
<T::Item as project::ProjectItem>::try_open(project, project_path, cx)?;
|
||||
let project = project.clone();
|
||||
Some(window.spawn(cx, async move |cx| {
|
||||
let project_item = project_item.await?;
|
||||
let project_entry_id: Option<ProjectEntryId> =
|
||||
project_item.read_with(cx, project::ProjectItem::entry_id)?;
|
||||
let build_workspace_item = Box::new(
|
||||
|pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
|
||||
Box::new(cx.new(|cx| {
|
||||
T::for_project_item(project, Some(pane), project_item, window, cx)
|
||||
})) as Box<dyn ItemHandle>
|
||||
},
|
||||
) as Box<_>;
|
||||
Ok((project_entry_id, build_workspace_item))
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
fn open_path(
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
path: &ProjectPath,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
|
||||
let Some(open_project_item) = self
|
||||
.build_project_item_for_path_fns
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|open_project_item| open_project_item(&project, &path, window, cx))
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
|
||||
};
|
||||
open_project_item
|
||||
}
|
||||
|
||||
fn build_item<T: project::ProjectItem>(
|
||||
&self,
|
||||
item: Entity<T>,
|
||||
project: Entity<Project>,
|
||||
pane: Option<&Pane>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Box<dyn ItemHandle>> {
|
||||
let build = self
|
||||
.build_project_item_fns_by_type
|
||||
.get(&TypeId::of::<T>())?;
|
||||
Some(build(item.into_any(), project, pane, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
type WorkspaceItemBuilder =
|
||||
Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
|
||||
|
||||
impl Global for ProjectItemOpeners {}
|
||||
impl Global for ProjectItemRegistry {}
|
||||
|
||||
/// Registers a [ProjectItem] for the app. When opening a file, all the registered
|
||||
/// items will get a chance to open the file, starting from the project item that
|
||||
/// was added last.
|
||||
pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
|
||||
let builders = cx.default_global::<ProjectItemOpeners>();
|
||||
builders.push(|project, project_path, window, cx| {
|
||||
let project_item = <I::Item as project::ProjectItem>::try_open(project, project_path, cx)?;
|
||||
let project = project.clone();
|
||||
Some(window.spawn(cx, async move |cx| {
|
||||
let project_item = project_item.await?;
|
||||
let project_entry_id: Option<ProjectEntryId> =
|
||||
project_item.read_with(cx, project::ProjectItem::entry_id)?;
|
||||
let build_workspace_item = Box::new(
|
||||
|pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
|
||||
Box::new(
|
||||
cx.new(|cx| I::for_project_item(project, pane, project_item, window, cx)),
|
||||
) as Box<dyn ItemHandle>
|
||||
},
|
||||
) as Box<_>;
|
||||
Ok((project_entry_id, build_workspace_item))
|
||||
}))
|
||||
});
|
||||
cx.default_global::<ProjectItemRegistry>().register::<I>();
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -666,6 +718,24 @@ pub struct WorkspaceStore {
|
|||
_subscriptions: Vec<client::Subscription>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
|
||||
pub enum CollaboratorId {
|
||||
PeerId(PeerId),
|
||||
Agent,
|
||||
}
|
||||
|
||||
impl From<PeerId> for CollaboratorId {
|
||||
fn from(peer_id: PeerId) -> Self {
|
||||
CollaboratorId::PeerId(peer_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PeerId> for CollaboratorId {
|
||||
fn from(peer_id: &PeerId) -> Self {
|
||||
CollaboratorId::PeerId(*peer_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||
struct Follower {
|
||||
project_id: Option<u64>,
|
||||
|
@ -852,8 +922,8 @@ pub struct Workspace {
|
|||
titlebar_item: Option<AnyView>,
|
||||
notifications: Notifications,
|
||||
project: Entity<Project>,
|
||||
follower_states: HashMap<PeerId, FollowerState>,
|
||||
last_leaders_by_pane: HashMap<WeakEntity<Pane>, PeerId>,
|
||||
follower_states: HashMap<CollaboratorId, FollowerState>,
|
||||
last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
|
||||
window_edited: bool,
|
||||
dirty_items: HashMap<EntityId, Subscription>,
|
||||
active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
|
||||
|
@ -883,7 +953,7 @@ impl EventEmitter<Event> for Workspace {}
|
|||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ViewId {
|
||||
pub creator: PeerId,
|
||||
pub creator: CollaboratorId,
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
|
@ -984,6 +1054,10 @@ impl Workspace {
|
|||
);
|
||||
}
|
||||
|
||||
project::Event::AgentLocationChanged => {
|
||||
this.handle_agent_location_changed(window, cx)
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
cx.notify()
|
||||
|
@ -3083,15 +3157,8 @@ impl Workspace {
|
|||
cx: &mut App,
|
||||
) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
|
||||
let project = self.project().clone();
|
||||
let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
|
||||
let Some(open_project_item) = project_item_builders
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|open_project_item| open_project_item(&project, &path, window, cx))
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
|
||||
};
|
||||
open_project_item
|
||||
let registry = cx.default_global::<ProjectItemRegistry>().clone();
|
||||
registry.open_path(&project, &path, window, cx)
|
||||
}
|
||||
|
||||
pub fn find_project_item<T>(
|
||||
|
@ -3152,7 +3219,9 @@ impl Workspace {
|
|||
}
|
||||
|
||||
let item = pane.update(cx, |pane, cx| {
|
||||
cx.new(|cx| T::for_project_item(self.project().clone(), pane, project_item, window, cx))
|
||||
cx.new(|cx| {
|
||||
T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
|
||||
})
|
||||
});
|
||||
let item_id = item.item_id();
|
||||
let mut destination_index = None;
|
||||
|
@ -3605,7 +3674,7 @@ impl Workspace {
|
|||
pane: &Entity<Pane>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Option<PeerId> {
|
||||
) -> Option<CollaboratorId> {
|
||||
let leader_id = self.leader_for_pane(pane)?;
|
||||
self.unfollow(leader_id, window, cx);
|
||||
Some(leader_id)
|
||||
|
@ -3785,9 +3854,9 @@ impl Workspace {
|
|||
|
||||
fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.follower_states.retain(|leader_id, state| {
|
||||
if *leader_id == peer_id {
|
||||
if *leader_id == CollaboratorId::PeerId(peer_id) {
|
||||
for item in state.items_by_leader_view_id.values() {
|
||||
item.view.set_leader_peer_id(None, window, cx);
|
||||
item.view.set_leader_id(None, window, cx);
|
||||
}
|
||||
false
|
||||
} else {
|
||||
|
@ -3799,10 +3868,11 @@ impl Workspace {
|
|||
|
||||
pub fn start_following(
|
||||
&mut self,
|
||||
leader_id: PeerId,
|
||||
leader_id: impl Into<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let leader_id = leader_id.into();
|
||||
let pane = self.active_pane().clone();
|
||||
|
||||
self.last_leaders_by_pane
|
||||
|
@ -3820,35 +3890,43 @@ impl Workspace {
|
|||
);
|
||||
cx.notify();
|
||||
|
||||
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||
let project_id = self.project.read(cx).remote_id();
|
||||
let request = self.app_state.client.request(proto::Follow {
|
||||
room_id,
|
||||
project_id,
|
||||
leader_id: Some(leader_id),
|
||||
});
|
||||
match leader_id {
|
||||
CollaboratorId::PeerId(leader_peer_id) => {
|
||||
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||
let project_id = self.project.read(cx).remote_id();
|
||||
let request = self.app_state.client.request(proto::Follow {
|
||||
room_id,
|
||||
project_id,
|
||||
leader_id: Some(leader_peer_id),
|
||||
});
|
||||
|
||||
Some(cx.spawn_in(window, async move |this, cx| {
|
||||
let response = request.await?;
|
||||
this.update(cx, |this, _| {
|
||||
let state = this
|
||||
.follower_states
|
||||
.get_mut(&leader_id)
|
||||
.ok_or_else(|| anyhow!("following interrupted"))?;
|
||||
state.active_view_id = response
|
||||
.active_view
|
||||
.as_ref()
|
||||
.and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})??;
|
||||
if let Some(view) = response.active_view {
|
||||
Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?;
|
||||
Some(cx.spawn_in(window, async move |this, cx| {
|
||||
let response = request.await?;
|
||||
this.update(cx, |this, _| {
|
||||
let state = this
|
||||
.follower_states
|
||||
.get_mut(&leader_id)
|
||||
.ok_or_else(|| anyhow!("following interrupted"))?;
|
||||
state.active_view_id = response
|
||||
.active_view
|
||||
.as_ref()
|
||||
.and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})??;
|
||||
if let Some(view) = response.active_view {
|
||||
Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
|
||||
}
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.leader_updated(leader_id, window, cx)
|
||||
})?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.leader_updated(leader_id, window, cx)
|
||||
})?;
|
||||
Ok(())
|
||||
}))
|
||||
CollaboratorId::Agent => {
|
||||
self.leader_updated(leader_id, window, cx)?;
|
||||
Some(Task::ready(Ok(())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn follow_next_collaborator(
|
||||
|
@ -3861,26 +3939,34 @@ impl Workspace {
|
|||
let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
|
||||
let mut collaborators = collaborators.keys().copied();
|
||||
for peer_id in collaborators.by_ref() {
|
||||
if peer_id == leader_id {
|
||||
if CollaboratorId::PeerId(peer_id) == leader_id {
|
||||
break;
|
||||
}
|
||||
}
|
||||
collaborators.next()
|
||||
collaborators.next().map(CollaboratorId::PeerId)
|
||||
} else if let Some(last_leader_id) =
|
||||
self.last_leaders_by_pane.get(&self.active_pane.downgrade())
|
||||
{
|
||||
if collaborators.contains_key(last_leader_id) {
|
||||
Some(*last_leader_id)
|
||||
} else {
|
||||
None
|
||||
match last_leader_id {
|
||||
CollaboratorId::PeerId(peer_id) => {
|
||||
if collaborators.contains_key(peer_id) {
|
||||
Some(*last_leader_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
CollaboratorId::Agent => Some(CollaboratorId::Agent),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let pane = self.active_pane.clone();
|
||||
let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
|
||||
else {
|
||||
let Some(leader_id) = next_leader_id.or_else(|| {
|
||||
Some(CollaboratorId::PeerId(
|
||||
collaborators.keys().copied().next()?,
|
||||
))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
|
||||
|
@ -3891,34 +3977,43 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn follow(&mut self, leader_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(room) = ActiveCall::global(cx).read(cx).room() else {
|
||||
return;
|
||||
};
|
||||
let room = room.read(cx);
|
||||
let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
|
||||
return;
|
||||
};
|
||||
pub fn follow(
|
||||
&mut self,
|
||||
leader_id: impl Into<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let leader_id = leader_id.into();
|
||||
|
||||
let project = self.project.read(cx);
|
||||
if let CollaboratorId::PeerId(peer_id) = leader_id {
|
||||
let Some(room) = ActiveCall::global(cx).read(cx).room() else {
|
||||
return;
|
||||
};
|
||||
let room = room.read(cx);
|
||||
let Some(remote_participant) = room.remote_participant_for_peer_id(peer_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let other_project_id = match remote_participant.location {
|
||||
call::ParticipantLocation::External => None,
|
||||
call::ParticipantLocation::UnsharedProject => None,
|
||||
call::ParticipantLocation::SharedProject { project_id } => {
|
||||
if Some(project_id) == project.remote_id() {
|
||||
None
|
||||
} else {
|
||||
Some(project_id)
|
||||
let project = self.project.read(cx);
|
||||
|
||||
let other_project_id = match remote_participant.location {
|
||||
call::ParticipantLocation::External => None,
|
||||
call::ParticipantLocation::UnsharedProject => None,
|
||||
call::ParticipantLocation::SharedProject { project_id } => {
|
||||
if Some(project_id) == project.remote_id() {
|
||||
None
|
||||
} else {
|
||||
Some(project_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// if they are active in another project, follow there.
|
||||
if let Some(project_id) = other_project_id {
|
||||
let app_state = self.app_state.clone();
|
||||
crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
|
||||
.detach_and_log_err(cx);
|
||||
// if they are active in another project, follow there.
|
||||
if let Some(project_id) = other_project_id {
|
||||
let app_state = self.app_state.clone();
|
||||
crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
// if you're already following, find the right pane and focus it.
|
||||
|
@ -3936,32 +4031,36 @@ impl Workspace {
|
|||
|
||||
pub fn unfollow(
|
||||
&mut self,
|
||||
leader_id: PeerId,
|
||||
leader_id: impl Into<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
cx.notify();
|
||||
|
||||
let leader_id = leader_id.into();
|
||||
let state = self.follower_states.remove(&leader_id)?;
|
||||
for (_, item) in state.items_by_leader_view_id {
|
||||
item.view.set_leader_peer_id(None, window, cx);
|
||||
item.view.set_leader_id(None, window, cx);
|
||||
}
|
||||
|
||||
let project_id = self.project.read(cx).remote_id();
|
||||
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||
self.app_state
|
||||
.client
|
||||
.send(proto::Unfollow {
|
||||
room_id,
|
||||
project_id,
|
||||
leader_id: Some(leader_id),
|
||||
})
|
||||
.log_err();
|
||||
if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
|
||||
let project_id = self.project.read(cx).remote_id();
|
||||
let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
|
||||
self.app_state
|
||||
.client
|
||||
.send(proto::Unfollow {
|
||||
room_id,
|
||||
project_id,
|
||||
leader_id: Some(leader_peer_id),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
|
||||
self.follower_states.contains_key(&peer_id)
|
||||
pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
|
||||
self.follower_states.contains_key(&id.into())
|
||||
}
|
||||
|
||||
fn active_item_path_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
@ -4096,6 +4195,10 @@ impl Workspace {
|
|||
let leader_id = self
|
||||
.pane_for(&*item)
|
||||
.and_then(|pane| self.leader_for_pane(&pane));
|
||||
let leader_peer_id = match leader_id {
|
||||
Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
|
||||
Some(CollaboratorId::Agent) | None => None,
|
||||
};
|
||||
|
||||
let item_handle = item.to_followable_item_handle(cx)?;
|
||||
let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
|
||||
|
@ -4109,8 +4212,8 @@ impl Workspace {
|
|||
}
|
||||
|
||||
Some(proto::View {
|
||||
id: Some(id.to_proto()),
|
||||
leader_id,
|
||||
id: id.to_proto(),
|
||||
leader_id: leader_peer_id,
|
||||
variant: Some(variant),
|
||||
panel_id: panel_id.map(|id| id as i32),
|
||||
})
|
||||
|
@ -4155,7 +4258,7 @@ impl Workspace {
|
|||
proto::update_followers::Variant::CreateView(view) => {
|
||||
let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
|
||||
let should_add_view = this.update(cx, |this, _| {
|
||||
if let Some(state) = this.follower_states.get_mut(&leader_id) {
|
||||
if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
|
||||
anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
|
||||
} else {
|
||||
anyhow::Ok(false)
|
||||
|
@ -4168,7 +4271,7 @@ impl Workspace {
|
|||
}
|
||||
proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
|
||||
let should_add_view = this.update(cx, |this, _| {
|
||||
if let Some(state) = this.follower_states.get_mut(&leader_id) {
|
||||
if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
|
||||
state.active_view_id = update_active_view
|
||||
.view
|
||||
.as_ref()
|
||||
|
@ -4202,7 +4305,7 @@ impl Workspace {
|
|||
let mut tasks = Vec::new();
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let project = this.project.clone();
|
||||
if let Some(state) = this.follower_states.get(&leader_id) {
|
||||
if let Some(state) = this.follower_states.get(&leader_id.into()) {
|
||||
let view_id = ViewId::from_proto(id.clone())?;
|
||||
if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
|
||||
tasks.push(item.view.apply_update_proto(
|
||||
|
@ -4241,7 +4344,7 @@ impl Workspace {
|
|||
let pane = this.update(cx, |this, _cx| {
|
||||
let state = this
|
||||
.follower_states
|
||||
.get(&leader_id)
|
||||
.get(&leader_id.into())
|
||||
.context("stopped following")?;
|
||||
anyhow::Ok(state.pane().clone())
|
||||
})??;
|
||||
|
@ -4304,8 +4407,8 @@ impl Workspace {
|
|||
};
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let state = this.follower_states.get_mut(&leader_id)?;
|
||||
item.set_leader_peer_id(Some(leader_id), window, cx);
|
||||
let state = this.follower_states.get_mut(&leader_id.into())?;
|
||||
item.set_leader_id(Some(leader_id.into()), window, cx);
|
||||
state.items_by_leader_view_id.insert(
|
||||
id,
|
||||
FollowerView {
|
||||
|
@ -4320,6 +4423,71 @@ impl Workspace {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(agent_location) = self.project.read(cx).agent_location() {
|
||||
let buffer_entity_id = agent_location.buffer.entity_id();
|
||||
let view_id = ViewId {
|
||||
creator: CollaboratorId::Agent,
|
||||
id: buffer_entity_id.as_u64(),
|
||||
};
|
||||
follower_state.active_view_id = Some(view_id);
|
||||
|
||||
let item = match follower_state.items_by_leader_view_id.entry(view_id) {
|
||||
hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let existing_view =
|
||||
follower_state
|
||||
.center_pane
|
||||
.read(cx)
|
||||
.items()
|
||||
.find_map(|item| {
|
||||
let item = item.to_followable_item_handle(cx)?;
|
||||
if item.is_singleton(cx)
|
||||
&& item.project_item_model_ids(cx).as_slice()
|
||||
== [buffer_entity_id]
|
||||
{
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let view = existing_view.or_else(|| {
|
||||
agent_location.buffer.upgrade().and_then(|buffer| {
|
||||
cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
|
||||
registry.build_item(buffer, self.project.clone(), None, window, cx)
|
||||
})?
|
||||
.to_followable_item_handle(cx)
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(view) = view {
|
||||
Some(entry.insert(FollowerView {
|
||||
view,
|
||||
location: None,
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(item) = item {
|
||||
item.view
|
||||
.set_leader_id(Some(CollaboratorId::Agent), window, cx);
|
||||
item.view
|
||||
.update_agent_location(agent_location.position, window, cx);
|
||||
}
|
||||
} else {
|
||||
follower_state.active_view_id = None;
|
||||
}
|
||||
|
||||
self.leader_updated(CollaboratorId::Agent, window, cx);
|
||||
}
|
||||
|
||||
pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
|
||||
let mut is_project_item = true;
|
||||
let mut update = proto::UpdateActiveView::default();
|
||||
|
@ -4331,6 +4499,10 @@ impl Workspace {
|
|||
let leader_id = self
|
||||
.pane_for(&*item)
|
||||
.and_then(|pane| self.leader_for_pane(&pane));
|
||||
let leader_peer_id = match leader_id {
|
||||
Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
|
||||
Some(CollaboratorId::Agent) | None => None,
|
||||
};
|
||||
|
||||
if let Some(item) = item.to_followable_item_handle(cx) {
|
||||
let id = item
|
||||
|
@ -4340,8 +4512,8 @@ impl Workspace {
|
|||
if let Some(id) = id.clone() {
|
||||
if let Some(variant) = item.to_state_proto(window, cx) {
|
||||
let view = Some(proto::View {
|
||||
id: Some(id.clone()),
|
||||
leader_id,
|
||||
id: id.clone(),
|
||||
leader_id: leader_peer_id,
|
||||
variant: Some(variant),
|
||||
panel_id: panel_id.map(|id| id as i32),
|
||||
});
|
||||
|
@ -4350,8 +4522,8 @@ impl Workspace {
|
|||
update = proto::UpdateActiveView {
|
||||
view,
|
||||
// TODO: Remove after version 0.145.x stabilizes.
|
||||
id: Some(id.clone()),
|
||||
leader_id,
|
||||
id: id.clone(),
|
||||
leader_id: leader_peer_id,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -4420,7 +4592,7 @@ impl Workspace {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<PeerId> {
|
||||
pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
|
||||
self.follower_states.iter().find_map(|(leader_id, state)| {
|
||||
if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
|
||||
Some(*leader_id)
|
||||
|
@ -4432,49 +4604,19 @@ impl Workspace {
|
|||
|
||||
fn leader_updated(
|
||||
&mut self,
|
||||
leader_id: PeerId,
|
||||
leader_id: impl Into<CollaboratorId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
) -> Option<Box<dyn ItemHandle>> {
|
||||
cx.notify();
|
||||
|
||||
let call = self.active_call()?;
|
||||
let room = call.read(cx).room()?.read(cx);
|
||||
let participant = room.remote_participant_for_peer_id(leader_id)?;
|
||||
|
||||
let leader_in_this_app;
|
||||
let leader_in_this_project;
|
||||
match participant.location {
|
||||
call::ParticipantLocation::SharedProject { project_id } => {
|
||||
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::External => {
|
||||
leader_in_this_app = false;
|
||||
leader_in_this_project = false;
|
||||
}
|
||||
let leader_id = leader_id.into();
|
||||
let (panel_id, item) = match leader_id {
|
||||
CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
|
||||
CollaboratorId::Agent => (None, self.active_item_for_agent()?),
|
||||
};
|
||||
|
||||
let state = self.follower_states.get(&leader_id)?;
|
||||
let mut item_to_activate = None;
|
||||
if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
|
||||
if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
|
||||
if leader_in_this_project || !item.view.is_project_item(window, cx) {
|
||||
item_to_activate = Some((item.location, item.view.boxed_clone()));
|
||||
}
|
||||
}
|
||||
} else if let Some(shared_screen) =
|
||||
self.shared_screen_for_peer(leader_id, &state.center_pane, window, cx)
|
||||
{
|
||||
item_to_activate = Some((None, Box::new(shared_screen)));
|
||||
}
|
||||
|
||||
let (panel_id, item) = item_to_activate?;
|
||||
|
||||
let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
|
||||
let pane;
|
||||
if let Some(panel_id) = panel_id {
|
||||
|
@ -4504,7 +4646,60 @@ impl Workspace {
|
|||
}
|
||||
});
|
||||
|
||||
None
|
||||
Some(item)
|
||||
}
|
||||
|
||||
fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
|
||||
let state = self.follower_states.get(&CollaboratorId::Agent)?;
|
||||
let active_view_id = state.active_view_id?;
|
||||
Some(
|
||||
state
|
||||
.items_by_leader_view_id
|
||||
.get(&active_view_id)?
|
||||
.view
|
||||
.boxed_clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn active_item_for_peer(
|
||||
&self,
|
||||
peer_id: PeerId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
|
||||
let call = self.active_call()?;
|
||||
let room = call.read(cx).room()?.read(cx);
|
||||
let participant = room.remote_participant_for_peer_id(peer_id)?;
|
||||
let leader_in_this_app;
|
||||
let leader_in_this_project;
|
||||
match participant.location {
|
||||
call::ParticipantLocation::SharedProject { project_id } => {
|
||||
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::External => {
|
||||
leader_in_this_app = false;
|
||||
leader_in_this_project = false;
|
||||
}
|
||||
};
|
||||
let state = self.follower_states.get(&peer_id.into())?;
|
||||
let mut item_to_activate = None;
|
||||
if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
|
||||
if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
|
||||
if leader_in_this_project || !item.view.is_project_item(window, cx) {
|
||||
item_to_activate = Some((item.location, item.view.boxed_clone()));
|
||||
}
|
||||
}
|
||||
} else if let Some(shared_screen) =
|
||||
self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
|
||||
{
|
||||
item_to_activate = Some((None, Box::new(shared_screen)));
|
||||
}
|
||||
item_to_activate
|
||||
}
|
||||
|
||||
fn shared_screen_for_peer(
|
||||
|
@ -4571,7 +4766,7 @@ impl Workspace {
|
|||
match event {
|
||||
call::room::Event::ParticipantLocationChanged { participant_id }
|
||||
| call::room::Event::RemoteVideoTracksChanged { participant_id } => {
|
||||
self.leader_updated(*participant_id, window, cx);
|
||||
self.leader_updated(participant_id, window, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@ -5285,7 +5480,7 @@ impl Workspace {
|
|||
}
|
||||
|
||||
fn leader_border_for_pane(
|
||||
follower_states: &HashMap<PeerId, FollowerState>,
|
||||
follower_states: &HashMap<CollaboratorId, FollowerState>,
|
||||
pane: &Entity<Pane>,
|
||||
_: &Window,
|
||||
cx: &App,
|
||||
|
@ -5298,14 +5493,18 @@ fn leader_border_for_pane(
|
|||
}
|
||||
})?;
|
||||
|
||||
let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
|
||||
let leader = room.remote_participant_for_peer_id(leader_id)?;
|
||||
let mut leader_color = match leader_id {
|
||||
CollaboratorId::PeerId(leader_peer_id) => {
|
||||
let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
|
||||
let leader = room.remote_participant_for_peer_id(leader_peer_id)?;
|
||||
|
||||
let mut leader_color = cx
|
||||
.theme()
|
||||
.players()
|
||||
.color_for_participant(leader.participant_index.0)
|
||||
.cursor;
|
||||
cx.theme()
|
||||
.players()
|
||||
.color_for_participant(leader.participant_index.0)
|
||||
.cursor
|
||||
}
|
||||
CollaboratorId::Agent => cx.theme().players().agent().cursor,
|
||||
};
|
||||
leader_color.fade_out(0.3);
|
||||
Some(
|
||||
div()
|
||||
|
@ -6029,15 +6228,20 @@ impl ViewId {
|
|||
Ok(Self {
|
||||
creator: message
|
||||
.creator
|
||||
.map(CollaboratorId::PeerId)
|
||||
.ok_or_else(|| anyhow!("creator is missing"))?,
|
||||
id: message.id,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn to_proto(self) -> proto::ViewId {
|
||||
proto::ViewId {
|
||||
creator: Some(self.creator),
|
||||
id: self.id,
|
||||
pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
|
||||
if let CollaboratorId::PeerId(peer_id) = self.creator {
|
||||
Some(proto::ViewId {
|
||||
creator: Some(peer_id),
|
||||
id: self.id,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9069,7 +9273,7 @@ mod tests {
|
|||
|
||||
fn for_project_item(
|
||||
_project: Entity<Project>,
|
||||
_pane: &Pane,
|
||||
_pane: Option<&Pane>,
|
||||
_item: Entity<Self::Item>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
@ -9144,7 +9348,7 @@ mod tests {
|
|||
|
||||
fn for_project_item(
|
||||
_project: Entity<Project>,
|
||||
_pane: &Pane,
|
||||
_pane: Option<&Pane>,
|
||||
_item: Entity<Self::Item>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
@ -9191,7 +9395,7 @@ mod tests {
|
|||
|
||||
fn for_project_item(
|
||||
_project: Entity<Project>,
|
||||
_pane: &Pane,
|
||||
_pane: Option<&Pane>,
|
||||
_item: Entity<Self::Item>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue