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