channel_view.rs

  1use anyhow::Result;
  2use call::report_call_event_for_channel;
  3use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelStore};
  4use client::{
  5    proto::{self, PeerId},
  6    ChannelId, Collaborator, ParticipantIndex,
  7};
  8use collections::HashMap;
  9use editor::{
 10    display_map::ToDisplayPoint, scroll::Autoscroll, CollaborationHub, DisplayPoint, Editor,
 11    EditorEvent,
 12};
 13use gpui::{
 14    actions, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter, FocusableView, Model,
 15    Pixels, Point, Render, Subscription, Task, View, ViewContext, VisualContext as _, WeakView,
 16    WindowContext,
 17};
 18use project::Project;
 19use rpc::proto::ChannelVisibility;
 20use std::{
 21    any::{Any, TypeId},
 22    sync::Arc,
 23};
 24use ui::prelude::*;
 25use util::ResultExt;
 26use workspace::item::TabContentParams;
 27use workspace::{item::Dedup, notifications::NotificationId};
 28use workspace::{
 29    item::{FollowableItem, Item, ItemEvent, ItemHandle},
 30    searchable::SearchableItemHandle,
 31    ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
 32};
 33
 34actions!(collab, [CopyLink]);
 35
 36pub fn init(cx: &mut AppContext) {
 37    workspace::FollowableViewRegistry::register::<ChannelView>(cx)
 38}
 39
 40pub struct ChannelView {
 41    pub editor: View<Editor>,
 42    workspace: WeakView<Workspace>,
 43    project: Model<Project>,
 44    channel_store: Model<ChannelStore>,
 45    channel_buffer: Model<ChannelBuffer>,
 46    remote_id: Option<ViewId>,
 47    _editor_event_subscription: Subscription,
 48    _reparse_subscription: Option<Subscription>,
 49}
 50
 51impl ChannelView {
 52    pub fn open(
 53        channel_id: ChannelId,
 54        link_position: Option<String>,
 55        workspace: View<Workspace>,
 56        cx: &mut WindowContext,
 57    ) -> Task<Result<View<Self>>> {
 58        let pane = workspace.read(cx).active_pane().clone();
 59        let channel_view = Self::open_in_pane(
 60            channel_id,
 61            link_position,
 62            pane.clone(),
 63            workspace.clone(),
 64            cx,
 65        );
 66        cx.spawn(|mut cx| async move {
 67            let channel_view = channel_view.await?;
 68            pane.update(&mut cx, |pane, cx| {
 69                report_call_event_for_channel(
 70                    "open channel notes",
 71                    channel_id,
 72                    &workspace.read(cx).app_state().client,
 73                    cx,
 74                );
 75                pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
 76            })?;
 77            anyhow::Ok(channel_view)
 78        })
 79    }
 80
 81    pub fn open_in_pane(
 82        channel_id: ChannelId,
 83        link_position: Option<String>,
 84        pane: View<Pane>,
 85        workspace: View<Workspace>,
 86        cx: &mut WindowContext,
 87    ) -> Task<Result<View<Self>>> {
 88        let channel_view = Self::load(channel_id, workspace, cx);
 89        cx.spawn(|mut cx| async move {
 90            let channel_view = channel_view.await?;
 91
 92            pane.update(&mut cx, |pane, cx| {
 93                let buffer_id = channel_view.read(cx).channel_buffer.read(cx).remote_id(cx);
 94
 95                let existing_view = pane
 96                    .items_of_type::<Self>()
 97                    .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
 98
 99                // If this channel buffer is already open in this pane, just return it.
100                if let Some(existing_view) = existing_view.clone() {
101                    if existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer
102                    {
103                        if let Some(link_position) = link_position {
104                            existing_view.update(cx, |channel_view, cx| {
105                                channel_view.focus_position_from_link(link_position, true, cx)
106                            });
107                        }
108                        return existing_view;
109                    }
110                }
111
112                // If the pane contained a disconnected view for this channel buffer,
113                // replace that.
114                if let Some(existing_item) = existing_view {
115                    if let Some(ix) = pane.index_for_item(&existing_item) {
116                        pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
117                            .detach();
118                        pane.add_item(Box::new(channel_view.clone()), true, true, Some(ix), cx);
119                    }
120                }
121
122                if let Some(link_position) = link_position {
123                    channel_view.update(cx, |channel_view, cx| {
124                        channel_view.focus_position_from_link(link_position, true, cx)
125                    });
126                }
127
128                channel_view
129            })
130        })
131    }
132
133    pub fn load(
134        channel_id: ChannelId,
135        workspace: View<Workspace>,
136        cx: &mut WindowContext,
137    ) -> Task<Result<View<Self>>> {
138        let weak_workspace = workspace.downgrade();
139        let workspace = workspace.read(cx);
140        let project = workspace.project().to_owned();
141        let channel_store = ChannelStore::global(cx);
142        let language_registry = workspace.app_state().languages.clone();
143        let markdown = language_registry.language_for_name("Markdown");
144        let channel_buffer =
145            channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
146
147        cx.spawn(|mut cx| async move {
148            let channel_buffer = channel_buffer.await?;
149            let markdown = markdown.await.log_err();
150
151            channel_buffer.update(&mut cx, |channel_buffer, cx| {
152                channel_buffer.buffer().update(cx, |buffer, cx| {
153                    buffer.set_language_registry(language_registry);
154                    let Some(markdown) = markdown else {
155                        return;
156                    };
157                    buffer.set_language(Some(markdown), cx);
158                })
159            })?;
160
161            cx.new_view(|cx| {
162                let mut this =
163                    Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
164                this.acknowledge_buffer_version(cx);
165                this
166            })
167        })
168    }
169
170    pub fn new(
171        project: Model<Project>,
172        workspace: WeakView<Workspace>,
173        channel_store: Model<ChannelStore>,
174        channel_buffer: Model<ChannelBuffer>,
175        cx: &mut ViewContext<Self>,
176    ) -> Self {
177        let buffer = channel_buffer.read(cx).buffer();
178        let this = cx.view().downgrade();
179        let editor = cx.new_view(|cx| {
180            let mut editor = Editor::for_buffer(buffer, None, cx);
181            editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
182                channel_buffer.clone(),
183            )));
184            editor.set_custom_context_menu(move |_, position, cx| {
185                let this = this.clone();
186                Some(ui::ContextMenu::build(cx, move |menu, _| {
187                    menu.entry("Copy link to section", None, move |cx| {
188                        this.update(cx, |this, cx| this.copy_link_for_position(position, cx))
189                            .ok();
190                    })
191                }))
192            });
193            editor
194        });
195        let _editor_event_subscription =
196            cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
197
198        cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
199            .detach();
200
201        Self {
202            editor,
203            workspace,
204            project,
205            channel_store,
206            channel_buffer,
207            remote_id: None,
208            _editor_event_subscription,
209            _reparse_subscription: None,
210        }
211    }
212
213    fn focus_position_from_link(
214        &mut self,
215        position: String,
216        first_attempt: bool,
217        cx: &mut ViewContext<Self>,
218    ) {
219        let position = Channel::slug(&position).to_lowercase();
220        let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
221
222        if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
223            if let Some(item) = outline
224                .items
225                .iter()
226                .find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
227            {
228                self.editor.update(cx, |editor, cx| {
229                    editor.change_selections(Some(Autoscroll::focused()), cx, |s| {
230                        s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)])
231                    })
232                });
233                return;
234            }
235        }
236
237        if !first_attempt {
238            return;
239        }
240        self._reparse_subscription = Some(cx.subscribe(
241            &self.editor,
242            move |this, _, e: &EditorEvent, cx| {
243                match e {
244                    EditorEvent::Reparsed(_) => {
245                        this.focus_position_from_link(position.clone(), false, cx);
246                        this._reparse_subscription.take();
247                    }
248                    EditorEvent::Edited { .. } | EditorEvent::SelectionsChanged { local: true } => {
249                        this._reparse_subscription.take();
250                    }
251                    _ => {}
252                };
253            },
254        ));
255    }
256
257    fn copy_link(&mut self, _: &CopyLink, cx: &mut ViewContext<Self>) {
258        let position = self
259            .editor
260            .update(cx, |editor, cx| editor.selections.newest_display(cx).start);
261        self.copy_link_for_position(position, cx)
262    }
263
264    fn copy_link_for_position(&self, position: DisplayPoint, cx: &mut ViewContext<Self>) {
265        let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
266
267        let mut closest_heading = None;
268
269        if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
270            for item in outline.items {
271                if item.range.start.to_display_point(&snapshot) > position {
272                    break;
273                }
274                closest_heading = Some(item);
275            }
276        }
277
278        let Some(channel) = self.channel(cx) else {
279            return;
280        };
281
282        let link = channel.notes_link(closest_heading.map(|heading| heading.text), cx);
283        cx.write_to_clipboard(ClipboardItem::new_string(link));
284        self.workspace
285            .update(cx, |workspace, cx| {
286                struct CopyLinkForPositionToast;
287
288                workspace.show_toast(
289                    Toast::new(
290                        NotificationId::unique::<CopyLinkForPositionToast>(),
291                        "Link copied to clipboard",
292                    ),
293                    cx,
294                );
295            })
296            .ok();
297    }
298
299    pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
300        self.channel_buffer.read(cx).channel(cx)
301    }
302
303    fn handle_channel_buffer_event(
304        &mut self,
305        _: Model<ChannelBuffer>,
306        event: &ChannelBufferEvent,
307        cx: &mut ViewContext<Self>,
308    ) {
309        match event {
310            ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
311                editor.set_read_only(true);
312                cx.notify();
313            }),
314            ChannelBufferEvent::ChannelChanged => {
315                self.editor.update(cx, |_, cx| {
316                    cx.emit(editor::EditorEvent::TitleChanged);
317                    cx.notify()
318                });
319            }
320            ChannelBufferEvent::BufferEdited => {
321                if self.editor.read(cx).is_focused(cx) {
322                    self.acknowledge_buffer_version(cx);
323                } else {
324                    self.channel_store.update(cx, |store, cx| {
325                        let channel_buffer = self.channel_buffer.read(cx);
326                        store.update_latest_notes_version(
327                            channel_buffer.channel_id,
328                            channel_buffer.epoch(),
329                            &channel_buffer.buffer().read(cx).version(),
330                            cx,
331                        )
332                    });
333                }
334            }
335            ChannelBufferEvent::CollaboratorsChanged => {}
336        }
337    }
338
339    fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
340        self.channel_store.update(cx, |store, cx| {
341            let channel_buffer = self.channel_buffer.read(cx);
342            store.acknowledge_notes_version(
343                channel_buffer.channel_id,
344                channel_buffer.epoch(),
345                &channel_buffer.buffer().read(cx).version(),
346                cx,
347            )
348        });
349        self.channel_buffer.update(cx, |buffer, cx| {
350            buffer.acknowledge_buffer_version(cx);
351        });
352    }
353}
354
355impl EventEmitter<EditorEvent> for ChannelView {}
356
357impl Render for ChannelView {
358    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
359        div()
360            .size_full()
361            .on_action(cx.listener(Self::copy_link))
362            .child(self.editor.clone())
363    }
364}
365
366impl FocusableView for ChannelView {
367    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
368        self.editor.read(cx).focus_handle(cx)
369    }
370}
371
372impl Item for ChannelView {
373    type Event = EditorEvent;
374
375    fn act_as_type<'a>(
376        &'a self,
377        type_id: TypeId,
378        self_handle: &'a View<Self>,
379        _: &'a AppContext,
380    ) -> Option<AnyView> {
381        if type_id == TypeId::of::<Self>() {
382            Some(self_handle.to_any())
383        } else if type_id == TypeId::of::<Editor>() {
384            Some(self.editor.to_any())
385        } else {
386            None
387        }
388    }
389
390    fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> {
391        let channel = self.channel(cx)?;
392        let icon = match channel.visibility {
393            ChannelVisibility::Public => IconName::Public,
394            ChannelVisibility::Members => IconName::Hash,
395        };
396
397        Some(Icon::new(icon))
398    }
399
400    fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> gpui::AnyElement {
401        let (channel_name, status) = if let Some(channel) = self.channel(cx) {
402            let status = match (
403                self.channel_buffer.read(cx).buffer().read(cx).read_only(),
404                self.channel_buffer.read(cx).is_connected(),
405            ) {
406                (false, true) => None,
407                (true, true) => Some("read-only"),
408                (_, false) => Some("disconnected"),
409            };
410
411            (channel.name.clone(), status)
412        } else {
413            ("<unknown>".into(), Some("disconnected"))
414        };
415
416        h_flex()
417            .gap_2()
418            .child(
419                Label::new(channel_name)
420                    .color(params.text_color())
421                    .italic(params.preview),
422            )
423            .when_some(status, |element, status| {
424                element.child(
425                    Label::new(status)
426                        .size(LabelSize::XSmall)
427                        .color(Color::Muted),
428                )
429            })
430            .into_any_element()
431    }
432
433    fn telemetry_event_text(&self) -> Option<&'static str> {
434        None
435    }
436
437    fn clone_on_split(
438        &self,
439        _: Option<WorkspaceId>,
440        cx: &mut ViewContext<Self>,
441    ) -> Option<View<Self>> {
442        Some(cx.new_view(|cx| {
443            Self::new(
444                self.project.clone(),
445                self.workspace.clone(),
446                self.channel_store.clone(),
447                self.channel_buffer.clone(),
448                cx,
449            )
450        }))
451    }
452
453    fn is_singleton(&self, _cx: &AppContext) -> bool {
454        false
455    }
456
457    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
458        self.editor
459            .update(cx, |editor, cx| editor.navigate(data, cx))
460    }
461
462    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
463        self.editor.update(cx, Item::deactivated)
464    }
465
466    fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
467        self.editor
468            .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
469    }
470
471    fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
472        Some(Box::new(self.editor.clone()))
473    }
474
475    fn show_toolbar(&self) -> bool {
476        true
477    }
478
479    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
480        self.editor.read(cx).pixel_position_of_cursor(cx)
481    }
482
483    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
484        Editor::to_item_events(event, f)
485    }
486}
487
488impl FollowableItem for ChannelView {
489    fn remote_id(&self) -> Option<workspace::ViewId> {
490        self.remote_id
491    }
492
493    fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
494        let channel_buffer = self.channel_buffer.read(cx);
495        if !channel_buffer.is_connected() {
496            return None;
497        }
498
499        Some(proto::view::Variant::ChannelView(
500            proto::view::ChannelView {
501                channel_id: channel_buffer.channel_id.0,
502                editor: if let Some(proto::view::Variant::Editor(proto)) =
503                    self.editor.read(cx).to_state_proto(cx)
504                {
505                    Some(proto)
506                } else {
507                    None
508                },
509            },
510        ))
511    }
512
513    fn from_state_proto(
514        workspace: View<workspace::Workspace>,
515        remote_id: workspace::ViewId,
516        state: &mut Option<proto::view::Variant>,
517        cx: &mut WindowContext,
518    ) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
519        let Some(proto::view::Variant::ChannelView(_)) = state else {
520            return None;
521        };
522        let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
523            unreachable!()
524        };
525
526        let open = ChannelView::load(ChannelId(state.channel_id), workspace, cx);
527
528        Some(cx.spawn(|mut cx| async move {
529            let this = open.await?;
530
531            let task = this.update(&mut cx, |this, cx| {
532                this.remote_id = Some(remote_id);
533
534                if let Some(state) = state.editor {
535                    Some(this.editor.update(cx, |editor, cx| {
536                        editor.apply_update_proto(
537                            &this.project,
538                            proto::update_view::Variant::Editor(proto::update_view::Editor {
539                                selections: state.selections,
540                                pending_selection: state.pending_selection,
541                                scroll_top_anchor: state.scroll_top_anchor,
542                                scroll_x: state.scroll_x,
543                                scroll_y: state.scroll_y,
544                                ..Default::default()
545                            }),
546                            cx,
547                        )
548                    }))
549                } else {
550                    None
551                }
552            })?;
553
554            if let Some(task) = task {
555                task.await?;
556            }
557
558            Ok(this)
559        }))
560    }
561
562    fn add_event_to_update_proto(
563        &self,
564        event: &EditorEvent,
565        update: &mut Option<proto::update_view::Variant>,
566        cx: &WindowContext,
567    ) -> bool {
568        self.editor
569            .read(cx)
570            .add_event_to_update_proto(event, update, cx)
571    }
572
573    fn apply_update_proto(
574        &mut self,
575        project: &Model<Project>,
576        message: proto::update_view::Variant,
577        cx: &mut ViewContext<Self>,
578    ) -> gpui::Task<anyhow::Result<()>> {
579        self.editor.update(cx, |editor, cx| {
580            editor.apply_update_proto(project, message, cx)
581        })
582    }
583
584    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
585        self.editor.update(cx, |editor, cx| {
586            editor.set_leader_peer_id(leader_peer_id, cx)
587        })
588    }
589
590    fn is_project_item(&self, _cx: &WindowContext) -> bool {
591        false
592    }
593
594    fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
595        Editor::to_follow_event(event)
596    }
597
598    fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
599        let existing = existing.channel_buffer.read(cx);
600        if self.channel_buffer.read(cx).channel_id == existing.channel_id {
601            if existing.is_connected() {
602                Some(Dedup::KeepExisting)
603            } else {
604                Some(Dedup::ReplaceExisting)
605            }
606        } else {
607            None
608        }
609    }
610}
611
612struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
613
614impl CollaborationHub for ChannelBufferCollaborationHub {
615    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
616        self.0.read(cx).collaborators()
617    }
618
619    fn user_participant_indices<'a>(
620        &self,
621        cx: &'a AppContext,
622    ) -> &'a HashMap<u64, ParticipantIndex> {
623        self.0.read(cx).user_store().read(cx).participant_indices()
624    }
625
626    fn user_names(&self, cx: &AppContext) -> HashMap<u64, SharedString> {
627        let user_ids = self.collaborators(cx).values().map(|c| c.user_id);
628        self.0
629            .read(cx)
630            .user_store()
631            .read(cx)
632            .participant_names(user_ids, cx)
633    }
634}