1use anyhow::{anyhow, Result};
2use call::ActiveCall;
3use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId};
4use client::proto;
5use clock::ReplicaId;
6use collections::HashMap;
7use editor::Editor;
8use gpui::{
9 actions,
10 elements::{ChildView, Label},
11 geometry::vector::Vector2F,
12 AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
13 ViewContext, ViewHandle,
14};
15use project::Project;
16use std::any::{Any, TypeId};
17use workspace::{
18 item::{FollowableItem, Item, ItemHandle},
19 register_followable_item,
20 searchable::SearchableItemHandle,
21 ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId,
22};
23
24actions!(channel_view, [Deploy]);
25
26pub(crate) fn init(cx: &mut AppContext) {
27 register_followable_item::<ChannelView>(cx)
28}
29
30pub struct ChannelView {
31 pub editor: ViewHandle<Editor>,
32 project: ModelHandle<Project>,
33 channel_buffer: ModelHandle<ChannelBuffer>,
34 remote_id: Option<ViewId>,
35 _editor_event_subscription: Subscription,
36}
37
38impl ChannelView {
39 pub fn deploy(channel_id: ChannelId, workspace: ViewHandle<Workspace>, cx: &mut AppContext) {
40 let pane = workspace.read(cx).active_pane().clone();
41 let channel_view = Self::open(channel_id, pane.clone(), workspace.clone(), cx);
42 cx.spawn(|mut cx| async move {
43 let channel_view = channel_view.await?;
44 pane.update(&mut cx, |pane, cx| {
45 let room_id = ActiveCall::global(cx)
46 .read(cx)
47 .room()
48 .map(|room| room.read(cx).id());
49 ActiveCall::report_call_event_for_room(
50 "open channel notes",
51 room_id,
52 Some(channel_id),
53 &workspace.read(cx).app_state().client,
54 cx,
55 );
56 pane.add_item(Box::new(channel_view), true, true, None, cx);
57 });
58 anyhow::Ok(())
59 })
60 .detach();
61 }
62
63 pub fn open(
64 channel_id: ChannelId,
65 pane: ViewHandle<Pane>,
66 workspace: ViewHandle<Workspace>,
67 cx: &mut AppContext,
68 ) -> Task<Result<ViewHandle<Self>>> {
69 let workspace = workspace.read(cx);
70 let project = workspace.project().to_owned();
71 let channel_store = workspace.app_state().channel_store.clone();
72 let markdown = workspace
73 .app_state()
74 .languages
75 .language_for_name("Markdown");
76 let channel_buffer =
77 channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
78
79 cx.spawn(|mut cx| async move {
80 let channel_buffer = channel_buffer.await?;
81
82 let markdown = markdown.await?;
83 channel_buffer.update(&mut cx, |buffer, cx| {
84 buffer.buffer().update(cx, |buffer, cx| {
85 buffer.set_language(Some(markdown), cx);
86 })
87 });
88
89 pane.update(&mut cx, |pane, cx| {
90 pane.items_of_type::<Self>()
91 .find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer)
92 .unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, cx)))
93 })
94 .ok_or_else(|| anyhow!("pane was dropped"))
95 })
96 }
97
98 pub fn new(
99 project: ModelHandle<Project>,
100 channel_buffer: ModelHandle<ChannelBuffer>,
101 cx: &mut ViewContext<Self>,
102 ) -> Self {
103 let buffer = channel_buffer.read(cx).buffer();
104 let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx));
105 let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
106
107 cx.subscribe(&project, Self::handle_project_event).detach();
108 cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
109 .detach();
110
111 let this = Self {
112 editor,
113 project,
114 channel_buffer,
115 remote_id: None,
116 _editor_event_subscription,
117 };
118 this.refresh_replica_id_map(cx);
119 this
120 }
121
122 fn handle_project_event(
123 &mut self,
124 _: ModelHandle<Project>,
125 event: &project::Event,
126 cx: &mut ViewContext<Self>,
127 ) {
128 match event {
129 project::Event::RemoteIdChanged(_) => {}
130 project::Event::DisconnectedFromHost => {}
131 project::Event::Closed => {}
132 project::Event::CollaboratorUpdated { .. } => {}
133 project::Event::CollaboratorLeft(_) => {}
134 project::Event::CollaboratorJoined(_) => {}
135 _ => return,
136 }
137 self.refresh_replica_id_map(cx);
138 }
139
140 fn handle_channel_buffer_event(
141 &mut self,
142 _: ModelHandle<ChannelBuffer>,
143 event: &ChannelBufferEvent,
144 cx: &mut ViewContext<Self>,
145 ) {
146 match event {
147 ChannelBufferEvent::CollaboratorsChanged => {
148 self.refresh_replica_id_map(cx);
149 }
150 ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
151 editor.set_read_only(true);
152 cx.notify();
153 }),
154 }
155 }
156
157 /// Build a mapping of channel buffer replica ids to the corresponding
158 /// replica ids in the current project.
159 ///
160 /// Using this mapping, a given user can be displayed with the same color
161 /// in the channel buffer as in other files in the project. Users who are
162 /// in the channel buffer but not the project will not have a color.
163 fn refresh_replica_id_map(&self, cx: &mut ViewContext<Self>) {
164 let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default();
165 let project = self.project.read(cx);
166 let channel_buffer = self.channel_buffer.read(cx);
167 project_replica_ids_by_channel_buffer_replica_id
168 .insert(channel_buffer.replica_id(cx), project.replica_id());
169 project_replica_ids_by_channel_buffer_replica_id.extend(
170 channel_buffer
171 .collaborators()
172 .iter()
173 .filter_map(|channel_buffer_collaborator| {
174 project
175 .collaborators()
176 .values()
177 .find_map(|project_collaborator| {
178 (project_collaborator.user_id == channel_buffer_collaborator.user_id)
179 .then_some((
180 channel_buffer_collaborator.replica_id as ReplicaId,
181 project_collaborator.replica_id,
182 ))
183 })
184 }),
185 );
186
187 self.editor.update(cx, |editor, cx| {
188 editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx)
189 });
190 }
191}
192
193impl Entity for ChannelView {
194 type Event = editor::Event;
195}
196
197impl View for ChannelView {
198 fn ui_name() -> &'static str {
199 "ChannelView"
200 }
201
202 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
203 ChildView::new(self.editor.as_any(), cx).into_any()
204 }
205
206 fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
207 if cx.is_self_focused() {
208 cx.focus(self.editor.as_any())
209 }
210 }
211}
212
213impl Item for ChannelView {
214 fn act_as_type<'a>(
215 &'a self,
216 type_id: TypeId,
217 self_handle: &'a ViewHandle<Self>,
218 _: &'a AppContext,
219 ) -> Option<&'a AnyViewHandle> {
220 if type_id == TypeId::of::<Self>() {
221 Some(self_handle)
222 } else if type_id == TypeId::of::<Editor>() {
223 Some(&self.editor)
224 } else {
225 None
226 }
227 }
228
229 fn tab_content<V: 'static>(
230 &self,
231 _: Option<usize>,
232 style: &theme::Tab,
233 cx: &gpui::AppContext,
234 ) -> AnyElement<V> {
235 let channel_name = &self.channel_buffer.read(cx).channel().name;
236 let label = if self.channel_buffer.read(cx).is_connected() {
237 format!("#{}", channel_name)
238 } else {
239 format!("#{} (disconnected)", channel_name)
240 };
241 Label::new(label, style.label.to_owned()).into_any()
242 }
243
244 fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
245 Some(Self::new(
246 self.project.clone(),
247 self.channel_buffer.clone(),
248 cx,
249 ))
250 }
251
252 fn is_singleton(&self, _cx: &AppContext) -> bool {
253 false
254 }
255
256 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
257 self.editor
258 .update(cx, |editor, cx| editor.navigate(data, cx))
259 }
260
261 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
262 self.editor
263 .update(cx, |editor, cx| Item::deactivated(editor, cx))
264 }
265
266 fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
267 self.editor
268 .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
269 }
270
271 fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
272 Some(Box::new(self.editor.clone()))
273 }
274
275 fn show_toolbar(&self) -> bool {
276 true
277 }
278
279 fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
280 self.editor.read(cx).pixel_position_of_cursor(cx)
281 }
282}
283
284impl FollowableItem for ChannelView {
285 fn remote_id(&self) -> Option<workspace::ViewId> {
286 self.remote_id
287 }
288
289 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
290 let channel = self.channel_buffer.read(cx).channel();
291 Some(proto::view::Variant::ChannelView(
292 proto::view::ChannelView {
293 channel_id: channel.id,
294 editor: if let Some(proto::view::Variant::Editor(proto)) =
295 self.editor.read(cx).to_state_proto(cx)
296 {
297 Some(proto)
298 } else {
299 None
300 },
301 },
302 ))
303 }
304
305 fn from_state_proto(
306 pane: ViewHandle<workspace::Pane>,
307 workspace: ViewHandle<workspace::Workspace>,
308 remote_id: workspace::ViewId,
309 state: &mut Option<proto::view::Variant>,
310 cx: &mut AppContext,
311 ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
312 let Some(proto::view::Variant::ChannelView(_)) = state else {
313 return None;
314 };
315 let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
316 unreachable!()
317 };
318
319 let open = ChannelView::open(state.channel_id, pane, workspace, cx);
320
321 Some(cx.spawn(|mut cx| async move {
322 let this = open.await?;
323
324 let task = this
325 .update(&mut cx, |this, cx| {
326 this.remote_id = Some(remote_id);
327
328 if let Some(state) = state.editor {
329 Some(this.editor.update(cx, |editor, cx| {
330 editor.apply_update_proto(
331 &this.project,
332 proto::update_view::Variant::Editor(proto::update_view::Editor {
333 selections: state.selections,
334 pending_selection: state.pending_selection,
335 scroll_top_anchor: state.scroll_top_anchor,
336 scroll_x: state.scroll_x,
337 scroll_y: state.scroll_y,
338 ..Default::default()
339 }),
340 cx,
341 )
342 }))
343 } else {
344 None
345 }
346 })
347 .ok_or_else(|| anyhow!("window was closed"))?;
348
349 if let Some(task) = task {
350 task.await?;
351 }
352
353 Ok(this)
354 }))
355 }
356
357 fn add_event_to_update_proto(
358 &self,
359 event: &Self::Event,
360 update: &mut Option<proto::update_view::Variant>,
361 cx: &AppContext,
362 ) -> bool {
363 self.editor
364 .read(cx)
365 .add_event_to_update_proto(event, update, cx)
366 }
367
368 fn apply_update_proto(
369 &mut self,
370 project: &ModelHandle<Project>,
371 message: proto::update_view::Variant,
372 cx: &mut ViewContext<Self>,
373 ) -> gpui::Task<anyhow::Result<()>> {
374 self.editor.update(cx, |editor, cx| {
375 editor.apply_update_proto(project, message, cx)
376 })
377 }
378
379 fn set_leader_replica_id(
380 &mut self,
381 leader_replica_id: Option<u16>,
382 cx: &mut ViewContext<Self>,
383 ) {
384 self.editor.update(cx, |editor, cx| {
385 editor.set_leader_replica_id(leader_replica_id, cx)
386 })
387 }
388
389 fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
390 Editor::should_unfollow_on_event(event, cx)
391 }
392}