channel_view.rs

  1use anyhow::{anyhow, Result};
  2use call::report_call_event_for_channel;
  3use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
  4use client::{
  5    proto::{self, PeerId},
  6    Collaborator, ParticipantIndex,
  7};
  8use collections::HashMap;
  9use editor::{CollaborationHub, Editor};
 10use gpui::{
 11    actions,
 12    elements::{ChildView, Label},
 13    geometry::vector::Vector2F,
 14    AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
 15    ViewContext, ViewHandle,
 16};
 17use project::Project;
 18use smallvec::SmallVec;
 19use std::{
 20    any::{Any, TypeId},
 21    sync::Arc,
 22};
 23use util::ResultExt;
 24use workspace::{
 25    item::{FollowableItem, Item, ItemEvent, ItemHandle},
 26    register_followable_item,
 27    searchable::SearchableItemHandle,
 28    ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
 29};
 30
 31actions!(channel_view, [Deploy]);
 32
 33pub fn init(cx: &mut AppContext) {
 34    register_followable_item::<ChannelView>(cx)
 35}
 36
 37pub struct ChannelView {
 38    pub editor: ViewHandle<Editor>,
 39    project: ModelHandle<Project>,
 40    channel_store: ModelHandle<ChannelStore>,
 41    channel_buffer: ModelHandle<ChannelBuffer>,
 42    remote_id: Option<ViewId>,
 43    _editor_event_subscription: Subscription,
 44}
 45
 46impl ChannelView {
 47    pub fn open(
 48        channel_id: ChannelId,
 49        workspace: ViewHandle<Workspace>,
 50        cx: &mut AppContext,
 51    ) -> Task<Result<ViewHandle<Self>>> {
 52        let pane = workspace.read(cx).active_pane().clone();
 53        let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
 54        cx.spawn(|mut cx| async move {
 55            let channel_view = channel_view.await?;
 56            pane.update(&mut cx, |pane, cx| {
 57                report_call_event_for_channel(
 58                    "open channel notes",
 59                    channel_id,
 60                    &workspace.read(cx).app_state().client,
 61                    cx,
 62                );
 63                pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
 64            });
 65            anyhow::Ok(channel_view)
 66        })
 67    }
 68
 69    pub fn open_in_pane(
 70        channel_id: ChannelId,
 71        pane: ViewHandle<Pane>,
 72        workspace: ViewHandle<Workspace>,
 73        cx: &mut AppContext,
 74    ) -> Task<Result<ViewHandle<Self>>> {
 75        let workspace = workspace.read(cx);
 76        let project = workspace.project().to_owned();
 77        let channel_store = ChannelStore::global(cx);
 78        let markdown = workspace
 79            .app_state()
 80            .languages
 81            .language_for_name("Markdown");
 82        let channel_buffer =
 83            channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
 84
 85        cx.spawn(|mut cx| async move {
 86            let channel_buffer = channel_buffer.await?;
 87
 88            if let Some(markdown) = markdown.await.log_err() {
 89                channel_buffer.update(&mut cx, |buffer, cx| {
 90                    buffer.buffer().update(cx, |buffer, cx| {
 91                        buffer.set_language(Some(markdown), cx);
 92                    })
 93                });
 94            }
 95
 96            pane.update(&mut cx, |pane, cx| {
 97                let buffer_id = channel_buffer.read(cx).remote_id(cx);
 98
 99                let existing_view = pane
100                    .items_of_type::<Self>()
101                    .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
102
103                // If this channel buffer is already open in this pane, just return it.
104                if let Some(existing_view) = existing_view.clone() {
105                    if existing_view.read(cx).channel_buffer == channel_buffer {
106                        return existing_view;
107                    }
108                }
109
110                let view = cx.add_view(|cx| {
111                    let mut this = Self::new(project, channel_store, channel_buffer, cx);
112                    this.acknowledge_buffer_version(cx);
113                    this
114                });
115
116                // If the pane contained a disconnected view for this channel buffer,
117                // replace that.
118                if let Some(existing_item) = existing_view {
119                    if let Some(ix) = pane.index_for_item(&existing_item) {
120                        pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
121                            .detach();
122                        pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
123                    }
124                }
125
126                view
127            })
128            .ok_or_else(|| anyhow!("pane was dropped"))
129        })
130    }
131
132    pub fn new(
133        project: ModelHandle<Project>,
134        channel_store: ModelHandle<ChannelStore>,
135        channel_buffer: ModelHandle<ChannelBuffer>,
136        cx: &mut ViewContext<Self>,
137    ) -> Self {
138        let buffer = channel_buffer.read(cx).buffer();
139        let editor = cx.add_view(|cx| {
140            let mut editor = Editor::for_buffer(buffer, None, cx);
141            editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
142                channel_buffer.clone(),
143            )));
144            editor.set_read_only(
145                !channel_buffer
146                    .read(cx)
147                    .channel(cx)
148                    .is_some_and(|c| c.can_edit_notes()),
149            );
150            editor
151        });
152        let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
153
154        cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
155            .detach();
156
157        Self {
158            editor,
159            project,
160            channel_store,
161            channel_buffer,
162            remote_id: None,
163            _editor_event_subscription,
164        }
165    }
166
167    pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
168        self.channel_buffer.read(cx).channel(cx)
169    }
170
171    fn handle_channel_buffer_event(
172        &mut self,
173        _: ModelHandle<ChannelBuffer>,
174        event: &ChannelBufferEvent,
175        cx: &mut ViewContext<Self>,
176    ) {
177        match event {
178            ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
179                editor.set_read_only(true);
180                cx.notify();
181            }),
182            ChannelBufferEvent::ChannelChanged => {
183                self.editor.update(cx, |editor, cx| {
184                    editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
185                    cx.emit(editor::Event::TitleChanged);
186                    cx.notify()
187                });
188            }
189            ChannelBufferEvent::BufferEdited => {
190                if cx.is_self_focused() || self.editor.is_focused(cx) {
191                    self.acknowledge_buffer_version(cx);
192                } else {
193                    self.channel_store.update(cx, |store, cx| {
194                        let channel_buffer = self.channel_buffer.read(cx);
195                        store.notes_changed(
196                            channel_buffer.channel_id,
197                            channel_buffer.epoch(),
198                            &channel_buffer.buffer().read(cx).version(),
199                            cx,
200                        )
201                    });
202                }
203            }
204            ChannelBufferEvent::CollaboratorsChanged => {}
205        }
206    }
207
208    fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
209        self.channel_store.update(cx, |store, cx| {
210            let channel_buffer = self.channel_buffer.read(cx);
211            store.acknowledge_notes_version(
212                channel_buffer.channel_id,
213                channel_buffer.epoch(),
214                &channel_buffer.buffer().read(cx).version(),
215                cx,
216            )
217        });
218        self.channel_buffer.update(cx, |buffer, cx| {
219            buffer.acknowledge_buffer_version(cx);
220        });
221    }
222}
223
224impl Entity for ChannelView {
225    type Event = editor::Event;
226}
227
228impl View for ChannelView {
229    fn ui_name() -> &'static str {
230        "ChannelView"
231    }
232
233    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
234        ChildView::new(self.editor.as_any(), cx).into_any()
235    }
236
237    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
238        if cx.is_self_focused() {
239            self.acknowledge_buffer_version(cx);
240            cx.focus(self.editor.as_any())
241        }
242    }
243}
244
245impl Item for ChannelView {
246    fn act_as_type<'a>(
247        &'a self,
248        type_id: TypeId,
249        self_handle: &'a ViewHandle<Self>,
250        _: &'a AppContext,
251    ) -> Option<&'a AnyViewHandle> {
252        if type_id == TypeId::of::<Self>() {
253            Some(self_handle)
254        } else if type_id == TypeId::of::<Editor>() {
255            Some(&self.editor)
256        } else {
257            None
258        }
259    }
260
261    fn tab_content<V: 'static>(
262        &self,
263        _: Option<usize>,
264        style: &theme::Tab,
265        cx: &gpui::AppContext,
266    ) -> AnyElement<V> {
267        let label = if let Some(channel) = self.channel(cx) {
268            match (
269                channel.can_edit_notes(),
270                self.channel_buffer.read(cx).is_connected(),
271            ) {
272                (true, true) => format!("#{}", channel.name),
273                (false, true) => format!("#{} (read-only)", channel.name),
274                (_, false) => format!("#{} (disconnected)", channel.name),
275            }
276        } else {
277            format!("channel notes (disconnected)")
278        };
279        Label::new(label, style.label.to_owned()).into_any()
280    }
281
282    fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
283        Some(Self::new(
284            self.project.clone(),
285            self.channel_store.clone(),
286            self.channel_buffer.clone(),
287            cx,
288        ))
289    }
290
291    fn is_singleton(&self, _cx: &AppContext) -> bool {
292        false
293    }
294
295    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
296        self.editor
297            .update(cx, |editor, cx| editor.navigate(data, cx))
298    }
299
300    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
301        self.editor
302            .update(cx, |editor, cx| Item::deactivated(editor, cx))
303    }
304
305    fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
306        self.editor
307            .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
308    }
309
310    fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
311        Some(Box::new(self.editor.clone()))
312    }
313
314    fn show_toolbar(&self) -> bool {
315        true
316    }
317
318    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
319        self.editor.read(cx).pixel_position_of_cursor(cx)
320    }
321
322    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
323        editor::Editor::to_item_events(event)
324    }
325}
326
327impl FollowableItem for ChannelView {
328    fn remote_id(&self) -> Option<workspace::ViewId> {
329        self.remote_id
330    }
331
332    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
333        let channel_buffer = self.channel_buffer.read(cx);
334        if !channel_buffer.is_connected() {
335            return None;
336        }
337
338        Some(proto::view::Variant::ChannelView(
339            proto::view::ChannelView {
340                channel_id: channel_buffer.channel_id,
341                editor: if let Some(proto::view::Variant::Editor(proto)) =
342                    self.editor.read(cx).to_state_proto(cx)
343                {
344                    Some(proto)
345                } else {
346                    None
347                },
348            },
349        ))
350    }
351
352    fn from_state_proto(
353        pane: ViewHandle<workspace::Pane>,
354        workspace: ViewHandle<workspace::Workspace>,
355        remote_id: workspace::ViewId,
356        state: &mut Option<proto::view::Variant>,
357        cx: &mut AppContext,
358    ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
359        let Some(proto::view::Variant::ChannelView(_)) = state else {
360            return None;
361        };
362        let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
363            unreachable!()
364        };
365
366        let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
367
368        Some(cx.spawn(|mut cx| async move {
369            let this = open.await?;
370
371            let task = this
372                .update(&mut cx, |this, cx| {
373                    this.remote_id = Some(remote_id);
374
375                    if let Some(state) = state.editor {
376                        Some(this.editor.update(cx, |editor, cx| {
377                            editor.apply_update_proto(
378                                &this.project,
379                                proto::update_view::Variant::Editor(proto::update_view::Editor {
380                                    selections: state.selections,
381                                    pending_selection: state.pending_selection,
382                                    scroll_top_anchor: state.scroll_top_anchor,
383                                    scroll_x: state.scroll_x,
384                                    scroll_y: state.scroll_y,
385                                    ..Default::default()
386                                }),
387                                cx,
388                            )
389                        }))
390                    } else {
391                        None
392                    }
393                })
394                .ok_or_else(|| anyhow!("window was closed"))?;
395
396            if let Some(task) = task {
397                task.await?;
398            }
399
400            Ok(this)
401        }))
402    }
403
404    fn add_event_to_update_proto(
405        &self,
406        event: &Self::Event,
407        update: &mut Option<proto::update_view::Variant>,
408        cx: &AppContext,
409    ) -> bool {
410        self.editor
411            .read(cx)
412            .add_event_to_update_proto(event, update, cx)
413    }
414
415    fn apply_update_proto(
416        &mut self,
417        project: &ModelHandle<Project>,
418        message: proto::update_view::Variant,
419        cx: &mut ViewContext<Self>,
420    ) -> gpui::Task<anyhow::Result<()>> {
421        self.editor.update(cx, |editor, cx| {
422            editor.apply_update_proto(project, message, cx)
423        })
424    }
425
426    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
427        self.editor.update(cx, |editor, cx| {
428            editor.set_leader_peer_id(leader_peer_id, cx)
429        })
430    }
431
432    fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
433        Editor::should_unfollow_on_event(event, cx)
434    }
435
436    fn is_project_item(&self, _cx: &AppContext) -> bool {
437        false
438    }
439}
440
441struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
442
443impl CollaborationHub for ChannelBufferCollaborationHub {
444    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
445        self.0.read(cx).collaborators()
446    }
447
448    fn user_participant_indices<'a>(
449        &self,
450        cx: &'a AppContext,
451    ) -> &'a HashMap<u64, ParticipantIndex> {
452        self.0.read(cx).user_store().read(cx).participant_indices()
453    }
454}