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