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