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