Merge branch 'main' into multibuffer-following
This commit is contained in:
commit
f797dfb88f
175 changed files with 16722 additions and 8699 deletions
|
@ -98,14 +98,14 @@ pub fn icon_for_dock_anchor(anchor: DockAnchor) -> &'static str {
|
|||
}
|
||||
|
||||
impl DockPosition {
|
||||
fn is_visible(&self) -> bool {
|
||||
pub fn is_visible(&self) -> bool {
|
||||
match self {
|
||||
DockPosition::Shown(_) => true,
|
||||
DockPosition::Hidden(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn anchor(&self) -> DockAnchor {
|
||||
pub fn anchor(&self) -> DockAnchor {
|
||||
match self {
|
||||
DockPosition::Shown(anchor) | DockPosition::Hidden(anchor) => *anchor,
|
||||
}
|
||||
|
@ -126,20 +126,24 @@ impl DockPosition {
|
|||
}
|
||||
}
|
||||
|
||||
pub type DefaultItemFactory =
|
||||
fn(&mut Workspace, &mut ViewContext<Workspace>) -> Box<dyn ItemHandle>;
|
||||
pub type DockDefaultItemFactory =
|
||||
fn(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Box<dyn ItemHandle>>;
|
||||
|
||||
pub struct Dock {
|
||||
position: DockPosition,
|
||||
panel_sizes: HashMap<DockAnchor, f32>,
|
||||
pane: ViewHandle<Pane>,
|
||||
default_item_factory: DefaultItemFactory,
|
||||
default_item_factory: DockDefaultItemFactory,
|
||||
}
|
||||
|
||||
impl Dock {
|
||||
pub fn new(cx: &mut ViewContext<Workspace>, default_item_factory: DefaultItemFactory) -> Self {
|
||||
let anchor = cx.global::<Settings>().default_dock_anchor;
|
||||
let pane = cx.add_view(|cx| Pane::new(Some(anchor), cx));
|
||||
pub fn new(
|
||||
default_item_factory: DockDefaultItemFactory,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Self {
|
||||
let position = DockPosition::Hidden(cx.global::<Settings>().default_dock_anchor);
|
||||
|
||||
let pane = cx.add_view(|cx| Pane::new(Some(position.anchor()), cx));
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.set_active(false, cx);
|
||||
});
|
||||
|
@ -152,7 +156,7 @@ impl Dock {
|
|||
Self {
|
||||
pane,
|
||||
panel_sizes: Default::default(),
|
||||
position: DockPosition::Hidden(anchor),
|
||||
position,
|
||||
default_item_factory,
|
||||
}
|
||||
}
|
||||
|
@ -169,7 +173,7 @@ impl Dock {
|
|||
self.position.is_visible() && self.position.anchor() == anchor
|
||||
}
|
||||
|
||||
fn set_dock_position(
|
||||
pub(crate) fn set_dock_position(
|
||||
workspace: &mut Workspace,
|
||||
new_position: DockPosition,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
|
@ -191,9 +195,11 @@ impl Dock {
|
|||
// Ensure that the pane has at least one item or construct a default item to put in it
|
||||
let pane = workspace.dock.pane.clone();
|
||||
if pane.read(cx).items().next().is_none() {
|
||||
let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
|
||||
// Adding the item focuses the pane by default
|
||||
Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
|
||||
if let Some(item_to_add) = (workspace.dock.default_item_factory)(workspace, cx) {
|
||||
Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
|
||||
} else {
|
||||
workspace.dock.position = workspace.dock.position.hide();
|
||||
}
|
||||
} else {
|
||||
cx.focus(pane);
|
||||
}
|
||||
|
@ -205,6 +211,7 @@ impl Dock {
|
|||
cx.focus(last_active_center_pane);
|
||||
}
|
||||
cx.emit(crate::Event::DockAnchorChanged);
|
||||
workspace.serialize_workspace(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -341,6 +348,10 @@ impl Dock {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn position(&self) -> DockPosition {
|
||||
self.position
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ToggleDockButton {
|
||||
|
@ -447,20 +458,77 @@ impl StatusItemView for ToggleDockButton {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use gpui::{AppContext, TestAppContext, UpdateView, ViewContext};
|
||||
use project::{FakeFs, Project};
|
||||
use settings::Settings;
|
||||
|
||||
use super::*;
|
||||
use crate::{sidebar::Sidebar, tests::TestItem, ItemHandle, Workspace};
|
||||
use crate::{
|
||||
dock,
|
||||
item::test::TestItem,
|
||||
persistence::model::{
|
||||
SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
|
||||
},
|
||||
register_deserializable_item,
|
||||
sidebar::Sidebar,
|
||||
ItemHandle, Workspace,
|
||||
};
|
||||
|
||||
pub fn default_item_factory(
|
||||
_workspace: &mut Workspace,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Box<dyn ItemHandle> {
|
||||
Box::new(cx.add_view(|_| TestItem::new()))
|
||||
) -> Option<Box<dyn ItemHandle>> {
|
||||
Some(Box::new(cx.add_view(|_| TestItem::new())))
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dock_workspace_infinite_loop(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
Settings::test_async(cx);
|
||||
|
||||
cx.update(|cx| {
|
||||
register_deserializable_item::<TestItem>(cx);
|
||||
});
|
||||
|
||||
let serialized_workspace = SerializedWorkspace {
|
||||
id: 0,
|
||||
location: Vec::<PathBuf>::new().into(),
|
||||
dock_position: dock::DockPosition::Shown(DockAnchor::Expanded),
|
||||
center_group: SerializedPaneGroup::Pane(SerializedPane {
|
||||
active: false,
|
||||
children: vec![],
|
||||
}),
|
||||
dock_pane: SerializedPane {
|
||||
active: true,
|
||||
children: vec![SerializedItem {
|
||||
active: true,
|
||||
item_id: 0,
|
||||
kind: "test".into(),
|
||||
}],
|
||||
},
|
||||
left_sidebar_open: false,
|
||||
};
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
|
||||
let (_, _workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Some(serialized_workspace),
|
||||
0,
|
||||
project.clone(),
|
||||
default_item_factory,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
//Should terminate
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -568,8 +636,9 @@ mod tests {
|
|||
|
||||
cx.update(|cx| init(cx));
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (window_id, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
|
||||
let (window_id, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, default_item_factory, cx)
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let left_panel = cx.add_view(|_| TestItem::new());
|
||||
|
|
918
crates/workspace/src/item.rs
Normal file
918
crates/workspace/src/item.rs
Normal file
|
@ -0,0 +1,918 @@
|
|||
use std::{
|
||||
any::{Any, TypeId},
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
fmt,
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use client::proto;
|
||||
use gpui::{
|
||||
AnyViewHandle, AppContext, ElementBox, ModelHandle, MutableAppContext, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use settings::{Autosave, Settings};
|
||||
use smallvec::SmallVec;
|
||||
use theme::Theme;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{
|
||||
pane, persistence::model::ItemId, searchable::SearchableItemHandle, DelayedDebouncedEditAction,
|
||||
FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
#[derive(Eq, PartialEq, Hash)]
|
||||
pub enum ItemEvent {
|
||||
CloseItem,
|
||||
UpdateTab,
|
||||
UpdateBreadcrumbs,
|
||||
Edit,
|
||||
}
|
||||
|
||||
pub trait Item: View {
|
||||
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
|
||||
fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
|
||||
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
|
||||
false
|
||||
}
|
||||
fn tab_description<'a>(&'a self, _: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
|
||||
None
|
||||
}
|
||||
fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
|
||||
-> ElementBox;
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||
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<Self>);
|
||||
fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext<Self>) -> Option<Self>
|
||||
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<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
fn save_as(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
abs_path: PathBuf,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
fn git_diff_recalc(
|
||||
&mut self,
|
||||
_project: ModelHandle<Project>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent>;
|
||||
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<Self>,
|
||||
_: &AppContext,
|
||||
) -> Option<AnyViewHandle> {
|
||||
if TypeId::of::<Self>() == type_id {
|
||||
Some(self_handle.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option<Vec<ElementBox>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str>;
|
||||
|
||||
fn deserialize(
|
||||
project: ModelHandle<Project>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
workspace_id: WorkspaceId,
|
||||
item_id: ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<Result<ViewHandle<Self>>>;
|
||||
}
|
||||
|
||||
pub trait ItemHandle: 'static + fmt::Debug {
|
||||
fn subscribe_to_item_events(
|
||||
&self,
|
||||
cx: &mut MutableAppContext,
|
||||
handler: Box<dyn Fn(ItemEvent, &mut MutableAppContext)>,
|
||||
) -> gpui::Subscription;
|
||||
fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>>;
|
||||
fn tab_content(&self, detail: Option<usize>, style: &theme::Tab, cx: &AppContext)
|
||||
-> ElementBox;
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
|
||||
fn is_singleton(&self, cx: &AppContext) -> bool;
|
||||
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
workspace_id: WorkspaceId,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Box<dyn ItemHandle>>;
|
||||
fn added_to_pane(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
pane: ViewHandle<Pane>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
);
|
||||
fn deactivated(&self, cx: &mut MutableAppContext);
|
||||
fn workspace_deactivated(&self, cx: &mut MutableAppContext);
|
||||
fn navigate(&self, data: Box<dyn Any>, 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<Project>, cx: &mut MutableAppContext) -> Task<Result<()>>;
|
||||
fn save_as(
|
||||
&self,
|
||||
project: ModelHandle<Project>,
|
||||
abs_path: PathBuf,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<()>>;
|
||||
fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
|
||||
-> Task<Result<()>>;
|
||||
fn git_diff_recalc(
|
||||
&self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<()>>;
|
||||
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
|
||||
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
|
||||
fn on_release(
|
||||
&self,
|
||||
cx: &mut MutableAppContext,
|
||||
callback: Box<dyn FnOnce(&mut MutableAppContext)>,
|
||||
) -> gpui::Subscription;
|
||||
fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
|
||||
fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
|
||||
fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<ElementBox>>;
|
||||
fn serialized_item_kind(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
pub trait WeakItemHandle {
|
||||
fn id(&self) -> usize;
|
||||
fn window_id(&self) -> usize;
|
||||
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>>;
|
||||
}
|
||||
|
||||
impl dyn ItemHandle {
|
||||
pub fn downcast<T: View>(&self) -> Option<ViewHandle<T>> {
|
||||
self.to_any().downcast()
|
||||
}
|
||||
|
||||
pub fn act_as<T: View>(&self, cx: &AppContext) -> Option<ViewHandle<T>> {
|
||||
self.act_as_type(TypeId::of::<T>(), cx)
|
||||
.and_then(|t| t.downcast())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
fn subscribe_to_item_events(
|
||||
&self,
|
||||
cx: &mut MutableAppContext,
|
||||
handler: Box<dyn Fn(ItemEvent, &mut MutableAppContext)>,
|
||||
) -> 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<Cow<'a, str>> {
|
||||
self.read(cx).tab_description(detail, cx)
|
||||
}
|
||||
|
||||
fn tab_content(
|
||||
&self,
|
||||
detail: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
self.read(cx).tab_content(detail, style, cx)
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
||||
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<dyn ItemHandle> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
workspace_id: WorkspaceId,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Box<dyn ItemHandle>> {
|
||||
self.update(cx, |item, cx| {
|
||||
cx.add_option_view(|cx| item.clone_on_split(workspace_id, cx))
|
||||
})
|
||||
.map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
|
||||
}
|
||||
|
||||
fn added_to_pane(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
pane: ViewHandle<Pane>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let history = pane.read(cx).nav_history_for_item(self);
|
||||
self.update(cx, |this, cx| {
|
||||
this.set_nav_history(history, cx);
|
||||
this.added_to_workspace(workspace, 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(Ordering::SeqCst)
|
||||
{
|
||||
pending_update_scheduled.store(true, Ordering::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, Ordering::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::<Settings>().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::<Settings>();
|
||||
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::<Settings>().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();
|
||||
}
|
||||
|
||||
cx.defer(|workspace, cx| {
|
||||
workspace.serialize_workspace(cx);
|
||||
});
|
||||
}
|
||||
|
||||
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<dyn Any>, 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<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
|
||||
self.update(cx, |item, cx| item.save(project, cx))
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&self,
|
||||
project: ModelHandle<Project>,
|
||||
abs_path: PathBuf,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<()>> {
|
||||
self.update(cx, |item, cx| item.reload(project, cx))
|
||||
}
|
||||
|
||||
fn git_diff_recalc(
|
||||
&self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<()>> {
|
||||
self.update(cx, |item, cx| item.git_diff_recalc(project, cx))
|
||||
}
|
||||
|
||||
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
|
||||
self.read(cx).act_as_type(type_id, self, cx)
|
||||
}
|
||||
|
||||
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>> {
|
||||
if cx.has_global::<FollowableItemBuilders>() {
|
||||
let builders = cx.global::<FollowableItemBuilders>();
|
||||
let item = self.to_any();
|
||||
Some(builders.get(&item.view_type())?.1(item))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn on_release(
|
||||
&self,
|
||||
cx: &mut MutableAppContext,
|
||||
callback: Box<dyn FnOnce(&mut MutableAppContext)>,
|
||||
) -> gpui::Subscription {
|
||||
cx.observe_release(self, move |_, cx| callback(cx))
|
||||
}
|
||||
|
||||
fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
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<Vec<ElementBox>> {
|
||||
self.read(cx).breadcrumbs(theme, cx)
|
||||
}
|
||||
|
||||
fn serialized_item_kind(&self) -> Option<&'static str> {
|
||||
T::serialized_item_kind()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn ItemHandle>> for AnyViewHandle {
|
||||
fn from(val: Box<dyn ItemHandle>) -> Self {
|
||||
val.to_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Box<dyn ItemHandle>> for AnyViewHandle {
|
||||
fn from(val: &Box<dyn ItemHandle>) -> Self {
|
||||
val.to_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Box<dyn ItemHandle> {
|
||||
fn clone(&self) -> Box<dyn ItemHandle> {
|
||||
self.boxed_clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
|
||||
fn id(&self) -> usize {
|
||||
self.id()
|
||||
}
|
||||
|
||||
fn window_id(&self) -> usize {
|
||||
self.window_id()
|
||||
}
|
||||
|
||||
fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
|
||||
self.upgrade(cx).map(|v| Box::new(v) as Box<dyn ItemHandle>)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ProjectItem: Item {
|
||||
type Item: project::Item;
|
||||
|
||||
fn for_project_item(
|
||||
project: ModelHandle<Project>,
|
||||
item: ModelHandle<Self::Item>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self;
|
||||
}
|
||||
|
||||
pub trait FollowableItem: Item {
|
||||
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
|
||||
fn from_state_proto(
|
||||
pane: ViewHandle<Pane>,
|
||||
project: ModelHandle<Project>,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Task<Result<ViewHandle<Self>>>>;
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
event: &Self::Event,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
cx: &AppContext,
|
||||
) -> bool;
|
||||
fn apply_update_proto(
|
||||
&mut self,
|
||||
project: &ModelHandle<Project>,
|
||||
message: proto::update_view::Variant,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
|
||||
fn set_leader_replica_id(&mut self, leader_replica_id: Option<u16>, cx: &mut ViewContext<Self>);
|
||||
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<u16>, cx: &mut MutableAppContext);
|
||||
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
event: &dyn Any,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
cx: &AppContext,
|
||||
) -> bool;
|
||||
fn apply_update_proto(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
message: proto::update_view::Variant,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<()>>;
|
||||
fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool;
|
||||
}
|
||||
|
||||
impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
|
||||
fn set_leader_replica_id(&self, leader_replica_id: Option<u16>, 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<proto::view::Variant> {
|
||||
self.read(cx).to_state_proto(cx)
|
||||
}
|
||||
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
event: &dyn Any,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
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,
|
||||
project: &ModelHandle<Project>,
|
||||
message: proto::update_view::Variant,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<()>> {
|
||||
self.update(cx, |this, cx| this.apply_update_proto(project, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use std::{any::Any, borrow::Cow, cell::Cell};
|
||||
|
||||
use gpui::{
|
||||
elements::Empty, AppContext, Element, ElementBox, Entity, ModelHandle, RenderContext, Task,
|
||||
View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{sidebar::SidebarItem, ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
|
||||
|
||||
use super::{Item, ItemEvent};
|
||||
|
||||
pub struct TestItem {
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub state: String,
|
||||
pub label: String,
|
||||
pub save_count: usize,
|
||||
pub save_as_count: usize,
|
||||
pub reload_count: usize,
|
||||
pub is_dirty: bool,
|
||||
pub is_singleton: bool,
|
||||
pub has_conflict: bool,
|
||||
pub project_entry_ids: Vec<ProjectEntryId>,
|
||||
pub project_path: Option<ProjectPath>,
|
||||
pub nav_history: Option<ItemNavHistory>,
|
||||
pub tab_descriptions: Option<Vec<&'static str>>,
|
||||
pub tab_detail: Cell<Option<usize>>,
|
||||
}
|
||||
|
||||
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(),
|
||||
workspace_id: self.workspace_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
workspace_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_deserialized(id: WorkspaceId) -> Self {
|
||||
let mut this = Self::new();
|
||||
this.workspace_id = id;
|
||||
this
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
|
||||
self.push_to_nav_history(cx);
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
|
||||
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<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for TestItem {
|
||||
fn tab_description<'a>(&'a self, detail: usize, _: &'a AppContext) -> Option<Cow<'a, str>> {
|
||||
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<usize>, _: &theme::Tab, _: &AppContext) -> ElementBox {
|
||||
self.tab_detail.set(detail);
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
|
||||
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>) {
|
||||
self.nav_history = Some(history);
|
||||
}
|
||||
|
||||
fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
|
||||
let state = *state.downcast::<String>().unwrap_or_default();
|
||||
if state != self.state {
|
||||
self.state = state;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.push_to_nav_history(cx);
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: WorkspaceId,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Option<Self>
|
||||
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<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.save_count += 1;
|
||||
self.is_dirty = false;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: std::path::PathBuf,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.save_as_count += 1;
|
||||
self.is_dirty = false;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.reload_count += 1;
|
||||
self.is_dirty = false;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn to_item_events(_: &Self::Event) -> Vec<ItemEvent> {
|
||||
vec![ItemEvent::UpdateTab, ItemEvent::Edit]
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
_project: ModelHandle<Project>,
|
||||
_workspace: WeakViewHandle<Workspace>,
|
||||
workspace_id: WorkspaceId,
|
||||
_item_id: ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<anyhow::Result<ViewHandle<Self>>> {
|
||||
let view = cx.add_view(|_cx| Self::new_deserialized(workspace_id));
|
||||
Task::Ready(Some(anyhow::Ok(view)))
|
||||
}
|
||||
}
|
||||
|
||||
impl SidebarItem for TestItem {}
|
||||
}
|
334
crates/workspace/src/notifications.rs
Normal file
334
crates/workspace/src/notifications.rs
Normal file
|
@ -0,0 +1,334 @@
|
|||
use std::{any::TypeId, ops::DerefMut};
|
||||
|
||||
use collections::HashSet;
|
||||
use gpui::{AnyViewHandle, Entity, MutableAppContext, View, ViewContext, ViewHandle};
|
||||
|
||||
use crate::Workspace;
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.set_global(NotificationTracker::new());
|
||||
simple_message_notification::init(cx);
|
||||
}
|
||||
|
||||
pub trait Notification: View {
|
||||
fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
|
||||
}
|
||||
|
||||
pub trait NotificationHandle {
|
||||
fn id(&self) -> usize;
|
||||
fn to_any(&self) -> AnyViewHandle;
|
||||
}
|
||||
|
||||
impl<T: Notification> NotificationHandle for ViewHandle<T> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationTracker {
|
||||
notifications_sent: HashSet<TypeId>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for NotificationTracker {
|
||||
type Target = HashSet<TypeId>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.notifications_sent
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for NotificationTracker {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.notifications_sent
|
||||
}
|
||||
}
|
||||
|
||||
impl NotificationTracker {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
notifications_sent: HashSet::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn show_notification_once<V: Notification>(
|
||||
&mut self,
|
||||
id: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
|
||||
) {
|
||||
if !cx
|
||||
.global::<NotificationTracker>()
|
||||
.contains(&TypeId::of::<V>())
|
||||
{
|
||||
cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
|
||||
tracker.insert(TypeId::of::<V>())
|
||||
});
|
||||
|
||||
self.show_notification::<V>(id, cx, build_notification)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_notification<V: Notification>(
|
||||
&mut self,
|
||||
id: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
|
||||
) {
|
||||
let type_id = TypeId::of::<V>();
|
||||
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>) {
|
||||
self.notifications
|
||||
.retain(|(existing_type_id, existing_id, _)| {
|
||||
if (*existing_type_id, *existing_id) == (type_id, id) {
|
||||
cx.notify();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub mod simple_message_notification {
|
||||
use std::process::Command;
|
||||
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
|
||||
impl_actions, Action, CursorStyle, Element, Entity, MouseButton, MutableAppContext, View,
|
||||
ViewContext,
|
||||
};
|
||||
use menu::Cancel;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
|
||||
use crate::Workspace;
|
||||
|
||||
use super::Notification;
|
||||
|
||||
actions!(message_notifications, [CancelMessageNotification]);
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct OsOpen(pub String);
|
||||
|
||||
impl_actions!(message_notifications, [OsOpen]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(MessageNotification::dismiss);
|
||||
cx.add_action(
|
||||
|_workspace: &mut Workspace, open_action: &OsOpen, _cx: &mut ViewContext<Workspace>| {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut command = Command::new("open");
|
||||
command.arg(open_action.0.clone());
|
||||
|
||||
command.spawn().ok();
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub struct MessageNotification {
|
||||
message: String,
|
||||
click_action: Option<Box<dyn Action>>,
|
||||
click_message: Option<String>,
|
||||
}
|
||||
|
||||
pub enum MessageNotificationEvent {
|
||||
Dismiss,
|
||||
}
|
||||
|
||||
impl Entity for MessageNotification {
|
||||
type Event = MessageNotificationEvent;
|
||||
}
|
||||
|
||||
impl MessageNotification {
|
||||
pub fn new_messsage<S: AsRef<str>>(message: S) -> MessageNotification {
|
||||
Self {
|
||||
message: message.as_ref().to_string(),
|
||||
click_action: None,
|
||||
click_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new<S1: AsRef<str>, A: Action, S2: AsRef<str>>(
|
||||
message: S1,
|
||||
click_action: A,
|
||||
click_message: S2,
|
||||
) -> Self {
|
||||
Self {
|
||||
message: message.as_ref().to_string(),
|
||||
click_action: Some(Box::new(click_action) as Box<dyn Action>),
|
||||
click_message: Some(click_message.as_ref().to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(MessageNotificationEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
impl View for MessageNotification {
|
||||
fn ui_name() -> &'static str {
|
||||
"MessageNotification"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let theme = &theme.update_notification;
|
||||
|
||||
enum MessageNotificationTag {}
|
||||
|
||||
let click_action = self
|
||||
.click_action
|
||||
.as_ref()
|
||||
.map(|action| action.boxed_clone());
|
||||
let click_message = self.click_message.as_ref().map(|message| message.clone());
|
||||
let message = self.message.clone();
|
||||
|
||||
MouseEventHandler::<MessageNotificationTag>::new(0, cx, |state, cx| {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Text::new(message, theme.message.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.message.container)
|
||||
.aligned()
|
||||
.top()
|
||||
.left()
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Cancel>::new(0, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state, false);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.boxed()
|
||||
})
|
||||
.with_padding(Padding::uniform(5.))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(CancelMessageNotification)
|
||||
})
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_height(
|
||||
cx.font_cache().line_height(theme.message.text.font_size),
|
||||
)
|
||||
.aligned()
|
||||
.top()
|
||||
.flex_float()
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children({
|
||||
let style = theme.action_message.style_for(state, false);
|
||||
if let Some(click_message) = click_message {
|
||||
Some(
|
||||
Text::new(click_message, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.into_iter()
|
||||
})
|
||||
.contained()
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
if let Some(click_action) = click_action.as_ref() {
|
||||
cx.dispatch_any_action(click_action.boxed_clone())
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Notification for MessageNotification {
|
||||
fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
|
||||
match event {
|
||||
MessageNotificationEvent::Dismiss => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NotifyResultExt {
|
||||
type Ok;
|
||||
|
||||
fn notify_err(
|
||||
self,
|
||||
workspace: &mut Workspace,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Option<Self::Ok>;
|
||||
}
|
||||
|
||||
impl<T, E> NotifyResultExt for Result<T, E>
|
||||
where
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
type Ok = T;
|
||||
|
||||
fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
|
||||
match self {
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => {
|
||||
workspace.show_notification(0, cx, |cx| {
|
||||
cx.add_view(|_cx| {
|
||||
simple_message_notification::MessageNotification::new_messsage(format!(
|
||||
"Error: {:?}",
|
||||
err,
|
||||
))
|
||||
})
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,8 +3,9 @@ mod dragged_item_receiver;
|
|||
use super::{ItemHandle, SplitDirection};
|
||||
use crate::{
|
||||
dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, ExpandDock, HideDock},
|
||||
item::WeakItemHandle,
|
||||
toolbar::Toolbar,
|
||||
Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace,
|
||||
Item, NewFile, NewSearch, NewTerminal, Workspace,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
|
@ -1634,7 +1635,7 @@ mod tests {
|
|||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::tests::TestItem;
|
||||
use crate::item::test::TestItem;
|
||||
use gpui::{executor::Deterministic, TestAppContext};
|
||||
use project::FakeFs;
|
||||
|
||||
|
@ -1645,8 +1646,9 @@ mod tests {
|
|||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// 1. Add with a destination index
|
||||
|
@ -1734,8 +1736,9 @@ mod tests {
|
|||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// 1. Add with a destination index
|
||||
|
@ -1811,8 +1814,9 @@ mod tests {
|
|||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// singleton view
|
||||
|
@ -1922,7 +1926,7 @@ mod tests {
|
|||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (_, workspace) =
|
||||
cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
|
||||
cx.add_window(|cx| Workspace::new(None, 0, project, |_, _| unimplemented!(), cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
add_labled_item(&workspace, &pane, "A", cx);
|
||||
|
|
|
@ -13,10 +13,14 @@ use theme::Theme;
|
|||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PaneGroup {
|
||||
root: Member,
|
||||
pub(crate) root: Member,
|
||||
}
|
||||
|
||||
impl PaneGroup {
|
||||
pub(crate) fn with_root(root: Member) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
|
||||
pub fn new(pane: ViewHandle<Pane>) -> Self {
|
||||
Self {
|
||||
root: Member::Pane(pane),
|
||||
|
@ -85,7 +89,7 @@ impl PaneGroup {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum Member {
|
||||
pub(crate) enum Member {
|
||||
Axis(PaneAxis),
|
||||
Pane(ViewHandle<Pane>),
|
||||
}
|
||||
|
@ -276,9 +280,9 @@ impl Member {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct PaneAxis {
|
||||
axis: Axis,
|
||||
members: Vec<Member>,
|
||||
pub(crate) struct PaneAxis {
|
||||
pub axis: Axis,
|
||||
pub members: Vec<Member>,
|
||||
}
|
||||
|
||||
impl PaneAxis {
|
||||
|
|
836
crates/workspace/src/persistence.rs
Normal file
836
crates/workspace/src/persistence.rs
Normal file
|
@ -0,0 +1,836 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
pub mod model;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
|
||||
use gpui::Axis;
|
||||
|
||||
use util::{iife, unzip_option, ResultExt};
|
||||
|
||||
use crate::dock::DockPosition;
|
||||
use crate::WorkspaceId;
|
||||
|
||||
use model::{
|
||||
GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
|
||||
WorkspaceLocation,
|
||||
};
|
||||
|
||||
define_connection! {
|
||||
pub static ref DB: WorkspaceDb<()> =
|
||||
&[sql!(
|
||||
CREATE TABLE workspaces(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
workspace_location BLOB UNIQUE,
|
||||
dock_visible INTEGER, // Boolean
|
||||
dock_anchor TEXT, // Enum: 'Bottom' / 'Right' / 'Expanded'
|
||||
dock_pane INTEGER, // NULL indicates that we don't have a dock pane yet
|
||||
left_sidebar_open INTEGER, //Boolean
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE pane_groups(
|
||||
group_id INTEGER PRIMARY KEY,
|
||||
workspace_id INTEGER NOT NULL,
|
||||
parent_group_id INTEGER, // NULL indicates that this is a root node
|
||||
position INTEGER, // NULL indicates that this is a root node
|
||||
axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE,
|
||||
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE panes(
|
||||
pane_id INTEGER PRIMARY KEY,
|
||||
workspace_id INTEGER NOT NULL,
|
||||
active INTEGER NOT NULL, // Boolean
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE center_panes(
|
||||
pane_id INTEGER PRIMARY KEY,
|
||||
parent_group_id INTEGER, // NULL means that this is a root pane
|
||||
position INTEGER, // NULL means that this is a root pane
|
||||
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE items(
|
||||
item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
|
||||
workspace_id INTEGER NOT NULL,
|
||||
pane_id INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
active INTEGER NOT NULL,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE,
|
||||
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
|
||||
ON DELETE CASCADE,
|
||||
PRIMARY KEY(item_id, workspace_id)
|
||||
) STRICT;
|
||||
)];
|
||||
}
|
||||
|
||||
impl WorkspaceDb {
|
||||
/// Returns a serialized workspace for the given worktree_roots. If the passed array
|
||||
/// is empty, the most recent workspace is returned instead. If no workspace for the
|
||||
/// passed roots is stored, returns none.
|
||||
pub fn workspace_for_roots<P: AsRef<Path>>(
|
||||
&self,
|
||||
worktree_roots: &[P],
|
||||
) -> Option<SerializedWorkspace> {
|
||||
let workspace_location: WorkspaceLocation = worktree_roots.into();
|
||||
|
||||
// Note that we re-assign the workspace_id here in case it's empty
|
||||
// and we've grabbed the most recent workspace
|
||||
let (workspace_id, workspace_location, left_sidebar_open, dock_position): (
|
||||
WorkspaceId,
|
||||
WorkspaceLocation,
|
||||
bool,
|
||||
DockPosition,
|
||||
) = iife!({
|
||||
if worktree_roots.len() == 0 {
|
||||
self.select_row(sql!(
|
||||
SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
|
||||
FROM workspaces
|
||||
ORDER BY timestamp DESC LIMIT 1))?()?
|
||||
} else {
|
||||
self.select_row_bound(sql!(
|
||||
SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
|
||||
FROM workspaces
|
||||
WHERE workspace_location = ?))?(&workspace_location)?
|
||||
}
|
||||
.context("No workspaces found")
|
||||
})
|
||||
.warn_on_err()
|
||||
.flatten()?;
|
||||
|
||||
Some(SerializedWorkspace {
|
||||
id: workspace_id,
|
||||
location: workspace_location.clone(),
|
||||
dock_pane: self
|
||||
.get_dock_pane(workspace_id)
|
||||
.context("Getting dock pane")
|
||||
.log_err()?,
|
||||
center_group: self
|
||||
.get_center_pane_group(workspace_id)
|
||||
.context("Getting center group")
|
||||
.log_err()?,
|
||||
dock_position,
|
||||
left_sidebar_open
|
||||
})
|
||||
}
|
||||
|
||||
/// Saves a workspace using the worktree roots. Will garbage collect any workspaces
|
||||
/// that used this workspace previously
|
||||
pub async fn save_workspace(&self, workspace: SerializedWorkspace) {
|
||||
self.write(move |conn| {
|
||||
conn.with_savepoint("update_worktrees", || {
|
||||
// Clear out panes and pane_groups
|
||||
conn.exec_bound(sql!(
|
||||
UPDATE workspaces SET dock_pane = NULL WHERE workspace_id = ?1;
|
||||
DELETE FROM pane_groups WHERE workspace_id = ?1;
|
||||
DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
|
||||
.expect("Clearing old panes");
|
||||
|
||||
conn.exec_bound(sql!(
|
||||
DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
|
||||
))?((&workspace.location, workspace.id.clone()))
|
||||
.context("clearing out old locations")?;
|
||||
|
||||
// Upsert
|
||||
conn.exec_bound(sql!(
|
||||
INSERT INTO workspaces(
|
||||
workspace_id,
|
||||
workspace_location,
|
||||
left_sidebar_open,
|
||||
dock_visible,
|
||||
dock_anchor,
|
||||
timestamp
|
||||
)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT DO
|
||||
UPDATE SET
|
||||
workspace_location = ?2,
|
||||
left_sidebar_open = ?3,
|
||||
dock_visible = ?4,
|
||||
dock_anchor = ?5,
|
||||
timestamp = CURRENT_TIMESTAMP
|
||||
))?((workspace.id, &workspace.location, workspace.left_sidebar_open, workspace.dock_position))
|
||||
.context("Updating workspace")?;
|
||||
|
||||
// Save center pane group and dock pane
|
||||
Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
|
||||
.context("save pane group in save workspace")?;
|
||||
|
||||
let dock_id = Self::save_pane(conn, workspace.id, &workspace.dock_pane, None, true)
|
||||
.context("save pane in save workspace")?;
|
||||
|
||||
// Complete workspace initialization
|
||||
conn.exec_bound(sql!(
|
||||
UPDATE workspaces
|
||||
SET dock_pane = ?
|
||||
WHERE workspace_id = ?
|
||||
))?((dock_id, workspace.id))
|
||||
.context("Finishing initialization with dock pane")?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
query! {
|
||||
pub async fn next_id() -> Result<WorkspaceId> {
|
||||
INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub fn recent_workspaces(limit: usize) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
|
||||
SELECT workspace_id, workspace_location
|
||||
FROM workspaces
|
||||
WHERE workspace_location IS NOT NULL
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
}
|
||||
}
|
||||
|
||||
fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
|
||||
self.get_pane_group(workspace_id, None)?
|
||||
.into_iter()
|
||||
.next()
|
||||
.context("No center pane group")
|
||||
}
|
||||
|
||||
fn get_pane_group(
|
||||
&self,
|
||||
workspace_id: WorkspaceId,
|
||||
group_id: Option<GroupId>,
|
||||
) -> Result<Vec<SerializedPaneGroup>> {
|
||||
type GroupKey = (Option<GroupId>, WorkspaceId);
|
||||
type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
|
||||
self.select_bound::<GroupKey, GroupOrPane>(sql!(
|
||||
SELECT group_id, axis, pane_id, active
|
||||
FROM (SELECT
|
||||
group_id,
|
||||
axis,
|
||||
NULL as pane_id,
|
||||
NULL as active,
|
||||
position,
|
||||
parent_group_id,
|
||||
workspace_id
|
||||
FROM pane_groups
|
||||
UNION
|
||||
SELECT
|
||||
NULL,
|
||||
NULL,
|
||||
center_panes.pane_id,
|
||||
panes.active as active,
|
||||
position,
|
||||
parent_group_id,
|
||||
panes.workspace_id as workspace_id
|
||||
FROM center_panes
|
||||
JOIN panes ON center_panes.pane_id = panes.pane_id)
|
||||
WHERE parent_group_id IS ? AND workspace_id = ?
|
||||
ORDER BY position
|
||||
))?((group_id, workspace_id))?
|
||||
.into_iter()
|
||||
.map(|(group_id, axis, pane_id, active)| {
|
||||
if let Some((group_id, axis)) = group_id.zip(axis) {
|
||||
Ok(SerializedPaneGroup::Group {
|
||||
axis,
|
||||
children: self.get_pane_group(workspace_id, Some(group_id))?,
|
||||
})
|
||||
} else if let Some((pane_id, active)) = pane_id.zip(active) {
|
||||
Ok(SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
self.get_items(pane_id)?,
|
||||
active,
|
||||
)))
|
||||
} else {
|
||||
bail!("Pane Group Child was neither a pane group or a pane");
|
||||
}
|
||||
})
|
||||
// Filter out panes and pane groups which don't have any children or items
|
||||
.filter(|pane_group| match pane_group {
|
||||
Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
|
||||
Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
|
||||
_ => true,
|
||||
})
|
||||
.collect::<Result<_>>()
|
||||
}
|
||||
|
||||
|
||||
fn save_pane_group(
|
||||
conn: &Connection,
|
||||
workspace_id: WorkspaceId,
|
||||
pane_group: &SerializedPaneGroup,
|
||||
parent: Option<(GroupId, usize)>,
|
||||
) -> Result<()> {
|
||||
match pane_group {
|
||||
SerializedPaneGroup::Group { axis, children } => {
|
||||
let (parent_id, position) = unzip_option(parent);
|
||||
|
||||
let group_id = conn.select_row_bound::<_, i64>(sql!(
|
||||
INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis)
|
||||
VALUES (?, ?, ?, ?)
|
||||
RETURNING group_id
|
||||
))?((
|
||||
workspace_id,
|
||||
parent_id,
|
||||
position,
|
||||
*axis,
|
||||
))?
|
||||
.ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
|
||||
|
||||
for (position, group) in children.iter().enumerate() {
|
||||
Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
SerializedPaneGroup::Pane(pane) => {
|
||||
Self::save_pane(conn, workspace_id, &pane, parent, false)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_dock_pane(&self, workspace_id: WorkspaceId) -> Result<SerializedPane> {
|
||||
let (pane_id, active) = self.select_row_bound(sql!(
|
||||
SELECT pane_id, active
|
||||
FROM panes
|
||||
WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?)
|
||||
))?(
|
||||
workspace_id,
|
||||
)?
|
||||
.context("No dock pane for workspace")?;
|
||||
|
||||
Ok(SerializedPane::new(
|
||||
self.get_items(pane_id).context("Reading items")?,
|
||||
active,
|
||||
))
|
||||
}
|
||||
|
||||
fn save_pane(
|
||||
conn: &Connection,
|
||||
workspace_id: WorkspaceId,
|
||||
pane: &SerializedPane,
|
||||
parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
|
||||
dock: bool,
|
||||
) -> Result<PaneId> {
|
||||
let pane_id = conn.select_row_bound::<_, i64>(sql!(
|
||||
INSERT INTO panes(workspace_id, active)
|
||||
VALUES (?, ?)
|
||||
RETURNING pane_id
|
||||
))?((workspace_id, pane.active))?
|
||||
.ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
|
||||
|
||||
if !dock {
|
||||
let (parent_id, order) = unzip_option(parent);
|
||||
conn.exec_bound(sql!(
|
||||
INSERT INTO center_panes(pane_id, parent_group_id, position)
|
||||
VALUES (?, ?, ?)
|
||||
))?((pane_id, parent_id, order))?;
|
||||
}
|
||||
|
||||
Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
|
||||
|
||||
Ok(pane_id)
|
||||
}
|
||||
|
||||
fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
|
||||
Ok(self.select_bound(sql!(
|
||||
SELECT kind, item_id, active FROM items
|
||||
WHERE pane_id = ?
|
||||
ORDER BY position
|
||||
))?(pane_id)?)
|
||||
}
|
||||
|
||||
fn save_items(
|
||||
conn: &Connection,
|
||||
workspace_id: WorkspaceId,
|
||||
pane_id: PaneId,
|
||||
items: &[SerializedItem],
|
||||
) -> Result<()> {
|
||||
let mut insert = conn.exec_bound(sql!(
|
||||
INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?)
|
||||
)).context("Preparing insertion")?;
|
||||
for (position, item) in items.iter().enumerate() {
|
||||
insert((workspace_id, pane_id, position, item))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use db::open_test_db;
|
||||
use settings::DockAnchor;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_next_id_stability() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
|
||||
|
||||
db.write(|conn| {
|
||||
conn.migrate(
|
||||
"test_table",
|
||||
&[sql!(
|
||||
CREATE TABLE test_table(
|
||||
text TEXT,
|
||||
workspace_id INTEGER,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
)],
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.await;
|
||||
|
||||
let id = db.next_id().await.unwrap();
|
||||
// Assert the empty row got inserted
|
||||
assert_eq!(
|
||||
Some(id),
|
||||
db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
|
||||
SELECT workspace_id FROM workspaces WHERE workspace_id = ?
|
||||
))
|
||||
.unwrap()(id)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
db.write(move |conn| {
|
||||
conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
|
||||
.unwrap()(("test-text-1", id))
|
||||
.unwrap()
|
||||
})
|
||||
.await;
|
||||
|
||||
let test_text_1 = db
|
||||
.select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
|
||||
.unwrap()(1)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(test_text_1, "test-text-1");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_workspace_id_stability() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
|
||||
|
||||
db.write(|conn| {
|
||||
conn.migrate(
|
||||
"test_table",
|
||||
&[sql!(
|
||||
CREATE TABLE test_table(
|
||||
text TEXT,
|
||||
workspace_id INTEGER,
|
||||
FOREIGN KEY(workspace_id)
|
||||
REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;)],
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut workspace_1 = SerializedWorkspace {
|
||||
id: 1,
|
||||
location: (["/tmp", "/tmp2"]).into(),
|
||||
dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: true
|
||||
};
|
||||
|
||||
let mut workspace_2 = SerializedWorkspace {
|
||||
id: 2,
|
||||
location: (["/tmp"]).into(),
|
||||
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: false
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
|
||||
db.write(|conn| {
|
||||
conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
|
||||
.unwrap()(("test-text-1", 1))
|
||||
.unwrap();
|
||||
})
|
||||
.await;
|
||||
|
||||
db.save_workspace(workspace_2.clone()).await;
|
||||
|
||||
db.write(|conn| {
|
||||
conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
|
||||
.unwrap()(("test-text-2", 2))
|
||||
.unwrap();
|
||||
})
|
||||
.await;
|
||||
|
||||
workspace_1.location = (["/tmp", "/tmp3"]).into();
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
db.save_workspace(workspace_1).await;
|
||||
|
||||
workspace_2.dock_pane.children.push(SerializedItem {
|
||||
kind: Arc::from("Test"),
|
||||
item_id: 10,
|
||||
active: true,
|
||||
});
|
||||
db.save_workspace(workspace_2).await;
|
||||
|
||||
let test_text_2 = db
|
||||
.select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
|
||||
.unwrap()(2)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(test_text_2, "test-text-2");
|
||||
|
||||
let test_text_1 = db
|
||||
.select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
|
||||
.unwrap()(1)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(test_text_1, "test-text-1");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_full_workspace_serialization() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
|
||||
|
||||
let dock_pane = crate::persistence::model::SerializedPane {
|
||||
children: vec![
|
||||
SerializedItem::new("Terminal", 1, false),
|
||||
SerializedItem::new("Terminal", 2, false),
|
||||
SerializedItem::new("Terminal", 3, true),
|
||||
SerializedItem::new("Terminal", 4, false),
|
||||
],
|
||||
active: false,
|
||||
};
|
||||
|
||||
// -----------------
|
||||
// | 1,2 | 5,6 |
|
||||
// | - - - | |
|
||||
// | 3,4 | |
|
||||
// -----------------
|
||||
let center_group = SerializedPaneGroup::Group {
|
||||
axis: gpui::Axis::Horizontal,
|
||||
children: vec![
|
||||
SerializedPaneGroup::Group {
|
||||
axis: gpui::Axis::Vertical,
|
||||
children: vec![
|
||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 5, false),
|
||||
SerializedItem::new("Terminal", 6, true),
|
||||
],
|
||||
false,
|
||||
)),
|
||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 7, true),
|
||||
SerializedItem::new("Terminal", 8, false),
|
||||
],
|
||||
false,
|
||||
)),
|
||||
],
|
||||
},
|
||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 9, false),
|
||||
SerializedItem::new("Terminal", 10, true),
|
||||
],
|
||||
false,
|
||||
)),
|
||||
],
|
||||
};
|
||||
|
||||
let workspace = SerializedWorkspace {
|
||||
id: 5,
|
||||
location: (["/tmp", "/tmp2"]).into(),
|
||||
dock_position: DockPosition::Shown(DockAnchor::Bottom),
|
||||
center_group,
|
||||
dock_pane,
|
||||
left_sidebar_open: true
|
||||
};
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
|
||||
|
||||
assert_eq!(workspace, round_trip_workspace.unwrap());
|
||||
|
||||
// Test guaranteed duplicate IDs
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
||||
let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
|
||||
assert_eq!(workspace, round_trip_workspace.unwrap());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_workspace_assignment() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
|
||||
|
||||
let workspace_1 = SerializedWorkspace {
|
||||
id: 1,
|
||||
location: (["/tmp", "/tmp2"]).into(),
|
||||
dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: true,
|
||||
};
|
||||
|
||||
let mut workspace_2 = SerializedWorkspace {
|
||||
id: 2,
|
||||
location: (["/tmp"]).into(),
|
||||
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: false,
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
db.save_workspace(workspace_2.clone()).await;
|
||||
|
||||
// Test that paths are treated as a set
|
||||
assert_eq!(
|
||||
db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
|
||||
workspace_1
|
||||
);
|
||||
assert_eq!(
|
||||
db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
|
||||
workspace_1
|
||||
);
|
||||
|
||||
// Make sure that other keys work
|
||||
assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
|
||||
assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
|
||||
|
||||
// Test 'mutate' case of updating a pre-existing id
|
||||
workspace_2.location = (["/tmp", "/tmp2"]).into();
|
||||
|
||||
db.save_workspace(workspace_2.clone()).await;
|
||||
assert_eq!(
|
||||
db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
|
||||
workspace_2
|
||||
);
|
||||
|
||||
// Test other mechanism for mutating
|
||||
let mut workspace_3 = SerializedWorkspace {
|
||||
id: 3,
|
||||
location: (&["/tmp", "/tmp2"]).into(),
|
||||
dock_position: DockPosition::Shown(DockAnchor::Right),
|
||||
center_group: Default::default(),
|
||||
dock_pane: Default::default(),
|
||||
left_sidebar_open: false
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_3.clone()).await;
|
||||
assert_eq!(
|
||||
db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
|
||||
workspace_3
|
||||
);
|
||||
|
||||
// Make sure that updating paths differently also works
|
||||
workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
|
||||
db.save_workspace(workspace_3.clone()).await;
|
||||
assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
|
||||
assert_eq!(
|
||||
db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
|
||||
.unwrap(),
|
||||
workspace_3
|
||||
);
|
||||
}
|
||||
|
||||
use crate::dock::DockPosition;
|
||||
use crate::persistence::model::SerializedWorkspace;
|
||||
use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
|
||||
|
||||
fn default_workspace<P: AsRef<Path>>(
|
||||
workspace_id: &[P],
|
||||
dock_pane: SerializedPane,
|
||||
center_group: &SerializedPaneGroup,
|
||||
) -> SerializedWorkspace {
|
||||
SerializedWorkspace {
|
||||
id: 4,
|
||||
location: workspace_id.into(),
|
||||
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
|
||||
center_group: center_group.clone(),
|
||||
dock_pane,
|
||||
left_sidebar_open: true
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_basic_dock_pane() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let db = WorkspaceDb(open_test_db("basic_dock_pane").await);
|
||||
|
||||
let dock_pane = crate::persistence::model::SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 1, false),
|
||||
SerializedItem::new("Terminal", 4, false),
|
||||
SerializedItem::new("Terminal", 2, false),
|
||||
SerializedItem::new("Terminal", 3, true),
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
||||
let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
|
||||
|
||||
assert_eq!(workspace.dock_pane, new_workspace.dock_pane);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_simple_split() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let db = WorkspaceDb(open_test_db("simple_split").await);
|
||||
|
||||
// -----------------
|
||||
// | 1,2 | 5,6 |
|
||||
// | - - - | |
|
||||
// | 3,4 | |
|
||||
// -----------------
|
||||
let center_pane = SerializedPaneGroup::Group {
|
||||
axis: gpui::Axis::Horizontal,
|
||||
children: vec![
|
||||
SerializedPaneGroup::Group {
|
||||
axis: gpui::Axis::Vertical,
|
||||
children: vec![
|
||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 1, false),
|
||||
SerializedItem::new("Terminal", 2, true),
|
||||
],
|
||||
false,
|
||||
)),
|
||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 4, false),
|
||||
SerializedItem::new("Terminal", 3, true),
|
||||
],
|
||||
true,
|
||||
)),
|
||||
],
|
||||
},
|
||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 5, true),
|
||||
SerializedItem::new("Terminal", 6, false),
|
||||
],
|
||||
false,
|
||||
)),
|
||||
],
|
||||
};
|
||||
|
||||
let workspace = default_workspace(&["/tmp"], Default::default(), ¢er_pane);
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
||||
let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
|
||||
|
||||
assert_eq!(workspace.center_group, new_workspace.center_group);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_cleanup_panes() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
|
||||
|
||||
let center_pane = SerializedPaneGroup::Group {
|
||||
axis: gpui::Axis::Horizontal,
|
||||
children: vec![
|
||||
SerializedPaneGroup::Group {
|
||||
axis: gpui::Axis::Vertical,
|
||||
children: vec![
|
||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 1, false),
|
||||
SerializedItem::new("Terminal", 2, true),
|
||||
],
|
||||
false,
|
||||
)),
|
||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 4, false),
|
||||
SerializedItem::new("Terminal", 3, true),
|
||||
],
|
||||
true,
|
||||
)),
|
||||
],
|
||||
},
|
||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 5, false),
|
||||
SerializedItem::new("Terminal", 6, true),
|
||||
],
|
||||
false,
|
||||
)),
|
||||
],
|
||||
};
|
||||
|
||||
let id = &["/tmp"];
|
||||
|
||||
let mut workspace = default_workspace(id, Default::default(), ¢er_pane);
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
||||
workspace.center_group = SerializedPaneGroup::Group {
|
||||
axis: gpui::Axis::Vertical,
|
||||
children: vec![
|
||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 1, false),
|
||||
SerializedItem::new("Terminal", 2, true),
|
||||
],
|
||||
false,
|
||||
)),
|
||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||
vec![
|
||||
SerializedItem::new("Terminal", 4, true),
|
||||
SerializedItem::new("Terminal", 3, false),
|
||||
],
|
||||
true,
|
||||
)),
|
||||
],
|
||||
};
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
||||
let new_workspace = db.workspace_for_roots(id).unwrap();
|
||||
|
||||
assert_eq!(workspace.center_group, new_workspace.center_group);
|
||||
}
|
||||
}
|
315
crates/workspace/src/persistence/model.rs
Normal file
315
crates/workspace/src/persistence/model.rs
Normal file
|
@ -0,0 +1,315 @@
|
|||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use async_recursion::async_recursion;
|
||||
use gpui::{AsyncAppContext, Axis, ModelHandle, Task, ViewHandle};
|
||||
|
||||
use db::sqlez::{
|
||||
bindable::{Bind, Column},
|
||||
statement::Statement,
|
||||
};
|
||||
use project::Project;
|
||||
use settings::DockAnchor;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{
|
||||
dock::DockPosition, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkspaceLocation(Arc<Vec<PathBuf>>);
|
||||
|
||||
impl WorkspaceLocation {
|
||||
pub fn paths(&self) -> Arc<Vec<PathBuf>> {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
|
||||
fn from(iterator: T) -> Self {
|
||||
let mut roots = iterator
|
||||
.into_iter()
|
||||
.map(|p| p.as_ref().to_path_buf())
|
||||
.collect::<Vec<_>>();
|
||||
roots.sort();
|
||||
Self(Arc::new(roots))
|
||||
}
|
||||
}
|
||||
|
||||
impl Bind for &WorkspaceLocation {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
bincode::serialize(&self.0)
|
||||
.expect("Bincode serialization of paths should not fail")
|
||||
.bind(statement, start_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for WorkspaceLocation {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let blob = statement.column_blob(start_index)?;
|
||||
Ok((
|
||||
WorkspaceLocation(bincode::deserialize(blob).context("Bincode failed")?),
|
||||
start_index + 1,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct SerializedWorkspace {
|
||||
pub id: WorkspaceId,
|
||||
pub location: WorkspaceLocation,
|
||||
pub dock_position: DockPosition,
|
||||
pub center_group: SerializedPaneGroup,
|
||||
pub dock_pane: SerializedPane,
|
||||
pub left_sidebar_open: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum SerializedPaneGroup {
|
||||
Group {
|
||||
axis: Axis,
|
||||
children: Vec<SerializedPaneGroup>,
|
||||
},
|
||||
Pane(SerializedPane),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Default for SerializedPaneGroup {
|
||||
fn default() -> Self {
|
||||
Self::Pane(SerializedPane {
|
||||
children: vec![SerializedItem::default()],
|
||||
active: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SerializedPaneGroup {
|
||||
#[async_recursion(?Send)]
|
||||
pub(crate) async fn deserialize(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
workspace_id: WorkspaceId,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Option<(Member, Option<ViewHandle<Pane>>)> {
|
||||
match self {
|
||||
SerializedPaneGroup::Group { axis, children } => {
|
||||
let mut current_active_pane = None;
|
||||
let mut members = Vec::new();
|
||||
for child in children {
|
||||
if let Some((new_member, active_pane)) = child
|
||||
.deserialize(project, workspace_id, workspace, cx)
|
||||
.await
|
||||
{
|
||||
members.push(new_member);
|
||||
|
||||
current_active_pane = current_active_pane.or(active_pane);
|
||||
}
|
||||
}
|
||||
|
||||
if members.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((
|
||||
Member::Axis(PaneAxis {
|
||||
axis: *axis,
|
||||
members,
|
||||
}),
|
||||
current_active_pane,
|
||||
))
|
||||
}
|
||||
SerializedPaneGroup::Pane(serialized_pane) => {
|
||||
let pane = workspace.update(cx, |workspace, cx| workspace.add_pane(cx));
|
||||
let active = serialized_pane.active;
|
||||
serialized_pane
|
||||
.deserialize_to(project, &pane, workspace_id, workspace, cx)
|
||||
.await;
|
||||
|
||||
if pane.read_with(cx, |pane, _| pane.items().next().is_some()) {
|
||||
Some((Member::Pane(pane.clone()), active.then(|| pane)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Default, Clone)]
|
||||
pub struct SerializedPane {
|
||||
pub(crate) active: bool,
|
||||
pub(crate) children: Vec<SerializedItem>,
|
||||
}
|
||||
|
||||
impl SerializedPane {
|
||||
pub fn new(children: Vec<SerializedItem>, active: bool) -> Self {
|
||||
SerializedPane { children, active }
|
||||
}
|
||||
|
||||
pub async fn deserialize_to(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
pane_handle: &ViewHandle<Pane>,
|
||||
workspace_id: WorkspaceId,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) {
|
||||
let mut active_item_index = None;
|
||||
for (index, item) in self.children.iter().enumerate() {
|
||||
let project = project.clone();
|
||||
let item_handle = pane_handle
|
||||
.update(cx, |_, cx| {
|
||||
if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
|
||||
deserializer(
|
||||
project,
|
||||
workspace.downgrade(),
|
||||
workspace_id,
|
||||
item.item_id,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Task::ready(Err(anyhow::anyhow!(
|
||||
"Deserializer does not exist for item kind: {}",
|
||||
item.kind
|
||||
)))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
if let Some(item_handle) = item_handle {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Pane::add_item(workspace, &pane_handle, item_handle, false, false, None, cx);
|
||||
})
|
||||
}
|
||||
|
||||
if item.active {
|
||||
active_item_index = Some(index);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(active_item_index) = active_item_index {
|
||||
pane_handle.update(cx, |pane, cx| {
|
||||
pane.activate_item(active_item_index, false, false, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type GroupId = i64;
|
||||
pub type PaneId = i64;
|
||||
pub type ItemId = usize;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct SerializedItem {
|
||||
pub kind: Arc<str>,
|
||||
pub item_id: ItemId,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
impl SerializedItem {
|
||||
pub fn new(kind: impl AsRef<str>, item_id: ItemId, active: bool) -> Self {
|
||||
Self {
|
||||
kind: Arc::from(kind.as_ref()),
|
||||
item_id,
|
||||
active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Default for SerializedItem {
|
||||
fn default() -> Self {
|
||||
SerializedItem {
|
||||
kind: Arc::from("Terminal"),
|
||||
item_id: 100000,
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Bind for &SerializedItem {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
let next_index = statement.bind(self.kind.clone(), start_index)?;
|
||||
let next_index = statement.bind(self.item_id, next_index)?;
|
||||
statement.bind(self.active, next_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for SerializedItem {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let (kind, next_index) = Arc::<str>::column(statement, start_index)?;
|
||||
let (item_id, next_index) = ItemId::column(statement, next_index)?;
|
||||
let (active, next_index) = bool::column(statement, next_index)?;
|
||||
Ok((
|
||||
SerializedItem {
|
||||
kind,
|
||||
item_id,
|
||||
active,
|
||||
},
|
||||
next_index,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Bind for DockPosition {
|
||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||
let next_index = statement.bind(self.is_visible(), start_index)?;
|
||||
statement.bind(self.anchor(), next_index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for DockPosition {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let (visible, next_index) = bool::column(statement, start_index)?;
|
||||
let (dock_anchor, next_index) = DockAnchor::column(statement, next_index)?;
|
||||
let position = if visible {
|
||||
DockPosition::Shown(dock_anchor)
|
||||
} else {
|
||||
DockPosition::Hidden(dock_anchor)
|
||||
};
|
||||
Ok((position, next_index))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use db::sqlez::connection::Connection;
|
||||
use settings::DockAnchor;
|
||||
|
||||
use super::WorkspaceLocation;
|
||||
|
||||
#[test]
|
||||
fn test_workspace_round_trips() {
|
||||
let db = Connection::open_memory(Some("workspace_id_round_trips"));
|
||||
|
||||
db.exec(indoc::indoc! {"
|
||||
CREATE TABLE workspace_id_test(
|
||||
workspace_id INTEGER,
|
||||
dock_anchor TEXT
|
||||
);"})
|
||||
.unwrap()()
|
||||
.unwrap();
|
||||
|
||||
let workspace_id: WorkspaceLocation = WorkspaceLocation::from(&["\test2", "\test1"]);
|
||||
|
||||
db.exec_bound("INSERT INTO workspace_id_test(workspace_id, dock_anchor) VALUES (?,?)")
|
||||
.unwrap()((&workspace_id, DockAnchor::Bottom))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
db.select_row("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1")
|
||||
.unwrap()()
|
||||
.unwrap(),
|
||||
Some((
|
||||
WorkspaceLocation::from(&["\test1", "\test2"]),
|
||||
DockAnchor::Bottom
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ use gpui::{
|
|||
};
|
||||
use project::search::SearchQuery;
|
||||
|
||||
use crate::{Item, ItemHandle, WeakItemHandle};
|
||||
use crate::{item::WeakItemHandle, Item, ItemHandle};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SearchEvent {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use crate::{Item, ItemNavHistory};
|
||||
use crate::{
|
||||
item::ItemEvent, persistence::model::ItemId, Item, ItemNavHistory, Pane, Workspace, WorkspaceId,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use call::participant::{Frame, RemoteVideoTrack};
|
||||
use client::{PeerId, User};
|
||||
|
@ -6,8 +8,10 @@ use futures::StreamExt;
|
|||
use gpui::{
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
Entity, ModelHandle, MouseButton, RenderContext, Task, View, ViewContext,
|
||||
Entity, ModelHandle, MouseButton, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
|
@ -142,7 +146,11 @@ impl Item for SharedScreen {
|
|||
self.nav_history = Some(history);
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: WorkspaceId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Self> {
|
||||
let track = self.track.upgrade()?;
|
||||
Some(Self::new(&track, self.peer_id, self.user.clone(), cx))
|
||||
}
|
||||
|
@ -176,9 +184,23 @@ impl Item for SharedScreen {
|
|||
Task::ready(Err(anyhow!("Item::reload called on SharedScreen")))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> Vec<crate::ItemEvent> {
|
||||
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
|
||||
match event {
|
||||
Event::Close => vec![crate::ItemEvent::CloseItem],
|
||||
Event::Close => vec![ItemEvent::CloseItem],
|
||||
}
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
_project: ModelHandle<Project>,
|
||||
_workspace: WeakViewHandle<Workspace>,
|
||||
_workspace_id: WorkspaceId,
|
||||
_item_id: ItemId,
|
||||
_cx: &mut ViewContext<Pane>,
|
||||
) -> Task<Result<ViewHandle<Self>>> {
|
||||
unreachable!("Shared screen can not be deserialized")
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue