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