Added UUID based, stable workspace ID for caching on item startup. Completed first sketch of terminal persistence. Still need to debug it though....

This commit is contained in:
Mikayla Maki 2022-11-20 22:41:10 -08:00
parent e659823e6c
commit a47f2ca445
20 changed files with 501 additions and 364 deletions

2
Cargo.lock generated
View file

@ -1572,6 +1572,7 @@ dependencies = [
"sqlez",
"tempdir",
"util",
"uuid 1.2.2",
]
[[package]]
@ -6834,6 +6835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
dependencies = [
"getrandom 0.2.8",
"rand 0.8.5",
]
[[package]]

View file

@ -22,6 +22,7 @@ lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
parking_lot = "0.11.1"
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.2.2", features = ["v4", "fast-rng"] }
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }

View file

@ -1,21 +1,26 @@
pub mod kvp;
// Re-export indoc and sqlez so clients only need to include us
// Re-export
pub use anyhow;
pub use indoc::indoc;
pub use lazy_static;
pub use sqlez;
use std::fs::{create_dir_all, remove_dir_all};
use std::path::Path;
use sqlez::bindable::{Bind, Column};
#[cfg(any(test, feature = "test-support"))]
use anyhow::Result;
#[cfg(any(test, feature = "test-support"))]
use sqlez::connection::Connection;
use sqlez::domain::{Domain, Migrator};
#[cfg(any(test, feature = "test-support"))]
use sqlez::domain::Domain;
use sqlez::domain::Migrator;
use sqlez::thread_safe_connection::ThreadSafeConnection;
use std::fs::{create_dir_all, remove_dir_all};
use std::path::Path;
use util::channel::{ReleaseChannel, RELEASE_CHANNEL, RELEASE_CHANNEL_NAME};
use util::paths::DB_DIR;
use uuid::Uuid as RealUuid;
const INITIALIZE_QUERY: &'static str = indoc! {"
PRAGMA journal_mode=WAL;
@ -25,6 +30,47 @@ const INITIALIZE_QUERY: &'static str = indoc! {"
PRAGMA case_sensitive_like=TRUE;
"};
#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Uuid(RealUuid);
impl std::ops::Deref for Uuid {
type Target = RealUuid;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Bind for Uuid {
fn bind(
&self,
statement: &sqlez::statement::Statement,
start_index: i32,
) -> anyhow::Result<i32> {
statement.bind(self.as_bytes(), start_index)
}
}
impl Column for Uuid {
fn column(
statement: &mut sqlez::statement::Statement,
start_index: i32,
) -> anyhow::Result<(Self, i32)> {
let blob = statement.column_blob(start_index)?;
Ok((Uuid::from_bytes(blob)?, start_index + 1))
}
}
impl Uuid {
pub fn new() -> Self {
Uuid(RealUuid::new_v4())
}
fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
Ok(Uuid(RealUuid::from_bytes(bytes.try_into()?)))
}
}
/// Open or create a database at the given directory path.
pub fn open_file_db<M: Migrator>() -> ThreadSafeConnection<M> {
// Use 0 for now. Will implement incrementing and clearing of old db files soon TM
@ -77,3 +123,88 @@ macro_rules! connection {
}
};
}
#[macro_export]
macro_rules! exec_method {
($id:ident(): $sql:literal) => {
pub fn $id(&self) -> $crate::sqlez::anyhow::Result<()> {
use $crate::anyhow::Context;
self.exec($sql)?()
.context(::std::format!(
"Error in {}, exec failed to execute or parse for: {}",
::std::stringify!($id),
::std::stringify!($sql),
))
}
};
($id:ident($($arg:ident: $arg_type:ty),+): $sql:literal) => {
pub fn $id(&self, $($arg: $arg_type),+) -> $crate::sqlez::anyhow::Result<()> {
use $crate::anyhow::Context;
self.exec_bound::<($($arg_type),+)>($sql)?(($($arg),+))
.context(::std::format!(
"Error in {}, exec_bound failed to execute or parse for: {}",
::std::stringify!($id),
::std::stringify!($sql),
))
}
};
}
#[macro_export]
macro_rules! select_method {
($id:ident() -> $return_type:ty: $sql:literal) => {
pub fn $id(&self) -> $crate::sqlez::anyhow::Result<Vec<$return_type>> {
use $crate::anyhow::Context;
self.select::<$return_type>($sql)?(())
.context(::std::format!(
"Error in {}, select_row failed to execute or parse for: {}",
::std::stringify!($id),
::std::stringify!($sql),
))
}
};
($id:ident($($arg:ident: $arg_type:ty),+) -> $return_type:ty: $sql:literal) => {
pub fn $id(&self, $($arg: $arg_type),+) -> $crate::sqlez::anyhow::Result<Vec<$return_type>> {
use $crate::anyhow::Context;
self.select_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+))
.context(::std::format!(
"Error in {}, exec_bound failed to execute or parse for: {}",
::std::stringify!($id),
::std::stringify!($sql),
))
}
};
}
#[macro_export]
macro_rules! select_row_method {
($id:ident() -> $return_type:ty: $sql:literal) => {
pub fn $id(&self) -> $crate::sqlez::anyhow::Result<Option<$return_type>> {
use $crate::anyhow::Context;
self.select_row::<$return_type>($sql)?(())
.context(::std::format!(
"Error in {}, select_row failed to execute or parse for: {}",
::std::stringify!($id),
::std::stringify!($sql),
))
}
};
($id:ident($($arg:ident: $arg_type:ty),+) -> $return_type:ty: $sql:literal) => {
pub fn $id(&self, $($arg: $arg_type),+) -> $crate::sqlez::anyhow::Result<Option<$return_type>> {
use $crate::anyhow::Context;
self.select_row_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+))
.context(::std::format!(
"Error in {}, select_row_bound failed to execute or parse for: {}",
::std::stringify!($id),
::std::stringify!($sql),
))
}
};
}

View file

