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