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