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.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 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
226 self.editor.clone()
227 }
228}
229
230impl FocusableView for ChannelView {
231 fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
232 self.editor.read(cx).focus_handle(cx)
233 }
234}
235
236impl Item for ChannelView {
237 type Event = EditorEvent;
238
239 fn act_as_type<'a>(
240 &'a self,
241 type_id: TypeId,
242 self_handle: &'a View<Self>,
243 _: &'a AppContext,
244 ) -> Option<AnyView> {
245 if type_id == TypeId::of::<Self>() {
246 Some(self_handle.to_any())
247 } else if type_id == TypeId::of::<Editor>() {
248 Some(self.editor.to_any())
249 } else {
250 None
251 }
252 }
253
254 fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
255 let label = if let Some(channel) = self.channel(cx) {
256 match (
257 channel.can_edit_notes(),
258 self.channel_buffer.read(cx).is_connected(),
259 ) {
260 (true, true) => format!("#{}", channel.name),
261 (false, true) => format!("#{} (read-only)", channel.name),
262 (_, false) => format!("#{} (disconnected)", channel.name),
263 }
264 } else {
265 format!("channel notes (disconnected)")
266 };
267 Label::new(label)
268 .color(if selected {
269 Color::Default
270 } else {
271 Color::Muted
272 })
273 .into_any_element()
274 }
275
276 fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
277 Some(cx.new_view(|cx| {
278 Self::new(
279 self.project.clone(),
280 self.channel_store.clone(),
281 self.channel_buffer.clone(),
282 cx,
283 )
284 }))
285 }
286
287 fn is_singleton(&self, _cx: &AppContext) -> bool {
288 false
289 }
290
291 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
292 self.editor
293 .update(cx, |editor, cx| editor.navigate(data, cx))
294 }
295
296 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
297 self.editor
298 .update(cx, |editor, cx| Item::deactivated(editor, cx))
299 }
300
301 fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
302 self.editor
303 .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
304 }
305
306 fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
307 Some(Box::new(self.editor.clone()))
308 }
309
310 fn show_toolbar(&self) -> bool {
311 true
312 }
313
314 fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
315 self.editor.read(cx).pixel_position_of_cursor(cx)
316 }
317
318 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
319 Editor::to_item_events(event, f)
320 }
321}
322
323impl FollowableItem for ChannelView {
324 fn remote_id(&self) -> Option<workspace::ViewId> {
325 self.remote_id
326 }
327
328 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
329 let channel_buffer = self.channel_buffer.read(cx);
330 if !channel_buffer.is_connected() {
331 return None;
332 }
333
334 Some(proto::view::Variant::ChannelView(
335 proto::view::ChannelView {
336 channel_id: channel_buffer.channel_id,
337 editor: if let Some(proto::view::Variant::Editor(proto)) =
338 self.editor.read(cx).to_state_proto(cx)
339 {
340 Some(proto)
341 } else {
342 None
343 },
344 },
345 ))
346 }
347
348 fn from_state_proto(
349 pane: View<workspace::Pane>,
350 workspace: View<workspace::Workspace>,
351 remote_id: workspace::ViewId,
352 state: &mut Option<proto::view::Variant>,
353 cx: &mut WindowContext,
354 ) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
355 let Some(proto::view::Variant::ChannelView(_)) = state else {
356 return None;
357 };
358 let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
359 unreachable!()
360 };
361
362 let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
363
364 Some(cx.spawn(|mut cx| async move {
365 let this = open.await?;
366
367 let task = this.update(&mut cx, |this, cx| {
368 this.remote_id = Some(remote_id);
369
370 if let Some(state) = state.editor {
371 Some(this.editor.update(cx, |editor, cx| {
372 editor.apply_update_proto(
373 &this.project,
374 proto::update_view::Variant::Editor(proto::update_view::Editor {
375 selections: state.selections,
376 pending_selection: state.pending_selection,
377 scroll_top_anchor: state.scroll_top_anchor,
378 scroll_x: state.scroll_x,
379 scroll_y: state.scroll_y,
380 ..Default::default()
381 }),
382 cx,
383 )
384 }))
385 } else {
386 None
387 }
388 })?;
389
390 if let Some(task) = task {
391 task.await?;
392 }
393
394 Ok(this)
395 }))
396 }
397
398 fn add_event_to_update_proto(
399 &self,
400 event: &EditorEvent,
401 update: &mut Option<proto::update_view::Variant>,
402 cx: &WindowContext,
403 ) -> bool {
404 self.editor
405 .read(cx)
406 .add_event_to_update_proto(event, update, cx)
407 }
408
409 fn apply_update_proto(
410 &mut self,
411 project: &Model<Project>,
412 message: proto::update_view::Variant,
413 cx: &mut ViewContext<Self>,
414 ) -> gpui::Task<anyhow::Result<()>> {
415 self.editor.update(cx, |editor, cx| {
416 editor.apply_update_proto(project, message, cx)
417 })
418 }
419
420 fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
421 self.editor.update(cx, |editor, cx| {
422 editor.set_leader_peer_id(leader_peer_id, cx)
423 })
424 }
425
426 fn is_project_item(&self, _cx: &WindowContext) -> bool {
427 false
428 }
429
430 fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
431 Editor::to_follow_event(event)
432 }
433}
434
435struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
436
437impl CollaborationHub for ChannelBufferCollaborationHub {
438 fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
439 self.0.read(cx).collaborators()
440 }
441
442 fn user_participant_indices<'a>(
443 &self,
444 cx: &'a AppContext,
445 ) -> &'a HashMap<u64, ParticipantIndex> {
446 self.0.read(cx).user_store().read(cx).participant_indices()
447 }
448}