channel_view.rs

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