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