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