1pub mod pane;
2pub mod pane_group;
3pub mod sidebar;
4mod status_bar;
5mod toolbar;
6mod waiting_room;
7
8use anyhow::{anyhow, Context, Result};
9use client::{
10 proto, Authenticate, Client, Contact, PeerId, Subscription, TypedEnvelope, User, UserStore,
11};
12use clock::ReplicaId;
13use collections::{hash_map, HashMap, HashSet};
14use gpui::{
15 actions,
16 color::Color,
17 elements::*,
18 geometry::{rect::RectF, vector::vec2f, PathBuilder},
19 impl_actions, impl_internal_actions,
20 json::{self, ToJson},
21 platform::{CursorStyle, WindowOptions},
22 AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData,
23 ModelContext, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext,
24 Task, View, ViewContext, ViewHandle, WeakViewHandle,
25};
26use language::LanguageRegistry;
27use log::error;
28pub use pane::*;
29pub use pane_group::*;
30use postage::prelude::Stream;
31use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
32use serde::Deserialize;
33use settings::Settings;
34use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
35use smallvec::SmallVec;
36use status_bar::StatusBar;
37pub use status_bar::StatusItemView;
38use std::{
39 any::{Any, TypeId},
40 borrow::Cow,
41 cell::RefCell,
42 fmt,
43 future::Future,
44 path::{Path, PathBuf},
45 rc::Rc,
46 sync::{
47 atomic::{AtomicBool, Ordering::SeqCst},
48 Arc,
49 },
50};
51use theme::{Theme, ThemeRegistry};
52pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
53use util::ResultExt;
54use waiting_room::WaitingRoom;
55
56type ProjectItemBuilders = HashMap<
57 TypeId,
58 fn(usize, ModelHandle<Project>, AnyModelHandle, &mut MutableAppContext) -> Box<dyn ItemHandle>,
59>;
60
61type FollowableItemBuilder = fn(
62 ViewHandle<Pane>,
63 ModelHandle<Project>,
64 &mut Option<proto::view::Variant>,
65 &mut MutableAppContext,
66) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
67type FollowableItemBuilders = HashMap<
68 TypeId,
69 (
70 FollowableItemBuilder,
71 fn(AnyViewHandle) -> Box<dyn FollowableItemHandle>,
72 ),
73>;
74
75#[derive(Clone, PartialEq)]
76pub struct RemoveWorktreeFromProject(pub WorktreeId);
77
78actions!(
79 workspace,
80 [
81 Open,
82 NewFile,
83 NewWindow,
84 CloseWindow,
85 AddFolderToProject,
86 Unfollow,
87 Save,
88 SaveAs,
89 SaveAll,
90 ActivatePreviousPane,
91 ActivateNextPane,
92 FollowNextCollaborator,
93 ToggleLeftSidebar,
94 ToggleRightSidebar,
95 ]
96);
97
98#[derive(Clone, PartialEq)]
99pub struct OpenPaths {
100 pub paths: Vec<PathBuf>,
101}
102
103#[derive(Clone, Deserialize, PartialEq)]
104pub struct ToggleProjectOnline {
105 #[serde(skip_deserializing)]
106 pub project: Option<ModelHandle<Project>>,
107}
108
109#[derive(Clone, Deserialize, PartialEq)]
110pub struct ActivatePane(pub usize);
111
112#[derive(Clone, PartialEq)]
113pub struct ToggleFollow(pub PeerId);
114
115#[derive(Clone, PartialEq)]
116pub struct JoinProject {
117 pub contact: Arc<Contact>,
118 pub project_index: usize,
119}
120
121impl_internal_actions!(
122 workspace,
123 [
124 OpenPaths,
125 ToggleFollow,
126 JoinProject,
127 RemoveWorktreeFromProject
128 ]
129);
130impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
131
132pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
133 pane::init(cx);
134
135 cx.add_global_action(open);
136 cx.add_global_action({
137 let app_state = Arc::downgrade(&app_state);
138 move |action: &OpenPaths, cx: &mut MutableAppContext| {
139 if let Some(app_state) = app_state.upgrade() {
140 open_paths(&action.paths, &app_state, cx).detach();
141 }
142 }
143 });
144 cx.add_global_action({
145 let app_state = Arc::downgrade(&app_state);
146 move |_: &NewFile, cx: &mut MutableAppContext| {
147 if let Some(app_state) = app_state.upgrade() {
148 open_new(&app_state, cx)
149 }
150 }
151 });
152 cx.add_global_action({
153 let app_state = Arc::downgrade(&app_state);
154 move |_: &NewWindow, cx: &mut MutableAppContext| {
155 if let Some(app_state) = app_state.upgrade() {
156 open_new(&app_state, cx)
157 }
158 }
159 });
160 cx.add_global_action({
161 let app_state = Arc::downgrade(&app_state);
162 move |action: &JoinProject, cx: &mut MutableAppContext| {
163 if let Some(app_state) = app_state.upgrade() {
164 join_project(action.contact.clone(), action.project_index, &app_state, cx);
165 }
166 }
167 });
168
169 cx.add_async_action(Workspace::toggle_follow);
170 cx.add_async_action(Workspace::follow_next_collaborator);
171 cx.add_async_action(Workspace::close);
172 cx.add_async_action(Workspace::save_all);
173 cx.add_action(Workspace::add_folder_to_project);
174 cx.add_action(Workspace::remove_folder_from_project);
175 cx.add_action(Workspace::toggle_project_online);
176 cx.add_action(
177 |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
178 let pane = workspace.active_pane().clone();
179 workspace.unfollow(&pane, cx);
180 },
181 );
182 cx.add_action(
183 |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
184 workspace.save_active_item(false, cx).detach_and_log_err(cx);
185 },
186 );
187 cx.add_action(
188 |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
189 workspace.save_active_item(true, cx).detach_and_log_err(cx);
190 },
191 );
192 cx.add_action(Workspace::toggle_sidebar_item);
193 cx.add_action(Workspace::focus_center);
194 cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
195 workspace.activate_previous_pane(cx)
196 });
197 cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
198 workspace.activate_next_pane(cx)
199 });
200 cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| {
201 workspace.toggle_sidebar(Side::Left, cx);
202 });
203 cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| {
204 workspace.toggle_sidebar(Side::Right, cx);
205 });
206 cx.add_action(Workspace::activate_pane_at_index);
207
208 let client = &app_state.client;
209 client.add_view_request_handler(Workspace::handle_follow);
210 client.add_view_message_handler(Workspace::handle_unfollow);
211 client.add_view_message_handler(Workspace::handle_update_followers);
212}
213
214pub fn register_project_item<I: ProjectItem>(cx: &mut MutableAppContext) {
215 cx.update_default_global(|builders: &mut ProjectItemBuilders, _| {
216 builders.insert(TypeId::of::<I::Item>(), |window_id, project, model, cx| {
217 let item = model.downcast::<I::Item>().unwrap();
218 Box::new(cx.add_view(window_id, |cx| I::for_project_item(project, item, cx)))
219 });
220 });
221}
222
223pub fn register_followable_item<I: FollowableItem>(cx: &mut MutableAppContext) {
224 cx.update_default_global(|builders: &mut FollowableItemBuilders, _| {
225 builders.insert(
226 TypeId::of::<I>(),
227 (
228 |pane, project, state, cx| {
229 I::from_state_proto(pane, project, state, cx).map(|task| {
230 cx.foreground()
231 .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
232 })
233 },
234 |this| Box::new(this.downcast::<I>().unwrap()),
235 ),
236 );
237 });
238}
239
240pub struct AppState {
241 pub languages: Arc<LanguageRegistry>,
242 pub themes: Arc<ThemeRegistry>,
243 pub client: Arc<client::Client>,
244 pub user_store: ModelHandle<client::UserStore>,
245 pub project_store: ModelHandle<ProjectStore>,
246 pub fs: Arc<dyn fs::Fs>,
247 pub build_window_options: fn() -> WindowOptions<'static>,
248 pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
249}
250
251pub trait Item: View {
252 fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
253 fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
254 false
255 }
256 fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
257 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
258 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
259 fn is_singleton(&self, cx: &AppContext) -> bool;
260 fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
261 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
262 where
263 Self: Sized,
264 {
265 None
266 }
267 fn is_dirty(&self, _: &AppContext) -> bool {
268 false
269 }
270 fn has_conflict(&self, _: &AppContext) -> bool {
271 false
272 }
273 fn can_save(&self, cx: &AppContext) -> bool;
274 fn save(
275 &mut self,
276 project: ModelHandle<Project>,
277 cx: &mut ViewContext<Self>,
278 ) -> Task<Result<()>>;
279 fn save_as(
280 &mut self,
281 project: ModelHandle<Project>,
282 abs_path: PathBuf,
283 cx: &mut ViewContext<Self>,
284 ) -> Task<Result<()>>;
285 fn reload(
286 &mut self,
287 project: ModelHandle<Project>,
288 cx: &mut ViewContext<Self>,
289 ) -> Task<Result<()>>;
290 fn should_activate_item_on_event(_: &Self::Event) -> bool {
291 false
292 }
293 fn should_close_item_on_event(_: &Self::Event) -> bool {
294 false
295 }
296 fn should_update_tab_on_event(_: &Self::Event) -> bool {
297 false
298 }
299 fn act_as_type(
300 &self,
301 type_id: TypeId,
302 self_handle: &ViewHandle<Self>,
303 _: &AppContext,
304 ) -> Option<AnyViewHandle> {
305 if TypeId::of::<Self>() == type_id {
306 Some(self_handle.into())
307 } else {
308 None
309 }
310 }
311}
312
313pub trait ProjectItem: Item {
314 type Item: project::Item;
315
316 fn for_project_item(
317 project: ModelHandle<Project>,
318 item: ModelHandle<Self::Item>,
319 cx: &mut ViewContext<Self>,
320 ) -> Self;
321}
322
323pub trait FollowableItem: Item {
324 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
325 fn from_state_proto(
326 pane: ViewHandle<Pane>,
327 project: ModelHandle<Project>,
328 state: &mut Option<proto::view::Variant>,
329 cx: &mut MutableAppContext,
330 ) -> Option<Task<Result<ViewHandle<Self>>>>;
331 fn add_event_to_update_proto(
332 &self,
333 event: &Self::Event,
334 update: &mut Option<proto::update_view::Variant>,
335 cx: &AppContext,
336 ) -> bool;
337 fn apply_update_proto(
338 &mut self,
339 message: proto::update_view::Variant,
340 cx: &mut ViewContext<Self>,
341 ) -> Result<()>;
342
343 fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
344 fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool;
345}
346
347pub trait FollowableItemHandle: ItemHandle {
348 fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext);
349 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
350 fn add_event_to_update_proto(
351 &self,
352 event: &dyn Any,
353 update: &mut Option<proto::update_view::Variant>,
354 cx: &AppContext,
355 ) -> bool;
356 fn apply_update_proto(
357 &self,
358 message: proto::update_view::Variant,
359 cx: &mut MutableAppContext,
360 ) -> Result<()>;
361 fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
362}
363
364impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
365 fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, cx: &mut MutableAppContext) {
366 self.update(cx, |this, cx| {
367 this.set_leader_replica_id(leader_replica_id, cx)
368 })
369 }
370
371 fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
372 self.read(cx).to_state_proto(cx)
373 }
374
375 fn add_event_to_update_proto(
376 &self,
377 event: &dyn Any,
378 update: &mut Option<proto::update_view::Variant>,
379 cx: &AppContext,
380 ) -> bool {
381 if let Some(event) = event.downcast_ref() {
382 self.read(cx).add_event_to_update_proto(event, update, cx)
383 } else {
384 false
385 }
386 }
387
388 fn apply_update_proto(
389 &self,
390 message: proto::update_view::Variant,
391 cx: &mut MutableAppContext,
392 ) -> Result<()> {
393 self.update(cx, |this, cx| this.apply_update_proto(message, cx))
394 }
395
396 fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool {
397 if let Some(event) = event.downcast_ref() {
398 T::should_unfollow_on_event(event, cx)
399 } else {
400 false
401 }
402 }
403}
404
405pub trait ItemHandle: 'static + fmt::Debug {
406 fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
407 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
408 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
409 fn is_singleton(&self, cx: &AppContext) -> bool;
410 fn boxed_clone(&self) -> Box<dyn ItemHandle>;
411 fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext);
412 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
413 fn added_to_pane(
414 &self,
415 workspace: &mut Workspace,
416 pane: ViewHandle<Pane>,
417 cx: &mut ViewContext<Workspace>,
418 );
419 fn deactivated(&self, cx: &mut MutableAppContext);
420 fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool;
421 fn id(&self) -> usize;
422 fn to_any(&self) -> AnyViewHandle;
423 fn is_dirty(&self, cx: &AppContext) -> bool;
424 fn has_conflict(&self, cx: &AppContext) -> bool;
425 fn can_save(&self, cx: &AppContext) -> bool;
426 fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>>;
427 fn save_as(
428 &self,
429 project: ModelHandle<Project>,
430 abs_path: PathBuf,
431 cx: &mut MutableAppContext,
432 ) -> Task<Result<()>>;
433 fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
434 -> Task<Result<()>>;
435 fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
436 fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
437 fn on_release(
438 &self,
439 cx: &mut MutableAppContext,
440 callback: Box<dyn FnOnce(&mut MutableAppContext)>,
441 ) -> gpui::Subscription;
442}
443
444pub trait WeakItemHandle {
445 fn id(&self) -> usize;
446 fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
447}
448
449impl dyn ItemHandle {
450 pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
451 self.to_any().downcast()
452 }
453
454 pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
455 self.act_as_type(TypeId::of::<T>(), cx)
456 .and_then(|t| t.downcast())
457 }
458}
459
460impl<T: Item> ItemHandle for ViewHandle<T> {
461 fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
462 self.read(cx).tab_content(style, cx)
463 }
464
465 fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
466 self.read(cx).project_path(cx)
467 }
468
469 fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
470 self.read(cx).project_entry_ids(cx)
471 }
472
473 fn is_singleton(&self, cx: &AppContext) -> bool {
474 self.read(cx).is_singleton(cx)
475 }
476
477 fn boxed_clone(&self) -> Box<dyn ItemHandle> {
478 Box::new(self.clone())
479 }
480
481 fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
482 self.update(cx, |item, cx| {
483 item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
484 })
485 }
486
487 fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
488 self.update(cx, |item, cx| {
489 cx.add_option_view(|cx| item.clone_on_split(cx))
490 })
491 .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
492 }
493
494 fn added_to_pane(
495 &self,
496 workspace: &mut Workspace,
497 pane: ViewHandle<Pane>,
498 cx: &mut ViewContext<Workspace>,
499 ) {
500 if let Some(followed_item) = self.to_followable_item_handle(cx) {
501 if let Some(message) = followed_item.to_state_proto(cx) {
502 workspace.update_followers(
503 proto::update_followers::Variant::CreateView(proto::View {
504 id: followed_item.id() as u64,
505 variant: Some(message),
506 leader_id: workspace.leader_for_pane(&pane).map(|id| id.0),
507 }),
508 cx,
509 );
510 }
511 }
512
513 let pending_update = Rc::new(RefCell::new(None));
514 let pending_update_scheduled = Rc::new(AtomicBool::new(false));
515 let pane = pane.downgrade();
516 cx.subscribe(self, move |workspace, item, event, cx| {
517 let pane = if let Some(pane) = pane.upgrade(cx) {
518 pane
519 } else {
520 log::error!("unexpected item event after pane was dropped");
521 return;
522 };
523
524 if let Some(item) = item.to_followable_item_handle(cx) {
525 let leader_id = workspace.leader_for_pane(&pane);
526
527 if leader_id.is_some() && item.should_unfollow_on_event(event, cx) {
528 workspace.unfollow(&pane, cx);
529 }
530
531 if item.add_event_to_update_proto(event, &mut *pending_update.borrow_mut(), cx)
532 && !pending_update_scheduled.load(SeqCst)
533 {
534 pending_update_scheduled.store(true, SeqCst);
535 cx.after_window_update({
536 let pending_update = pending_update.clone();
537 let pending_update_scheduled = pending_update_scheduled.clone();
538 move |this, cx| {
539 pending_update_scheduled.store(false, SeqCst);
540 this.update_followers(
541 proto::update_followers::Variant::UpdateView(proto::UpdateView {
542 id: item.id() as u64,
543 variant: pending_update.borrow_mut().take(),
544 leader_id: leader_id.map(|id| id.0),
545 }),
546 cx,
547 );
548 }
549 });
550 }
551 }
552
553 if T::should_close_item_on_event(event) {
554 Pane::close_item(workspace, pane, item.id(), cx).detach_and_log_err(cx);
555 return;
556 }
557
558 if T::should_activate_item_on_event(event) {
559 pane.update(cx, |pane, cx| {
560 if let Some(ix) = pane.index_for_item(&item) {
561 pane.activate_item(ix, true, true, cx);
562 pane.activate(cx);
563 }
564 });
565 }
566
567 if T::should_update_tab_on_event(event) {
568 pane.update(cx, |_, cx| {
569 cx.emit(pane::Event::ChangeItemTitle);
570 cx.notify();
571 });
572 }
573 })
574 .detach();
575 }
576
577 fn deactivated(&self, cx: &mut MutableAppContext) {
578 self.update(cx, |this, cx| this.deactivated(cx));
579 }
580
581 fn navigate(&self, data: Box<dyn Any>, cx: &mut MutableAppContext) -> bool {
582 self.update(cx, |this, cx| this.navigate(data, cx))
583 }
584
585 fn id(&self) -> usize {
586 self.id()
587 }
588
589 fn to_any(&self) -> AnyViewHandle {
590 self.into()
591 }
592
593 fn is_dirty(&self, cx: &AppContext) -> bool {
594 self.read(cx).is_dirty(cx)
595 }
596
597 fn has_conflict(&self, cx: &AppContext) -> bool {
598 self.read(cx).has_conflict(cx)
599 }
600
601 fn can_save(&self, cx: &AppContext) -> bool {
602 self.read(cx).can_save(cx)
603 }
604
605 fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
606 self.update(cx, |item, cx| item.save(project, cx))
607 }
608
609 fn save_as(
610 &self,
611 project: ModelHandle<Project>,
612 abs_path: PathBuf,
613 cx: &mut MutableAppContext,
614 ) -> Task<anyhow::Result<()>> {
615 self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
616 }
617
618 fn reload(
619 &self,
620 project: ModelHandle<Project>,
621 cx: &mut MutableAppContext,
622 ) -> Task<Result<()>> {
623 self.update(cx, |item, cx| item.reload(project, cx))
624 }
625
626 fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
627 self.read(cx).act_as_type(type_id, self, cx)
628 }
629
630 fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
631 if cx.has_global::<FollowableItemBuilders>() {
632 let builders = cx.global::<FollowableItemBuilders>();
633 let item = self.to_any();
634 Some(builders.get(&item.view_type())?.1(item))
635 } else {
636 None
637 }
638 }
639
640 fn on_release(
641 &self,
642 cx: &mut MutableAppContext,
643 callback: Box<dyn FnOnce(&mut MutableAppContext)>,
644 ) -> gpui::Subscription {
645 cx.observe_release(self, move |_, cx| callback(cx))
646 }
647}
648
649impl Into<AnyViewHandle> for Box<dyn ItemHandle> {
650 fn into(self) -> AnyViewHandle {
651 self.to_any()
652 }
653}
654
655impl Clone for Box<dyn ItemHandle> {
656 fn clone(&self) -> Box<dyn ItemHandle> {
657 self.boxed_clone()
658 }
659}
660
661impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
662 fn id(&self) -> usize {
663 self.id()
664 }
665
666 fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
667 self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
668 }
669}
670
671pub trait Notification: View {
672 fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
673}
674
675pub trait NotificationHandle {
676 fn id(&self) -> usize;
677 fn to_any(&self) -> AnyViewHandle;
678}
679
680impl<T: Notification> NotificationHandle for ViewHandle<T> {
681 fn id(&self) -> usize {
682 self.id()
683 }
684
685 fn to_any(&self) -> AnyViewHandle {
686 self.into()
687 }
688}
689
690impl Into<AnyViewHandle> for &dyn NotificationHandle {
691 fn into(self) -> AnyViewHandle {
692 self.to_any()
693 }
694}
695
696impl AppState {
697 #[cfg(any(test, feature = "test-support"))]
698 pub fn test(cx: &mut MutableAppContext) -> Arc<Self> {
699 let settings = Settings::test(cx);
700 cx.set_global(settings);
701
702 let fs = project::FakeFs::new(cx.background().clone());
703 let languages = Arc::new(LanguageRegistry::test());
704 let http_client = client::test::FakeHttpClient::with_404_response();
705 let client = Client::new(http_client.clone());
706 let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
707 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
708 let themes = ThemeRegistry::new((), cx.font_cache().clone());
709 Arc::new(Self {
710 client,
711 themes,
712 fs,
713 languages,
714 user_store,
715 project_store,
716 initialize_workspace: |_, _, _| {},
717 build_window_options: || Default::default(),
718 })
719 }
720}
721
722pub enum Event {
723 PaneAdded(ViewHandle<Pane>),
724 ContactRequestedJoin(u64),
725}
726
727pub struct Workspace {
728 weak_self: WeakViewHandle<Self>,
729 client: Arc<Client>,
730 user_store: ModelHandle<client::UserStore>,
731 remote_entity_subscription: Option<Subscription>,
732 fs: Arc<dyn Fs>,
733 modal: Option<AnyViewHandle>,
734 center: PaneGroup,
735 left_sidebar: ViewHandle<Sidebar>,
736 right_sidebar: ViewHandle<Sidebar>,
737 panes: Vec<ViewHandle<Pane>>,
738 active_pane: ViewHandle<Pane>,
739 status_bar: ViewHandle<StatusBar>,
740 notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
741 project: ModelHandle<Project>,
742 leader_state: LeaderState,
743 follower_states_by_leader: FollowerStatesByLeader,
744 last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
745 window_edited: bool,
746 _observe_current_user: Task<()>,
747}
748
749#[derive(Default)]
750struct LeaderState {
751 followers: HashSet<PeerId>,
752}
753
754type FollowerStatesByLeader = HashMap<PeerId, HashMap<ViewHandle<Pane>, FollowerState>>;
755
756#[derive(Default)]
757struct FollowerState {
758 active_view_id: Option<u64>,
759 items_by_leader_view_id: HashMap<u64, FollowerItem>,
760}
761
762#[derive(Debug)]
763enum FollowerItem {
764 Loading(Vec<proto::update_view::Variant>),
765 Loaded(Box<dyn FollowableItemHandle>),
766}
767
768impl Workspace {
769 pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
770 cx.observe(&project, |_, project, cx| {
771 if project.read(cx).is_read_only() {
772 cx.blur();
773 }
774 cx.notify()
775 })
776 .detach();
777
778 cx.subscribe(&project, move |this, project, event, cx| {
779 match event {
780 project::Event::RemoteIdChanged(remote_id) => {
781 this.project_remote_id_changed(*remote_id, cx);
782 }
783 project::Event::CollaboratorLeft(peer_id) => {
784 this.collaborator_left(*peer_id, cx);
785 }
786 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
787 this.update_window_title(cx);
788 }
789 _ => {}
790 }
791 if project.read(cx).is_read_only() {
792 cx.blur();
793 }
794 cx.notify()
795 })
796 .detach();
797
798 let pane = cx.add_view(|cx| Pane::new(cx));
799 let pane_id = pane.id();
800 cx.subscribe(&pane, move |this, _, event, cx| {
801 this.handle_pane_event(pane_id, event, cx)
802 })
803 .detach();
804 cx.focus(&pane);
805 cx.emit(Event::PaneAdded(pane.clone()));
806
807 let fs = project.read(cx).fs().clone();
808 let user_store = project.read(cx).user_store();
809 let client = project.read(cx).client();
810 let mut current_user = user_store.read(cx).watch_current_user().clone();
811 let mut connection_status = client.status().clone();
812 let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
813 current_user.recv().await;
814 connection_status.recv().await;
815 let mut stream =
816 Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
817
818 while stream.recv().await.is_some() {
819 cx.update(|cx| {
820 if let Some(this) = this.upgrade(cx) {
821 this.update(cx, |_, cx| cx.notify());
822 }
823 })
824 }
825 });
826
827 let weak_self = cx.weak_handle();
828
829 cx.emit_global(WorkspaceCreated(weak_self.clone()));
830
831 let left_sidebar = cx.add_view(|_| Sidebar::new(Side::Left));
832 let right_sidebar = cx.add_view(|_| Sidebar::new(Side::Right));
833 let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
834 let right_sidebar_buttons =
835 cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
836 let status_bar = cx.add_view(|cx| {
837 let mut status_bar = StatusBar::new(&pane.clone(), cx);
838 status_bar.add_left_item(left_sidebar_buttons, cx);
839 status_bar.add_right_item(right_sidebar_buttons, cx);
840 status_bar
841 });
842
843 let mut this = Workspace {
844 modal: None,
845 weak_self,
846 center: PaneGroup::new(pane.clone()),
847 panes: vec![pane.clone()],
848 active_pane: pane.clone(),
849 status_bar,
850 notifications: Default::default(),
851 client,
852 remote_entity_subscription: None,
853 user_store,
854 fs,
855 left_sidebar,
856 right_sidebar,
857 project,
858 leader_state: Default::default(),
859 follower_states_by_leader: Default::default(),
860 last_leaders_by_pane: Default::default(),
861 window_edited: false,
862 _observe_current_user,
863 };
864 this.project_remote_id_changed(this.project.read(cx).remote_id(), cx);
865 cx.defer(|this, cx| this.update_window_title(cx));
866
867 this
868 }
869
870 pub fn weak_handle(&self) -> WeakViewHandle<Self> {
871 self.weak_self.clone()
872 }
873
874 pub fn left_sidebar(&self) -> &ViewHandle<Sidebar> {
875 &self.left_sidebar
876 }
877
878 pub fn right_sidebar(&self) -> &ViewHandle<Sidebar> {
879 &self.right_sidebar
880 }
881
882 pub fn status_bar(&self) -> &ViewHandle<StatusBar> {
883 &self.status_bar
884 }
885
886 pub fn user_store(&self) -> &ModelHandle<UserStore> {
887 &self.user_store
888 }
889
890 pub fn project(&self) -> &ModelHandle<Project> {
891 &self.project
892 }
893
894 /// Call the given callback with a workspace whose project is local.
895 ///
896 /// If the given workspace has a local project, then it will be passed
897 /// to the callback. Otherwise, a new empty window will be created.
898 pub fn with_local_workspace<T, F>(
899 &mut self,
900 cx: &mut ViewContext<Self>,
901 app_state: Arc<AppState>,
902 mut callback: F,
903 ) -> T
904 where
905 T: 'static,
906 F: FnMut(&mut Workspace, &mut ViewContext<Workspace>) -> T,
907 {
908 if self.project.read(cx).is_local() {
909 callback(self, cx)
910 } else {
911 let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
912 let mut workspace = Workspace::new(
913 Project::local(
914 false,
915 app_state.client.clone(),
916 app_state.user_store.clone(),
917 app_state.project_store.clone(),
918 app_state.languages.clone(),
919 app_state.fs.clone(),
920 cx,
921 ),
922 cx,
923 );
924 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
925 workspace
926 });
927 workspace.update(cx, callback)
928 }
929 }
930
931 pub fn worktrees<'a>(
932 &self,
933 cx: &'a AppContext,
934 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
935 self.project.read(cx).worktrees(cx)
936 }
937
938 pub fn visible_worktrees<'a>(
939 &self,
940 cx: &'a AppContext,
941 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
942 self.project.read(cx).visible_worktrees(cx)
943 }
944
945 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
946 let futures = self
947 .worktrees(cx)
948 .filter_map(|worktree| worktree.read(cx).as_local())
949 .map(|worktree| worktree.scan_complete())
950 .collect::<Vec<_>>();
951 async move {
952 for future in futures {
953 future.await;
954 }
955 }
956 }
957
958 pub fn close(
959 &mut self,
960 _: &CloseWindow,
961 cx: &mut ViewContext<Self>,
962 ) -> Option<Task<Result<()>>> {
963 let prepare = self.prepare_to_close(cx);
964 Some(cx.spawn(|this, mut cx| async move {
965 if prepare.await? {
966 this.update(&mut cx, |_, cx| {
967 let window_id = cx.window_id();
968 cx.remove_window(window_id);
969 });
970 }
971 Ok(())
972 }))
973 }
974
975 pub fn prepare_to_close(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
976 self.save_all_internal(true, cx)
977 }
978
979 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
980 let save_all = self.save_all_internal(false, cx);
981 Some(cx.foreground().spawn(async move {
982 save_all.await?;
983 Ok(())
984 }))
985 }
986
987 fn save_all_internal(
988 &mut self,
989 should_prompt_to_save: bool,
990 cx: &mut ViewContext<Self>,
991 ) -> Task<Result<bool>> {
992 let dirty_items = self
993 .panes
994 .iter()
995 .flat_map(|pane| {
996 pane.read(cx).items().filter_map(|item| {
997 if item.is_dirty(cx) {
998 Some((pane.clone(), item.boxed_clone()))
999 } else {
1000 None
1001 }
1002 })
1003 })
1004 .collect::<Vec<_>>();
1005
1006 let project = self.project.clone();
1007 cx.spawn_weak(|_, mut cx| async move {
1008 // let mut saved_project_entry_ids = HashSet::default();
1009 for (pane, item) in dirty_items {
1010 let (is_singl, project_entry_ids) =
1011 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
1012 if is_singl || !project_entry_ids.is_empty() {
1013 if let Some(ix) =
1014 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))
1015 {
1016 if !Pane::save_item(
1017 project.clone(),
1018 &pane,
1019 ix,
1020 &item,
1021 should_prompt_to_save,
1022 &mut cx,
1023 )
1024 .await?
1025 {
1026 return Ok(false);
1027 }
1028 }
1029 }
1030 }
1031 Ok(true)
1032 })
1033 }
1034
1035 pub fn open_paths(
1036 &mut self,
1037 mut abs_paths: Vec<PathBuf>,
1038 visible: bool,
1039 cx: &mut ViewContext<Self>,
1040 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>> {
1041 let fs = self.fs.clone();
1042
1043 // Sort the paths to ensure we add worktrees for parents before their children.
1044 abs_paths.sort_unstable();
1045 cx.spawn(|this, mut cx| async move {
1046 let mut project_paths = Vec::new();
1047 for path in &abs_paths {
1048 project_paths.push(
1049 this.update(&mut cx, |this, cx| {
1050 this.project_path_for_path(path, visible, cx)
1051 })
1052 .await
1053 .log_err(),
1054 );
1055 }
1056
1057 let tasks = abs_paths
1058 .iter()
1059 .cloned()
1060 .zip(project_paths.into_iter())
1061 .map(|(abs_path, project_path)| {
1062 let this = this.clone();
1063 cx.spawn(|mut cx| {
1064 let fs = fs.clone();
1065 async move {
1066 let (_worktree, project_path) = project_path?;
1067 if fs.is_file(&abs_path).await {
1068 Some(
1069 this.update(&mut cx, |this, cx| {
1070 this.open_path(project_path, true, cx)
1071 })
1072 .await,
1073 )
1074 } else {
1075 None
1076 }
1077 }
1078 })
1079 })
1080 .collect::<Vec<_>>();
1081
1082 futures::future::join_all(tasks).await
1083 })
1084 }
1085
1086 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1087 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1088 files: false,
1089 directories: true,
1090 multiple: true,
1091 });
1092 cx.spawn(|this, mut cx| async move {
1093 if let Some(paths) = paths.recv().await.flatten() {
1094 let results = this
1095 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))
1096 .await;
1097 for result in results {
1098 if let Some(result) = result {
1099 result.log_err();
1100 }
1101 }
1102 }
1103 })
1104 .detach();
1105 }
1106
1107 fn remove_folder_from_project(
1108 &mut self,
1109 RemoveWorktreeFromProject(worktree_id): &RemoveWorktreeFromProject,
1110 cx: &mut ViewContext<Self>,
1111 ) {
1112 self.project
1113 .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
1114 }
1115
1116 fn toggle_project_online(&mut self, action: &ToggleProjectOnline, cx: &mut ViewContext<Self>) {
1117 let project = action
1118 .project
1119 .clone()
1120 .unwrap_or_else(|| self.project.clone());
1121 project.update(cx, |project, cx| {
1122 let public = !project.is_online();
1123 project.set_online(public, cx);
1124 });
1125 }
1126
1127 fn project_path_for_path(
1128 &self,
1129 abs_path: &Path,
1130 visible: bool,
1131 cx: &mut ViewContext<Self>,
1132 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1133 let entry = self.project().update(cx, |project, cx| {
1134 project.find_or_create_local_worktree(abs_path, visible, cx)
1135 });
1136 cx.spawn(|_, cx| async move {
1137 let (worktree, path) = entry.await?;
1138 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1139 Ok((
1140 worktree,
1141 ProjectPath {
1142 worktree_id,
1143 path: path.into(),
1144 },
1145 ))
1146 })
1147 }
1148
1149 /// Returns the modal that was toggled closed if it was open.
1150 pub fn toggle_modal<V, F>(
1151 &mut self,
1152 cx: &mut ViewContext<Self>,
1153 add_view: F,
1154 ) -> Option<ViewHandle<V>>
1155 where
1156 V: 'static + View,
1157 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1158 {
1159 cx.notify();
1160 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1161 // it. Otherwise, create a new modal and set it as active.
1162 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1163 if let Some(already_open_modal) = already_open_modal {
1164 cx.focus_self();
1165 Some(already_open_modal)
1166 } else {
1167 let modal = add_view(self, cx);
1168 cx.focus(&modal);
1169 self.modal = Some(modal.into());
1170 None
1171 }
1172 }
1173
1174 pub fn modal(&self) -> Option<&AnyViewHandle> {
1175 self.modal.as_ref()
1176 }
1177
1178 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1179 if self.modal.take().is_some() {
1180 cx.focus(&self.active_pane);
1181 cx.notify();
1182 }
1183 }
1184
1185 pub fn show_notification<V: Notification>(
1186 &mut self,
1187 id: usize,
1188 cx: &mut ViewContext<Self>,
1189 build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
1190 ) {
1191 let type_id = TypeId::of::<V>();
1192 if self
1193 .notifications
1194 .iter()
1195 .all(|(existing_type_id, existing_id, _)| {
1196 (*existing_type_id, *existing_id) != (type_id, id)
1197 })
1198 {
1199 let notification = build_notification(cx);
1200 cx.subscribe(¬ification, move |this, handle, event, cx| {
1201 if handle.read(cx).should_dismiss_notification_on_event(event) {
1202 this.dismiss_notification(type_id, id, cx);
1203 }
1204 })
1205 .detach();
1206 self.notifications
1207 .push((type_id, id, Box::new(notification)));
1208 cx.notify();
1209 }
1210 }
1211
1212 fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext<Self>) {
1213 self.notifications
1214 .retain(|(existing_type_id, existing_id, _)| {
1215 if (*existing_type_id, *existing_id) == (type_id, id) {
1216 cx.notify();
1217 false
1218 } else {
1219 true
1220 }
1221 });
1222 }
1223
1224 pub fn items<'a>(
1225 &'a self,
1226 cx: &'a AppContext,
1227 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1228 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1229 }
1230
1231 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1232 self.items_of_type(cx).max_by_key(|item| item.id())
1233 }
1234
1235 pub fn items_of_type<'a, T: Item>(
1236 &'a self,
1237 cx: &'a AppContext,
1238 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1239 self.panes
1240 .iter()
1241 .flat_map(|pane| pane.read(cx).items_of_type())
1242 }
1243
1244 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1245 self.active_pane().read(cx).active_item()
1246 }
1247
1248 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1249 self.active_item(cx).and_then(|item| item.project_path(cx))
1250 }
1251
1252 pub fn save_active_item(
1253 &mut self,
1254 force_name_change: bool,
1255 cx: &mut ViewContext<Self>,
1256 ) -> Task<Result<()>> {
1257 let project = self.project.clone();
1258 if let Some(item) = self.active_item(cx) {
1259 if !force_name_change && item.can_save(cx) {
1260 if item.has_conflict(cx.as_ref()) {
1261 const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1262
1263 let mut answer = cx.prompt(
1264 PromptLevel::Warning,
1265 CONFLICT_MESSAGE,
1266 &["Overwrite", "Cancel"],
1267 );
1268 cx.spawn(|_, mut cx| async move {
1269 let answer = answer.recv().await;
1270 if answer == Some(0) {
1271 cx.update(|cx| item.save(project, cx)).await?;
1272 }
1273 Ok(())
1274 })
1275 } else {
1276 item.save(project, cx)
1277 }
1278 } else if item.is_singleton(cx) {
1279 let worktree = self.worktrees(cx).next();
1280 let start_abs_path = worktree
1281 .and_then(|w| w.read(cx).as_local())
1282 .map_or(Path::new(""), |w| w.abs_path())
1283 .to_path_buf();
1284 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1285 cx.spawn(|_, mut cx| async move {
1286 if let Some(abs_path) = abs_path.recv().await.flatten() {
1287 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
1288 }
1289 Ok(())
1290 })
1291 } else {
1292 Task::ready(Ok(()))
1293 }
1294 } else {
1295 Task::ready(Ok(()))
1296 }
1297 }
1298
1299 pub fn toggle_sidebar(&mut self, side: Side, cx: &mut ViewContext<Self>) {
1300 let sidebar = match side {
1301 Side::Left => &mut self.left_sidebar,
1302 Side::Right => &mut self.right_sidebar,
1303 };
1304 sidebar.update(cx, |sidebar, cx| {
1305 sidebar.set_open(!sidebar.is_open(), cx);
1306 });
1307 cx.focus_self();
1308 cx.notify();
1309 }
1310
1311 pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
1312 let sidebar = match action.side {
1313 Side::Left => &mut self.left_sidebar,
1314 Side::Right => &mut self.right_sidebar,
1315 };
1316 let active_item = sidebar.update(cx, |sidebar, cx| {
1317 if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
1318 sidebar.set_open(false, cx);
1319 None
1320 } else {
1321 sidebar.set_open(true, cx);
1322 sidebar.activate_item(action.item_index, cx);
1323 sidebar.active_item().cloned()
1324 }
1325 });
1326 if let Some(active_item) = active_item {
1327 if active_item.is_focused(cx) {
1328 cx.focus_self();
1329 } else {
1330 cx.focus(active_item.to_any());
1331 }
1332 } else {
1333 cx.focus_self();
1334 }
1335 cx.notify();
1336 }
1337
1338 pub fn toggle_sidebar_item_focus(
1339 &mut self,
1340 side: Side,
1341 item_index: usize,
1342 cx: &mut ViewContext<Self>,
1343 ) {
1344 let sidebar = match side {
1345 Side::Left => &mut self.left_sidebar,
1346 Side::Right => &mut self.right_sidebar,
1347 };
1348 let active_item = sidebar.update(cx, |sidebar, cx| {
1349 sidebar.set_open(true, cx);
1350 sidebar.activate_item(item_index, cx);
1351 sidebar.active_item().cloned()
1352 });
1353 if let Some(active_item) = active_item {
1354 if active_item.is_focused(cx) {
1355 cx.focus_self();
1356 } else {
1357 cx.focus(active_item.to_any());
1358 }
1359 }
1360 cx.notify();
1361 }
1362
1363 pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
1364 cx.focus_self();
1365 cx.notify();
1366 }
1367
1368 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1369 let pane = cx.add_view(|cx| Pane::new(cx));
1370 let pane_id = pane.id();
1371 cx.subscribe(&pane, move |this, _, event, cx| {
1372 this.handle_pane_event(pane_id, event, cx)
1373 })
1374 .detach();
1375 self.panes.push(pane.clone());
1376 self.activate_pane(pane.clone(), cx);
1377 cx.emit(Event::PaneAdded(pane.clone()));
1378 pane
1379 }
1380
1381 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1382 let pane = self.active_pane().clone();
1383 Pane::add_item(self, pane, item, true, true, cx);
1384 }
1385
1386 pub fn open_path(
1387 &mut self,
1388 path: impl Into<ProjectPath>,
1389 focus_item: bool,
1390 cx: &mut ViewContext<Self>,
1391 ) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
1392 let pane = self.active_pane().downgrade();
1393 let task = self.load_path(path.into(), cx);
1394 cx.spawn(|this, mut cx| async move {
1395 let (project_entry_id, build_item) = task.await?;
1396 let pane = pane
1397 .upgrade(&cx)
1398 .ok_or_else(|| anyhow!("pane was closed"))?;
1399 this.update(&mut cx, |this, cx| {
1400 Ok(Pane::open_item(
1401 this,
1402 pane,
1403 project_entry_id,
1404 focus_item,
1405 cx,
1406 build_item,
1407 ))
1408 })
1409 })
1410 }
1411
1412 pub(crate) fn load_path(
1413 &mut self,
1414 path: ProjectPath,
1415 cx: &mut ViewContext<Self>,
1416 ) -> Task<
1417 Result<(
1418 ProjectEntryId,
1419 impl 'static + FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
1420 )>,
1421 > {
1422 let project = self.project().clone();
1423 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1424 let window_id = cx.window_id();
1425 cx.as_mut().spawn(|mut cx| async move {
1426 let (project_entry_id, project_item) = project_item.await?;
1427 let build_item = cx.update(|cx| {
1428 cx.default_global::<ProjectItemBuilders>()
1429 .get(&project_item.model_type())
1430 .ok_or_else(|| anyhow!("no item builder for project item"))
1431 .cloned()
1432 })?;
1433 let build_item =
1434 move |cx: &mut MutableAppContext| build_item(window_id, project, project_item, cx);
1435 Ok((project_entry_id, build_item))
1436 })
1437 }
1438
1439 pub fn open_project_item<T>(
1440 &mut self,
1441 project_item: ModelHandle<T::Item>,
1442 cx: &mut ViewContext<Self>,
1443 ) -> ViewHandle<T>
1444 where
1445 T: ProjectItem,
1446 {
1447 use project::Item as _;
1448
1449 let entry_id = project_item.read(cx).entry_id(cx);
1450 if let Some(item) = entry_id
1451 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1452 .and_then(|item| item.downcast())
1453 {
1454 self.activate_item(&item, cx);
1455 return item;
1456 }
1457
1458 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1459 self.add_item(Box::new(item.clone()), cx);
1460 item
1461 }
1462
1463 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1464 let result = self.panes.iter().find_map(|pane| {
1465 if let Some(ix) = pane.read(cx).index_for_item(item) {
1466 Some((pane.clone(), ix))
1467 } else {
1468 None
1469 }
1470 });
1471 if let Some((pane, ix)) = result {
1472 self.activate_pane(pane.clone(), cx);
1473 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1474 true
1475 } else {
1476 false
1477 }
1478 }
1479
1480 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1481 let panes = self.center.panes();
1482 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1483 self.activate_pane(pane, cx);
1484 } else {
1485 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1486 }
1487 }
1488
1489 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1490 let next_pane = {
1491 let panes = self.center.panes();
1492 let ix = panes
1493 .iter()
1494 .position(|pane| **pane == self.active_pane)
1495 .unwrap();
1496 let next_ix = (ix + 1) % panes.len();
1497 panes[next_ix].clone()
1498 };
1499 self.activate_pane(next_pane, cx);
1500 }
1501
1502 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1503 let prev_pane = {
1504 let panes = self.center.panes();
1505 let ix = panes
1506 .iter()
1507 .position(|pane| **pane == self.active_pane)
1508 .unwrap();
1509 let prev_ix = if ix == 0 { panes.len() - 1 } else { ix - 1 };
1510 panes[prev_ix].clone()
1511 };
1512 self.activate_pane(prev_pane, cx);
1513 }
1514
1515 fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1516 if self.active_pane != pane {
1517 self.active_pane = pane.clone();
1518 self.status_bar.update(cx, |status_bar, cx| {
1519 status_bar.set_active_pane(&self.active_pane, cx);
1520 });
1521 self.active_item_path_changed(cx);
1522 cx.focus(&self.active_pane);
1523 cx.notify();
1524 }
1525
1526 self.update_followers(
1527 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1528 id: self.active_item(cx).map(|item| item.id() as u64),
1529 leader_id: self.leader_for_pane(&pane).map(|id| id.0),
1530 }),
1531 cx,
1532 );
1533 }
1534
1535 fn handle_pane_event(
1536 &mut self,
1537 pane_id: usize,
1538 event: &pane::Event,
1539 cx: &mut ViewContext<Self>,
1540 ) {
1541 if let Some(pane) = self.pane(pane_id) {
1542 match event {
1543 pane::Event::Split(direction) => {
1544 self.split_pane(pane, *direction, cx);
1545 }
1546 pane::Event::Remove => {
1547 self.remove_pane(pane, cx);
1548 }
1549 pane::Event::Activate => {
1550 self.activate_pane(pane, cx);
1551 }
1552 pane::Event::ActivateItem { local } => {
1553 if *local {
1554 self.unfollow(&pane, cx);
1555 }
1556 if pane == self.active_pane {
1557 self.active_item_path_changed(cx);
1558 }
1559 }
1560 pane::Event::ChangeItemTitle => {
1561 if pane == self.active_pane {
1562 self.active_item_path_changed(cx);
1563 }
1564 self.update_window_edited(cx);
1565 }
1566 pane::Event::RemoveItem => {
1567 self.update_window_edited(cx);
1568 }
1569 }
1570 } else {
1571 error!("pane {} not found", pane_id);
1572 }
1573 }
1574
1575 pub fn split_pane(
1576 &mut self,
1577 pane: ViewHandle<Pane>,
1578 direction: SplitDirection,
1579 cx: &mut ViewContext<Self>,
1580 ) -> ViewHandle<Pane> {
1581 let new_pane = self.add_pane(cx);
1582 self.activate_pane(new_pane.clone(), cx);
1583 if let Some(item) = pane.read(cx).active_item() {
1584 if let Some(clone) = item.clone_on_split(cx.as_mut()) {
1585 Pane::add_item(self, new_pane.clone(), clone, true, true, cx);
1586 }
1587 }
1588 self.center.split(&pane, &new_pane, direction).unwrap();
1589 cx.notify();
1590 new_pane
1591 }
1592
1593 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1594 if self.center.remove(&pane).unwrap() {
1595 self.panes.retain(|p| p != &pane);
1596 self.activate_pane(self.panes.last().unwrap().clone(), cx);
1597 self.unfollow(&pane, cx);
1598 self.last_leaders_by_pane.remove(&pane.downgrade());
1599 cx.notify();
1600 } else {
1601 self.active_item_path_changed(cx);
1602 }
1603 }
1604
1605 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1606 &self.panes
1607 }
1608
1609 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
1610 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
1611 }
1612
1613 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1614 &self.active_pane
1615 }
1616
1617 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1618 if let Some(remote_id) = remote_id {
1619 self.remote_entity_subscription =
1620 Some(self.client.add_view_for_remote_entity(remote_id, cx));
1621 } else {
1622 self.remote_entity_subscription.take();
1623 }
1624 }
1625
1626 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1627 self.leader_state.followers.remove(&peer_id);
1628 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1629 for state in states_by_pane.into_values() {
1630 for item in state.items_by_leader_view_id.into_values() {
1631 if let FollowerItem::Loaded(item) = item {
1632 item.set_leader_replica_id(None, cx);
1633 }
1634 }
1635 }
1636 }
1637 cx.notify();
1638 }
1639
1640 pub fn toggle_follow(
1641 &mut self,
1642 ToggleFollow(leader_id): &ToggleFollow,
1643 cx: &mut ViewContext<Self>,
1644 ) -> Option<Task<Result<()>>> {
1645 let leader_id = *leader_id;
1646 let pane = self.active_pane().clone();
1647
1648 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1649 if leader_id == prev_leader_id {
1650 return None;
1651 }
1652 }
1653
1654 self.last_leaders_by_pane
1655 .insert(pane.downgrade(), leader_id);
1656 self.follower_states_by_leader
1657 .entry(leader_id)
1658 .or_default()
1659 .insert(pane.clone(), Default::default());
1660 cx.notify();
1661
1662 let project_id = self.project.read(cx).remote_id()?;
1663 let request = self.client.request(proto::Follow {
1664 project_id,
1665 leader_id: leader_id.0,
1666 });
1667 Some(cx.spawn_weak(|this, mut cx| async move {
1668 let response = request.await?;
1669 if let Some(this) = this.upgrade(&cx) {
1670 this.update(&mut cx, |this, _| {
1671 let state = this
1672 .follower_states_by_leader
1673 .get_mut(&leader_id)
1674 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1675 .ok_or_else(|| anyhow!("following interrupted"))?;
1676 state.active_view_id = response.active_view_id;
1677 Ok::<_, anyhow::Error>(())
1678 })?;
1679 Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx)
1680 .await?;
1681 }
1682 Ok(())
1683 }))
1684 }
1685
1686 pub fn follow_next_collaborator(
1687 &mut self,
1688 _: &FollowNextCollaborator,
1689 cx: &mut ViewContext<Self>,
1690 ) -> Option<Task<Result<()>>> {
1691 let collaborators = self.project.read(cx).collaborators();
1692 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1693 let mut collaborators = collaborators.keys().copied();
1694 while let Some(peer_id) = collaborators.next() {
1695 if peer_id == leader_id {
1696 break;
1697 }
1698 }
1699 collaborators.next()
1700 } else if let Some(last_leader_id) =
1701 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1702 {
1703 if collaborators.contains_key(last_leader_id) {
1704 Some(*last_leader_id)
1705 } else {
1706 None
1707 }
1708 } else {
1709 None
1710 };
1711
1712 next_leader_id
1713 .or_else(|| collaborators.keys().copied().next())
1714 .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
1715 }
1716
1717 pub fn unfollow(
1718 &mut self,
1719 pane: &ViewHandle<Pane>,
1720 cx: &mut ViewContext<Self>,
1721 ) -> Option<PeerId> {
1722 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1723 let leader_id = *leader_id;
1724 if let Some(state) = states_by_pane.remove(&pane) {
1725 for (_, item) in state.items_by_leader_view_id {
1726 if let FollowerItem::Loaded(item) = item {
1727 item.set_leader_replica_id(None, cx);
1728 }
1729 }
1730
1731 if states_by_pane.is_empty() {
1732 self.follower_states_by_leader.remove(&leader_id);
1733 if let Some(project_id) = self.project.read(cx).remote_id() {
1734 self.client
1735 .send(proto::Unfollow {
1736 project_id,
1737 leader_id: leader_id.0,
1738 })
1739 .log_err();
1740 }
1741 }
1742
1743 cx.notify();
1744 return Some(leader_id);
1745 }
1746 }
1747 None
1748 }
1749
1750 fn render_connection_status(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
1751 let theme = &cx.global::<Settings>().theme;
1752 match &*self.client.status().borrow() {
1753 client::Status::ConnectionError
1754 | client::Status::ConnectionLost
1755 | client::Status::Reauthenticating
1756 | client::Status::Reconnecting { .. }
1757 | client::Status::ReconnectionError { .. } => Some(
1758 Container::new(
1759 Align::new(
1760 ConstrainedBox::new(
1761 Svg::new("icons/offline-14.svg")
1762 .with_color(theme.workspace.titlebar.offline_icon.color)
1763 .boxed(),
1764 )
1765 .with_width(theme.workspace.titlebar.offline_icon.width)
1766 .boxed(),
1767 )
1768 .boxed(),
1769 )
1770 .with_style(theme.workspace.titlebar.offline_icon.container)
1771 .boxed(),
1772 ),
1773 client::Status::UpgradeRequired => Some(
1774 Label::new(
1775 "Please update Zed to collaborate".to_string(),
1776 theme.workspace.titlebar.outdated_warning.text.clone(),
1777 )
1778 .contained()
1779 .with_style(theme.workspace.titlebar.outdated_warning.container)
1780 .aligned()
1781 .boxed(),
1782 ),
1783 _ => None,
1784 }
1785 }
1786
1787 fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
1788 let project = &self.project.read(cx);
1789 let replica_id = project.replica_id();
1790 let mut worktree_root_names = String::new();
1791 for (i, name) in project.worktree_root_names(cx).enumerate() {
1792 if i > 0 {
1793 worktree_root_names.push_str(", ");
1794 }
1795 worktree_root_names.push_str(name);
1796 }
1797
1798 ConstrainedBox::new(
1799 Container::new(
1800 Stack::new()
1801 .with_child(
1802 Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
1803 .aligned()
1804 .left()
1805 .boxed(),
1806 )
1807 .with_child(
1808 Align::new(
1809 Flex::row()
1810 .with_children(self.render_collaborators(theme, cx))
1811 .with_children(self.render_current_user(
1812 self.user_store.read(cx).current_user().as_ref(),
1813 replica_id,
1814 theme,
1815 cx,
1816 ))
1817 .with_children(self.render_connection_status(cx))
1818 .boxed(),
1819 )
1820 .right()
1821 .boxed(),
1822 )
1823 .boxed(),
1824 )
1825 .with_style(theme.workspace.titlebar.container)
1826 .boxed(),
1827 )
1828 .with_height(theme.workspace.titlebar.height)
1829 .named("titlebar")
1830 }
1831
1832 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
1833 let active_entry = self.active_project_path(cx);
1834 self.project
1835 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
1836 self.update_window_title(cx);
1837 }
1838
1839 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
1840 let mut title = String::new();
1841 let project = self.project().read(cx);
1842 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
1843 let filename = path
1844 .path
1845 .file_name()
1846 .map(|s| s.to_string_lossy())
1847 .or_else(|| {
1848 Some(Cow::Borrowed(
1849 project
1850 .worktree_for_id(path.worktree_id, cx)?
1851 .read(cx)
1852 .root_name(),
1853 ))
1854 });
1855 if let Some(filename) = filename {
1856 title.push_str(filename.as_ref());
1857 title.push_str(" — ");
1858 }
1859 }
1860 for (i, name) in project.worktree_root_names(cx).enumerate() {
1861 if i > 0 {
1862 title.push_str(", ");
1863 }
1864 title.push_str(name);
1865 }
1866 if title.is_empty() {
1867 title = "empty project".to_string();
1868 }
1869 cx.set_window_title(&title);
1870 }
1871
1872 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
1873 let is_edited = self
1874 .items(cx)
1875 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
1876 if is_edited != self.window_edited {
1877 self.window_edited = is_edited;
1878 cx.set_window_edited(self.window_edited)
1879 }
1880 }
1881
1882 fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
1883 let mut collaborators = self
1884 .project
1885 .read(cx)
1886 .collaborators()
1887 .values()
1888 .cloned()
1889 .collect::<Vec<_>>();
1890 collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id);
1891 collaborators
1892 .into_iter()
1893 .filter_map(|collaborator| {
1894 Some(self.render_avatar(
1895 collaborator.user.avatar.clone()?,
1896 collaborator.replica_id,
1897 Some((collaborator.peer_id, &collaborator.user.github_login)),
1898 theme,
1899 cx,
1900 ))
1901 })
1902 .collect()
1903 }
1904
1905 fn render_current_user(
1906 &self,
1907 user: Option<&Arc<User>>,
1908 replica_id: ReplicaId,
1909 theme: &Theme,
1910 cx: &mut RenderContext<Self>,
1911 ) -> Option<ElementBox> {
1912 let status = *self.client.status().borrow();
1913 if let Some(avatar) = user.and_then(|user| user.avatar.clone()) {
1914 Some(self.render_avatar(avatar, replica_id, None, theme, cx))
1915 } else if matches!(status, client::Status::UpgradeRequired) {
1916 None
1917 } else {
1918 Some(
1919 MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| {
1920 let style = theme
1921 .workspace
1922 .titlebar
1923 .sign_in_prompt
1924 .style_for(state, false);
1925 Label::new("Sign in".to_string(), style.text.clone())
1926 .contained()
1927 .with_style(style.container)
1928 .boxed()
1929 })
1930 .on_click(|_, _, cx| cx.dispatch_action(Authenticate))
1931 .with_cursor_style(CursorStyle::PointingHand)
1932 .aligned()
1933 .boxed(),
1934 )
1935 }
1936 }
1937
1938 fn render_avatar(
1939 &self,
1940 avatar: Arc<ImageData>,
1941 replica_id: ReplicaId,
1942 peer: Option<(PeerId, &str)>,
1943 theme: &Theme,
1944 cx: &mut RenderContext<Self>,
1945 ) -> ElementBox {
1946 let replica_color = theme.editor.replica_selection_style(replica_id).cursor;
1947 let is_followed = peer.map_or(false, |(peer_id, _)| {
1948 self.follower_states_by_leader.contains_key(&peer_id)
1949 });
1950 let mut avatar_style = theme.workspace.titlebar.avatar;
1951 if is_followed {
1952 avatar_style.border = Border::all(1.0, replica_color);
1953 }
1954 let content = Stack::new()
1955 .with_child(
1956 Image::new(avatar)
1957 .with_style(avatar_style)
1958 .constrained()
1959 .with_width(theme.workspace.titlebar.avatar_width)
1960 .aligned()
1961 .boxed(),
1962 )
1963 .with_child(
1964 AvatarRibbon::new(replica_color)
1965 .constrained()
1966 .with_width(theme.workspace.titlebar.avatar_ribbon.width)
1967 .with_height(theme.workspace.titlebar.avatar_ribbon.height)
1968 .aligned()
1969 .bottom()
1970 .boxed(),
1971 )
1972 .constrained()
1973 .with_width(theme.workspace.titlebar.avatar_width)
1974 .contained()
1975 .with_margin_left(theme.workspace.titlebar.avatar_margin)
1976 .boxed();
1977
1978 if let Some((peer_id, peer_github_login)) = peer {
1979 MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
1980 .with_cursor_style(CursorStyle::PointingHand)
1981 .on_click(move |_, _, cx| cx.dispatch_action(ToggleFollow(peer_id)))
1982 .with_tooltip::<ToggleFollow, _>(
1983 peer_id.0 as usize,
1984 if is_followed {
1985 format!("Unfollow {}", peer_github_login)
1986 } else {
1987 format!("Follow {}", peer_github_login)
1988 },
1989 Some(Box::new(FollowNextCollaborator)),
1990 theme.tooltip.clone(),
1991 cx,
1992 )
1993 .boxed()
1994 } else {
1995 content
1996 }
1997 }
1998
1999 fn render_disconnected_overlay(&self, cx: &AppContext) -> Option<ElementBox> {
2000 if self.project.read(cx).is_read_only() {
2001 let theme = &cx.global::<Settings>().theme;
2002 Some(
2003 EventHandler::new(
2004 Label::new(
2005 "Your connection to the remote project has been lost.".to_string(),
2006 theme.workspace.disconnected_overlay.text.clone(),
2007 )
2008 .aligned()
2009 .contained()
2010 .with_style(theme.workspace.disconnected_overlay.container)
2011 .boxed(),
2012 )
2013 .capture_all::<Self>(0)
2014 .boxed(),
2015 )
2016 } else {
2017 None
2018 }
2019 }
2020
2021 fn render_notifications(&self, theme: &theme::Workspace) -> Option<ElementBox> {
2022 if self.notifications.is_empty() {
2023 None
2024 } else {
2025 Some(
2026 Flex::column()
2027 .with_children(self.notifications.iter().map(|(_, _, notification)| {
2028 ChildView::new(notification.as_ref())
2029 .contained()
2030 .with_style(theme.notification)
2031 .boxed()
2032 }))
2033 .constrained()
2034 .with_width(theme.notifications.width)
2035 .contained()
2036 .with_style(theme.notifications.container)
2037 .aligned()
2038 .bottom()
2039 .right()
2040 .boxed(),
2041 )
2042 }
2043 }
2044
2045 // RPC handlers
2046
2047 async fn handle_follow(
2048 this: ViewHandle<Self>,
2049 envelope: TypedEnvelope<proto::Follow>,
2050 _: Arc<Client>,
2051 mut cx: AsyncAppContext,
2052 ) -> Result<proto::FollowResponse> {
2053 this.update(&mut cx, |this, cx| {
2054 this.leader_state
2055 .followers
2056 .insert(envelope.original_sender_id()?);
2057
2058 let active_view_id = this
2059 .active_item(cx)
2060 .and_then(|i| i.to_followable_item_handle(cx))
2061 .map(|i| i.id() as u64);
2062 Ok(proto::FollowResponse {
2063 active_view_id,
2064 views: this
2065 .panes()
2066 .iter()
2067 .flat_map(|pane| {
2068 let leader_id = this.leader_for_pane(pane).map(|id| id.0);
2069 pane.read(cx).items().filter_map({
2070 let cx = &cx;
2071 move |item| {
2072 let id = item.id() as u64;
2073 let item = item.to_followable_item_handle(cx)?;
2074 let variant = item.to_state_proto(cx)?;
2075 Some(proto::View {
2076 id,
2077 leader_id,
2078 variant: Some(variant),
2079 })
2080 }
2081 })
2082 })
2083 .collect(),
2084 })
2085 })
2086 }
2087
2088 async fn handle_unfollow(
2089 this: ViewHandle<Self>,
2090 envelope: TypedEnvelope<proto::Unfollow>,
2091 _: Arc<Client>,
2092 mut cx: AsyncAppContext,
2093 ) -> Result<()> {
2094 this.update(&mut cx, |this, _| {
2095 this.leader_state
2096 .followers
2097 .remove(&envelope.original_sender_id()?);
2098 Ok(())
2099 })
2100 }
2101
2102 async fn handle_update_followers(
2103 this: ViewHandle<Self>,
2104 envelope: TypedEnvelope<proto::UpdateFollowers>,
2105 _: Arc<Client>,
2106 mut cx: AsyncAppContext,
2107 ) -> Result<()> {
2108 let leader_id = envelope.original_sender_id()?;
2109 match envelope
2110 .payload
2111 .variant
2112 .ok_or_else(|| anyhow!("invalid update"))?
2113 {
2114 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2115 this.update(&mut cx, |this, cx| {
2116 this.update_leader_state(leader_id, cx, |state, _| {
2117 state.active_view_id = update_active_view.id;
2118 });
2119 Ok::<_, anyhow::Error>(())
2120 })
2121 }
2122 proto::update_followers::Variant::UpdateView(update_view) => {
2123 this.update(&mut cx, |this, cx| {
2124 let variant = update_view
2125 .variant
2126 .ok_or_else(|| anyhow!("missing update view variant"))?;
2127 this.update_leader_state(leader_id, cx, |state, cx| {
2128 let variant = variant.clone();
2129 match state
2130 .items_by_leader_view_id
2131 .entry(update_view.id)
2132 .or_insert(FollowerItem::Loading(Vec::new()))
2133 {
2134 FollowerItem::Loaded(item) => {
2135 item.apply_update_proto(variant, cx).log_err();
2136 }
2137 FollowerItem::Loading(updates) => updates.push(variant),
2138 }
2139 });
2140 Ok(())
2141 })
2142 }
2143 proto::update_followers::Variant::CreateView(view) => {
2144 let panes = this.read_with(&cx, |this, _| {
2145 this.follower_states_by_leader
2146 .get(&leader_id)
2147 .into_iter()
2148 .flat_map(|states_by_pane| states_by_pane.keys())
2149 .cloned()
2150 .collect()
2151 });
2152 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx)
2153 .await?;
2154 Ok(())
2155 }
2156 }
2157 .log_err();
2158
2159 Ok(())
2160 }
2161
2162 async fn add_views_from_leader(
2163 this: ViewHandle<Self>,
2164 leader_id: PeerId,
2165 panes: Vec<ViewHandle<Pane>>,
2166 views: Vec<proto::View>,
2167 cx: &mut AsyncAppContext,
2168 ) -> Result<()> {
2169 let project = this.read_with(cx, |this, _| this.project.clone());
2170 let replica_id = project
2171 .read_with(cx, |project, _| {
2172 project
2173 .collaborators()
2174 .get(&leader_id)
2175 .map(|c| c.replica_id)
2176 })
2177 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2178
2179 let item_builders = cx.update(|cx| {
2180 cx.default_global::<FollowableItemBuilders>()
2181 .values()
2182 .map(|b| b.0)
2183 .collect::<Vec<_>>()
2184 .clone()
2185 });
2186
2187 let mut item_tasks_by_pane = HashMap::default();
2188 for pane in panes {
2189 let mut item_tasks = Vec::new();
2190 let mut leader_view_ids = Vec::new();
2191 for view in &views {
2192 let mut variant = view.variant.clone();
2193 if variant.is_none() {
2194 Err(anyhow!("missing variant"))?;
2195 }
2196 for build_item in &item_builders {
2197 let task =
2198 cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx));
2199 if let Some(task) = task {
2200 item_tasks.push(task);
2201 leader_view_ids.push(view.id);
2202 break;
2203 } else {
2204 assert!(variant.is_some());
2205 }
2206 }
2207 }
2208
2209 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2210 }
2211
2212 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2213 let items = futures::future::try_join_all(item_tasks).await?;
2214 this.update(cx, |this, cx| {
2215 let state = this
2216 .follower_states_by_leader
2217 .get_mut(&leader_id)?
2218 .get_mut(&pane)?;
2219
2220 for (id, item) in leader_view_ids.into_iter().zip(items) {
2221 item.set_leader_replica_id(Some(replica_id), cx);
2222 match state.items_by_leader_view_id.entry(id) {
2223 hash_map::Entry::Occupied(e) => {
2224 let e = e.into_mut();
2225 if let FollowerItem::Loading(updates) = e {
2226 for update in updates.drain(..) {
2227 item.apply_update_proto(update, cx)
2228 .context("failed to apply view update")
2229 .log_err();
2230 }
2231 }
2232 *e = FollowerItem::Loaded(item);
2233 }
2234 hash_map::Entry::Vacant(e) => {
2235 e.insert(FollowerItem::Loaded(item));
2236 }
2237 }
2238 }
2239
2240 Some(())
2241 });
2242 }
2243 this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
2244
2245 Ok(())
2246 }
2247
2248 fn update_followers(
2249 &self,
2250 update: proto::update_followers::Variant,
2251 cx: &AppContext,
2252 ) -> Option<()> {
2253 let project_id = self.project.read(cx).remote_id()?;
2254 if !self.leader_state.followers.is_empty() {
2255 self.client
2256 .send(proto::UpdateFollowers {
2257 project_id,
2258 follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(),
2259 variant: Some(update),
2260 })
2261 .log_err();
2262 }
2263 None
2264 }
2265
2266 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2267 self.follower_states_by_leader
2268 .iter()
2269 .find_map(|(leader_id, state)| {
2270 if state.contains_key(pane) {
2271 Some(*leader_id)
2272 } else {
2273 None
2274 }
2275 })
2276 }
2277
2278 fn update_leader_state(
2279 &mut self,
2280 leader_id: PeerId,
2281 cx: &mut ViewContext<Self>,
2282 mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext<Self>),
2283 ) {
2284 for (_, state) in self
2285 .follower_states_by_leader
2286 .get_mut(&leader_id)
2287 .into_iter()
2288 .flatten()
2289 {
2290 update_fn(state, cx);
2291 }
2292 self.leader_updated(leader_id, cx);
2293 }
2294
2295 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2296 let mut items_to_add = Vec::new();
2297 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2298 if let Some(active_item) = state
2299 .active_view_id
2300 .and_then(|id| state.items_by_leader_view_id.get(&id))
2301 {
2302 if let FollowerItem::Loaded(item) = active_item {
2303 items_to_add.push((pane.clone(), item.boxed_clone()));
2304 }
2305 }
2306 }
2307
2308 for (pane, item) in items_to_add {
2309 Pane::add_item(self, pane.clone(), item.boxed_clone(), false, false, cx);
2310 if pane == self.active_pane {
2311 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2312 }
2313 cx.notify();
2314 }
2315 None
2316 }
2317}
2318
2319impl Entity for Workspace {
2320 type Event = Event;
2321}
2322
2323impl View for Workspace {
2324 fn ui_name() -> &'static str {
2325 "Workspace"
2326 }
2327
2328 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
2329 let theme = cx.global::<Settings>().theme.clone();
2330 Stack::new()
2331 .with_child(
2332 Flex::column()
2333 .with_child(self.render_titlebar(&theme, cx))
2334 .with_child(
2335 Stack::new()
2336 .with_child({
2337 Flex::row()
2338 .with_children(
2339 if self.left_sidebar.read(cx).active_item().is_some() {
2340 Some(
2341 ChildView::new(&self.left_sidebar)
2342 .flex(0.8, false)
2343 .boxed(),
2344 )
2345 } else {
2346 None
2347 },
2348 )
2349 .with_child(
2350 FlexItem::new(self.center.render(
2351 &theme,
2352 &self.follower_states_by_leader,
2353 self.project.read(cx).collaborators(),
2354 ))
2355 .flex(1., true)
2356 .boxed(),
2357 )
2358 .with_children(
2359 if self.right_sidebar.read(cx).active_item().is_some() {
2360 Some(
2361 ChildView::new(&self.right_sidebar)
2362 .flex(0.8, false)
2363 .boxed(),
2364 )
2365 } else {
2366 None
2367 },
2368 )
2369 .boxed()
2370 })
2371 .with_children(self.modal.as_ref().map(|m| {
2372 ChildView::new(m)
2373 .contained()
2374 .with_style(theme.workspace.modal)
2375 .aligned()
2376 .top()
2377 .boxed()
2378 }))
2379 .with_children(self.render_notifications(&theme.workspace))
2380 .flex(1.0, true)
2381 .boxed(),
2382 )
2383 .with_child(ChildView::new(&self.status_bar).boxed())
2384 .contained()
2385 .with_background_color(theme.workspace.background)
2386 .boxed(),
2387 )
2388 .with_children(self.render_disconnected_overlay(cx))
2389 .named("workspace")
2390 }
2391
2392 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
2393 cx.focus(&self.active_pane);
2394 }
2395}
2396
2397pub trait WorkspaceHandle {
2398 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2399}
2400
2401impl WorkspaceHandle for ViewHandle<Workspace> {
2402 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2403 self.read(cx)
2404 .worktrees(cx)
2405 .flat_map(|worktree| {
2406 let worktree_id = worktree.read(cx).id();
2407 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2408 worktree_id,
2409 path: f.path.clone(),
2410 })
2411 })
2412 .collect::<Vec<_>>()
2413 }
2414}
2415
2416pub struct AvatarRibbon {
2417 color: Color,
2418}
2419
2420impl AvatarRibbon {
2421 pub fn new(color: Color) -> AvatarRibbon {
2422 AvatarRibbon { color }
2423 }
2424}
2425
2426impl Element for AvatarRibbon {
2427 type LayoutState = ();
2428
2429 type PaintState = ();
2430
2431 fn layout(
2432 &mut self,
2433 constraint: gpui::SizeConstraint,
2434 _: &mut gpui::LayoutContext,
2435 ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
2436 (constraint.max, ())
2437 }
2438
2439 fn paint(
2440 &mut self,
2441 bounds: gpui::geometry::rect::RectF,
2442 _: gpui::geometry::rect::RectF,
2443 _: &mut Self::LayoutState,
2444 cx: &mut gpui::PaintContext,
2445 ) -> Self::PaintState {
2446 let mut path = PathBuilder::new();
2447 path.reset(bounds.lower_left());
2448 path.curve_to(
2449 bounds.origin() + vec2f(bounds.height(), 0.),
2450 bounds.origin(),
2451 );
2452 path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
2453 path.curve_to(bounds.lower_right(), bounds.upper_right());
2454 path.line_to(bounds.lower_left());
2455 cx.scene.push_path(path.build(self.color, None));
2456 }
2457
2458 fn dispatch_event(
2459 &mut self,
2460 _: &gpui::Event,
2461 _: RectF,
2462 _: RectF,
2463 _: &mut Self::LayoutState,
2464 _: &mut Self::PaintState,
2465 _: &mut gpui::EventContext,
2466 ) -> bool {
2467 false
2468 }
2469
2470 fn debug(
2471 &self,
2472 bounds: gpui::geometry::rect::RectF,
2473 _: &Self::LayoutState,
2474 _: &Self::PaintState,
2475 _: &gpui::DebugContext,
2476 ) -> gpui::json::Value {
2477 json::json!({
2478 "type": "AvatarRibbon",
2479 "bounds": bounds.to_json(),
2480 "color": self.color.to_json(),
2481 })
2482 }
2483}
2484
2485impl std::fmt::Debug for OpenPaths {
2486 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2487 f.debug_struct("OpenPaths")
2488 .field("paths", &self.paths)
2489 .finish()
2490 }
2491}
2492
2493fn open(_: &Open, cx: &mut MutableAppContext) {
2494 let mut paths = cx.prompt_for_paths(PathPromptOptions {
2495 files: true,
2496 directories: true,
2497 multiple: true,
2498 });
2499 cx.spawn(|mut cx| async move {
2500 if let Some(paths) = paths.recv().await.flatten() {
2501 cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
2502 }
2503 })
2504 .detach();
2505}
2506
2507pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2508
2509pub fn activate_workspace_for_project(
2510 cx: &mut MutableAppContext,
2511 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2512) -> Option<ViewHandle<Workspace>> {
2513 for window_id in cx.window_ids().collect::<Vec<_>>() {
2514 if let Some(workspace_handle) = cx.root_view::<Workspace>(window_id) {
2515 let project = workspace_handle.read(cx).project.clone();
2516 if project.update(cx, &predicate) {
2517 cx.activate_window(window_id);
2518 return Some(workspace_handle);
2519 }
2520 }
2521 }
2522 None
2523}
2524
2525pub fn open_paths(
2526 abs_paths: &[PathBuf],
2527 app_state: &Arc<AppState>,
2528 cx: &mut MutableAppContext,
2529) -> Task<(
2530 ViewHandle<Workspace>,
2531 Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
2532)> {
2533 log::info!("open paths {:?}", abs_paths);
2534
2535 // Open paths in existing workspace if possible
2536 let existing =
2537 activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
2538
2539 let app_state = app_state.clone();
2540 let abs_paths = abs_paths.to_vec();
2541 cx.spawn(|mut cx| async move {
2542 let mut new_project = None;
2543 let workspace = if let Some(existing) = existing {
2544 existing
2545 } else {
2546 let contains_directory =
2547 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2548 .await
2549 .contains(&false);
2550
2551 cx.add_window((app_state.build_window_options)(), |cx| {
2552 let project = Project::local(
2553 false,
2554 app_state.client.clone(),
2555 app_state.user_store.clone(),
2556 app_state.project_store.clone(),
2557 app_state.languages.clone(),
2558 app_state.fs.clone(),
2559 cx,
2560 );
2561 new_project = Some(project.clone());
2562 let mut workspace = Workspace::new(project, cx);
2563 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
2564 if contains_directory {
2565 workspace.toggle_sidebar(Side::Left, cx);
2566 }
2567 workspace
2568 })
2569 .1
2570 };
2571
2572 let items = workspace
2573 .update(&mut cx, |workspace, cx| {
2574 workspace.open_paths(abs_paths, true, cx)
2575 })
2576 .await;
2577
2578 if let Some(project) = new_project {
2579 project
2580 .update(&mut cx, |project, cx| project.restore_state(cx))
2581 .await
2582 .log_err();
2583 }
2584
2585 (workspace, items)
2586 })
2587}
2588
2589pub fn join_project(
2590 contact: Arc<Contact>,
2591 project_index: usize,
2592 app_state: &Arc<AppState>,
2593 cx: &mut MutableAppContext,
2594) {
2595 let project_id = contact.projects[project_index].id;
2596
2597 for window_id in cx.window_ids().collect::<Vec<_>>() {
2598 if let Some(workspace) = cx.root_view::<Workspace>(window_id) {
2599 if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) {
2600 cx.activate_window(window_id);
2601 return;
2602 }
2603 }
2604 }
2605
2606 cx.add_window((app_state.build_window_options)(), |cx| {
2607 WaitingRoom::new(contact, project_index, app_state.clone(), cx)
2608 });
2609}
2610
2611fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
2612 let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
2613 let mut workspace = Workspace::new(
2614 Project::local(
2615 false,
2616 app_state.client.clone(),
2617 app_state.user_store.clone(),
2618 app_state.project_store.clone(),
2619 app_state.languages.clone(),
2620 app_state.fs.clone(),
2621 cx,
2622 ),
2623 cx,
2624 );
2625 (app_state.initialize_workspace)(&mut workspace, app_state, cx);
2626 workspace
2627 });
2628 cx.dispatch_action(window_id, vec![workspace.id()], &NewFile);
2629}
2630
2631#[cfg(test)]
2632mod tests {
2633 use super::*;
2634 use gpui::{ModelHandle, TestAppContext, ViewContext};
2635 use project::{FakeFs, Project, ProjectEntryId};
2636 use serde_json::json;
2637
2638 #[gpui::test]
2639 async fn test_tracking_active_path(cx: &mut TestAppContext) {
2640 cx.foreground().forbid_parking();
2641 Settings::test_async(cx);
2642 let fs = FakeFs::new(cx.background());
2643 fs.insert_tree(
2644 "/root1",
2645 json!({
2646 "one.txt": "",
2647 "two.txt": "",
2648 }),
2649 )
2650 .await;
2651 fs.insert_tree(
2652 "/root2",
2653 json!({
2654 "three.txt": "",
2655 }),
2656 )
2657 .await;
2658
2659 let project = Project::test(fs, ["root1".as_ref()], cx).await;
2660 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
2661 let worktree_id = project.read_with(cx, |project, cx| {
2662 project.worktrees(cx).next().unwrap().read(cx).id()
2663 });
2664
2665 let item1 = cx.add_view(window_id, |_| {
2666 let mut item = TestItem::new();
2667 item.project_path = Some((worktree_id, "one.txt").into());
2668 item
2669 });
2670 let item2 = cx.add_view(window_id, |_| {
2671 let mut item = TestItem::new();
2672 item.project_path = Some((worktree_id, "two.txt").into());
2673 item
2674 });
2675
2676 // Add an item to an empty pane
2677 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
2678 project.read_with(cx, |project, cx| {
2679 assert_eq!(
2680 project.active_entry(),
2681 project.entry_for_path(&(worktree_id, "one.txt").into(), cx)
2682 );
2683 });
2684 assert_eq!(
2685 cx.current_window_title(window_id).as_deref(),
2686 Some("one.txt — root1")
2687 );
2688
2689 // Add a second item to a non-empty pane
2690 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
2691 assert_eq!(
2692 cx.current_window_title(window_id).as_deref(),
2693 Some("two.txt — root1")
2694 );
2695 project.read_with(cx, |project, cx| {
2696 assert_eq!(
2697 project.active_entry(),
2698 project.entry_for_path(&(worktree_id, "two.txt").into(), cx)
2699 );
2700 });
2701
2702 // Close the active item
2703 workspace
2704 .update(cx, |workspace, cx| {
2705 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
2706 })
2707 .await
2708 .unwrap();
2709 assert_eq!(
2710 cx.current_window_title(window_id).as_deref(),
2711 Some("one.txt — root1")
2712 );
2713 project.read_with(cx, |project, cx| {
2714 assert_eq!(
2715 project.active_entry(),
2716 project.entry_for_path(&(worktree_id, "one.txt").into(), cx)
2717 );
2718 });
2719
2720 // Add a project folder
2721 project
2722 .update(cx, |project, cx| {
2723 project.find_or_create_local_worktree("/root2", true, cx)
2724 })
2725 .await
2726 .unwrap();
2727 assert_eq!(
2728 cx.current_window_title(window_id).as_deref(),
2729 Some("one.txt — root1, root2")
2730 );
2731
2732 // Remove a project folder
2733 project.update(cx, |project, cx| {
2734 project.remove_worktree(worktree_id, cx);
2735 });
2736 assert_eq!(
2737 cx.current_window_title(window_id).as_deref(),
2738 Some("one.txt — root2")
2739 );
2740 }
2741
2742 #[gpui::test]
2743 async fn test_close_window(cx: &mut TestAppContext) {
2744 cx.foreground().forbid_parking();
2745 Settings::test_async(cx);
2746 let fs = FakeFs::new(cx.background());
2747 fs.insert_tree("/root", json!({ "one": "" })).await;
2748
2749 let project = Project::test(fs, ["root".as_ref()], cx).await;
2750 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
2751
2752 // When there are no dirty items, there's nothing to do.
2753 let item1 = cx.add_view(window_id, |_| TestItem::new());
2754 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
2755 let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
2756 assert_eq!(task.await.unwrap(), true);
2757
2758 // When there are dirty untitled items, prompt to save each one. If the user
2759 // cancels any prompt, then abort.
2760 let item2 = cx.add_view(window_id, |_| {
2761 let mut item = TestItem::new();
2762 item.is_dirty = true;
2763 item
2764 });
2765 let item3 = cx.add_view(window_id, |_| {
2766 let mut item = TestItem::new();
2767 item.is_dirty = true;
2768 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
2769 item
2770 });
2771 workspace.update(cx, |w, cx| {
2772 w.add_item(Box::new(item2.clone()), cx);
2773 w.add_item(Box::new(item3.clone()), cx);
2774 });
2775 let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
2776 cx.foreground().run_until_parked();
2777 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
2778 cx.foreground().run_until_parked();
2779 assert!(!cx.has_pending_prompt(window_id));
2780 assert_eq!(task.await.unwrap(), false);
2781 }
2782
2783 #[gpui::test]
2784 async fn test_close_pane_items(cx: &mut TestAppContext) {
2785 cx.foreground().forbid_parking();
2786 Settings::test_async(cx);
2787 let fs = FakeFs::new(cx.background());
2788
2789 let project = Project::test(fs, None, cx).await;
2790 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
2791
2792 let item1 = cx.add_view(window_id, |_| {
2793 let mut item = TestItem::new();
2794 item.is_dirty = true;
2795 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
2796 item
2797 });
2798 let item2 = cx.add_view(window_id, |_| {
2799 let mut item = TestItem::new();
2800 item.is_dirty = true;
2801 item.has_conflict = true;
2802 item.project_entry_ids = vec![ProjectEntryId::from_proto(2)];
2803 item
2804 });
2805 let item3 = cx.add_view(window_id, |_| {
2806 let mut item = TestItem::new();
2807 item.is_dirty = true;
2808 item.has_conflict = true;
2809 item.project_entry_ids = vec![ProjectEntryId::from_proto(3)];
2810 item
2811 });
2812 let item4 = cx.add_view(window_id, |_| {
2813 let mut item = TestItem::new();
2814 item.is_dirty = true;
2815 item
2816 });
2817 let pane = workspace.update(cx, |workspace, cx| {
2818 workspace.add_item(Box::new(item1.clone()), cx);
2819 workspace.add_item(Box::new(item2.clone()), cx);
2820 workspace.add_item(Box::new(item3.clone()), cx);
2821 workspace.add_item(Box::new(item4.clone()), cx);
2822 workspace.active_pane().clone()
2823 });
2824
2825 let close_items = workspace.update(cx, |workspace, cx| {
2826 pane.update(cx, |pane, cx| {
2827 pane.activate_item(1, true, true, cx);
2828 assert_eq!(pane.active_item().unwrap().id(), item2.id());
2829 });
2830
2831 let item1_id = item1.id();
2832 let item3_id = item3.id();
2833 let item4_id = item4.id();
2834 Pane::close_items(workspace, pane.clone(), cx, move |id| {
2835 [item1_id, item3_id, item4_id].contains(&id)
2836 })
2837 });
2838
2839 cx.foreground().run_until_parked();
2840 pane.read_with(cx, |pane, _| {
2841 assert_eq!(pane.items().count(), 4);
2842 assert_eq!(pane.active_item().unwrap().id(), item1.id());
2843 });
2844
2845 cx.simulate_prompt_answer(window_id, 0);
2846 cx.foreground().run_until_parked();
2847 pane.read_with(cx, |pane, cx| {
2848 assert_eq!(item1.read(cx).save_count, 1);
2849 assert_eq!(item1.read(cx).save_as_count, 0);
2850 assert_eq!(item1.read(cx).reload_count, 0);
2851 assert_eq!(pane.items().count(), 3);
2852 assert_eq!(pane.active_item().unwrap().id(), item3.id());
2853 });
2854
2855 cx.simulate_prompt_answer(window_id, 1);
2856 cx.foreground().run_until_parked();
2857 pane.read_with(cx, |pane, cx| {
2858 assert_eq!(item3.read(cx).save_count, 0);
2859 assert_eq!(item3.read(cx).save_as_count, 0);
2860 assert_eq!(item3.read(cx).reload_count, 1);
2861 assert_eq!(pane.items().count(), 2);
2862 assert_eq!(pane.active_item().unwrap().id(), item4.id());
2863 });
2864
2865 cx.simulate_prompt_answer(window_id, 0);
2866 cx.foreground().run_until_parked();
2867 cx.simulate_new_path_selection(|_| Some(Default::default()));
2868 close_items.await.unwrap();
2869 pane.read_with(cx, |pane, cx| {
2870 assert_eq!(item4.read(cx).save_count, 0);
2871 assert_eq!(item4.read(cx).save_as_count, 1);
2872 assert_eq!(item4.read(cx).reload_count, 0);
2873 assert_eq!(pane.items().count(), 1);
2874 assert_eq!(pane.active_item().unwrap().id(), item2.id());
2875 });
2876 }
2877
2878 #[gpui::test]
2879 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
2880 cx.foreground().forbid_parking();
2881 Settings::test_async(cx);
2882 let fs = FakeFs::new(cx.background());
2883
2884 let project = Project::test(fs, [], cx).await;
2885 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
2886
2887 // Create several workspace items with single project entries, and two
2888 // workspace items with multiple project entries.
2889 let single_entry_items = (0..=4)
2890 .map(|project_entry_id| {
2891 let mut item = TestItem::new();
2892 item.is_dirty = true;
2893 item.project_entry_ids = vec![ProjectEntryId::from_proto(project_entry_id)];
2894 item.is_singleton = true;
2895 item
2896 })
2897 .collect::<Vec<_>>();
2898 let item_2_3 = {
2899 let mut item = TestItem::new();
2900 item.is_dirty = true;
2901 item.is_singleton = false;
2902 item.project_entry_ids =
2903 vec![ProjectEntryId::from_proto(2), ProjectEntryId::from_proto(3)];
2904 item
2905 };
2906 let item_3_4 = {
2907 let mut item = TestItem::new();
2908 item.is_dirty = true;
2909 item.is_singleton = false;
2910 item.project_entry_ids =
2911 vec![ProjectEntryId::from_proto(3), ProjectEntryId::from_proto(4)];
2912 item
2913 };
2914
2915 // Create two panes that contain the following project entries:
2916 // left pane:
2917 // multi-entry items: (2, 3)
2918 // single-entry items: 0, 1, 2, 3, 4
2919 // right pane:
2920 // single-entry items: 1
2921 // multi-entry items: (3, 4)
2922 let left_pane = workspace.update(cx, |workspace, cx| {
2923 let left_pane = workspace.active_pane().clone();
2924 let right_pane = workspace.split_pane(left_pane.clone(), SplitDirection::Right, cx);
2925
2926 workspace.activate_pane(left_pane.clone(), cx);
2927 workspace.add_item(Box::new(cx.add_view(|_| item_2_3.clone())), cx);
2928 for item in &single_entry_items {
2929 workspace.add_item(Box::new(cx.add_view(|_| item.clone())), cx);
2930 }
2931
2932 workspace.activate_pane(right_pane.clone(), cx);
2933 workspace.add_item(Box::new(cx.add_view(|_| single_entry_items[1].clone())), cx);
2934 workspace.add_item(Box::new(cx.add_view(|_| item_3_4.clone())), cx);
2935
2936 left_pane
2937 });
2938
2939 // When closing all of the items in the left pane, we should be prompted twice:
2940 // once for project entry 0, and once for project entry 2. After those two
2941 // prompts, the task should complete.
2942 let close = workspace.update(cx, |workspace, cx| {
2943 workspace.activate_pane(left_pane.clone(), cx);
2944 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
2945 });
2946
2947 cx.foreground().run_until_parked();
2948 left_pane.read_with(cx, |pane, cx| {
2949 assert_eq!(
2950 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
2951 &[ProjectEntryId::from_proto(0)]
2952 );
2953 });
2954 cx.simulate_prompt_answer(window_id, 0);
2955
2956 cx.foreground().run_until_parked();
2957 left_pane.read_with(cx, |pane, cx| {
2958 assert_eq!(
2959 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
2960 &[ProjectEntryId::from_proto(2)]
2961 );
2962 });
2963 cx.simulate_prompt_answer(window_id, 0);
2964
2965 cx.foreground().run_until_parked();
2966 close.await.unwrap();
2967 left_pane.read_with(cx, |pane, _| {
2968 assert_eq!(pane.items().count(), 0);
2969 });
2970 }
2971
2972 #[derive(Clone)]
2973 struct TestItem {
2974 save_count: usize,
2975 save_as_count: usize,
2976 reload_count: usize,
2977 is_dirty: bool,
2978 has_conflict: bool,
2979 project_entry_ids: Vec<ProjectEntryId>,
2980 project_path: Option<ProjectPath>,
2981 is_singleton: bool,
2982 }
2983
2984 impl TestItem {
2985 fn new() -> Self {
2986 Self {
2987 save_count: 0,
2988 save_as_count: 0,
2989 reload_count: 0,
2990 is_dirty: false,
2991 has_conflict: false,
2992 project_entry_ids: Vec::new(),
2993 project_path: None,
2994 is_singleton: true,
2995 }
2996 }
2997 }
2998
2999 impl Entity for TestItem {
3000 type Event = ();
3001 }
3002
3003 impl View for TestItem {
3004 fn ui_name() -> &'static str {
3005 "TestItem"
3006 }
3007
3008 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
3009 Empty::new().boxed()
3010 }
3011 }
3012
3013 impl Item for TestItem {
3014 fn tab_content(&self, _: &theme::Tab, _: &AppContext) -> ElementBox {
3015 Empty::new().boxed()
3016 }
3017
3018 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
3019 self.project_path.clone()
3020 }
3021
3022 fn project_entry_ids(&self, _: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
3023 self.project_entry_ids.iter().copied().collect()
3024 }
3025
3026 fn is_singleton(&self, _: &AppContext) -> bool {
3027 self.is_singleton
3028 }
3029
3030 fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
3031
3032 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
3033 where
3034 Self: Sized,
3035 {
3036 Some(self.clone())
3037 }
3038
3039 fn is_dirty(&self, _: &AppContext) -> bool {
3040 self.is_dirty
3041 }
3042
3043 fn has_conflict(&self, _: &AppContext) -> bool {
3044 self.has_conflict
3045 }
3046
3047 fn can_save(&self, _: &AppContext) -> bool {
3048 self.project_entry_ids.len() > 0
3049 }
3050
3051 fn save(
3052 &mut self,
3053 _: ModelHandle<Project>,
3054 _: &mut ViewContext<Self>,
3055 ) -> Task<anyhow::Result<()>> {
3056 self.save_count += 1;
3057 Task::ready(Ok(()))
3058 }
3059
3060 fn save_as(
3061 &mut self,
3062 _: ModelHandle<Project>,
3063 _: std::path::PathBuf,
3064 _: &mut ViewContext<Self>,
3065 ) -> Task<anyhow::Result<()>> {
3066 self.save_as_count += 1;
3067 Task::ready(Ok(()))
3068 }
3069
3070 fn reload(
3071 &mut self,
3072 _: ModelHandle<Project>,
3073 _: &mut ViewContext<Self>,
3074 ) -> Task<anyhow::Result<()>> {
3075 self.reload_count += 1;
3076 Task::ready(Ok(()))
3077 }
3078
3079 fn should_update_tab_on_event(_: &Self::Event) -> bool {
3080 true
3081 }
3082 }
3083}