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.new_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.new_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
142 });
143 let _editor_event_subscription =
144 cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
145
146 cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
147 .detach();
148
149 Self {
150 editor,
151 project,
152 channel_store,
153 channel_buffer,
154 remote_id: None,
155 _editor_event_subscription,
156 }
157 }
158
159 pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
160 self.channel_buffer.read(cx).channel(cx)
161 }
162
163 fn handle_channel_buffer_event(
164 &mut self,
165 _: Model<ChannelBuffer>,
166 event: &ChannelBufferEvent,
167 cx: &mut ViewContext<Self>,
168 ) {
169 match event {
170 ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
171 editor.set_read_only(true);
172 cx.notify();
173 }),
174 ChannelBufferEvent::ChannelChanged => {
175 self.editor.update(cx, |_, cx| {
176 cx.emit(editor::EditorEvent::TitleChanged);
177 cx.notify()
178 });
179 }
180 ChannelBufferEvent::BufferEdited => {
181 if self.editor.read(cx).is_focused(cx) {
182 self.acknowledge_buffer_version(cx);
183 } else {
184 self.channel_store.update(cx, |store, cx| {
185 let channel_buffer = self.channel_buffer.read(cx);
186 store.notes_changed(
187 channel_buffer.channel_id,
188 channel_buffer.epoch(),
189 &channel_buffer.buffer().read(cx).version(),
190 cx,
191 )
192 });
193 }
194 }
195 ChannelBufferEvent::CollaboratorsChanged => {}
196 }
197 }
198
199 fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
200 self.channel_store.update(cx, |store, cx| {
201 let channel_buffer = self.channel_buffer.read(cx);
202 store.acknowledge_notes_version(
203 channel_buffer.channel_id,
204 channel_buffer.epoch(),
205 &channel_buffer.buffer().read(cx).version(),
206 cx,
207 )
208 });
209 self.channel_buffer.update(cx, |buffer, cx| {
210 buffer.acknowledge_buffer_version(cx);
211 });
212 }
213}
214
215impl EventEmitter<EditorEvent> for ChannelView {}
216
217impl Render for ChannelView {
218 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
219 self.editor.clone()
220 }
221}
222
223impl FocusableView for ChannelView {
224 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
225 self.editor.read(cx).focus_handle(cx)
226 }
227}
228
229impl Item for ChannelView {
230 type Event = EditorEvent;
231
232 fn act_as_type<'a>(
233 &'a self,
234 type_id: TypeId,
235 self_handle: &'a View<Self>,
236 _: &'a AppContext,
237 ) -> Option<AnyView> {
238 if type_id == TypeId::of::<Self>() {
239 Some(self_handle.to_any())
240 } else if type_id == TypeId::of::<Editor>() {
241 Some(self.editor.to_any())
242 } else {
243 None
244 }
245 }
246
247 fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
248 let label = if let Some(channel) = self.channel(cx) {
249 match (
250 self.channel_buffer.read(cx).buffer().read(cx).read_only(),
251 self.channel_buffer.read(cx).is_connected(),
252 ) {
253 (false, true) => format!("#{}", channel.name),
254 (true, true) => format!("#{} (read-only)", channel.name),
255 (_, false) => format!("#{} (disconnected)", channel.name),
256 }
257 } else {
258 format!("channel notes (disconnected)")
259 };
260 Label::new(label)
261 .color(if selected {
262 Color::Default
263 } else {
264 Color::Muted
265 })
266 .into_any_element()
267 }
268
269 fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
270 Some(cx.new_view(|cx| {
271 Self::new(
272 self.project.clone(),
273 self.channel_store.clone(),
274 self.channel_buffer.clone(),
275 cx,
276 )
277 }))
278 }
279
280 fn is_singleton(&self, _cx: &AppContext) -> bool {
281 false
282 }
283
284 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
285 self.editor
286 .update(cx, |editor, cx| editor.navigate(data, cx))
287 }
288
289 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
290 self.editor
291 .update(cx, |editor, cx| Item::deactivated(editor, cx))
292 }
293
294 fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
295 self.editor
296 .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
297 }
298
299 fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
300 Some(Box::new(self.editor.clone()))
301 }
302
303 fn show_toolbar(&self) -> bool {
304 true
305 }
306
307 fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
308 self.editor.read(cx).pixel_position_of_cursor(cx)
309 }
310
311 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
312 Editor::to_item_events(event, f)
313 }
314}
315
316impl FollowableItem for ChannelView {
317 fn remote_id(&self) -> Option<workspace::ViewId> {
318 self.remote_id
319 }
320
321 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
322 let channel_buffer = self.channel_buffer.read(cx);
323 if !channel_buffer.is_connected() {
324 return None;
325 }
326
327 Some(proto::view::Variant::ChannelView(
328 proto::view::ChannelView {
329 channel_id: channel_buffer.channel_id,
330 editor: if let Some(proto::view::Variant::Editor(proto)) =
331 self.editor.read(cx).to_state_proto(cx)
332 {
333 Some(proto)
334 } else {
335 None
336 },
337 },
338 ))
339 }
340
341 fn from_state_proto(
342 pane: View<workspace::Pane>,
343 workspace: View<workspace::Workspace>,
344 remote_id: workspace::ViewId,
345 state: &mut Option<proto::view::Variant>,
346 cx: &mut WindowContext,
347 ) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
348 let Some(proto::view::Variant::ChannelView(_)) = state else {
349 return None;
350 };
351 let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
352 unreachable!()
353 };
354
355 let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
356
357 Some(cx.spawn(|mut cx| async move {
358 let this = open.await?;
359
360 let task = this.update(&mut cx, |this, cx| {
361 this.remote_id = Some(remote_id);
362
363 if let Some(state) = state.editor {
364 Some(this.editor.update(cx, |editor, cx| {
365 editor.apply_update_proto(
366 &this.project,
367 proto::update_view::Variant::Editor(proto::update_view::Editor {
368 selections: state.selections,
369 pending_selection: state.pending_selection,
370 scroll_top_anchor: state.scroll_top_anchor,
371 scroll_x: state.scroll_x,
372 scroll_y: state.scroll_y,
373 ..Default::default()
374 }),
375 cx,
376 )
377 }))
378 } else {
379 None
380 }
381 })?;
382
383 if let Some(task) = task {
384 task.await?;
385 }
386
387 Ok(this)
388 }))
389 }
390
391 fn add_event_to_update_proto(
392 &self,
393 event: &EditorEvent,
394 update: &mut Option<proto::update_view::Variant>,
395 cx: &WindowContext,
396 ) -> bool {
397 self.editor
398 .read(cx)
399 .add_event_to_update_proto(event, update, cx)
400 }
401
402 fn apply_update_proto(
403 &mut self,
404 project: &Model<Project>,
405 message: proto::update_view::Variant,
406 cx: &mut ViewContext<Self>,
407 ) -> gpui::Task<anyhow::Result<()>> {
408 self.editor.update(cx, |editor, cx| {
409 editor.apply_update_proto(project, message, cx)
410 })
411 }
412
413 fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
414 self.editor.update(cx, |editor, cx| {
415 editor.set_leader_peer_id(leader_peer_id, cx)
416 })
417 }
418
419 fn is_project_item(&self, _cx: &WindowContext) -> bool {
420 false
421 }
422
423 fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
424 Editor::to_follow_event(event)
425 }
426}
427
428struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
429
430impl CollaborationHub for ChannelBufferCollaborationHub {
431 fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
432 self.0.read(cx).collaborators()
433 }
434
435 fn user_participant_indices<'a>(
436 &self,
437 cx: &'a AppContext,
438 ) -> &'a HashMap<u64, ParticipantIndex> {
439 self.0.read(cx).user_store().read(cx).participant_indices()
440 }
441}