Merge branch 'main' into multibuffer-following

This commit is contained in:
Max Brunsfeld 2022-12-12 11:47:39 -08:00
commit f797dfb88f
175 changed files with 16722 additions and 8699 deletions

View file

@ -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());

View 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 {}
}

View 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(&notification, 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
}
}
}
}

View file

@ -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);

View file

@ -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 {

View 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(), &center_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(), &center_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);
}
}

View 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
))
);
}
}

View file

@ -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 {

View file

@ -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