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