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