1use anyhow::Result;
2use call::ActiveCall;
3use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelStore};
4use client::{
5 ChannelId, Collaborator, ParticipantIndex,
6 proto::{self, PeerId},
7};
8use collections::HashMap;
9use editor::{
10 CollaborationHub, DisplayPoint, Editor, EditorEvent, SelectionEffects,
11 display_map::ToDisplayPoint, scroll::Autoscroll,
12};
13use gpui::{
14 AnyView, App, ClipboardItem, Context, Entity, EventEmitter, Focusable, Pixels, Point, Render,
15 Subscription, Task, VisualContext as _, WeakEntity, Window, actions,
16};
17use project::Project;
18use rpc::proto::ChannelVisibility;
19use std::{
20 any::{Any, TypeId},
21 sync::Arc,
22};
23use ui::prelude::*;
24use util::ResultExt;
25use workspace::{CollaboratorId, item::TabContentParams};
26use workspace::{
27 ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
28 item::{FollowableItem, Item, ItemEvent, ItemHandle},
29 searchable::SearchableItemHandle,
30};
31use workspace::{item::Dedup, notifications::NotificationId};
32
33actions!(
34 collab,
35 [
36 /// Copies a link to the current position in the channel buffer.
37 CopyLink
38 ]
39);
40
41pub fn init(cx: &mut App) {
42 workspace::FollowableViewRegistry::register::<ChannelView>(cx)
43}
44
45pub struct ChannelView {
46 pub editor: Entity<Editor>,
47 workspace: WeakEntity<Workspace>,
48 project: Entity<Project>,
49 channel_store: Entity<ChannelStore>,
50 channel_buffer: Entity<ChannelBuffer>,
51 remote_id: Option<ViewId>,
52 _editor_event_subscription: Subscription,
53 _reparse_subscription: Option<Subscription>,
54}
55
56impl ChannelView {
57 pub fn open(
58 channel_id: ChannelId,
59 link_position: Option<String>,
60 workspace: Entity<Workspace>,
61 window: &mut Window,
62 cx: &mut App,
63 ) -> Task<Result<Entity<Self>>> {
64 let pane = workspace.read(cx).active_pane().clone();
65 let channel_view = Self::open_in_pane(
66 channel_id,
67 link_position,
68 pane.clone(),
69 workspace.clone(),
70 window,
71 cx,
72 );
73 window.spawn(cx, async move |cx| {
74 let channel_view = channel_view.await?;
75 pane.update_in(cx, |pane, window, cx| {
76 telemetry::event!(
77 "Channel Notes Opened",
78 channel_id,
79 room_id = ActiveCall::global(cx)
80 .read(cx)
81 .room()
82 .map(|r| r.read(cx).id())
83 );
84 pane.add_item(Box::new(channel_view.clone()), true, true, None, window, cx);
85 })?;
86 anyhow::Ok(channel_view)
87 })
88 }
89
90 pub fn open_in_pane(
91 channel_id: ChannelId,
92 link_position: Option<String>,
93 pane: Entity<Pane>,
94 workspace: Entity<Workspace>,
95 window: &mut Window,
96 cx: &mut App,
97 ) -> Task<Result<Entity<Self>>> {
98 let channel_view = Self::load(channel_id, workspace, window, cx);
99 window.spawn(cx, async move |cx| {
100 let channel_view = channel_view.await?;
101
102 pane.update_in(cx, |pane, window, cx| {
103 let buffer_id = channel_view.read(cx).channel_buffer.read(cx).remote_id(cx);
104
105 let existing_view = pane
106 .items_of_type::<Self>()
107 .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
108
109 // If this channel buffer is already open in this pane, just return it.
110 if let Some(existing_view) = existing_view.clone() {
111 if existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer
112 {
113 if let Some(link_position) = link_position {
114 existing_view.update(cx, |channel_view, cx| {
115 channel_view.focus_position_from_link(
116 link_position,
117 true,
118 window,
119 cx,
120 )
121 });
122 }
123 return existing_view;
124 }
125 }
126
127 // If the pane contained a disconnected view for this channel buffer,
128 // replace that.
129 if let Some(existing_item) = existing_view {
130 if let Some(ix) = pane.index_for_item(&existing_item) {
131 pane.close_item_by_id(
132 existing_item.entity_id(),
133 SaveIntent::Skip,
134 window,
135 cx,
136 )
137 .detach();
138 pane.add_item(
139 Box::new(channel_view.clone()),
140 true,
141 true,
142 Some(ix),
143 window,
144 cx,
145 );
146 }
147 }
148
149 if let Some(link_position) = link_position {
150 channel_view.update(cx, |channel_view, cx| {
151 channel_view.focus_position_from_link(link_position, true, window, cx)
152 });
153 }
154
155 channel_view
156 })
157 })
158 }
159
160 pub fn load(
161 channel_id: ChannelId,
162 workspace: Entity<Workspace>,
163 window: &mut Window,
164 cx: &mut App,
165 ) -> Task<Result<Entity<Self>>> {
166 let weak_workspace = workspace.downgrade();
167 let workspace = workspace.read(cx);
168 let project = workspace.project().to_owned();
169 let channel_store = ChannelStore::global(cx);
170 let language_registry = workspace.app_state().languages.clone();
171 let markdown = language_registry.language_for_name("Markdown");
172 let channel_buffer =
173 channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
174
175 window.spawn(cx, async move |cx| {
176 let channel_buffer = channel_buffer.await?;
177 let markdown = markdown.await.log_err();
178
179 channel_buffer.update(cx, |channel_buffer, cx| {
180 channel_buffer.buffer().update(cx, |buffer, cx| {
181 buffer.set_language_registry(language_registry);
182 let Some(markdown) = markdown else {
183 return;
184 };
185 buffer.set_language(Some(markdown), cx);
186 })
187 })?;
188
189 cx.new_window_entity(|window, cx| {
190 let mut this = Self::new(
191 project,
192 weak_workspace,
193 channel_store,
194 channel_buffer,
195 window,
196 cx,
197 );
198 this.acknowledge_buffer_version(cx);
199 this
200 })
201 })
202 }
203
204 pub fn new(
205 project: Entity<Project>,
206 workspace: WeakEntity<Workspace>,
207 channel_store: Entity<ChannelStore>,
208 channel_buffer: Entity<ChannelBuffer>,
209 window: &mut Window,
210 cx: &mut Context<Self>,
211 ) -> Self {
212 let buffer = channel_buffer.read(cx).buffer();
213 let this = cx.entity().downgrade();
214 let editor = cx.new(|cx| {
215 let mut editor = Editor::for_buffer(buffer, None, window, cx);
216 editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
217 channel_buffer.clone(),
218 )));
219 editor.set_custom_context_menu(move |_, position, window, cx| {
220 let this = this.clone();
221 Some(ui::ContextMenu::build(window, cx, move |menu, _, _| {
222 menu.entry("Copy link to section", None, move |window, cx| {
223 this.update(cx, |this, cx| {
224 this.copy_link_for_position(position, window, cx)
225 })
226 .ok();
227 })
228 }))
229 });
230 editor
231 });
232 let _editor_event_subscription =
233 cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
234
235 cx.subscribe_in(&channel_buffer, window, Self::handle_channel_buffer_event)
236 .detach();
237
238 Self {
239 editor,
240 workspace,
241 project,
242 channel_store,
243 channel_buffer,
244 remote_id: None,
245 _editor_event_subscription,
246 _reparse_subscription: None,
247 }
248 }
249
250 fn focus_position_from_link(
251 &mut self,
252 position: String,
253 first_attempt: bool,
254 window: &mut Window,
255 cx: &mut Context<Self>,
256 ) {
257 let position = Channel::slug(&position).to_lowercase();
258 let snapshot = self
259 .editor
260 .update(cx, |editor, cx| editor.snapshot(window, cx));
261
262 if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
263 if let Some(item) = outline
264 .items
265 .iter()
266 .find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
267 {
268 self.editor.update(cx, |editor, cx| {
269 editor.change_selections(
270 SelectionEffects::scroll(Autoscroll::focused()),
271 window,
272 cx,
273 |s| {
274 s.replace_cursors_with(|map| {
275 vec![item.range.start.to_display_point(map)]
276 })
277 },
278 )
279 });
280 return;
281 }
282 }
283
284 if !first_attempt {
285 return;
286 }
287 self._reparse_subscription = Some(cx.subscribe_in(
288 &self.editor,
289 window,
290 move |this, _, e: &EditorEvent, window, cx| {
291 match e {
292 EditorEvent::Reparsed(_) => {
293 this.focus_position_from_link(position.clone(), false, window, cx);
294 this._reparse_subscription.take();
295 }
296 EditorEvent::Edited { .. } | EditorEvent::SelectionsChanged { local: true } => {
297 this._reparse_subscription.take();
298 }
299 _ => {}
300 };
301 },
302 ));
303 }
304
305 fn copy_link(&mut self, _: &CopyLink, window: &mut Window, cx: &mut Context<Self>) {
306 let position = self
307 .editor
308 .update(cx, |editor, cx| editor.selections.newest_display(cx).start);
309 self.copy_link_for_position(position, window, cx)
310 }
311
312 fn copy_link_for_position(
313 &self,
314 position: DisplayPoint,
315 window: &mut Window,
316 cx: &mut Context<Self>,
317 ) {
318 let snapshot = self
319 .editor
320 .update(cx, |editor, cx| editor.snapshot(window, cx));
321
322 let mut closest_heading = None;
323
324 if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
325 for item in outline.items {
326 if item.range.start.to_display_point(&snapshot) > position {
327 break;
328 }
329 closest_heading = Some(item);
330 }
331 }
332
333 let Some(channel) = self.channel(cx) else {
334 return;
335 };
336
337 let link = channel.notes_link(closest_heading.map(|heading| heading.text), cx);
338 cx.write_to_clipboard(ClipboardItem::new_string(link));
339 self.workspace
340 .update(cx, |workspace, cx| {
341 struct CopyLinkForPositionToast;
342
343 workspace.show_toast(
344 Toast::new(
345 NotificationId::unique::<CopyLinkForPositionToast>(),
346 "Link copied to clipboard",
347 ),
348 cx,
349 );
350 })
351 .ok();
352 }
353
354 pub fn channel(&self, cx: &App) -> Option<Arc<Channel>> {
355 self.channel_buffer.read(cx).channel(cx)
356 }
357
358 fn handle_channel_buffer_event(
359 &mut self,
360 _: &Entity<ChannelBuffer>,
361 event: &ChannelBufferEvent,
362 window: &mut Window,
363 cx: &mut Context<Self>,
364 ) {
365 match event {
366 ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
367 editor.set_read_only(true);
368 cx.notify();
369 }),
370 ChannelBufferEvent::Connected => self.editor.update(cx, |editor, cx| {
371 editor.set_read_only(false);
372 cx.notify();
373 }),
374 ChannelBufferEvent::ChannelChanged => {
375 self.editor.update(cx, |_, cx| {
376 cx.emit(editor::EditorEvent::TitleChanged);
377 cx.notify()
378 });
379 }
380 ChannelBufferEvent::BufferEdited => {
381 if self.editor.read(cx).is_focused(window) {
382 self.acknowledge_buffer_version(cx);
383 } else {
384 self.channel_store.update(cx, |store, cx| {
385 let channel_buffer = self.channel_buffer.read(cx);
386 store.update_latest_notes_version(
387 channel_buffer.channel_id,
388 channel_buffer.epoch(),
389 &channel_buffer.buffer().read(cx).version(),
390 cx,
391 )
392 });
393 }
394 }
395 ChannelBufferEvent::CollaboratorsChanged => {}
396 }
397 }
398
399 fn acknowledge_buffer_version(&mut self, cx: &mut Context<ChannelView>) {
400 self.channel_store.update(cx, |store, cx| {
401 let channel_buffer = self.channel_buffer.read(cx);
402 store.acknowledge_notes_version(
403 channel_buffer.channel_id,
404 channel_buffer.epoch(),
405 &channel_buffer.buffer().read(cx).version(),
406 cx,
407 )
408 });
409 self.channel_buffer.update(cx, |buffer, cx| {
410 buffer.acknowledge_buffer_version(cx);
411 });
412 }
413
414 fn get_channel(&self, cx: &App) -> (SharedString, Option<SharedString>) {
415 if let Some(channel) = self.channel(cx) {
416 let status = match (
417 self.channel_buffer.read(cx).buffer().read(cx).read_only(),
418 self.channel_buffer.read(cx).is_connected(),
419 ) {
420 (false, true) => None,
421 (true, true) => Some("read-only"),
422 (_, false) => Some("disconnected"),
423 };
424
425 (channel.name.clone(), status.map(Into::into))
426 } else {
427 ("<unknown>".into(), Some("disconnected".into()))
428 }
429 }
430}
431
432impl EventEmitter<EditorEvent> for ChannelView {}
433
434impl Render for ChannelView {
435 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
436 div()
437 .size_full()
438 .on_action(cx.listener(Self::copy_link))
439 .child(self.editor.clone())
440 }
441}
442
443impl Focusable for ChannelView {
444 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
445 self.editor.read(cx).focus_handle(cx)
446 }
447}
448
449impl Item for ChannelView {
450 type Event = EditorEvent;
451
452 fn act_as_type<'a>(
453 &'a self,
454 type_id: TypeId,
455 self_handle: &'a Entity<Self>,
456 _: &'a App,
457 ) -> Option<AnyView> {
458 if type_id == TypeId::of::<Self>() {
459 Some(self_handle.to_any())
460 } else if type_id == TypeId::of::<Editor>() {
461 Some(self.editor.to_any())
462 } else {
463 None
464 }
465 }
466
467 fn tab_icon(&self, _: &Window, cx: &App) -> Option<Icon> {
468 let channel = self.channel(cx)?;
469 let icon = match channel.visibility {
470 ChannelVisibility::Public => IconName::Public,
471 ChannelVisibility::Members => IconName::Hash,
472 };
473
474 Some(Icon::new(icon))
475 }
476
477 fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
478 let (name, status) = self.get_channel(cx);
479 if let Some(status) = status {
480 format!("{name} - {status}").into()
481 } else {
482 name
483 }
484 }
485
486 fn tab_content(&self, params: TabContentParams, _: &Window, cx: &App) -> gpui::AnyElement {
487 let (name, status) = self.get_channel(cx);
488 h_flex()
489 .gap_2()
490 .child(
491 Label::new(name)
492 .color(params.text_color())
493 .when(params.preview, |this| this.italic()),
494 )
495 .when_some(status, |element, status| {
496 element.child(
497 Label::new(status)
498 .size(LabelSize::XSmall)
499 .color(Color::Muted),
500 )
501 })
502 .into_any_element()
503 }
504
505 fn telemetry_event_text(&self) -> Option<&'static str> {
506 None
507 }
508
509 fn clone_on_split(
510 &self,
511 _: Option<WorkspaceId>,
512 window: &mut Window,
513 cx: &mut Context<Self>,
514 ) -> Option<Entity<Self>> {
515 Some(cx.new(|cx| {
516 Self::new(
517 self.project.clone(),
518 self.workspace.clone(),
519 self.channel_store.clone(),
520 self.channel_buffer.clone(),
521 window,
522 cx,
523 )
524 }))
525 }
526
527 fn is_singleton(&self, _cx: &App) -> bool {
528 false
529 }
530
531 fn navigate(
532 &mut self,
533 data: Box<dyn Any>,
534 window: &mut Window,
535 cx: &mut Context<Self>,
536 ) -> bool {
537 self.editor
538 .update(cx, |editor, cx| editor.navigate(data, window, cx))
539 }
540
541 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
542 self.editor
543 .update(cx, |item, cx| item.deactivated(window, cx))
544 }
545
546 fn set_nav_history(
547 &mut self,
548 history: ItemNavHistory,
549 window: &mut Window,
550 cx: &mut Context<Self>,
551 ) {
552 self.editor.update(cx, |editor, cx| {
553 Item::set_nav_history(editor, history, window, cx)
554 })
555 }
556
557 fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
558 Some(Box::new(self.editor.clone()))
559 }
560
561 fn show_toolbar(&self) -> bool {
562 true
563 }
564
565 fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> {
566 self.editor.read(cx).pixel_position_of_cursor(cx)
567 }
568
569 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
570 Editor::to_item_events(event, f)
571 }
572}
573
574impl FollowableItem for ChannelView {
575 fn remote_id(&self) -> Option<workspace::ViewId> {
576 self.remote_id
577 }
578
579 fn to_state_proto(&self, window: &Window, cx: &App) -> Option<proto::view::Variant> {
580 let channel_buffer = self.channel_buffer.read(cx);
581 if !channel_buffer.is_connected() {
582 return None;
583 }
584
585 Some(proto::view::Variant::ChannelView(
586 proto::view::ChannelView {
587 channel_id: channel_buffer.channel_id.0,
588 editor: if let Some(proto::view::Variant::Editor(proto)) =
589 self.editor.read(cx).to_state_proto(window, cx)
590 {
591 Some(proto)
592 } else {
593 None
594 },
595 },
596 ))
597 }
598
599 fn from_state_proto(
600 workspace: Entity<workspace::Workspace>,
601 remote_id: workspace::ViewId,
602 state: &mut Option<proto::view::Variant>,
603 window: &mut Window,
604 cx: &mut App,
605 ) -> Option<gpui::Task<anyhow::Result<Entity<Self>>>> {
606 let Some(proto::view::Variant::ChannelView(_)) = state else {
607 return None;
608 };
609 let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
610 unreachable!()
611 };
612
613 let open = ChannelView::load(ChannelId(state.channel_id), workspace, window, cx);
614
615 Some(window.spawn(cx, async move |cx| {
616 let this = open.await?;
617
618 let task = this.update_in(cx, |this, window, cx| {
619 this.remote_id = Some(remote_id);
620
621 if let Some(state) = state.editor {
622 Some(this.editor.update(cx, |editor, cx| {
623 editor.apply_update_proto(
624 &this.project,
625 proto::update_view::Variant::Editor(proto::update_view::Editor {
626 selections: state.selections,
627 pending_selection: state.pending_selection,
628 scroll_top_anchor: state.scroll_top_anchor,
629 scroll_x: state.scroll_x,
630 scroll_y: state.scroll_y,
631 ..Default::default()
632 }),
633 window,
634 cx,
635 )
636 }))
637 } else {
638 None
639 }
640 })?;
641
642 if let Some(task) = task {
643 task.await?;
644 }
645
646 Ok(this)
647 }))
648 }
649
650 fn add_event_to_update_proto(
651 &self,
652 event: &EditorEvent,
653 update: &mut Option<proto::update_view::Variant>,
654 window: &Window,
655 cx: &App,
656 ) -> bool {
657 self.editor
658 .read(cx)
659 .add_event_to_update_proto(event, update, window, cx)
660 }
661
662 fn apply_update_proto(
663 &mut self,
664 project: &Entity<Project>,
665 message: proto::update_view::Variant,
666 window: &mut Window,
667 cx: &mut Context<Self>,
668 ) -> gpui::Task<anyhow::Result<()>> {
669 self.editor.update(cx, |editor, cx| {
670 editor.apply_update_proto(project, message, window, cx)
671 })
672 }
673
674 fn set_leader_id(
675 &mut self,
676 leader_id: Option<CollaboratorId>,
677 window: &mut Window,
678 cx: &mut Context<Self>,
679 ) {
680 self.editor
681 .update(cx, |editor, cx| editor.set_leader_id(leader_id, window, cx))
682 }
683
684 fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {
685 false
686 }
687
688 fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
689 Editor::to_follow_event(event)
690 }
691
692 fn dedup(&self, existing: &Self, _: &Window, cx: &App) -> Option<Dedup> {
693 let existing = existing.channel_buffer.read(cx);
694 if self.channel_buffer.read(cx).channel_id == existing.channel_id {
695 if existing.is_connected() {
696 Some(Dedup::KeepExisting)
697 } else {
698 Some(Dedup::ReplaceExisting)
699 }
700 } else {
701 None
702 }
703 }
704}
705
706struct ChannelBufferCollaborationHub(Entity<ChannelBuffer>);
707
708impl CollaborationHub for ChannelBufferCollaborationHub {
709 fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap<PeerId, Collaborator> {
710 self.0.read(cx).collaborators()
711 }
712
713 fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap<u64, ParticipantIndex> {
714 self.0.read(cx).user_store().read(cx).participant_indices()
715 }
716
717 fn user_names(&self, cx: &App) -> HashMap<u64, SharedString> {
718 let user_ids = self.collaborators(cx).values().map(|c| c.user_id);
719 self.0
720 .read(cx)
721 .user_store()
722 .read(cx)
723 .participant_names(user_ids, cx)
724 }
725}