@ -584,7 +584,11 @@ impl Item for ProjectDiagnosticsEditor {
});
}
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
fn clone_on_split(
&self,
_workspace_id: workspace::WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Option<Self>
where
Self: Sized,
{

View file

@ -83,7 +83,7 @@ use theme::{DiagnosticStyle, Theme};
use util::{post_inc, ResultExt, TryFutureExt};
use workspace::{ItemNavHistory, Workspace};
use crate::{git::diff_hunk_to_display, persistence::DB};
use crate::git::diff_hunk_to_display;
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
@ -1137,30 +1137,30 @@ impl Editor {
cx: &mut ViewContext<Self>,
) -> Self {
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
if let Some(project) = project.as_ref() {
if let Some(file) = buffer
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).file())
.and_then(|file| file.as_local())
{
let item_id = cx.weak_handle().id();
let workspace_id = project
.read(cx)
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).abs_path())
.collect::<Vec<_>>()
.into();
let path = file.abs_path(cx);
dbg!(&path);
// if let Some(project) = project.as_ref() {
// if let Some(file) = buffer
// .read(cx)
// .as_singleton()
// .and_then(|buffer| buffer.read(cx).file())
// .and_then(|file| file.as_local())
// {
// // let item_id = cx.weak_handle().id();
// // let workspace_id = project
// // .read(cx)
// // .visible_worktrees(cx)
// // .map(|worktree| worktree.read(cx).abs_path())
// // .collect::<Vec<_>>()
// // .into();
// let path = file.abs_path(cx);
// dbg!(&path);
cx.background()
.spawn(async move {
DB.save_path(item_id, workspace_id, path).log_err();
})
.detach();
}
}
// // cx.background()
// // .spawn(async move {
// // DB.save_path(item_id, workspace_id, path).log_err();
// // })
// // .detach();
// }
// }
Self::new(EditorMode::Full, buffer, project, None, cx)
}

View file

@ -368,7 +368,7 @@ impl Item for Editor {
self.buffer.read(cx).is_singleton()
}
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>
where
Self: Sized,
{
@ -561,14 +561,13 @@ impl Item for Editor {
fn deserialize(
project: ModelHandle<Project>,
_workspace: WeakViewHandle<Workspace>,
workspace_id: WorkspaceId,
workspace_id: workspace::WorkspaceId,
item_id: ItemId,
cx: &mut ViewContext<Pane>,
) -> Task<Result<ViewHandle<Self>>> {
if let Some(project_item) = project.update(cx, |project, cx| {
// Look up the path with this key associated, create a self with that path
let path = DB.get_path(item_id, workspace_id).ok()?;
dbg!(&path);
let (worktree, path) = project.find_local_worktree(&path, cx)?;
let project_path = ProjectPath {
worktree_id: worktree.read(cx).id(),

View file

@ -1,7 +1,7 @@
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use db::connection;
use db::{connection, exec_method};
use indoc::indoc;
use sqlez::domain::Domain;
use workspace::{ItemId, Workspace, WorkspaceId};
@ -35,18 +35,12 @@ impl EditorDb {
pub fn get_path(&self, item_id: ItemId, workspace_id: WorkspaceId) -> Result<PathBuf> {
self.select_row_bound(indoc! {"
SELECT path FROM editors
WHERE item_id = ? AND workspace_id = ?"})?((item_id, &workspace_id))?
WHERE item_id = ? AND workspace_id = ?"})?((item_id, workspace_id))?
.context("Path not found for serialized editor")
}
pub fn save_path(
&self,
item_id: ItemId,
workspace_id: WorkspaceId,
path: PathBuf,
) -> Result<()> {
self.exec_bound::<(ItemId, &WorkspaceId, &Path)>(indoc! {"
INSERT OR REPLACE INTO editors(item_id, workspace_id, path)
VALUES (?, ?, ?)"})?((item_id, &workspace_id, &path))
}
exec_method!(save_path(item_id: ItemId, workspace_id: WorkspaceId, path: &Path):
"INSERT OR REPLACE INTO editors(item_id, workspace_id, path)
VALUES (?, ?, ?)"
);
}

View file

@ -26,7 +26,7 @@ use util::ResultExt as _;
use workspace::{
item::{Item, ItemEvent, ItemHandle},
searchable::{Direction, SearchableItem, SearchableItemHandle},
ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace,
ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
};
actions!(project_search, [SearchInNew, ToggleFocus]);
@ -315,7 +315,7 @@ impl Item for ProjectSearchView {
.update(cx, |editor, cx| editor.reload(project, cx))
}
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>
where
Self: Sized,
{

View file

@ -36,6 +36,13 @@ impl Bind for &[u8] {
}
}
impl<const C: usize> Bind for &[u8; C] {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement.bind_blob(start_index, self.as_slice())?;
Ok(start_index + 1)
}
}
impl Bind for Vec<u8> {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement.bind_blob(start_index, self)?;

View file

@ -52,57 +52,3 @@ impl Connection {
Ok(move |bindings| statement.with_bindings(bindings)?.maybe_row::<C>())
}
}
#[macro_export]
macro_rules! exec_method {
($id:ident(): $sql:literal) => {
pub fn $id(&self) -> $crate::anyhow::Result<()> {
iife!({
self.exec($sql)?()
})
}
};
($id:ident($($arg:ident: $arg_type:ty),+): $sql:literal) => {
pub fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
iife!({
self.exec_bound::<($($arg_type),+)>($sql)?(($($arg),+))
})
}
};
}
#[macro_export]
macro_rules! select_method {
($id:ident() -> $return_type:ty: $sql:literal) => {
pub fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
iife!({
self.select::<$return_type>($sql)?(())
})
}
};
($id:ident($($arg:ident: $arg_type:ty),+) -> $return_type:ty: $sql:literal) => {
pub fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
iife!({
self.exec_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+))
})
}
};
}
#[macro_export]
macro_rules! select_row_method {
($id:ident() -> $return_type:ty: $sql:literal) => {
pub fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
iife!({
self.select_row::<$return_type>($sql)?(())
})
}
};
($id:ident($($arg:ident: $arg_type:ty),+) -> $return_type:ty: $sql:literal) => {
pub fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
iife!({
self.select_row_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+))
})
}
};
}

