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 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},
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,
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 && 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(link_position, true, window, cx)
116 });
117 }
118 return existing_view;
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 && let Some(ix) = pane.index_for_item(&existing_item)
125 {
126 pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, window, cx)
127 .detach();
128 pane.add_item(
129 Box::new(channel_view.clone()),
130 true,
131 true,
132 Some(ix),
133 window,
134 cx,
135 );
136 }
137
138 if let Some(link_position) = link_position {
139 channel_view.update(cx, |channel_view, cx| {
140 channel_view.focus_position_from_link(link_position, true, window, cx)
141 });
142 }
143
144 channel_view
145 })
146 })
147 }
148
149 pub fn load(
150 channel_id: ChannelId,
151 workspace: Entity<Workspace>,
152 window: &mut Window,
153 cx: &mut App,
154 ) -> Task<Result<Entity<Self>>> {
155 let weak_workspace = workspace.downgrade();
156 let workspace = workspace.read(cx);
157 let project = workspace.project().to_owned();
158 let channel_store = ChannelStore::global(cx);
159 let language_registry = workspace.app_state().languages.clone();
160 let markdown = language_registry.language_for_name("Markdown");
161 let channel_buffer =
162 channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
163
164 window.spawn(cx, async move |cx| {
165 let channel_buffer = channel_buffer.await?;
166 let markdown = markdown.await.log_err();
167
168 channel_buffer.update(cx, |channel_buffer, cx| {
169 channel_buffer.buffer().update(cx, |buffer, cx| {
170 buffer.set_language_registry(language_registry);
171 let Some(markdown) = markdown else {
172 return;
173 };
174 buffer.set_language(Some(markdown), cx);
175 })
176 });
177
178 cx.new_window_entity(|window, cx| {
179 let mut this = Self::new(
180 project,
181 weak_workspace,
182 channel_store,
183 channel_buffer,
184 window,
185 cx,
186 );
187 this.acknowledge_buffer_version(cx);
188 this
189 })
190 })
191 }
192
193 pub fn new(
194 project: Entity<Project>,
195 workspace: WeakEntity<Workspace>,
196 channel_store: Entity<ChannelStore>,
197 channel_buffer: Entity<ChannelBuffer>,
198 window: &mut Window,
199 cx: &mut Context<Self>,
200 ) -> Self {
201 let buffer = channel_buffer.read(cx).buffer();
202 let this = cx.entity().downgrade();
203 let editor = cx.new(|cx| {
204 let mut editor = Editor::for_buffer(buffer, None, window, cx);
205 editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
206 channel_buffer.clone(),
207 )));
208 editor.set_custom_context_menu(move |_, position, window, cx| {
209 let this = this.clone();
210 Some(ui::ContextMenu::build(window, cx, move |menu, _, _| {
211 menu.entry("Copy Link to Section", None, move |window, cx| {
212 this.update(cx, |this, cx| {
213 this.copy_link_for_position(position, window, cx)
214 })
215 .ok();
216 })
217 }))
218 });
219 editor.set_show_bookmarks(false, cx);
220 editor.set_show_breakpoints(false, cx);
221 editor.set_show_runnables(false, cx);
222 editor
223 });
224 let _editor_event_subscription =
225 cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
226
227 cx.subscribe_in(&channel_buffer, window, Self::handle_channel_buffer_event)
228 .detach();
229
230 Self {
231 editor,
232 workspace,
233 project,
234 channel_store,
235 channel_buffer,
236 remote_id: None,
237 _editor_event_subscription,
238 _reparse_subscription: None,
239 }
240 }
241
242 fn focus_position_from_link(
243 &mut self,
244 position: String,
245 first_attempt: bool,
246 window: &mut Window,
247 cx: &mut Context<Self>,
248 ) {
249 let position = Channel::slug(&position).to_lowercase();
250 let snapshot = self
251 .editor
252 .update(cx, |editor, cx| editor.snapshot(window, cx));
253
254 if let Some(outline) = snapshot.buffer_snapshot().outline(None)
255 && let Some(item) = outline
256 .items
257 .iter()
258 .find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
259 {
260 self.editor.update(cx, |editor, cx| {
261 editor.change_selections(
262 SelectionEffects::scroll(Autoscroll::focused()),
263 window,
264 cx,
265 |s| s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)]),
266 )
267 });
268 return;
269 }
270
271 if !first_attempt {
272 return;
273 }
274 self._reparse_subscription = Some(cx.subscribe_in(
275 &self.editor,
276 window,
277 move |this, _, e: &EditorEvent, window, cx| {
278 match e {
279 EditorEvent::Reparsed(_) => {
280 this.focus_position_from_link(position.clone(), false, window, cx);
281 this._reparse_subscription.take();
282 }
283 EditorEvent::Edited { .. } | EditorEvent::SelectionsChanged { local: true } => {
284 this._reparse_subscription.take();
285 }
286 _ => {}
287 };
288 },
289 ));
290 }
291
292 fn copy_link(&mut self, _: &CopyLink, window: &mut Window, cx: &mut Context<Self>) {
293 let position = self.editor.update(cx, |editor, cx| {
294 editor
295 .selections
296 .newest_display(&editor.display_snapshot(cx))
297 .start
298 });
299 self.copy_link_for_position(position, window, cx)
300 }
301
302 fn copy_link_for_position(
303 &self,
304 position: DisplayPoint,
305 window: &mut Window,
306 cx: &mut Context<Self>,
307 ) {
308 let snapshot = self
309 .editor
310 .update(cx, |editor, cx| editor.snapshot(window, cx));
311
312 let mut closest_heading = None;
313
314 if let Some(outline) = snapshot.buffer_snapshot().outline(None) {
315 for item in outline.items {
316 if item.range.start.to_display_point(&snapshot) > position {
317 break;
318 }
319 closest_heading = Some(item);
320 }
321 }
322
323 let Some(channel) = self.channel(cx) else {
324 return;
325 };
326
327 let link = channel.notes_link(closest_heading.map(|heading| heading.text), cx);
328 cx.write_to_clipboard(ClipboardItem::new_string(link));
329 self.workspace
330 .update(cx, |workspace, cx| {
331 struct CopyLinkForPositionToast;
332
333 workspace.show_toast(
334 Toast::new(
335 NotificationId::unique::<CopyLinkForPositionToast>(),
336 "Link copied to clipboard",
337 ),
338 cx,
339 );
340 })
341 .ok();
342 }
343
344 pub fn channel(&self, cx: &App) -> Option<Arc<Channel>> {
345 self.channel_buffer.read(cx).channel(cx)
346 }
347
348 fn handle_channel_buffer_event(
349 &mut self,
350 _: &Entity<ChannelBuffer>,
351 event: &ChannelBufferEvent,
352 window: &mut Window,
353 cx: &mut Context<Self>,
354 ) {
355 match event {
356 ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
357 editor.set_read_only(true);
358 cx.notify();
359 }),
360 ChannelBufferEvent::Connected => self.editor.update(cx, |editor, cx| {
361 editor.set_read_only(false);
362 cx.notify();
363 }),
364 ChannelBufferEvent::ChannelChanged => {
365 self.editor.update(cx, |_, cx| {
366 cx.emit(editor::EditorEvent::TitleChanged);
367 cx.notify()
368 });
369 }
370 ChannelBufferEvent::BufferEdited => {
371 if self.editor.read(cx).is_focused(window) {
372 self.acknowledge_buffer_version(cx);
373 } else {
374 self.channel_store.update(cx, |store, cx| {
375 let channel_buffer = self.channel_buffer.read(cx);
376 store.update_latest_notes_version(
377 channel_buffer.channel_id,
378 channel_buffer.epoch(),
379 &channel_buffer.buffer().read(cx).version(),
380 cx,
381 )
382 });
383 }
384 }
385 ChannelBufferEvent::CollaboratorsChanged => {}
386 }
387 }
388
389 fn acknowledge_buffer_version(&mut self, cx: &mut Context<ChannelView>) {
390 self.channel_store.update(cx, |store, cx| {
391 let channel_buffer = self.channel_buffer.read(cx);
392 store.acknowledge_notes_version(
393 channel_buffer.channel_id,
394 channel_buffer.epoch(),
395 &channel_buffer.buffer().read(cx).version(),
396 cx,
397 )
398 });
399 self.channel_buffer.update(cx, |buffer, cx| {
400 buffer.acknowledge_buffer_version(cx);
401 });
402 }
403
404 fn get_channel(&self, cx: &App) -> (SharedString, Option<SharedString>) {
405 if let Some(channel) = self.channel(cx) {
406 let status = match (
407 self.channel_buffer.read(cx).buffer().read(cx).read_only(),
408 self.channel_buffer.read(cx).is_connected(),
409 ) {
410 (false, true) => None,
411 (true, true) => Some("read-only"),
412 (_, false) => Some("disconnected"),
413 };
414
415 (channel.name.clone(), status.map(Into::into))
416 } else {
417 ("<unknown>".into(), Some("disconnected".into()))
418 }
419 }
420}
421
422impl EventEmitter<EditorEvent> for ChannelView {}
423
424impl Render for ChannelView {
425 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
426 div()
427 .size_full()
428 .on_action(cx.listener(Self::copy_link))
429 .child(self.editor.clone())
430 }
431}
432
433impl Focusable for ChannelView {
434 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
435 self.editor.read(cx).focus_handle(cx)
436 }
437}
438
439impl Item for ChannelView {
440 type Event = EditorEvent;
441
442 fn act_as_type<'a>(
443 &'a self,
444 type_id: TypeId,
445 self_handle: &'a Entity<Self>,
446 _: &'a App,
447 ) -> Option<gpui::AnyEntity> {
448 if type_id == TypeId::of::<Self>() {
449 Some(self_handle.clone().into())
450 } else if type_id == TypeId::of::<Editor>() {
451 Some(self.editor.clone().into())
452 } else {
453 None
454 }
455 }
456
457 fn tab_icon(&self, _: &Window, cx: &App) -> Option<Icon> {
458 let channel = self.channel(cx)?;
459 let icon = match channel.visibility {
460 ChannelVisibility::Public => IconName::Public,
461 ChannelVisibility::Members => IconName::Hash,
462 };
463
464 Some(Icon::new(icon))
465 }
466
467 fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
468 let (name, status) = self.get_channel(cx);
469 if let Some(status) = status {
470 format!("{name} - {status}").into()
471 } else {
472 name
473 }
474 }
475
476 fn tab_content(&self, params: TabContentParams, _: &Window, cx: &App) -> gpui::AnyElement {
477 let (name, status) = self.get_channel(cx);
478 h_flex()
479 .gap_2()
480 .child(
481 Label::new(name)
482 .color(params.text_color())
483 .when(params.preview, |this| this.italic()),
484 )
485 .when_some(status, |element, status| {
486 element.child(
487 Label::new(status)
488 .size(LabelSize::XSmall)
489 .color(Color::Muted),
490 )
491 })
492 .into_any_element()
493 }
494
495 fn telemetry_event_text(&self) -> Option<&'static str> {
496 None
497 }
498
499 fn can_split(&self) -> bool {
500 true
501 }
502
503 fn clone_on_split(
504 &self,
505 _: Option<WorkspaceId>,
506 window: &mut Window,
507 cx: &mut Context<Self>,
508 ) -> Task<Option<Entity<Self>>> {
509 Task::ready(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 navigate(
522 &mut self,
523 data: Arc<dyn Any + Send>,
524 window: &mut Window,
525 cx: &mut Context<Self>,
526 ) -> bool {
527 self.editor
528 .update(cx, |editor, cx| editor.navigate(data, window, cx))
529 }
530
531 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
532 self.editor
533 .update(cx, |item, cx| item.deactivated(window, cx))
534 }
535
536 fn set_nav_history(
537 &mut self,
538 history: ItemNavHistory,
539 window: &mut Window,
540 cx: &mut Context<Self>,
541 ) {
542 self.editor.update(cx, |editor, cx| {
543 Item::set_nav_history(editor, history, window, cx)
544 })
545 }
546
547 fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
548 Some(Box::new(self.editor.clone()))
549 }
550
551 fn show_toolbar(&self) -> bool {
552 true
553 }
554
555 fn pixel_position_of_cursor(&self, cx: &App) -> Option<Point<Pixels>> {
556 self.editor.read(cx).pixel_position_of_cursor(cx)
557 }
558
559 fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) {
560 Editor::to_item_events(event, f)
561 }
562}
563
564impl FollowableItem for ChannelView {
565 fn remote_id(&self) -> Option<workspace::ViewId> {
566 self.remote_id
567 }
568
569 fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option<proto::view::Variant> {
570 let (is_connected, channel_id) = {
571 let channel_buffer = self.channel_buffer.read(cx);
572 (channel_buffer.is_connected(), channel_buffer.channel_id.0)
573 };
574 if !is_connected {
575 return None;
576 }
577
578 let editor_proto = self
579 .editor
580 .update(cx, |editor, cx| editor.to_state_proto(window, cx));
581 Some(proto::view::Variant::ChannelView(
582 proto::view::ChannelView {
583 channel_id,
584 editor: if let Some(proto::view::Variant::Editor(proto)) = editor_proto {
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: &mut Window,
649 cx: &mut App,
650 ) -> bool {
651 self.editor.update(cx, |editor, cx| {
652 editor.add_event_to_update_proto(event, update, window, cx)
653 })
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}