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