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