Introduce following for assistant panel (#14479)

Release Notes:

- Added support for following into the assistant panel.

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2024-07-15 11:36:27 +02:00 committed by GitHub
parent 977a1b7a82
commit decdd3b6ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 819 additions and 541 deletions

View file

@ -18,6 +18,7 @@ use crate::{
use anyhow::{anyhow, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
use breadcrumbs::Breadcrumbs;
use client::proto;
use collections::{BTreeSet, HashMap, HashSet};
use editor::{
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
@ -58,7 +59,7 @@ use ui::{
use util::ResultExt;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
item::{BreadcrumbText, Item, ItemHandle},
item::{self, BreadcrumbText, FollowableItem, Item, ItemHandle},
pane,
searchable::{SearchEvent, SearchableItem},
Pane, Save, ToggleZoom, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@ -66,6 +67,7 @@ use workspace::{
use workspace::{searchable::SearchableItemHandle, NewFile};
pub fn init(cx: &mut AppContext) {
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace
@ -374,7 +376,7 @@ impl AssistantPanel {
fn handle_pane_event(
&mut self,
_pane: View<Pane>,
pane: View<Pane>,
event: &pane::Event,
cx: &mut ViewContext<Self>,
) {
@ -384,14 +386,25 @@ impl AssistantPanel {
pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
pane::Event::AddItem { item } => {
if let Some(workspace) = self.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
self.workspace
.update(cx, |workspace, cx| {
item.added_to_pane(workspace, self.pane.clone(), cx)
});
}
})
.ok();
}
pane::Event::RemoveItem { .. } | pane::Event::ActivateItem { .. } => {
pane::Event::ActivateItem { local } => {
if *local {
self.workspace
.update(cx, |workspace, cx| {
workspace.unfollow_in_pane(&pane, cx);
})
.ok();
}
cx.emit(AssistantPanelEvent::ContextEdited);
}
pane::Event::RemoveItem { .. } => {
cx.emit(AssistantPanelEvent::ContextEdited);
}
@ -613,12 +626,13 @@ impl AssistantPanel {
fn handle_context_editor_event(
&mut self,
_: View<ContextEditor>,
event: &ContextEditorEvent,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
) {
match event {
ContextEditorEvent::TabContentChanged => cx.notify(),
ContextEditorEvent::Edited => cx.emit(AssistantPanelEvent::ContextEdited),
EditorEvent::TitleChanged { .. } => cx.notify(),
EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
_ => {}
}
}
@ -722,14 +736,17 @@ impl AssistantPanel {
&mut self,
id: ContextId,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
) -> Task<Result<View<ContextEditor>>> {
let existing_context = self.pane.read(cx).items().find_map(|item| {
item.downcast::<ContextEditor>()
.filter(|editor| *editor.read(cx).context.read(cx).id() == id)
});
if let Some(existing_context) = existing_context {
return cx.spawn(|this, mut cx| async move {
this.update(&mut cx, |this, cx| this.show_context(existing_context, cx))
this.update(&mut cx, |this, cx| {
this.show_context(existing_context.clone(), cx)
})?;
Ok(existing_context)
});
}
@ -755,10 +772,9 @@ impl AssistantPanel {
let editor = cx.new_view(|cx| {
ContextEditor::for_context(context, fs, workspace, lsp_adapter_delegate, cx)
});
this.show_context(editor, cx);
anyhow::Ok(())
})??;
Ok(())
this.show_context(editor.clone(), cx);
anyhow::Ok(editor)
})?
})
}
@ -878,6 +894,14 @@ impl Panel for AssistantPanel {
}
}
fn pane(&self) -> Option<View<Pane>> {
Some(self.pane.clone())
}
fn remote_id() -> Option<proto::PanelId> {
Some(proto::PanelId::AssistantPanel)
}
fn icon(&self, cx: &WindowContext) -> Option<IconName> {
let settings = AssistantSettings::get_global(cx);
if !settings.enabled || !settings.button {
@ -924,6 +948,7 @@ pub struct ContextEditor {
editor: View<Editor>,
blocks: HashSet<BlockId>,
scroll_position: Option<ScrollPosition>,
remote_id: Option<workspace::ViewId>,
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
pending_slash_command_blocks: HashMap<Range<language::Anchor>, BlockId>,
_subscriptions: Vec<Subscription>,
@ -971,6 +996,7 @@ impl ContextEditor {
lsp_adapter_delegate,
blocks: Default::default(),
scroll_position: None,
remote_id: None,
fs,
workspace: workspace.downgrade(),
pending_slash_command_creases: HashMap::default(),
@ -1213,7 +1239,7 @@ impl ContextEditor {
});
}
ContextEvent::SummaryChanged => {
cx.emit(ContextEditorEvent::TabContentChanged);
cx.emit(EditorEvent::TitleChanged);
self.context.update(cx, |context, cx| {
context.save(None, self.fs.clone(), cx);
});
@ -1472,9 +1498,9 @@ impl ContextEditor {
EditorEvent::SelectionsChanged { .. } => {
self.scroll_position = self.cursor_scroll_position(cx);
}
EditorEvent::BufferEdited => cx.emit(ContextEditorEvent::Edited),
_ => {}
}
cx.emit(event.clone());
}
fn handle_editor_search_event(
@ -1935,7 +1961,7 @@ impl ContextEditor {
}
}
impl EventEmitter<ContextEditorEvent> for ContextEditor {}
impl EventEmitter<EditorEvent> for ContextEditor {}
impl EventEmitter<SearchEvent> for ContextEditor {}
impl Render for ContextEditor {
@ -1977,13 +2003,9 @@ impl FocusableView for ContextEditor {
}
impl Item for ContextEditor {
type Event = ContextEditorEvent;
type Event = editor::EditorEvent;
fn tab_content(
&self,
params: workspace::item::TabContentParams,
cx: &WindowContext,
) -> AnyElement {
fn tab_content(&self, params: item::TabContentParams, cx: &WindowContext) -> AnyElement {
let color = if params.selected {
Color::Default
} else {
@ -1997,15 +2019,16 @@ impl Item for ContextEditor {
.into_any_element()
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
match event {
ContextEditorEvent::Edited => {
f(workspace::item::ItemEvent::Edit);
f(workspace::item::ItemEvent::UpdateBreadcrumbs);
EditorEvent::Edited { .. } => {
f(item::ItemEvent::Edit);
f(item::ItemEvent::UpdateBreadcrumbs);
}
ContextEditorEvent::TabContentChanged => {
f(workspace::item::ItemEvent::UpdateTab);
EditorEvent::TitleChanged => {
f(item::ItemEvent::UpdateTab);
}
_ => {}
}
}
@ -2021,7 +2044,7 @@ impl Item for ContextEditor {
&self,
theme: &theme::Theme,
cx: &AppContext,
) -> Option<Vec<workspace::item::BreadcrumbText>> {
) -> Option<Vec<item::BreadcrumbText>> {
let editor = self.editor.read(cx);
let cursor = editor.selections.newest_anchor().head();
let multibuffer = &editor.buffer().read(cx);
@ -2133,6 +2156,127 @@ impl SearchableItem for ContextEditor {
}
}
impl FollowableItem for ContextEditor {
fn remote_id(&self) -> Option<workspace::ViewId> {
self.remote_id
}
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
let context = self.context.read(cx);
Some(proto::view::Variant::ContextEditor(
proto::view::ContextEditor {
context_id: context.id().to_proto(),
editor: if let Some(proto::view::Variant::Editor(proto)) =
self.editor.read(cx).to_state_proto(cx)
{
Some(proto)
} else {
None
},
},
))
}
fn from_state_proto(
workspace: View<Workspace>,
id: workspace::ViewId,
state: &mut Option<proto::view::Variant>,
cx: &mut WindowContext,
) -> Option<Task<Result<View<Self>>>> {
let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
return None;
};
let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
unreachable!()
};
let context_id = ContextId::from_proto(state.context_id);
let editor_state = state.editor?;
let (project, panel) = workspace.update(cx, |workspace, cx| {
Some((
workspace.project().clone(),
workspace.panel::<AssistantPanel>(cx)?,
))
})?;
let context_editor =
panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
Some(cx.spawn(|mut cx| async move {
let context_editor = context_editor.await?;
context_editor
.update(&mut cx, |context_editor, cx| {
context_editor.remote_id = Some(id);
context_editor.editor.update(cx, |editor, cx| {
editor.apply_update_proto(
&project,
proto::update_view::Variant::Editor(proto::update_view::Editor {
selections: editor_state.selections,
pending_selection: editor_state.pending_selection,
scroll_top_anchor: editor_state.scroll_top_anchor,
scroll_x: editor_state.scroll_y,
scroll_y: editor_state.scroll_y,
..Default::default()
}),
cx,
)
})
})?
.await?;
Ok(context_editor)
}))
}
fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
Editor::to_follow_event(event)
}
fn add_event_to_update_proto(
&self,
event: &Self::Event,
update: &mut Option<proto::update_view::Variant>,
cx: &WindowContext,
) -> bool {
self.editor
.read(cx)
.add_event_to_update_proto(event, update, cx)
}
fn apply_update_proto(
&mut self,
project: &Model<Project>,
message: proto::update_view::Variant,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.editor.update(cx, |editor, cx| {
editor.apply_update_proto(project, message, cx)
})
}
fn is_project_item(&self, _cx: &WindowContext) -> bool {
true
}
fn set_leader_peer_id(
&mut self,
leader_peer_id: Option<proto::PeerId>,
cx: &mut ViewContext<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.set_leader_peer_id(leader_peer_id, cx)
})
}
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
if existing.context.read(cx).id() == self.context.read(cx).id() {
Some(item::Dedup::KeepExisting)
} else {
None
}
}
}
pub struct ContextEditorToolbarItem {
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
@ -2369,11 +2513,7 @@ impl EventEmitter<()> for ContextHistory {}
impl Item for ContextHistory {
type Event = ();
fn tab_content(
&self,
params: workspace::item::TabContentParams,
_: &WindowContext,
) -> AnyElement {
fn tab_content(&self, params: item::TabContentParams, _: &WindowContext) -> AnyElement {
let color = if params.selected {
Color::Default
} else {

View file

@ -135,7 +135,7 @@ async fn test_basic_following(
assert_eq!(editor.selections.ranges(cx), vec![2..1]);
});
// When client B starts following client A, all visible view states are replicated to client B.
// When client B starts following client A, only the active view state is replicated to client B.
workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
cx_c.executor().run_until_parked();
@ -156,7 +156,7 @@ async fn test_basic_following(
);
assert_eq!(
editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![3..2]
vec![3..3]
);
executor.run_until_parked();
@ -194,7 +194,7 @@ async fn test_basic_following(
// Client C unfollows client A.
workspace_c.update(cx_c, |workspace, cx| {
workspace.unfollow(&workspace.active_pane().clone(), cx);
workspace.unfollow(peer_id_a, cx).unwrap();
});
// All clients see that clients B is following client A.
@ -398,7 +398,7 @@ async fn test_basic_following(
// After unfollowing, client B stops receiving updates from client A.
workspace_b.update(cx_b, |workspace, cx| {
workspace.unfollow(&workspace.active_pane().clone(), cx)
workspace.unfollow(peer_id_a, cx).unwrap()
});
workspace_a.update(cx_a, |workspace, cx| {
workspace.activate_item(&editor_a2, cx)

View file

@ -22,10 +22,9 @@ use std::{
};
use ui::{prelude::*, Label};
use util::ResultExt;
use workspace::notifications::NotificationId;
use workspace::{item::Dedup, notifications::NotificationId};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, TabContentParams},
register_followable_item,
searchable::SearchableItemHandle,
ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
};
@ -33,7 +32,7 @@ use workspace::{
actions!(collab, [CopyLink]);
pub fn init(cx: &mut AppContext) {
register_followable_item::<ChannelView>(cx)
workspace::FollowableViewRegistry::register::<ChannelView>(cx)
}
pub struct ChannelView {
@ -83,6 +82,56 @@ impl ChannelView {
pane: View<Pane>,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
let channel_view = Self::load(channel_id, workspace, cx);
cx.spawn(|mut cx| async move {
let channel_view = channel_view.await?;
pane.update(&mut cx, |pane, cx| {
let buffer_id = channel_view.read(cx).channel_buffer.read(cx).remote_id(cx);
let existing_view = pane
.items_of_type::<Self>()
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
// If this channel buffer is already open in this pane, just return it.
if let Some(existing_view) = existing_view.clone() {
if existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer
{
if let Some(link_position) = link_position {
existing_view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
return existing_view;
}
}
// If the pane contained a disconnected view for this channel buffer,
// replace that.
if let Some(existing_item) = existing_view {
if let Some(ix) = pane.index_for_item(&existing_item) {
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
.detach();
pane.add_item(Box::new(channel_view.clone()), true, true, Some(ix), cx);
}
}
if let Some(link_position) = link_position {
channel_view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
channel_view
})
})
}
pub fn load(
channel_id: ChannelId,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
let weak_workspace = workspace.downgrade();
let workspace = workspace.read(cx);
@ -107,49 +156,11 @@ impl ChannelView {
})
})?;
pane.update(&mut cx, |pane, cx| {
let buffer_id = channel_buffer.read(cx).remote_id(cx);
let existing_view = pane
.items_of_type::<Self>()
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
// If this channel buffer is already open in this pane, just return it.
if let Some(existing_view) = existing_view.clone() {
if existing_view.read(cx).channel_buffer == channel_buffer {
if let Some(link_position) = link_position {
existing_view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
return existing_view;
}
}
let view = cx.new_view(|cx| {
cx.new_view(|cx| {
let mut this =
Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
this.acknowledge_buffer_version(cx);
this
});
// If the pane contained a disconnected view for this channel buffer,
// replace that.
if let Some(existing_item) = existing_view {
if let Some(ix) = pane.index_for_item(&existing_item) {
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
.detach();
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
}
}
if let Some(link_position) = link_position {
view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
view
})
})
}
@ -478,7 +489,6 @@ impl FollowableItem for ChannelView {
}
fn from_state_proto(
pane: View<workspace::Pane>,
workspace: View<workspace::Workspace>,
remote_id: workspace::ViewId,
state: &mut Option<proto::view::Variant>,
@ -491,8 +501,7 @@ impl FollowableItem for ChannelView {
unreachable!()
};
let open =
ChannelView::open_in_pane(ChannelId(state.channel_id), None, pane, workspace, cx);
let open = ChannelView::load(ChannelId(state.channel_id), workspace, cx);
Some(cx.spawn(|mut cx| async move {
let this = open.await?;
@ -563,6 +572,19 @@ impl FollowableItem for ChannelView {
fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
Editor::to_follow_event(event)
}
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
let existing = existing.channel_buffer.read(cx);
if self.channel_buffer.read(cx).channel_id == existing.channel_id {
if existing.is_connected() {
Some(Dedup::KeepExisting)
} else {
Some(Dedup::ReplaceExisting)
}
} else {
None
}
}
}
struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);

View file

@ -271,7 +271,7 @@ pub fn init(cx: &mut AppContext) {
init_settings(cx);
workspace::register_project_item::<Editor>(cx);
workspace::register_followable_item::<Editor>(cx);
workspace::FollowableViewRegistry::register::<Editor>(cx);
workspace::register_deserializable_item::<Editor>(cx);
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {

View file

@ -8812,7 +8812,6 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
let follower_1 = cx
.update_window(*workspace.deref(), |_, cx| {
Editor::from_state_proto(
pane.clone(),
workspace.root_view(cx).unwrap(),
ViewId {
creator: Default::default(),
@ -8904,7 +8903,6 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
let follower_2 = cx
.update_window(*workspace.deref(), |_, cx| {
Editor::from_state_proto(
pane.clone(),
workspace.root_view(cx).unwrap().clone(),
ViewId {
creator: Default::default(),

View file

@ -19,7 +19,7 @@ use multi_buffer::AnchorRangeExt;
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view, PeerId};
use settings::Settings;
use workspace::item::{ItemSettings, TabContentParams};
use workspace::item::{Dedup, ItemSettings, TabContentParams};
use std::{
any::TypeId,
@ -34,7 +34,7 @@ use text::{BufferId, Selection};
use theme::{Theme, ThemeSettings};
use ui::{h_flex, prelude::*, Label};
use util::{paths::PathExt, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowEvent, FollowableItemHandle};
use workspace::item::{BreadcrumbText, FollowEvent};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
@ -49,7 +49,6 @@ impl FollowableItem for Editor {
}
fn from_state_proto(
pane: View<workspace::Pane>,
workspace: View<Workspace>,
remote_id: ViewId,
state: &mut Option<proto::view::Variant>,
@ -63,7 +62,6 @@ impl FollowableItem for Editor {
unreachable!()
};
let client = project.read(cx).client();
let replica_id = project.read(cx).replica_id();
let buffer_ids = state
.excerpts
@ -77,34 +75,18 @@ impl FollowableItem for Editor {
.collect::<Result<Vec<_>>>()
});
let pane = pane.downgrade();
Some(cx.spawn(|mut cx| async move {
let mut buffers = futures::future::try_join_all(buffers?)
.await
.debug_assert_ok("leaders don't share views for unshared buffers")?;
let editor = pane.update(&mut cx, |pane, cx| {
let mut editors = pane.items_of_type::<Self>();
editors.find(|editor| {
let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
let singleton_buffer_matches = state.singleton
&& buffers.first()
== editor.read(cx).buffer.read(cx).as_singleton().as_ref();
ids_match || singleton_buffer_matches
})
})?;
let editor = if let Some(editor) = editor {
editor
} else {
pane.update(&mut cx, |_, cx| {
let editor = cx.update(|cx| {
let multibuffer = cx.new_model(|cx| {
let mut multibuffer;
if state.singleton && buffers.len() == 1 {
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
} else {
multibuffer =
MultiBuffer::new(replica_id, project.read(cx).capability());
multibuffer = MultiBuffer::new(replica_id, project.read(cx).capability());
let mut excerpts = state.excerpts.into_iter().peekable();
while let Some(excerpt) = excerpts.peek() {
let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
@ -140,8 +122,7 @@ impl FollowableItem for Editor {
editor.remote_id = Some(remote_id);
editor
})
})?
};
})?;
update_editor_from_message(
editor.downgrade(),
@ -327,6 +308,16 @@ impl FollowableItem for Editor {
fn is_project_item(&self, _cx: &WindowContext) -> bool {
true
}
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
let self_singleton = self.buffer.read(cx).as_singleton()?;
let other_singleton = existing.buffer.read(cx).as_singleton()?;
if self_singleton == other_singleton {
Some(Dedup::KeepExisting)
} else {
None
}
}
}
async fn update_editor_from_message(

View file

@ -291,6 +291,10 @@ pub trait BorrowAppContext {
fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where
G: Global;
/// Updates the global state of the given type, creating a default if it didn't exist before.
fn update_default_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where
G: Global + Default;
}
impl<C> BorrowAppContext for C
@ -310,6 +314,14 @@ where
self.borrow_mut().end_global_lease(global);
result
}
fn update_default_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where
G: Global + Default,
{
self.borrow_mut().default_global::<G>();
self.update_global(f)
}
}
/// A flatten equivalent for anyhow `Result`s.

View file

@ -1629,7 +1629,7 @@ message Follow {
message FollowResponse {
View active_view = 3;
// TODO: after 0.124.0 is retired, remove these.
// TODO: Remove after version 0.145.x stabilizes.
optional ViewId active_view_id = 1;
repeated View views = 2;
}
@ -1640,7 +1640,7 @@ message UpdateFollowers {
reserved 3;
oneof variant {
View create_view = 5;
// TODO: after 0.124.0 is retired, remove these.
// TODO: Remove after version 0.145.x stabilizes.
UpdateActiveView update_active_view = 4;
UpdateView update_view = 6;
}
@ -1673,6 +1673,10 @@ message UpdateActiveView {
View view = 3;
}
enum PanelId {
AssistantPanel = 0;
}
message UpdateView {
ViewId id = 1;
optional PeerId leader_id = 2;
@ -1695,10 +1699,12 @@ message UpdateView {
message View {
ViewId id = 1;
optional PeerId leader_id = 2;
optional PanelId panel_id = 6;
oneof variant {
Editor editor = 3;
ChannelView channel_view = 4;
ContextEditor context_editor = 5;
}
message Editor {
@ -1716,6 +1722,11 @@ message View {
uint64 channel_id = 1;
Editor editor = 2;
}
message ContextEditor {
string context_id = 1;
Editor editor = 2;
}
}
message Collaborator {

View file

@ -168,7 +168,11 @@ impl TitleBar {
cx.listener(move |this, _, cx| {
this.workspace
.update(cx, |workspace, cx| {
if is_following {
workspace.unfollow(peer_id, cx);
} else {
workspace.follow(peer_id, cx);
}
})
.ok();
})

View file

@ -1,6 +1,7 @@
use crate::persistence::model::DockData;
use crate::{status_bar::StatusItemView, Workspace};
use crate::{DraggedDock, Event};
use crate::{DraggedDock, Event, Pane};
use client::proto;
use gpui::{
deferred, div, px, Action, AnchorCorner, AnyView, AppContext, Axis, Entity, EntityId,
EventEmitter, FocusHandle, FocusableView, IntoElement, KeyContext, MouseButton, MouseDownEvent,
@ -23,6 +24,8 @@ pub enum PanelEvent {
Close,
}
pub use proto::PanelId;
pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
fn persistent_name() -> &'static str;
fn position(&self, cx: &WindowContext) -> DockPosition;
@ -44,6 +47,12 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
}
fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
fn pane(&self) -> Option<View<Pane>> {
None
}
fn remote_id() -> Option<proto::PanelId> {
None
}
}
pub trait PanelHandle: Send + Sync {
@ -55,6 +64,8 @@ pub trait PanelHandle: Send + Sync {
fn is_zoomed(&self, cx: &WindowContext) -> bool;
fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
fn set_active(&self, active: bool, cx: &mut WindowContext);
fn remote_id(&self) -> Option<proto::PanelId>;
fn pane(&self, cx: &WindowContext) -> Option<View<Pane>>;
fn size(&self, cx: &WindowContext) -> Pixels;
fn set_size(&self, size: Option<Pixels>, cx: &mut WindowContext);
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName>;
@ -101,6 +112,14 @@ where
self.update(cx, |this, cx| this.set_active(active, cx))
}
fn pane(&self, cx: &WindowContext) -> Option<View<Pane>> {
self.read(cx).pane()
}
fn remote_id(&self) -> Option<PanelId> {
T::remote_id()
}
fn size(&self, cx: &WindowContext) -> Pixels {
self.read(cx).size(cx)
}
@ -296,6 +315,12 @@ impl Dock {
.position(|entry| entry.panel.persistent_name() == ui_name)
}
pub fn panel_index_for_proto_id(&self, panel_id: PanelId) -> Option<usize> {
self.panel_entries
.iter()
.position(|entry| entry.panel.remote_id() == Some(panel_id))
}
pub fn active_panel_index(&self) -> usize {
self.active_panel_index
}

View file

@ -3,7 +3,7 @@ use crate::{
persistence::model::ItemId,
searchable::SearchableItemHandle,
workspace_settings::{AutosaveSetting, WorkspaceSettings},
DelayedDebouncedEditAction, FollowableItemBuilders, ItemNavHistory, ToolbarItemLocation,
DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, ToolbarItemLocation,
ViewId, Workspace, WorkspaceId,
};
use anyhow::Result;
@ -472,22 +472,6 @@ impl<T: Item> ItemHandle for View<T> {
this.added_to_workspace(workspace, cx);
});
if let Some(followed_item) = self.to_followable_item_handle(cx) {
if let Some(message) = followed_item.to_state_proto(cx) {
workspace.update_followers(
followed_item.is_project_item(cx),
proto::update_followers::Variant::CreateView(proto::View {
id: followed_item
.remote_id(&workspace.client(), cx)
.map(|id| id.to_proto()),
variant: Some(message),
leader_id: workspace.leader_for_pane(&pane),
}),
cx,
);
}
}
if workspace
.panes_by_item
.insert(self.item_id(), pane.downgrade())
@ -548,11 +532,11 @@ impl<T: Item> ItemHandle for View<T> {
if let Some(item) = item.to_followable_item_handle(cx) {
let leader_id = workspace.leader_for_pane(&pane);
let follow_event = item.to_follow_event(event);
if leader_id.is_some()
&& matches!(follow_event, Some(FollowEvent::Unfollow))
{
workspace.unfollow(&pane, cx);
if let Some(leader_id) = leader_id {
if let Some(FollowEvent::Unfollow) = item.to_follow_event(event) {
workspace.unfollow(leader_id, cx);
}
}
if item.focus_handle(cx).contains_focused(cx) {
@ -682,9 +666,7 @@ impl<T: Item> ItemHandle for View<T> {
}
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
let builders = cx.try_global::<FollowableItemBuilders>()?;
let item = self.to_any();
Some(builders.get(&item.entity_type())?.1(&item))
FollowableViewRegistry::to_followable_view(self.clone(), cx)
}
fn on_release(
@ -769,11 +751,15 @@ pub enum FollowEvent {
Unfollow,
}
pub enum Dedup {
KeepExisting,
ReplaceExisting,
}
pub trait FollowableItem: Item {
fn remote_id(&self) -> Option<ViewId>;
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
fn from_state_proto(
pane: View<Pane>,
project: View<Workspace>,
id: ViewId,
state: &mut Option<proto::view::Variant>,
@ -794,6 +780,7 @@ pub trait FollowableItem: Item {
) -> Task<Result<()>>;
fn is_project_item(&self, cx: &WindowContext) -> bool;
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup>;
}
pub trait FollowableItemHandle: ItemHandle {
@ -815,6 +802,7 @@ pub trait FollowableItemHandle: ItemHandle {
cx: &mut WindowContext,
) -> Task<Result<()>>;
fn is_project_item(&self, cx: &WindowContext) -> bool;
fn dedup(&self, existing: &dyn FollowableItemHandle, cx: &WindowContext) -> Option<Dedup>;
}
impl<T: FollowableItem> FollowableItemHandle for View<T> {
@ -868,6 +856,11 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
fn is_project_item(&self, cx: &WindowContext) -> bool {
self.read(cx).is_project_item(cx)
}
fn dedup(&self, existing: &dyn FollowableItemHandle, cx: &WindowContext) -> Option<Dedup> {
let existing = existing.to_any().downcast::<T>().ok()?;
self.read(cx).dedup(existing.read(cx), cx)
}
}
pub trait WeakFollowableItemHandle: Send + Sync {

View file

@ -1,6 +1,7 @@
use crate::{pane_group::element::pane_axis, AppState, FollowerState, Pane, Workspace};
use anyhow::{anyhow, Result};
use call::{ActiveCall, ParticipantLocation};
use client::proto::PeerId;
use collections::HashMap;
use gpui::{
point, size, AnyView, AnyWeakView, Axis, Bounds, IntoElement, Model, MouseButton, Pixels,
@ -95,7 +96,7 @@ impl PaneGroup {
pub(crate) fn render(
&self,
project: &Model<Project>,
follower_states: &HashMap<View<Pane>, FollowerState>,
follower_states: &HashMap<PeerId, FollowerState>,
active_call: Option<&Model<ActiveCall>>,
active_pane: &View<Pane>,
zoomed: Option<&AnyWeakView>,
@ -168,7 +169,7 @@ impl Member {
&self,
project: &Model<Project>,
basis: usize,
follower_states: &HashMap<View<Pane>, FollowerState>,
follower_states: &HashMap<PeerId, FollowerState>,
active_call: Option<&Model<ActiveCall>>,
active_pane: &View<Pane>,
zoomed: Option<&AnyWeakView>,
@ -181,19 +182,29 @@ impl Member {
return div().into_any();
}
let follower_state = follower_states.get(pane);
let leader = follower_state.and_then(|state| {
let room = active_call?.read(cx).room()?.read(cx);
room.remote_participant_for_peer_id(state.leader_id)
let follower_state = follower_states.iter().find_map(|(leader_id, state)| {
if state.center_pane == *pane {
Some((*leader_id, state))
} else {
None
}
});
let is_in_unshared_view = follower_state.map_or(false, |state| {
let leader = follower_state.as_ref().and_then(|(leader_id, _)| {
let room = active_call?.read(cx).room()?.read(cx);
room.remote_participant_for_peer_id(*leader_id)
});
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_border = None;
let mut leader_status_box = None;
let mut leader_join_data = None;
@ -203,7 +214,11 @@ impl Member {
.players()
.color_for_participant(leader.participant_index.0)
.cursor;
if is_in_panel {
leader_color.fade_out(0.75);
} else {
leader_color.fade_out(0.3);
}
leader_border = Some(leader_color);
leader_status_box = match leader.location {
@ -483,7 +498,7 @@ impl PaneAxis {
&self,
project: &Model<Project>,
basis: usize,
follower_states: &HashMap<View<Pane>, FollowerState>,
follower_states: &HashMap<PeerId, FollowerState>,
active_call: Option<&Model<ActiveCall>>,
active_pane: &View<Pane>,
zoomed: Option<&AnyWeakView>,

File diff suppressed because it is too large Load diff