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(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
404 Some(cx.new_view(|cx| {
405 Self::new(
406 self.project.clone(),
407 self.workspace.clone(),
408 self.channel_store.clone(),
409 self.channel_buffer.clone(),
410 cx,
411 )
412 }))
413 }
414
415 fn is_singleton(&self, _cx: &AppContext) -> bool {
416 false
417 }
418
419 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
420 self.editor
421 .update(cx, |editor, cx| editor.navigate(data, cx))
422 }
423
424 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
425 self.editor
426 .update(cx, |editor, cx| Item::deactivated(editor, cx))
427 }
428
429 fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
430 self.editor
431 .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
432 }
433
434 fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
435 Some(Box::new(self.editor.clone()))
436 }
437
438 fn show_toolbar(&self) -> bool {
439 true
440 }
441
442 fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
443 self.editor.read(cx).pixel_position_of_cursor(cx)
444 }
445
446 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
447 Editor::to_item_events(event, f)
448 }
449}
450
451impl FollowableItem for ChannelView {
452 fn remote_id(&self) -> Option<workspace::ViewId> {
453 self.remote_id
454 }
455
456 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
457 let channel_buffer = self.channel_buffer.read(cx);
458 if !channel_buffer.is_connected() {
459 return None;
460 }
461
462 Some(proto::view::Variant::ChannelView(
463 proto::view::ChannelView {
464 channel_id: channel_buffer.channel_id.0,
465 editor: if let Some(proto::view::Variant::Editor(proto)) =
466 self.editor.read(cx).to_state_proto(cx)
467 {
468 Some(proto)
469 } else {
470 None
471 },
472 },
473 ))
474 }
475
476 fn from_state_proto(
477 pane: View<workspace::Pane>,
478 workspace: View<workspace::Workspace>,
479 remote_id: workspace::ViewId,
480 state: &mut Option<proto::view::Variant>,
481 cx: &mut WindowContext,
482 ) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
483 let Some(proto::view::Variant::ChannelView(_)) = state else {
484 return None;
485 };
486 let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
487 unreachable!()
488 };
489
490 let open =
491 ChannelView::open_in_pane(ChannelId(state.channel_id), None, pane, workspace, cx);
492
493 Some(cx.spawn(|mut cx| async move {
494 let this = open.await?;
495
496 let task = this.update(&mut cx, |this, cx| {
497 this.remote_id = Some(remote_id);
498
499 if let Some(state) = state.editor {
500 Some(this.editor.update(cx, |editor, cx| {
501 editor.apply_update_proto(
502 &this.project,
503 proto::update_view::Variant::Editor(proto::update_view::Editor {
504 selections: state.selections,
505 pending_selection: state.pending_selection,
506 scroll_top_anchor: state.scroll_top_anchor,
507 scroll_x: state.scroll_x,
508 scroll_y: state.scroll_y,
509 ..Default::default()
510 }),
511 cx,
512 )
513 }))
514 } else {
515 None
516 }
517 })?;
518
519 if let Some(task) = task {
520 task.await?;
521 }
522
523 Ok(this)
524 }))
525 }
526
527 fn add_event_to_update_proto(
528 &self,
529 event: &EditorEvent,
530 update: &mut Option<proto::update_view::Variant>,
531 cx: &WindowContext,
532 ) -> bool {
533 self.editor
534 .read(cx)
535 .add_event_to_update_proto(event, update, cx)
536 }
537
538 fn apply_update_proto(
539 &mut self,
540 project: &Model<Project>,
541 message: proto::update_view::Variant,
542 cx: &mut ViewContext<Self>,
543 ) -> gpui::Task<anyhow::Result<()>> {
544 self.editor.update(cx, |editor, cx| {
545 editor.apply_update_proto(project, message, cx)
546 })
547 }
548
549 fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
550 self.editor.update(cx, |editor, cx| {
551 editor.set_leader_peer_id(leader_peer_id, cx)
552 })
553 }
554
555 fn is_project_item(&self, _cx: &WindowContext) -> bool {
556 false
557 }
558
559 fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
560 Editor::to_follow_event(event)
561 }
562}
563
564struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
565
566impl CollaborationHub for ChannelBufferCollaborationHub {
567 fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
568 self.0.read(cx).collaborators()
569 }
570
571 fn user_participant_indices<'a>(
572 &self,
573 cx: &'a AppContext,
574 ) -> &'a HashMap<u64, ParticipantIndex> {
575 self.0.read(cx).user_store().read(cx).participant_indices()
576 }
577
578 fn user_names(&self, cx: &AppContext) -> HashMap<u64, SharedString> {
579 let user_ids = self.collaborators(cx).values().map(|c| c.user_id);
580 self.0
581 .read(cx)
582 .user_store()
583 .read(cx)
584 .participant_names(user_ids, cx)
585 }
586}