Compare commits

...

8 Commits

Author SHA1 Message Date
Antonio Scandurra
1e3063b65e WIP 2024-07-11 14:20:46 +02:00
Max Brunsfeld
6b3d53159e WIP - Start work on following items in panels 2024-07-10 18:10:33 -07:00
Max Brunsfeld
1641f0f73d Add the ability to follow into context editors when they're in a center pane 2024-07-10 17:22:58 -07:00
Antonio Scandurra
8f3cb69daa Introduce FollowableViewRegistry
Co-Authored-By: Max <max@zed.dev>
2024-07-10 19:47:51 +02:00
Antonio Scandurra
6bebf42ed7 Add FollowableItem::dedup
Co-Authored-By: Max <max@zed.dev>
2024-07-10 18:37:17 +02:00
Antonio Scandurra
abf9f86fb7 Don't pass pane to FollowableItemHandle::from_state_proto 2024-07-10 18:37:17 +02:00
Antonio Scandurra
6591344b60 WIP 2024-07-10 18:37:16 +02:00
Antonio Scandurra
8bfab8fef6 WIP 2024-07-10 18:37:16 +02:00
19 changed files with 811 additions and 489 deletions

View File

@@ -18,6 +18,7 @@ use crate::{
use anyhow::{anyhow, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
use breadcrumbs::Breadcrumbs;
use client::proto;
use collections::{BTreeSet, HashMap, HashSet};
use editor::{
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
@@ -58,7 +59,7 @@ use ui::{
use util::ResultExt;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
item::{BreadcrumbText, Item, ItemHandle},
item::{self, BreadcrumbText, FollowableItem, FollowableItemHandle, 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
@@ -579,9 +581,10 @@ impl AssistantPanel {
});
let editor = cx.new_view(|cx| {
let mut editor = ContextEditor::for_context(
let mut editor = ContextEditor::new(
context,
self.fs.clone(),
self.project.clone(),
workspace,
lsp_adapter_delegate,
cx,
@@ -613,12 +616,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),
_ => {}
}
}
@@ -627,6 +631,7 @@ impl AssistantPanel {
.pane
.read(cx)
.items()
.iter()
.position(|item| item.downcast::<ContextHistory>().is_some());
if let Some(history_item_ix) = history_item_ix {
@@ -679,7 +684,7 @@ impl AssistantPanel {
path: PathBuf,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let existing_context = self.pane.read(cx).items().find_map(|item| {
let existing_context = self.pane.read(cx).items().iter().find_map(|item| {
item.downcast::<ContextEditor>()
.filter(|editor| editor.read(cx).context.read(cx).path() == Some(&path))
});
@@ -694,6 +699,7 @@ impl AssistantPanel {
.update(cx, |store, cx| store.open_local_context(path.clone(), cx));
let fs = self.fs.clone();
let workspace = self.workspace.clone();
let project = self.project.clone();
let lsp_adapter_delegate = workspace
.update(cx, |workspace, cx| {
@@ -709,7 +715,7 @@ impl AssistantPanel {
.upgrade()
.ok_or_else(|| anyhow!("workspace dropped"))?;
let editor = cx.new_view(|cx| {
ContextEditor::for_context(context, fs, workspace, lsp_adapter_delegate, cx)
ContextEditor::new(context, fs, project, workspace, lsp_adapter_delegate, cx)
});
this.show_context(editor, cx);
anyhow::Ok(())
@@ -722,14 +728,17 @@ impl AssistantPanel {
&mut self,
id: ContextId,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let existing_context = self.pane.read(cx).items().find_map(|item| {
) -> Task<Result<View<ContextEditor>>> {
let existing_context = self.pane.read(cx).items().iter().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)
});
}
@@ -737,6 +746,7 @@ impl AssistantPanel {
.context_store
.update(cx, |store, cx| store.open_remote_context(id, cx));
let fs = self.fs.clone();
let project = self.project.clone();
let workspace = self.workspace.clone();
let lsp_adapter_delegate = workspace
@@ -753,12 +763,11 @@ impl AssistantPanel {
.upgrade()
.ok_or_else(|| anyhow!("workspace dropped"))?;
let editor = cx.new_view(|cx| {
ContextEditor::for_context(context, fs, workspace, lsp_adapter_delegate, cx)
ContextEditor::new(context, fs, project, workspace, lsp_adapter_delegate, cx)
});
this.show_context(editor, cx);
anyhow::Ok(())
})??;
Ok(())
this.show_context(editor.clone(), cx);
anyhow::Ok(editor)
})?
})
}
@@ -878,6 +887,37 @@ impl Panel for AssistantPanel {
}
}
fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
self.pane.read(cx).active_item()
}
fn items<'a>(&'a self, cx: &'a AppContext) -> &'a [Box<dyn ItemHandle>] {
self.pane.read(cx).items()
}
fn remote_id() -> Option<proto::PanelId> {
Some(proto::PanelId::AssistantPanel)
}
fn leader_updated(&mut self, view: Box<dyn FollowableItemHandle>, cx: &mut ViewContext<Self>) {
let item = ItemHandle::boxed_clone(view.as_ref());
self.pane.update(cx, |pane, cx| {
if let Some(index) = pane.index_for_item(item.as_ref()) {
pane.activate_item(index, false, false, cx);
} else {
pane.add_item(item, false, false, None, cx)
}
});
}
fn dedup(
&mut self,
view: Box<dyn FollowableItemHandle>,
cx: &mut ViewContext<Self>,
) -> Box<dyn FollowableItemHandle> {
self.pane.update(cx, |pane, cx| pane.dedup(view, cx))
}
fn icon(&self, cx: &WindowContext) -> Option<IconName> {
let settings = AssistantSettings::get_global(cx);
if !settings.enabled || !settings.button {
@@ -924,6 +964,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>,
@@ -932,9 +973,10 @@ pub struct ContextEditor {
impl ContextEditor {
const MAX_TAB_TITLE_LEN: usize = 16;
fn for_context(
fn new(
context: Model<Context>,
fs: Arc<dyn Fs>,
project: Model<Project>,
workspace: View<Workspace>,
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut ViewContext<Self>,
@@ -954,6 +996,7 @@ impl ContextEditor {
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_completion_provider(Box::new(completion_provider));
editor.set_collaboration_hub(Box::new(project));
editor
});
@@ -971,6 +1014,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 +1257,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 +1516,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 +1979,7 @@ impl ContextEditor {
}
}
impl EventEmitter<ContextEditorEvent> for ContextEditor {}
impl EventEmitter<EditorEvent> for ContextEditor {}
impl EventEmitter<SearchEvent> for ContextEditor {}
impl Render for ContextEditor {
@@ -1977,13 +2021,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 +2037,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 +2062,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 +2174,131 @@ 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 leader_peer_id(&self, cx: &AppContext) -> Option<proto::PeerId> {
self.editor.read(cx).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 +2535,7 @@ impl EventEmitter<()> for ContextHistory {}
impl Item for ContextHistory {
type Event = ();
fn tab_content(
&self,
params: workspace::item::TabContentParams,
_: &WindowContext,
) -> AnyElement {
fn tab_content(&self, params: item::TabContentParams, _: &WindowContext) -> AnyElement {
let color = if params.selected {
Color::Default
} else {

View File

@@ -135,7 +135,7 @@ async fn test_basic_following(
assert_eq!(editor.selections.ranges(cx), vec![2..1]);
});
// When client B starts following client A, all visible view states are replicated to client B.
// When client B starts following client A, only the active view state is replicated to client B.
workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
cx_c.executor().run_until_parked();
@@ -156,7 +156,7 @@ async fn test_basic_following(
);
assert_eq!(
editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![3..2]
vec![3..3]
);
executor.run_until_parked();
@@ -601,6 +601,7 @@ async fn test_following_tab_order(
let pane_paths = |pane: &View<workspace::Pane>, cx: &mut VisualTestContext| {
pane.update(cx, |pane, cx| {
pane.items()
.iter()
.map(|item| {
item.project_path(cx)
.unwrap()
@@ -1829,6 +1830,7 @@ fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Ve
leader,
items: pane
.items()
.iter()
.enumerate()
.map(|(ix, item)| {
(

View File

@@ -6243,10 +6243,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[1].item_id()));
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
@@ -6264,10 +6261,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[1].item_id()));
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
@@ -6283,10 +6277,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[1].item_id()));
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
@@ -6322,10 +6313,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[1].item_id()));
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
@@ -6333,7 +6321,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Close permanent tab
pane.update(cx, |pane, cx| {
let id = pane.items().nth(0).unwrap().item_id();
let id = pane.items()[0].item_id();
pane.close_item_by_id(id, workspace::SaveIntent::Skip, cx)
})
.await
@@ -6342,10 +6330,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(0).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[0].item_id()));
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
@@ -6361,10 +6346,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(0).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[0].item_id()));
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
@@ -6390,10 +6372,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(0).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[0].item_id()));
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
@@ -6403,10 +6382,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[1].item_id()));
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
@@ -6428,10 +6404,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_2.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(0).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[0].item_id()));
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
@@ -6441,10 +6414,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[1].item_id()));
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());

View File

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

View File

@@ -268,7 +268,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>| {
@@ -2037,10 +2037,6 @@ impl Editor {
self.buffer.read(cx).replica_id()
}
pub fn leader_peer_id(&self) -> Option<PeerId> {
self.leader_peer_id
}
pub fn buffer(&self) -> &Model<MultiBuffer> {
&self.buffer
}

View File

@@ -8189,7 +8189,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(),
@@ -8281,7 +8280,6 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
let follower_2 = cx
.update_window(*workspace.deref(), |_, cx| {
Editor::from_state_proto(
pane.clone(),
workspace.root_view(cx).unwrap().clone(),
ViewId {
creator: Default::default(),

View File

@@ -19,7 +19,7 @@ use multi_buffer::AnchorRangeExt;
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view, PeerId};
use settings::Settings;
use workspace::item::{ItemSettings, TabContentParams};
use workspace::item::{Dedup, ItemSettings, TabContentParams};
use std::{
any::TypeId,
@@ -34,7 +34,7 @@ use text::{BufferId, Selection};
use theme::{Theme, ThemeSettings};
use ui::{h_flex, prelude::*, Label};
use util::{paths::PathExt, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowEvent, FollowableItemHandle};
use workspace::item::{BreadcrumbText, FollowEvent};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
@@ -49,7 +49,6 @@ impl FollowableItem for Editor {
}
fn from_state_proto(
pane: View<workspace::Pane>,
workspace: View<Workspace>,
remote_id: ViewId,
state: &mut Option<proto::view::Variant>,
@@ -63,7 +62,6 @@ impl FollowableItem for Editor {
unreachable!()
};
let client = project.read(cx).client();
let replica_id = project.read(cx).replica_id();
let buffer_ids = state
.excerpts
@@ -77,72 +75,55 @@ 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 = 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,
@@ -181,6 +162,10 @@ impl FollowableItem for Editor {
cx.notify();
}
fn leader_peer_id(&self, _cx: &AppContext) -> Option<PeerId> {
self.leader_peer_id.clone()
}
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
let buffer = self.buffer.read(cx);
if buffer
@@ -327,6 +312,16 @@ impl FollowableItem for Editor {
fn is_project_item(&self, _cx: &WindowContext) -> bool {
true
}
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
let self_singleton = self.buffer.read(cx).as_singleton()?;
let other_singleton = existing.buffer.read(cx).as_singleton()?;
if self_singleton == other_singleton {
Some(Dedup::KeepExisting)
} else {
None
}
}
}
async fn update_editor_from_message(

View File

@@ -41,6 +41,7 @@ pub fn init(cx: &mut AppContext) {
.active_pane()
.read(cx)
.items()
.iter()
.find_map(|item| item.downcast::<ExtensionsPage>());
if let Some(existing) = existing {

View File

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

View File

@@ -1577,18 +1577,16 @@ message Follow {
message FollowResponse {
View active_view = 3;
// TODO: after 0.124.0 is retired, remove these.
optional ViewId active_view_id = 1;
repeated View views = 2;
reserved 1;
reserved 2;
}
message UpdateFollowers {
uint64 room_id = 1;
optional uint64 project_id = 2;
reserved 3;
reserved 5;
oneof variant {
View create_view = 5;
// TODO: after 0.124.0 is retired, remove these.
UpdateActiveView update_active_view = 4;
UpdateView update_view = 6;
}
@@ -1616,11 +1614,15 @@ message ViewId {
}
message UpdateActiveView {
optional ViewId id = 1;
optional PeerId leader_id = 2;
reserved 1;
reserved 2;
View view = 3;
}
enum PanelId {
AssistantPanel = 0;
}
message UpdateView {
ViewId id = 1;
optional PeerId leader_id = 2;
@@ -1643,10 +1645,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 {
@@ -1664,6 +1668,11 @@ message View {
uint64 channel_id = 1;
Editor editor = 2;
}
message ContextEditor {
string context_id = 1;
Editor editor = 2;
}
}
message Collaborator {

View File

@@ -757,6 +757,7 @@ impl ProjectSearchView {
.active_pane()
.read(cx)
.items()
.iter()
.find_map(|item| item.downcast::<ProjectSearchView>());
Self::existing_or_new_search(workspace, existing, action, cx);

View File

@@ -194,7 +194,8 @@ impl TabSwitcherDelegate {
},
);
let items: Vec<Box<dyn ItemHandle>> = pane.items().map(|item| item.boxed_clone()).collect();
let items: Vec<Box<dyn ItemHandle>> =
pane.items().iter().map(|item| item.boxed_clone()).collect();
items
.iter()
.enumerate()

View File

@@ -456,6 +456,7 @@ impl TerminalPanel {
self.pane
.read(cx)
.items()
.iter()
.enumerate()
.filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
.filter_map(|(index, terminal_view)| {
@@ -545,6 +546,7 @@ impl TerminalPanel {
.pane
.read(cx)
.items()
.iter()
.filter_map(|item| {
let terminal_view = item.act_as::<TerminalView>(cx)?;
if terminal_view.read(cx).terminal().read(cx).task().is_some() {

View File

@@ -46,7 +46,7 @@ use std::{ops::Range, sync::Arc};
use surrounds::{add_surrounds, change_surrounds, delete_surrounds, SurroundsType};
use ui::BorrowAppContext;
use visual::{visual_block_motion, visual_replace};
use workspace::{self, Workspace};
use workspace::{self, item::FollowableItem, Workspace};
use crate::state::ReplayableAction;
@@ -241,7 +241,7 @@ impl Vim {
self.active_editor = Some(editor.clone().downgrade());
self.editor_subscription = Some(cx.subscribe(&editor, |editor, event, cx| match event {
EditorEvent::SelectionsChanged { local: true } => {
if editor.read(cx).leader_peer_id().is_none() {
if editor.read(cx).leader_peer_id(cx).is_none() {
Vim::update(cx, |vim, cx| {
vim.local_selections_changed(editor, cx);
})
@@ -275,7 +275,7 @@ impl Vim {
&& !newest_selection_empty
&& self.state().mode == Mode::Normal
// When following someone, don't switch vim mode.
&& editor.leader_peer_id().is_none()
&& editor.leader_peer_id(cx).is_none()
{
self.switch_mode(Mode::Visual, true, cx);
}

View File

@@ -1,6 +1,9 @@
use crate::item::{FollowableItemHandle, ItemHandle};
use crate::persistence::model::DockData;
use crate::{status_bar::StatusItemView, Workspace};
use crate::{DraggedDock, Event};
use call::ActiveCall;
use client::proto::{self, PeerId};
use gpui::{
deferred, div, px, Action, AnchorCorner, AnyView, AppContext, Axis, Entity, EntityId,
EventEmitter, FocusHandle, FocusableView, IntoElement, KeyContext, MouseButton, MouseDownEvent,
@@ -44,6 +47,29 @@ 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 active_item(&self, _cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
None
}
fn items<'a>(&'a self, _cx: &'a AppContext) -> &'a [Box<dyn ItemHandle>] {
&[]
}
fn remote_id() -> Option<proto::PanelId> {
None
}
fn leader_updated(
&mut self,
_view: Box<dyn FollowableItemHandle>,
_cx: &mut ViewContext<Self>,
) {
unimplemented!("leader_updated was not implemented")
}
fn dedup(
&mut self,
_view: Box<dyn FollowableItemHandle>,
_cx: &mut ViewContext<Self>,
) -> Box<dyn FollowableItemHandle> {
unimplemented!("dedup was not implemented")
}
}
pub trait PanelHandle: Send + Sync {
@@ -55,6 +81,15 @@ 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 active_item(&self, cx: &WindowContext) -> Option<Box<dyn ItemHandle>>;
fn items<'a>(&'a self, cx: &'a WindowContext) -> &'a [Box<dyn ItemHandle>];
fn dedup(
&self,
item: Box<dyn FollowableItemHandle>,
cx: &mut WindowContext,
) -> Box<dyn FollowableItemHandle>;
fn remote_id(&self) -> Option<proto::PanelId>;
fn leader_updated(&self, view: Box<dyn FollowableItemHandle>, cx: &mut WindowContext);
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 +136,30 @@ where
self.update(cx, |this, cx| this.set_active(active, cx))
}
fn active_item(&self, cx: &WindowContext) -> Option<Box<dyn ItemHandle>> {
self.read(cx).active_item(cx)
}
fn items<'a>(&'a self, cx: &'a WindowContext) -> &'a [Box<dyn ItemHandle>] {
self.read(cx).items(cx)
}
fn remote_id(&self) -> Option<proto::PanelId> {
T::remote_id()
}
fn leader_updated(&self, view: Box<dyn FollowableItemHandle>, cx: &mut WindowContext) {
self.update(cx, |this, cx| this.leader_updated(view, cx))
}
fn dedup(
&self,
item: Box<dyn FollowableItemHandle>,
cx: &mut WindowContext,
) -> Box<dyn FollowableItemHandle> {
self.update(cx, |this, cx| this.dedup(item, cx))
}
fn size(&self, cx: &WindowContext) -> Pixels {
self.read(cx).size(cx)
}
@@ -280,6 +339,11 @@ impl Dock {
.find_map(|entry| entry.panel.to_any().clone().downcast().ok())
}
pub fn panel_for_index(&self, index: usize) -> Option<&dyn PanelHandle> {
let entry = self.panel_entries.get(index)?;
Some(entry.panel.as_ref())
}
pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
self.panel_entries
.iter()
@@ -296,6 +360,12 @@ impl Dock {
.position(|entry| entry.panel.persistent_name() == ui_name)
}
pub fn panel_index_for_remote_id(&self, remote_id: proto::PanelId) -> Option<usize> {
self.panel_entries
.iter()
.position(|entry| entry.panel.remote_id() == Some(remote_id))
}
pub fn active_panel_index(&self) -> usize {
self.active_panel_index
}
@@ -508,6 +578,17 @@ impl Dock {
Some(&self.panel_entries.get(self.active_panel_index)?.panel)
}
pub fn leader_peer_id(&self, cx: &WindowContext) -> Option<PeerId> {
if self.focus_handle.contains_focused(cx) {
let panel = self.active_panel()?;
let item = panel.active_item(cx)?;
let view = item.to_followable_item_handle(cx)?;
view.leader_peer_id(cx)
} else {
None
}
}
fn visible_entry(&self) -> Option<&PanelEntry> {
if self.is_open {
self.panel_entries.get(self.active_panel_index)
@@ -627,6 +708,26 @@ impl Render for Dock {
}
};
let leader_border = self.leader_peer_id(cx).and_then(|peer_id| {
let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
let leader = room.remote_participant_for_peer_id(peer_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),
)
});
div()
.key_context(dispatch_context)
.track_focus(&self.focus_handle)
@@ -656,6 +757,7 @@ impl Render for Dock {
.cached(StyleRefinement::default().v_flex().size_full()),
),
)
.children(leader_border)
.when(self.resizeable, |this| this.child(create_resize_handle()))
} else {
div()

View File

@@ -3,7 +3,7 @@ use crate::{
persistence::model::ItemId,
searchable::SearchableItemHandle,
workspace_settings::{AutosaveSetting, WorkspaceSettings},
DelayedDebouncedEditAction, FollowableItemBuilders, ItemNavHistory, ToolbarItemLocation,
DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, ToolbarItemLocation,
ViewId, Workspace, WorkspaceId,
};
use anyhow::Result;
@@ -472,22 +472,6 @@ impl<T: Item> ItemHandle for View<T> {
this.added_to_workspace(workspace, cx);
});
if let Some(followed_item) = self.to_followable_item_handle(cx) {
if let Some(message) = followed_item.to_state_proto(cx) {
workspace.update_followers(
followed_item.is_project_item(cx),
proto::update_followers::Variant::CreateView(proto::View {
id: followed_item
.remote_id(&workspace.client(), cx)
.map(|id| id.to_proto()),
variant: Some(message),
leader_id: workspace.leader_for_pane(&pane),
}),
cx,
);
}
}
if workspace
.panes_by_item
.insert(self.item_id(), pane.downgrade())
@@ -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,12 +780,16 @@ 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 leader_peer_id(&self, cx: &AppContext) -> Option<PeerId>;
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup>;
}
pub trait FollowableItemHandle: ItemHandle {
fn boxed_clone(&self) -> Box<dyn FollowableItemHandle>;
fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId>;
fn downgrade(&self) -> Box<dyn WeakFollowableItemHandle>;
fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
fn leader_peer_id(&self, cx: &AppContext) -> Option<PeerId>;
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
fn add_event_to_update_proto(
&self,
@@ -815,9 +805,14 @@ 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> {
fn boxed_clone(&self) -> Box<dyn FollowableItemHandle> {
Box::new(self.clone())
}
fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId> {
self.read(cx).remote_id().or_else(|| {
client.peer_id().map(|creator| ViewId {
@@ -835,6 +830,10 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
}
fn leader_peer_id(&self, cx: &AppContext) -> Option<PeerId> {
self.read(cx).leader_peer_id(cx)
}
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
self.read(cx).to_state_proto(cx)
}
@@ -868,6 +867,11 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
fn is_project_item(&self, cx: &WindowContext) -> bool {
self.read(cx).is_project_item(cx)
}
fn dedup(&self, existing: &dyn FollowableItemHandle, cx: &WindowContext) -> Option<Dedup> {
let existing = existing.to_any().downcast::<T>().ok()?;
self.read(cx).dedup(existing.read(cx), cx)
}
}
pub trait WeakFollowableItemHandle: Send + Sync {

View File

@@ -1,7 +1,7 @@
use crate::{
item::{
ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, TabContentParams,
WeakItemHandle,
self, ClosePosition, FollowableItemHandle, Item, ItemHandle, ItemSettings,
PreviewTabsSettings, TabContentParams, WeakItemHandle,
},
toolbar::Toolbar,
workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
@@ -452,6 +452,7 @@ impl Pane {
if let Some(alternative) = alternative {
let existing = self
.items()
.iter()
.find_position(|item| item.item_id() == alternative.id());
if let Some((ix, _)) = existing {
self.activate_item(ix, true, true, cx);
@@ -859,12 +860,42 @@ impl Pane {
cx.emit(Event::AddItem { item });
}
pub fn dedup(
&mut self,
new_item: Box<dyn FollowableItemHandle>,
cx: &mut ViewContext<Self>,
) -> Box<dyn FollowableItemHandle> {
for (ix, item) in self.items().iter().enumerate() {
if let Some(item) = item.to_followable_item_handle(cx) {
match new_item.dedup(item.as_ref(), cx) {
Some(item::Dedup::KeepExisting) => {
return item.to_followable_item_handle(cx).unwrap();
}
Some(item::Dedup::ReplaceExisting) => {
self.remove_item(ix, false, false, cx);
self.add_item(
ItemHandle::boxed_clone(new_item.as_ref()),
false,
false,
Some(ix),
cx,
);
return new_item;
}
None => {}
}
}
}
new_item
}
pub fn items_len(&self) -> usize {
self.items.len()
}
pub fn items(&self) -> impl DoubleEndedIterator<Item = &Box<dyn ItemHandle>> {
self.items.iter()
pub fn items(&self) -> &[Box<dyn ItemHandle>] {
&self.items
}
pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
@@ -1040,6 +1071,7 @@ impl Pane {
) -> Option<Task<Result<()>>> {
let item_ids: Vec<_> = self
.items()
.iter()
.filter(|item| !item.is_dirty(cx))
.map(|item| item.item_id())
.collect();
@@ -1067,6 +1099,7 @@ impl Pane {
) -> Task<Result<()>> {
let item_ids: Vec<_> = self
.items()
.iter()
.take_while(|item| item.item_id() != item_id)
.map(|item| item.item_id())
.collect();
@@ -1094,6 +1127,7 @@ impl Pane {
) -> Task<Result<()>> {
let item_ids: Vec<_> = self
.items()
.iter()
.rev()
.take_while(|item| item.item_id() != item_id)
.map(|item| item.item_id())
@@ -1527,13 +1561,14 @@ impl Pane {
entry_id: ProjectEntryId,
cx: &mut ViewContext<Pane>,
) -> Option<()> {
let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
Some((i, item.item_id()))
} else {
None
}
})?;
let (item_index_to_delete, item_id) =
self.items().iter().enumerate().find_map(|(i, item)| {
if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
Some((i, item.item_id()))
} else {
None
}
})?;
self.remove_item(item_index_to_delete, false, true, cx);
self.nav_history.remove_item(item_id);

View File

@@ -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;
@@ -356,41 +356,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)]
pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
#[derive(Default, Deref, DerefMut)]
struct FollowableItemBuilders(
HashMap<
TypeId,
(
FollowableItemBuilder,
fn(&AnyView) -> Box<dyn FollowableItemHandle>,
),
>,
);
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 {}
impl Global for FollowableViewRegistry {}
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 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)]
@@ -628,7 +646,12 @@ pub struct ViewId {
struct FollowerState {
leader_id: PeerId,
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 {
@@ -1515,7 +1538,7 @@ impl Workspace {
.panes
.iter()
.flat_map(|pane| {
pane.read(cx).items().filter_map(|item| {
pane.read(cx).items().iter().filter_map(|item| {
if item.is_dirty(cx) {
Some((pane.downgrade(), item.boxed_clone()))
} else {
@@ -1950,9 +1973,7 @@ impl Workspace {
}
pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
for dock in docks {
for dock in self.docks() {
dock.update(cx, |dock, cx| {
dock.set_open(false, cx);
});
@@ -1963,6 +1984,10 @@ impl Workspace {
self.serialize_workspace(cx);
}
fn docks(&self) -> [&View<Dock>; 3] {
[&self.left_dock, &self.bottom_dock, &self.right_dock]
}
/// Transfer focus to the panel of the given type.
pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
@@ -1985,7 +2010,7 @@ impl Workspace {
) -> Option<Arc<dyn PanelHandle>> {
let mut result_panel = None;
let mut serialize = false;
for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
for dock in self.docks() {
if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
let mut focus_center = false;
let panel = dock.update(cx, |dock, cx| {
@@ -2023,7 +2048,7 @@ impl Workspace {
/// Open the panel of the given type
pub fn open_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
for dock in self.docks() {
if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
dock.update(cx, |dock, cx| {
dock.activate_panel(panel_index, cx);
@@ -2034,7 +2059,7 @@ 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] {
for dock in self.docks() {
let dock = dock.read(cx);
if let Some(panel) = dock.panel::<T>() {
return Some(panel);
@@ -2057,7 +2082,7 @@ impl Workspace {
// If another dock is zoomed, hide it.
let mut focus_center = false;
for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
for dock in self.docks() {
dock.update(cx, |dock, cx| {
if Some(dock.position()) != dock_to_reveal {
if let Some(panel) = dock.active_panel() {
@@ -2716,6 +2741,7 @@ impl Workspace {
let Some((item_ix, item_handle)) = source
.read(cx)
.items()
.iter()
.enumerate()
.find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
else {
@@ -2779,7 +2805,7 @@ impl Workspace {
self.follower_states.retain(|_, state| {
if state.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 {
@@ -2824,25 +2850,16 @@ impl Workspace {
.follower_states
.get_mut(&pane)
.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_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(())
}))
@@ -2935,7 +2952,7 @@ impl Workspace {
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);
item.view.set_leader_peer_id(None, cx);
}
if self
@@ -3060,7 +3077,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));
@@ -3080,6 +3098,7 @@ impl Workspace {
id: Some(id.to_proto()),
leader_id,
variant: Some(variant),
panel_id: panel_id.map(|id| id as i32),
})
}
@@ -3088,52 +3107,9 @@ 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 {
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(),
active_view: self.active_view_for_follower(follower_project_id, cx),
}
}
@@ -3163,12 +3139,10 @@ impl Workspace {
continue;
}
state.active_view_id =
if let Some(active_view_id) = update_active_view.id.clone() {
Some(ViewId::from_proto(active_view_id)?)
} else {
None
};
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)
@@ -3200,7 +3174,11 @@ impl Workspace {
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));
tasks.push(item.view.apply_update_proto(
&project,
variant.clone(),
cx,
));
}
}
}
@@ -3208,16 +3186,6 @@ impl Workspace {
})??;
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(())
@@ -3232,40 +3200,90 @@ impl Workspace {
) -> 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 mut 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 existing_item = if let Some(panel_id) = panel_id {
this.update(cx, |this, cx| {
let client = this.client().clone();
for dock in this.docks() {
if let Some(panel_ix) = dock.read(cx).panel_index_for_remote_id(panel_id) {
let panel = dock.read(cx).panel_for_index(panel_ix)?;
return panel.items(cx).iter().find_map(|item| {
let item = item.to_followable_item_handle(cx)?;
if item.remote_id(&client, cx) == Some(id) {
Some(item)
} else {
None
}
});
}
}
None
})?
} else {
pane.update(cx, |pane, cx| {
let client = this.read(cx).client().clone();
pane.items().iter().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 = task.await?;
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 = 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 new_item = task.await?;
if let Some(panel_id) = panel_id {
this.update(cx, |this, cx| {
for dock in this.docks() {
if let Some(panel_ix) = dock.read(cx).panel_index_for_remote_id(panel_id) {
return dock.update(cx, |dock, cx| {
let panel = dock.panel_for_index(panel_ix).unwrap();
panel.dedup(new_item, cx)
});
}
}
new_item
})?
} else {
pane.update(cx, |pane, cx| pane.dedup(new_item, cx))?
}
};
this.update(cx, |this, cx| {
let state = this.follower_states.get_mut(&pane)?;
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(())
})?;
@@ -3273,74 +3291,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)
@@ -3357,15 +3314,11 @@ 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),
leader_id,
};
update = proto::UpdateActiveView { view };
}
};
}
@@ -3373,8 +3326,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),
@@ -3383,6 +3337,30 @@ 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.docks() {
if dock.focus_handle(cx).contains_focused(cx) {
if let Some(panel) = dock.read(cx).active_panel() {
if let Some(item) = panel.active_item(cx) {
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,
@@ -3413,7 +3391,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;
@@ -3432,36 +3409,79 @@ impl Workspace {
}
};
let mut panel_items_to_activate = Vec::new();
let mut pane_items_to_activate = Vec::new();
for (pane, state) in &self.follower_states {
if state.leader_id != leader_id {
continue;
}
let transfer_focus = pane.read(cx).has_focus(cx)
|| self
.docks()
.iter()
.any(|dock| dock.read(cx).leader_peer_id(cx) == Some(leader_id));
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()));
if leader_in_this_project || !item.view.is_project_item(cx) {
if let Some(panel_id) = item.location {
if *pane == self.active_pane {
panel_items_to_activate.push((
panel_id,
FollowableItemHandle::boxed_clone(item.view.as_ref()),
transfer_focus,
));
}
} else {
pane_items_to_activate.push((
pane.clone(),
ItemHandle::boxed_clone(item.view.as_ref()),
transfer_focus,
));
}
}
}
continue;
}
if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
items_to_activate.push((pane.clone(), Box::new(shared_screen)));
pane_items_to_activate.push((
pane.clone(),
Box::new(shared_screen),
transfer_focus,
));
}
}
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));
} else {
pane.update(cx, |pane, cx| {
pane.add_item(item.boxed_clone(), false, false, None, cx)
});
}
for (pane, item, transfer_focus) in pane_items_to_activate {
pane.update(cx, |pane, cx| {
if let Some(index) = pane.index_for_item(item.as_ref()) {
pane.activate_item(index, false, false, cx);
} else {
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 transfer_focus {
pane.focus_active_item(cx);
}
});
}
for (panel_id, item, transfer_focus) in panel_items_to_activate {
for dock in self.docks() {
if let Some(panel_index) = dock.read(cx).panel_index_for_remote_id(panel_id) {
dock.update(cx, |dock, cx| {
dock.activate_panel(panel_index, cx);
dock.set_open(true, cx);
let panel = dock.panel_for_index(panel_index).unwrap();
panel.leader_updated(item, cx);
if transfer_focus {
panel.focus_handle(cx).focus(cx);
}
});
break;
}
}
}
@@ -3607,6 +3627,7 @@ impl Workspace {
let active_item_id = pane.active_item().map(|item| item.item_id());
(
pane.items()
.iter()
.filter_map(|item_handle| {
Some(SerializedItem {
kind: Arc::from(item_handle.serialized_item_kind()?),
@@ -4371,24 +4392,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)
}
}

View File

@@ -1545,6 +1545,7 @@ mod tests {
);
let pane_entries = pane
.items()
.iter()
.map(|i| i.project_path(cx).unwrap())
.collect::<Vec<_>>();
assert_eq!(pane_entries, &[file1, file2, file3]);
@@ -1879,7 +1880,7 @@ mod tests {
cx.read(|cx| {
let pane = workspace.read(cx).active_pane().read(cx);
let mut opened_buffer_paths = pane
.items()
.items().iter()
.map(|i| {
i.project_path(cx)
.expect("all excluded files that got open should have a path")