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::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!(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>, 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).into_any_element()
270 }
271
272 fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
273 Some(cx.build_view(|cx| {
274 Self::new(
275 self.project.clone(),
276 self.channel_store.clone(),
277 self.channel_buffer.clone(),
278 cx,
279 )
280 }))
281 }
282
283 fn is_singleton(&self, _cx: &AppContext) -> bool {
284 false
285 }
286
287 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
288 self.editor
289 .update(cx, |editor, cx| editor.navigate(data, cx))
290 }
291
292 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
293 self.editor
294 .update(cx, |editor, cx| Item::deactivated(editor, cx))
295 }
296
297 fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
298 self.editor
299 .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
300 }
301
302 fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
303 Some(Box::new(self.editor.clone()))
304 }
305
306 fn show_toolbar(&self) -> bool {
307 true
308 }
309
310 fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
311 self.editor.read(cx).pixel_position_of_cursor(cx)
312 }
313
314 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
315 Editor::to_item_events(event, f)
316 }
317}
318
319impl FollowableItem for ChannelView {
320 fn remote_id(&self) -> Option<workspace::ViewId> {
321 self.remote_id
322 }
323
324 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
325 let channel_buffer = self.channel_buffer.read(cx);
326 if !channel_buffer.is_connected() {
327 return None;
328 }
329
330 Some(proto::view::Variant::ChannelView(
331 proto::view::ChannelView {
332 channel_id: channel_buffer.channel_id,
333 editor: if let Some(proto::view::Variant::Editor(proto)) =
334 self.editor.read(cx).to_state_proto(cx)
335 {
336 Some(proto)
337 } else {
338 None
339 },
340 },
341 ))
342 }
343
344 fn from_state_proto(
345 pane: View<workspace::Pane>,
346 workspace: View<workspace::Workspace>,
347 remote_id: workspace::ViewId,
348 state: &mut Option<proto::view::Variant>,
349 cx: &mut WindowContext,
350 ) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
351 let Some(proto::view::Variant::ChannelView(_)) = state else {
352 return None;
353 };
354 let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
355 unreachable!()
356 };
357
358 let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
359
360 Some(cx.spawn(|mut cx| async move {
361 let this = open.await?;
362
363 let task = this.update(&mut cx, |this, cx| {
364 this.remote_id = Some(remote_id);
365
366 if let Some(state) = state.editor {
367 Some(this.editor.update(cx, |editor, cx| {
368 editor.apply_update_proto(
369 &this.project,
370 proto::update_view::Variant::Editor(proto::update_view::Editor {
371 selections: state.selections,
372 pending_selection: state.pending_selection,
373 scroll_top_anchor: state.scroll_top_anchor,
374 scroll_x: state.scroll_x,
375 scroll_y: state.scroll_y,
376 ..Default::default()
377 }),
378 cx,
379 )
380 }))
381 } else {
382 None
383 }
384 })?;
385
386 if let Some(task) = task {
387 task.await?;
388 }
389
390 Ok(this)
391 }))
392 }
393
394 fn add_event_to_update_proto(
395 &self,
396 event: &EditorEvent,
397 update: &mut Option<proto::update_view::Variant>,
398 cx: &WindowContext,
399 ) -> bool {
400 self.editor
401 .read(cx)
402 .add_event_to_update_proto(event, update, cx)
403 }
404
405 fn apply_update_proto(
406 &mut self,
407 project: &Model<Project>,
408 message: proto::update_view::Variant,
409 cx: &mut ViewContext<Self>,
410 ) -> gpui::Task<anyhow::Result<()>> {
411 self.editor.update(cx, |editor, cx| {
412 editor.apply_update_proto(project, message, cx)
413 })
414 }
415
416 fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
417 self.editor.update(cx, |editor, cx| {
418 editor.set_leader_peer_id(leader_peer_id, cx)
419 })
420 }
421
422 fn is_project_item(&self, _cx: &WindowContext) -> bool {
423 false
424 }
425
426 fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
427 Editor::to_follow_event(event)
428 }
429}
430
431struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
432
433impl CollaborationHub for ChannelBufferCollaborationHub {
434 fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
435 self.0.read(cx).collaborators()
436 }
437
438 fn user_participant_indices<'a>(
439 &self,
440 cx: &'a AppContext,
441 ) -> &'a HashMap<u64, ParticipantIndex> {
442 self.0.read(cx).user_store().read(cx).participant_indices()
443 }
444}