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::{CollaborationHub, Editor, EditorEvent};
 10use gpui::{
 11    actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
 12    IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
 13    VisualContext as _, WindowContext,
 14};
 15use project::Project;
 16use std::{
 17    any::{Any, TypeId},
 18    sync::Arc,
 19};
 20use ui::{prelude::*, Label};
 21use util::ResultExt;
 22use workspace::{
 23    item::{FollowableItem, Item, ItemEvent, ItemHandle},
 24    register_followable_item,
 25    searchable::SearchableItemHandle,
 26    ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
 27};
 28
 29actions!(collab, [Deploy]);
 30
 31pub fn init(cx: &mut AppContext) {
 32    register_followable_item::<ChannelView>(cx)
 33}
 34
 35pub struct ChannelView {
 36    pub editor: View<Editor>,
 37    project: Model<Project>,
 38    channel_store: Model<ChannelStore>,
 39    channel_buffer: Model<ChannelBuffer>,
 40    remote_id: Option<ViewId>,
 41    _editor_event_subscription: Subscription,
 42}
 43
 44impl ChannelView {
 45    pub fn open(
 46        channel_id: ChannelId,
 47        workspace: View<Workspace>,
 48        cx: &mut WindowContext,
 49    ) -> Task<Result<View<Self>>> {
 50        let pane = workspace.read(cx).active_pane().clone();
 51        let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
 52        cx.spawn(|mut cx| async move {
 53            let channel_view = channel_view.await?;
 54            pane.update(&mut cx, |pane, cx| {
 55                report_call_event_for_channel(
 56                    "open channel notes",
 57                    channel_id,
 58                    &workspace.read(cx).app_state().client,
 59                    cx,
 60                );
 61                pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
 62            })?;
 63            anyhow::Ok(channel_view)
 64        })
 65    }
 66
 67    pub fn open_in_pane(
 68        channel_id: ChannelId,
 69        pane: View<Pane>,
 70        workspace: View<Workspace>,
 71        cx: &mut WindowContext,
 72    ) -> Task<Result<View<Self>>> {
 73        let workspace = workspace.read(cx);
 74        let project = workspace.project().to_owned();
 75        let channel_store = ChannelStore::global(cx);
 76        let language_registry = workspace.app_state().languages.clone();
 77        let markdown = language_registry.language_for_name("Markdown");
 78        let channel_buffer =
 79            channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
 80
 81        cx.spawn(|mut cx| async move {
 82            let channel_buffer = channel_buffer.await?;
 83            let markdown = markdown.await.log_err();
 84
 85            channel_buffer.update(&mut cx, |buffer, cx| {
 86                buffer.buffer().update(cx, |buffer, cx| {
 87                    buffer.set_language_registry(language_registry);
 88                    if let Some(markdown) = markdown {
 89                        buffer.set_language(Some(markdown), cx);
 90                    }
 91                })
 92            })?;
 93
 94            pane.update(&mut cx, |pane, cx| {
 95                let buffer_id = 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_buffer {
104                        return existing_view;
105                    }
106                }
107
108                let view = cx.new_view(|cx| {
109                    let mut this = Self::new(project, channel_store, channel_buffer, cx);
110                    this.acknowledge_buffer_version(cx);
111                    this
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(view.clone()), true, true, Some(ix), cx);
121                    }
122                }
123
124                view
125            })
126        })
127    }
128
129    pub fn new(
130        project: Model<Project>,
131        channel_store: Model<ChannelStore>,
132        channel_buffer: Model<ChannelBuffer>,
133        cx: &mut ViewContext<Self>,
134    ) -> Self {
135        let buffer = channel_buffer.read(cx).buffer();
136        let editor = cx.new_view(|cx| {
137            let mut editor = Editor::for_buffer(buffer, None, cx);
138            editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
139                channel_buffer.clone(),
140            )));
141            editor
142        });
143        let _editor_event_subscription =
144            cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
145
146        cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
147            .detach();
148
149        Self {
150            editor,
151            project,
152            channel_store,
153            channel_buffer,
154            remote_id: None,
155            _editor_event_subscription,
156        }
157    }
158
159    pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
160        self.channel_buffer.read(cx).channel(cx)
161    }
162
163    fn handle_channel_buffer_event(
164        &mut self,
165        _: Model<ChannelBuffer>,
166        event: &ChannelBufferEvent,
167        cx: &mut ViewContext<Self>,
168    ) {
169        match event {
170            ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
171                editor.set_read_only(true);
172                cx.notify();
173            }),
174            ChannelBufferEvent::ChannelChanged => {
175                self.editor.update(cx, |_, cx| {
176                    cx.emit(editor::EditorEvent::TitleChanged);
177                    cx.notify()
178                });
179            }
180            ChannelBufferEvent::BufferEdited => {
181                if self.editor.read(cx).is_focused(cx) {
182                    self.acknowledge_buffer_version(cx);
183                } else {
184                    self.channel_store.update(cx, |store, cx| {
185                        let channel_buffer = self.channel_buffer.read(cx);
186                        store.notes_changed(
187                            channel_buffer.channel_id,
188                            channel_buffer.epoch(),
189                            &channel_buffer.buffer().read(cx).version(),
190                            cx,
191                        )
192                    });
193                }
194            }
195            ChannelBufferEvent::CollaboratorsChanged => {}
196        }
197    }
198
199    fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
200        self.channel_store.update(cx, |store, cx| {
201            let channel_buffer = self.channel_buffer.read(cx);
202            store.acknowledge_notes_version(
203                channel_buffer.channel_id,
204                channel_buffer.epoch(),
205                &channel_buffer.buffer().read(cx).version(),
206                cx,
207            )
208        });
209        self.channel_buffer.update(cx, |buffer, cx| {
210            buffer.acknowledge_buffer_version(cx);
211        });
212    }
213}
214
215impl EventEmitter<EditorEvent> for ChannelView {}
216
217impl Render for ChannelView {
218    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
219        self.editor.clone()
220    }
221}
222
223impl FocusableView for ChannelView {
224    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
225        self.editor.read(cx).focus_handle(cx)
226    }
227}
228
229impl Item for ChannelView {
230    type Event = EditorEvent;
231
232    fn act_as_type<'a>(
233        &'a self,
234        type_id: TypeId,
235        self_handle: &'a View<Self>,
236        _: &'a AppContext,
237    ) -> Option<AnyView> {
238        if type_id == TypeId::of::<Self>() {
239            Some(self_handle.to_any())
240        } else if type_id == TypeId::of::<Editor>() {
241            Some(self.editor.to_any())
242        } else {
243            None
244        }
245    }
246
247    fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
248        let label = if let Some(channel) = self.channel(cx) {
249            match (
250                self.channel_buffer.read(cx).buffer().read(cx).read_only(),
251                self.channel_buffer.read(cx).is_connected(),
252            ) {
253                (false, true) => format!("#{}", channel.name),
254                (true, true) => format!("#{} (read-only)", channel.name),
255                (_, false) => format!("#{} (disconnected)", channel.name),
256            }
257        } else {
258            format!("channel notes (disconnected)")
259        };
260        Label::new(label)
261            .color(if selected {
262                Color::Default
263            } else {
264                Color::Muted
265            })
266            .into_any_element()
267    }
268
269    fn telemetry_event_text(&self) -> Option<&'static str> {
270        None
271    }
272
273    fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
274        Some(cx.new_view(|cx| {
275            Self::new(
276                self.project.clone(),
277                self.channel_store.clone(),
278                self.channel_buffer.clone(),
279                cx,
280            )
281        }))
282    }
283
284    fn is_singleton(&self, _cx: &AppContext) -> bool {
285        false
286    }
287
288    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
289        self.editor
290            .update(cx, |editor, cx| editor.navigate(data, cx))
291    }
292
293    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
294        self.editor
295            .update(cx, |editor, cx| Item::deactivated(editor, cx))
296    }
297
298    fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
299        self.editor
300            .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
301    }
302
303    fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
304        Some(Box::new(self.editor.clone()))
305    }
306
307    fn show_toolbar(&self) -> bool {
308        true
309    }
310
311    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
312        self.editor.read(cx).pixel_position_of_cursor(cx)
313    }
314
315    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
316        Editor::to_item_events(event, f)
317    }
318}
319
320impl FollowableItem for ChannelView {
321    fn remote_id(&self) -> Option<workspace::ViewId> {
322        self.remote_id
323    }
324
325    fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
326        let channel_buffer = self.channel_buffer.read(cx);
327        if !channel_buffer.is_connected() {
328            return None;
329        }
330
331        Some(proto::view::Variant::ChannelView(
332            proto::view::ChannelView {
333                channel_id: channel_buffer.channel_id,
334                editor: if let Some(proto::view::Variant::Editor(proto)) =
335                    self.editor.read(cx).to_state_proto(cx)
336                {
337                    Some(proto)
338                } else {
339                    None
340                },
341            },
342        ))
343    }
344
345    fn from_state_proto(
346        pane: View<workspace::Pane>,
347        workspace: View<workspace::Workspace>,
348        remote_id: workspace::ViewId,
349        state: &mut Option<proto::view::Variant>,
350        cx: &mut WindowContext,
351    ) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
352        let Some(proto::view::Variant::ChannelView(_)) = state else {
353            return None;
354        };
355        let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
356            unreachable!()
357        };
358
359        let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
360
361        Some(cx.spawn(|mut cx| async move {
362            let this = open.await?;
363
364            let task = this.update(&mut cx, |this, cx| {
365                this.remote_id = Some(remote_id);
366
367                if let Some(state) = state.editor {
368                    Some(this.editor.update(cx, |editor, cx| {
369                        editor.apply_update_proto(
370                            &this.project,
371                            proto::update_view::Variant::Editor(proto::update_view::Editor {
372                                selections: state.selections,
373                                pending_selection: state.pending_selection,
374                                scroll_top_anchor: state.scroll_top_anchor,
375                                scroll_x: state.scroll_x,
376                                scroll_y: state.scroll_y,
377                                ..Default::default()
378                            }),
379                            cx,
380                        )
381                    }))
382                } else {
383                    None
384                }
385            })?;
386
387            if let Some(task) = task {
388                task.await?;
389            }
390
391            Ok(this)
392        }))
393    }
394
395    fn add_event_to_update_proto(
396        &self,
397        event: &EditorEvent,
398        update: &mut Option<proto::update_view::Variant>,
399        cx: &WindowContext,
400    ) -> bool {
401        self.editor
402            .read(cx)
403            .add_event_to_update_proto(event, update, cx)
404    }
405
406    fn apply_update_proto(
407        &mut self,
408        project: &Model<Project>,
409        message: proto::update_view::Variant,
410        cx: &mut ViewContext<Self>,
411    ) -> gpui::Task<anyhow::Result<()>> {
412        self.editor.update(cx, |editor, cx| {
413            editor.apply_update_proto(project, message, cx)
414        })
415    }
416
417    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
418        self.editor.update(cx, |editor, cx| {
419            editor.set_leader_peer_id(leader_peer_id, cx)
420        })
421    }
422
423    fn is_project_item(&self, _cx: &WindowContext) -> bool {
424        false
425    }
426
427    fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
428        Editor::to_follow_event(event)
429    }
430}
431
432struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
433
434impl CollaborationHub for ChannelBufferCollaborationHub {
435    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
436        self.0.read(cx).collaborators()
437    }
438
439    fn user_participant_indices<'a>(
440        &self,
441        cx: &'a AppContext,
442    ) -> &'a HashMap<u64, ParticipantIndex> {
443        self.0.read(cx).user_store().read(cx).participant_indices()
444    }
445
446    fn user_names(&self, cx: &AppContext) -> HashMap<u64, SharedString> {
447        let user_ids = self.collaborators(cx).values().map(|c| c.user_id);
448        self.0
449            .read(cx)
450            .user_store()
451            .read(cx)
452            .participant_names(user_ids, cx)
453    }
454}