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