channel_view.rs

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