use anyhow::{anyhow, Result}; use call::ActiveCall; use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId}; use client::proto; use clock::ReplicaId; use collections::HashMap; use editor::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 std::{ any::{Any, TypeId}, sync::Arc, }; use util::ResultExt; use workspace::{ item::{FollowableItem, Item, ItemHandle}, register_followable_item, searchable::SearchableItemHandle, ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId, }; actions!(channel_view, [Deploy]); pub fn init(cx: &mut AppContext) { register_followable_item::(cx) } pub struct ChannelView { pub editor: ViewHandle, project: ModelHandle, channel_buffer: ModelHandle, 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| { let room_id = ActiveCall::global(cx) .read(cx) .room() .map(|room| room.read(cx).id()); ActiveCall::report_call_event_for_room( "open channel notes", room_id, Some(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 = workspace.app_state().channel_store.clone(); let markdown = workspace .app_state() .languages .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?; if let Some(markdown) = markdown.await.log_err() { channel_buffer.update(&mut cx, |buffer, cx| { buffer.buffer().update(cx, |buffer, cx| { buffer.set_language(Some(markdown), cx); }) }); } pane.update(&mut cx, |pane, cx| { pane.items_of_type::() .find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer) .unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, cx))) }) .ok_or_else(|| anyhow!("pane was dropped")) }) } pub fn new( project: ModelHandle, channel_buffer: ModelHandle, cx: &mut ViewContext, ) -> Self { let buffer = channel_buffer.read(cx).buffer(); let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); cx.subscribe(&project, Self::handle_project_event).detach(); cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) .detach(); let this = Self { editor, project, channel_buffer, remote_id: None, _editor_event_subscription, }; this.refresh_replica_id_map(cx); this } pub fn channel(&self, cx: &AppContext) -> Arc { self.channel_buffer.read(cx).channel() } fn handle_project_event( &mut self, _: ModelHandle, event: &project::Event, cx: &mut ViewContext, ) { match event { project::Event::RemoteIdChanged(_) => {} project::Event::DisconnectedFromHost => {} project::Event::Closed => {} project::Event::CollaboratorUpdated { .. } => {} project::Event::CollaboratorLeft(_) => {} project::Event::CollaboratorJoined(_) => {} _ => return, } self.refresh_replica_id_map(cx); } fn handle_channel_buffer_event( &mut self, _: ModelHandle, event: &ChannelBufferEvent, cx: &mut ViewContext, ) { match event { ChannelBufferEvent::CollaboratorsChanged => { self.refresh_replica_id_map(cx); } ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { editor.set_read_only(true); cx.notify(); }), } } /// Build a mapping of channel buffer replica ids to the corresponding /// replica ids in the current project. /// /// Using this mapping, a given user can be displayed with the same color /// in the channel buffer as in other files in the project. Users who are /// in the channel buffer but not the project will not have a color. fn refresh_replica_id_map(&self, cx: &mut ViewContext) { let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default(); let project = self.project.read(cx); let channel_buffer = self.channel_buffer.read(cx); project_replica_ids_by_channel_buffer_replica_id .insert(channel_buffer.replica_id(cx), project.replica_id()); project_replica_ids_by_channel_buffer_replica_id.extend( channel_buffer .collaborators() .iter() .filter_map(|channel_buffer_collaborator| { project .collaborators() .values() .find_map(|project_collaborator| { (project_collaborator.user_id == channel_buffer_collaborator.user_id) .then_some(( channel_buffer_collaborator.replica_id as ReplicaId, project_collaborator.replica_id, )) }) }), ); self.editor.update(cx, |editor, cx| { editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx) }); } } impl Entity for ChannelView { type Event = editor::Event; } impl View for ChannelView { fn ui_name() -> &'static str { "ChannelView" } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { ChildView::new(self.editor.as_any(), cx).into_any() } fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { if cx.is_self_focused() { cx.focus(self.editor.as_any()) } } } 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 } } fn tab_content( &self, _: Option, style: &theme::Tab, cx: &gpui::AppContext, ) -> AnyElement { let channel_name = &self.channel_buffer.read(cx).channel().name; let label = if self.channel_buffer.read(cx).is_connected() { format!("#{}", channel_name) } else { format!("#{} (disconnected)", channel_name) }; Label::new(label, style.label.to_owned()).into_any() } fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { Some(Self::new( self.project.clone(), self.channel_buffer.clone(), cx, )) } fn is_singleton(&self, _cx: &AppContext) -> bool { false } fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { self.editor .update(cx, |editor, cx| editor.navigate(data, cx)) } fn deactivated(&mut self, cx: &mut ViewContext) { self.editor .update(cx, |editor, cx| Item::deactivated(editor, 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 as_searchable(&self, _: &ViewHandle) -> Option> { Some(Box::new(self.editor.clone())) } fn show_toolbar(&self) -> bool { true } 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_state_proto(&self, cx: &AppContext) -> Option { let channel = self.channel_buffer.read(cx).channel(); Some(proto::view::Variant::ChannelView( proto::view::ChannelView { channel_id: channel.id, 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( 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!() }; let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx); Some(cx.spawn(|mut cx| async move { let this = open.await?; let task = this .update(&mut cx, |this, cx| { this.remote_id = Some(remote_id); 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"))?; if let Some(task) = task { task.await?; } Ok(this) })) } 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) } 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 set_leader_replica_id( &mut self, leader_replica_id: Option, cx: &mut ViewContext, ) { self.editor.update(cx, |editor, cx| { editor.set_leader_replica_id(leader_replica_id, cx) }) } fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { Editor::should_unfollow_on_event(event, cx) } fn is_project_item(&self, _cx: &AppContext) -> bool { false } }