From bcdefb8ec84ce3f721c4f7f851b3dca6425a76fc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 15:36:26 -0800 Subject: [PATCH 1/4] Bring back channel notes --- crates/collab_ui2/src/channel_view.rs | 794 +++++++++++++------------- crates/collab_ui2/src/collab_panel.rs | 4 +- 2 files changed, 394 insertions(+), 404 deletions(-) diff --git a/crates/collab_ui2/src/channel_view.rs b/crates/collab_ui2/src/channel_view.rs index d2ffc0de57..8d2c037f9d 100644 --- a/crates/collab_ui2/src/channel_view.rs +++ b/crates/collab_ui2/src/channel_view.rs @@ -1,454 +1,444 @@ -// use anyhow::{anyhow, Result}; -// use call::report_call_event_for_channel; -// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore}; -// use client::{ -// proto::{self, PeerId}, -// Collaborator, ParticipantIndex, -// }; -// use collections::HashMap; -// use editor::{CollaborationHub, Editor}; -// use gpui::{ -// actions, -// elements::{ChildView, Label}, -// geometry::vector::Vector2F, -// AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View, -// ViewContext, ViewHandle, -// }; -// use project::Project; -// use smallvec::SmallVec; -// use std::{ -// any::{Any, TypeId}, -// sync::Arc, -// }; -// use util::ResultExt; -// use workspace::{ -// item::{FollowableItem, Item, ItemEvent, ItemHandle}, -// register_followable_item, -// searchable::SearchableItemHandle, -// ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, -// }; +use anyhow::Result; +use call::report_call_event_for_channel; +use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore}; +use client::{ + proto::{self, PeerId}, + Collaborator, ParticipantIndex, +}; +use collections::HashMap; +use editor::{CollaborationHub, Editor, EditorEvent}; +use gpui::{ + actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView, + IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext, + VisualContext as _, WindowContext, +}; +use project::Project; +use std::{ + any::{Any, TypeId}, + sync::Arc, +}; +use ui::Label; +use util::ResultExt; +use workspace::{ + item::{FollowableItem, Item, ItemEvent, ItemHandle}, + register_followable_item, + searchable::SearchableItemHandle, + ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, +}; -// actions!(channel_view, [Deploy]); +actions!(Deploy); -// pub fn init(cx: &mut AppContext) { -// register_followable_item::(cx) -// } +pub fn init(cx: &mut AppContext) { + register_followable_item::(cx) +} -// pub struct ChannelView { -// pub editor: ViewHandle, -// project: ModelHandle, -// channel_store: ModelHandle, -// channel_buffer: ModelHandle, -// remote_id: Option, -// _editor_event_subscription: Subscription, -// } +pub struct ChannelView { + pub editor: View, + project: Model, + channel_store: Model, + channel_buffer: Model, + remote_id: Option, + _editor_event_subscription: Subscription, +} -// impl ChannelView { -// pub fn open( -// channel_id: ChannelId, -// workspace: ViewHandle, -// cx: &mut AppContext, -// ) -> Task>> { -// let pane = workspace.read(cx).active_pane().clone(); -// let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx); -// cx.spawn(|mut cx| async move { -// let channel_view = channel_view.await?; -// pane.update(&mut cx, |pane, cx| { -// report_call_event_for_channel( -// "open channel notes", -// channel_id, -// &workspace.read(cx).app_state().client, -// cx, -// ); -// pane.add_item(Box::new(channel_view.clone()), true, true, None, cx); -// }); -// anyhow::Ok(channel_view) -// }) -// } +impl ChannelView { + pub fn open( + channel_id: ChannelId, + workspace: View, + cx: &mut WindowContext, + ) -> Task>> { + let pane = workspace.read(cx).active_pane().clone(); + let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx); + cx.spawn(|mut cx| async move { + let channel_view = channel_view.await?; + pane.update(&mut cx, |pane, cx| { + report_call_event_for_channel( + "open channel notes", + channel_id, + &workspace.read(cx).app_state().client, + cx, + ); + pane.add_item(Box::new(channel_view.clone()), true, true, None, cx); + })?; + anyhow::Ok(channel_view) + }) + } -// pub fn open_in_pane( -// channel_id: ChannelId, -// pane: ViewHandle, -// workspace: ViewHandle, -// cx: &mut AppContext, -// ) -> Task>> { -// 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)); + pub fn open_in_pane( + channel_id: ChannelId, + pane: View, + workspace: View, + cx: &mut WindowContext, + ) -> Task>> { + 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(); + cx.spawn(|mut cx| async move { + let channel_buffer = channel_buffer.await?; + let markdown = markdown.await.log_err(); -// channel_buffer.update(&mut cx, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.set_language_registry(language_registry); -// if let Some(markdown) = markdown { -// buffer.set_language(Some(markdown), cx); -// } -// }) -// }); + channel_buffer.update(&mut cx, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.set_language_registry(language_registry); + if let Some(markdown) = markdown { + buffer.set_language(Some(markdown), cx); + } + }) + })?; -// pane.update(&mut cx, |pane, cx| { -// let buffer_id = channel_buffer.read(cx).remote_id(cx); + pane.update(&mut cx, |pane, cx| { + let buffer_id = channel_buffer.read(cx).remote_id(cx); -// let existing_view = pane -// .items_of_type::() -// .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id); + let existing_view = pane + .items_of_type::() + .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id); -// // If this channel buffer is already open in this pane, just return it. -// if let Some(existing_view) = existing_view.clone() { -// if existing_view.read(cx).channel_buffer == channel_buffer { -// return existing_view; -// } -// } + // 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 { + return existing_view; + } + } -// let view = cx.add_view(|cx| { -// let mut this = Self::new(project, channel_store, channel_buffer, cx); -// this.acknowledge_buffer_version(cx); -// this -// }); + let view = cx.build_view(|cx| { + let mut this = Self::new(project, 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.id(), SaveIntent::Skip, cx) -// .detach(); -// pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx); -// } -// } + // 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); + } + } -// view -// }) -// .ok_or_else(|| anyhow!("pane was dropped")) -// }) -// } + view + }) + }) + } -// pub fn new( -// project: ModelHandle, -// channel_store: ModelHandle, -// channel_buffer: ModelHandle, -// cx: &mut ViewContext, -// ) -> Self { -// let buffer = channel_buffer.read(cx).buffer(); -// let editor = cx.add_view(|cx| { -// let mut editor = Editor::for_buffer(buffer, None, cx); -// editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( -// channel_buffer.clone(), -// ))); -// editor.set_read_only( -// !channel_buffer -// .read(cx) -// .channel(cx) -// .is_some_and(|c| c.can_edit_notes()), -// ); -// editor -// }); -// let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); + pub fn new( + project: Model, + channel_store: Model, + channel_buffer: Model, + cx: &mut ViewContext, + ) -> Self { + let buffer = channel_buffer.read(cx).buffer(); + let editor = cx.build_view(|cx| { + let mut editor = Editor::for_buffer(buffer, None, cx); + editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( + channel_buffer.clone(), + ))); + editor.set_read_only( + !channel_buffer + .read(cx) + .channel(cx) + .is_some_and(|c| c.can_edit_notes()), + ); + editor + }); + let _editor_event_subscription = + cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone())); -// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) -// .detach(); + cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) + .detach(); -// Self { -// editor, -// project, -// channel_store, -// channel_buffer, -// remote_id: None, -// _editor_event_subscription, -// } -// } + Self { + editor, + project, + channel_store, + channel_buffer, + remote_id: None, + _editor_event_subscription, + } + } -// pub fn channel(&self, cx: &AppContext) -> Option> { -// self.channel_buffer.read(cx).channel(cx) -// } + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_buffer.read(cx).channel(cx) + } -// fn handle_channel_buffer_event( -// &mut self, -// _: ModelHandle, -// event: &ChannelBufferEvent, -// cx: &mut ViewContext, -// ) { -// match event { -// ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { -// editor.set_read_only(true); -// cx.notify(); -// }), -// ChannelBufferEvent::ChannelChanged => { -// self.editor.update(cx, |editor, cx| { -// editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes())); -// cx.emit(editor::Event::TitleChanged); -// cx.notify() -// }); -// } -// ChannelBufferEvent::BufferEdited => { -// if cx.is_self_focused() || self.editor.is_focused(cx) { -// self.acknowledge_buffer_version(cx); -// } else { -// self.channel_store.update(cx, |store, cx| { -// let channel_buffer = self.channel_buffer.read(cx); -// store.notes_changed( -// channel_buffer.channel_id, -// channel_buffer.epoch(), -// &channel_buffer.buffer().read(cx).version(), -// cx, -// ) -// }); -// } -// } -// ChannelBufferEvent::CollaboratorsChanged => {} -// } -// } + fn handle_channel_buffer_event( + &mut self, + _: Model, + event: &ChannelBufferEvent, + cx: &mut ViewContext, + ) { + match event { + ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { + editor.set_read_only(true); + cx.notify(); + }), + ChannelBufferEvent::ChannelChanged => { + self.editor.update(cx, |editor, cx| { + editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes())); + cx.emit(editor::EditorEvent::TitleChanged); + cx.notify() + }); + } + ChannelBufferEvent::BufferEdited => { + if self.editor.read(cx).is_focused(cx) { + self.acknowledge_buffer_version(cx); + } else { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.notes_changed( + channel_buffer.channel_id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + } + } + ChannelBufferEvent::CollaboratorsChanged => {} + } + } -// fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) { -// self.channel_store.update(cx, |store, cx| { -// let channel_buffer = self.channel_buffer.read(cx); -// store.acknowledge_notes_version( -// channel_buffer.channel_id, -// channel_buffer.epoch(), -// &channel_buffer.buffer().read(cx).version(), -// cx, -// ) -// }); -// self.channel_buffer.update(cx, |buffer, cx| { -// buffer.acknowledge_buffer_version(cx); -// }); -// } -// } + fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext) { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.acknowledge_notes_version( + channel_buffer.channel_id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + self.channel_buffer.update(cx, |buffer, cx| { + buffer.acknowledge_buffer_version(cx); + }); + } +} -// impl Entity for ChannelView { -// type Event = editor::Event; -// } +impl EventEmitter for ChannelView {} -// impl View for ChannelView { -// fn ui_name() -> &'static str { -// "ChannelView" -// } +impl Render for ChannelView { + type Element = AnyView; -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// ChildView::new(self.editor.as_any(), cx).into_any() -// } + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + self.editor.clone().into() + } +} -// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { -// if cx.is_self_focused() { -// self.acknowledge_buffer_version(cx); -// cx.focus(self.editor.as_any()) -// } -// } -// } +impl FocusableView for ChannelView { + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.editor.read(cx).focus_handle(cx) + } +} -// impl Item for ChannelView { -// fn act_as_type<'a>( -// &'a self, -// type_id: TypeId, -// self_handle: &'a ViewHandle, -// _: &'a AppContext, -// ) -> Option<&'a AnyViewHandle> { -// if type_id == TypeId::of::() { -// Some(self_handle) -// } else if type_id == TypeId::of::() { -// Some(&self.editor) -// } else { -// None -// } -// } +impl Item for ChannelView { + type Event = EditorEvent; -// fn tab_content( -// &self, -// _: Option, -// style: &theme::Tab, -// cx: &gpui::AppContext, -// ) -> AnyElement { -// let label = if let Some(channel) = self.channel(cx) { -// match ( -// channel.can_edit_notes(), -// self.channel_buffer.read(cx).is_connected(), -// ) { -// (true, true) => format!("#{}", channel.name), -// (false, true) => format!("#{} (read-only)", channel.name), -// (_, false) => format!("#{} (disconnected)", channel.name), -// } -// } else { -// format!("channel notes (disconnected)") -// }; -// Label::new(label, style.label.to_owned()).into_any() -// } + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a View, + _: &'a AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.to_any()) + } else if type_id == TypeId::of::() { + Some(self.editor.to_any()) + } else { + None + } + } -// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { -// Some(Self::new( -// self.project.clone(), -// self.channel_store.clone(), -// self.channel_buffer.clone(), -// cx, -// )) -// } + fn tab_content(&self, _: Option, cx: &WindowContext) -> AnyElement { + let label = if let Some(channel) = self.channel(cx) { + match ( + channel.can_edit_notes(), + self.channel_buffer.read(cx).is_connected(), + ) { + (true, true) => format!("#{}", channel.name), + (false, true) => format!("#{} (read-only)", channel.name), + (_, false) => format!("#{} (disconnected)", channel.name), + } + } else { + format!("channel notes (disconnected)") + }; + Label::new(label).into_any_element() + } -// fn is_singleton(&self, _cx: &AppContext) -> bool { -// false -// } + fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option> { + Some(cx.build_view(|cx| { + Self::new( + self.project.clone(), + self.channel_store.clone(), + self.channel_buffer.clone(), + cx, + ) + })) + } -// fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { -// self.editor -// .update(cx, |editor, cx| editor.navigate(data, cx)) -// } + fn is_singleton(&self, _cx: &AppContext) -> bool { + false + } -// fn deactivated(&mut self, cx: &mut ViewContext) { -// self.editor -// .update(cx, |editor, cx| Item::deactivated(editor, cx)) -// } + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.editor + .update(cx, |editor, cx| editor.navigate(data, cx)) + } -// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { -// self.editor -// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) -// } + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::deactivated(editor, cx)) + } -// fn as_searchable(&self, _: &ViewHandle) -> Option> { -// Some(Box::new(self.editor.clone())) -// } + fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) + } -// fn show_toolbar(&self) -> bool { -// true -// } + fn as_searchable(&self, _: &View) -> Option> { + Some(Box::new(self.editor.clone())) + } -// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { -// self.editor.read(cx).pixel_position_of_cursor(cx) -// } + fn show_toolbar(&self) -> bool { + true + } -// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { -// editor::Editor::to_item_events(event) -// } -// } + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option> { + self.editor.read(cx).pixel_position_of_cursor(cx) + } -// impl FollowableItem for ChannelView { -// fn remote_id(&self) -> Option { -// self.remote_id -// } + fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { + Editor::to_item_events(event, f) + } +} -// fn to_state_proto(&self, cx: &AppContext) -> Option { -// let channel_buffer = self.channel_buffer.read(cx); -// if !channel_buffer.is_connected() { -// return None; -// } +impl FollowableItem for ChannelView { + fn remote_id(&self) -> Option { + self.remote_id + } -// Some(proto::view::Variant::ChannelView( -// proto::view::ChannelView { -// channel_id: channel_buffer.channel_id, -// editor: if let Some(proto::view::Variant::Editor(proto)) = -// self.editor.read(cx).to_state_proto(cx) -// { -// Some(proto) -// } else { -// None -// }, -// }, -// )) -// } + fn to_state_proto(&self, cx: &WindowContext) -> Option { + let channel_buffer = self.channel_buffer.read(cx); + if !channel_buffer.is_connected() { + return None; + } -// fn from_state_proto( -// pane: ViewHandle, -// workspace: ViewHandle, -// remote_id: workspace::ViewId, -// state: &mut Option, -// cx: &mut AppContext, -// ) -> Option>>> { -// let Some(proto::view::Variant::ChannelView(_)) = state else { -// return None; -// }; -// let Some(proto::view::Variant::ChannelView(state)) = state.take() else { -// unreachable!() -// }; + Some(proto::view::Variant::ChannelView( + proto::view::ChannelView { + channel_id: channel_buffer.channel_id, + editor: if let Some(proto::view::Variant::Editor(proto)) = + self.editor.read(cx).to_state_proto(cx) + { + Some(proto) + } else { + None + }, + }, + )) + } -// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx); + fn from_state_proto( + pane: View, + workspace: View, + remote_id: workspace::ViewId, + state: &mut Option, + cx: &mut WindowContext, + ) -> Option>>> { + let Some(proto::view::Variant::ChannelView(_)) = state else { + return None; + }; + let Some(proto::view::Variant::ChannelView(state)) = state.take() else { + unreachable!() + }; -// Some(cx.spawn(|mut cx| async move { -// let this = open.await?; + let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx); -// let task = this -// .update(&mut cx, |this, cx| { -// this.remote_id = Some(remote_id); + Some(cx.spawn(|mut cx| async move { + let this = open.await?; -// if let Some(state) = state.editor { -// Some(this.editor.update(cx, |editor, cx| { -// editor.apply_update_proto( -// &this.project, -// proto::update_view::Variant::Editor(proto::update_view::Editor { -// selections: state.selections, -// pending_selection: state.pending_selection, -// scroll_top_anchor: state.scroll_top_anchor, -// scroll_x: state.scroll_x, -// scroll_y: state.scroll_y, -// ..Default::default() -// }), -// cx, -// ) -// })) -// } else { -// None -// } -// }) -// .ok_or_else(|| anyhow!("window was closed"))?; + let task = this.update(&mut cx, |this, cx| { + this.remote_id = Some(remote_id); -// if let Some(task) = task { -// task.await?; -// } + if let Some(state) = state.editor { + Some(this.editor.update(cx, |editor, cx| { + editor.apply_update_proto( + &this.project, + proto::update_view::Variant::Editor(proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }), + cx, + ) + })) + } else { + None + } + })?; -// Ok(this) -// })) -// } + if let Some(task) = task { + task.await?; + } -// fn add_event_to_update_proto( -// &self, -// event: &Self::Event, -// update: &mut Option, -// cx: &AppContext, -// ) -> bool { -// self.editor -// .read(cx) -// .add_event_to_update_proto(event, update, cx) -// } + Ok(this) + })) + } -// fn apply_update_proto( -// &mut self, -// project: &ModelHandle, -// message: proto::update_view::Variant, -// cx: &mut ViewContext, -// ) -> gpui::Task> { -// self.editor.update(cx, |editor, cx| { -// editor.apply_update_proto(project, message, cx) -// }) -// } + fn add_event_to_update_proto( + &self, + event: &EditorEvent, + update: &mut Option, + cx: &WindowContext, + ) -> bool { + self.editor + .read(cx) + .add_event_to_update_proto(event, update, cx) + } -// fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { -// self.editor.update(cx, |editor, cx| { -// editor.set_leader_peer_id(leader_peer_id, cx) -// }) -// } + fn apply_update_proto( + &mut self, + project: &Model, + message: proto::update_view::Variant, + cx: &mut ViewContext, + ) -> gpui::Task> { + self.editor.update(cx, |editor, cx| { + editor.apply_update_proto(project, message, cx) + }) + } -// fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { -// Editor::should_unfollow_on_event(event, cx) -// } + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| { + editor.set_leader_peer_id(leader_peer_id, cx) + }) + } -// fn is_project_item(&self, _cx: &AppContext) -> bool { -// false -// } -// } + fn is_project_item(&self, _cx: &WindowContext) -> bool { + false + } -// struct ChannelBufferCollaborationHub(ModelHandle); + fn to_follow_event(event: &Self::Event) -> Option { + Editor::to_follow_event(event) + } +} -// impl CollaborationHub for ChannelBufferCollaborationHub { -// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { -// self.0.read(cx).collaborators() -// } +struct ChannelBufferCollaborationHub(Model); -// fn user_participant_indices<'a>( -// &self, -// cx: &'a AppContext, -// ) -> &'a HashMap { -// self.0.read(cx).user_store().read(cx).participant_indices() -// } -// } +impl CollaborationHub for ChannelBufferCollaborationHub { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.0.read(cx).collaborators() + } + + fn user_participant_indices<'a>( + &self, + cx: &'a AppContext, + ) -> &'a HashMap { + self.0.read(cx).user_store().read(cx).participant_indices() + } +} diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 8c0a92ad52..272f02bb49 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -191,6 +191,7 @@ use workspace::{ Workspace, }; +use crate::channel_view::ChannelView; use crate::{face_pile::FacePile, CollaborationPanelSettings}; use self::channel_modal::ChannelModal; @@ -1935,8 +1936,7 @@ impl CollabPanel { fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade() { - todo!(); - // ChannelView::open(action.channel_id, workspace, cx).detach(); + ChannelView::open(channel_id, workspace, cx).detach(); } } From 1ec81e02da8b7d24648186ad3c58d02583ed5ffb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 15:44:07 -0800 Subject: [PATCH 2/4] Allow opening channel notes from the channel list --- crates/collab_ui2/src/collab_panel.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 272f02bb49..bfef193cf7 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -2619,6 +2619,9 @@ impl CollabPanel { } else { Color::Muted }) + .on_click(cx.listener(move |this, _, cx| { + this.open_channel_notes(channel_id, cx) + })) .tooltip(|cx| { Tooltip::text("Open channel notes", cx) }), From e9dcca7712569f7573610c62a0d5376c09c54d6e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 15:59:54 -0800 Subject: [PATCH 3/4] Initialize channel notes to register the view as followable --- crates/collab_ui2/src/collab_ui.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/collab_ui2/src/collab_ui.rs b/crates/collab_ui2/src/collab_ui.rs index efd3ff8692..fba76932fc 100644 --- a/crates/collab_ui2/src/collab_ui.rs +++ b/crates/collab_ui2/src/collab_ui.rs @@ -33,6 +33,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { // vcs_menu::init(cx); collab_titlebar_item::init(cx); collab_panel::init(cx); + channel_view::init(cx); // chat_panel::init(cx); notifications::init(&app_state, cx); From 6bbb1642b86545da18119d1e729145cfbc2e33b1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Dec 2023 12:18:48 -0800 Subject: [PATCH 4/4] Fix propagation of active item to followers Enable channel buffer integration tests. --- Cargo.lock | 2 +- crates/collab2/Cargo.toml | 2 +- .../collab2/src/tests/channel_buffer_tests.rs | 1756 +++++++++-------- crates/collab2/src/tests/test_server.rs | 14 +- crates/gpui2/src/app/test_context.rs | 4 + crates/workspace2/src/workspace2.rs | 25 +- 6 files changed, 906 insertions(+), 897 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b5af36a19..773c2a3df5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1830,7 +1830,7 @@ dependencies = [ "clap 3.2.25", "client2", "clock", - "collab_ui", + "collab_ui2", "collections", "ctor", "dashmap", diff --git a/crates/collab2/Cargo.toml b/crates/collab2/Cargo.toml index b8e6a45b06..06443afdbc 100644 --- a/crates/collab2/Cargo.toml +++ b/crates/collab2/Cargo.toml @@ -81,7 +81,7 @@ settings = { package = "settings2", path = "../settings2", features = ["test-sup theme = { package = "theme2", path = "../theme2" } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } -collab_ui = { path = "../collab_ui", features = ["test-support"] } +collab_ui = { path = "../collab_ui2", package = "collab_ui2", features = ["test-support"] } async-trait.workspace = true pretty_assertions.workspace = true diff --git a/crates/collab2/src/tests/channel_buffer_tests.rs b/crates/collab2/src/tests/channel_buffer_tests.rs index 63057cbd41..b0af360fd5 100644 --- a/crates/collab2/src/tests/channel_buffer_tests.rs +++ b/crates/collab2/src/tests/channel_buffer_tests.rs @@ -1,875 +1,881 @@ -//todo(partially ported) -// use std::ops::Range; - -// use crate::{ -// rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, -// tests::TestServer, -// }; -// use client::{Collaborator, ParticipantIndex, UserId}; -// use collections::HashMap; -// use editor::{Anchor, Editor, ToOffset}; -// use futures::future; -// use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext}; -// use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; - -// #[gpui::test] -// async fn test_core_channel_buffers( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; - -// let channel_id = server -// .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) -// .await; - -// // Client A joins the channel buffer -// let channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// // Client A edits the buffer -// let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); -// buffer_a.update(cx_a, |buffer, cx| { -// buffer.edit([(0..0, "hello world")], None, cx) -// }); -// buffer_a.update(cx_a, |buffer, cx| { -// buffer.edit([(5..5, ", cruel")], None, cx) -// }); -// buffer_a.update(cx_a, |buffer, cx| { -// buffer.edit([(0..5, "goodbye")], None, cx) -// }); -// buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); -// assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); -// executor.run_until_parked(); - -// // Client B joins the channel buffer -// let channel_buffer_b = client_b -// .channel_store() -// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// channel_buffer_b.read_with(cx_b, |buffer, _| { -// assert_collaborators( -// buffer.collaborators(), -// &[client_a.user_id(), client_b.user_id()], -// ); -// }); - -// // Client B sees the correct text, and then edits it -// let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); -// assert_eq!( -// buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), -// buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) -// ); -// assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); -// buffer_b.update(cx_b, |buffer, cx| { -// buffer.edit([(7..12, "beautiful")], None, cx) -// }); - -// // Both A and B see the new edit -// executor.run_until_parked(); -// assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); -// assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); - -// // Client A closes the channel buffer. -// cx_a.update(|_| drop(channel_buffer_a)); -// executor.run_until_parked(); - -// // Client B sees that client A is gone from the channel buffer. -// channel_buffer_b.read_with(cx_b, |buffer, _| { -// assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); -// }); - -// // Client A rejoins the channel buffer -// let _channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// executor.run_until_parked(); - -// // Sanity test, make sure we saw A rejoining -// channel_buffer_b.read_with(cx_b, |buffer, _| { -// assert_collaborators( -// &buffer.collaborators(), -// &[client_a.user_id(), client_b.user_id()], -// ); -// }); - -// // Client A loses connection. -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); -// executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// // Client B observes A disconnect -// channel_buffer_b.read_with(cx_b, |buffer, _| { -// assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); -// }); - -// // TODO: -// // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects -// // - Test interaction with channel deletion while buffer is open -// } - -// // todo!("collab_ui") -// // #[gpui::test] -// // async fn test_channel_notes_participant_indices( -// // executor: BackgroundExecutor, -// // mut cx_a: &mut TestAppContext, -// // mut cx_b: &mut TestAppContext, -// // cx_c: &mut TestAppContext, -// // ) { -// // let mut server = TestServer::start(&executor).await; -// // let client_a = server.create_client(cx_a, "user_a").await; -// // let client_b = server.create_client(cx_b, "user_b").await; -// // let client_c = server.create_client(cx_c, "user_c").await; - -// // let active_call_a = cx_a.read(ActiveCall::global); -// // let active_call_b = cx_b.read(ActiveCall::global); - -// // cx_a.update(editor::init); -// // cx_b.update(editor::init); -// // cx_c.update(editor::init); - -// // let channel_id = server -// // .make_channel( -// // "the-channel", -// // None, -// // (&client_a, cx_a), -// // &mut [(&client_b, cx_b), (&client_c, cx_c)], -// // ) -// // .await; - -// // client_a -// // .fs() -// // .insert_tree("/root", json!({"file.txt": "123"})) -// // .await; -// // let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await; -// // let project_b = client_b.build_empty_local_project(cx_b); -// // let project_c = client_c.build_empty_local_project(cx_c); -// // let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// // let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); - -// // // Clients A, B, and C open the channel notes -// // let channel_view_a = cx_a -// // .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx)) -// // .await -// // .unwrap(); -// // let channel_view_b = cx_b -// // .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) -// // .await -// // .unwrap(); -// // let channel_view_c = cx_c -// // .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx)) -// // .await -// // .unwrap(); - -// // // Clients A, B, and C all insert and select some text -// // channel_view_a.update(cx_a, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // editor.insert("a", cx); -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![0..1]); -// // }); -// // }); -// // }); -// // executor.run_until_parked(); -// // channel_view_b.update(cx_b, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // editor.move_down(&Default::default(), cx); -// // editor.insert("b", cx); -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![1..2]); -// // }); -// // }); -// // }); -// // executor.run_until_parked(); -// // channel_view_c.update(cx_c, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // editor.move_down(&Default::default(), cx); -// // editor.insert("c", cx); -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![2..3]); -// // }); -// // }); -// // }); - -// // // Client A sees clients B and C without assigned colors, because they aren't -// // // in a call together. -// // executor.run_until_parked(); -// // channel_view_a.update(cx_a, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx); -// // }); -// // }); - -// // // Clients A and B join the same call. -// // for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] { -// // call.update(*cx, |call, cx| call.join_channel(channel_id, cx)) -// // .await -// // .unwrap(); -// // } - -// // // Clients A and B see each other with two different assigned colors. Client C -// // // still doesn't have a color. -// // executor.run_until_parked(); -// // channel_view_a.update(cx_a, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // assert_remote_selections( -// // editor, -// // &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)], -// // cx, -// // ); -// // }); -// // }); -// // channel_view_b.update(cx_b, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // assert_remote_selections( -// // editor, -// // &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)], -// // cx, -// // ); -// // }); -// // }); - -// // // Client A shares a project, and client B joins. -// // let project_id = active_call_a -// // .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) -// // .await -// // .unwrap(); -// // let project_b = client_b.build_remote_project(project_id, cx_b).await; -// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); - -// // // Clients A and B open the same file. -// // let editor_a = workspace_a -// // .update(cx_a, |workspace, cx| { -// // workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) -// // }) -// // .await -// // .unwrap() -// // .downcast::() -// // .unwrap(); -// // let editor_b = workspace_b -// // .update(cx_b, |workspace, cx| { -// // workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) -// // }) -// // .await -// // .unwrap() -// // .downcast::() -// // .unwrap(); - -// // editor_a.update(cx_a, |editor, cx| { -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![0..1]); -// // }); -// // }); -// // editor_b.update(cx_b, |editor, cx| { -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![2..3]); -// // }); -// // }); -// // executor.run_until_parked(); - -// // // Clients A and B see each other with the same colors as in the channel notes. -// // editor_a.update(cx_a, |editor, cx| { -// // assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx); -// // }); -// // editor_b.update(cx_b, |editor, cx| { -// // assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx); -// // }); -// // } - -// #[track_caller] -// fn assert_remote_selections( -// editor: &mut Editor, -// expected_selections: &[(Option, Range)], -// cx: &mut ViewContext, -// ) { -// let snapshot = editor.snapshot(cx); -// let range = Anchor::min()..Anchor::max(); -// let remote_selections = snapshot -// .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) -// .map(|s| { -// let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); -// let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); -// (s.participant_index, start..end) -// }) -// .collect::>(); -// assert_eq!( -// remote_selections, expected_selections, -// "incorrect remote selections" -// ); -// } - -// #[gpui::test] -// async fn test_multiple_handles_to_channel_buffer( -// deterministic: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(deterministic.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; - -// let channel_id = server -// .make_channel("the-channel", None, (&client_a, cx_a), &mut []) -// .await; - -// let channel_buffer_1 = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); -// let channel_buffer_2 = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); -// let channel_buffer_3 = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); - -// // All concurrent tasks for opening a channel buffer return the same model handle. -// let (channel_buffer, channel_buffer_2, channel_buffer_3) = -// future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) -// .await -// .unwrap(); -// let channel_buffer_model_id = channel_buffer.entity_id(); -// assert_eq!(channel_buffer, channel_buffer_2); -// assert_eq!(channel_buffer, channel_buffer_3); - -// channel_buffer.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "hello")], None, cx); -// }) -// }); -// deterministic.run_until_parked(); - -// cx_a.update(|_| { -// drop(channel_buffer); -// drop(channel_buffer_2); -// drop(channel_buffer_3); -// }); -// deterministic.run_until_parked(); - -// // The channel buffer can be reopened after dropping it. -// let channel_buffer = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id); -// channel_buffer.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, _| { -// assert_eq!(buffer.text(), "hello"); -// }) -// }); -// } - -// #[gpui::test] -// async fn test_channel_buffer_disconnect( -// deterministic: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(deterministic.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; - -// let channel_id = server -// .make_channel( -// "the-channel", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b)], -// ) -// .await; - -// let channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// let channel_buffer_b = client_b -// .channel_store() -// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); -// deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// channel_buffer_a.update(cx_a, |buffer, cx| { -// assert_eq!(buffer.channel(cx).unwrap().name, "the-channel"); -// assert!(!buffer.is_connected()); -// }); - -// deterministic.run_until_parked(); - -// server.allow_connections(); -// deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// deterministic.run_until_parked(); - -// client_a -// .channel_store() -// .update(cx_a, |channel_store, _| { -// channel_store.remove_channel(channel_id) -// }) -// .await -// .unwrap(); -// deterministic.run_until_parked(); - -// // Channel buffer observed the deletion -// channel_buffer_b.update(cx_b, |buffer, cx| { -// assert!(buffer.channel(cx).is_none()); -// assert!(!buffer.is_connected()); -// }); -// } - -// #[gpui::test] -// async fn test_rejoin_channel_buffer( -// deterministic: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(deterministic.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; - -// let channel_id = server -// .make_channel( -// "the-channel", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b)], -// ) -// .await; - -// let channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// let channel_buffer_b = client_b -// .channel_store() -// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// channel_buffer_a.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "1")], None, cx); -// }) -// }); -// deterministic.run_until_parked(); - -// // Client A disconnects. -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); - -// // Both clients make an edit. -// channel_buffer_a.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(1..1, "2")], None, cx); -// }) -// }); -// channel_buffer_b.update(cx_b, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "0")], None, cx); -// }) -// }); - -// // Both clients see their own edit. -// deterministic.run_until_parked(); -// channel_buffer_a.read_with(cx_a, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "12"); -// }); -// channel_buffer_b.read_with(cx_b, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "01"); -// }); - -// // Client A reconnects. Both clients see each other's edits, and see -// // the same collaborators. -// server.allow_connections(); -// deterministic.advance_clock(RECEIVE_TIMEOUT); -// channel_buffer_a.read_with(cx_a, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "012"); -// }); -// channel_buffer_b.read_with(cx_b, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "012"); -// }); - -// channel_buffer_a.read_with(cx_a, |buffer_a, _| { -// channel_buffer_b.read_with(cx_b, |buffer_b, _| { -// assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); -// }); -// }); -// } - -// #[gpui::test] -// async fn test_channel_buffers_and_server_restarts( -// deterministic: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// cx_c: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(deterministic.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// let client_c = server.create_client(cx_c, "user_c").await; - -// let channel_id = server -// .make_channel( -// "the-channel", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b), (&client_c, cx_c)], -// ) -// .await; - -// let channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// let channel_buffer_b = client_b -// .channel_store() -// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// let _channel_buffer_c = client_c -// .channel_store() -// .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// channel_buffer_a.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "1")], None, cx); -// }) -// }); -// deterministic.run_until_parked(); - -// // Client C can't reconnect. -// client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - -// // Server stops. -// server.reset().await; -// deterministic.advance_clock(RECEIVE_TIMEOUT); - -// // While the server is down, both clients make an edit. -// channel_buffer_a.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(1..1, "2")], None, cx); -// }) -// }); -// channel_buffer_b.update(cx_b, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "0")], None, cx); -// }) -// }); - -// // Server restarts. -// server.start().await.unwrap(); -// deterministic.advance_clock(CLEANUP_TIMEOUT); - -// // Clients reconnects. Clients A and B see each other's edits, and see -// // that client C has disconnected. -// channel_buffer_a.read_with(cx_a, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "012"); -// }); -// channel_buffer_b.read_with(cx_b, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "012"); -// }); - -// channel_buffer_a.read_with(cx_a, |buffer_a, _| { -// channel_buffer_b.read_with(cx_b, |buffer_b, _| { -// assert_collaborators( -// buffer_a.collaborators(), -// &[client_a.user_id(), client_b.user_id()], -// ); -// assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); -// }); -// }); -// } - -// //todo!(collab_ui) -// // #[gpui::test(iterations = 10)] -// // async fn test_following_to_channel_notes_without_a_shared_project( -// // deterministic: BackgroundExecutor, -// // mut cx_a: &mut TestAppContext, -// // mut cx_b: &mut TestAppContext, -// // mut cx_c: &mut TestAppContext, -// // ) { -// // let mut server = TestServer::start(&deterministic).await; -// // let client_a = server.create_client(cx_a, "user_a").await; -// // let client_b = server.create_client(cx_b, "user_b").await; - -// // let client_c = server.create_client(cx_c, "user_c").await; - -// // cx_a.update(editor::init); -// // cx_b.update(editor::init); -// // cx_c.update(editor::init); -// // cx_a.update(collab_ui::channel_view::init); -// // cx_b.update(collab_ui::channel_view::init); -// // cx_c.update(collab_ui::channel_view::init); - -// // let channel_1_id = server -// // .make_channel( -// // "channel-1", -// // None, -// // (&client_a, cx_a), -// // &mut [(&client_b, cx_b), (&client_c, cx_c)], -// // ) -// // .await; -// // let channel_2_id = server -// // .make_channel( -// // "channel-2", -// // None, -// // (&client_a, cx_a), -// // &mut [(&client_b, cx_b), (&client_c, cx_c)], -// // ) -// // .await; - -// // // Clients A, B, and C join a channel. -// // let active_call_a = cx_a.read(ActiveCall::global); -// // let active_call_b = cx_b.read(ActiveCall::global); -// // let active_call_c = cx_c.read(ActiveCall::global); -// // for (call, cx) in [ -// // (&active_call_a, &mut cx_a), -// // (&active_call_b, &mut cx_b), -// // (&active_call_c, &mut cx_c), -// // ] { -// // call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx)) -// // .await -// // .unwrap(); -// // } -// // deterministic.run_until_parked(); - -// // // Clients A, B, and C all open their own unshared projects. -// // client_a.fs().insert_tree("/a", json!({})).await; -// // client_b.fs().insert_tree("/b", json!({})).await; -// // client_c.fs().insert_tree("/c", json!({})).await; -// // let (project_a, _) = client_a.build_local_project("/a", cx_a).await; -// // let (project_b, _) = client_b.build_local_project("/b", cx_b).await; -// // let (project_c, _) = client_b.build_local_project("/c", cx_c).await; -// // let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); -// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// // let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); - -// // active_call_a -// // .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// // .await -// // .unwrap(); - -// // // Client A opens the notes for channel 1. -// // let channel_view_1_a = cx_a -// // .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx)) -// // .await -// // .unwrap(); -// // channel_view_1_a.update(cx_a, |notes, cx| { -// // assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); -// // notes.editor.update(cx, |editor, cx| { -// // editor.insert("Hello from A.", cx); -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![3..4]); -// // }); -// // }); -// // }); - -// // // Client B follows client A. -// // workspace_b -// // .update(cx_b, |workspace, cx| { -// // workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() -// // }) -// // .await -// // .unwrap(); - -// // // Client B is taken to the notes for channel 1, with the same -// // // text selected as client A. -// // deterministic.run_until_parked(); -// // let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| { -// // assert_eq!( -// // workspace.leader_for_pane(workspace.active_pane()), -// // Some(client_a.peer_id().unwrap()) -// // ); -// // workspace -// // .active_item(cx) -// // .expect("no active item") -// // .downcast::() -// // .expect("active item is not a channel view") -// // }); -// // channel_view_1_b.read_with(cx_b, |notes, cx| { -// // assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); -// // let editor = notes.editor.read(cx); -// // assert_eq!(editor.text(cx), "Hello from A."); -// // assert_eq!(editor.selections.ranges::(cx), &[3..4]); -// // }); - -// // // Client A opens the notes for channel 2. -// // let channel_view_2_a = cx_a -// // .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx)) -// // .await -// // .unwrap(); -// // channel_view_2_a.read_with(cx_a, |notes, cx| { -// // assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); -// // }); - -// // // Client B is taken to the notes for channel 2. -// // deterministic.run_until_parked(); -// // let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| { -// // assert_eq!( -// // workspace.leader_for_pane(workspace.active_pane()), -// // Some(client_a.peer_id().unwrap()) -// // ); -// // workspace -// // .active_item(cx) -// // .expect("no active item") -// // .downcast::() -// // .expect("active item is not a channel view") -// // }); -// // channel_view_2_b.read_with(cx_b, |notes, cx| { -// // assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); -// // }); -// // } - -// //todo!(collab_ui) -// // #[gpui::test] -// // async fn test_channel_buffer_changes( -// // deterministic: BackgroundExecutor, -// // cx_a: &mut TestAppContext, -// // cx_b: &mut TestAppContext, -// // ) { -// // let mut server = TestServer::start(&deterministic).await; -// // let client_a = server.create_client(cx_a, "user_a").await; -// // let client_b = server.create_client(cx_b, "user_b").await; - -// // let channel_id = server -// // .make_channel( -// // "the-channel", -// // None, -// // (&client_a, cx_a), -// // &mut [(&client_b, cx_b)], -// // ) -// // .await; - -// // let channel_buffer_a = client_a -// // .channel_store() -// // .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// // .await -// // .unwrap(); - -// // // Client A makes an edit, and client B should see that the note has changed. -// // channel_buffer_a.update(cx_a, |buffer, cx| { -// // buffer.buffer().update(cx, |buffer, cx| { -// // buffer.edit([(0..0, "1")], None, cx); -// // }) -// // }); -// // deterministic.run_until_parked(); - -// // let has_buffer_changed = cx_b.update(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(has_buffer_changed); - -// // // Opening the buffer should clear the changed flag. -// // let project_b = client_b.build_empty_local_project(cx_b); -// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// // let channel_view_b = cx_b -// // .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) -// // .await -// // .unwrap(); -// // deterministic.run_until_parked(); - -// // let has_buffer_changed = cx_b.update(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(!has_buffer_changed); - -// // // Editing the channel while the buffer is open should not show that the buffer has changed. -// // channel_buffer_a.update(cx_a, |buffer, cx| { -// // buffer.buffer().update(cx, |buffer, cx| { -// // buffer.edit([(0..0, "2")], None, cx); -// // }) -// // }); -// // deterministic.run_until_parked(); - -// // let has_buffer_changed = cx_b.read(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(!has_buffer_changed); - -// // deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL); - -// // // Test that the server is tracking things correctly, and we retain our 'not changed' -// // // state across a disconnect -// // server.simulate_long_connection_interruption(client_b.peer_id().unwrap(), &deterministic); -// // let has_buffer_changed = cx_b.read(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(!has_buffer_changed); - -// // // Closing the buffer should re-enable change tracking -// // cx_b.update(|cx| { -// // workspace_b.update(cx, |workspace, cx| { -// // workspace.close_all_items_and_panes(&Default::default(), cx) -// // }); - -// // drop(channel_view_b) -// // }); - -// // deterministic.run_until_parked(); - -// // channel_buffer_a.update(cx_a, |buffer, cx| { -// // buffer.buffer().update(cx, |buffer, cx| { -// // buffer.edit([(0..0, "3")], None, cx); -// // }) -// // }); -// // deterministic.run_until_parked(); - -// // let has_buffer_changed = cx_b.read(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(has_buffer_changed); -// // } - -// #[track_caller] -// fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { -// let mut user_ids = collaborators -// .values() -// .map(|collaborator| collaborator.user_id) -// .collect::>(); -// user_ids.sort(); -// assert_eq!( -// user_ids, -// ids.into_iter().map(|id| id.unwrap()).collect::>() -// ); -// } - -// fn buffer_text(channel_buffer: &Model, cx: &mut TestAppContext) -> String { -// channel_buffer.read_with(cx, |buffer, _| buffer.text()) -// } +use crate::{ + rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, + tests::TestServer, +}; +use call::ActiveCall; +use channel::ACKNOWLEDGE_DEBOUNCE_INTERVAL; +use client::{Collaborator, ParticipantIndex, UserId}; +use collab_ui::channel_view::ChannelView; +use collections::HashMap; +use editor::{Anchor, Editor, ToOffset}; +use futures::future; +use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext}; +use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; +use serde_json::json; +use std::ops::Range; + +#[gpui::test] +async fn test_core_channel_buffers( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + // Client A joins the channel buffer + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + // Client A edits the buffer + let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..0, "hello world")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(5..5, ", cruel")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..5, "goodbye")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); + executor.run_until_parked(); + + // Client B joins the channel buffer + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + buffer.collaborators(), + &[client_a.user_id(), client_b.user_id()], + ); + }); + + // Client B sees the correct text, and then edits it + let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); + assert_eq!( + buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), + buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) + ); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); + buffer_b.update(cx_b, |buffer, cx| { + buffer.edit([(7..12, "beautiful")], None, cx) + }); + + // Both A and B see the new edit + executor.run_until_parked(); + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); + + // Client A closes the channel buffer. + cx_a.update(|_| drop(channel_buffer_a)); + executor.run_until_parked(); + + // Client B sees that client A is gone from the channel buffer. + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); + + // Client A rejoins the channel buffer + let _channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + executor.run_until_parked(); + + // Sanity test, make sure we saw A rejoining + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + &buffer.collaborators(), + &[client_a.user_id(), client_b.user_id()], + ); + }); + + // Client A loses connection. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + // Client B observes A disconnect + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); + + // TODO: + // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects + // - Test interaction with channel deletion while buffer is open +} + +#[gpui::test] +async fn test_channel_notes_participant_indices( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + cx_c.update(editor::init); + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + client_a + .fs() + .insert_tree("/root", json!({"file.txt": "123"})) + .await; + let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await; + let project_b = client_b.build_empty_local_project(cx_b); + let project_c = client_c.build_empty_local_project(cx_c); + + let (workspace_a, mut cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, mut cx_b) = client_b.build_workspace(&project_b, cx_b); + let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); + + // Clients A, B, and C open the channel notes + let channel_view_a = cx_a + .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx)) + .await + .unwrap(); + let channel_view_b = cx_b + .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) + .await + .unwrap(); + let channel_view_c = cx_c + .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx)) + .await + .unwrap(); + + // Clients A, B, and C all insert and select some text + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.insert("a", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![0..1]); + }); + }); + }); + executor.run_until_parked(); + channel_view_b.update(cx_b, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.move_down(&Default::default(), cx); + editor.insert("b", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![1..2]); + }); + }); + }); + executor.run_until_parked(); + channel_view_c.update(cx_c, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.move_down(&Default::default(), cx); + editor.insert("c", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![2..3]); + }); + }); + }); + + // Client A sees clients B and C without assigned colors, because they aren't + // in a call together. + executor.run_until_parked(); + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx); + }); + }); + + // Clients A and B join the same call. + for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] { + call.update(*cx, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + } + + // Clients A and B see each other with two different assigned colors. Client C + // still doesn't have a color. + executor.run_until_parked(); + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections( + editor, + &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)], + cx, + ); + }); + }); + channel_view_b.update(cx_b, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections( + editor, + &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)], + cx, + ); + }); + }); + + // Client A shares a project, and client B joins. + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.build_remote_project(project_id, cx_b).await; + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + // Clients A and B open the same file. + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + editor_a.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![0..1]); + }); + }); + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![2..3]); + }); + }); + executor.run_until_parked(); + + // Clients A and B see each other with the same colors as in the channel notes. + editor_a.update(cx_a, |editor, cx| { + assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx); + }); + editor_b.update(cx_b, |editor, cx| { + assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx); + }); +} + +#[track_caller] +fn assert_remote_selections( + editor: &mut Editor, + expected_selections: &[(Option, Range)], + cx: &mut ViewContext, +) { + let snapshot = editor.snapshot(cx); + let range = Anchor::min()..Anchor::max(); + let remote_selections = snapshot + .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) + .map(|s| { + let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); + let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); + (s.participant_index, start..end) + }) + .collect::>(); + assert_eq!( + remote_selections, expected_selections, + "incorrect remote selections" + ); +} + +#[gpui::test] +async fn test_multiple_handles_to_channel_buffer( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + + let channel_id = server + .make_channel("the-channel", None, (&client_a, cx_a), &mut []) + .await; + + let channel_buffer_1 = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); + let channel_buffer_2 = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); + let channel_buffer_3 = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); + + // All concurrent tasks for opening a channel buffer return the same model handle. + let (channel_buffer, channel_buffer_2, channel_buffer_3) = + future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) + .await + .unwrap(); + let channel_buffer_model_id = channel_buffer.entity_id(); + assert_eq!(channel_buffer, channel_buffer_2); + assert_eq!(channel_buffer, channel_buffer_3); + + channel_buffer.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "hello")], None, cx); + }) + }); + deterministic.run_until_parked(); + + cx_a.update(|_| { + drop(channel_buffer); + drop(channel_buffer_2); + drop(channel_buffer_3); + }); + deterministic.run_until_parked(); + + // The channel buffer can be reopened after dropping it. + let channel_buffer = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id); + channel_buffer.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, _| { + assert_eq!(buffer.text(), "hello"); + }) + }); +} + +#[gpui::test] +async fn test_channel_buffer_disconnect( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + channel_buffer_a.update(cx_a, |buffer, cx| { + assert_eq!(buffer.channel(cx).unwrap().name, "the-channel"); + assert!(!buffer.is_connected()); + }); + + deterministic.run_until_parked(); + + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + deterministic.run_until_parked(); + + client_a + .channel_store() + .update(cx_a, |channel_store, _| { + channel_store.remove_channel(channel_id) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Channel buffer observed the deletion + channel_buffer_b.update(cx_b, |buffer, cx| { + assert!(buffer.channel(cx).is_none()); + assert!(!buffer.is_connected()); + }); +} + +#[gpui::test] +async fn test_rejoin_channel_buffer( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "1")], None, cx); + }) + }); + deterministic.run_until_parked(); + + // Client A disconnects. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + + // Both clients make an edit. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(1..1, "2")], None, cx); + }) + }); + channel_buffer_b.update(cx_b, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "0")], None, cx); + }) + }); + + // Both clients see their own edit. + deterministic.run_until_parked(); + channel_buffer_a.read_with(cx_a, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "12"); + }); + channel_buffer_b.read_with(cx_b, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "01"); + }); + + // Client A reconnects. Both clients see each other's edits, and see + // the same collaborators. + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT); + channel_buffer_a.read_with(cx_a, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + channel_buffer_b.read_with(cx_b, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + + channel_buffer_a.read_with(cx_a, |buffer_a, _| { + channel_buffer_b.read_with(cx_b, |buffer_b, _| { + assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); + }); + }); +} + +#[gpui::test] +async fn test_channel_buffers_and_server_restarts( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + let _channel_buffer_c = client_c + .channel_store() + .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "1")], None, cx); + }) + }); + deterministic.run_until_parked(); + + // Client C can't reconnect. + client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); + + // Server stops. + server.reset().await; + deterministic.advance_clock(RECEIVE_TIMEOUT); + + // While the server is down, both clients make an edit. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(1..1, "2")], None, cx); + }) + }); + channel_buffer_b.update(cx_b, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "0")], None, cx); + }) + }); + + // Server restarts. + server.start().await.unwrap(); + deterministic.advance_clock(CLEANUP_TIMEOUT); + + // Clients reconnects. Clients A and B see each other's edits, and see + // that client C has disconnected. + channel_buffer_a.read_with(cx_a, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + channel_buffer_b.read_with(cx_b, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + + channel_buffer_a.read_with(cx_a, |buffer_a, _| { + channel_buffer_b.read_with(cx_b, |buffer_b, _| { + assert_collaborators( + buffer_a.collaborators(), + &[client_a.user_id(), client_b.user_id()], + ); + assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); + }); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_following_to_channel_notes_without_a_shared_project( + deterministic: BackgroundExecutor, + mut cx_a: &mut TestAppContext, + mut cx_b: &mut TestAppContext, + mut cx_c: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let client_c = server.create_client(cx_c, "user_c").await; + + cx_a.update(editor::init); + cx_b.update(editor::init); + cx_c.update(editor::init); + cx_a.update(collab_ui::channel_view::init); + cx_b.update(collab_ui::channel_view::init); + cx_c.update(collab_ui::channel_view::init); + + let channel_1_id = server + .make_channel( + "channel-1", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + let channel_2_id = server + .make_channel( + "channel-2", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + // Clients A, B, and C join a channel. + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); + for (call, cx) in [ + (&active_call_a, &mut cx_a), + (&active_call_b, &mut cx_b), + (&active_call_c, &mut cx_c), + ] { + call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx)) + .await + .unwrap(); + } + deterministic.run_until_parked(); + + // Clients A, B, and C all open their own unshared projects. + client_a.fs().insert_tree("/a", json!({})).await; + client_b.fs().insert_tree("/b", json!({})).await; + client_c.fs().insert_tree("/c", json!({})).await; + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; + let (project_b, _) = client_b.build_local_project("/b", cx_b).await; + let (project_c, _) = client_b.build_local_project("/c", cx_c).await; + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c); + + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + // Client A opens the notes for channel 1. + let channel_view_1_a = cx_a + .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx)) + .await + .unwrap(); + channel_view_1_a.update(cx_a, |notes, cx| { + assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); + notes.editor.update(cx, |editor, cx| { + editor.insert("Hello from A.", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![3..4]); + }); + }); + }); + + // Client B follows client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() + }) + .await + .unwrap(); + + // Client B is taken to the notes for channel 1, with the same + // text selected as client A. + deterministic.run_until_parked(); + let channel_view_1_b = workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a.peer_id().unwrap()) + ); + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item is not a channel view") + }); + channel_view_1_b.update(cx_b, |notes, cx| { + assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); + let editor = notes.editor.read(cx); + assert_eq!(editor.text(cx), "Hello from A."); + assert_eq!(editor.selections.ranges::(cx), &[3..4]); + }); + + // Client A opens the notes for channel 2. + eprintln!("opening -------------------->"); + + let channel_view_2_a = cx_a + .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx)) + .await + .unwrap(); + channel_view_2_a.update(cx_a, |notes, cx| { + assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); + }); + + // Client B is taken to the notes for channel 2. + deterministic.run_until_parked(); + + eprintln!("opening <--------------------"); + + let channel_view_2_b = workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a.peer_id().unwrap()) + ); + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item is not a channel view") + }); + channel_view_2_b.update(cx_b, |notes, cx| { + assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); + }); +} + +#[gpui::test] +async fn test_channel_buffer_changes( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + // Client A makes an edit, and client B should see that the note has changed. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "1")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.update(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(has_buffer_changed); + + // Opening the buffer should clear the changed flag. + let project_b = client_b.build_empty_local_project(cx_b); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let channel_view_b = cx_b + .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.update(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(!has_buffer_changed); + + // Editing the channel while the buffer is open should not show that the buffer has changed. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "2")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(!has_buffer_changed); + + deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL); + + // Test that the server is tracking things correctly, and we retain our 'not changed' + // state across a disconnect + server + .simulate_long_connection_interruption(client_b.peer_id().unwrap(), deterministic.clone()); + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(!has_buffer_changed); + + // Closing the buffer should re-enable change tracking + cx_b.update(|cx| { + workspace_b.update(cx, |workspace, cx| { + workspace.close_all_items_and_panes(&Default::default(), cx) + }); + + drop(channel_view_b) + }); + + deterministic.run_until_parked(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "3")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(has_buffer_changed); +} + +#[track_caller] +fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { + let mut user_ids = collaborators + .values() + .map(|collaborator| collaborator.user_id) + .collect::>(); + user_ids.sort(); + assert_eq!( + user_ids, + ids.into_iter().map(|id| id.unwrap()).collect::>() + ); +} + +fn buffer_text(channel_buffer: &Model, cx: &mut TestAppContext) -> String { + channel_buffer.read_with(cx, |buffer, _| buffer.text()) +} diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index 6bb57e11ab..f751736971 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -13,7 +13,7 @@ use client::{ use collections::{HashMap, HashSet}; use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; -use gpui::{BackgroundExecutor, Context, Model, TestAppContext, WindowHandle}; +use gpui::{BackgroundExecutor, Context, Model, TestAppContext, View, VisualTestContext}; use language::LanguageRegistry; use node_runtime::FakeNodeRuntime; @@ -602,14 +602,12 @@ impl TestClient { .unwrap() } - //todo(workspace) - #[allow(dead_code)] - pub fn build_workspace( - &self, + pub fn build_workspace<'a>( + &'a self, project: &Model, - cx: &mut TestAppContext, - ) -> WindowHandle { - cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) + cx: &'a mut TestAppContext, + ) -> (View, &'a mut VisualTestContext) { + cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index cbd70e52ff..9f0c7e6aca 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -545,6 +545,10 @@ pub struct VisualTestContext<'a> { } impl<'a> VisualTestContext<'a> { + pub fn update(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R { + self.cx.update_window(self.window, |_, cx| f(cx)).unwrap() + } + pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self { Self { cx, window } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 9c886839c9..d5583be0bc 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -2077,6 +2077,7 @@ impl Workspace { } if &pane == self.active_pane() { self.active_item_path_changed(cx); + self.update_active_view_for_followers(cx); } } pane::Event::ChangeItemTitle => { @@ -2756,18 +2757,18 @@ impl Workspace { fn update_active_view_for_followers(&mut self, cx: &mut ViewContext) { let mut is_project_item = true; let mut update = proto::UpdateActiveView::default(); - if self.active_pane.read(cx).has_focus(cx) { - let item = self - .active_item(cx) - .and_then(|item| item.to_followable_item_handle(cx)); - if let Some(item) = item { - is_project_item = item.is_project_item(cx); - update = proto::UpdateActiveView { - id: item - .remote_id(&self.app_state.client, cx) - .map(|id| id.to_proto()), - leader_id: self.leader_for_pane(&self.active_pane), - }; + + if let Some(item) = self.active_item(cx) { + if item.focus_handle(cx).contains_focused(cx) { + if let Some(item) = item.to_followable_item_handle(cx) { + is_project_item = item.is_project_item(cx); + update = proto::UpdateActiveView { + id: item + .remote_id(&self.app_state.client, cx) + .map(|id| id.to_proto()), + leader_id: self.leader_for_pane(&self.active_pane), + }; + } } }