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