View file

@ -1,10 +1,7 @@
use std::path::{Path, PathBuf};
use db::{
connection, indoc,
sqlez::{domain::Domain, exec_method, select_row_method},
};
use util::iife;
use db::{connection, exec_method, indoc, select_row_method, sqlez::domain::Domain};
use workspace::{ItemId, Workspace, WorkspaceId};
use crate::Terminal;
@ -19,13 +16,12 @@ impl Domain for Terminal {
fn migrations() -> &'static [&'static str] {
&[indoc! {"
CREATE TABLE terminals (
item_id INTEGER,
workspace_id BLOB,
item_id INTEGER,
working_directory BLOB,
PRIMARY KEY(item_id, workspace_id),
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) STRICT;
"}]
}
@ -33,15 +29,15 @@ impl Domain for Terminal {
impl TerminalDb {
exec_method!(
save_working_directory(item_id: ItemId, workspace_id: &WorkspaceId, working_directory: &Path):
save_working_directory(model_id: ItemId, workspace_id: WorkspaceId, working_directory: &Path):
"INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory)
VALUES (?, ?, ?)"
VALUES (?1, ?2, ?3)"
);
select_row_method!(
get_working_directory(item_id: ItemId, workspace_id: &WorkspaceId) -> PathBuf:
get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> PathBuf:
"SELECT working_directory
FROM terminals
WHERE item_id = ? workspace_id = ?"
WHERE item_id = ? AND workspace_id = ?"
);
}

View file

@ -33,9 +33,11 @@ use mappings::mouse::{
alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
};
use persistence::TERMINAL_CONNECTION;
use procinfo::LocalProcessInfo;
use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
use util::ResultExt;
use workspace::{ItemId, WorkspaceId};
use std::{
cmp::min,
@ -282,6 +284,8 @@ impl TerminalBuilder {
blink_settings: Option<TerminalBlink>,
alternate_scroll: &AlternateScroll,
window_id: usize,
item_id: ItemId,
workspace_id: WorkspaceId,
) -> Result<TerminalBuilder> {
let pty_config = {
let alac_shell = shell.clone().and_then(|shell| match shell {
@ -386,6 +390,8 @@ impl TerminalBuilder {
last_mouse_position: None,
next_link_id: 0,
selection_phase: SelectionPhase::Ended,
workspace_id,
item_id,
};
Ok(TerminalBuilder {
@ -529,6 +535,8 @@ pub struct Terminal {
scroll_px: f32,
next_link_id: usize,
selection_phase: SelectionPhase,
workspace_id: WorkspaceId,
item_id: ItemId,
}
impl Terminal {
@ -566,20 +574,6 @@ impl Terminal {
}
AlacTermEvent::Wakeup => {
cx.emit(Event::Wakeup);
if self.update_process_info() {
cx.emit(Event::TitleChanged);
// if let Some(foreground_info) = self.foreground_process_info {
// cx.background().spawn(async move {
// TERMINAL_CONNECTION.save_working_directory(
// self.item_id,
// &self.workspace_id,
// &foreground_info.cwd,
// );
// });
// }
}
}
AlacTermEvent::ColorRequest(idx, fun_ptr) => {
self.events
@ -888,6 +882,19 @@ impl Terminal {
if self.update_process_info() {
cx.emit(Event::TitleChanged);
if let Some(foreground_info) = &self.foreground_process_info {
let cwd = foreground_info.cwd.clone();
let item_id = self.item_id;
let workspace_id = self.workspace_id;
cx.background()
.spawn(async move {
TERMINAL_CONNECTION
.save_working_directory(item_id, workspace_id, cwd.as_path())
.log_err();
})
.detach();
}
}
//Note that the ordering of events matters for event processing

View file

@ -1,6 +1,6 @@
use crate::persistence::TERMINAL_CONNECTION;
use crate::terminal_view::TerminalView;
use crate::{Event, Terminal, TerminalBuilder, TerminalError};
use crate::{Event, TerminalBuilder, TerminalError};
use alacritty_terminal::index::Point;
use dirs::home_dir;
@ -14,7 +14,7 @@ use workspace::{
item::{Item, ItemEvent},
ToolbarItemLocation, Workspace,
};
use workspace::{register_deserializable_item, Pane};
use workspace::{register_deserializable_item, Pane, WorkspaceId};
use project::{LocalWorktree, Project, ProjectPath};
use settings::{AlternateScroll, Settings, WorkingDirectory};
@ -82,7 +82,9 @@ impl TerminalContainer {
.unwrap_or(WorkingDirectory::CurrentProjectDirectory);
let working_directory = get_working_directory(workspace, cx, strategy);
let view = cx.add_view(|cx| TerminalContainer::new(working_directory, false, cx));
let view = cx.add_view(|cx| {
TerminalContainer::new(working_directory, false, workspace.database_id(), cx)
});
workspace.add_item(Box::new(view), cx);
}
@ -90,6 +92,7 @@ impl TerminalContainer {
pub fn new(
working_directory: Option<PathBuf>,
modal: bool,
workspace_id: WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Self {
let settings = cx.global::<Settings>();
@ -116,10 +119,13 @@ impl TerminalContainer {
settings.terminal_overrides.blinking.clone(),
scroll,
cx.window_id(),
cx.view_id(),
workspace_id,
) {
Ok(terminal) => {
let terminal = cx.add_model(|cx| terminal.subscribe(cx));
let view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
cx.subscribe(&view, |_this, _content, event, cx| cx.emit(*event))
.detach();
TerminalContainerContent::Connected(view)
@ -139,18 +145,6 @@ impl TerminalContainer {
}
}
pub fn from_terminal(
terminal: ModelHandle<Terminal>,
modal: bool,
cx: &mut ViewContext<Self>,
) -> Self {
let connected_view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
TerminalContainer {
content: TerminalContainerContent::Connected(connected_view),
associated_directory: None,
}
}
fn connected(&self) -> Option<ViewHandle<TerminalView>> {
match &self.content {
TerminalContainerContent::Connected(vh) => Some(vh.clone()),
@ -278,13 +272,18 @@ impl Item for TerminalContainer {
.boxed()
}
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> {
//From what I can tell, there's no way to tell the current working
//Directory of the terminal from outside the shell. There might be
//solutions to this, but they are non-trivial and require more IPC
Some(TerminalContainer::new(
self.associated_directory.clone(),
false,
workspace_id,
cx,
))
}
@ -391,9 +390,14 @@ impl Item for TerminalContainer {
item_id: workspace::ItemId,
cx: &mut ViewContext<Pane>,
) -> Task<anyhow::Result<ViewHandle<Self>>> {
let working_directory = TERMINAL_CONNECTION.get_working_directory(item_id, &workspace_id);
let working_directory = TERMINAL_CONNECTION.get_working_directory(item_id, workspace_id);
Task::ready(Ok(cx.add_view(|cx| {
TerminalContainer::new(working_directory.log_err().flatten(), false, cx)
TerminalContainer::new(
working_directory.log_err().flatten(),
false,
workspace_id,
cx,
)
})))
}
}

View file

@ -206,7 +206,7 @@ impl Dock {
cx.focus(last_active_center_pane);
}
cx.emit(crate::Event::DockAnchorChanged);
workspace.serialize_workspace(None, cx);
workspace.serialize_workspace(cx);
cx.notify();
}

View file

@ -22,11 +22,8 @@ use theme::Theme;
use util::ResultExt;
use crate::{
pane,
persistence::model::{ItemId, WorkspaceId},
searchable::SearchableItemHandle,
DelayedDebouncedEditAction, FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation,
Workspace,
pane, persistence::model::ItemId, searchable::SearchableItemHandle, DelayedDebouncedEditAction,
FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
};
#[derive(Eq, PartialEq, Hash)]
@ -52,7 +49,7 @@ pub trait Item: View {
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, _: &mut ViewContext<Self>) -> Option<Self>
fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext<Self>) -> Option<Self>
where
Self: Sized,
{
@ -121,7 +118,9 @@ pub trait Item: View {
fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option<Vec<ElementBox>> {
None
}
fn serialized_item_kind() -> Option<&'static str>;
fn deserialize(
project: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
@ -144,7 +143,11 @@ pub trait ItemHandle: 'static + fmt::Debug {
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, cx: &mut MutableAppContext) -> Option<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,
@ -246,9 +249,13 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
Box::new(self.clone())
}
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
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(cx))
cx.add_option_view(|cx| item.clone_on_split(workspace_id, cx))
})
.map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
}
@ -812,7 +819,11 @@ pub(crate) mod test {
self.push_to_nav_history(cx);
}
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
fn clone_on_split(
&self,
_workspace_id: WorkspaceId,
_: &mut ViewContext<Self>,
) -> Option<Self>
where
Self: Sized,
{

View file

@ -2,39 +2,38 @@
pub mod model;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::path::Path;
use anyhow::{anyhow, bail, Result, Context};
use anyhow::{anyhow, bail, Context, Result};
use db::connection;
use gpui::Axis;
use indoc::indoc;
use db::sqlez::domain::Domain;
use util::{iife, unzip_option, ResultExt};
use crate::dock::DockPosition;
use crate::WorkspaceId;
use super::Workspace;
use model::{
GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
SerializedWorkspace, WorkspaceId,
GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
WorkspaceLocation,
};
connection!(DB: WorkspaceDb<Workspace>);
impl Domain for Workspace {
fn name() -> &'static str {
"workspace"
}
fn migrations() -> &'static [&'static str] {
&[indoc! {"
CREATE TABLE workspaces(
workspace_id BLOB PRIMARY KEY,
workspace_location BLOB NOT NULL 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
@ -97,21 +96,25 @@ impl WorkspaceDb {
&self,
worktree_roots: &[P],
) -> Option<SerializedWorkspace> {
let workspace_id: WorkspaceId = worktree_roots.into();
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, dock_position): (WorkspaceId, DockPosition) = iife!({
let (workspace_id, workspace_location, dock_position): (
WorkspaceId,
WorkspaceLocation,
DockPosition,
) = iife!({
if worktree_roots.len() == 0 {
self.select_row(indoc! {"
SELECT workspace_id, dock_visible, dock_anchor
SELECT workspace_id, workspace_location, dock_visible, dock_anchor
FROM workspaces
ORDER BY timestamp DESC LIMIT 1"})?()?
} else {
self.select_row_bound(indoc! {"
SELECT workspace_id, dock_visible, dock_anchor
SELECT workspace_id, workspace_location, dock_visible, dock_anchor
FROM workspaces
WHERE workspace_id = ?"})?(&workspace_id)?
WHERE workspace_location = ?"})?(&workspace_location)?
}
.context("No workspaces found")
})
@ -119,13 +122,14 @@ impl WorkspaceDb {
.flatten()?;
Some(SerializedWorkspace {
workspace_id: workspace_id.clone(),
id: workspace_id,
location: workspace_location.clone(),
dock_pane: self
.get_dock_pane(&workspace_id)
.get_dock_pane(workspace_id)
.context("Getting dock pane")
.log_err()?,
center_group: self
.get_center_pane_group(&workspace_id)
.get_center_pane_group(workspace_id)
.context("Getting center group")
.log_err()?,
dock_position,
@ -134,72 +138,61 @@ impl WorkspaceDb {
/// Saves a workspace using the worktree roots. Will garbage collect any workspaces
/// that used this workspace previously
pub fn save_workspace(
&self,
old_id: Option<WorkspaceId>,
workspace: &SerializedWorkspace,
) {
pub fn save_workspace(&self, workspace: &SerializedWorkspace) {
self.with_savepoint("update_worktrees", || {
// Clear out panes and pane_groups
self.exec_bound(indoc! {"
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;"})?
(old_id.as_ref().unwrap_or(&workspace.workspace_id)).context("Clearing old panes")?;
if let Some(old_id) = old_id {
self.exec_bound(indoc! {"
UPDATE OR REPLACE workspaces
SET workspace_id = ?,
dock_visible = ?,
dock_anchor = ?,
timestamp = CURRENT_TIMESTAMP
WHERE workspace_id = ?"})?((
&workspace.workspace_id,
workspace.dock_position,
&old_id,
)).context("Updating workspace with new worktree roots")?;
} else {
self.exec_bound(
"INSERT OR REPLACE INTO workspaces(workspace_id, dock_visible, dock_anchor) VALUES (?, ?, ?)",
)?((&workspace.workspace_id, workspace.dock_position)).context("Uodating workspace")?;
}
DELETE FROM panes WHERE workspace_id = ?1;"})?(workspace.id)
.context("Clearing old panes")?;
// Update or insert
self.exec_bound(indoc! {
"INSERT OR REPLACE INTO
workspaces(workspace_id, workspace_location, dock_visible, dock_anchor, timestamp)
VALUES
(?1, ?2, ?3, ?4, CURRENT_TIMESTAMP)"
})?((workspace.id, &workspace.location, workspace.dock_position))
.context("Updating workspace")?;
// Save center pane group and dock pane
self.save_pane_group(&workspace.workspace_id, &workspace.center_group, None).context("save pane group in save workspace")?;
let dock_id = self.save_pane(&workspace.workspace_id, &workspace.dock_pane, None, true).context("save pane in save workspace")?;
self.save_pane_group(workspace.id, &workspace.center_group, None)
.context("save pane group in save workspace")?;
let dock_id = self
.save_pane(workspace.id, &workspace.dock_pane, None, true)
.context("save pane in save workspace")?;
// Complete workspace initialization
self.exec_bound(indoc! {"
UPDATE workspaces
SET dock_pane = ?
WHERE workspace_id = ?"})?((
dock_id,
&workspace.workspace_id,
)).context("Finishing initialization with dock pane")?;
WHERE workspace_id = ?"})?((dock_id, workspace.id))
.context("Finishing initialization with dock pane")?;
Ok(())
})
.with_context(|| {
format!(
"Update workspace with roots {:?} failed.",
workspace.workspace_id.paths()
"Update workspace with roots {:?} and id {:?} failed.",
workspace.location.paths(),
workspace.id
)
})
.log_err();
}
/// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
pub fn recent_workspaces(&self, limit: usize) -> Vec<Arc<Vec<PathBuf>>> {
pub fn recent_workspaces(&self, limit: usize) -> Vec<(WorkspaceId, WorkspaceLocation)> {
iife!({
// TODO, upgrade anyhow: https://docs.rs/anyhow/1.0.66/anyhow/fn.Ok.html
Ok::<_, anyhow::Error>(
self.select_bound::<usize, WorkspaceId>(
"SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?",
self.select_bound::<usize, (WorkspaceId, WorkspaceLocation)>(
"SELECT workspace_id, workspace_location FROM workspaces ORDER BY timestamp DESC LIMIT ?",
)?(limit)?
.into_iter()
.map(|id| id.paths())
.collect::<Vec<Arc<Vec<PathBuf>>>>(),
.collect::<Vec<(WorkspaceId, WorkspaceLocation)>>(),
)
})
.log_err()
@ -208,7 +201,7 @@ impl WorkspaceDb {
pub(crate) fn get_center_pane_group(
&self,
workspace_id: &WorkspaceId,
workspace_id: WorkspaceId,
) -> Result<SerializedPaneGroup> {
self.get_pane_group(workspace_id, None)?
.into_iter()
@ -218,10 +211,10 @@ impl WorkspaceDb {
fn get_pane_group(
&self,
workspace_id: &WorkspaceId,
workspace_id: WorkspaceId,
group_id: Option<GroupId>,
) -> Result<Vec<SerializedPaneGroup>> {
type GroupKey<'a> = (Option<GroupId>, &'a WorkspaceId);
type GroupKey = (Option<GroupId>, WorkspaceId);
type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
self.select_bound::<GroupKey, GroupOrPane>(indoc! {"
SELECT group_id, axis, pane_id, active
@ -253,31 +246,29 @@ impl WorkspaceDb {
if let Some((group_id, axis)) = group_id.zip(axis) {
Ok(SerializedPaneGroup::Group {
axis,
children: self.get_pane_group(
workspace_id,
Some(group_id),
)?,
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)))
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,
}
.filter(|pane_group| match pane_group {
Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
_ => true,
})
.collect::<Result<_>>()
}
pub(crate) fn save_pane_group(
&self,
workspace_id: &WorkspaceId,
workspace_id: WorkspaceId,
pane_group: &SerializedPaneGroup,
parent: Option<(GroupId, usize)>,
) -> Result<()> {
@ -285,26 +276,31 @@ impl WorkspaceDb {
SerializedPaneGroup::Group { axis, children } => {
let (parent_id, position) = unzip_option(parent);
let group_id = self.select_row_bound::<_, i64>(indoc!{"
let group_id = self.select_row_bound::<_, i64>(indoc! {"
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"))?;
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(workspace_id, group, Some((group_id, position)))?
}
Ok(())
}
SerializedPaneGroup::Pane(pane) => {
self.save_pane(workspace_id, &pane, parent, false)?;
Ok(())
},
}
}
}
pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result<SerializedPane> {
pub(crate) fn get_dock_pane(&self, workspace_id: WorkspaceId) -> Result<SerializedPane> {
let (pane_id, active) = self.select_row_bound(indoc! {"
SELECT pane_id, active
FROM panes
@ -315,40 +311,35 @@ impl WorkspaceDb {
Ok(SerializedPane::new(
self.get_items(pane_id).context("Reading items")?,
active
active,
))
}
pub(crate) fn save_pane(
&self,
workspace_id: &WorkspaceId,
workspace_id: WorkspaceId,
pane: &SerializedPane,
parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
dock: bool,
) -> Result<PaneId> {
let pane_id = self.select_row_bound::<_, i64>(indoc!{"
let pane_id = self.select_row_bound::<_, i64>(indoc! {"
INSERT INTO panes(workspace_id, active)
VALUES (?, ?)
RETURNING pane_id"},
)?((workspace_id, pane.active))?
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);
self.exec_bound(indoc! {"
INSERT INTO center_panes(pane_id, parent_group_id, position)
VALUES (?, ?, ?)"})?((
pane_id, parent_id, order
))?;
VALUES (?, ?, ?)"})?((pane_id, parent_id, order))?;
}
self.save_items(workspace_id, pane_id, &pane.children)
.context("Saving items")?;
Ok(pane_id)
}
pub(crate) fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
Ok(self.select_bound(indoc! {"
@ -359,7 +350,7 @@ impl WorkspaceDb {
pub(crate) fn save_items(
&self,
workspace_id: &WorkspaceId,
workspace_id: WorkspaceId,
pane_id: PaneId,
items: &[SerializedItem],
) -> Result<()> {
@ -376,7 +367,8 @@ impl WorkspaceDb {
#[cfg(test)]
mod tests {
use db::{open_memory_db};
use db::{open_memory_db, Uuid};
use settings::DockAnchor;
use super::*;
@ -388,15 +380,13 @@ mod tests {
let db = WorkspaceDb(open_memory_db(Some("test_full_workspace_serialization")));
let dock_pane = crate::persistence::model::SerializedPane {
children: vec![
SerializedItem::new("Terminal", 1),
SerializedItem::new("Terminal", 2),
SerializedItem::new("Terminal", 3),
SerializedItem::new("Terminal", 4),
],
active: false
active: false,
};
// -----------------
@ -415,8 +405,8 @@ mod tests {
SerializedItem::new("Terminal", 5),
SerializedItem::new("Terminal", 6),
],
false)
),
false,
)),
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 7),
@ -430,7 +420,6 @@ mod tests {
vec![
SerializedItem::new("Terminal", 9),
SerializedItem::new("Terminal", 10),
],
false,
)),
@ -438,25 +427,24 @@ mod tests {
};
let workspace = SerializedWorkspace {
workspace_id: (["/tmp", "/tmp2"]).into(),
dock_position: DockPosition::Shown(DockAnchor::Bottom),
id: Uuid::new(),
location: (["/tmp", "/tmp2"]).into(),
dock_position: DockPosition::Shown(DockAnchor::Bottom),
center_group,
dock_pane,
};
db.save_workspace(None, &workspace);
db.save_workspace(&workspace);
let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
assert_eq!(workspace, round_trip_workspace.unwrap());
// Test guaranteed duplicate IDs
db.save_workspace(None, &workspace);
db.save_workspace(None, &workspace);
db.save_workspace(&workspace);
db.save_workspace(&workspace);
let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
assert_eq!(workspace, round_trip_workspace.unwrap());
}
#[test]
@ -466,21 +454,23 @@ mod tests {
let db = WorkspaceDb(open_memory_db(Some("test_basic_functionality")));
let workspace_1 = SerializedWorkspace {
workspace_id: (["/tmp", "/tmp2"]).into(),
id: WorkspaceId::new(),
location: (["/tmp", "/tmp2"]).into(),
dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
center_group: Default::default(),
dock_pane: Default::default(),
};
let mut workspace_2 = SerializedWorkspace {
workspace_id: (["/tmp"]).into(),
id: WorkspaceId::new(),
location: (["/tmp"]).into(),
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
center_group: Default::default(),
dock_pane: Default::default(),
};
db.save_workspace(None, &workspace_1);
db.save_workspace(None, &workspace_2);
db.save_workspace(&workspace_1);
db.save_workspace(&workspace_2);
// Test that paths are treated as a set
assert_eq!(
@ -497,8 +487,9 @@ mod tests {
assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
// Test 'mutate' case of updating a pre-existing id
workspace_2.workspace_id = (["/tmp", "/tmp2"]).into();
db.save_workspace(Some((&["/tmp"]).into()), &workspace_2);
workspace_2.location = (["/tmp", "/tmp2"]).into();
db.save_workspace(&workspace_2);
assert_eq!(
db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
workspace_2
@ -506,33 +497,28 @@ mod tests {
// Test other mechanism for mutating
let mut workspace_3 = SerializedWorkspace {
workspace_id: (&["/tmp", "/tmp2"]).into(),
id: WorkspaceId::new(),
location: (&["/tmp", "/tmp2"]).into(),
dock_position: DockPosition::Shown(DockAnchor::Right),
center_group: Default::default(),
dock_pane: Default::default(),
};
db.save_workspace(None, &workspace_3);
db.save_workspace(&workspace_3);
assert_eq!(
db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
workspace_3
);
// Make sure that updating paths differently also works
workspace_3.workspace_id = (["/tmp3", "/tmp4", "/tmp2"]).into();
db.save_workspace(
Some((&["/tmp", "/tmp2"]).into()),
&workspace_3,
);
workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
db.save_workspace(&workspace_3);
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;
@ -545,7 +531,8 @@ mod tests {
center_group: &SerializedPaneGroup,
) -> SerializedWorkspace {
SerializedWorkspace {
workspace_id: workspace_id.into(),
id: WorkspaceId::new(),
location: workspace_id.into(),
dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
center_group: center_group.clone(),
dock_pane,
@ -564,12 +551,13 @@ mod tests {
SerializedItem::new("Terminal", 4),
SerializedItem::new("Terminal", 2),
SerializedItem::new("Terminal", 3),
], false
],
false,
);
let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
db.save_workspace(None, &workspace);
db.save_workspace(&workspace);
let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
@ -593,16 +581,20 @@ mod tests {
SerializedPaneGroup::Group {
axis: gpui::Axis::Vertical,
children: vec![
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 1),
SerializedItem::new("Terminal", 2),
],
false)),
SerializedPaneGroup::Pane(SerializedPane::new(vec![
SerializedItem::new("Terminal", 4),
SerializedItem::new("Terminal", 3),
], true)),
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 1),
SerializedItem::new("Terminal", 2),
],
false,
)),
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 4),
SerializedItem::new("Terminal", 3),
],
true,
)),
],
},
SerializedPaneGroup::Pane(SerializedPane::new(
@ -610,41 +602,46 @@ mod tests {
SerializedItem::new("Terminal", 5),
SerializedItem::new("Terminal", 6),
],
false)),
false,
)),
],
};
let workspace = default_workspace(&["/tmp"], Default::default(), &center_pane);
db.save_workspace(None, &workspace);
db.save_workspace(&workspace);
let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
assert_eq!(workspace.center_group, new_workspace.center_group);
}
#[test]
fn test_cleanup_panes() {
env_logger::try_init().ok();
let db = WorkspaceDb(open_memory_db(Some("test_cleanup_panes")));
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),
SerializedItem::new("Terminal", 2),
],
false)),
SerializedPaneGroup::Pane(SerializedPane::new(vec![
SerializedItem::new("Terminal", 4),
SerializedItem::new("Terminal", 3),
], true)),
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 1),
SerializedItem::new("Terminal", 2),
],
false,
)),
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 4),
SerializedItem::new("Terminal", 3),
],
true,
)),
],
},
SerializedPaneGroup::Pane(SerializedPane::new(
@ -652,37 +649,41 @@ mod tests {
SerializedItem::new("Terminal", 5),
SerializedItem::new("Terminal", 6),
],
false)),
false,
)),
],
};
let id = &["/tmp"];
let mut workspace = default_workspace(id, Default::default(), &center_pane);
db.save_workspace(None, &workspace);
db.save_workspace(&workspace);
workspace.center_group = SerializedPaneGroup::Group {
axis: gpui::Axis::Vertical,
children: vec![
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 1),
SerializedItem::new("Terminal", 2),
],
false)),
SerializedPaneGroup::Pane(SerializedPane::new(vec![
SerializedItem::new("Terminal", 4),
SerializedItem::new("Terminal", 3),
], true)),
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 1),
SerializedItem::new("Terminal", 2),
],
false,
)),
SerializedPaneGroup::Pane(SerializedPane::new(
vec![
SerializedItem::new("Terminal", 4),
SerializedItem::new("Terminal", 3),
],
true,
)),
],
};
db.save_workspace(None, &workspace);
db.save_workspace(&workspace);
let new_workspace = db.workspace_for_roots(id).unwrap();
assert_eq!(workspace.center_group, new_workspace.center_group);
}
}

View file

@ -16,18 +16,20 @@ use project::Project;
use settings::DockAnchor;
use util::ResultExt;
use crate::{dock::DockPosition, ItemDeserializers, Member, Pane, PaneAxis, Workspace};
use crate::{
dock::DockPosition, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceId(Arc<Vec<PathBuf>>);
pub struct WorkspaceLocation(Arc<Vec<PathBuf>>);
impl WorkspaceId {
impl WorkspaceLocation {
pub fn paths(&self) -> Arc<Vec<PathBuf>> {
self.0.clone()
}
}
impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceId {
impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
fn from(iterator: T) -> Self {
let mut roots = iterator
.into_iter()
@ -38,7 +40,7 @@ impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceId {
}
}
impl Bind for &WorkspaceId {
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")
@ -46,16 +48,20 @@ impl Bind for &WorkspaceId {
}
}
impl Column for WorkspaceId {
impl Column for WorkspaceLocation {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let blob = statement.column_blob(start_index)?;
Ok((WorkspaceId(bincode::deserialize(blob)?), start_index + 1))
Ok((
WorkspaceLocation(bincode::deserialize(blob)?),
start_index + 1,
))
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct SerializedWorkspace {
pub workspace_id: WorkspaceId,
pub id: WorkspaceId,
pub location: WorkspaceLocation,
pub dock_position: DockPosition,
pub center_group: SerializedPaneGroup,
pub dock_pane: SerializedPane,
@ -70,10 +76,11 @@ pub enum SerializedPaneGroup {
Pane(SerializedPane),
}
#[cfg(test)]
impl Default for SerializedPaneGroup {
fn default() -> Self {
Self::Pane(SerializedPane {
children: Vec::new(),
children: vec![SerializedItem::default()],
active: false,
})
}
@ -84,7 +91,7 @@ impl SerializedPaneGroup {
pub(crate) async fn deserialize(
&self,
project: &ModelHandle<Project>,
workspace_id: &WorkspaceId,
workspace_id: WorkspaceId,
workspace: &ViewHandle<Workspace>,
cx: &mut AsyncAppContext,
) -> (Member, Option<ViewHandle<Pane>>) {
@ -136,13 +143,12 @@ impl SerializedPane {
&self,
project: &ModelHandle<Project>,
pane_handle: &ViewHandle<Pane>,
workspace_id: &WorkspaceId,
workspace_id: WorkspaceId,
workspace: &ViewHandle<Workspace>,
cx: &mut AsyncAppContext,
) {
for item in self.children.iter() {
let project = project.clone();
let workspace_id = workspace_id.clone();
let item_handle = pane_handle
.update(cx, |_, cx| {
if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
@ -191,6 +197,16 @@ impl SerializedItem {
}
}
#[cfg(test)]
impl Default for SerializedItem {
fn default() -> Self {
SerializedItem {
kind: Arc::from("Terminal"),
item_id: 100000,
}
}
}
impl Bind for &SerializedItem {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let next_index = statement.bind(self.kind.clone(), start_index)?;
@ -231,7 +247,7 @@ mod tests {
use db::sqlez::connection::Connection;
use settings::DockAnchor;
use super::WorkspaceId;
use super::WorkspaceLocation;
#[test]
fn test_workspace_round_trips() {
@ -245,7 +261,7 @@ mod tests {
.unwrap()()
.unwrap();
let workspace_id: WorkspaceId = WorkspaceId::from(&["\test2", "\test1"]);
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))
@ -255,7 +271,10 @@ mod tests {
db.select_row("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1")
.unwrap()()
.unwrap(),
Some((WorkspaceId::from(&["\test1", "\test2"]), DockAnchor::Bottom))
Some((
WorkspaceLocation::from(&["\test1", "\test2"]),
DockAnchor::Bottom
))
);
}
}

View file

@ -1,7 +1,5 @@
use crate::{
item::ItemEvent,
persistence::model::{ItemId, WorkspaceId},
Item, ItemNavHistory, Pane, Workspace,
item::ItemEvent, persistence::model::ItemId, Item, ItemNavHistory, Pane, Workspace, WorkspaceId,
};
use anyhow::{anyhow, Result};
use call::participant::{Frame, RemoteVideoTrack};
@ -148,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))
}

View file

@ -26,6 +26,7 @@ use anyhow::{anyhow, Context, Result};
use call::ActiveCall;
use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
use collections::{hash_map, HashMap, HashSet};
use db::Uuid;
use dock::{DefaultItemFactory, Dock, ToggleDockButton};
use drag_and_drop::DragAndDrop;
use fs::{self, Fs};
@ -45,7 +46,7 @@ use log::{error, warn};
pub use pane::*;
pub use pane_group::*;
use persistence::model::SerializedItem;
pub use persistence::model::{ItemId, WorkspaceId};
pub use persistence::model::{ItemId, WorkspaceLocation};
use postage::prelude::Stream;
use project::{Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
use serde::Deserialize;
@ -128,6 +129,8 @@ pub struct OpenProjectEntryInPane {
project_entry: ProjectEntryId,
}
pub type WorkspaceId = Uuid;
impl_internal_actions!(
workspace,
[
@ -530,6 +533,7 @@ pub struct Workspace {
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
window_edited: bool,
active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
database_id: WorkspaceId,
_observe_current_user: Task<()>,
}
@ -556,7 +560,7 @@ impl Workspace {
project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
this.update_window_title(cx);
// TODO: Cache workspace_id on workspace and read from it here
this.serialize_workspace(None, cx);
this.serialize_workspace(cx);
}
project::Event::DisconnectedFromHost => {
this.update_window_edited(cx);
@ -630,6 +634,12 @@ impl Workspace {
active_call = Some((call, subscriptions));
}
let id = if let Some(id) = serialized_workspace.as_ref().map(|ws| ws.id) {
id
} else {
WorkspaceId::new()
};
let mut this = Workspace {
modal: None,
weak_self: weak_handle.clone(),
@ -657,6 +667,7 @@ impl Workspace {
last_leaders_by_pane: Default::default(),
window_edited: false,
active_call,
database_id: id,
_observe_current_user,
};
this.project_remote_id_changed(project.read(cx).remote_id(), cx);
@ -1317,7 +1328,7 @@ impl Workspace {
pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
let active_pane = self.active_pane().clone();
Pane::add_item(self, &active_pane, item, true, true, None, cx);
self.serialize_workspace(None, cx);
self.serialize_workspace(cx);
}
pub fn open_path(
@ -1522,7 +1533,7 @@ impl Workspace {
entry.remove();
}
}
self.serialize_workspace(None, cx);
self.serialize_workspace(cx);
}
_ => {}
}
@ -1544,7 +1555,7 @@ impl Workspace {
pane.read(cx).active_item().map(|item| {
let new_pane = self.add_pane(cx);
if let Some(clone) = item.clone_on_split(cx.as_mut()) {
if let Some(clone) = item.clone_on_split(self.database_id(), cx.as_mut()) {
Pane::add_item(self, &new_pane, clone, true, true, None, cx);
}
self.center.split(&pane, &new_pane, direction).unwrap();
@ -2255,7 +2266,11 @@ impl Workspace {
}
}
fn workspace_id(&self, cx: &AppContext) -> WorkspaceId {
pub fn database_id(&self) -> WorkspaceId {
self.database_id
}
fn location(&self, cx: &AppContext) -> WorkspaceLocation {
self.project()
.read(cx)
.visible_worktrees(cx)
@ -2275,7 +2290,7 @@ impl Workspace {
}
}
fn serialize_workspace(&self, old_id: Option<WorkspaceId>, cx: &AppContext) {
fn serialize_workspace(&self, cx: &AppContext) {
fn serialize_pane_handle(
pane_handle: &ViewHandle<Pane>,
cx: &AppContext,
@ -2320,7 +2335,8 @@ impl Workspace {
let center_group = build_serialized_pane_group(&self.center.root, cx);
let serialized_workspace = SerializedWorkspace {
workspace_id: self.workspace_id(cx),
id: self.database_id,
location: self.location(cx),
dock_position: self.dock.position(),
dock_pane,
center_group,
@ -2328,7 +2344,7 @@ impl Workspace {
cx.background()
.spawn(async move {
persistence::DB.save_workspace(old_id, &serialized_workspace);
persistence::DB.save_workspace(&serialized_workspace);
})
.detach();
}
@ -2349,7 +2365,7 @@ impl Workspace {
.deserialize_to(
&project,
&dock_pane_handle,
&serialized_workspace.workspace_id,
serialized_workspace.id,
&workspace,
&mut cx,
)
@ -2359,12 +2375,7 @@ impl Workspace {
let (root, active_pane) = serialized_workspace
.center_group
.deserialize(
&project,
&serialized_workspace.workspace_id,
&workspace,
&mut cx,
)
.deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
.await;
// Remove old panes from workspace panes list

View file

@ -597,6 +597,8 @@ pub fn default_item_factory(
let working_directory = get_working_directory(workspace, cx, strategy);
let terminal_handle = cx.add_view(|cx| TerminalContainer::new(working_directory, false, cx));
let terminal_handle = cx.add_view(|cx| {
TerminalContainer::new(working_directory, false, workspace.database_id(), cx)
});
Box::new(terminal_handle)
}