channel_view.rs

  1use anyhow::{anyhow, Result};
  2use call::report_call_event_for_channel;
  3use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId};
  4use client::proto;
  5use clock::ReplicaId;
  6use collections::HashMap;
  7use editor::Editor;
  8use gpui::{
  9    actions,
 10    elements::{ChildView, Label},
 11    geometry::vector::Vector2F,
 12    AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
 13    ViewContext, ViewHandle,
 14};
 15use project::Project;
 16use std::any::{Any, TypeId};
 17use workspace::{
 18    item::{FollowableItem, Item, ItemHandle},
 19    register_followable_item,
 20    searchable::SearchableItemHandle,
 21    ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId,
 22};
 23
 24actions!(channel_view, [Deploy]);
 25
 26pub(crate) fn init(cx: &mut AppContext) {
 27    register_followable_item::<ChannelView>(cx)
 28}
 29
 30pub struct ChannelView {
 31    pub editor: ViewHandle<Editor>,
 32    project: ModelHandle<Project>,
 33    channel_buffer: ModelHandle<ChannelBuffer>,
 34    remote_id: Option<ViewId>,
 35    _editor_event_subscription: Subscription,
 36}
 37
 38impl ChannelView {
 39    pub fn deploy(channel_id: ChannelId, workspace: ViewHandle<Workspace>, cx: &mut AppContext) {
 40        let pane = workspace.read(cx).active_pane().clone();
 41        let channel_view = Self::open(channel_id, pane.clone(), workspace.clone(), cx);
 42        cx.spawn(|mut cx| async move {
 43            let channel_view = channel_view.await?;
 44            pane.update(&mut cx, |pane, cx| {
 45                report_call_event_for_channel(
 46                    "open channel notes",
 47                    channel_id,
 48                    &workspace.read(cx).app_state().client,
 49                    cx,
 50                );
 51                pane.add_item(Box::new(channel_view), true, true, None, cx);
 52            });
 53            anyhow::Ok(())
 54        })
 55        .detach();
 56    }
 57
 58    pub fn open(
 59        channel_id: ChannelId,
 60        pane: ViewHandle<Pane>,
 61        workspace: ViewHandle<Workspace>,
 62        cx: &mut AppContext,
 63    ) -> Task<Result<ViewHandle<Self>>> {
 64        let workspace = workspace.read(cx);
 65        let project = workspace.project().to_owned();
 66        let channel_store = workspace.app_state().channel_store.clone();
 67        let markdown = workspace
 68            .app_state()
 69            .languages
 70            .language_for_name("Markdown");
 71        let channel_buffer =
 72            channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
 73
 74        cx.spawn(|mut cx| async move {
 75            let channel_buffer = channel_buffer.await?;
 76
 77            let markdown = markdown.await?;
 78            channel_buffer.update(&mut cx, |buffer, cx| {
 79                buffer.buffer().update(cx, |buffer, cx| {
 80                    buffer.set_language(Some(markdown), cx);
 81                })
 82            });
 83
 84            pane.update(&mut cx, |pane, cx| {
 85                pane.items_of_type::<Self>()
 86                    .find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer)
 87                    .unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, cx)))
 88            })
 89            .ok_or_else(|| anyhow!("pane was dropped"))
 90        })
 91    }
 92
 93    pub fn new(
 94        project: ModelHandle<Project>,
 95        channel_buffer: ModelHandle<ChannelBuffer>,
 96        cx: &mut ViewContext<Self>,
 97    ) -> Self {
 98        let buffer = channel_buffer.read(cx).buffer();
 99        let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
100        let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
101
102        cx.subscribe(&project, Self::handle_project_event).detach();
103        cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
104            .detach();
105
106        let this = Self {
107            editor,
108            project,
109            channel_buffer,
110            remote_id: None,
111            _editor_event_subscription,
112        };
113        this.refresh_replica_id_map(cx);
114        this
115    }
116
117    fn handle_project_event(
118        &mut self,
119        _: ModelHandle<Project>,
120        event: &project::Event,
121        cx: &mut ViewContext<Self>,
122    ) {
123        match event {
124            project::Event::RemoteIdChanged(_) => {}
125            project::Event::DisconnectedFromHost => {}
126            project::Event::Closed => {}
127            project::Event::CollaboratorUpdated { .. } => {}
128            project::Event::CollaboratorLeft(_) => {}
129            project::Event::CollaboratorJoined(_) => {}
130            _ => return,
131        }
132        self.refresh_replica_id_map(cx);
133    }
134
135    fn handle_channel_buffer_event(
136        &mut self,
137        _: ModelHandle<ChannelBuffer>,
138        event: &ChannelBufferEvent,
139        cx: &mut ViewContext<Self>,
140    ) {
141        match event {
142            ChannelBufferEvent::CollaboratorsChanged => {
143                self.refresh_replica_id_map(cx);
144            }
145            ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
146                editor.set_read_only(true);
147                cx.notify();
148            }),
149        }
150    }
151
152    /// Build a mapping of channel buffer replica ids to the corresponding
153    /// replica ids in the current project.
154    ///
155    /// Using this mapping, a given user can be displayed with the same color
156    /// in the channel buffer as in other files in the project. Users who are
157    /// in the channel buffer but not the project will not have a color.
158    fn refresh_replica_id_map(&self, cx: &mut ViewContext<Self>) {
159        let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default();
160        let project = self.project.read(cx);
161        let channel_buffer = self.channel_buffer.read(cx);
162        project_replica_ids_by_channel_buffer_replica_id
163            .insert(channel_buffer.replica_id(cx), project.replica_id());
164        project_replica_ids_by_channel_buffer_replica_id.extend(
165            channel_buffer
166                .collaborators()
167                .iter()
168                .filter_map(|channel_buffer_collaborator| {
169                    project
170                        .collaborators()
171                        .values()
172                        .find_map(|project_collaborator| {
173                            (project_collaborator.user_id == channel_buffer_collaborator.user_id)
174                                .then_some((
175                                    channel_buffer_collaborator.replica_id as ReplicaId,
176                                    project_collaborator.replica_id,
177                                ))
178                        })
179                }),
180        );
181
182        self.editor.update(cx, |editor, cx| {
183            editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx)
184        });
185    }
186}
187
188impl Entity for ChannelView {
189    type Event = editor::Event;
190}
191
192impl View for ChannelView {
193    fn ui_name() -> &'static str {
194        "ChannelView"
195    }
196
197    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
198        ChildView::new(self.editor.as_any(), cx).into_any()
199    }
200
201    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
202        if cx.is_self_focused() {
203            cx.focus(self.editor.as_any())
204        }
205    }
206}
207
208impl Item for ChannelView {
209    fn act_as_type<'a>(
210        &'a self,
211        type_id: TypeId,
212        self_handle: &'a ViewHandle<Self>,
213        _: &'a AppContext,
214    ) -> Option<&'a AnyViewHandle> {
215        if type_id == TypeId::of::<Self>() {
216            Some(self_handle)
217        } else if type_id == TypeId::of::<Editor>() {
218            Some(&self.editor)
219        } else {
220            None
221        }
222    }
223
224    fn tab_content<V: 'static>(
225        &self,
226        _: Option<usize>,
227        style: &theme::Tab,
228        cx: &gpui::AppContext,
229    ) -> AnyElement<V> {
230        let channel_name = &self.channel_buffer.read(cx).channel().name;
231        let label = if self.channel_buffer.read(cx).is_connected() {
232            format!("#{}", channel_name)
233        } else {
234            format!("#{} (disconnected)", channel_name)
235        };
236        Label::new(label, style.label.to_owned()).into_any()
237    }
238
239    fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
240        Some(Self::new(
241            self.project.clone(),
242            self.channel_buffer.clone(),
243            cx,
244        ))
245    }
246
247    fn is_singleton(&self, _cx: &AppContext) -> bool {
248        false
249    }
250
251    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
252        self.editor
253            .update(cx, |editor, cx| editor.navigate(data, cx))
254    }
255
256    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
257        self.editor
258            .update(cx, |editor, cx| Item::deactivated(editor, cx))
259    }
260
261    fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
262        self.editor
263            .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
264    }
265
266    fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
267        Some(Box::new(self.editor.clone()))
268    }
269
270    fn show_toolbar(&self) -> bool {
271        true
272    }
273
274    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
275        self.editor.read(cx).pixel_position_of_cursor(cx)
276    }
277}
278
279impl FollowableItem for ChannelView {
280    fn remote_id(&self) -> Option<workspace::ViewId> {
281        self.remote_id
282    }
283
284    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
285        let channel = self.channel_buffer.read(cx).channel();
286        Some(proto::view::Variant::ChannelView(
287            proto::view::ChannelView {
288                channel_id: channel.id,
289                editor: if let Some(proto::view::Variant::Editor(proto)) =
290                    self.editor.read(cx).to_state_proto(cx)
291                {
292                    Some(proto)
293                } else {
294                    None
295                },
296            },
297        ))
298    }
299
300    fn from_state_proto(
301        pane: ViewHandle<workspace::Pane>,
302        workspace: ViewHandle<workspace::Workspace>,
303        remote_id: workspace::ViewId,
304        state: &mut Option<proto::view::Variant>,
305        cx: &mut AppContext,
306    ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
307        let Some(proto::view::Variant::ChannelView(_)) = state else {
308            return None;
309        };
310        let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
311            unreachable!()
312        };
313
314        let open = ChannelView::open(state.channel_id, pane, workspace, cx);
315
316        Some(cx.spawn(|mut cx| async move {
317            let this = open.await?;
318
319            let task = this
320                .update(&mut cx, |this, cx| {
321                    this.remote_id = Some(remote_id);
322
323                    if let Some(state) = state.editor {
324                        Some(this.editor.update(cx, |editor, cx| {
325                            editor.apply_update_proto(
326                                &this.project,
327                                proto::update_view::Variant::Editor(proto::update_view::Editor {
328                                    selections: state.selections,
329                                    pending_selection: state.pending_selection,
330                                    scroll_top_anchor: state.scroll_top_anchor,
331                                    scroll_x: state.scroll_x,
332                                    scroll_y: state.scroll_y,
333                                    ..Default::default()
334                                }),
335                                cx,
336                            )
337                        }))
338                    } else {
339                        None
340                    }
341                })
342                .ok_or_else(|| anyhow!("window was closed"))?;
343
344            if let Some(task) = task {
345                task.await?;
346            }
347
348            Ok(this)
349        }))
350    }
351
352    fn add_event_to_update_proto(
353        &self,
354        event: &Self::Event,
355        update: &mut Option<proto::update_view::Variant>,
356        cx: &AppContext,
357    ) -> bool {
358        self.editor
359            .read(cx)
360            .add_event_to_update_proto(event, update, cx)
361    }
362
363    fn apply_update_proto(
364        &mut self,
365        project: &ModelHandle<Project>,
366        message: proto::update_view::Variant,
367        cx: &mut ViewContext<Self>,
368    ) -> gpui::Task<anyhow::Result<()>> {
369        self.editor.update(cx, |editor, cx| {
370            editor.apply_update_proto(project, message, cx)
371        })
372    }
373
374    fn set_leader_replica_id(
375        &mut self,
376        leader_replica_id: Option<u16>,
377        cx: &mut ViewContext<Self>,
378    ) {
379        self.editor.update(cx, |editor, cx| {
380            editor.set_leader_replica_id(leader_replica_id, cx)
381        })
382    }
383
384    fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
385        Editor::should_unfollow_on_event(event, cx)
386    }
387}