Detailed changes
@@ -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::ActivateItem { local } => {
+ if *local {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ workspace.unfollow_in_pane(&pane, cx);
+ })
+ .ok();
}
+ cx.emit(AssistantPanelEvent::ContextEdited);
}
- pane::Event::RemoveItem { .. } | pane::Event::ActivateItem { .. } => {
+ 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 {
@@ -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)
@@ -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 {
@@ -84,31 +83,12 @@ impl ChannelView {
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
- let weak_workspace = workspace.downgrade();
- let workspace = workspace.read(cx);
- let project = workspace.project().to_owned();
- let channel_store = ChannelStore::global(cx);
- let language_registry = workspace.app_state().languages.clone();
- let markdown = language_registry.language_for_name("Markdown");
- let channel_buffer =
- channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
-
+ let channel_view = Self::load(channel_id, workspace, cx);
cx.spawn(|mut cx| async move {
- let channel_buffer = channel_buffer.await?;
- let markdown = markdown.await.log_err();
-
- channel_buffer.update(&mut cx, |channel_buffer, cx| {
- channel_buffer.buffer().update(cx, |buffer, cx| {
- buffer.set_language_registry(language_registry);
- let Some(markdown) = markdown else {
- return;
- };
- buffer.set_language(Some(markdown), cx);
- })
- })?;
+ let channel_view = channel_view.await?;
pane.update(&mut cx, |pane, cx| {
- let buffer_id = channel_buffer.read(cx).remote_id(cx);
+ let buffer_id = channel_view.read(cx).channel_buffer.read(cx).remote_id(cx);
let existing_view = pane
.items_of_type::<Self>()
@@ -116,7 +96,8 @@ impl ChannelView {
// 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 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)
@@ -126,30 +107,60 @@ impl ChannelView {
}
}
- 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);
+ pane.add_item(Box::new(channel_view.clone()), true, true, Some(ix), cx);
}
}
if let Some(link_position) = link_position {
- view.update(cx, |channel_view, cx| {
+ channel_view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
- view
+ 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);
+ let project = workspace.project().to_owned();
+ let channel_store = ChannelStore::global(cx);
+ let language_registry = workspace.app_state().languages.clone();
+ let markdown = language_registry.language_for_name("Markdown");
+ let channel_buffer =
+ channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
+
+ cx.spawn(|mut cx| async move {
+ let channel_buffer = channel_buffer.await?;
+ let markdown = markdown.await.log_err();
+
+ channel_buffer.update(&mut cx, |channel_buffer, cx| {
+ channel_buffer.buffer().update(cx, |buffer, cx| {
+ buffer.set_language_registry(language_registry);
+ let Some(markdown) = markdown else {
+ return;
+ };
+ buffer.set_language(Some(markdown), cx);
+ })
+ })?;
+
+ 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::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>);
@@ -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>| {
@@ -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(),
@@ -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,71 +75,54 @@ 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 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,
- );
- }
+ 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
- });
+ if let Some(title) = &state.title {
+ multibuffer = multibuffer.with_title(title.clone())
+ }
- cx.new_view(|cx| {
- let mut editor =
- Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx);
- editor.remote_id = Some(remote_id);
- editor
- })
- })?
- };
+ 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(),
@@ -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(
@@ -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.
@@ -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 {
@@ -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();
})
@@ -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
}
@@ -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 {
@@ -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 follower_state = follower_states.iter().find_map(|(leader_id, state)| {
+ if state.center_pane == *pane {
+ Some((*leader_id, state))
+ } else {
+ None
+ }
+ });
- let leader = follower_state.and_then(|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(state.leader_id)
+ room.remote_participant_for_peer_id(*leader_id)
});
- let is_in_unshared_view = follower_state.map_or(false, |state| {
+ 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<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>,
@@ -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<I: ProjectItem>(cx: &mut AppContext) {
});
}
-type FollowableItemBuilder = fn(
- View<Pane>,
- View<Workspace>,
- ViewId,
- &mut Option<proto::view::Variant>,
- &mut WindowContext,
-) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
-
-#[derive(Default, Deref, DerefMut)]
-struct FollowableItemBuilders(
- HashMap<
- TypeId,
- (
- FollowableItemBuilder,
- fn(&AnyView) -> Box<dyn FollowableItemHandle>,
- ),
- >,
-);
+#[derive(Default)]
+pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
+
+struct FollowableViewDescriptor {
+ from_state_proto: fn(
+ View<Workspace>,
+ ViewId,
+ &mut Option<proto::view::Variant>,
+ &mut WindowContext,
+ ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
+ to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
+}
-impl Global for FollowableItemBuilders {}
-
-pub fn register_followable_item<I: FollowableItem>(cx: &mut AppContext) {
- let builders = cx.default_global::<FollowableItemBuilders>();
- builders.insert(
- TypeId::of::<I>(),
- (
- |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 Global for FollowableViewRegistry {}
+
+impl FollowableViewRegistry {
+ pub fn register<I: FollowableItem>(cx: &mut AppContext) {
+ cx.default_global::<Self>().0.insert(
+ TypeId::of::<I>(),
+ 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::<I>().unwrap()),
},
- |this| Box::new(this.clone().downcast::<I>().unwrap()),
- ),
- );
+ );
+ }
+
+ pub fn from_state_proto(
+ workspace: View<Workspace>,
+ view_id: ViewId,
+ mut state: Option<proto::view::Variant>,
+ cx: &mut WindowContext,
+ ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
+ 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<AnyView>,
+ cx: &AppContext,
+ ) -> Option<Box<dyn FollowableItemHandle>> {
+ let this = cx.try_global::<Self>()?;
+ 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<AnyView>,
notifications: Vec<(NotificationId, Box<dyn NotificationHandle>)>,
project: Model<Project>,
- follower_states: HashMap<View<Pane>, FollowerState>,
+ follower_states: HashMap<PeerId, FollowerState>,
last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
window_edited: bool,
active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
@@ -622,11 +640,16 @@ pub struct ViewId {
pub id: u64,
}
-#[derive(Default)]
struct FollowerState {
- leader_id: PeerId,
+ center_pane: View<Pane>,
+ dock_pane: Option<View<Pane>>,
active_view_id: Option<ViewId>,
- items_by_leader_view_id: HashMap<ViewId, Box<dyn FollowableItemHandle>>,
+ items_by_leader_view_id: HashMap<ViewId, FollowerView>,
+}
+
+struct FollowerView {
+ view: Box<dyn FollowableItemHandle>,
+ location: Option<proto::PanelId>,
}
impl Workspace {
@@ -657,10 +680,10 @@ impl Workspace {
project::Event::DisconnectedFromHost => {
this.update_window_edited(cx);
- let panes_to_unfollow: Vec<View<Pane>> =
- 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::<Vec<_>>();
+ 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<T: Panel>(&mut self, panel: View<T>, cx: &mut WindowContext) {
+ pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
+ 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<Self>,
+ ) -> Option<Arc<dyn PanelHandle>> {
+ 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<T: Panel>(
&mut self,
@@ -2032,13 +2084,9 @@ impl Workspace {
}
pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
- for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
- let dock = dock.read(cx);
- if let Some(panel) = dock.panel::<T>() {
- return Some(panel);
- }
- }
- None
+ [&self.left_dock, &self.bottom_dock, &self.right_dock]
+ .iter()
+ .find_map(|dock| dock.read(cx).panel::<T>())
}
fn dismiss_zoomed_items_to_reveal(
@@ -2557,6 +2605,10 @@ impl Workspace {
cx.notify();
}
+ fn handle_panel_focused(&mut self, cx: &mut ViewContext<Self>) {
+ self.update_active_view_for_followers(cx);
+ }
+
fn handle_pane_event(
&mut self,
pane: View<Pane>,
@@ -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<Pane>,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<PeerId> {
+ 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<Pane>,
@@ -2740,7 +2802,7 @@ impl Workspace {
fn remove_pane(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
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>) {
- 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<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
- let state = self.follower_states.remove(pane)?;
- let leader_id = state.leader_id;
+ pub fn unfollow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
+ cx.notify();
+ let state = self.follower_states.remove(&leader_id)?;
for (_, item) in state.items_by_leader_view_id {
- item.set_leader_peer_id(None, cx);
+ item.view.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();
- }
+ 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();
- cx.notify();
- Some(leader_id)
+ 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<Self>) {
@@ -3058,7 +3101,8 @@ impl Workspace {
follower_project_id: Option<u64>,
cx: &mut ViewContext<Self>,
) -> Option<proto::View> {
- 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<u64>,
cx: &mut ViewContext<Self>,
) -> 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<Self>,
leader_id: PeerId,
- pane: View<Pane>,
view: &proto::View,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let this = this.upgrade().context("workspace dropped")?;
- let item_builders = cx.update(|cx| {
- cx.default_global::<FollowableItemBuilders>()
- .values()
- .map(|b| b.0)
- .collect::<Vec<_>>()
- })?;
-
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 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 mut variant = view.variant.clone();
- if variant.is_none() {
- Err(anyhow!("missing view variant"))?;
- }
+ let task = cx.update(|cx| {
+ FollowableViewRegistry::from_state_proto(this.clone(), id, variant, cx)
+ })?;
- 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 Some(task) = task else {
+ return Err(anyhow!(
+ "failed to construct view from leader (maybe from a different version of zed?)"
+ ));
+ };
- let item = task.await?;
+ 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
+ };
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<Self>,
- leader_id: PeerId,
- panes: Vec<View<Pane>>,
- views: Vec<proto::View>,
- cx: &mut AsyncWindowContext,
- ) -> Result<()> {
- let this = this.upgrade().context("workspace dropped")?;
-
- let item_builders = cx.update(|cx| {
- cx.default_global::<FollowableItemBuilders>()
- .values()
- .map(|b| b.0)
- .collect::<Vec<_>>()
- })?;
-
- 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<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
+ 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<Pane>) -> Option<PeerId> {
- 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<Self>) -> 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)));
+ }
+
+ let (panel_id, item) = item_to_activate?;
- 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 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<Dock>,
+ cx: &WindowContext,
+ ) -> Option<Div> {
+ 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<PeerId, FollowerState>,
+ pane: &View<Pane>,
+ cx: &WindowContext,
+) -> Option<Div> {
+ 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<Bounds<Pixels>> {