channel_view.rs

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