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