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