channel_view.rs

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