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::{CollaborationHub, Editor, EditorEvent};
10use gpui::{
11 actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
12 IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
13 VisualContext as _, WindowContext,
14};
15use project::Project;
16use std::{
17 any::{Any, TypeId},
18 sync::Arc,
19};
20use ui::{prelude::*, Label};
21use util::ResultExt;
22use workspace::{
23 item::{FollowableItem, Item, ItemEvent, ItemHandle},
24 register_followable_item,
25 searchable::SearchableItemHandle,
26 ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
27};
28
29actions!(collab, [Deploy]);
30
31pub fn init(cx: &mut AppContext) {
32 register_followable_item::<ChannelView>(cx)
33}
34
35pub struct ChannelView {
36 pub editor: View<Editor>,
37 project: Model<Project>,
38 channel_store: Model<ChannelStore>,
39 channel_buffer: Model<ChannelBuffer>,
40 remote_id: Option<ViewId>,
41 _editor_event_subscription: Subscription,
42}
43
44impl ChannelView {
45 pub fn open(
46 channel_id: ChannelId,
47 workspace: View<Workspace>,
48 cx: &mut WindowContext,
49 ) -> Task<Result<View<Self>>> {
50 let pane = workspace.read(cx).active_pane().clone();
51 let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
52 cx.spawn(|mut cx| async move {
53 let channel_view = channel_view.await?;
54 pane.update(&mut cx, |pane, cx| {
55 report_call_event_for_channel(
56 "open channel notes",
57 channel_id,
58 &workspace.read(cx).app_state().client,
59 cx,
60 );
61 pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
62 })?;
63 anyhow::Ok(channel_view)
64 })
65 }
66
67 pub fn open_in_pane(
68 channel_id: ChannelId,
69 pane: View<Pane>,
70 workspace: View<Workspace>,
71 cx: &mut WindowContext,
72 ) -> Task<Result<View<Self>>> {
73 let workspace = workspace.read(cx);
74 let project = workspace.project().to_owned();
75 let channel_store = ChannelStore::global(cx);
76 let language_registry = workspace.app_state().languages.clone();
77 let markdown = language_registry.language_for_name("Markdown");
78 let channel_buffer =
79 channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
80
81 cx.spawn(|mut cx| async move {
82 let channel_buffer = channel_buffer.await?;
83 let markdown = markdown.await.log_err();
84
85 channel_buffer.update(&mut cx, |buffer, cx| {
86 buffer.buffer().update(cx, |buffer, cx| {
87 buffer.set_language_registry(language_registry);
88 if let Some(markdown) = markdown {
89 buffer.set_language(Some(markdown), cx);
90 }
91 })
92 })?;
93
94 pane.update(&mut cx, |pane, cx| {
95 let buffer_id = channel_buffer.read(cx).remote_id(cx);
96
97 let existing_view = pane
98 .items_of_type::<Self>()
99 .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
100
101 // If this channel buffer is already open in this pane, just return it.
102 if let Some(existing_view) = existing_view.clone() {
103 if existing_view.read(cx).channel_buffer == channel_buffer {
104 return existing_view;
105 }
106 }
107
108 let view = cx.build_view(|cx| {
109 let mut this = Self::new(project, channel_store, channel_buffer, cx);
110 this.acknowledge_buffer_version(cx);
111 this
112 });
113
114 // If the pane contained a disconnected view for this channel buffer,
115 // replace that.
116 if let Some(existing_item) = existing_view {
117 if let Some(ix) = pane.index_for_item(&existing_item) {
118 pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
119 .detach();
120 pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
121 }
122 }
123
124 view
125 })
126 })
127 }
128
129 pub fn new(
130 project: Model<Project>,
131 channel_store: Model<ChannelStore>,
132 channel_buffer: Model<ChannelBuffer>,
133 cx: &mut ViewContext<Self>,
134 ) -> Self {
135 let buffer = channel_buffer.read(cx).buffer();
136 let editor = cx.build_view(|cx| {
137 let mut editor = Editor::for_buffer(buffer, None, cx);
138 editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
139 channel_buffer.clone(),
140 )));
141 editor.set_read_only(
142 !channel_buffer
143 .read(cx)
144 .channel(cx)
145 .is_some_and(|c| c.can_edit_notes()),
146 );
147 editor
148 });
149 let _editor_event_subscription =
150 cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
151
152 cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
153 .detach();
154
155 Self {
156 editor,
157 project,
158 channel_store,
159 channel_buffer,
160 remote_id: None,
161 _editor_event_subscription,
162 }
163 }
164
165 pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
166 self.channel_buffer.read(cx).channel(cx)
167 }
168
169 fn handle_channel_buffer_event(
170 &mut self,
171 _: Model<ChannelBuffer>,
172 event: &ChannelBufferEvent,
173 cx: &mut ViewContext<Self>,
174 ) {
175 match event {
176 ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
177 editor.set_read_only(true);
178 cx.notify();
179 }),
180 ChannelBufferEvent::ChannelChanged => {
181 self.editor.update(cx, |editor, cx| {
182 editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
183 cx.emit(editor::EditorEvent::TitleChanged);
184 cx.notify()
185 });
186 }
187 ChannelBufferEvent::BufferEdited => {
188 if self.editor.read(cx).is_focused(cx) {
189 self.acknowledge_buffer_version(cx);
190 } else {
191 self.channel_store.update(cx, |store, cx| {
192 let channel_buffer = self.channel_buffer.read(cx);
193 store.notes_changed(
194 channel_buffer.channel_id,
195 channel_buffer.epoch(),
196 &channel_buffer.buffer().read(cx).version(),
197 cx,
198 )
199 });
200 }
201 }
202 ChannelBufferEvent::CollaboratorsChanged => {}
203 }
204 }
205
206 fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
207 self.channel_store.update(cx, |store, cx| {
208 let channel_buffer = self.channel_buffer.read(cx);
209 store.acknowledge_notes_version(
210 channel_buffer.channel_id,
211 channel_buffer.epoch(),
212 &channel_buffer.buffer().read(cx).version(),
213 cx,
214 )
215 });
216 self.channel_buffer.update(cx, |buffer, cx| {
217 buffer.acknowledge_buffer_version(cx);
218 });
219 }
220}
221
222impl EventEmitter<EditorEvent> for ChannelView {}
223
224impl Render for ChannelView {
225 type Element = AnyView;
226
227 fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
228 self.editor.clone().into()
229 }
230}
231
232impl FocusableView for ChannelView {
233 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
234 self.editor.read(cx).focus_handle(cx)
235 }
236}
237
238impl Item for ChannelView {
239 type Event = EditorEvent;
240
241 fn act_as_type<'a>(
242 &'a self,
243 type_id: TypeId,
244 self_handle: &'a View<Self>,
245 _: &'a AppContext,
246 ) -> Option<AnyView> {
247 if type_id == TypeId::of::<Self>() {
248 Some(self_handle.to_any())
249 } else if type_id == TypeId::of::<Editor>() {
250 Some(self.editor.to_any())
251 } else {
252 None
253 }
254 }
255
256 fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
257 let label = if let Some(channel) = self.channel(cx) {
258 match (
259 channel.can_edit_notes(),
260 self.channel_buffer.read(cx).is_connected(),
261 ) {
262 (true, true) => format!("#{}", channel.name),
263 (false, true) => format!("#{} (read-only)", channel.name),
264 (_, false) => format!("#{} (disconnected)", channel.name),
265 }
266 } else {
267 format!("channel notes (disconnected)")
268 };
269 Label::new(label)
270 .color(if selected {
271 Color::Default
272 } else {
273 Color::Muted
274 })
275 .into_any_element()
276 }
277
278 fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
279 Some(cx.build_view(|cx| {
280 Self::new(
281 self.project.clone(),
282 self.channel_store.clone(),
283 self.channel_buffer.clone(),
284 cx,
285 )
286 }))
287 }
288
289 fn is_singleton(&self, _cx: &AppContext) -> bool {
290 false
291 }
292
293 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
294 self.editor
295 .update(cx, |editor, cx| editor.navigate(data, cx))
296 }
297
298 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
299 self.editor
300 .update(cx, |editor, cx| Item::deactivated(editor, cx))
301 }
302
303 fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
304 self.editor
305 .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
306 }
307
308 fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
309 Some(Box::new(self.editor.clone()))
310 }
311
312 fn show_toolbar(&self) -> bool {
313 true
314 }
315
316 fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
317 self.editor.read(cx).pixel_position_of_cursor(cx)
318 }
319
320 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
321 Editor::to_item_events(event, f)
322 }
323}
324
325impl FollowableItem for ChannelView {
326 fn remote_id(&self) -> Option<workspace::ViewId> {
327 self.remote_id
328 }
329
330 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
331 let channel_buffer = self.channel_buffer.read(cx);
332 if !channel_buffer.is_connected() {
333 return None;
334 }
335
336 Some(proto::view::Variant::ChannelView(
337 proto::view::ChannelView {
338 channel_id: channel_buffer.channel_id,
339 editor: if let Some(proto::view::Variant::Editor(proto)) =
340 self.editor.read(cx).to_state_proto(cx)
341 {
342 Some(proto)
343 } else {
344 None
345 },
346 },
347 ))
348 }
349
350 fn from_state_proto(
351 pane: View<workspace::Pane>,
352 workspace: View<workspace::Workspace>,
353 remote_id: workspace::ViewId,
354 state: &mut Option<proto::view::Variant>,
355 cx: &mut WindowContext,
356 ) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
357 let Some(proto::view::Variant::ChannelView(_)) = state else {
358 return None;
359 };
360 let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
361 unreachable!()
362 };
363
364 let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
365
366 Some(cx.spawn(|mut cx| async move {
367 let this = open.await?;
368
369 let task = this.update(&mut cx, |this, cx| {
370 this.remote_id = Some(remote_id);
371
372 if let Some(state) = state.editor {
373 Some(this.editor.update(cx, |editor, cx| {
374 editor.apply_update_proto(
375 &this.project,
376 proto::update_view::Variant::Editor(proto::update_view::Editor {
377 selections: state.selections,
378 pending_selection: state.pending_selection,
379 scroll_top_anchor: state.scroll_top_anchor,
380 scroll_x: state.scroll_x,
381 scroll_y: state.scroll_y,
382 ..Default::default()
383 }),
384 cx,
385 )
386 }))
387 } else {
388 None
389 }
390 })?;
391
392 if let Some(task) = task {
393 task.await?;
394 }
395
396 Ok(this)
397 }))
398 }
399
400 fn add_event_to_update_proto(
401 &self,
402 event: &EditorEvent,
403 update: &mut Option<proto::update_view::Variant>,
404 cx: &WindowContext,
405 ) -> bool {
406 self.editor
407 .read(cx)
408 .add_event_to_update_proto(event, update, cx)
409 }
410
411 fn apply_update_proto(
412 &mut self,
413 project: &Model<Project>,
414 message: proto::update_view::Variant,
415 cx: &mut ViewContext<Self>,
416 ) -> gpui::Task<anyhow::Result<()>> {
417 self.editor.update(cx, |editor, cx| {
418 editor.apply_update_proto(project, message, cx)
419 })
420 }
421
422 fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
423 self.editor.update(cx, |editor, cx| {
424 editor.set_leader_peer_id(leader_peer_id, cx)
425 })
426 }
427
428 fn is_project_item(&self, _cx: &WindowContext) -> bool {
429 false
430 }
431
432 fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
433 Editor::to_follow_event(event)
434 }
435}
436
437struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
438
439impl CollaborationHub for ChannelBufferCollaborationHub {
440 fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
441 self.0.read(cx).collaborators()
442 }
443
444 fn user_participant_indices<'a>(
445 &self,
446 cx: &'a AppContext,
447 ) -> &'a HashMap<u64, ParticipantIndex> {
448 self.0.read(cx).user_store().read(cx).participant_indices()
449 }
450}