/// NOTE: Focus only 'takes' after an update has flushed_effects. /// /// This may cause issues when you're trying to write tests that use workspace focus to add items at /// specific locations. pub mod dock; pub mod pane; pub mod pane_group; pub mod searchable; pub mod shared_screen; pub mod sidebar; mod status_bar; mod toolbar; use anyhow::{anyhow, Context, Result}; use call::ActiveCall; use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use collections::{hash_map, HashMap, HashSet}; use dock::{DefaultItemFactory, Dock, ToggleDockButton}; use drag_and_drop::DragAndDrop; use fs::{self, Fs}; use futures::{channel::oneshot, FutureExt, StreamExt}; use gpui::{ actions, elements::*, impl_actions, impl_internal_actions, platform::{CursorStyle, WindowOptions}, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::{error, warn}; pub use pane::*; pub use pane_group::*; use postage::prelude::Stream; use project::{Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId}; use searchable::SearchableItemHandle; use serde::Deserialize; use settings::{Autosave, DockAnchor, Settings}; use shared_screen::SharedScreen; use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem}; use smallvec::SmallVec; use status_bar::StatusBar; pub use status_bar::StatusItemView; use std::{ any::{Any, TypeId}, borrow::Cow, cell::RefCell, fmt, future::Future, path::{Path, PathBuf}, rc::Rc, sync::{ atomic::{AtomicBool, Ordering::SeqCst}, Arc, }, time::Duration, }; use theme::{Theme, ThemeRegistry}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use util::ResultExt; type ProjectItemBuilders = HashMap< TypeId, fn(ModelHandle, AnyModelHandle, &mut ViewContext) -> Box, >; type FollowableItemBuilder = fn( ViewHandle, ModelHandle, &mut Option, &mut MutableAppContext, ) -> Option>>>; type FollowableItemBuilders = HashMap< TypeId, ( FollowableItemBuilder, fn(AnyViewHandle) -> Box, ), >; #[derive(Clone, PartialEq)] pub struct RemoveWorktreeFromProject(pub WorktreeId); actions!( workspace, [ Open, NewFile, NewWindow, CloseWindow, AddFolderToProject, Unfollow, Save, SaveAs, SaveAll, ActivatePreviousPane, ActivateNextPane, FollowNextCollaborator, ToggleLeftSidebar, ToggleRightSidebar, NewTerminal, NewSearch, ] ); #[derive(Clone, PartialEq)] pub struct OpenPaths { pub paths: Vec, } #[derive(Clone, Deserialize, PartialEq)] pub struct ActivatePane(pub usize); #[derive(Clone, PartialEq)] pub struct ToggleFollow(pub PeerId); #[derive(Clone, PartialEq)] pub struct JoinProject { pub project_id: u64, pub follow_user_id: u64, } #[derive(Clone, PartialEq)] pub struct OpenSharedScreen { pub peer_id: PeerId, } #[derive(Clone, PartialEq)] pub struct SplitWithItem { pane_to_split: WeakViewHandle, split_direction: SplitDirection, from: WeakViewHandle, item_id_to_move: usize, } #[derive(Clone, PartialEq)] pub struct SplitWithProjectEntry { pane_to_split: WeakViewHandle, split_direction: SplitDirection, project_entry: ProjectEntryId, } #[derive(Clone, PartialEq)] pub struct OpenProjectEntryInPane { pane: WeakViewHandle, project_entry: ProjectEntryId, } impl_internal_actions!( workspace, [ OpenPaths, ToggleFollow, JoinProject, OpenSharedScreen, RemoveWorktreeFromProject, SplitWithItem, SplitWithProjectEntry, OpenProjectEntryInPane, ] ); impl_actions!(workspace, [ActivatePane]); pub fn init(app_state: Arc, cx: &mut MutableAppContext) { pane::init(cx); dock::init(cx); cx.add_global_action(open); cx.add_global_action({ let app_state = Arc::downgrade(&app_state); move |action: &OpenPaths, cx: &mut MutableAppContext| { if let Some(app_state) = app_state.upgrade() { open_paths(&action.paths, &app_state, cx).detach(); } } }); cx.add_global_action({ let app_state = Arc::downgrade(&app_state); move |_: &NewFile, cx: &mut MutableAppContext| { if let Some(app_state) = app_state.upgrade() { open_new(&app_state, cx) } } }); cx.add_global_action({ let app_state = Arc::downgrade(&app_state); move |_: &NewWindow, cx: &mut MutableAppContext| { if let Some(app_state) = app_state.upgrade() { open_new(&app_state, cx) } } }); cx.add_async_action(Workspace::toggle_follow); cx.add_async_action(Workspace::follow_next_collaborator); cx.add_async_action(Workspace::close); cx.add_async_action(Workspace::save_all); cx.add_action(Workspace::open_shared_screen); cx.add_action(Workspace::add_folder_to_project); cx.add_action(Workspace::remove_folder_from_project); cx.add_action( |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { let pane = workspace.active_pane().clone(); workspace.unfollow(&pane, cx); }, ); cx.add_action( |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext| { workspace.save_active_item(false, cx).detach_and_log_err(cx); }, ); cx.add_action( |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { workspace.save_active_item(true, cx).detach_and_log_err(cx); }, ); cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::focus_center); cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { workspace.activate_previous_pane(cx) }); cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { workspace.activate_next_pane(cx) }); cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| { workspace.toggle_sidebar(SidebarSide::Left, cx); }); cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| { workspace.toggle_sidebar(SidebarSide::Right, cx); }); cx.add_action(Workspace::activate_pane_at_index); cx.add_action( |workspace: &mut Workspace, SplitWithItem { from, pane_to_split, item_id_to_move, split_direction, }: &_, cx| { workspace.split_pane_with_item( from.clone(), pane_to_split.clone(), *item_id_to_move, *split_direction, cx, ) }, ); cx.add_async_action( |workspace: &mut Workspace, SplitWithProjectEntry { pane_to_split, split_direction, project_entry, }: &_, cx| { pane_to_split.upgrade(cx).and_then(|pane_to_split| { let new_pane = workspace.add_pane(cx); workspace .center .split(&pane_to_split, &new_pane, *split_direction) .unwrap(); workspace .project .read(cx) .path_for_entry(*project_entry, cx) .map(|path| { let task = workspace.open_path(path, Some(new_pane.downgrade()), true, cx); cx.foreground().spawn(async move { task.await?; Ok(()) }) }) }) }, ); cx.add_async_action( |workspace: &mut Workspace, OpenProjectEntryInPane { pane, project_entry, }: &_, cx| { workspace .project .read(cx) .path_for_entry(*project_entry, cx) .map(|path| { let task = workspace.open_path(path, Some(pane.clone()), true, cx); cx.foreground().spawn(async move { task.await?; Ok(()) }) }) }, ); let client = &app_state.client; client.add_view_request_handler(Workspace::handle_follow); client.add_view_message_handler(Workspace::handle_unfollow); client.add_view_message_handler(Workspace::handle_update_followers); } pub fn register_project_item(cx: &mut MutableAppContext) { cx.update_default_global(|builders: &mut ProjectItemBuilders, _| { builders.insert(TypeId::of::(), |project, model, cx| { let item = model.downcast::().unwrap(); Box::new(cx.add_view(|cx| I::for_project_item(project, item, cx))) }); }); } pub fn register_followable_item(cx: &mut MutableAppContext) { cx.update_default_global(|builders: &mut FollowableItemBuilders, _| { builders.insert( TypeId::of::(), ( |pane, project, state, cx| { I::from_state_proto(pane, project, state, cx).map(|task| { cx.foreground() .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) }) }, |this| Box::new(this.downcast::().unwrap()), ), ); }); } pub struct AppState { pub languages: Arc, pub themes: Arc, pub client: Arc, pub user_store: ModelHandle, pub project_store: ModelHandle, pub fs: Arc, pub build_window_options: fn() -> WindowOptions<'static>, pub initialize_workspace: fn(&mut Workspace, &Arc, &mut ViewContext), pub default_item_factory: DefaultItemFactory, } #[derive(Eq, PartialEq, Hash)] pub enum ItemEvent { CloseItem, UpdateTab, UpdateBreadcrumbs, Edit, } pub trait Item: View { fn deactivated(&mut self, _: &mut ViewContext) {} fn workspace_deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { false } fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option> { None } fn tab_content(&self, detail: Option, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>; fn is_singleton(&self, cx: &AppContext) -> bool; fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext); fn clone_on_split(&self, _: &mut ViewContext) -> Option where Self: Sized, { None } fn is_dirty(&self, _: &AppContext) -> bool { false } fn has_conflict(&self, _: &AppContext) -> bool { false } fn can_save(&self, cx: &AppContext) -> bool; fn save( &mut self, project: ModelHandle, cx: &mut ViewContext, ) -> Task>; fn save_as( &mut self, project: ModelHandle, abs_path: PathBuf, cx: &mut ViewContext, ) -> Task>; fn reload( &mut self, project: ModelHandle, cx: &mut ViewContext, ) -> Task>; fn git_diff_recalc( &mut self, _project: ModelHandle, _cx: &mut ViewContext, ) -> Task> { Task::ready(Ok(())) } fn to_item_events(event: &Self::Event) -> Vec; fn should_close_item_on_event(_: &Self::Event) -> bool { false } fn should_update_tab_on_event(_: &Self::Event) -> bool { false } fn is_edit_event(_: &Self::Event) -> bool { false } fn act_as_type( &self, type_id: TypeId, self_handle: &ViewHandle, _: &AppContext, ) -> Option { if TypeId::of::() == type_id { Some(self_handle.into()) } else { None } } fn as_searchable(&self, _: &ViewHandle) -> Option> { None } fn breadcrumb_location(&self) -> ToolbarItemLocation { ToolbarItemLocation::Hidden } fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option> { None } } pub trait ProjectItem: Item { type Item: project::Item; fn for_project_item( project: ModelHandle, item: ModelHandle, cx: &mut ViewContext, ) -> Self; } pub trait FollowableItem: Item { fn to_state_proto(&self, cx: &AppContext) -> Option; fn from_state_proto( pane: ViewHandle, project: ModelHandle, state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>>; fn add_event_to_update_proto( &self, event: &Self::Event, update: &mut Option, cx: &AppContext, ) -> bool; fn apply_update_proto( &mut self, message: proto::update_view::Variant, cx: &mut ViewContext, ) -> Result<()>; fn set_leader_replica_id(&mut self, leader_replica_id: Option, cx: &mut ViewContext); fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; } pub trait FollowableItemHandle: ItemHandle { fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut MutableAppContext); fn to_state_proto(&self, cx: &AppContext) -> Option; fn add_event_to_update_proto( &self, event: &dyn Any, update: &mut Option, cx: &AppContext, ) -> bool; fn apply_update_proto( &self, message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()>; fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool; } impl FollowableItemHandle for ViewHandle { fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut MutableAppContext) { self.update(cx, |this, cx| { this.set_leader_replica_id(leader_replica_id, cx) }) } fn to_state_proto(&self, cx: &AppContext) -> Option { self.read(cx).to_state_proto(cx) } fn add_event_to_update_proto( &self, event: &dyn Any, update: &mut Option, cx: &AppContext, ) -> bool { if let Some(event) = event.downcast_ref() { self.read(cx).add_event_to_update_proto(event, update, cx) } else { false } } fn apply_update_proto( &self, message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()> { self.update(cx, |this, cx| this.apply_update_proto(message, cx)) } fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { if let Some(event) = event.downcast_ref() { T::should_unfollow_on_event(event, cx) } else { false } } } struct DelayedDebouncedEditAction { task: Option>, cancel_channel: Option>, } impl DelayedDebouncedEditAction { fn new() -> DelayedDebouncedEditAction { DelayedDebouncedEditAction { task: None, cancel_channel: None, } } fn fire_new( &mut self, delay: Duration, workspace: &Workspace, cx: &mut ViewContext, f: F, ) where F: FnOnce(ModelHandle, AsyncAppContext) -> Fut + 'static, Fut: 'static + Future, { if let Some(channel) = self.cancel_channel.take() { _ = channel.send(()); } let project = workspace.project().downgrade(); let (sender, mut receiver) = oneshot::channel::<()>(); self.cancel_channel = Some(sender); let previous_task = self.task.take(); self.task = Some(cx.spawn_weak(|_, cx| async move { let mut timer = cx.background().timer(delay).fuse(); if let Some(previous_task) = previous_task { previous_task.await; } futures::select_biased! { _ = receiver => return, _ = timer => {} } if let Some(project) = project.upgrade(&cx) { (f)(project, cx).await; } })); } } pub trait ItemHandle: 'static + fmt::Debug { fn subscribe_to_item_events( &self, cx: &mut MutableAppContext, handler: Box, ) -> gpui::Subscription; fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option>; fn tab_content(&self, detail: Option, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>; fn is_singleton(&self, cx: &AppContext) -> bool; fn boxed_clone(&self) -> Box; fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; fn added_to_pane( &self, workspace: &mut Workspace, pane: ViewHandle, cx: &mut ViewContext, ); fn deactivated(&self, cx: &mut MutableAppContext); fn workspace_deactivated(&self, cx: &mut MutableAppContext); fn navigate(&self, data: Box, cx: &mut MutableAppContext) -> bool; fn id(&self) -> usize; fn window_id(&self) -> usize; fn to_any(&self) -> AnyViewHandle; fn is_dirty(&self, cx: &AppContext) -> bool; fn has_conflict(&self, cx: &AppContext) -> bool; fn can_save(&self, cx: &AppContext) -> bool; fn save(&self, project: ModelHandle, cx: &mut MutableAppContext) -> Task>; fn save_as( &self, project: ModelHandle, abs_path: PathBuf, cx: &mut MutableAppContext, ) -> Task>; fn reload(&self, project: ModelHandle, cx: &mut MutableAppContext) -> Task>; fn git_diff_recalc( &self, project: ModelHandle, cx: &mut MutableAppContext, ) -> Task>; fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; fn on_release( &self, cx: &mut MutableAppContext, callback: Box, ) -> gpui::Subscription; fn to_searchable_item_handle(&self, cx: &AppContext) -> Option>; fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation; fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option>; } pub trait WeakItemHandle { fn id(&self) -> usize; fn window_id(&self) -> usize; fn upgrade(&self, cx: &AppContext) -> Option>; } impl dyn ItemHandle { pub fn downcast(&self) -> Option> { self.to_any().downcast() } pub fn act_as(&self, cx: &AppContext) -> Option> { self.act_as_type(TypeId::of::(), cx) .and_then(|t| t.downcast()) } } impl ItemHandle for ViewHandle { fn subscribe_to_item_events( &self, cx: &mut MutableAppContext, handler: Box, ) -> gpui::Subscription { cx.subscribe(self, move |_, event, cx| { for item_event in T::to_item_events(event) { handler(item_event, cx) } }) } fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option> { self.read(cx).tab_description(detail, cx) } fn tab_content( &self, detail: Option, style: &theme::Tab, cx: &AppContext, ) -> ElementBox { self.read(cx).tab_content(detail, style, cx) } fn project_path(&self, cx: &AppContext) -> Option { self.read(cx).project_path(cx) } fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> { self.read(cx).project_entry_ids(cx) } fn is_singleton(&self, cx: &AppContext) -> bool { self.read(cx).is_singleton(cx) } fn boxed_clone(&self) -> Box { Box::new(self.clone()) } fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option> { self.update(cx, |item, cx| { cx.add_option_view(|cx| item.clone_on_split(cx)) }) .map(|handle| Box::new(handle) as Box) } fn added_to_pane( &self, workspace: &mut Workspace, pane: ViewHandle, cx: &mut ViewContext, ) { let history = pane.read(cx).nav_history_for_item(self); self.update(cx, |this, cx| this.set_nav_history(history, cx)); if let Some(followed_item) = self.to_followable_item_handle(cx) { if let Some(message) = followed_item.to_state_proto(cx) { workspace.update_followers( proto::update_followers::Variant::CreateView(proto::View { id: followed_item.id() as u64, variant: Some(message), leader_id: workspace.leader_for_pane(&pane).map(|id| id.0), }), cx, ); } } if workspace .panes_by_item .insert(self.id(), pane.downgrade()) .is_none() { let mut pending_autosave = DelayedDebouncedEditAction::new(); let mut pending_git_update = DelayedDebouncedEditAction::new(); let pending_update = Rc::new(RefCell::new(None)); let pending_update_scheduled = Rc::new(AtomicBool::new(false)); let mut event_subscription = Some(cx.subscribe(self, move |workspace, item, event, cx| { let pane = if let Some(pane) = workspace .panes_by_item .get(&item.id()) .and_then(|pane| pane.upgrade(cx)) { pane } else { log::error!("unexpected item event after pane was dropped"); return; }; if let Some(item) = item.to_followable_item_handle(cx) { let leader_id = workspace.leader_for_pane(&pane); if leader_id.is_some() && item.should_unfollow_on_event(event, cx) { workspace.unfollow(&pane, cx); } if item.add_event_to_update_proto( event, &mut *pending_update.borrow_mut(), cx, ) && !pending_update_scheduled.load(SeqCst) { pending_update_scheduled.store(true, SeqCst); cx.after_window_update({ let pending_update = pending_update.clone(); let pending_update_scheduled = pending_update_scheduled.clone(); move |this, cx| { pending_update_scheduled.store(false, SeqCst); this.update_followers( proto::update_followers::Variant::UpdateView( proto::UpdateView { id: item.id() as u64, variant: pending_update.borrow_mut().take(), leader_id: leader_id.map(|id| id.0), }, ), cx, ); } }); } } for item_event in T::to_item_events(event).into_iter() { match item_event { ItemEvent::CloseItem => { Pane::close_item(workspace, pane, item.id(), cx) .detach_and_log_err(cx); return; } ItemEvent::UpdateTab => { pane.update(cx, |_, cx| { cx.emit(pane::Event::ChangeItemTitle); cx.notify(); }); } ItemEvent::Edit => { if let Autosave::AfterDelay { milliseconds } = cx.global::().autosave { let delay = Duration::from_millis(milliseconds); let item = item.clone(); pending_autosave.fire_new( delay, workspace, cx, |project, mut cx| async move { cx.update(|cx| Pane::autosave_item(&item, project, cx)) .await .log_err(); }, ); } let settings = cx.global::(); let debounce_delay = settings.git_overrides.gutter_debounce; let item = item.clone(); if let Some(delay) = debounce_delay { const MIN_GIT_DELAY: u64 = 50; let delay = delay.max(MIN_GIT_DELAY); let duration = Duration::from_millis(delay); pending_git_update.fire_new( duration, workspace, cx, |project, mut cx| async move { cx.update(|cx| item.git_diff_recalc(project, cx)) .await .log_err(); }, ); } else { let project = workspace.project().downgrade(); cx.spawn_weak(|_, mut cx| async move { if let Some(project) = project.upgrade(&cx) { cx.update(|cx| item.git_diff_recalc(project, cx)) .await .log_err(); } }) .detach(); } } _ => {} } } })); cx.observe_focus(self, move |workspace, item, focused, cx| { if !focused && cx.global::().autosave == Autosave::OnFocusChange { Pane::autosave_item(&item, workspace.project.clone(), cx) .detach_and_log_err(cx); } }) .detach(); let item_id = self.id(); cx.observe_release(self, move |workspace, _, _| { workspace.panes_by_item.remove(&item_id); event_subscription.take(); }) .detach(); } } fn deactivated(&self, cx: &mut MutableAppContext) { self.update(cx, |this, cx| this.deactivated(cx)); } fn workspace_deactivated(&self, cx: &mut MutableAppContext) { self.update(cx, |this, cx| this.workspace_deactivated(cx)); } fn navigate(&self, data: Box, cx: &mut MutableAppContext) -> bool { self.update(cx, |this, cx| this.navigate(data, cx)) } fn id(&self) -> usize { self.id() } fn window_id(&self) -> usize { self.window_id() } fn to_any(&self) -> AnyViewHandle { self.into() } fn is_dirty(&self, cx: &AppContext) -> bool { self.read(cx).is_dirty(cx) } fn has_conflict(&self, cx: &AppContext) -> bool { self.read(cx).has_conflict(cx) } fn can_save(&self, cx: &AppContext) -> bool { self.read(cx).can_save(cx) } fn save(&self, project: ModelHandle, cx: &mut MutableAppContext) -> Task> { self.update(cx, |item, cx| item.save(project, cx)) } fn save_as( &self, project: ModelHandle, abs_path: PathBuf, cx: &mut MutableAppContext, ) -> Task> { self.update(cx, |item, cx| item.save_as(project, abs_path, cx)) } fn reload( &self, project: ModelHandle, cx: &mut MutableAppContext, ) -> Task> { self.update(cx, |item, cx| item.reload(project, cx)) } fn git_diff_recalc( &self, project: ModelHandle, cx: &mut MutableAppContext, ) -> Task> { self.update(cx, |item, cx| item.git_diff_recalc(project, cx)) } fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option { self.read(cx).act_as_type(type_id, self, cx) } fn to_followable_item_handle(&self, cx: &AppContext) -> Option> { if cx.has_global::() { let builders = cx.global::(); let item = self.to_any(); Some(builders.get(&item.view_type())?.1(item)) } else { None } } fn on_release( &self, cx: &mut MutableAppContext, callback: Box, ) -> gpui::Subscription { cx.observe_release(self, move |_, cx| callback(cx)) } fn to_searchable_item_handle(&self, cx: &AppContext) -> Option> { self.read(cx).as_searchable(self) } fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation { self.read(cx).breadcrumb_location() } fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option> { self.read(cx).breadcrumbs(theme, cx) } } impl From> for AnyViewHandle { fn from(val: Box) -> Self { val.to_any() } } impl From<&Box> for AnyViewHandle { fn from(val: &Box) -> Self { val.to_any() } } impl Clone for Box { fn clone(&self) -> Box { self.boxed_clone() } } impl WeakItemHandle for WeakViewHandle { fn id(&self) -> usize { self.id() } fn window_id(&self) -> usize { self.window_id() } fn upgrade(&self, cx: &AppContext) -> Option> { self.upgrade(cx).map(|v| Box::new(v) as Box) } } pub trait Notification: View { fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool; } pub trait NotificationHandle { fn id(&self) -> usize; fn to_any(&self) -> AnyViewHandle; } impl NotificationHandle for ViewHandle { fn id(&self) -> usize { self.id() } fn to_any(&self) -> AnyViewHandle { self.into() } } impl From<&dyn NotificationHandle> for AnyViewHandle { fn from(val: &dyn NotificationHandle) -> Self { val.to_any() } } impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut MutableAppContext) -> Arc { use fs::HomeDir; cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf())); let settings = Settings::test(cx); cx.set_global(settings); let fs = fs::FakeFs::new(cx.background().clone()); let languages = Arc::new(LanguageRegistry::test()); let http_client = client::test::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone(), cx); let project_store = cx.add_model(|_| ProjectStore::new()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let themes = ThemeRegistry::new((), cx.font_cache().clone()); Arc::new(Self { client, themes, fs, languages, user_store, project_store, initialize_workspace: |_, _, _| {}, build_window_options: Default::default, default_item_factory: |_, _| unimplemented!(), }) } } pub enum Event { DockAnchorChanged, PaneAdded(ViewHandle), ContactRequestedJoin(u64), } pub struct Workspace { weak_self: WeakViewHandle, client: Arc, user_store: ModelHandle, remote_entity_subscription: Option, fs: Arc, modal: Option, center: PaneGroup, left_sidebar: ViewHandle, right_sidebar: ViewHandle, panes: Vec>, panes_by_item: HashMap>, active_pane: ViewHandle, last_active_center_pane: Option>, status_bar: ViewHandle, titlebar_item: Option, dock: Dock, notifications: Vec<(TypeId, usize, Box)>, project: ModelHandle, leader_state: LeaderState, follower_states_by_leader: FollowerStatesByLeader, last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, active_call: Option<(ModelHandle, Vec)>, _observe_current_user: Task<()>, } #[derive(Default)] struct LeaderState { followers: HashSet, } type FollowerStatesByLeader = HashMap, FollowerState>>; #[derive(Default)] struct FollowerState { active_view_id: Option, items_by_leader_view_id: HashMap, } #[derive(Debug)] enum FollowerItem { Loading(Vec), Loaded(Box), } impl Workspace { pub fn new( serialized_workspace: SerializedWorkspace, project: ModelHandle, dock_default_factory: DefaultItemFactory, cx: &mut ViewContext, ) -> Self { cx.observe_fullscreen(|_, _, cx| cx.notify()).detach(); cx.observe_window_activation(Self::on_window_activation_changed) .detach(); cx.observe(&project, |_, _, cx| cx.notify()).detach(); cx.subscribe(&project, move |this, _, event, cx| { match event { project::Event::RemoteIdChanged(remote_id) => { this.project_remote_id_changed(*remote_id, cx); } project::Event::CollaboratorLeft(peer_id) => { this.collaborator_left(*peer_id, cx); } project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => { this.update_window_title(cx); } project::Event::DisconnectedFromHost => { this.update_window_edited(cx); cx.blur(); } _ => {} } cx.notify() }) .detach(); let center_pane = cx.add_view(|cx| Pane::new(None, cx)); let pane_id = center_pane.id(); cx.subscribe(¢er_pane, move |this, _, event, cx| { this.handle_pane_event(pane_id, event, cx) }) .detach(); cx.focus(¢er_pane); cx.emit(Event::PaneAdded(center_pane.clone())); let fs = project.read(cx).fs().clone(); let user_store = project.read(cx).user_store(); let client = project.read(cx).client(); let mut current_user = user_store.read(cx).watch_current_user(); let mut connection_status = client.status(); let _observe_current_user = cx.spawn_weak(|this, mut cx| async move { current_user.recv().await; connection_status.recv().await; let mut stream = Stream::map(current_user, drop).merge(Stream::map(connection_status, drop)); while stream.recv().await.is_some() { cx.update(|cx| { if let Some(this) = this.upgrade(cx) { this.update(cx, |_, cx| cx.notify()); } }) } }); let handle = cx.handle(); let weak_handle = cx.weak_handle(); cx.emit_global(WorkspaceCreated(weak_handle.clone())); let dock = Dock::new(cx, dock_default_factory); let dock_pane = dock.pane().clone(); let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left)); let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right)); let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx)); let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx)); let right_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx)); let status_bar = cx.add_view(|cx| { let mut status_bar = StatusBar::new(¢er_pane.clone(), cx); status_bar.add_left_item(left_sidebar_buttons, cx); status_bar.add_right_item(right_sidebar_buttons, cx); status_bar.add_right_item(toggle_dock, cx); status_bar }); cx.update_default_global::, _, _>(|drag_and_drop, _| { drag_and_drop.register_container(weak_handle.clone()); }); let mut active_call = None; if cx.has_global::>() { let call = cx.global::>().clone(); let mut subscriptions = Vec::new(); subscriptions.push(cx.subscribe(&call, Self::on_active_call_event)); active_call = Some((call, subscriptions)); } let mut this = Workspace { modal: None, weak_self: weak_handle, center: PaneGroup::new(center_pane.clone()), dock, // When removing an item, the last element remaining in this array // is used to find where focus should fallback to. As such, the order // of these two variables is important. panes: vec![dock_pane, center_pane.clone()], panes_by_item: Default::default(), active_pane: center_pane.clone(), last_active_center_pane: Some(center_pane.downgrade()), status_bar, titlebar_item: None, notifications: Default::default(), client, remote_entity_subscription: None, user_store, fs, left_sidebar, right_sidebar, project, leader_state: Default::default(), follower_states_by_leader: Default::default(), last_leaders_by_pane: Default::default(), window_edited: false, active_call, _observe_current_user, }; this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); cx.defer(|this, cx| this.update_window_title(cx)); this } pub fn weak_handle(&self) -> WeakViewHandle { self.weak_self.clone() } pub fn left_sidebar(&self) -> &ViewHandle { &self.left_sidebar } pub fn right_sidebar(&self) -> &ViewHandle { &self.right_sidebar } pub fn status_bar(&self) -> &ViewHandle { &self.status_bar } pub fn user_store(&self) -> &ModelHandle { &self.user_store } pub fn project(&self) -> &ModelHandle { &self.project } pub fn client(&self) -> &Arc { &self.client } pub fn set_titlebar_item( &mut self, item: impl Into, cx: &mut ViewContext, ) { self.titlebar_item = Some(item.into()); cx.notify(); } pub fn titlebar_item(&self) -> Option { self.titlebar_item.clone() } /// Call the given callback with a workspace whose project is local. /// /// If the given workspace has a local project, then it will be passed /// to the callback. Otherwise, a new empty window will be created. pub fn with_local_workspace( &mut self, cx: &mut ViewContext, app_state: Arc, callback: F, ) -> T where T: 'static, F: FnOnce(&mut Workspace, &mut ViewContext) -> T, { if self.project.read(cx).is_local() { callback(self, cx) } else { let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { let mut workspace = Workspace::new( Project::local( app_state.client.clone(), app_state.user_store.clone(), app_state.project_store.clone(), app_state.languages.clone(), app_state.fs.clone(), cx, ), app_state.default_item_factory, cx, ); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); workspace }); workspace.update(cx, callback) } } pub fn worktrees<'a>( &self, cx: &'a AppContext, ) -> impl 'a + Iterator> { self.project.read(cx).worktrees(cx) } pub fn visible_worktrees<'a>( &self, cx: &'a AppContext, ) -> impl 'a + Iterator> { self.project.read(cx).visible_worktrees(cx) } pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future + 'static { let futures = self .worktrees(cx) .filter_map(|worktree| worktree.read(cx).as_local()) .map(|worktree| worktree.scan_complete()) .collect::>(); async move { for future in futures { future.await; } } } pub fn close( &mut self, _: &CloseWindow, cx: &mut ViewContext, ) -> Option>> { let prepare = self.prepare_to_close(false, cx); Some(cx.spawn(|this, mut cx| async move { if prepare.await? { this.update(&mut cx, |_, cx| { let window_id = cx.window_id(); cx.remove_window(window_id); }); } Ok(()) })) } pub fn prepare_to_close( &mut self, quitting: bool, cx: &mut ViewContext, ) -> Task> { let active_call = self.active_call().cloned(); let window_id = cx.window_id(); let workspace_count = cx .window_ids() .flat_map(|window_id| cx.root_view::(window_id)) .count(); cx.spawn(|this, mut cx| async move { if let Some(active_call) = active_call { if !quitting && workspace_count == 1 && active_call.read_with(&cx, |call, _| call.room().is_some()) { let answer = cx .prompt( window_id, PromptLevel::Warning, "Do you want to leave the current call?", &["Close window and hang up", "Cancel"], ) .next() .await; if answer == Some(1) { return anyhow::Ok(false); } else { active_call.update(&mut cx, |call, cx| call.hang_up(cx))?; } } } Ok(this .update(&mut cx, |this, cx| this.save_all_internal(true, cx)) .await?) }) } fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext) -> Option>> { let save_all = self.save_all_internal(false, cx); Some(cx.foreground().spawn(async move { save_all.await?; Ok(()) })) } fn save_all_internal( &mut self, should_prompt_to_save: bool, cx: &mut ViewContext, ) -> Task> { if self.project.read(cx).is_read_only() { return Task::ready(Ok(true)); } let dirty_items = self .panes .iter() .flat_map(|pane| { pane.read(cx).items().filter_map(|item| { if item.is_dirty(cx) { Some((pane.clone(), item.boxed_clone())) } else { None } }) }) .collect::>(); let project = self.project.clone(); cx.spawn_weak(|_, mut cx| async move { for (pane, item) in dirty_items { let (singleton, project_entry_ids) = cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx))); if singleton || !project_entry_ids.is_empty() { if let Some(ix) = pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref())) { if !Pane::save_item( project.clone(), &pane, ix, &*item, should_prompt_to_save, &mut cx, ) .await? { return Ok(false); } } } } Ok(true) }) } #[allow(clippy::type_complexity)] pub fn open_paths( &mut self, mut abs_paths: Vec, visible: bool, cx: &mut ViewContext, ) -> Task, anyhow::Error>>>> { let fs = self.fs.clone(); // Sort the paths to ensure we add worktrees for parents before their children. abs_paths.sort_unstable(); cx.spawn(|this, mut cx| async move { let mut project_paths = Vec::new(); for path in &abs_paths { project_paths.push( this.update(&mut cx, |this, cx| { this.project_path_for_path(path, visible, cx) }) .await .log_err(), ); } let tasks = abs_paths .iter() .cloned() .zip(project_paths.into_iter()) .map(|(abs_path, project_path)| { let this = this.clone(); cx.spawn(|mut cx| { let fs = fs.clone(); async move { let (_worktree, project_path) = project_path?; if fs.is_file(&abs_path).await { Some( this.update(&mut cx, |this, cx| { this.open_path(project_path, None, true, cx) }) .await, ) } else { None } } }) }) .collect::>(); futures::future::join_all(tasks).await }) } fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext) { let mut paths = cx.prompt_for_paths(PathPromptOptions { files: false, directories: true, multiple: true, }); cx.spawn(|this, mut cx| async move { if let Some(paths) = paths.recv().await.flatten() { let results = this .update(&mut cx, |this, cx| this.open_paths(paths, true, cx)) .await; for result in results.into_iter().flatten() { result.log_err(); } } }) .detach(); } fn remove_folder_from_project( &mut self, RemoveWorktreeFromProject(worktree_id): &RemoveWorktreeFromProject, cx: &mut ViewContext, ) { self.project .update(cx, |project, cx| project.remove_worktree(*worktree_id, cx)); } fn project_path_for_path( &self, abs_path: &Path, visible: bool, cx: &mut ViewContext, ) -> Task, ProjectPath)>> { let entry = self.project().update(cx, |project, cx| { project.find_or_create_local_worktree(abs_path, visible, cx) }); cx.spawn(|_, cx| async move { let (worktree, path) = entry.await?; let worktree_id = worktree.read_with(&cx, |t, _| t.id()); Ok(( worktree, ProjectPath { worktree_id, path: path.into(), }, )) }) } /// Returns the modal that was toggled closed if it was open. pub fn toggle_modal( &mut self, cx: &mut ViewContext, add_view: F, ) -> Option> where V: 'static + View, F: FnOnce(&mut Self, &mut ViewContext) -> ViewHandle, { cx.notify(); // Whatever modal was visible is getting clobbered. If its the same type as V, then return // it. Otherwise, create a new modal and set it as active. let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::()); if let Some(already_open_modal) = already_open_modal { cx.focus_self(); Some(already_open_modal) } else { let modal = add_view(self, cx); cx.focus(&modal); self.modal = Some(modal.into()); None } } pub fn modal(&self) -> Option> { self.modal .as_ref() .and_then(|modal| modal.clone().downcast::()) } pub fn dismiss_modal(&mut self, cx: &mut ViewContext) { if self.modal.take().is_some() { cx.focus(&self.active_pane); cx.notify(); } } pub fn show_notification( &mut self, id: usize, cx: &mut ViewContext, build_notification: impl FnOnce(&mut ViewContext) -> ViewHandle, ) { let type_id = TypeId::of::(); if self .notifications .iter() .all(|(existing_type_id, existing_id, _)| { (*existing_type_id, *existing_id) != (type_id, id) }) { let notification = build_notification(cx); cx.subscribe(¬ification, move |this, handle, event, cx| { if handle.read(cx).should_dismiss_notification_on_event(event) { this.dismiss_notification(type_id, id, cx); } }) .detach(); self.notifications .push((type_id, id, Box::new(notification))); cx.notify(); } } fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext) { self.notifications .retain(|(existing_type_id, existing_id, _)| { if (*existing_type_id, *existing_id) == (type_id, id) { cx.notify(); false } else { true } }); } pub fn items<'a>( &'a self, cx: &'a AppContext, ) -> impl 'a + Iterator> { self.panes.iter().flat_map(|pane| pane.read(cx).items()) } pub fn item_of_type(&self, cx: &AppContext) -> Option> { self.items_of_type(cx).max_by_key(|item| item.id()) } pub fn items_of_type<'a, T: Item>( &'a self, cx: &'a AppContext, ) -> impl 'a + Iterator> { self.panes .iter() .flat_map(|pane| pane.read(cx).items_of_type()) } pub fn active_item(&self, cx: &AppContext) -> Option> { self.active_pane().read(cx).active_item() } fn active_project_path(&self, cx: &ViewContext) -> Option { self.active_item(cx).and_then(|item| item.project_path(cx)) } pub fn save_active_item( &mut self, force_name_change: bool, cx: &mut ViewContext, ) -> Task> { let project = self.project.clone(); if let Some(item) = self.active_item(cx) { if !force_name_change && item.can_save(cx) { if item.has_conflict(cx.as_ref()) { const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; let mut answer = cx.prompt( PromptLevel::Warning, CONFLICT_MESSAGE, &["Overwrite", "Cancel"], ); cx.spawn(|_, mut cx| async move { let answer = answer.recv().await; if answer == Some(0) { cx.update(|cx| item.save(project, cx)).await?; } Ok(()) }) } else { item.save(project, cx) } } else if item.is_singleton(cx) { let worktree = self.worktrees(cx).next(); let start_abs_path = worktree .and_then(|w| w.read(cx).as_local()) .map_or(Path::new(""), |w| w.abs_path()) .to_path_buf(); let mut abs_path = cx.prompt_for_new_path(&start_abs_path); cx.spawn(|_, mut cx| async move { if let Some(abs_path) = abs_path.recv().await.flatten() { cx.update(|cx| item.save_as(project, abs_path, cx)).await?; } Ok(()) }) } else { Task::ready(Ok(())) } } else { Task::ready(Ok(())) } } pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext) { let sidebar = match sidebar_side { SidebarSide::Left => &mut self.left_sidebar, SidebarSide::Right => &mut self.right_sidebar, }; let open = sidebar.update(cx, |sidebar, cx| { let open = !sidebar.is_open(); sidebar.set_open(open, cx); open }); if open { Dock::hide_on_sidebar_shown(self, sidebar_side, cx); } cx.focus_self(); cx.notify(); } pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext) { let sidebar = match action.sidebar_side { SidebarSide::Left => &mut self.left_sidebar, SidebarSide::Right => &mut self.right_sidebar, }; let active_item = sidebar.update(cx, move |sidebar, cx| { if sidebar.is_open() && sidebar.active_item_ix() == action.item_index { sidebar.set_open(false, cx); None } else { sidebar.set_open(true, cx); sidebar.activate_item(action.item_index, cx); sidebar.active_item().cloned() } }); if let Some(active_item) = active_item { Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx); if active_item.is_focused(cx) { cx.focus_self(); } else { cx.focus(active_item.to_any()); } } else { cx.focus_self(); } cx.notify(); } pub fn toggle_sidebar_item_focus( &mut self, sidebar_side: SidebarSide, item_index: usize, cx: &mut ViewContext, ) { let sidebar = match sidebar_side { SidebarSide::Left => &mut self.left_sidebar, SidebarSide::Right => &mut self.right_sidebar, }; let active_item = sidebar.update(cx, |sidebar, cx| { sidebar.set_open(true, cx); sidebar.activate_item(item_index, cx); sidebar.active_item().cloned() }); if let Some(active_item) = active_item { Dock::hide_on_sidebar_shown(self, sidebar_side, cx); if active_item.is_focused(cx) { cx.focus_self(); } else { cx.focus(active_item.to_any()); } } cx.notify(); } pub fn focus_center(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { cx.focus_self(); cx.notify(); } fn add_pane(&mut self, cx: &mut ViewContext) -> ViewHandle { let pane = cx.add_view(|cx| Pane::new(None, cx)); let pane_id = pane.id(); cx.subscribe(&pane, move |this, _, event, cx| { this.handle_pane_event(pane_id, event, cx) }) .detach(); self.panes.push(pane.clone()); cx.focus(pane.clone()); cx.emit(Event::PaneAdded(pane.clone())); pane } pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { let active_pane = self.active_pane().clone(); Pane::add_item(self, &active_pane, item, true, true, None, cx); } pub fn open_path( &mut self, path: impl Into, pane: Option>, focus_item: bool, cx: &mut ViewContext, ) -> Task, anyhow::Error>> { let pane = pane.unwrap_or_else(|| self.active_pane().downgrade()); let task = self.load_path(path.into(), cx); cx.spawn(|this, mut cx| async move { let (project_entry_id, build_item) = task.await?; let pane = pane .upgrade(&cx) .ok_or_else(|| anyhow!("pane was closed"))?; this.update(&mut cx, |this, cx| { Ok(Pane::open_item( this, pane, project_entry_id, focus_item, cx, build_item, )) }) }) } pub(crate) fn load_path( &mut self, path: ProjectPath, cx: &mut ViewContext, ) -> Task< Result<( ProjectEntryId, impl 'static + FnOnce(&mut ViewContext) -> Box, )>, > { let project = self.project().clone(); let project_item = project.update(cx, |project, cx| project.open_path(path, cx)); cx.as_mut().spawn(|mut cx| async move { let (project_entry_id, project_item) = project_item.await?; let build_item = cx.update(|cx| { cx.default_global::() .get(&project_item.model_type()) .ok_or_else(|| anyhow!("no item builder for project item")) .cloned() })?; let build_item = move |cx: &mut ViewContext| build_item(project, project_item, cx); Ok((project_entry_id, build_item)) }) } pub fn open_project_item( &mut self, project_item: ModelHandle, cx: &mut ViewContext, ) -> ViewHandle where T: ProjectItem, { use project::Item as _; let entry_id = project_item.read(cx).entry_id(cx); if let Some(item) = entry_id .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx)) .and_then(|item| item.downcast()) { self.activate_item(&item, cx); return item; } let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); self.add_item(Box::new(item.clone()), cx); item } pub fn open_shared_screen(&mut self, action: &OpenSharedScreen, cx: &mut ViewContext) { if let Some(shared_screen) = self.shared_screen_for_peer(action.peer_id, &self.active_pane, cx) { let pane = self.active_pane.clone(); Pane::add_item(self, &pane, Box::new(shared_screen), false, true, None, cx); } } pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext) -> bool { let result = self.panes.iter().find_map(|pane| { pane.read(cx) .index_for_item(item) .map(|ix| (pane.clone(), ix)) }); if let Some((pane, ix)) = result { pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx)); true } else { false } } fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext) { let panes = self.center.panes(); if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { cx.focus(pane); } else { self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx); } } pub fn activate_next_pane(&mut self, cx: &mut ViewContext) { let next_pane = { let panes = self.center.panes(); let ix = panes .iter() .position(|pane| **pane == self.active_pane) .unwrap(); let next_ix = (ix + 1) % panes.len(); panes[next_ix].clone() }; cx.focus(next_pane); } pub fn activate_previous_pane(&mut self, cx: &mut ViewContext) { let prev_pane = { let panes = self.center.panes(); let ix = panes .iter() .position(|pane| **pane == self.active_pane) .unwrap(); let prev_ix = if ix == 0 { panes.len() - 1 } else { ix - 1 }; panes[prev_ix].clone() }; cx.focus(prev_pane); } fn handle_pane_focused(&mut self, pane: ViewHandle, cx: &mut ViewContext) { if self.active_pane != pane { self.active_pane .update(cx, |pane, cx| pane.set_active(false, cx)); self.active_pane = pane.clone(); self.active_pane .update(cx, |pane, cx| pane.set_active(true, cx)); self.status_bar.update(cx, |status_bar, cx| { status_bar.set_active_pane(&self.active_pane, cx); }); self.active_item_path_changed(cx); if &pane == self.dock_pane() { Dock::show(self, cx); } else { self.last_active_center_pane = Some(pane.downgrade()); if self.dock.is_anchored_at(DockAnchor::Expanded) { Dock::hide(self, cx); } } cx.notify(); } self.update_followers( proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { id: self.active_item(cx).map(|item| item.id() as u64), leader_id: self.leader_for_pane(&pane).map(|id| id.0), }), cx, ); } fn handle_pane_event( &mut self, pane_id: usize, event: &pane::Event, cx: &mut ViewContext, ) { if let Some(pane) = self.pane(pane_id) { let is_dock = &pane == self.dock.pane(); match event { pane::Event::Split(direction) if !is_dock => { self.split_pane(pane, *direction, cx); } pane::Event::Remove if !is_dock => self.remove_pane(pane, cx), pane::Event::Remove if is_dock => Dock::hide(self, cx), pane::Event::ActivateItem { local } => { if *local { self.unfollow(&pane, cx); } if &pane == self.active_pane() { self.active_item_path_changed(cx); } } pane::Event::ChangeItemTitle => { if pane == self.active_pane { self.active_item_path_changed(cx); } self.update_window_edited(cx); } pane::Event::RemoveItem { item_id } => { self.update_window_edited(cx); if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) { if entry.get().id() == pane.id() { entry.remove(); } } } _ => {} } } else if self.dock.visible_pane().is_none() { error!("pane {} not found", pane_id); } } pub fn split_pane( &mut self, pane: ViewHandle, direction: SplitDirection, cx: &mut ViewContext, ) -> Option> { if &pane == self.dock_pane() { warn!("Can't split dock pane."); return None; } pane.read(cx).active_item().map(|item| { let new_pane = self.add_pane(cx); if let Some(clone) = item.clone_on_split(cx.as_mut()) { Pane::add_item(self, &new_pane, clone, true, true, None, cx); } self.center.split(&pane, &new_pane, direction).unwrap(); cx.notify(); new_pane }) } pub fn split_pane_with_item( &mut self, from: WeakViewHandle, pane_to_split: WeakViewHandle, item_id_to_move: usize, split_direction: SplitDirection, cx: &mut ViewContext, ) { if let Some((pane_to_split, from)) = pane_to_split.upgrade(cx).zip(from.upgrade(cx)) { if &pane_to_split == self.dock_pane() { warn!("Can't split dock pane."); return; } let new_pane = self.add_pane(cx); Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx); self.center .split(&pane_to_split, &new_pane, split_direction) .unwrap(); cx.notify(); } } fn remove_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { if self.center.remove(&pane).unwrap() { self.panes.retain(|p| p != &pane); cx.focus(self.panes.last().unwrap().clone()); self.unfollow(&pane, cx); self.last_leaders_by_pane.remove(&pane.downgrade()); for removed_item in pane.read(cx).items() { self.panes_by_item.remove(&removed_item.id()); } if self.last_active_center_pane == Some(pane.downgrade()) { self.last_active_center_pane = None; } cx.notify(); } else { self.active_item_path_changed(cx); } } pub fn panes(&self) -> &[ViewHandle] { &self.panes } fn pane(&self, pane_id: usize) -> Option> { self.panes.iter().find(|pane| pane.id() == pane_id).cloned() } pub fn active_pane(&self) -> &ViewHandle { &self.active_pane } pub fn dock_pane(&self) -> &ViewHandle { self.dock.pane() } fn project_remote_id_changed(&mut self, remote_id: Option, cx: &mut ViewContext) { if let Some(remote_id) = remote_id { self.remote_entity_subscription = Some(self.client.add_view_for_remote_entity(remote_id, cx)); } else { self.remote_entity_subscription.take(); } } fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { self.leader_state.followers.remove(&peer_id); if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) { for state in states_by_pane.into_values() { for item in state.items_by_leader_view_id.into_values() { if let FollowerItem::Loaded(item) = item { item.set_leader_replica_id(None, cx); } } } } cx.notify(); } pub fn toggle_follow( &mut self, ToggleFollow(leader_id): &ToggleFollow, cx: &mut ViewContext, ) -> Option>> { let leader_id = *leader_id; let pane = self.active_pane().clone(); if let Some(prev_leader_id) = self.unfollow(&pane, cx) { if leader_id == prev_leader_id { return None; } } self.last_leaders_by_pane .insert(pane.downgrade(), leader_id); self.follower_states_by_leader .entry(leader_id) .or_default() .insert(pane.clone(), Default::default()); cx.notify(); let project_id = self.project.read(cx).remote_id()?; let request = self.client.request(proto::Follow { project_id, leader_id: leader_id.0, }); Some(cx.spawn_weak(|this, mut cx| async move { let response = request.await?; if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, _| { let state = this .follower_states_by_leader .get_mut(&leader_id) .and_then(|states_by_pane| states_by_pane.get_mut(&pane)) .ok_or_else(|| anyhow!("following interrupted"))?; state.active_view_id = response.active_view_id; Ok::<_, anyhow::Error>(()) })?; Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx) .await?; } Ok(()) })) } pub fn follow_next_collaborator( &mut self, _: &FollowNextCollaborator, cx: &mut ViewContext, ) -> Option>> { let collaborators = self.project.read(cx).collaborators(); let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) { let mut collaborators = collaborators.keys().copied(); for peer_id in collaborators.by_ref() { if peer_id == leader_id { break; } } collaborators.next() } else if let Some(last_leader_id) = self.last_leaders_by_pane.get(&self.active_pane.downgrade()) { if collaborators.contains_key(last_leader_id) { Some(*last_leader_id) } else { None } } else { None }; next_leader_id .or_else(|| collaborators.keys().copied().next()) .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx)) } pub fn unfollow( &mut self, pane: &ViewHandle, cx: &mut ViewContext, ) -> Option { for (leader_id, states_by_pane) in &mut self.follower_states_by_leader { let leader_id = *leader_id; if let Some(state) = states_by_pane.remove(pane) { for (_, item) in state.items_by_leader_view_id { if let FollowerItem::Loaded(item) = item { item.set_leader_replica_id(None, cx); } } if states_by_pane.is_empty() { self.follower_states_by_leader.remove(&leader_id); if let Some(project_id) = self.project.read(cx).remote_id() { self.client .send(proto::Unfollow { project_id, leader_id: leader_id.0, }) .log_err(); } } cx.notify(); return Some(leader_id); } } None } pub fn is_following(&self, peer_id: PeerId) -> bool { self.follower_states_by_leader.contains_key(&peer_id) } fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext) -> ElementBox { let project = &self.project.read(cx); let mut worktree_root_names = String::new(); for (i, name) in project.worktree_root_names(cx).enumerate() { if i > 0 { worktree_root_names.push_str(", "); } worktree_root_names.push_str(name); } // TODO: There should be a better system in place for this // (https://github.com/zed-industries/zed/issues/1290) let is_fullscreen = cx.window_is_fullscreen(cx.window_id()); let container_theme = if is_fullscreen { let mut container_theme = theme.workspace.titlebar.container; container_theme.padding.left = container_theme.padding.right; container_theme } else { theme.workspace.titlebar.container }; enum TitleBar {} ConstrainedBox::new( MouseEventHandler::::new(0, cx, |_, cx| { Container::new( Stack::new() .with_child( Label::new(worktree_root_names, theme.workspace.titlebar.title.clone()) .aligned() .left() .boxed(), ) .with_children( self.titlebar_item .as_ref() .map(|item| ChildView::new(item, cx).aligned().right().boxed()), ) .boxed(), ) .with_style(container_theme) .boxed() }) .on_click(MouseButton::Left, |event, cx| { if event.click_count == 2 { cx.zoom_window(cx.window_id()); } }) .boxed(), ) .with_height(theme.workspace.titlebar.height) .named("titlebar") } fn active_item_path_changed(&mut self, cx: &mut ViewContext) { let active_entry = self.active_project_path(cx); self.project .update(cx, |project, cx| project.set_active_path(active_entry, cx)); self.update_window_title(cx); } fn update_window_title(&mut self, cx: &mut ViewContext) { let mut title = String::new(); let project = self.project().read(cx); if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) { let filename = path .path .file_name() .map(|s| s.to_string_lossy()) .or_else(|| { Some(Cow::Borrowed( project .worktree_for_id(path.worktree_id, cx)? .read(cx) .root_name(), )) }); if let Some(filename) = filename { title.push_str(filename.as_ref()); title.push_str(" — "); } } for (i, name) in project.worktree_root_names(cx).enumerate() { if i > 0 { title.push_str(", "); } title.push_str(name); } if title.is_empty() { title = "empty project".to_string(); } cx.set_window_title(&title); } fn update_window_edited(&mut self, cx: &mut ViewContext) { let is_edited = !self.project.read(cx).is_read_only() && self .items(cx) .any(|item| item.has_conflict(cx) || item.is_dirty(cx)); if is_edited != self.window_edited { self.window_edited = is_edited; cx.set_window_edited(self.window_edited) } } fn render_disconnected_overlay(&self, cx: &mut RenderContext) -> Option { if self.project.read(cx).is_read_only() { enum DisconnectedOverlay {} Some( MouseEventHandler::::new(0, cx, |_, cx| { let theme = &cx.global::().theme; Label::new( "Your connection to the remote project has been lost.".to_string(), theme.workspace.disconnected_overlay.text.clone(), ) .aligned() .contained() .with_style(theme.workspace.disconnected_overlay.container) .boxed() }) .with_cursor_style(CursorStyle::Arrow) .capture_all() .boxed(), ) } else { None } } fn render_notifications( &self, theme: &theme::Workspace, cx: &AppContext, ) -> Option { if self.notifications.is_empty() { None } else { Some( Flex::column() .with_children(self.notifications.iter().map(|(_, _, notification)| { ChildView::new(notification.as_ref(), cx) .contained() .with_style(theme.notification) .boxed() })) .constrained() .with_width(theme.notifications.width) .contained() .with_style(theme.notifications.container) .aligned() .bottom() .right() .boxed(), ) } } // RPC handlers async fn handle_follow( this: ViewHandle, envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result { this.update(&mut cx, |this, cx| { this.leader_state .followers .insert(envelope.original_sender_id()?); let active_view_id = this .active_item(cx) .and_then(|i| i.to_followable_item_handle(cx)) .map(|i| i.id() as u64); Ok(proto::FollowResponse { active_view_id, views: this .panes() .iter() .flat_map(|pane| { let leader_id = this.leader_for_pane(pane).map(|id| id.0); pane.read(cx).items().filter_map({ let cx = &cx; move |item| { let id = item.id() as u64; let item = item.to_followable_item_handle(cx)?; let variant = item.to_state_proto(cx)?; Some(proto::View { id, leader_id, variant: Some(variant), }) } }) }) .collect(), }) }) } async fn handle_unfollow( this: ViewHandle, envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { this.update(&mut cx, |this, _| { this.leader_state .followers .remove(&envelope.original_sender_id()?); Ok(()) }) } async fn handle_update_followers( this: ViewHandle, envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { let leader_id = envelope.original_sender_id()?; match envelope .payload .variant .ok_or_else(|| anyhow!("invalid update"))? { proto::update_followers::Variant::UpdateActiveView(update_active_view) => { this.update(&mut cx, |this, cx| { this.update_leader_state(leader_id, cx, |state, _| { state.active_view_id = update_active_view.id; }); Ok::<_, anyhow::Error>(()) }) } proto::update_followers::Variant::UpdateView(update_view) => { this.update(&mut cx, |this, cx| { let variant = update_view .variant .ok_or_else(|| anyhow!("missing update view variant"))?; this.update_leader_state(leader_id, cx, |state, cx| { let variant = variant.clone(); match state .items_by_leader_view_id .entry(update_view.id) .or_insert(FollowerItem::Loading(Vec::new())) { FollowerItem::Loaded(item) => { item.apply_update_proto(variant, cx).log_err(); } FollowerItem::Loading(updates) => updates.push(variant), } }); Ok(()) }) } proto::update_followers::Variant::CreateView(view) => { let panes = this.read_with(&cx, |this, _| { this.follower_states_by_leader .get(&leader_id) .into_iter() .flat_map(|states_by_pane| states_by_pane.keys()) .cloned() .collect() }); Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx) .await?; Ok(()) } } .log_err(); Ok(()) } async fn add_views_from_leader( this: ViewHandle, leader_id: PeerId, panes: Vec>, views: Vec, cx: &mut AsyncAppContext, ) -> Result<()> { let project = this.read_with(cx, |this, _| this.project.clone()); let replica_id = project .read_with(cx, |project, _| { project .collaborators() .get(&leader_id) .map(|c| c.replica_id) }) .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?; let item_builders = cx.update(|cx| { cx.default_global::() .values() .map(|b| b.0) .collect::>() }); let mut item_tasks_by_pane = HashMap::default(); for pane in panes { let mut item_tasks = Vec::new(); let mut leader_view_ids = Vec::new(); for view in &views { let mut variant = view.variant.clone(); if variant.is_none() { Err(anyhow!("missing variant"))?; } for build_item in &item_builders { let task = cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx)); if let Some(task) = task { item_tasks.push(task); leader_view_ids.push(view.id); break; } else { assert!(variant.is_some()); } } } item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids)); } for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane { let items = futures::future::try_join_all(item_tasks).await?; this.update(cx, |this, cx| { let state = this .follower_states_by_leader .get_mut(&leader_id)? .get_mut(&pane)?; for (id, item) in leader_view_ids.into_iter().zip(items) { item.set_leader_replica_id(Some(replica_id), cx); match state.items_by_leader_view_id.entry(id) { hash_map::Entry::Occupied(e) => { let e = e.into_mut(); if let FollowerItem::Loading(updates) = e { for update in updates.drain(..) { item.apply_update_proto(update, cx) .context("failed to apply view update") .log_err(); } } *e = FollowerItem::Loaded(item); } hash_map::Entry::Vacant(e) => { e.insert(FollowerItem::Loaded(item)); } } } Some(()) }); } this.update(cx, |this, cx| this.leader_updated(leader_id, cx)); Ok(()) } fn update_followers( &self, update: proto::update_followers::Variant, cx: &AppContext, ) -> Option<()> { let project_id = self.project.read(cx).remote_id()?; if !self.leader_state.followers.is_empty() { self.client .send(proto::UpdateFollowers { project_id, follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(), variant: Some(update), }) .log_err(); } None } pub fn leader_for_pane(&self, pane: &ViewHandle) -> Option { self.follower_states_by_leader .iter() .find_map(|(leader_id, state)| { if state.contains_key(pane) { Some(*leader_id) } else { None } }) } fn update_leader_state( &mut self, leader_id: PeerId, cx: &mut ViewContext, mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext), ) { for (_, state) in self .follower_states_by_leader .get_mut(&leader_id) .into_iter() .flatten() { update_fn(state, cx); } self.leader_updated(leader_id, cx); } fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { cx.notify(); let call = self.active_call()?; let room = call.read(cx).room()?.read(cx); let participant = room.remote_participants().get(&leader_id)?; let mut items_to_add = Vec::new(); match participant.location { call::ParticipantLocation::SharedProject { project_id } => { if Some(project_id) == self.project.read(cx).remote_id() { for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { if let Some(FollowerItem::Loaded(item)) = state .active_view_id .and_then(|id| state.items_by_leader_view_id.get(&id)) { items_to_add.push((pane.clone(), item.boxed_clone())); } } } } call::ParticipantLocation::UnsharedProject => {} call::ParticipantLocation::External => { for (pane, _) in self.follower_states_by_leader.get(&leader_id)? { if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { items_to_add.push((pane.clone(), Box::new(shared_screen))); } } } } for (pane, item) in items_to_add { Pane::add_item(self, &pane, item.boxed_clone(), false, false, None, cx); if pane == self.active_pane { pane.update(cx, |pane, cx| pane.focus_active_item(cx)); } } None } fn shared_screen_for_peer( &self, peer_id: PeerId, pane: &ViewHandle, cx: &mut ViewContext, ) -> Option> { let call = self.active_call()?; let room = call.read(cx).room()?.read(cx); let participant = room.remote_participants().get(&peer_id)?; let track = participant.tracks.values().next()?.clone(); let user = participant.user.clone(); for item in pane.read(cx).items_of_type::() { if item.read(cx).peer_id == peer_id { return Some(item); } } Some(cx.add_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) } pub fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { if !active { for pane in &self.panes { pane.update(cx, |pane, cx| { if let Some(item) = pane.active_item() { item.workspace_deactivated(cx); } if matches!( cx.global::().autosave, Autosave::OnWindowChange | Autosave::OnFocusChange ) { for item in pane.items() { Pane::autosave_item(item.as_ref(), self.project.clone(), cx) .detach_and_log_err(cx); } } }); } } } fn active_call(&self) -> Option<&ModelHandle> { self.active_call.as_ref().map(|(call, _)| call) } fn on_active_call_event( &mut self, _: ModelHandle, event: &call::room::Event, cx: &mut ViewContext, ) { match event { call::room::Event::ParticipantLocationChanged { participant_id } | call::room::Event::RemoteVideoTracksChanged { participant_id } => { self.leader_updated(*participant_id, cx); } _ => {} } } } impl Entity for Workspace { type Event = Event; } impl View for Workspace { fn ui_name() -> &'static str { "Workspace" } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = cx.global::().theme.clone(); Stack::new() .with_child( Flex::column() .with_child(self.render_titlebar(&theme, cx)) .with_child( Stack::new() .with_child({ let project = self.project.clone(); Flex::row() .with_children( if self.left_sidebar.read(cx).active_item().is_some() { Some( ChildView::new(&self.left_sidebar, cx) .flex(0.8, false) .boxed(), ) } else { None }, ) .with_child( FlexItem::new( Flex::column() .with_child( FlexItem::new(self.center.render( &project, &theme, &self.follower_states_by_leader, self.active_call(), self.active_pane(), cx, )) .flex(1., true) .boxed(), ) .with_children(self.dock.render( &theme, DockAnchor::Bottom, cx, )) .boxed(), ) .flex(1., true) .boxed(), ) .with_children(self.dock.render(&theme, DockAnchor::Right, cx)) .with_children( if self.right_sidebar.read(cx).active_item().is_some() { Some( ChildView::new(&self.right_sidebar, cx) .flex(0.8, false) .boxed(), ) } else { None }, ) .boxed() }) .with_child( Overlay::new( Stack::new() .with_children(self.dock.render( &theme, DockAnchor::Expanded, cx, )) .with_children(self.modal.as_ref().map(|modal| { ChildView::new(modal, cx) .contained() .with_style(theme.workspace.modal) .aligned() .top() .boxed() })) .with_children( self.render_notifications(&theme.workspace, cx), ) .boxed(), ) .boxed(), ) .flex(1.0, true) .boxed(), ) .with_child(ChildView::new(&self.status_bar, cx).boxed()) .contained() .with_background_color(theme.workspace.background) .boxed(), ) .with_children(DragAndDrop::render(cx)) .with_children(self.render_disconnected_overlay(cx)) .named("workspace") } fn focus_in(&mut self, view: AnyViewHandle, cx: &mut ViewContext) { if cx.is_self_focused() { cx.focus(&self.active_pane); } else { for pane in self.panes() { let view = view.clone(); if pane.update(cx, |_, cx| cx.is_child(view)) { self.handle_pane_focused(pane.clone(), cx); break; } } } } fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context { let mut keymap = Self::default_keymap_context(); if self.active_pane() == self.dock_pane() { keymap.set.insert("Dock".into()); } keymap } } pub trait WorkspaceHandle { fn file_project_paths(&self, cx: &AppContext) -> Vec; } impl WorkspaceHandle for ViewHandle { fn file_project_paths(&self, cx: &AppContext) -> Vec { self.read(cx) .worktrees(cx) .flat_map(|worktree| { let worktree_id = worktree.read(cx).id(); worktree.read(cx).files(true, 0).map(move |f| ProjectPath { worktree_id, path: f.path.clone(), }) }) .collect::>() } } impl std::fmt::Debug for OpenPaths { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("OpenPaths") .field("paths", &self.paths) .finish() } } fn open(_: &Open, cx: &mut MutableAppContext) { let mut paths = cx.prompt_for_paths(PathPromptOptions { files: true, directories: true, multiple: true, }); cx.spawn(|mut cx| async move { if let Some(paths) = paths.recv().await.flatten() { cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths })); } }) .detach(); } pub struct WorkspaceCreated(WeakViewHandle); pub fn activate_workspace_for_project( cx: &mut MutableAppContext, predicate: impl Fn(&mut Project, &mut ModelContext) -> bool, ) -> Option> { for window_id in cx.window_ids().collect::>() { if let Some(workspace_handle) = cx.root_view::(window_id) { let project = workspace_handle.read(cx).project.clone(); if project.update(cx, &predicate) { cx.activate_window(window_id); return Some(workspace_handle); } } } None } #[allow(clippy::type_complexity)] pub fn open_paths( abs_paths: &[PathBuf], app_state: &Arc, cx: &mut MutableAppContext, ) -> Task<( ViewHandle, Vec, anyhow::Error>>>, )> { log::info!("open paths {:?}", abs_paths); // Open paths in existing workspace if possible let existing = activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx)); let app_state = app_state.clone(); let abs_paths = abs_paths.to_vec(); cx.spawn(|mut cx| async move { let mut new_project = None; let workspace = if let Some(existing) = existing { existing } else { let contains_directory = futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path))) .await .contains(&false); cx.add_window((app_state.build_window_options)(), |cx| { let project = Project::local( app_state.client.clone(), app_state.user_store.clone(), app_state.project_store.clone(), app_state.languages.clone(), app_state.fs.clone(), cx, ); new_project = Some(project.clone()); let mut workspace = Workspace::new(project, app_state.default_item_factory, cx); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); if contains_directory { workspace.toggle_sidebar(SidebarSide::Left, cx); } workspace }) .1 }; let items = workspace .update(&mut cx, |workspace, cx| { workspace.open_paths(abs_paths, true, cx) }) .await; (workspace, items) }) } fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { let mut workspace = Workspace::new( Project::local( app_state.client.clone(), app_state.user_store.clone(), app_state.project_store.clone(), app_state.languages.clone(), app_state.fs.clone(), cx, ), app_state.default_item_factory, cx, ); (app_state.initialize_workspace)(&mut workspace, app_state, cx); workspace }); cx.dispatch_action_at(window_id, workspace.id(), NewFile); } #[cfg(test)] mod tests { use std::cell::Cell; use crate::sidebar::SidebarItem; use super::*; use fs::FakeFs; use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext}; use project::{Project, ProjectEntryId}; use serde_json::json; pub fn default_item_factory( _workspace: &mut Workspace, _cx: &mut ViewContext, ) -> Box { unimplemented!(); } #[gpui::test] async fn test_tab_disambiguation(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); Settings::test_async(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx)); // Adding an item with no ambiguity renders the tab without detail. let item1 = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]); item }); workspace.update(cx, |workspace, cx| { workspace.add_item(Box::new(item1.clone()), cx); }); item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), None)); // Adding an item that creates ambiguity increases the level of detail on // both tabs. let item2 = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); item }); workspace.update(cx, |workspace, cx| { workspace.add_item(Box::new(item2.clone()), cx); }); item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); // Adding an item that creates ambiguity increases the level of detail only // on the ambiguous tabs. In this case, the ambiguity can't be resolved so // we stop at the highest detail available. let item3 = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); item }); workspace.update(cx, |workspace, cx| { workspace.add_item(Box::new(item3.clone()), cx); }); item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); } #[gpui::test] async fn test_tracking_active_path(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); Settings::test_async(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( "/root1", json!({ "one.txt": "", "two.txt": "", }), ) .await; fs.insert_tree( "/root2", json!({ "three.txt": "", }), ) .await; let project = Project::test(fs, ["root1".as_ref()], cx).await; let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx)); let worktree_id = project.read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() }); let item1 = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); item.project_path = Some((worktree_id, "one.txt").into()); item }); let item2 = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); item.project_path = Some((worktree_id, "two.txt").into()); item }); // Add an item to an empty pane workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx)); project.read_with(cx, |project, cx| { assert_eq!( project.active_entry(), project .entry_for_path(&(worktree_id, "one.txt").into(), cx) .map(|e| e.id) ); }); assert_eq!( cx.current_window_title(window_id).as_deref(), Some("one.txt — root1") ); // Add a second item to a non-empty pane workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx)); assert_eq!( cx.current_window_title(window_id).as_deref(), Some("two.txt — root1") ); project.read_with(cx, |project, cx| { assert_eq!( project.active_entry(), project .entry_for_path(&(worktree_id, "two.txt").into(), cx) .map(|e| e.id) ); }); // Close the active item workspace .update(cx, |workspace, cx| { Pane::close_active_item(workspace, &Default::default(), cx).unwrap() }) .await .unwrap(); assert_eq!( cx.current_window_title(window_id).as_deref(), Some("one.txt — root1") ); project.read_with(cx, |project, cx| { assert_eq!( project.active_entry(), project .entry_for_path(&(worktree_id, "one.txt").into(), cx) .map(|e| e.id) ); }); // Add a project folder project .update(cx, |project, cx| { project.find_or_create_local_worktree("/root2", true, cx) }) .await .unwrap(); assert_eq!( cx.current_window_title(window_id).as_deref(), Some("one.txt — root1, root2") ); // Remove a project folder project.update(cx, |project, cx| { project.remove_worktree(worktree_id, cx); }); assert_eq!( cx.current_window_title(window_id).as_deref(), Some("one.txt — root2") ); } #[gpui::test] async fn test_close_window(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); Settings::test_async(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree("/root", json!({ "one": "" })).await; let project = Project::test(fs, ["root".as_ref()], cx).await; let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx)); // When there are no dirty items, there's nothing to do. let item1 = cx.add_view(&workspace, |_| TestItem::new()); workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx)); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); assert!(task.await.unwrap()); // When there are dirty untitled items, prompt to save each one. If the user // cancels any prompt, then abort. let item2 = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); item.is_dirty = true; item }); let item3 = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); item.is_dirty = true; item.project_entry_ids = vec![ProjectEntryId::from_proto(1)]; item }); workspace.update(cx, |w, cx| { w.add_item(Box::new(item2.clone()), cx); w.add_item(Box::new(item3.clone()), cx); }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); cx.foreground().run_until_parked(); cx.simulate_prompt_answer(window_id, 2 /* cancel */); cx.foreground().run_until_parked(); assert!(!cx.has_pending_prompt(window_id)); assert!(!task.await.unwrap()); } #[gpui::test] async fn test_close_pane_items(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); Settings::test_async(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx)); let item1 = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); item.is_dirty = true; item.project_entry_ids = vec![ProjectEntryId::from_proto(1)]; item }); let item2 = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); item.is_dirty = true; item.has_conflict = true; item.project_entry_ids = vec![ProjectEntryId::from_proto(2)]; item }); let item3 = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); item.is_dirty = true; item.has_conflict = true; item.project_entry_ids = vec![ProjectEntryId::from_proto(3)]; item }); let item4 = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); item.is_dirty = true; item }); let pane = workspace.update(cx, |workspace, cx| { workspace.add_item(Box::new(item1.clone()), cx); workspace.add_item(Box::new(item2.clone()), cx); workspace.add_item(Box::new(item3.clone()), cx); workspace.add_item(Box::new(item4.clone()), cx); workspace.active_pane().clone() }); let close_items = workspace.update(cx, |workspace, cx| { pane.update(cx, |pane, cx| { pane.activate_item(1, true, true, cx); assert_eq!(pane.active_item().unwrap().id(), item2.id()); }); let item1_id = item1.id(); let item3_id = item3.id(); let item4_id = item4.id(); Pane::close_items(workspace, pane.clone(), cx, move |id| { [item1_id, item3_id, item4_id].contains(&id) }) }); cx.foreground().run_until_parked(); pane.read_with(cx, |pane, _| { assert_eq!(pane.items_len(), 4); assert_eq!(pane.active_item().unwrap().id(), item1.id()); }); cx.simulate_prompt_answer(window_id, 0); cx.foreground().run_until_parked(); pane.read_with(cx, |pane, cx| { assert_eq!(item1.read(cx).save_count, 1); assert_eq!(item1.read(cx).save_as_count, 0); assert_eq!(item1.read(cx).reload_count, 0); assert_eq!(pane.items_len(), 3); assert_eq!(pane.active_item().unwrap().id(), item3.id()); }); cx.simulate_prompt_answer(window_id, 1); cx.foreground().run_until_parked(); pane.read_with(cx, |pane, cx| { assert_eq!(item3.read(cx).save_count, 0); assert_eq!(item3.read(cx).save_as_count, 0); assert_eq!(item3.read(cx).reload_count, 1); assert_eq!(pane.items_len(), 2); assert_eq!(pane.active_item().unwrap().id(), item4.id()); }); cx.simulate_prompt_answer(window_id, 0); cx.foreground().run_until_parked(); cx.simulate_new_path_selection(|_| Some(Default::default())); close_items.await.unwrap(); pane.read_with(cx, |pane, cx| { assert_eq!(item4.read(cx).save_count, 0); assert_eq!(item4.read(cx).save_as_count, 1); assert_eq!(item4.read(cx).reload_count, 0); assert_eq!(pane.items_len(), 1); assert_eq!(pane.active_item().unwrap().id(), item2.id()); }); } #[gpui::test] async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); Settings::test_async(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx)); // Create several workspace items with single project entries, and two // workspace items with multiple project entries. let single_entry_items = (0..=4) .map(|project_entry_id| { let mut item = TestItem::new(); item.is_dirty = true; item.project_entry_ids = vec![ProjectEntryId::from_proto(project_entry_id)]; item.is_singleton = true; item }) .collect::>(); let item_2_3 = { let mut item = TestItem::new(); item.is_dirty = true; item.is_singleton = false; item.project_entry_ids = vec![ProjectEntryId::from_proto(2), ProjectEntryId::from_proto(3)]; item }; let item_3_4 = { let mut item = TestItem::new(); item.is_dirty = true; item.is_singleton = false; item.project_entry_ids = vec![ProjectEntryId::from_proto(3), ProjectEntryId::from_proto(4)]; item }; // Create two panes that contain the following project entries: // left pane: // multi-entry items: (2, 3) // single-entry items: 0, 1, 2, 3, 4 // right pane: // single-entry items: 1 // multi-entry items: (3, 4) let left_pane = workspace.update(cx, |workspace, cx| { let left_pane = workspace.active_pane().clone(); workspace.add_item(Box::new(cx.add_view(|_| item_2_3.clone())), cx); for item in &single_entry_items { workspace.add_item(Box::new(cx.add_view(|_| item.clone())), cx); } left_pane.update(cx, |pane, cx| { pane.activate_item(2, true, true, cx); }); workspace .split_pane(left_pane.clone(), SplitDirection::Right, cx) .unwrap(); left_pane }); //Need to cause an effect flush in order to respect new focus workspace.update(cx, |workspace, cx| { workspace.add_item(Box::new(cx.add_view(|_| item_3_4.clone())), cx); cx.focus(left_pane.clone()); }); // When closing all of the items in the left pane, we should be prompted twice: // once for project entry 0, and once for project entry 2. After those two // prompts, the task should complete. let close = workspace.update(cx, |workspace, cx| { Pane::close_items(workspace, left_pane.clone(), cx, |_| true) }); cx.foreground().run_until_parked(); left_pane.read_with(cx, |pane, cx| { assert_eq!( pane.active_item().unwrap().project_entry_ids(cx).as_slice(), &[ProjectEntryId::from_proto(0)] ); }); cx.simulate_prompt_answer(window_id, 0); cx.foreground().run_until_parked(); left_pane.read_with(cx, |pane, cx| { assert_eq!( pane.active_item().unwrap().project_entry_ids(cx).as_slice(), &[ProjectEntryId::from_proto(2)] ); }); cx.simulate_prompt_answer(window_id, 0); cx.foreground().run_until_parked(); close.await.unwrap(); left_pane.read_with(cx, |pane, _| { assert_eq!(pane.items_len(), 0); }); } #[gpui::test] async fn test_autosave(deterministic: Arc, cx: &mut gpui::TestAppContext) { deterministic.forbid_parking(); Settings::test_async(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx)); let item = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); item.project_entry_ids = vec![ProjectEntryId::from_proto(1)]; item }); let item_id = item.id(); workspace.update(cx, |workspace, cx| { workspace.add_item(Box::new(item.clone()), cx); }); // Autosave on window change. item.update(cx, |item, cx| { cx.update_global(|settings: &mut Settings, _| { settings.autosave = Autosave::OnWindowChange; }); item.is_dirty = true; }); // Deactivating the window saves the file. cx.simulate_window_activation(None); deterministic.run_until_parked(); item.read_with(cx, |item, _| assert_eq!(item.save_count, 1)); // Autosave on focus change. item.update(cx, |item, cx| { cx.focus_self(); cx.update_global(|settings: &mut Settings, _| { settings.autosave = Autosave::OnFocusChange; }); item.is_dirty = true; }); // Blurring the item saves the file. item.update(cx, |_, cx| cx.blur()); deterministic.run_until_parked(); item.read_with(cx, |item, _| assert_eq!(item.save_count, 2)); // Deactivating the window still saves the file. cx.simulate_window_activation(Some(window_id)); item.update(cx, |item, cx| { cx.focus_self(); item.is_dirty = true; }); cx.simulate_window_activation(None); deterministic.run_until_parked(); item.read_with(cx, |item, _| assert_eq!(item.save_count, 3)); // Autosave after delay. item.update(cx, |item, cx| { cx.update_global(|settings: &mut Settings, _| { settings.autosave = Autosave::AfterDelay { milliseconds: 500 }; }); item.is_dirty = true; cx.emit(TestItemEvent::Edit); }); // Delay hasn't fully expired, so the file is still dirty and unsaved. deterministic.advance_clock(Duration::from_millis(250)); item.read_with(cx, |item, _| assert_eq!(item.save_count, 3)); // After delay expires, the file is saved. deterministic.advance_clock(Duration::from_millis(250)); item.read_with(cx, |item, _| assert_eq!(item.save_count, 4)); // Autosave on focus change, ensuring closing the tab counts as such. item.update(cx, |item, cx| { cx.update_global(|settings: &mut Settings, _| { settings.autosave = Autosave::OnFocusChange; }); item.is_dirty = true; }); workspace .update(cx, |workspace, cx| { let pane = workspace.active_pane().clone(); Pane::close_items(workspace, pane, cx, move |id| id == item_id) }) .await .unwrap(); assert!(!cx.has_pending_prompt(window_id)); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. workspace.update(cx, |workspace, cx| { workspace.add_item(Box::new(item.clone()), cx); }); item.update(cx, |item, cx| { item.project_entry_ids = Default::default(); item.is_dirty = true; cx.blur(); }); deterministic.run_until_parked(); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); // Ensure autosave is prevented for deleted files also when closing the buffer. let _close_items = workspace.update(cx, |workspace, cx| { let pane = workspace.active_pane().clone(); Pane::close_items(workspace, pane, cx, move |id| id == item_id) }); deterministic.run_until_parked(); assert!(cx.has_pending_prompt(window_id)); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); } #[gpui::test] async fn test_pane_navigation( deterministic: Arc, cx: &mut gpui::TestAppContext, ) { deterministic.forbid_parking(); Settings::test_async(cx); let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; let (_, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx)); let item = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); item.project_entry_ids = vec![ProjectEntryId::from_proto(1)]; item }); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone()); let toolbar_notify_count = Rc::new(RefCell::new(0)); workspace.update(cx, |workspace, cx| { workspace.add_item(Box::new(item.clone()), cx); let toolbar_notification_count = toolbar_notify_count.clone(); cx.observe(&toolbar, move |_, _, _| { *toolbar_notification_count.borrow_mut() += 1 }) .detach(); }); pane.read_with(cx, |pane, _| { assert!(!pane.can_navigate_backward()); assert!(!pane.can_navigate_forward()); }); item.update(cx, |item, cx| { item.set_state("one".to_string(), cx); }); // Toolbar must be notified to re-render the navigation buttons assert_eq!(*toolbar_notify_count.borrow(), 1); pane.read_with(cx, |pane, _| { assert!(pane.can_navigate_backward()); assert!(!pane.can_navigate_forward()); }); workspace .update(cx, |workspace, cx| { Pane::go_back(workspace, Some(pane.clone()), cx) }) .await; assert_eq!(*toolbar_notify_count.borrow(), 3); pane.read_with(cx, |pane, _| { assert!(!pane.can_navigate_backward()); assert!(pane.can_navigate_forward()); }); } pub struct TestItem { state: String, pub label: String, save_count: usize, save_as_count: usize, reload_count: usize, is_dirty: bool, is_singleton: bool, has_conflict: bool, project_entry_ids: Vec, project_path: Option, nav_history: Option, tab_descriptions: Option>, tab_detail: Cell>, } pub enum TestItemEvent { Edit, } impl Clone for TestItem { fn clone(&self) -> Self { Self { state: self.state.clone(), label: self.label.clone(), save_count: self.save_count, save_as_count: self.save_as_count, reload_count: self.reload_count, is_dirty: self.is_dirty, is_singleton: self.is_singleton, has_conflict: self.has_conflict, project_entry_ids: self.project_entry_ids.clone(), project_path: self.project_path.clone(), nav_history: None, tab_descriptions: None, tab_detail: Default::default(), } } } impl TestItem { pub fn new() -> Self { Self { state: String::new(), label: String::new(), save_count: 0, save_as_count: 0, reload_count: 0, is_dirty: false, has_conflict: false, project_entry_ids: Vec::new(), project_path: None, is_singleton: true, nav_history: None, tab_descriptions: None, tab_detail: Default::default(), } } pub fn with_label(mut self, state: &str) -> Self { self.label = state.to_string(); self } pub fn with_singleton(mut self, singleton: bool) -> Self { self.is_singleton = singleton; self } pub fn with_project_entry_ids(mut self, project_entry_ids: &[u64]) -> Self { self.project_entry_ids.extend( project_entry_ids .iter() .copied() .map(ProjectEntryId::from_proto), ); self } fn set_state(&mut self, state: String, cx: &mut ViewContext) { self.push_to_nav_history(cx); self.state = state; } fn push_to_nav_history(&mut self, cx: &mut ViewContext) { if let Some(history) = &mut self.nav_history { history.push(Some(Box::new(self.state.clone())), cx); } } } impl Entity for TestItem { type Event = TestItemEvent; } impl View for TestItem { fn ui_name() -> &'static str { "TestItem" } fn render(&mut self, _: &mut RenderContext) -> ElementBox { Empty::new().boxed() } } impl Item for TestItem { fn tab_description<'a>(&'a self, detail: usize, _: &'a AppContext) -> Option> { self.tab_descriptions.as_ref().and_then(|descriptions| { let description = *descriptions.get(detail).or_else(|| descriptions.last())?; Some(description.into()) }) } fn tab_content(&self, detail: Option, _: &theme::Tab, _: &AppContext) -> ElementBox { self.tab_detail.set(detail); Empty::new().boxed() } fn project_path(&self, _: &AppContext) -> Option { self.project_path.clone() } fn project_entry_ids(&self, _: &AppContext) -> SmallVec<[ProjectEntryId; 3]> { self.project_entry_ids.iter().copied().collect() } fn is_singleton(&self, _: &AppContext) -> bool { self.is_singleton } fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { self.nav_history = Some(history); } fn navigate(&mut self, state: Box, _: &mut ViewContext) -> bool { let state = *state.downcast::().unwrap_or_default(); if state != self.state { self.state = state; true } else { false } } fn deactivated(&mut self, cx: &mut ViewContext) { self.push_to_nav_history(cx); } fn clone_on_split(&self, _: &mut ViewContext) -> Option where Self: Sized, { Some(self.clone()) } fn is_dirty(&self, _: &AppContext) -> bool { self.is_dirty } fn has_conflict(&self, _: &AppContext) -> bool { self.has_conflict } fn can_save(&self, _: &AppContext) -> bool { !self.project_entry_ids.is_empty() } fn save( &mut self, _: ModelHandle, _: &mut ViewContext, ) -> Task> { self.save_count += 1; self.is_dirty = false; Task::ready(Ok(())) } fn save_as( &mut self, _: ModelHandle, _: std::path::PathBuf, _: &mut ViewContext, ) -> Task> { self.save_as_count += 1; self.is_dirty = false; Task::ready(Ok(())) } fn reload( &mut self, _: ModelHandle, _: &mut ViewContext, ) -> Task> { self.reload_count += 1; self.is_dirty = false; Task::ready(Ok(())) } fn to_item_events(_: &Self::Event) -> Vec { vec![ItemEvent::UpdateTab, ItemEvent::Edit] } } impl SidebarItem for TestItem {} }