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.build_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.build_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.set_read_only(
142                !channel_buffer
143                    .read(cx)
144                    .channel(cx)
145                    .is_some_and(|c| c.can_edit_notes()),
146            );
147            editor
148        });
149        let _editor_event_subscription =
150            cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
151
152        cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
153            .detach();
154
155        Self {
156            editor,
157            project,
158            channel_store,
159            channel_buffer,
160            remote_id: None,
161            _editor_event_subscription,
162        }
163    }
164
165    pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
166        self.channel_buffer.read(cx).channel(cx)
167    }
168
169    fn handle_channel_buffer_event(
170        &mut self,
171        _: Model<ChannelBuffer>,
172        event: &ChannelBufferEvent,
173        cx: &mut ViewContext<Self>,
174    ) {
175        match event {
176            ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
177                editor.set_read_only(true);
178                cx.notify();
179            }),
180            ChannelBufferEvent::ChannelChanged => {
181                self.editor.update(cx, |editor, cx| {
182                    editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
183                    cx.emit(editor::EditorEvent::TitleChanged);
184                    cx.notify()
185                });
186            }
187            ChannelBufferEvent::BufferEdited => {
188                if self.editor.read(cx).is_focused(cx) {
189                    self.acknowledge_buffer_version(cx);
190                } else {
191                    self.channel_store.update(cx, |store, cx| {
192                        let channel_buffer = self.channel_buffer.read(cx);
193                        store.notes_changed(
194                            channel_buffer.channel_id,
195                            channel_buffer.epoch(),
196                            &channel_buffer.buffer().read(cx).version(),
197                            cx,
198                        )
199                    });
200                }
201            }
202            ChannelBufferEvent::CollaboratorsChanged => {}
203        }
204    }
205
206    fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
207        self.channel_store.update(cx, |store, cx| {
208            let channel_buffer = self.channel_buffer.read(cx);
209            store.acknowledge_notes_version(
210                channel_buffer.channel_id,
211                channel_buffer.epoch(),
212                &channel_buffer.buffer().read(cx).version(),
213                cx,
214            )
215        });
216        self.channel_buffer.update(cx, |buffer, cx| {
217            buffer.acknowledge_buffer_version(cx);
218        });
219    }
220}
221
222impl EventEmitter<EditorEvent> for ChannelView {}
223
224impl Render for ChannelView {
225    type Element = AnyView;
226
227    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
228        self.editor.clone().into()
229    }
230}
231
232impl FocusableView for ChannelView {
233    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
234        self.editor.read(cx).focus_handle(cx)
235    }
236}
237
238impl Item for ChannelView {
239    type Event = EditorEvent;
240
241    fn act_as_type<'a>(
242        &'a self,
243        type_id: TypeId,
244        self_handle: &'a View<Self>,
245        _: &'a AppContext,
246    ) -> Option<AnyView> {
247        if type_id == TypeId::of::<Self>() {
248            Some(self_handle.to_any())
249        } else if type_id == TypeId::of::<Editor>() {
250            Some(self.editor.to_any())
251        } else {
252            None
253        }
254    }
255
256    fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
257        let label = if let Some(channel) = self.channel(cx) {
258            match (
259                channel.can_edit_notes(),
260                self.channel_buffer.read(cx).is_connected(),
261            ) {
262                (true, true) => format!("#{}", channel.name),
263                (false, true) => format!("#{} (read-only)", channel.name),
264                (_, false) => format!("#{} (disconnected)", channel.name),
265            }
266        } else {
267            format!("channel notes (disconnected)")
268        };
269        Label::new(label)
270            .color(if selected {
271                Color::Default
272            } else {
273                Color::Muted
274            })
275            .into_any_element()
276    }
277
278    fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
279        Some(cx.build_view(|cx| {
280            Self::new(
281                self.project.clone(),
282                self.channel_store.clone(),
283                self.channel_buffer.clone(),
284                cx,
285            )
286        }))
287    }
288
289    fn is_singleton(&self, _cx: &AppContext) -> bool {
290        false
291    }
292
293    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
294        self.editor
295            .update(cx, |editor, cx| editor.navigate(data, cx))
296    }
297
298    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
299        self.editor
300            .update(cx, |editor, cx| Item::deactivated(editor, cx))
301    }
302
303    fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
304        self.editor
305            .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
306    }
307
308    fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
309        Some(Box::new(self.editor.clone()))
310    }
311
312    fn show_toolbar(&self) -> bool {
313        true
314    }
315
316    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
317        self.editor.read(cx).pixel_position_of_cursor(cx)
318    }
319
320    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
321        Editor::to_item_events(event, f)
322    }
323}
324
325impl FollowableItem for ChannelView {
326    fn remote_id(&self) -> Option<workspace::ViewId> {
327        self.remote_id
328    }
329
330    fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
331        let channel_buffer = self.channel_buffer.read(cx);
332        if !channel_buffer.is_connected() {
333            return None;
334        }
335
336        Some(proto::view::Variant::ChannelView(
337            proto::view::ChannelView {
338                channel_id: channel_buffer.channel_id,
339                editor: if let Some(proto::view::Variant::Editor(proto)) =
340                    self.editor.read(cx).to_state_proto(cx)
341                {
342                    Some(proto)
343                } else {
344                    None
345                },
346            },
347        ))
348    }
349
350    fn from_state_proto(
351        pane: View<workspace::Pane>,
352        workspace: View<workspace::Workspace>,
353        remote_id: workspace::ViewId,
354        state: &mut Option<proto::view::Variant>,
355        cx: &mut WindowContext,
356    ) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
357        let Some(proto::view::Variant::ChannelView(_)) = state else {
358            return None;
359        };
360        let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
361            unreachable!()
362        };
363
364        let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
365
366        Some(cx.spawn(|mut cx| async move {
367            let this = open.await?;
368
369            let task = this.update(&mut cx, |this, cx| {
370                this.remote_id = Some(remote_id);
371
372                if let Some(state) = state.editor {
373                    Some(this.editor.update(cx, |editor, cx| {
374                        editor.apply_update_proto(
375                            &this.project,
376                            proto::update_view::Variant::Editor(proto::update_view::Editor {
377                                selections: state.selections,
378                                pending_selection: state.pending_selection,
379                                scroll_top_anchor: state.scroll_top_anchor,
380                                scroll_x: state.scroll_x,
381                                scroll_y: state.scroll_y,
382                                ..Default::default()
383                            }),
384                            cx,
385                        )
386                    }))
387                } else {
388                    None
389                }
390            })?;
391
392            if let Some(task) = task {
393                task.await?;
394            }
395
396            Ok(this)
397        }))
398    }
399
400    fn add_event_to_update_proto(
401        &self,
402        event: &EditorEvent,
403        update: &mut Option<proto::update_view::Variant>,
404        cx: &WindowContext,
405    ) -> bool {
406        self.editor
407            .read(cx)
408            .add_event_to_update_proto(event, update, cx)
409    }
410
411    fn apply_update_proto(
412        &mut self,
413        project: &Model<Project>,
414        message: proto::update_view::Variant,
415        cx: &mut ViewContext<Self>,
416    ) -> gpui::Task<anyhow::Result<()>> {
417        self.editor.update(cx, |editor, cx| {
418            editor.apply_update_proto(project, message, cx)
419        })
420    }
421
422    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
423        self.editor.update(cx, |editor, cx| {
424            editor.set_leader_peer_id(leader_peer_id, cx)
425        })
426    }
427
428    fn is_project_item(&self, _cx: &WindowContext) -> bool {
429        false
430    }
431
432    fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
433        Editor::to_follow_event(event)
434    }
435}
436
437struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
438
439impl CollaborationHub for ChannelBufferCollaborationHub {
440    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
441        self.0.read(cx).collaborators()
442    }
443
444    fn user_participant_indices<'a>(
445        &self,
446        cx: &'a AppContext,
447    ) -> &'a HashMap<u64, ParticipantIndex> {
448        self.0.read(cx).user_store().read(cx).participant_indices()
449    }
450}