From decdd3b6ac9f9971897abfa4cade88da8bb0173e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 15 Jul 2024 11:36:27 +0200 Subject: [PATCH] Introduce following for assistant panel (#14479) Release Notes: - Added support for following into the assistant panel. --------- Co-authored-by: Max Co-authored-by: Max Brunsfeld Co-authored-by: Nathan --- crates/assistant/src/assistant_panel.rs | 214 +++++- crates/collab/src/tests/following_tests.rs | 8 +- crates/collab_ui/src/channel_view.rs | 120 ++-- crates/editor/src/editor.rs | 2 +- crates/editor/src/editor_tests.rs | 2 - crates/editor/src/items.rs | 115 ++-- crates/gpui/src/gpui.rs | 12 + crates/proto/proto/zed.proto | 15 +- crates/title_bar/src/collab.rs | 6 +- crates/workspace/src/dock.rs | 27 +- crates/workspace/src/item.rs | 45 +- crates/workspace/src/pane_group.rs | 35 +- crates/workspace/src/workspace.rs | 759 +++++++++++---------- 13 files changed, 819 insertions(+), 541 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 85021081d4..48a7648065 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -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::(cx); cx.observe_new_views( |workspace: &mut Workspace, _cx: &mut ViewContext| { workspace @@ -374,7 +376,7 @@ impl AssistantPanel { fn handle_pane_event( &mut self, - _pane: View, + pane: View, event: &pane::Event, cx: &mut ViewContext, ) { @@ -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, - event: &ContextEditorEvent, + event: &EditorEvent, cx: &mut ViewContext, ) { 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, - ) -> Task> { + ) -> Task>> { let existing_context = self.pane.read(cx).items().find_map(|item| { item.downcast::() .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> { + Some(self.pane.clone()) + } + + fn remote_id() -> Option { + Some(proto::PanelId::AssistantPanel) + } + fn icon(&self, cx: &WindowContext) -> Option { let settings = AssistantSettings::get_global(cx); if !settings.enabled || !settings.button { @@ -924,6 +948,7 @@ pub struct ContextEditor { editor: View, blocks: HashSet, scroll_position: Option, + remote_id: Option, pending_slash_command_creases: HashMap, CreaseId>, pending_slash_command_blocks: HashMap, BlockId>, _subscriptions: Vec, @@ -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 for ContextEditor {} +impl EventEmitter for ContextEditor {} impl EventEmitter 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> { + ) -> Option> { 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 { + self.remote_id + } + + fn to_state_proto(&self, cx: &WindowContext) -> Option { + 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, + id: workspace::ViewId, + state: &mut Option, + cx: &mut WindowContext, + ) -> Option>>> { + 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::(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 { + Editor::to_follow_event(event) + } + + fn add_event_to_update_proto( + &self, + event: &Self::Event, + update: &mut Option, + cx: &WindowContext, + ) -> bool { + self.editor + .read(cx) + .add_event_to_update_proto(event, update, cx) + } + + fn apply_update_proto( + &mut self, + project: &Model, + message: proto::update_view::Variant, + cx: &mut ViewContext, + ) -> Task> { + 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, + cx: &mut ViewContext, + ) { + self.editor.update(cx, |editor, cx| { + editor.set_leader_peer_id(leader_peer_id, cx) + }) + } + + fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option { + if existing.context.read(cx).id() == self.context.read(cx).id() { + Some(item::Dedup::KeepExisting) + } else { + None + } + } +} + pub struct ContextEditorToolbarItem { fs: Arc, workspace: WeakView, @@ -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 { diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 5cf7e82b3f..c75786d951 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -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) diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 5e017821b6..dfd19264e5 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -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::(cx) + workspace::FollowableViewRegistry::register::(cx) } pub struct ChannelView { @@ -83,6 +82,56 @@ impl ChannelView { pane: View, workspace: View, cx: &mut WindowContext, + ) -> Task>> { + 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::() + .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, + cx: &mut WindowContext, ) -> Task>> { 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::() - .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| { - 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 + cx.new_view(|cx| { + let mut this = + Self::new(project, weak_workspace, channel_store, channel_buffer, cx); + this.acknowledge_buffer_version(cx); + this }) }) } @@ -478,7 +489,6 @@ impl FollowableItem for ChannelView { } fn from_state_proto( - pane: View, workspace: View, remote_id: workspace::ViewId, state: &mut Option, @@ -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 { Editor::to_follow_event(event) } + + fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option { + 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); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c9f25d8a4e..6e9e1e871a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -271,7 +271,7 @@ pub fn init(cx: &mut AppContext) { init_settings(cx); workspace::register_project_item::(cx); - workspace::register_followable_item::(cx); + workspace::FollowableViewRegistry::register::(cx); workspace::register_deserializable_item::(cx); cx.observe_new_views( |workspace: &mut Workspace, _cx: &mut ViewContext| { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index eb4612027e..e0b910f3b6 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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(), diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 5289923c04..57cf5c7560 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -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: View, remote_id: ViewId, state: &mut Option, @@ -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,72 +75,55 @@ impl FollowableItem for Editor { .collect::>>() }); - 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::(); - 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 = 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()); + let mut excerpts = state.excerpts.into_iter().peekable(); + while let Some(excerpt) = excerpts.peek() { + let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else { + continue; + }; + let buffer_excerpts = iter::from_fn(|| { + let excerpt = excerpts.peek()?; + (excerpt.buffer_id == u64::from(buffer_id)) + .then(|| excerpts.next().unwrap()) + }); + let buffer = + buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id); + if let Some(buffer) = buffer { + multibuffer.push_excerpts( + buffer.clone(), + buffer_excerpts.filter_map(deserialize_excerpt_range), + cx, + ); + } + } + }; + + if let Some(title) = &state.title { + multibuffer = multibuffer.with_title(title.clone()) + } + + multibuffer + }); + + cx.new_view(|cx| { + let mut editor = + Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx); + editor.remote_id = Some(remote_id); + editor }) })?; - let editor = if let Some(editor) = editor { - editor - } else { - pane.update(&mut cx, |_, 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()); - let mut excerpts = state.excerpts.into_iter().peekable(); - while let Some(excerpt) = excerpts.peek() { - let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else { - continue; - }; - let buffer_excerpts = iter::from_fn(|| { - let excerpt = excerpts.peek()?; - (excerpt.buffer_id == u64::from(buffer_id)) - .then(|| excerpts.next().unwrap()) - }); - let buffer = - buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id); - if let Some(buffer) = buffer { - multibuffer.push_excerpts( - buffer.clone(), - buffer_excerpts.filter_map(deserialize_excerpt_range), - cx, - ); - } - } - }; - - if let Some(title) = &state.title { - multibuffer = multibuffer.with_title(title.clone()) - } - - multibuffer - }); - - cx.new_view(|cx| { - let mut editor = - Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx); - editor.remote_id = Some(remote_id); - editor - }) - })? - }; - update_editor_from_message( editor.downgrade(), project, @@ -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 { + 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( diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index ac97382473..29ffed710d 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -291,6 +291,10 @@ pub trait BorrowAppContext { fn update_global(&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(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R + where + G: Global + Default; } impl BorrowAppContext for C @@ -310,6 +314,14 @@ where self.borrow_mut().end_global_lease(global); result } + + fn update_default_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R + where + G: Global + Default, + { + self.borrow_mut().default_global::(); + self.update_global(f) + } } /// A flatten equivalent for anyhow `Result`s. diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index ebf98983f9..f429c244c7 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -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 { diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index cc5b37fd64..8df25a2cd5 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -168,7 +168,11 @@ impl TitleBar { cx.listener(move |this, _, cx| { this.workspace .update(cx, |workspace, cx| { - workspace.follow(peer_id, cx); + if is_following { + workspace.unfollow(peer_id, cx); + } else { + workspace.follow(peer_id, cx); + } }) .ok(); }) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index f9fb7c8801..7337f5e56d 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -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 { fn persistent_name() -> &'static str; fn position(&self, cx: &WindowContext) -> DockPosition; @@ -44,6 +47,12 @@ pub trait Panel: FocusableView + EventEmitter { } fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) {} fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} + fn pane(&self) -> Option> { + None + } + fn remote_id() -> Option { + 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; + fn pane(&self, cx: &WindowContext) -> Option>; fn size(&self, cx: &WindowContext) -> Pixels; fn set_size(&self, size: Option, cx: &mut WindowContext); fn icon(&self, cx: &WindowContext) -> Option; @@ -101,6 +112,14 @@ where self.update(cx, |this, cx| this.set_active(active, cx)) } + fn pane(&self, cx: &WindowContext) -> Option> { + self.read(cx).pane() + } + + fn remote_id(&self) -> Option { + 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 { + self.panel_entries + .iter() + .position(|entry| entry.panel.remote_id() == Some(panel_id)) + } + pub fn active_panel_index(&self) -> usize { self.active_panel_index } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 48b8fb1d1e..5da2c5a5ef 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -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 ItemHandle for View { 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 ItemHandle for View { 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 ItemHandle for View { } fn to_followable_item_handle(&self, cx: &AppContext) -> Option> { - let builders = cx.try_global::()?; - 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; fn to_state_proto(&self, cx: &WindowContext) -> Option; fn from_state_proto( - pane: View, project: View, id: ViewId, state: &mut Option, @@ -794,6 +780,7 @@ pub trait FollowableItem: Item { ) -> Task>; fn is_project_item(&self, cx: &WindowContext) -> bool; fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext); + fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option; } pub trait FollowableItemHandle: ItemHandle { @@ -815,6 +802,7 @@ pub trait FollowableItemHandle: ItemHandle { cx: &mut WindowContext, ) -> Task>; fn is_project_item(&self, cx: &WindowContext) -> bool; + fn dedup(&self, existing: &dyn FollowableItemHandle, cx: &WindowContext) -> Option; } impl FollowableItemHandle for View { @@ -868,6 +856,11 @@ impl FollowableItemHandle for View { fn is_project_item(&self, cx: &WindowContext) -> bool { self.read(cx).is_project_item(cx) } + + fn dedup(&self, existing: &dyn FollowableItemHandle, cx: &WindowContext) -> Option { + let existing = existing.to_any().downcast::().ok()?; + self.read(cx).dedup(existing.read(cx), cx) + } } pub trait WeakFollowableItemHandle: Send + Sync { diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 439507e56e..adc187fbd1 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -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, - follower_states: &HashMap, FollowerState>, + follower_states: &HashMap, active_call: Option<&Model>, active_pane: &View, zoomed: Option<&AnyWeakView>, @@ -168,7 +169,7 @@ impl Member { &self, project: &Model, basis: usize, - follower_states: &HashMap, FollowerState>, + follower_states: &HashMap, active_call: Option<&Model>, active_pane: &View, 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; - leader_color.fade_out(0.3); + 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, basis: usize, - follower_states: &HashMap, FollowerState>, + follower_states: &HashMap, active_call: Option<&Model>, active_pane: &View, zoomed: Option<&AnyWeakView>, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 636198bdac..573a08a0cd 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -15,7 +15,7 @@ mod workspace_settings; use anyhow::{anyhow, Context as _, Result}; use call::{call_settings::CallSettings, ActiveCall}; use client::{ - proto::{self, ErrorCode, PeerId}, + proto::{self, ErrorCode, PanelId, PeerId}, ChannelId, Client, DevServerProjectId, ErrorExt, ProjectId, Status, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; @@ -81,9 +81,9 @@ use theme::{ActiveTheme, SystemAppearance, ThemeSettings}; pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; use ui::{ - div, h_flex, px, Context as _, Div, FluentBuilder, InteractiveElement as _, IntoElement, - ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _, - WindowContext, + div, h_flex, px, BorrowAppContext, Context as _, Div, FluentBuilder, InteractiveElement as _, + IntoElement, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, + VisualContext as _, WindowContext, }; use util::{maybe, ResultExt}; use uuid::Uuid; @@ -354,41 +354,59 @@ pub fn register_project_item(cx: &mut AppContext) { }); } -type FollowableItemBuilder = fn( - View, - View, - ViewId, - &mut Option, - &mut WindowContext, -) -> Option>>>; +#[derive(Default)] +pub struct FollowableViewRegistry(HashMap); -#[derive(Default, Deref, DerefMut)] -struct FollowableItemBuilders( - HashMap< - TypeId, - ( - FollowableItemBuilder, - fn(&AnyView) -> Box, - ), - >, -); +struct FollowableViewDescriptor { + from_state_proto: fn( + View, + ViewId, + &mut Option, + &mut WindowContext, + ) -> Option>>>, + to_followable_view: fn(&AnyView) -> Box, +} -impl Global for FollowableItemBuilders {} +impl Global for FollowableViewRegistry {} -pub fn register_followable_item(cx: &mut AppContext) { - let builders = cx.default_global::(); - builders.insert( - TypeId::of::(), - ( - |pane, workspace, id, state, cx| { - I::from_state_proto(pane, workspace, id, state, cx).map(|task| { - cx.foreground_executor() - .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) - }) +impl FollowableViewRegistry { + pub fn register(cx: &mut AppContext) { + cx.default_global::().0.insert( + TypeId::of::(), + FollowableViewDescriptor { + from_state_proto: |workspace, id, state, cx| { + I::from_state_proto(workspace, id, state, cx).map(|task| { + cx.foreground_executor() + .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) + }) + }, + to_followable_view: |view| Box::new(view.clone().downcast::().unwrap()), }, - |this| Box::new(this.clone().downcast::().unwrap()), - ), - ); + ); + } + + pub fn from_state_proto( + workspace: View, + view_id: ViewId, + mut state: Option, + cx: &mut WindowContext, + ) -> Option>>> { + cx.update_default_global(|this: &mut Self, cx| { + this.0.values().find_map(|descriptor| { + (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, cx) + }) + }) + } + + pub fn to_followable_view( + view: impl Into, + cx: &AppContext, + ) -> Option> { + let this = cx.try_global::()?; + let view = view.into(); + let descriptor = this.0.get(&view.entity_type())?; + Some((descriptor.to_followable_view)(&view)) + } } #[derive(Default, Deref, DerefMut)] @@ -593,7 +611,7 @@ pub struct Workspace { titlebar_item: Option, notifications: Vec<(NotificationId, Box)>, project: Model, - follower_states: HashMap, FollowerState>, + follower_states: HashMap, last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, active_call: Option<(Model, Vec)>, @@ -622,11 +640,16 @@ pub struct ViewId { pub id: u64, } -#[derive(Default)] struct FollowerState { - leader_id: PeerId, + center_pane: View, + dock_pane: Option>, active_view_id: Option, - items_by_leader_view_id: HashMap>, + items_by_leader_view_id: HashMap, +} + +struct FollowerView { + view: Box, + location: Option, } impl Workspace { @@ -657,10 +680,10 @@ impl Workspace { project::Event::DisconnectedFromHost => { this.update_window_edited(cx); - let panes_to_unfollow: Vec> = - this.follower_states.keys().map(|k| k.clone()).collect(); - for pane in panes_to_unfollow { - this.unfollow(&pane, cx); + let leaders_to_unfollow = + this.follower_states.keys().copied().collect::>(); + for leader_id in leaders_to_unfollow { + this.unfollow(leader_id, cx); } } @@ -1056,7 +1079,11 @@ impl Workspace { self.window_edited } - pub fn add_panel(&mut self, panel: View, cx: &mut WindowContext) { + pub fn add_panel(&mut self, panel: View, cx: &mut ViewContext) { + let focus_handle = panel.focus_handle(cx); + cx.on_focus_in(&focus_handle, Self::handle_panel_focused) + .detach(); + let dock = match panel.position(cx) { DockPosition::Left => &self.left_dock, DockPosition::Bottom => &self.bottom_dock, @@ -1975,6 +2002,31 @@ impl Workspace { }); } + pub fn activate_panel_for_proto_id( + &mut self, + panel_id: PanelId, + cx: &mut ViewContext, + ) -> Option> { + let mut panel = None; + for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { + if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) { + panel = dock.update(cx, |dock, cx| { + dock.activate_panel(panel_index, cx); + dock.set_open(true, cx); + dock.active_panel().cloned() + }); + break; + } + } + + if panel.is_some() { + cx.notify(); + self.serialize_workspace(cx); + } + + panel + } + /// Focus or unfocus the given panel type, depending on the given callback. fn focus_or_unfocus_panel( &mut self, @@ -2032,13 +2084,9 @@ impl Workspace { } pub fn panel(&self, cx: &WindowContext) -> Option> { - for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { - let dock = dock.read(cx); - if let Some(panel) = dock.panel::() { - return Some(panel); - } - } - None + [&self.left_dock, &self.bottom_dock, &self.right_dock] + .iter() + .find_map(|dock| dock.read(cx).panel::()) } fn dismiss_zoomed_items_to_reveal( @@ -2557,6 +2605,10 @@ impl Workspace { cx.notify(); } + fn handle_panel_focused(&mut self, cx: &mut ViewContext) { + self.update_active_view_for_followers(cx); + } + fn handle_pane_event( &mut self, pane: View, @@ -2577,7 +2629,7 @@ impl Workspace { pane.track_alternate_file_items(); }); if *local { - self.unfollow(&pane, cx); + self.unfollow_in_pane(&pane, cx); } if &pane == self.active_pane() { self.active_item_path_changed(cx); @@ -2626,6 +2678,16 @@ impl Workspace { self.serialize_workspace(cx); } + pub fn unfollow_in_pane( + &mut self, + pane: &View, + cx: &mut ViewContext, + ) -> Option { + let leader_id = self.leader_for_pane(pane)?; + self.unfollow(leader_id, cx); + Some(leader_id) + } + pub fn split_pane( &mut self, pane_to_split: View, @@ -2740,7 +2802,7 @@ impl Workspace { fn remove_pane(&mut self, pane: View, cx: &mut ViewContext) { if self.center.remove(&pane).unwrap() { self.force_remove_pane(&pane, cx); - self.unfollow(&pane, cx); + self.unfollow_in_pane(&pane, cx); self.last_leaders_by_pane.remove(&pane.downgrade()); for removed_item in pane.read(cx).items() { self.panes_by_item.remove(&removed_item.item_id()); @@ -2774,10 +2836,10 @@ impl Workspace { } fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { - self.follower_states.retain(|_, state| { - if state.leader_id == peer_id { + self.follower_states.retain(|leader_id, state| { + if *leader_id == peer_id { for item in state.items_by_leader_view_id.values() { - item.set_leader_peer_id(None, cx); + item.view.set_leader_peer_id(None, cx); } false } else { @@ -2796,11 +2858,13 @@ impl Workspace { self.last_leaders_by_pane .insert(pane.downgrade(), leader_id); - self.unfollow(&pane, cx); + self.unfollow(leader_id, cx); + self.unfollow_in_pane(&pane, cx); self.follower_states.insert( - pane.clone(), + leader_id, FollowerState { - leader_id, + center_pane: pane.clone(), + dock_pane: None, active_view_id: None, items_by_leader_view_id: Default::default(), }, @@ -2820,27 +2884,17 @@ impl Workspace { this.update(&mut cx, |this, _| { let state = this .follower_states - .get_mut(&pane) + .get_mut(&leader_id) .ok_or_else(|| anyhow!("following interrupted"))?; - state.active_view_id = if let Some(active_view_id) = response.active_view_id { - Some(ViewId::from_proto(active_view_id)?) - } else { - None - }; + 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, pane.clone(), &view, &mut cx) - .await?; + Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?; } - Self::add_views_from_leader( - this.clone(), - leader_id, - vec![pane], - response.views, - &mut cx, - ) - .await?; this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?; Ok(()) })) @@ -2877,7 +2931,7 @@ impl Workspace { else { return; }; - if Some(leader_id) == self.unfollow(&pane, cx) { + if self.unfollow_in_pane(&pane, cx) == Some(leader_id) { return; } if let Some(task) = self.start_following(leader_id, cx) { @@ -2916,11 +2970,9 @@ impl Workspace { } // if you're already following, find the right pane and focus it. - for (pane, state) in &self.follower_states { - if leader_id == state.leader_id { - cx.focus_view(pane); - return; - } + if let Some(follower_state) = self.follower_states.get(&leader_id) { + cx.focus_view(&follower_state.pane()); + return; } // Otherwise, follow. @@ -2929,38 +2981,29 @@ impl Workspace { } } - pub fn unfollow(&mut self, pane: &View, cx: &mut ViewContext) -> Option { - let state = self.follower_states.remove(pane)?; - let leader_id = state.leader_id; - for (_, item) in state.items_by_leader_view_id { - item.set_leader_peer_id(None, cx); - } - - if self - .follower_states - .values() - .all(|state| state.leader_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_id), - }) - .log_err(); - } - + pub fn unfollow(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { cx.notify(); - Some(leader_id) + let state = self.follower_states.remove(&leader_id)?; + for (_, item) in state.items_by_leader_view_id { + item.view.set_leader_peer_id(None, 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(); + + Some(()) } pub fn is_being_followed(&self, peer_id: PeerId) -> bool { - self.follower_states - .values() - .any(|state| state.leader_id == peer_id) + self.follower_states.contains_key(&peer_id) } fn active_item_path_changed(&mut self, cx: &mut ViewContext) { @@ -3058,7 +3101,8 @@ impl Workspace { follower_project_id: Option, cx: &mut ViewContext, ) -> Option { - let item = self.active_item(cx)?; + let (item, panel_id) = self.active_item_for_followers(cx); + let item = item?; let leader_id = self .pane_for(&*item) .and_then(|pane| self.leader_for_pane(&pane)); @@ -3078,6 +3122,7 @@ impl Workspace { id: Some(id.to_proto()), leader_id, variant: Some(variant), + panel_id: panel_id.map(|id| id as i32), }) } @@ -3086,52 +3131,14 @@ impl Workspace { follower_project_id: Option, cx: &mut ViewContext, ) -> proto::FollowResponse { - let client = &self.app_state.client; - let project_id = self.project.read(cx).remote_id(); - let active_view = self.active_view_for_follower(follower_project_id, cx); - let active_view_id = active_view.as_ref().and_then(|view| view.id.clone()); cx.notify(); - proto::FollowResponse { + // TODO: Remove after version 0.145.x stabilizes. + active_view_id: active_view.as_ref().and_then(|view| view.id.clone()), + views: active_view.iter().cloned().collect(), active_view, - // TODO: once v0.124.0 is retired we can stop sending these - active_view_id, - views: self - .panes() - .iter() - .flat_map(|pane| { - let leader_id = self.leader_for_pane(pane); - pane.read(cx).items().filter_map({ - let cx = &cx; - move |item| { - let item = item.to_followable_item_handle(cx)?; - - // If the item belongs to a particular project, then it should - // only be included if this project is shared, and the follower - // is in the project. - // - // Some items, like channel notes, do not belong to a particular - // project, so they should be included regardless of whether the - // current project is shared, or what project the follower is in. - if item.is_project_item(cx) - && (project_id.is_none() || project_id != follower_project_id) - { - return None; - } - - let id = item.remote_id(client, cx)?.to_proto(); - let variant = item.to_state_proto(cx)?; - Some(proto::View { - id: Some(id), - leader_id, - variant: Some(variant), - }) - } - }) - }) - .collect(), } } @@ -3153,34 +3160,43 @@ impl Workspace { cx: &mut AsyncWindowContext, ) -> Result<()> { match update.variant.ok_or_else(|| anyhow!("invalid update"))? { - proto::update_followers::Variant::UpdateActiveView(update_active_view) => { - let panes_missing_view = this.update(cx, |this, _| { - let mut panes = vec![]; - for (pane, state) in &mut this.follower_states { - if state.leader_id != leader_id { - continue; - } + 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) { + anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id)) + } else { + anyhow::Ok(false) + } + })??; - state.active_view_id = - if let Some(active_view_id) = update_active_view.id.clone() { - Some(ViewId::from_proto(active_view_id)?) - } else { - None - }; + if should_add_view { + Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await? + } + } + 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) { + state.active_view_id = update_active_view + .view + .as_ref() + .and_then(|view| ViewId::from_proto(view.id.clone()?).ok()); if state.active_view_id.is_some_and(|view_id| { !state.items_by_leader_view_id.contains_key(&view_id) }) { - panes.push(pane.clone()) + anyhow::Ok(true) + } else { + anyhow::Ok(false) } + } else { + anyhow::Ok(false) } - anyhow::Ok(panes) })??; - if let Some(view) = update_active_view.view { - for pane in panes_missing_view { - Self::add_view_from_leader(this.clone(), leader_id, pane.clone(), &view, cx) - .await? + if should_add_view { + if let Some(view) = update_active_view.view { + Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await? } } } @@ -3194,28 +3210,16 @@ impl Workspace { let mut tasks = Vec::new(); this.update(cx, |this, cx| { let project = this.project.clone(); - for (_, state) in &mut this.follower_states { - if state.leader_id == leader_id { - let view_id = ViewId::from_proto(id.clone())?; - if let Some(item) = state.items_by_leader_view_id.get(&view_id) { - tasks.push(item.apply_update_proto(&project, variant.clone(), cx)); - } + if let Some(state) = this.follower_states.get(&leader_id) { + 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(&project, variant.clone(), cx)); } } anyhow::Ok(()) })??; try_join_all(tasks).await.log_err(); } - proto::update_followers::Variant::CreateView(view) => { - let panes = this.update(cx, |this, _| { - this.follower_states - .iter() - .filter_map(|(pane, state)| (state.leader_id == leader_id).then_some(pane)) - .cloned() - .collect() - })?; - Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], cx).await?; - } } this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?; Ok(()) @@ -3224,46 +3228,92 @@ impl Workspace { async fn add_view_from_leader( this: WeakView, leader_id: PeerId, - pane: View, view: &proto::View, cx: &mut AsyncWindowContext, ) -> Result<()> { let this = this.upgrade().context("workspace dropped")?; - let item_builders = cx.update(|cx| { - cx.default_global::() - .values() - .map(|b| b.0) - .collect::>() - })?; - let Some(id) = view.id.clone() else { return Err(anyhow!("no id for view")); }; let id = ViewId::from_proto(id)?; + let panel_id = view.panel_id.and_then(|id| proto::PanelId::from_i32(id)); - let mut variant = view.variant.clone(); - if variant.is_none() { - Err(anyhow!("missing view variant"))?; - } + let pane = this.update(cx, |this, _cx| { + let state = this + .follower_states + .get(&leader_id) + .context("stopped following")?; + anyhow::Ok(state.pane().clone()) + })??; + let existing_item = pane.update(cx, |pane, cx| { + let client = this.read(cx).client().clone(); + pane.items().find_map(|item| { + let item = item.to_followable_item_handle(cx)?; + if item.remote_id(&client, cx) == Some(id) { + Some(item) + } else { + None + } + }) + })?; + let item = if let Some(existing_item) = existing_item { + existing_item + } else { + let variant = view.variant.clone(); + if variant.is_none() { + Err(anyhow!("missing view variant"))?; + } - let task = item_builders.iter().find_map(|build_item| { - cx.update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx)) - .log_err() - .flatten() - }); - let Some(task) = task else { - return Err(anyhow!( - "failed to construct view from leader (maybe from a different version of zed?)" - )); + let task = cx.update(|cx| { + FollowableViewRegistry::from_state_proto(this.clone(), id, variant, cx) + })?; + + let Some(task) = task else { + return Err(anyhow!( + "failed to construct view from leader (maybe from a different version of zed?)" + )); + }; + + let mut new_item = task.await?; + pane.update(cx, |pane, cx| { + let mut item_ix_to_remove = None; + for (ix, item) in pane.items().enumerate() { + if let Some(item) = item.to_followable_item_handle(cx) { + match new_item.dedup(item.as_ref(), cx) { + Some(item::Dedup::KeepExisting) => { + new_item = + item.boxed_clone().to_followable_item_handle(cx).unwrap(); + break; + } + Some(item::Dedup::ReplaceExisting) => { + item_ix_to_remove = Some(ix); + break; + } + None => {} + } + } + } + + if let Some(ix) = item_ix_to_remove { + pane.remove_item(ix, false, false, cx); + pane.add_item(new_item.boxed_clone(), false, false, Some(ix), cx); + } + })?; + + new_item }; - let item = task.await?; - this.update(cx, |this, cx| { - let state = this.follower_states.get_mut(&pane)?; + let state = this.follower_states.get_mut(&leader_id)?; item.set_leader_peer_id(Some(leader_id), cx); - state.items_by_leader_view_id.insert(id, item); + state.items_by_leader_view_id.insert( + id, + FollowerView { + view: item, + location: panel_id, + }, + ); Some(()) })?; @@ -3271,74 +3321,13 @@ impl Workspace { Ok(()) } - async fn add_views_from_leader( - this: WeakView, - leader_id: PeerId, - panes: Vec>, - views: Vec, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - let this = this.upgrade().context("workspace dropped")?; - - let item_builders = cx.update(|cx| { - cx.default_global::() - .values() - .map(|b| b.0) - .collect::>() - })?; - - let mut item_tasks_by_pane = HashMap::default(); - for pane in panes { - let mut item_tasks = Vec::new(); - let mut leader_view_ids = Vec::new(); - for view in &views { - let Some(id) = &view.id else { - continue; - }; - let id = ViewId::from_proto(id.clone())?; - let mut variant = view.variant.clone(); - if variant.is_none() { - Err(anyhow!("missing view variant"))?; - } - for build_item in &item_builders { - let task = cx.update(|cx| { - build_item(pane.clone(), this.clone(), id, &mut variant, cx) - })?; - if let Some(task) = task { - item_tasks.push(task); - leader_view_ids.push(id); - break; - } else if variant.is_none() { - Err(anyhow!( - "failed to construct view from leader (maybe from a different version of zed?)" - ))?; - } - } - } - - item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids)); - } - - for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane { - let items = futures::future::try_join_all(item_tasks).await?; - this.update(cx, |this, cx| { - let state = this.follower_states.get_mut(&pane)?; - for (id, item) in leader_view_ids.into_iter().zip(items) { - item.set_leader_peer_id(Some(leader_id), cx); - state.items_by_leader_view_id.insert(id, item); - } - - Some(()) - })?; - } - Ok(()) - } - pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) { let mut is_project_item = true; let mut update = proto::UpdateActiveView::default(); if cx.is_window_active() { - if let Some(item) = self.active_item(cx) { + let (active_item, panel_id) = self.active_item_for_followers(cx); + + if let Some(item) = active_item { if item.focus_handle(cx).contains_focused(cx) { let leader_id = self .pane_for(&*item) @@ -3355,13 +3344,14 @@ impl Workspace { id: Some(id.clone()), leader_id, variant: Some(variant), + panel_id: panel_id.map(|id| id as i32), }); is_project_item = item.is_project_item(cx); update = proto::UpdateActiveView { view, - // TODO: once v0.124.0 is retired we can stop sending these - id: Some(id), + // TODO: Remove after version 0.145.x stabilizes. + id: Some(id.clone()), leader_id, }; } @@ -3371,8 +3361,9 @@ impl Workspace { } } - if &update.id != &self.last_active_view_id { - self.last_active_view_id.clone_from(&update.id); + let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref()); + if active_view_id != self.last_active_view_id.as_ref() { + self.last_active_view_id = active_view_id.cloned(); self.update_followers( is_project_item, proto::update_followers::Variant::UpdateActiveView(update), @@ -3381,6 +3372,32 @@ impl Workspace { } } + fn active_item_for_followers( + &self, + cx: &mut WindowContext, + ) -> (Option>, Option) { + let mut active_item = None; + let mut panel_id = None; + for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] { + if dock.focus_handle(cx).contains_focused(cx) { + if let Some(panel) = dock.read(cx).active_panel() { + if let Some(pane) = panel.pane(cx) { + if let Some(item) = pane.read(cx).active_item() { + active_item = Some(item); + panel_id = panel.remote_id(); + break; + } + } + } + } + } + + if active_item.is_none() { + active_item = self.active_pane().read(cx).active_item(); + } + (active_item, panel_id) + } + fn update_followers( &self, project_only: bool, @@ -3402,7 +3419,13 @@ impl Workspace { } pub fn leader_for_pane(&self, pane: &View) -> Option { - self.follower_states.get(pane).map(|state| state.leader_id) + self.follower_states.iter().find_map(|(leader_id, state)| { + if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) { + Some(*leader_id) + } else { + None + } + }) } fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { @@ -3411,7 +3434,6 @@ impl Workspace { let call = self.active_call()?; 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; @@ -3430,38 +3452,48 @@ impl Workspace { } }; - for (pane, state) in &self.follower_states { - if state.leader_id != leader_id { - continue; - } - 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.is_project_item(cx) { - items_to_activate.push((pane.clone(), item.boxed_clone())); - } + 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(cx) { + item_to_activate = Some((item.location, item.view.boxed_clone())); } - continue; } + } else if let Some(shared_screen) = + self.shared_screen_for_peer(leader_id, &state.center_pane, cx) + { + item_to_activate = Some((None, Box::new(shared_screen))); + } - if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { - items_to_activate.push((pane.clone(), Box::new(shared_screen))); + let (panel_id, item) = item_to_activate?; + + let mut transfer_focus = state.center_pane.read(cx).has_focus(cx); + let pane; + if let Some(panel_id) = panel_id { + pane = self.activate_panel_for_proto_id(panel_id, cx)?.pane(cx)?; + let state = self.follower_states.get_mut(&leader_id)?; + state.dock_pane = Some(pane.clone()); + } else { + pane = state.center_pane.clone(); + let state = self.follower_states.get_mut(&leader_id)?; + if let Some(dock_pane) = state.dock_pane.take() { + transfer_focus |= dock_pane.focus_handle(cx).contains_focused(cx); } } - for (pane, item) in items_to_activate { - let pane_was_focused = pane.read(cx).has_focus(cx); - if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) { - pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx)); + pane.update(cx, |pane, cx| { + let focus_active_item = pane.has_focus(cx) || transfer_focus; + if let Some(index) = pane.index_for_item(item.as_ref()) { + pane.activate_item(index, false, false, cx); } else { - pane.update(cx, |pane, cx| { - pane.add_item(item.boxed_clone(), false, false, None, cx) - }); + pane.add_item(item.boxed_clone(), false, false, None, cx) } - if pane_was_focused { - pane.update(cx, |pane, cx| pane.focus_active_item(cx)); + if focus_active_item { + pane.focus_active_item(cx) } - } + }); None } @@ -3848,7 +3880,7 @@ impl Workspace { .on_action(cx.listener(Self::follow_next_collaborator)) .on_action(cx.listener(|workspace, _: &Unfollow, cx| { let pane = workspace.active_pane().clone(); - workspace.unfollow(&pane, cx); + workspace.unfollow_in_pane(&pane, cx); })) .on_action(cx.listener(|workspace, action: &Save, cx| { workspace @@ -3995,6 +4027,65 @@ impl Workspace { .unwrap_or(Self::DEFAULT_PADDING) .clamp(0.0, Self::MAX_PADDING) } + + fn render_dock( + &self, + position: DockPosition, + dock: &View, + cx: &WindowContext, + ) -> Option
{ + if self.zoomed_position == Some(position) { + return None; + } + + let leader_border = dock.read(cx).active_panel().and_then(|panel| { + let pane = panel.pane(cx)?; + let follower_states = &self.follower_states; + leader_border_for_pane(follower_states, &pane, cx) + }); + + Some( + div() + .flex() + .flex_none() + .overflow_hidden() + .child(dock.clone()) + .children(leader_border), + ) + } +} + +fn leader_border_for_pane( + follower_states: &HashMap, + pane: &View, + cx: &WindowContext, +) -> Option
{ + let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| { + if state.pane() == pane { + Some((*leader_id, state)) + } else { + None + } + })?; + + 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 = cx + .theme() + .players() + .color_for_participant(leader.participant_index.0) + .cursor; + leader_color.fade_out(0.3); + Some( + div() + .absolute() + .size_full() + .left_0() + .top_0() + .border_2() + .border_color(leader_color), + ) } fn window_bounds_env_override() -> Option> { @@ -4231,15 +4322,7 @@ impl Render for Workspace { .flex_row() .h_full() // Left Dock - .children(self.zoomed_position.ne(&Some(DockPosition::Left)).then( - || { - div() - .flex() - .flex_none() - .overflow_hidden() - .child(self.left_dock.clone()) - }, - )) + .children(self.render_dock(DockPosition::Left, &self.left_dock, cx)) // Panes .child( div() @@ -4266,24 +4349,18 @@ impl Render for Workspace { this.child(p.border_l_1()) }), ) - .children( - self.zoomed_position - .ne(&Some(DockPosition::Bottom)) - .then(|| self.bottom_dock.clone()), - ), + .children(self.render_dock( + DockPosition::Bottom, + &self.bottom_dock, + cx, + )), ) // Right Dock - .children( - self.zoomed_position.ne(&Some(DockPosition::Right)).then( - || { - div() - .flex() - .flex_none() - .overflow_hidden() - .child(self.right_dock.clone()) - }, - ), - ), + .children(self.render_dock( + DockPosition::Right, + &self.right_dock, + cx, + )), ) .children(self.zoomed.as_ref().and_then(|view| { let zoomed_view = view.upgrade()?; @@ -4369,24 +4446,8 @@ impl WorkspaceStore { workspace .update(cx, |workspace, cx| { let handler_response = workspace.handle_follow(follower.project_id, cx); - if response.views.is_empty() { - response.views = handler_response.views; - } else { - response.views.extend_from_slice(&handler_response.views); - } - - if let Some(active_view_id) = handler_response.active_view_id.clone() { - if response.active_view_id.is_none() - || workspace.project.read(cx).remote_id() == follower.project_id - { - response.active_view_id = Some(active_view_id); - } - } - if let Some(active_view) = handler_response.active_view.clone() { - if response.active_view_id.is_none() - || workspace.project.read(cx).remote_id() == follower.project_id - { + if workspace.project.read(cx).remote_id() == follower.project_id { response.active_view = Some(active_view) } } @@ -4441,6 +4502,12 @@ impl ViewId { } } +impl FollowerState { + fn pane(&self) -> &View { + self.dock_pane.as_ref().unwrap_or(&self.center_pane) + } +} + pub trait WorkspaceHandle { fn file_project_paths(&self, cx: &AppContext) -> Vec; }