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