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!(collab, [CopyLink]);
 34
 35pub fn init(cx: &mut App) {
 36    workspace::FollowableViewRegistry::register::<ChannelView>(cx)
 37}
 38
 39pub struct ChannelView {
 40    pub editor: Entity<Editor>,
 41    workspace: WeakEntity<Workspace>,
 42    project: Entity<Project>,
 43    channel_store: Entity<ChannelStore>,
 44    channel_buffer: Entity<ChannelBuffer>,
 45    remote_id: Option<ViewId>,
 46    _editor_event_subscription: Subscription,
 47    _reparse_subscription: Option<Subscription>,
 48}
 49
 50impl ChannelView {
 51    pub fn open(
 52        channel_id: ChannelId,
 53        link_position: Option<String>,
 54        workspace: Entity<Workspace>,
 55        window: &mut Window,
 56        cx: &mut App,
 57    ) -> Task<Result<Entity<Self>>> {
 58        let pane = workspace.read(cx).active_pane().clone();
 59        let channel_view = Self::open_in_pane(
 60            channel_id,
 61            link_position,
 62            pane.clone(),
 63            workspace.clone(),
 64            window,
 65            cx,
 66        );
 67        window.spawn(cx, async move |cx| {
 68            let channel_view = channel_view.await?;
 69            pane.update_in(cx, |pane, window, cx| {
 70                telemetry::event!(
 71                    "Channel Notes Opened",
 72                    channel_id,
 73                    room_id = ActiveCall::global(cx)
 74                        .read(cx)
 75                        .room()
 76                        .map(|r| r.read(cx).id())
 77                );
 78                pane.add_item(Box::new(channel_view.clone()), true, true, None, window, cx);
 79            })?;
 80            anyhow::Ok(channel_view)
 81        })
 82    }
 83
 84    pub fn open_in_pane(
 85        channel_id: ChannelId,
 86        link_position: Option<String>,
 87        pane: Entity<Pane>,
 88        workspace: Entity<Workspace>,
 89        window: &mut Window,
 90        cx: &mut App,
 91    ) -> Task<Result<Entity<Self>>> {
 92        let channel_view = Self::load(channel_id, workspace, window, cx);
 93        window.spawn(cx, async move |cx| {
 94            let channel_view = channel_view.await?;
 95
 96            pane.update_in(cx, |pane, window, cx| {
 97                let buffer_id = channel_view.read(cx).channel_buffer.read(cx).remote_id(cx);
 98
 99                let existing_view = pane
100                    .items_of_type::<Self>()
101                    .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
102
103                // If this channel buffer is already open in this pane, just return it.
104                if let Some(existing_view) = existing_view.clone() {
105                    if existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer
106                    {
107                        if let Some(link_position) = link_position {
108                            existing_view.update(cx, |channel_view, cx| {
109                                channel_view.focus_position_from_link(
110                                    link_position,
111                                    true,
112                                    window,
113                                    cx,
114                                )
115                            });
116                        }
117                        return existing_view;
118                    }
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                    if let Some(ix) = pane.index_for_item(&existing_item) {
125                        pane.close_item_by_id(
126                            existing_item.entity_id(),
127                            SaveIntent::Skip,
128                            window,
129                            cx,
130                        )
131                        .detach();
132                        pane.add_item(
133                            Box::new(channel_view.clone()),
134                            true,
135                            true,
136                            Some(ix),
137                            window,
138                            cx,
139                        );
140                    }
141                }
142
143                if let Some(link_position) = link_position {
144                    channel_view.update(cx, |channel_view, cx| {
145                        channel_view.focus_position_from_link(link_position, true, window, cx)
146                    });
147                }
148
149                channel_view
150            })
151        })
152    }
153
154    pub fn load(
155        channel_id: ChannelId,
156        workspace: Entity<Workspace>,
157        window: &mut Window,
158        cx: &mut App,
159    ) -> Task<Result<Entity<Self>>> {
160        let weak_workspace = workspace.downgrade();
161        let workspace = workspace.read(cx);
162        let project = workspace.project().to_owned();
163        let channel_store = ChannelStore::global(cx);
164        let language_registry = workspace.app_state().languages.clone();
165        let markdown = language_registry.language_for_name("Markdown");
166        let channel_buffer =
167            channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
168
169        window.spawn(cx, async move |cx| {
170            let channel_buffer = channel_buffer.await?;
171            let markdown = markdown.await.log_err();
172
173            channel_buffer.update(cx, |channel_buffer, cx| {
174                channel_buffer.buffer().update(cx, |buffer, cx| {
175                    buffer.set_language_registry(language_registry);
176                    let Some(markdown) = markdown else {
177                        return;
178                    };
179                    buffer.set_language(Some(markdown), cx);
180                })
181            })?;
182
183            cx.new_window_entity(|window, cx| {
184                let mut this = Self::new(
185                    project,
186                    weak_workspace,
187                    channel_store,
188                    channel_buffer,
189                    window,
190                    cx,
191                );
192                this.acknowledge_buffer_version(cx);
193                this
194            })
195        })
196    }
197
198    pub fn new(
199        project: Entity<Project>,
200        workspace: WeakEntity<Workspace>,
201        channel_store: Entity<ChannelStore>,
202        channel_buffer: Entity<ChannelBuffer>,
203        window: &mut Window,
204        cx: &mut Context<Self>,
205    ) -> Self {
206        let buffer = channel_buffer.read(cx).buffer();
207        let this = cx.entity().downgrade();
208        let editor = cx.new(|cx| {
209            let mut editor = Editor::for_buffer(buffer, None, window, cx);
210            editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
211                channel_buffer.clone(),
212            )));
213            editor.set_custom_context_menu(move |_, position, window, cx| {
214                let this = this.clone();
215                Some(ui::ContextMenu::build(window, cx, move |menu, _, _| {
216                    menu.entry("Copy link to section", None, move |window, cx| {
217                        this.update(cx, |this, cx| {
218                            this.copy_link_for_position(position, window, cx)
219                        })
220                        .ok();
221                    })
222                }))
223            });
224            editor
225        });
226        let _editor_event_subscription =
227            cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
228
229        cx.subscribe_in(&channel_buffer, window, Self::handle_channel_buffer_event)
230            .detach();
231
232        Self {
233            editor,
234            workspace,
235            project,
236            channel_store,
237            channel_buffer,
238            remote_id: None,
239            _editor_event_subscription,
240            _reparse_subscription: None,
241        }
242    }
243
244    fn focus_position_from_link(
245        &mut self,
246        position: String,
247        first_attempt: bool,
248        window: &mut Window,
249        cx: &mut Context<Self>,
250    ) {
251        let position = Channel::slug(&position).to_lowercase();
252        let snapshot = self
253            .editor
254            .update(cx, |editor, cx| editor.snapshot(window, cx));
255
256        if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
257            if let Some(item) = outline
258                .items
259                .iter()
260                .find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
261            {
262                self.editor.update(cx, |editor, cx| {
263                    editor.change_selections(
264                        SelectionEffects::scroll(Autoscroll::focused()),
265                        window,
266                        cx,
267                        |s| {
268                            s.replace_cursors_with(|map| {
269                                vec![item.range.start.to_display_point(map)]
270                            })
271                        },
272                    )
273                });
274                return;
275            }
276        }
277
278        if !first_attempt {
279            return;
280        }
281        self._reparse_subscription = Some(cx.subscribe_in(
282            &self.editor,
283            window,
284            move |this, _, e: &EditorEvent, window, cx| {
285                match e {
286                    EditorEvent::Reparsed(_) => {
287                        this.focus_position_from_link(position.clone(), false, window, cx);
288                        this._reparse_subscription.take();
289                    }
290                    EditorEvent::Edited { .. } | EditorEvent::SelectionsChanged { local: true } => {
291                        this._reparse_subscription.take();
292                    }
293                    _ => {}
294                };
295            },
296        ));
297    }
298
299    fn copy_link(&mut self, _: &CopyLink, window: &mut Window, cx: &mut Context<Self>) {
300        let position = self
301            .editor
302            .update(cx, |editor, cx| editor.selections.newest_display(cx).start);
303        self.copy_link_for_position(position, window, cx)
304    }
305
306    fn copy_link_for_position(
307        &self,
308        position: DisplayPoint,
309        window: &mut Window,
310        cx: &mut Context<Self>,
311    ) {
312        let snapshot = self
313            .editor
314            .update(cx, |editor, cx| editor.snapshot(window, cx));
315
316        let mut closest_heading = None;
317
318        if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
319            for item in outline.items {
320                if item.range.start.to_display_point(&snapshot) > position {
321                    break;
322                }
323                closest_heading = Some(item);
324            }
325        }
326
327        let Some(channel) = self.channel(cx) else {
328            return;
329        };
330
331        let link = channel.notes_link(closest_heading.map(|heading| heading.text), cx);
332        cx.write_to_clipboard(ClipboardItem::new_string(link));
333        self.workspace
334            .update(cx, |workspace, cx| {
335                struct CopyLinkForPositionToast;
336
337                workspace.show_toast(
338                    Toast::new(
339                        NotificationId::unique::<CopyLinkForPositionToast>(),
340                        "Link copied to clipboard",
341                    ),
342                    cx,
343                );
344            })
345            .ok();
346    }
347
348    pub fn channel(&self, cx: &App) -> Option<Arc<Channel>> {
349        self.channel_buffer.read(cx).channel(cx)
350    }
351
352    fn handle_channel_buffer_event(
353        &mut self,
354        _: &Entity<ChannelBuffer>,
355        event: &ChannelBufferEvent,
356        window: &mut Window,
357        cx: &mut Context<Self>,
358    ) {
359        match event {
360            ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
361                editor.set_read_only(true);
362                cx.notify();
363            }),
364            ChannelBufferEvent::Connected => self.editor.update(cx, |editor, cx| {
365                editor.set_read_only(false);
366                cx.notify();
367            }),
368            ChannelBufferEvent::ChannelChanged => {
369                self.editor.update(cx, |_, cx| {
370                    cx.emit(editor::EditorEvent::TitleChanged);
371                    cx.notify()
372                });
373            }
374            ChannelBufferEvent::BufferEdited => {
375                if self.editor.read(cx).is_focused(window) {
376                    self.acknowledge_buffer_version(cx);
377                } else {
378                    self.channel_store.update(cx, |store, cx| {
379                        let channel_buffer = self.channel_buffer.read(cx);
380                        store.update_latest_notes_version(
381                            channel_buffer.channel_id,
382                            channel_buffer.epoch(),
383                            &channel_buffer.buffer().read(cx).version(),
384                            cx,
385                        )
386                    });
387                }
388            }
389            ChannelBufferEvent::CollaboratorsChanged => {}
390        }
391    }
392
393    fn acknowledge_buffer_version(&mut self, cx: &mut Context<ChannelView>) {
394        self.channel_store.update(cx, |store, cx| {
395            let channel_buffer = self.channel_buffer.read(cx);
396            store.acknowledge_notes_version(
397                channel_buffer.channel_id,
398                channel_buffer.epoch(),
399                &channel_buffer.buffer().read(cx).version(),
400                cx,
401            )
402        });
403        self.channel_buffer.update(cx, |buffer, cx| {
404            buffer.acknowledge_buffer_version(cx);
405        });
406    }
407
408    fn get_channel(&self, cx: &App) -> (SharedString, Option<SharedString>) {
409        if let Some(channel) = self.channel(cx) {
410            let status = match (
411                self.channel_buffer.read(cx).buffer().read(cx).read_only(),
412                self.channel_buffer.read(cx).is_connected(),
413            ) {
414                (false, true) => None,
415                (true, true) => Some("read-only"),
416                (_, false) => Some("disconnected"),
417            };
418
419            (channel.name.clone(), status.map(Into::into))
420        } else {
421            ("<unknown>".into(), Some("disconnected".into()))
422        }
423    }
424}
425
426impl EventEmitter<EditorEvent> for ChannelView {}
427
428impl Render for ChannelView {
429    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
430        div()
431            .size_full()
432            .on_action(cx.listener(Self::copy_link))
433            .child(self.editor.clone())
434    }
435}
436
437impl Focusable for ChannelView {
438    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
439        self.editor.read(cx).focus_handle(cx)
440    }
441}
442
443impl Item for ChannelView {
444    type Event = EditorEvent;
445
446    fn act_as_type<'a>(
447        &'a self,
448        type_id: TypeId,
449        self_handle: &'a Entity<Self>,
450        _: &'a App,
451    ) -> Option<AnyView> {
452        if type_id == TypeId::of::<Self>() {
453            Some(self_handle.to_any())
454        } else if type_id == TypeId::of::<Editor>() {
455            Some(self.editor.to_any())
456        } else {
457            None
458        }
459    }
460
461    fn tab_icon(&self, _: &Window, cx: &App) -> Option<Icon> {
462        let channel = self.channel(cx)?;
463        let icon = match channel.visibility {
464            ChannelVisibility::Public => IconName::Public,
465            ChannelVisibility::Members => IconName::Hash,
466        };
467
468        Some(Icon::new(icon))
469    }
470
471    fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
472        let (name, status) = self.get_channel(cx);
473        if let Some(status) = status {
474            format!("{name} - {status}").into()
475        } else {
476            name
477        }
478    }
479
480    fn tab_content(&self, params: TabContentParams, _: &Window, cx: &App) -> gpui::AnyElement {
481        let (name, status) = self.get_channel(cx);
482        h_flex()
483            .gap_2()
484            .child(
485                Label::new(name)
486                    .color(params.text_color())
487                    .when(params.preview, |this| this.italic()),
488            )
489            .when_some(status, |element, status| {
490                element.child(
491                    Label::new(status)
492                        .size(LabelSize::XSmall)
493                        .color(Color::Muted),
494                )
495            })
496            .into_any_element()
497    }
498
499    fn telemetry_event_text(&self) -> Option<&'static str> {
500        None
501    }
502
503    fn clone_on_split(
504        &self,
505        _: Option<WorkspaceId>,
506        window: &mut Window,
507        cx: &mut Context<Self>,
508    ) -> Option<Entity<Self>> {
509        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 is_singleton(&self, _cx: &App) -> bool {
522        false
523    }
524
525    fn navigate(
526        &mut self,
527        data: Box<dyn Any>,
528        window: &mut Window,
529        cx: &mut Context<Self>,
530    ) -> bool {
531        self.editor
532            .update(cx, |editor, cx| editor.navigate(data, window, cx))
533    }
534
535    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
536        self.editor
537            .update(cx, |item, cx| item.deactivated(window, cx))
538    }
539
540    fn set_nav_history(
541        &mut self,
542        history: ItemNavHistory,
543        window: &mut Window,
544        cx: &mut Context<Self>,
545    ) {
546        self.editor.update(cx, |editor, cx| {
547            Item::set_nav_history(editor, history, window, cx)
548        })
549    }
550
551    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
552        Some(Box::new(self.editor.clone()))
553    }
554
555    fn show_toolbar(&self) -> bool {
556        true
557    }
558
559    fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> {
560        self.editor.read(cx).pixel_position_of_cursor(cx)
561    }
562
563    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
564        Editor::to_item_events(event, f)
565    }
566}
567
568impl FollowableItem for ChannelView {
569    fn remote_id(&self) -> Option<workspace::ViewId> {
570        self.remote_id
571    }
572
573    fn to_state_proto(&self, window: &Window, cx: &App) -> Option<proto::view::Variant> {
574        let channel_buffer = self.channel_buffer.read(cx);
575        if !channel_buffer.is_connected() {
576            return None;
577        }
578
579        Some(proto::view::Variant::ChannelView(
580            proto::view::ChannelView {
581                channel_id: channel_buffer.channel_id.0,
582                editor: if let Some(proto::view::Variant::Editor(proto)) =
583                    self.editor.read(cx).to_state_proto(window, cx)
584                {
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: &Window,
649        cx: &App,
650    ) -> bool {
651        self.editor
652            .read(cx)
653            .add_event_to_update_proto(event, update, window, cx)
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}