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 pub fn worktrees<'a>(
895 &self,
896 cx: &'a AppContext,
897 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
898 self.project.read(cx).worktrees(cx)
899 }
900
901 pub fn visible_worktrees<'a>(
902 &self,
903 cx: &'a AppContext,
904 ) -> impl 'a + Iterator<Item = ModelHandle<Worktree>> {
905 self.project.read(cx).visible_worktrees(cx)
906 }
907
908 pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future<Output = ()> + 'static {
909 let futures = self
910 .worktrees(cx)
911 .filter_map(|worktree| worktree.read(cx).as_local())
912 .map(|worktree| worktree.scan_complete())
913 .collect::<Vec<_>>();
914 async move {
915 for future in futures {
916 future.await;
917 }
918 }
919 }
920
921 pub fn close(
922 &mut self,
923 _: &CloseWindow,
924 cx: &mut ViewContext<Self>,
925 ) -> Option<Task<Result<()>>> {
926 let prepare = self.prepare_to_close(cx);
927 Some(cx.spawn(|this, mut cx| async move {
928 if prepare.await? {
929 this.update(&mut cx, |_, cx| {
930 let window_id = cx.window_id();
931 cx.remove_window(window_id);
932 });
933 }
934 Ok(())
935 }))
936 }
937
938 pub fn prepare_to_close(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
939 self.save_all_internal(true, cx)
940 }
941
942 fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
943 let save_all = self.save_all_internal(false, cx);
944 Some(cx.foreground().spawn(async move {
945 save_all.await?;
946 Ok(())
947 }))
948 }
949
950 fn save_all_internal(
951 &mut self,
952 should_prompt_to_save: bool,
953 cx: &mut ViewContext<Self>,
954 ) -> Task<Result<bool>> {
955 let dirty_items = self
956 .panes
957 .iter()
958 .flat_map(|pane| {
959 pane.read(cx).items().filter_map(|item| {
960 if item.is_dirty(cx) {
961 Some((pane.clone(), item.boxed_clone()))
962 } else {
963 None
964 }
965 })
966 })
967 .collect::<Vec<_>>();
968
969 let project = self.project.clone();
970 cx.spawn_weak(|_, mut cx| async move {
971 // let mut saved_project_entry_ids = HashSet::default();
972 for (pane, item) in dirty_items {
973 let (is_singl, project_entry_ids) =
974 cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
975 if is_singl || !project_entry_ids.is_empty() {
976 if let Some(ix) =
977 pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))
978 {
979 if !Pane::save_item(
980 project.clone(),
981 &pane,
982 ix,
983 &item,
984 should_prompt_to_save,
985 &mut cx,
986 )
987 .await?
988 {
989 return Ok(false);
990 }
991 }
992 }
993 }
994 Ok(true)
995 })
996 }
997
998 pub fn open_paths(
999 &mut self,
1000 mut abs_paths: Vec<PathBuf>,
1001 visible: bool,
1002 cx: &mut ViewContext<Self>,
1003 ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>> {
1004 let fs = self.fs.clone();
1005
1006 // Sort the paths to ensure we add worktrees for parents before their children.
1007 abs_paths.sort_unstable();
1008 cx.spawn(|this, mut cx| async move {
1009 let mut project_paths = Vec::new();
1010 for path in &abs_paths {
1011 project_paths.push(
1012 this.update(&mut cx, |this, cx| {
1013 this.project_path_for_path(path, visible, cx)
1014 })
1015 .await
1016 .log_err(),
1017 );
1018 }
1019
1020 let tasks = abs_paths
1021 .iter()
1022 .cloned()
1023 .zip(project_paths.into_iter())
1024 .map(|(abs_path, project_path)| {
1025 let this = this.clone();
1026 cx.spawn(|mut cx| {
1027 let fs = fs.clone();
1028 async move {
1029 let (_worktree, project_path) = project_path?;
1030 if fs.is_file(&abs_path).await {
1031 Some(
1032 this.update(&mut cx, |this, cx| {
1033 this.open_path(project_path, true, cx)
1034 })
1035 .await,
1036 )
1037 } else {
1038 None
1039 }
1040 }
1041 })
1042 })
1043 .collect::<Vec<_>>();
1044
1045 futures::future::join_all(tasks).await
1046 })
1047 }
1048
1049 fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
1050 let mut paths = cx.prompt_for_paths(PathPromptOptions {
1051 files: false,
1052 directories: true,
1053 multiple: true,
1054 });
1055 cx.spawn(|this, mut cx| async move {
1056 if let Some(paths) = paths.recv().await.flatten() {
1057 let results = this
1058 .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))
1059 .await;
1060 for result in results {
1061 if let Some(result) = result {
1062 result.log_err();
1063 }
1064 }
1065 }
1066 })
1067 .detach();
1068 }
1069
1070 fn remove_folder_from_project(
1071 &mut self,
1072 RemoveWorktreeFromProject(worktree_id): &RemoveWorktreeFromProject,
1073 cx: &mut ViewContext<Self>,
1074 ) {
1075 self.project
1076 .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx));
1077 }
1078
1079 fn toggle_project_online(&mut self, action: &ToggleProjectOnline, cx: &mut ViewContext<Self>) {
1080 let project = action
1081 .project
1082 .clone()
1083 .unwrap_or_else(|| self.project.clone());
1084 project.update(cx, |project, cx| {
1085 let public = !project.is_online();
1086 project.set_online(public, cx);
1087 });
1088 }
1089
1090 fn project_path_for_path(
1091 &self,
1092 abs_path: &Path,
1093 visible: bool,
1094 cx: &mut ViewContext<Self>,
1095 ) -> Task<Result<(ModelHandle<Worktree>, ProjectPath)>> {
1096 let entry = self.project().update(cx, |project, cx| {
1097 project.find_or_create_local_worktree(abs_path, visible, cx)
1098 });
1099 cx.spawn(|_, cx| async move {
1100 let (worktree, path) = entry.await?;
1101 let worktree_id = worktree.read_with(&cx, |t, _| t.id());
1102 Ok((
1103 worktree,
1104 ProjectPath {
1105 worktree_id,
1106 path: path.into(),
1107 },
1108 ))
1109 })
1110 }
1111
1112 /// Returns the modal that was toggled closed if it was open.
1113 pub fn toggle_modal<V, F>(
1114 &mut self,
1115 cx: &mut ViewContext<Self>,
1116 add_view: F,
1117 ) -> Option<ViewHandle<V>>
1118 where
1119 V: 'static + View,
1120 F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
1121 {
1122 cx.notify();
1123 // Whatever modal was visible is getting clobbered. If its the same type as V, then return
1124 // it. Otherwise, create a new modal and set it as active.
1125 let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
1126 if let Some(already_open_modal) = already_open_modal {
1127 cx.focus_self();
1128 Some(already_open_modal)
1129 } else {
1130 let modal = add_view(self, cx);
1131 cx.focus(&modal);
1132 self.modal = Some(modal.into());
1133 None
1134 }
1135 }
1136
1137 pub fn modal(&self) -> Option<&AnyViewHandle> {
1138 self.modal.as_ref()
1139 }
1140
1141 pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
1142 if self.modal.take().is_some() {
1143 cx.focus(&self.active_pane);
1144 cx.notify();
1145 }
1146 }
1147
1148 pub fn show_notification<V: Notification>(
1149 &mut self,
1150 id: usize,
1151 cx: &mut ViewContext<Self>,
1152 build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
1153 ) {
1154 let type_id = TypeId::of::<V>();
1155 if self
1156 .notifications
1157 .iter()
1158 .all(|(existing_type_id, existing_id, _)| {
1159 (*existing_type_id, *existing_id) != (type_id, id)
1160 })
1161 {
1162 let notification = build_notification(cx);
1163 cx.subscribe(¬ification, move |this, handle, event, cx| {
1164 if handle.read(cx).should_dismiss_notification_on_event(event) {
1165 this.dismiss_notification(type_id, id, cx);
1166 }
1167 })
1168 .detach();
1169 self.notifications
1170 .push((type_id, id, Box::new(notification)));
1171 cx.notify();
1172 }
1173 }
1174
1175 fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext<Self>) {
1176 self.notifications
1177 .retain(|(existing_type_id, existing_id, _)| {
1178 if (*existing_type_id, *existing_id) == (type_id, id) {
1179 cx.notify();
1180 false
1181 } else {
1182 true
1183 }
1184 });
1185 }
1186
1187 pub fn items<'a>(
1188 &'a self,
1189 cx: &'a AppContext,
1190 ) -> impl 'a + Iterator<Item = &Box<dyn ItemHandle>> {
1191 self.panes.iter().flat_map(|pane| pane.read(cx).items())
1192 }
1193
1194 pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
1195 self.items_of_type(cx).max_by_key(|item| item.id())
1196 }
1197
1198 pub fn items_of_type<'a, T: Item>(
1199 &'a self,
1200 cx: &'a AppContext,
1201 ) -> impl 'a + Iterator<Item = ViewHandle<T>> {
1202 self.panes
1203 .iter()
1204 .flat_map(|pane| pane.read(cx).items_of_type())
1205 }
1206
1207 pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
1208 self.active_pane().read(cx).active_item()
1209 }
1210
1211 fn active_project_path(&self, cx: &ViewContext<Self>) -> Option<ProjectPath> {
1212 self.active_item(cx).and_then(|item| item.project_path(cx))
1213 }
1214
1215 pub fn save_active_item(
1216 &mut self,
1217 force_name_change: bool,
1218 cx: &mut ViewContext<Self>,
1219 ) -> Task<Result<()>> {
1220 let project = self.project.clone();
1221 if let Some(item) = self.active_item(cx) {
1222 if !force_name_change && item.can_save(cx) {
1223 if item.has_conflict(cx.as_ref()) {
1224 const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
1225
1226 let mut answer = cx.prompt(
1227 PromptLevel::Warning,
1228 CONFLICT_MESSAGE,
1229 &["Overwrite", "Cancel"],
1230 );
1231 cx.spawn(|_, mut cx| async move {
1232 let answer = answer.recv().await;
1233 if answer == Some(0) {
1234 cx.update(|cx| item.save(project, cx)).await?;
1235 }
1236 Ok(())
1237 })
1238 } else {
1239 item.save(project, cx)
1240 }
1241 } else if item.is_singleton(cx) {
1242 let worktree = self.worktrees(cx).next();
1243 let start_abs_path = worktree
1244 .and_then(|w| w.read(cx).as_local())
1245 .map_or(Path::new(""), |w| w.abs_path())
1246 .to_path_buf();
1247 let mut abs_path = cx.prompt_for_new_path(&start_abs_path);
1248 cx.spawn(|_, mut cx| async move {
1249 if let Some(abs_path) = abs_path.recv().await.flatten() {
1250 cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
1251 }
1252 Ok(())
1253 })
1254 } else {
1255 Task::ready(Ok(()))
1256 }
1257 } else {
1258 Task::ready(Ok(()))
1259 }
1260 }
1261
1262 pub fn toggle_sidebar(&mut self, side: Side, cx: &mut ViewContext<Self>) {
1263 let sidebar = match side {
1264 Side::Left => &mut self.left_sidebar,
1265 Side::Right => &mut self.right_sidebar,
1266 };
1267 sidebar.update(cx, |sidebar, cx| {
1268 sidebar.set_open(!sidebar.is_open(), cx);
1269 });
1270 cx.focus_self();
1271 cx.notify();
1272 }
1273
1274 pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
1275 let sidebar = match action.side {
1276 Side::Left => &mut self.left_sidebar,
1277 Side::Right => &mut self.right_sidebar,
1278 };
1279 let active_item = sidebar.update(cx, |sidebar, cx| {
1280 if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
1281 sidebar.set_open(false, cx);
1282 None
1283 } else {
1284 sidebar.set_open(true, cx);
1285 sidebar.activate_item(action.item_index, cx);
1286 sidebar.active_item().cloned()
1287 }
1288 });
1289 if let Some(active_item) = active_item {
1290 if active_item.is_focused(cx) {
1291 cx.focus_self();
1292 } else {
1293 cx.focus(active_item.to_any());
1294 }
1295 } else {
1296 cx.focus_self();
1297 }
1298 cx.notify();
1299 }
1300
1301 pub fn toggle_sidebar_item_focus(
1302 &mut self,
1303 side: Side,
1304 item_index: usize,
1305 cx: &mut ViewContext<Self>,
1306 ) {
1307 let sidebar = match side {
1308 Side::Left => &mut self.left_sidebar,
1309 Side::Right => &mut self.right_sidebar,
1310 };
1311 let active_item = sidebar.update(cx, |sidebar, cx| {
1312 sidebar.set_open(true, cx);
1313 sidebar.activate_item(item_index, cx);
1314 sidebar.active_item().cloned()
1315 });
1316 if let Some(active_item) = active_item {
1317 if active_item.is_focused(cx) {
1318 cx.focus_self();
1319 } else {
1320 cx.focus(active_item.to_any());
1321 }
1322 }
1323 cx.notify();
1324 }
1325
1326 pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
1327 cx.focus_self();
1328 cx.notify();
1329 }
1330
1331 fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
1332 let pane = cx.add_view(|cx| Pane::new(cx));
1333 let pane_id = pane.id();
1334 cx.subscribe(&pane, move |this, _, event, cx| {
1335 this.handle_pane_event(pane_id, event, cx)
1336 })
1337 .detach();
1338 self.panes.push(pane.clone());
1339 self.activate_pane(pane.clone(), cx);
1340 cx.emit(Event::PaneAdded(pane.clone()));
1341 pane
1342 }
1343
1344 pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
1345 let pane = self.active_pane().clone();
1346 Pane::add_item(self, pane, item, true, true, cx);
1347 }
1348
1349 pub fn open_path(
1350 &mut self,
1351 path: impl Into<ProjectPath>,
1352 focus_item: bool,
1353 cx: &mut ViewContext<Self>,
1354 ) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
1355 let pane = self.active_pane().downgrade();
1356 let task = self.load_path(path.into(), cx);
1357 cx.spawn(|this, mut cx| async move {
1358 let (project_entry_id, build_item) = task.await?;
1359 let pane = pane
1360 .upgrade(&cx)
1361 .ok_or_else(|| anyhow!("pane was closed"))?;
1362 this.update(&mut cx, |this, cx| {
1363 Ok(Pane::open_item(
1364 this,
1365 pane,
1366 project_entry_id,
1367 focus_item,
1368 cx,
1369 build_item,
1370 ))
1371 })
1372 })
1373 }
1374
1375 pub(crate) fn load_path(
1376 &mut self,
1377 path: ProjectPath,
1378 cx: &mut ViewContext<Self>,
1379 ) -> Task<
1380 Result<(
1381 ProjectEntryId,
1382 impl 'static + FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
1383 )>,
1384 > {
1385 let project = self.project().clone();
1386 let project_item = project.update(cx, |project, cx| project.open_path(path, cx));
1387 let window_id = cx.window_id();
1388 cx.as_mut().spawn(|mut cx| async move {
1389 let (project_entry_id, project_item) = project_item.await?;
1390 let build_item = cx.update(|cx| {
1391 cx.default_global::<ProjectItemBuilders>()
1392 .get(&project_item.model_type())
1393 .ok_or_else(|| anyhow!("no item builder for project item"))
1394 .cloned()
1395 })?;
1396 let build_item =
1397 move |cx: &mut MutableAppContext| build_item(window_id, project, project_item, cx);
1398 Ok((project_entry_id, build_item))
1399 })
1400 }
1401
1402 pub fn open_project_item<T>(
1403 &mut self,
1404 project_item: ModelHandle<T::Item>,
1405 cx: &mut ViewContext<Self>,
1406 ) -> ViewHandle<T>
1407 where
1408 T: ProjectItem,
1409 {
1410 use project::Item as _;
1411
1412 let entry_id = project_item.read(cx).entry_id(cx);
1413 if let Some(item) = entry_id
1414 .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx))
1415 .and_then(|item| item.downcast())
1416 {
1417 self.activate_item(&item, cx);
1418 return item;
1419 }
1420
1421 let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
1422 self.add_item(Box::new(item.clone()), cx);
1423 item
1424 }
1425
1426 pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
1427 let result = self.panes.iter().find_map(|pane| {
1428 if let Some(ix) = pane.read(cx).index_for_item(item) {
1429 Some((pane.clone(), ix))
1430 } else {
1431 None
1432 }
1433 });
1434 if let Some((pane, ix)) = result {
1435 self.activate_pane(pane.clone(), cx);
1436 pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
1437 true
1438 } else {
1439 false
1440 }
1441 }
1442
1443 fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
1444 let panes = self.center.panes();
1445 if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
1446 self.activate_pane(pane, cx);
1447 } else {
1448 self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx);
1449 }
1450 }
1451
1452 pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
1453 let next_pane = {
1454 let panes = self.center.panes();
1455 let ix = panes
1456 .iter()
1457 .position(|pane| **pane == self.active_pane)
1458 .unwrap();
1459 let next_ix = (ix + 1) % panes.len();
1460 panes[next_ix].clone()
1461 };
1462 self.activate_pane(next_pane, cx);
1463 }
1464
1465 pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
1466 let prev_pane = {
1467 let panes = self.center.panes();
1468 let ix = panes
1469 .iter()
1470 .position(|pane| **pane == self.active_pane)
1471 .unwrap();
1472 let prev_ix = if ix == 0 { panes.len() - 1 } else { ix - 1 };
1473 panes[prev_ix].clone()
1474 };
1475 self.activate_pane(prev_pane, cx);
1476 }
1477
1478 fn activate_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1479 if self.active_pane != pane {
1480 self.active_pane = pane.clone();
1481 self.status_bar.update(cx, |status_bar, cx| {
1482 status_bar.set_active_pane(&self.active_pane, cx);
1483 });
1484 self.active_item_path_changed(cx);
1485 cx.focus(&self.active_pane);
1486 cx.notify();
1487 }
1488
1489 self.update_followers(
1490 proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
1491 id: self.active_item(cx).map(|item| item.id() as u64),
1492 leader_id: self.leader_for_pane(&pane).map(|id| id.0),
1493 }),
1494 cx,
1495 );
1496 }
1497
1498 fn handle_pane_event(
1499 &mut self,
1500 pane_id: usize,
1501 event: &pane::Event,
1502 cx: &mut ViewContext<Self>,
1503 ) {
1504 if let Some(pane) = self.pane(pane_id) {
1505 match event {
1506 pane::Event::Split(direction) => {
1507 self.split_pane(pane, *direction, cx);
1508 }
1509 pane::Event::Remove => {
1510 self.remove_pane(pane, cx);
1511 }
1512 pane::Event::Activate => {
1513 self.activate_pane(pane, cx);
1514 }
1515 pane::Event::ActivateItem { local } => {
1516 if *local {
1517 self.unfollow(&pane, cx);
1518 }
1519 if pane == self.active_pane {
1520 self.active_item_path_changed(cx);
1521 }
1522 }
1523 pane::Event::ChangeItemTitle => {
1524 if pane == self.active_pane {
1525 self.active_item_path_changed(cx);
1526 }
1527 self.update_window_edited(cx);
1528 }
1529 pane::Event::RemoveItem => {
1530 self.update_window_edited(cx);
1531 }
1532 }
1533 } else {
1534 error!("pane {} not found", pane_id);
1535 }
1536 }
1537
1538 pub fn split_pane(
1539 &mut self,
1540 pane: ViewHandle<Pane>,
1541 direction: SplitDirection,
1542 cx: &mut ViewContext<Self>,
1543 ) -> ViewHandle<Pane> {
1544 let new_pane = self.add_pane(cx);
1545 self.activate_pane(new_pane.clone(), cx);
1546 if let Some(item) = pane.read(cx).active_item() {
1547 if let Some(clone) = item.clone_on_split(cx.as_mut()) {
1548 Pane::add_item(self, new_pane.clone(), clone, true, true, cx);
1549 }
1550 }
1551 self.center.split(&pane, &new_pane, direction).unwrap();
1552 cx.notify();
1553 new_pane
1554 }
1555
1556 fn remove_pane(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
1557 if self.center.remove(&pane).unwrap() {
1558 self.panes.retain(|p| p != &pane);
1559 self.activate_pane(self.panes.last().unwrap().clone(), cx);
1560 self.unfollow(&pane, cx);
1561 self.last_leaders_by_pane.remove(&pane.downgrade());
1562 cx.notify();
1563 } else {
1564 self.active_item_path_changed(cx);
1565 }
1566 }
1567
1568 pub fn panes(&self) -> &[ViewHandle<Pane>] {
1569 &self.panes
1570 }
1571
1572 fn pane(&self, pane_id: usize) -> Option<ViewHandle<Pane>> {
1573 self.panes.iter().find(|pane| pane.id() == pane_id).cloned()
1574 }
1575
1576 pub fn active_pane(&self) -> &ViewHandle<Pane> {
1577 &self.active_pane
1578 }
1579
1580 fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
1581 if let Some(remote_id) = remote_id {
1582 self.remote_entity_subscription =
1583 Some(self.client.add_view_for_remote_entity(remote_id, cx));
1584 } else {
1585 self.remote_entity_subscription.take();
1586 }
1587 }
1588
1589 fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
1590 self.leader_state.followers.remove(&peer_id);
1591 if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) {
1592 for state in states_by_pane.into_values() {
1593 for item in state.items_by_leader_view_id.into_values() {
1594 if let FollowerItem::Loaded(item) = item {
1595 item.set_leader_replica_id(None, cx);
1596 }
1597 }
1598 }
1599 }
1600 cx.notify();
1601 }
1602
1603 pub fn toggle_follow(
1604 &mut self,
1605 ToggleFollow(leader_id): &ToggleFollow,
1606 cx: &mut ViewContext<Self>,
1607 ) -> Option<Task<Result<()>>> {
1608 let leader_id = *leader_id;
1609 let pane = self.active_pane().clone();
1610
1611 if let Some(prev_leader_id) = self.unfollow(&pane, cx) {
1612 if leader_id == prev_leader_id {
1613 return None;
1614 }
1615 }
1616
1617 self.last_leaders_by_pane
1618 .insert(pane.downgrade(), leader_id);
1619 self.follower_states_by_leader
1620 .entry(leader_id)
1621 .or_default()
1622 .insert(pane.clone(), Default::default());
1623 cx.notify();
1624
1625 let project_id = self.project.read(cx).remote_id()?;
1626 let request = self.client.request(proto::Follow {
1627 project_id,
1628 leader_id: leader_id.0,
1629 });
1630 Some(cx.spawn_weak(|this, mut cx| async move {
1631 let response = request.await?;
1632 if let Some(this) = this.upgrade(&cx) {
1633 this.update(&mut cx, |this, _| {
1634 let state = this
1635 .follower_states_by_leader
1636 .get_mut(&leader_id)
1637 .and_then(|states_by_pane| states_by_pane.get_mut(&pane))
1638 .ok_or_else(|| anyhow!("following interrupted"))?;
1639 state.active_view_id = response.active_view_id;
1640 Ok::<_, anyhow::Error>(())
1641 })?;
1642 Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx)
1643 .await?;
1644 }
1645 Ok(())
1646 }))
1647 }
1648
1649 pub fn follow_next_collaborator(
1650 &mut self,
1651 _: &FollowNextCollaborator,
1652 cx: &mut ViewContext<Self>,
1653 ) -> Option<Task<Result<()>>> {
1654 let collaborators = self.project.read(cx).collaborators();
1655 let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
1656 let mut collaborators = collaborators.keys().copied();
1657 while let Some(peer_id) = collaborators.next() {
1658 if peer_id == leader_id {
1659 break;
1660 }
1661 }
1662 collaborators.next()
1663 } else if let Some(last_leader_id) =
1664 self.last_leaders_by_pane.get(&self.active_pane.downgrade())
1665 {
1666 if collaborators.contains_key(last_leader_id) {
1667 Some(*last_leader_id)
1668 } else {
1669 None
1670 }
1671 } else {
1672 None
1673 };
1674
1675 next_leader_id
1676 .or_else(|| collaborators.keys().copied().next())
1677 .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx))
1678 }
1679
1680 pub fn unfollow(
1681 &mut self,
1682 pane: &ViewHandle<Pane>,
1683 cx: &mut ViewContext<Self>,
1684 ) -> Option<PeerId> {
1685 for (leader_id, states_by_pane) in &mut self.follower_states_by_leader {
1686 let leader_id = *leader_id;
1687 if let Some(state) = states_by_pane.remove(&pane) {
1688 for (_, item) in state.items_by_leader_view_id {
1689 if let FollowerItem::Loaded(item) = item {
1690 item.set_leader_replica_id(None, cx);
1691 }
1692 }
1693
1694 if states_by_pane.is_empty() {
1695 self.follower_states_by_leader.remove(&leader_id);
1696 if let Some(project_id) = self.project.read(cx).remote_id() {
1697 self.client
1698 .send(proto::Unfollow {
1699 project_id,
1700 leader_id: leader_id.0,
1701 })
1702 .log_err();
1703 }
1704 }
1705
1706 cx.notify();
1707 return Some(leader_id);
1708 }
1709 }
1710 None
1711 }
1712
1713 fn render_connection_status(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
1714 let theme = &cx.global::<Settings>().theme;
1715 match &*self.client.status().borrow() {
1716 client::Status::ConnectionError
1717 | client::Status::ConnectionLost
1718 | client::Status::Reauthenticating
1719 | client::Status::Reconnecting { .. }
1720 | client::Status::ReconnectionError { .. } => Some(
1721 Container::new(
1722 Align::new(
1723 ConstrainedBox::new(
1724 Svg::new("icons/offline-14.svg")
1725 .with_color(theme.workspace.titlebar.offline_icon.color)
1726 .boxed(),
1727 )
1728 .with_width(theme.workspace.titlebar.offline_icon.width)
1729 .boxed(),
1730 )
1731 .boxed(),
1732 )
1733 .with_style(theme.workspace.titlebar.offline_icon.container)
1734 .boxed(),
1735 ),
1736 client::Status::UpgradeRequired => Some(
1737 Label::new(
1738 "Please update Zed to collaborate".to_string(),
1739 theme.workspace.titlebar.outdated_warning.text.clone(),
1740 )
1741 .contained()
1742 .with_style(theme.workspace.titlebar.outdated_warning.container)
1743 .aligned()
1744 .boxed(),
1745 ),
1746 _ => None,
1747 }
1748 }
1749
1750 fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
1751 let project = &self.project.read(cx);
1752 let replica_id = project.replica_id();
1753 let mut worktree_root_names = String::new();
1754 for (i, name) in project.worktree_root_names(cx).enumerate() {
1755 if i > 0 {
1756 worktree_root_names.push_str(", ");
1757 }
1758 worktree_root_names.push_str(name);
1759 }
1760
1761 ConstrainedBox::new(
1762 Container::new(
1763 Stack::new()
1764 .with_child(
1765 Label::new(worktree_root_names, theme.workspace.titlebar.title.clone())
1766 .aligned()
1767 .left()
1768 .boxed(),
1769 )
1770 .with_child(
1771 Align::new(
1772 Flex::row()
1773 .with_children(self.render_collaborators(theme, cx))
1774 .with_children(self.render_current_user(
1775 self.user_store.read(cx).current_user().as_ref(),
1776 replica_id,
1777 theme,
1778 cx,
1779 ))
1780 .with_children(self.render_connection_status(cx))
1781 .boxed(),
1782 )
1783 .right()
1784 .boxed(),
1785 )
1786 .boxed(),
1787 )
1788 .with_style(theme.workspace.titlebar.container)
1789 .boxed(),
1790 )
1791 .with_height(theme.workspace.titlebar.height)
1792 .named("titlebar")
1793 }
1794
1795 fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
1796 let active_entry = self.active_project_path(cx);
1797 self.project
1798 .update(cx, |project, cx| project.set_active_path(active_entry, cx));
1799 self.update_window_title(cx);
1800 }
1801
1802 fn update_window_title(&mut self, cx: &mut ViewContext<Self>) {
1803 let mut title = String::new();
1804 let project = self.project().read(cx);
1805 if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
1806 let filename = path
1807 .path
1808 .file_name()
1809 .map(|s| s.to_string_lossy())
1810 .or_else(|| {
1811 Some(Cow::Borrowed(
1812 project
1813 .worktree_for_id(path.worktree_id, cx)?
1814 .read(cx)
1815 .root_name(),
1816 ))
1817 });
1818 if let Some(filename) = filename {
1819 title.push_str(filename.as_ref());
1820 title.push_str(" — ");
1821 }
1822 }
1823 for (i, name) in project.worktree_root_names(cx).enumerate() {
1824 if i > 0 {
1825 title.push_str(", ");
1826 }
1827 title.push_str(name);
1828 }
1829 if title.is_empty() {
1830 title = "empty project".to_string();
1831 }
1832 cx.set_window_title(&title);
1833 }
1834
1835 fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
1836 let is_edited = self
1837 .items(cx)
1838 .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
1839 if is_edited != self.window_edited {
1840 self.window_edited = is_edited;
1841 cx.set_window_edited(self.window_edited)
1842 }
1843 }
1844
1845 fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
1846 let mut collaborators = self
1847 .project
1848 .read(cx)
1849 .collaborators()
1850 .values()
1851 .cloned()
1852 .collect::<Vec<_>>();
1853 collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id);
1854 collaborators
1855 .into_iter()
1856 .filter_map(|collaborator| {
1857 Some(self.render_avatar(
1858 collaborator.user.avatar.clone()?,
1859 collaborator.replica_id,
1860 Some((collaborator.peer_id, &collaborator.user.github_login)),
1861 theme,
1862 cx,
1863 ))
1864 })
1865 .collect()
1866 }
1867
1868 fn render_current_user(
1869 &self,
1870 user: Option<&Arc<User>>,
1871 replica_id: ReplicaId,
1872 theme: &Theme,
1873 cx: &mut RenderContext<Self>,
1874 ) -> Option<ElementBox> {
1875 let status = *self.client.status().borrow();
1876 if let Some(avatar) = user.and_then(|user| user.avatar.clone()) {
1877 Some(self.render_avatar(avatar, replica_id, None, theme, cx))
1878 } else if matches!(status, client::Status::UpgradeRequired) {
1879 None
1880 } else {
1881 Some(
1882 MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| {
1883 let style = theme
1884 .workspace
1885 .titlebar
1886 .sign_in_prompt
1887 .style_for(state, false);
1888 Label::new("Sign in".to_string(), style.text.clone())
1889 .contained()
1890 .with_style(style.container)
1891 .boxed()
1892 })
1893 .on_click(|_, _, cx| cx.dispatch_action(Authenticate))
1894 .with_cursor_style(CursorStyle::PointingHand)
1895 .aligned()
1896 .boxed(),
1897 )
1898 }
1899 }
1900
1901 fn render_avatar(
1902 &self,
1903 avatar: Arc<ImageData>,
1904 replica_id: ReplicaId,
1905 peer: Option<(PeerId, &str)>,
1906 theme: &Theme,
1907 cx: &mut RenderContext<Self>,
1908 ) -> ElementBox {
1909 let replica_color = theme.editor.replica_selection_style(replica_id).cursor;
1910 let is_followed = peer.map_or(false, |(peer_id, _)| {
1911 self.follower_states_by_leader.contains_key(&peer_id)
1912 });
1913 let mut avatar_style = theme.workspace.titlebar.avatar;
1914 if is_followed {
1915 avatar_style.border = Border::all(1.0, replica_color);
1916 }
1917 let content = Stack::new()
1918 .with_child(
1919 Image::new(avatar)
1920 .with_style(avatar_style)
1921 .constrained()
1922 .with_width(theme.workspace.titlebar.avatar_width)
1923 .aligned()
1924 .boxed(),
1925 )
1926 .with_child(
1927 AvatarRibbon::new(replica_color)
1928 .constrained()
1929 .with_width(theme.workspace.titlebar.avatar_ribbon.width)
1930 .with_height(theme.workspace.titlebar.avatar_ribbon.height)
1931 .aligned()
1932 .bottom()
1933 .boxed(),
1934 )
1935 .constrained()
1936 .with_width(theme.workspace.titlebar.avatar_width)
1937 .contained()
1938 .with_margin_left(theme.workspace.titlebar.avatar_margin)
1939 .boxed();
1940
1941 if let Some((peer_id, peer_github_login)) = peer {
1942 MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
1943 .with_cursor_style(CursorStyle::PointingHand)
1944 .on_click(move |_, _, cx| cx.dispatch_action(ToggleFollow(peer_id)))
1945 .with_tooltip::<ToggleFollow, _>(
1946 peer_id.0 as usize,
1947 if is_followed {
1948 format!("Unfollow {}", peer_github_login)
1949 } else {
1950 format!("Follow {}", peer_github_login)
1951 },
1952 Some(Box::new(FollowNextCollaborator)),
1953 theme.tooltip.clone(),
1954 cx,
1955 )
1956 .boxed()
1957 } else {
1958 content
1959 }
1960 }
1961
1962 fn render_disconnected_overlay(&self, cx: &AppContext) -> Option<ElementBox> {
1963 if self.project.read(cx).is_read_only() {
1964 let theme = &cx.global::<Settings>().theme;
1965 Some(
1966 EventHandler::new(
1967 Label::new(
1968 "Your connection to the remote project has been lost.".to_string(),
1969 theme.workspace.disconnected_overlay.text.clone(),
1970 )
1971 .aligned()
1972 .contained()
1973 .with_style(theme.workspace.disconnected_overlay.container)
1974 .boxed(),
1975 )
1976 .capture_all::<Self>(0)
1977 .boxed(),
1978 )
1979 } else {
1980 None
1981 }
1982 }
1983
1984 fn render_notifications(&self, theme: &theme::Workspace) -> Option<ElementBox> {
1985 if self.notifications.is_empty() {
1986 None
1987 } else {
1988 Some(
1989 Flex::column()
1990 .with_children(self.notifications.iter().map(|(_, _, notification)| {
1991 ChildView::new(notification.as_ref())
1992 .contained()
1993 .with_style(theme.notification)
1994 .boxed()
1995 }))
1996 .constrained()
1997 .with_width(theme.notifications.width)
1998 .contained()
1999 .with_style(theme.notifications.container)
2000 .aligned()
2001 .bottom()
2002 .right()
2003 .boxed(),
2004 )
2005 }
2006 }
2007
2008 // RPC handlers
2009
2010 async fn handle_follow(
2011 this: ViewHandle<Self>,
2012 envelope: TypedEnvelope<proto::Follow>,
2013 _: Arc<Client>,
2014 mut cx: AsyncAppContext,
2015 ) -> Result<proto::FollowResponse> {
2016 this.update(&mut cx, |this, cx| {
2017 this.leader_state
2018 .followers
2019 .insert(envelope.original_sender_id()?);
2020
2021 let active_view_id = this
2022 .active_item(cx)
2023 .and_then(|i| i.to_followable_item_handle(cx))
2024 .map(|i| i.id() as u64);
2025 Ok(proto::FollowResponse {
2026 active_view_id,
2027 views: this
2028 .panes()
2029 .iter()
2030 .flat_map(|pane| {
2031 let leader_id = this.leader_for_pane(pane).map(|id| id.0);
2032 pane.read(cx).items().filter_map({
2033 let cx = &cx;
2034 move |item| {
2035 let id = item.id() as u64;
2036 let item = item.to_followable_item_handle(cx)?;
2037 let variant = item.to_state_proto(cx)?;
2038 Some(proto::View {
2039 id,
2040 leader_id,
2041 variant: Some(variant),
2042 })
2043 }
2044 })
2045 })
2046 .collect(),
2047 })
2048 })
2049 }
2050
2051 async fn handle_unfollow(
2052 this: ViewHandle<Self>,
2053 envelope: TypedEnvelope<proto::Unfollow>,
2054 _: Arc<Client>,
2055 mut cx: AsyncAppContext,
2056 ) -> Result<()> {
2057 this.update(&mut cx, |this, _| {
2058 this.leader_state
2059 .followers
2060 .remove(&envelope.original_sender_id()?);
2061 Ok(())
2062 })
2063 }
2064
2065 async fn handle_update_followers(
2066 this: ViewHandle<Self>,
2067 envelope: TypedEnvelope<proto::UpdateFollowers>,
2068 _: Arc<Client>,
2069 mut cx: AsyncAppContext,
2070 ) -> Result<()> {
2071 let leader_id = envelope.original_sender_id()?;
2072 match envelope
2073 .payload
2074 .variant
2075 .ok_or_else(|| anyhow!("invalid update"))?
2076 {
2077 proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
2078 this.update(&mut cx, |this, cx| {
2079 this.update_leader_state(leader_id, cx, |state, _| {
2080 state.active_view_id = update_active_view.id;
2081 });
2082 Ok::<_, anyhow::Error>(())
2083 })
2084 }
2085 proto::update_followers::Variant::UpdateView(update_view) => {
2086 this.update(&mut cx, |this, cx| {
2087 let variant = update_view
2088 .variant
2089 .ok_or_else(|| anyhow!("missing update view variant"))?;
2090 this.update_leader_state(leader_id, cx, |state, cx| {
2091 let variant = variant.clone();
2092 match state
2093 .items_by_leader_view_id
2094 .entry(update_view.id)
2095 .or_insert(FollowerItem::Loading(Vec::new()))
2096 {
2097 FollowerItem::Loaded(item) => {
2098 item.apply_update_proto(variant, cx).log_err();
2099 }
2100 FollowerItem::Loading(updates) => updates.push(variant),
2101 }
2102 });
2103 Ok(())
2104 })
2105 }
2106 proto::update_followers::Variant::CreateView(view) => {
2107 let panes = this.read_with(&cx, |this, _| {
2108 this.follower_states_by_leader
2109 .get(&leader_id)
2110 .into_iter()
2111 .flat_map(|states_by_pane| states_by_pane.keys())
2112 .cloned()
2113 .collect()
2114 });
2115 Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx)
2116 .await?;
2117 Ok(())
2118 }
2119 }
2120 .log_err();
2121
2122 Ok(())
2123 }
2124
2125 async fn add_views_from_leader(
2126 this: ViewHandle<Self>,
2127 leader_id: PeerId,
2128 panes: Vec<ViewHandle<Pane>>,
2129 views: Vec<proto::View>,
2130 cx: &mut AsyncAppContext,
2131 ) -> Result<()> {
2132 let project = this.read_with(cx, |this, _| this.project.clone());
2133 let replica_id = project
2134 .read_with(cx, |project, _| {
2135 project
2136 .collaborators()
2137 .get(&leader_id)
2138 .map(|c| c.replica_id)
2139 })
2140 .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?;
2141
2142 let item_builders = cx.update(|cx| {
2143 cx.default_global::<FollowableItemBuilders>()
2144 .values()
2145 .map(|b| b.0)
2146 .collect::<Vec<_>>()
2147 .clone()
2148 });
2149
2150 let mut item_tasks_by_pane = HashMap::default();
2151 for pane in panes {
2152 let mut item_tasks = Vec::new();
2153 let mut leader_view_ids = Vec::new();
2154 for view in &views {
2155 let mut variant = view.variant.clone();
2156 if variant.is_none() {
2157 Err(anyhow!("missing variant"))?;
2158 }
2159 for build_item in &item_builders {
2160 let task =
2161 cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx));
2162 if let Some(task) = task {
2163 item_tasks.push(task);
2164 leader_view_ids.push(view.id);
2165 break;
2166 } else {
2167 assert!(variant.is_some());
2168 }
2169 }
2170 }
2171
2172 item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids));
2173 }
2174
2175 for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane {
2176 let items = futures::future::try_join_all(item_tasks).await?;
2177 this.update(cx, |this, cx| {
2178 let state = this
2179 .follower_states_by_leader
2180 .get_mut(&leader_id)?
2181 .get_mut(&pane)?;
2182
2183 for (id, item) in leader_view_ids.into_iter().zip(items) {
2184 item.set_leader_replica_id(Some(replica_id), cx);
2185 match state.items_by_leader_view_id.entry(id) {
2186 hash_map::Entry::Occupied(e) => {
2187 let e = e.into_mut();
2188 if let FollowerItem::Loading(updates) = e {
2189 for update in updates.drain(..) {
2190 item.apply_update_proto(update, cx)
2191 .context("failed to apply view update")
2192 .log_err();
2193 }
2194 }
2195 *e = FollowerItem::Loaded(item);
2196 }
2197 hash_map::Entry::Vacant(e) => {
2198 e.insert(FollowerItem::Loaded(item));
2199 }
2200 }
2201 }
2202
2203 Some(())
2204 });
2205 }
2206 this.update(cx, |this, cx| this.leader_updated(leader_id, cx));
2207
2208 Ok(())
2209 }
2210
2211 fn update_followers(
2212 &self,
2213 update: proto::update_followers::Variant,
2214 cx: &AppContext,
2215 ) -> Option<()> {
2216 let project_id = self.project.read(cx).remote_id()?;
2217 if !self.leader_state.followers.is_empty() {
2218 self.client
2219 .send(proto::UpdateFollowers {
2220 project_id,
2221 follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(),
2222 variant: Some(update),
2223 })
2224 .log_err();
2225 }
2226 None
2227 }
2228
2229 pub fn leader_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<PeerId> {
2230 self.follower_states_by_leader
2231 .iter()
2232 .find_map(|(leader_id, state)| {
2233 if state.contains_key(pane) {
2234 Some(*leader_id)
2235 } else {
2236 None
2237 }
2238 })
2239 }
2240
2241 fn update_leader_state(
2242 &mut self,
2243 leader_id: PeerId,
2244 cx: &mut ViewContext<Self>,
2245 mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext<Self>),
2246 ) {
2247 for (_, state) in self
2248 .follower_states_by_leader
2249 .get_mut(&leader_id)
2250 .into_iter()
2251 .flatten()
2252 {
2253 update_fn(state, cx);
2254 }
2255 self.leader_updated(leader_id, cx);
2256 }
2257
2258 fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
2259 let mut items_to_add = Vec::new();
2260 for (pane, state) in self.follower_states_by_leader.get(&leader_id)? {
2261 if let Some(active_item) = state
2262 .active_view_id
2263 .and_then(|id| state.items_by_leader_view_id.get(&id))
2264 {
2265 if let FollowerItem::Loaded(item) = active_item {
2266 items_to_add.push((pane.clone(), item.boxed_clone()));
2267 }
2268 }
2269 }
2270
2271 for (pane, item) in items_to_add {
2272 Pane::add_item(self, pane.clone(), item.boxed_clone(), false, false, cx);
2273 if pane == self.active_pane {
2274 pane.update(cx, |pane, cx| pane.focus_active_item(cx));
2275 }
2276 cx.notify();
2277 }
2278 None
2279 }
2280}
2281
2282impl Entity for Workspace {
2283 type Event = Event;
2284}
2285
2286impl View for Workspace {
2287 fn ui_name() -> &'static str {
2288 "Workspace"
2289 }
2290
2291 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
2292 let theme = cx.global::<Settings>().theme.clone();
2293 Stack::new()
2294 .with_child(
2295 Flex::column()
2296 .with_child(self.render_titlebar(&theme, cx))
2297 .with_child(
2298 Stack::new()
2299 .with_child({
2300 Flex::row()
2301 .with_children(
2302 if self.left_sidebar.read(cx).active_item().is_some() {
2303 Some(
2304 ChildView::new(&self.left_sidebar)
2305 .flex(0.8, false)
2306 .boxed(),
2307 )
2308 } else {
2309 None
2310 },
2311 )
2312 .with_child(
2313 FlexItem::new(self.center.render(
2314 &theme,
2315 &self.follower_states_by_leader,
2316 self.project.read(cx).collaborators(),
2317 ))
2318 .flex(1., true)
2319 .boxed(),
2320 )
2321 .with_children(
2322 if self.right_sidebar.read(cx).active_item().is_some() {
2323 Some(
2324 ChildView::new(&self.right_sidebar)
2325 .flex(0.8, false)
2326 .boxed(),
2327 )
2328 } else {
2329 None
2330 },
2331 )
2332 .boxed()
2333 })
2334 .with_children(self.modal.as_ref().map(|m| {
2335 ChildView::new(m)
2336 .contained()
2337 .with_style(theme.workspace.modal)
2338 .aligned()
2339 .top()
2340 .boxed()
2341 }))
2342 .with_children(self.render_notifications(&theme.workspace))
2343 .flex(1.0, true)
2344 .boxed(),
2345 )
2346 .with_child(ChildView::new(&self.status_bar).boxed())
2347 .contained()
2348 .with_background_color(theme.workspace.background)
2349 .boxed(),
2350 )
2351 .with_children(self.render_disconnected_overlay(cx))
2352 .named("workspace")
2353 }
2354
2355 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
2356 cx.focus(&self.active_pane);
2357 }
2358}
2359
2360pub trait WorkspaceHandle {
2361 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
2362}
2363
2364impl WorkspaceHandle for ViewHandle<Workspace> {
2365 fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
2366 self.read(cx)
2367 .worktrees(cx)
2368 .flat_map(|worktree| {
2369 let worktree_id = worktree.read(cx).id();
2370 worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
2371 worktree_id,
2372 path: f.path.clone(),
2373 })
2374 })
2375 .collect::<Vec<_>>()
2376 }
2377}
2378
2379pub struct AvatarRibbon {
2380 color: Color,
2381}
2382
2383impl AvatarRibbon {
2384 pub fn new(color: Color) -> AvatarRibbon {
2385 AvatarRibbon { color }
2386 }
2387}
2388
2389impl Element for AvatarRibbon {
2390 type LayoutState = ();
2391
2392 type PaintState = ();
2393
2394 fn layout(
2395 &mut self,
2396 constraint: gpui::SizeConstraint,
2397 _: &mut gpui::LayoutContext,
2398 ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
2399 (constraint.max, ())
2400 }
2401
2402 fn paint(
2403 &mut self,
2404 bounds: gpui::geometry::rect::RectF,
2405 _: gpui::geometry::rect::RectF,
2406 _: &mut Self::LayoutState,
2407 cx: &mut gpui::PaintContext,
2408 ) -> Self::PaintState {
2409 let mut path = PathBuilder::new();
2410 path.reset(bounds.lower_left());
2411 path.curve_to(
2412 bounds.origin() + vec2f(bounds.height(), 0.),
2413 bounds.origin(),
2414 );
2415 path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
2416 path.curve_to(bounds.lower_right(), bounds.upper_right());
2417 path.line_to(bounds.lower_left());
2418 cx.scene.push_path(path.build(self.color, None));
2419 }
2420
2421 fn dispatch_event(
2422 &mut self,
2423 _: &gpui::Event,
2424 _: RectF,
2425 _: RectF,
2426 _: &mut Self::LayoutState,
2427 _: &mut Self::PaintState,
2428 _: &mut gpui::EventContext,
2429 ) -> bool {
2430 false
2431 }
2432
2433 fn debug(
2434 &self,
2435 bounds: gpui::geometry::rect::RectF,
2436 _: &Self::LayoutState,
2437 _: &Self::PaintState,
2438 _: &gpui::DebugContext,
2439 ) -> gpui::json::Value {
2440 json::json!({
2441 "type": "AvatarRibbon",
2442 "bounds": bounds.to_json(),
2443 "color": self.color.to_json(),
2444 })
2445 }
2446}
2447
2448impl std::fmt::Debug for OpenPaths {
2449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2450 f.debug_struct("OpenPaths")
2451 .field("paths", &self.paths)
2452 .finish()
2453 }
2454}
2455
2456fn open(_: &Open, cx: &mut MutableAppContext) {
2457 let mut paths = cx.prompt_for_paths(PathPromptOptions {
2458 files: true,
2459 directories: true,
2460 multiple: true,
2461 });
2462 cx.spawn(|mut cx| async move {
2463 if let Some(paths) = paths.recv().await.flatten() {
2464 cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
2465 }
2466 })
2467 .detach();
2468}
2469
2470pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
2471
2472pub fn activate_workspace_for_project(
2473 cx: &mut MutableAppContext,
2474 predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
2475) -> Option<ViewHandle<Workspace>> {
2476 for window_id in cx.window_ids().collect::<Vec<_>>() {
2477 if let Some(workspace_handle) = cx.root_view::<Workspace>(window_id) {
2478 let project = workspace_handle.read(cx).project.clone();
2479 if project.update(cx, &predicate) {
2480 cx.activate_window(window_id);
2481 return Some(workspace_handle);
2482 }
2483 }
2484 }
2485 None
2486}
2487
2488pub fn open_paths(
2489 abs_paths: &[PathBuf],
2490 app_state: &Arc<AppState>,
2491 cx: &mut MutableAppContext,
2492) -> Task<(
2493 ViewHandle<Workspace>,
2494 Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
2495)> {
2496 log::info!("open paths {:?}", abs_paths);
2497
2498 // Open paths in existing workspace if possible
2499 let existing =
2500 activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
2501
2502 let app_state = app_state.clone();
2503 let abs_paths = abs_paths.to_vec();
2504 cx.spawn(|mut cx| async move {
2505 let mut new_project = None;
2506 let workspace = if let Some(existing) = existing {
2507 existing
2508 } else {
2509 let contains_directory =
2510 futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path)))
2511 .await
2512 .contains(&false);
2513
2514 cx.add_window((app_state.build_window_options)(), |cx| {
2515 let project = Project::local(
2516 false,
2517 app_state.client.clone(),
2518 app_state.user_store.clone(),
2519 app_state.project_store.clone(),
2520 app_state.languages.clone(),
2521 app_state.fs.clone(),
2522 cx,
2523 );
2524 new_project = Some(project.clone());
2525 let mut workspace = Workspace::new(project, cx);
2526 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
2527 if contains_directory {
2528 workspace.toggle_sidebar(Side::Left, cx);
2529 }
2530 workspace
2531 })
2532 .1
2533 };
2534
2535 let items = workspace
2536 .update(&mut cx, |workspace, cx| {
2537 workspace.open_paths(abs_paths, true, cx)
2538 })
2539 .await;
2540
2541 if let Some(project) = new_project {
2542 project
2543 .update(&mut cx, |project, cx| project.restore_state(cx))
2544 .await
2545 .log_err();
2546 }
2547
2548 (workspace, items)
2549 })
2550}
2551
2552pub fn join_project(
2553 contact: Arc<Contact>,
2554 project_index: usize,
2555 app_state: &Arc<AppState>,
2556 cx: &mut MutableAppContext,
2557) {
2558 let project_id = contact.projects[project_index].id;
2559
2560 for window_id in cx.window_ids().collect::<Vec<_>>() {
2561 if let Some(workspace) = cx.root_view::<Workspace>(window_id) {
2562 if workspace.read(cx).project().read(cx).remote_id() == Some(project_id) {
2563 cx.activate_window(window_id);
2564 return;
2565 }
2566 }
2567 }
2568
2569 cx.add_window((app_state.build_window_options)(), |cx| {
2570 WaitingRoom::new(contact, project_index, app_state.clone(), cx)
2571 });
2572}
2573
2574fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
2575 let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
2576 let mut workspace = Workspace::new(
2577 Project::local(
2578 false,
2579 app_state.client.clone(),
2580 app_state.user_store.clone(),
2581 app_state.project_store.clone(),
2582 app_state.languages.clone(),
2583 app_state.fs.clone(),
2584 cx,
2585 ),
2586 cx,
2587 );
2588 (app_state.initialize_workspace)(&mut workspace, app_state, cx);
2589 workspace
2590 });
2591 cx.dispatch_action(window_id, vec![workspace.id()], &NewFile);
2592}
2593
2594#[cfg(test)]
2595mod tests {
2596 use super::*;
2597 use gpui::{ModelHandle, TestAppContext, ViewContext};
2598 use project::{FakeFs, Project, ProjectEntryId};
2599 use serde_json::json;
2600
2601 #[gpui::test]
2602 async fn test_tracking_active_path(cx: &mut TestAppContext) {
2603 cx.foreground().forbid_parking();
2604 Settings::test_async(cx);
2605 let fs = FakeFs::new(cx.background());
2606 fs.insert_tree(
2607 "/root1",
2608 json!({
2609 "one.txt": "",
2610 "two.txt": "",
2611 }),
2612 )
2613 .await;
2614 fs.insert_tree(
2615 "/root2",
2616 json!({
2617 "three.txt": "",
2618 }),
2619 )
2620 .await;
2621
2622 let project = Project::test(fs, ["root1".as_ref()], cx).await;
2623 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
2624 let worktree_id = project.read_with(cx, |project, cx| {
2625 project.worktrees(cx).next().unwrap().read(cx).id()
2626 });
2627
2628 let item1 = cx.add_view(window_id, |_| {
2629 let mut item = TestItem::new();
2630 item.project_path = Some((worktree_id, "one.txt").into());
2631 item
2632 });
2633 let item2 = cx.add_view(window_id, |_| {
2634 let mut item = TestItem::new();
2635 item.project_path = Some((worktree_id, "two.txt").into());
2636 item
2637 });
2638
2639 // Add an item to an empty pane
2640 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
2641 project.read_with(cx, |project, cx| {
2642 assert_eq!(
2643 project.active_entry(),
2644 project.entry_for_path(&(worktree_id, "one.txt").into(), cx)
2645 );
2646 });
2647 assert_eq!(
2648 cx.current_window_title(window_id).as_deref(),
2649 Some("one.txt — root1")
2650 );
2651
2652 // Add a second item to a non-empty pane
2653 workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
2654 assert_eq!(
2655 cx.current_window_title(window_id).as_deref(),
2656 Some("two.txt — root1")
2657 );
2658 project.read_with(cx, |project, cx| {
2659 assert_eq!(
2660 project.active_entry(),
2661 project.entry_for_path(&(worktree_id, "two.txt").into(), cx)
2662 );
2663 });
2664
2665 // Close the active item
2666 workspace
2667 .update(cx, |workspace, cx| {
2668 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
2669 })
2670 .await
2671 .unwrap();
2672 assert_eq!(
2673 cx.current_window_title(window_id).as_deref(),
2674 Some("one.txt — root1")
2675 );
2676 project.read_with(cx, |project, cx| {
2677 assert_eq!(
2678 project.active_entry(),
2679 project.entry_for_path(&(worktree_id, "one.txt").into(), cx)
2680 );
2681 });
2682
2683 // Add a project folder
2684 project
2685 .update(cx, |project, cx| {
2686 project.find_or_create_local_worktree("/root2", true, cx)
2687 })
2688 .await
2689 .unwrap();
2690 assert_eq!(
2691 cx.current_window_title(window_id).as_deref(),
2692 Some("one.txt — root1, root2")
2693 );
2694
2695 // Remove a project folder
2696 project.update(cx, |project, cx| {
2697 project.remove_worktree(worktree_id, cx);
2698 });
2699 assert_eq!(
2700 cx.current_window_title(window_id).as_deref(),
2701 Some("one.txt — root2")
2702 );
2703 }
2704
2705 #[gpui::test]
2706 async fn test_close_window(cx: &mut TestAppContext) {
2707 cx.foreground().forbid_parking();
2708 Settings::test_async(cx);
2709 let fs = FakeFs::new(cx.background());
2710 fs.insert_tree("/root", json!({ "one": "" })).await;
2711
2712 let project = Project::test(fs, ["root".as_ref()], cx).await;
2713 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
2714
2715 // When there are no dirty items, there's nothing to do.
2716 let item1 = cx.add_view(window_id, |_| TestItem::new());
2717 workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
2718 let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
2719 assert_eq!(task.await.unwrap(), true);
2720
2721 // When there are dirty untitled items, prompt to save each one. If the user
2722 // cancels any prompt, then abort.
2723 let item2 = cx.add_view(window_id, |_| {
2724 let mut item = TestItem::new();
2725 item.is_dirty = true;
2726 item
2727 });
2728 let item3 = cx.add_view(window_id, |_| {
2729 let mut item = TestItem::new();
2730 item.is_dirty = true;
2731 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
2732 item
2733 });
2734 workspace.update(cx, |w, cx| {
2735 w.add_item(Box::new(item2.clone()), cx);
2736 w.add_item(Box::new(item3.clone()), cx);
2737 });
2738 let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
2739 cx.foreground().run_until_parked();
2740 cx.simulate_prompt_answer(window_id, 2 /* cancel */);
2741 cx.foreground().run_until_parked();
2742 assert!(!cx.has_pending_prompt(window_id));
2743 assert_eq!(task.await.unwrap(), false);
2744 }
2745
2746 #[gpui::test]
2747 async fn test_close_pane_items(cx: &mut TestAppContext) {
2748 cx.foreground().forbid_parking();
2749 Settings::test_async(cx);
2750 let fs = FakeFs::new(cx.background());
2751
2752 let project = Project::test(fs, None, cx).await;
2753 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
2754
2755 let item1 = cx.add_view(window_id, |_| {
2756 let mut item = TestItem::new();
2757 item.is_dirty = true;
2758 item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
2759 item
2760 });
2761 let item2 = cx.add_view(window_id, |_| {
2762 let mut item = TestItem::new();
2763 item.is_dirty = true;
2764 item.has_conflict = true;
2765 item.project_entry_ids = vec![ProjectEntryId::from_proto(2)];
2766 item
2767 });
2768 let item3 = cx.add_view(window_id, |_| {
2769 let mut item = TestItem::new();
2770 item.is_dirty = true;
2771 item.has_conflict = true;
2772 item.project_entry_ids = vec![ProjectEntryId::from_proto(3)];
2773 item
2774 });
2775 let item4 = cx.add_view(window_id, |_| {
2776 let mut item = TestItem::new();
2777 item.is_dirty = true;
2778 item
2779 });
2780 let pane = workspace.update(cx, |workspace, cx| {
2781 workspace.add_item(Box::new(item1.clone()), cx);
2782 workspace.add_item(Box::new(item2.clone()), cx);
2783 workspace.add_item(Box::new(item3.clone()), cx);
2784 workspace.add_item(Box::new(item4.clone()), cx);
2785 workspace.active_pane().clone()
2786 });
2787
2788 let close_items = workspace.update(cx, |workspace, cx| {
2789 pane.update(cx, |pane, cx| {
2790 pane.activate_item(1, true, true, cx);
2791 assert_eq!(pane.active_item().unwrap().id(), item2.id());
2792 });
2793
2794 let item1_id = item1.id();
2795 let item3_id = item3.id();
2796 let item4_id = item4.id();
2797 Pane::close_items(workspace, pane.clone(), cx, move |id| {
2798 [item1_id, item3_id, item4_id].contains(&id)
2799 })
2800 });
2801
2802 cx.foreground().run_until_parked();
2803 pane.read_with(cx, |pane, _| {
2804 assert_eq!(pane.items().count(), 4);
2805 assert_eq!(pane.active_item().unwrap().id(), item1.id());
2806 });
2807
2808 cx.simulate_prompt_answer(window_id, 0);
2809 cx.foreground().run_until_parked();
2810 pane.read_with(cx, |pane, cx| {
2811 assert_eq!(item1.read(cx).save_count, 1);
2812 assert_eq!(item1.read(cx).save_as_count, 0);
2813 assert_eq!(item1.read(cx).reload_count, 0);
2814 assert_eq!(pane.items().count(), 3);
2815 assert_eq!(pane.active_item().unwrap().id(), item3.id());
2816 });
2817
2818 cx.simulate_prompt_answer(window_id, 1);
2819 cx.foreground().run_until_parked();
2820 pane.read_with(cx, |pane, cx| {
2821 assert_eq!(item3.read(cx).save_count, 0);
2822 assert_eq!(item3.read(cx).save_as_count, 0);
2823 assert_eq!(item3.read(cx).reload_count, 1);
2824 assert_eq!(pane.items().count(), 2);
2825 assert_eq!(pane.active_item().unwrap().id(), item4.id());
2826 });
2827
2828 cx.simulate_prompt_answer(window_id, 0);
2829 cx.foreground().run_until_parked();
2830 cx.simulate_new_path_selection(|_| Some(Default::default()));
2831 close_items.await.unwrap();
2832 pane.read_with(cx, |pane, cx| {
2833 assert_eq!(item4.read(cx).save_count, 0);
2834 assert_eq!(item4.read(cx).save_as_count, 1);
2835 assert_eq!(item4.read(cx).reload_count, 0);
2836 assert_eq!(pane.items().count(), 1);
2837 assert_eq!(pane.active_item().unwrap().id(), item2.id());
2838 });
2839 }
2840
2841 #[gpui::test]
2842 async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
2843 cx.foreground().forbid_parking();
2844 Settings::test_async(cx);
2845 let fs = FakeFs::new(cx.background());
2846
2847 let project = Project::test(fs, [], cx).await;
2848 let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
2849
2850 // Create several workspace items with single project entries, and two
2851 // workspace items with multiple project entries.
2852 let single_entry_items = (0..=4)
2853 .map(|project_entry_id| {
2854 let mut item = TestItem::new();
2855 item.is_dirty = true;
2856 item.project_entry_ids = vec![ProjectEntryId::from_proto(project_entry_id)];
2857 item.is_singleton = true;
2858 item
2859 })
2860 .collect::<Vec<_>>();
2861 let item_2_3 = {
2862 let mut item = TestItem::new();
2863 item.is_dirty = true;
2864 item.is_singleton = false;
2865 item.project_entry_ids =
2866 vec![ProjectEntryId::from_proto(2), ProjectEntryId::from_proto(3)];
2867 item
2868 };
2869 let item_3_4 = {
2870 let mut item = TestItem::new();
2871 item.is_dirty = true;
2872 item.is_singleton = false;
2873 item.project_entry_ids =
2874 vec![ProjectEntryId::from_proto(3), ProjectEntryId::from_proto(4)];
2875 item
2876 };
2877
2878 // Create two panes that contain the following project entries:
2879 // left pane:
2880 // multi-entry items: (2, 3)
2881 // single-entry items: 0, 1, 2, 3, 4
2882 // right pane:
2883 // single-entry items: 1
2884 // multi-entry items: (3, 4)
2885 let left_pane = workspace.update(cx, |workspace, cx| {
2886 let left_pane = workspace.active_pane().clone();
2887 let right_pane = workspace.split_pane(left_pane.clone(), SplitDirection::Right, cx);
2888
2889 workspace.activate_pane(left_pane.clone(), cx);
2890 workspace.add_item(Box::new(cx.add_view(|_| item_2_3.clone())), cx);
2891 for item in &single_entry_items {
2892 workspace.add_item(Box::new(cx.add_view(|_| item.clone())), cx);
2893 }
2894
2895 workspace.activate_pane(right_pane.clone(), cx);
2896 workspace.add_item(Box::new(cx.add_view(|_| single_entry_items[1].clone())), cx);
2897 workspace.add_item(Box::new(cx.add_view(|_| item_3_4.clone())), cx);
2898
2899 left_pane
2900 });
2901
2902 // When closing all of the items in the left pane, we should be prompted twice:
2903 // once for project entry 0, and once for project entry 2. After those two
2904 // prompts, the task should complete.
2905 let close = workspace.update(cx, |workspace, cx| {
2906 workspace.activate_pane(left_pane.clone(), cx);
2907 Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
2908 });
2909
2910 cx.foreground().run_until_parked();
2911 left_pane.read_with(cx, |pane, cx| {
2912 assert_eq!(
2913 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
2914 &[ProjectEntryId::from_proto(0)]
2915 );
2916 });
2917 cx.simulate_prompt_answer(window_id, 0);
2918
2919 cx.foreground().run_until_parked();
2920 left_pane.read_with(cx, |pane, cx| {
2921 assert_eq!(
2922 pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
2923 &[ProjectEntryId::from_proto(2)]
2924 );
2925 });
2926 cx.simulate_prompt_answer(window_id, 0);
2927
2928 cx.foreground().run_until_parked();
2929 close.await.unwrap();
2930 left_pane.read_with(cx, |pane, _| {
2931 assert_eq!(pane.items().count(), 0);
2932 });
2933 }
2934
2935 #[derive(Clone)]
2936 struct TestItem {
2937 save_count: usize,
2938 save_as_count: usize,
2939 reload_count: usize,
2940 is_dirty: bool,
2941 has_conflict: bool,
2942 project_entry_ids: Vec<ProjectEntryId>,
2943 project_path: Option<ProjectPath>,
2944 is_singleton: bool,
2945 }
2946
2947 impl TestItem {
2948 fn new() -> Self {
2949 Self {
2950 save_count: 0,
2951 save_as_count: 0,
2952 reload_count: 0,
2953 is_dirty: false,
2954 has_conflict: false,
2955 project_entry_ids: Vec::new(),
2956 project_path: None,
2957 is_singleton: true,
2958 }
2959 }
2960 }
2961
2962 impl Entity for TestItem {
2963 type Event = ();
2964 }
2965
2966 impl View for TestItem {
2967 fn ui_name() -> &'static str {
2968 "TestItem"
2969 }
2970
2971 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
2972 Empty::new().boxed()
2973 }
2974 }
2975
2976 impl Item for TestItem {
2977 fn tab_content(&self, _: &theme::Tab, _: &AppContext) -> ElementBox {
2978 Empty::new().boxed()
2979 }
2980
2981 fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
2982 self.project_path.clone()
2983 }
2984
2985 fn project_entry_ids(&self, _: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
2986 self.project_entry_ids.iter().copied().collect()
2987 }
2988
2989 fn is_singleton(&self, _: &AppContext) -> bool {
2990 self.is_singleton
2991 }
2992
2993 fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
2994
2995 fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
2996 where
2997 Self: Sized,
2998 {
2999 Some(self.clone())
3000 }
3001
3002 fn is_dirty(&self, _: &AppContext) -> bool {
3003 self.is_dirty
3004 }
3005
3006 fn has_conflict(&self, _: &AppContext) -> bool {
3007 self.has_conflict
3008 }
3009
3010 fn can_save(&self, _: &AppContext) -> bool {
3011 self.project_entry_ids.len() > 0
3012 }
3013
3014 fn save(
3015 &mut self,
3016 _: ModelHandle<Project>,
3017 _: &mut ViewContext<Self>,
3018 ) -> Task<anyhow::Result<()>> {
3019 self.save_count += 1;
3020 Task::ready(Ok(()))
3021 }
3022
3023 fn save_as(
3024 &mut self,
3025 _: ModelHandle<Project>,
3026 _: std::path::PathBuf,
3027 _: &mut ViewContext<Self>,
3028 ) -> Task<anyhow::Result<()>> {
3029 self.save_as_count += 1;
3030 Task::ready(Ok(()))
3031 }
3032
3033 fn reload(
3034 &mut self,
3035 _: ModelHandle<Project>,
3036 _: &mut ViewContext<Self>,
3037 ) -> Task<anyhow::Result<()>> {
3038 self.reload_count += 1;
3039 Task::ready(Ok(()))
3040 }
3041
3042 fn should_update_tab_on_event(_: &Self::Event) -> bool {
3043 true
3044 }
3045 }
3046}