From 1cc3e4820a1e32c32b3e6f41d8b55b9800b047bd Mon Sep 17 00:00:00 2001 From: Kay Simmons Date: Wed, 23 Nov 2022 01:53:58 -0800 Subject: [PATCH] working serialized writes with panics on failure. Everything seems to be working --- Cargo.lock | 3 + crates/collab/src/integration_tests.rs | 4 + crates/collab_ui/src/collab_ui.rs | 1 + crates/command_palette/src/command_palette.rs | 2 +- crates/db/src/db.rs | 143 ++++++++- crates/db/src/kvp.rs | 2 +- crates/db/test.db | Bin 40960 -> 0 bytes crates/diagnostics/src/diagnostics.rs | 1 + crates/editor/src/editor.rs | 29 +- crates/editor/src/items.rs | 39 ++- crates/editor/src/persistence.rs | 4 +- .../src/test/editor_lsp_test_context.rs | 1 + crates/file_finder/src/file_finder.rs | 12 +- crates/project_panel/src/project_panel.rs | 2 + crates/sqlez/Cargo.toml | 5 +- crates/sqlez/src/bindable.rs | 12 + crates/sqlez/src/connection.rs | 12 +- crates/sqlez/src/lib.rs | 5 +- crates/sqlez/src/migrations.rs | 58 ++-- crates/sqlez/src/statement.rs | 11 +- crates/sqlez/src/thread_safe_connection.rs | 133 +++++--- crates/sqlez/src/util.rs | 28 ++ crates/terminal/src/persistence.rs | 40 ++- crates/terminal/src/terminal.rs | 21 +- .../terminal/src/terminal_container_view.rs | 8 + .../src/tests/terminal_test_context.rs | 1 + crates/vim/src/test/vim_test_context.rs | 1 + crates/workspace/src/dock.rs | 2 +- crates/workspace/src/item.rs | 11 +- crates/workspace/src/pane.rs | 8 +- crates/workspace/src/persistence.rs | 295 ++++++++++-------- crates/workspace/src/persistence/model.rs | 2 +- crates/workspace/src/workspace.rs | 71 +++-- crates/zed/src/zed.rs | 14 +- 34 files changed, 669 insertions(+), 312 deletions(-) delete mode 100644 crates/db/test.db create mode 100644 crates/sqlez/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index e887dfee66..150149c529 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5590,8 +5590,11 @@ name = "sqlez" version = "0.1.0" dependencies = [ "anyhow", + "futures 0.3.25", "indoc", + "lazy_static", "libsqlite3-sys", + "parking_lot 0.11.2", "thread_local", ] diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 386ccfbbff..989f0ac586 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -909,6 +909,7 @@ async fn test_host_disconnect( let (_, workspace_b) = cx_b.add_window(|cx| { Workspace::new( Default::default(), + 0, project_b.clone(), |_, _| unimplemented!(), cx, @@ -3711,6 +3712,7 @@ async fn test_collaborating_with_code_actions( let (_window_b, workspace_b) = cx_b.add_window(|cx| { Workspace::new( Default::default(), + 0, project_b.clone(), |_, _| unimplemented!(), cx, @@ -3938,6 +3940,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T let (_window_b, workspace_b) = cx_b.add_window(|cx| { Workspace::new( Default::default(), + 0, project_b.clone(), |_, _| unimplemented!(), cx, @@ -6075,6 +6078,7 @@ impl TestClient { cx.add_view(&root_view, |cx| { Workspace::new( Default::default(), + 0, project.clone(), |_, _| unimplemented!(), cx, diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 3a20a2fc69..964cec0f82 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -53,6 +53,7 @@ pub fn init(app_state: Arc, cx: &mut MutableAppContext) { let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { let mut workspace = Workspace::new( Default::default(), + 0, project, app_state.default_item_factory, cx, diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 5af23b45d7..3742e36c72 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -351,7 +351,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), [], cx).await; let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); let editor = cx.add_view(&workspace, |cx| { let mut editor = Editor::single_line(None, cx); diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index b3370db753..b42b264b56 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -42,11 +42,11 @@ pub fn open_file_db() -> ThreadSafeConnection { create_dir_all(¤t_db_dir).expect("Should be able to create the database directory"); let db_path = current_db_dir.join(Path::new("db.sqlite")); - ThreadSafeConnection::new(Some(db_path.to_string_lossy().as_ref()), true) + ThreadSafeConnection::new(db_path.to_string_lossy().as_ref(), true) .with_initialize_query(INITIALIZE_QUERY) } -pub fn open_memory_db(db_name: Option<&str>) -> ThreadSafeConnection { +pub fn open_memory_db(db_name: &str) -> ThreadSafeConnection { ThreadSafeConnection::new(db_name, false).with_initialize_query(INITIALIZE_QUERY) } @@ -66,7 +66,7 @@ macro_rules! connection { ::db::lazy_static::lazy_static! { pub static ref $id: $t = $t(if cfg!(any(test, feature = "test-support")) { - ::db::open_memory_db(None) + ::db::open_memory_db(stringify!($id)) } else { ::db::open_file_db() }); @@ -77,7 +77,7 @@ macro_rules! connection { #[macro_export] macro_rules! sql_method { ($id:ident() -> Result<()>: $sql:expr) => { - pub fn $id(&self) -> $crate::sqlez::anyhow::Result<()> { + pub fn $id(&self) -> $crate::anyhow::Result<()> { use $crate::anyhow::Context; self.exec($sql)?().context(::std::format!( @@ -87,8 +87,21 @@ macro_rules! sql_method { )) } }; + (async $id:ident() -> Result<()>: $sql:expr) => { + pub async fn $id(&self) -> $crate::anyhow::Result<()> { + use $crate::anyhow::Context; + + self.write(|connection| { + connection.exec($sql)?().context(::std::format!( + "Error in {}, exec failed to execute or parse for: {}", + ::std::stringify!($id), + ::std::stringify!($sql), + )) + }).await + } + }; ($id:ident($($arg:ident: $arg_type:ty),+) -> Result<()>: $sql:expr) => { - pub fn $id(&self, $($arg: $arg_type),+) -> $crate::sqlez::anyhow::Result<()> { + pub fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> { use $crate::anyhow::Context; self.exec_bound::<($($arg_type),+)>($sql)?(($($arg),+)) @@ -99,8 +112,22 @@ macro_rules! sql_method { )) } }; + (async $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()>: $sql:expr) => { + pub async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> { + use $crate::anyhow::Context; + + self.write(move |connection| { + connection.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), + )) + }).await + } + }; ($id:ident() -> Result>: $sql:expr) => { - pub fn $id(&self) -> $crate::sqlez::anyhow::Result> { + pub fn $id(&self) -> $crate::anyhow::Result> { use $crate::anyhow::Context; self.select::<$return_type>($sql)?(()) @@ -111,8 +138,22 @@ macro_rules! sql_method { )) } }; + (async $id:ident() -> Result>: $sql:expr) => { + pub async fn $id(&self) -> $crate::anyhow::Result> { + use $crate::anyhow::Context; + + self.write(|connection| { + connection.select::<$return_type>($sql)?(()) + .context(::std::format!( + "Error in {}, select_row failed to execute or parse for: {}", + ::std::stringify!($id), + ::std::stringify!($sql), + )) + }).await + } + }; ($id:ident($($arg:ident: $arg_type:ty),+) -> Result>: $sql:expr) => { - pub fn $id(&self, $($arg: $arg_type),+) -> $crate::sqlez::anyhow::Result> { + pub fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result> { use $crate::anyhow::Context; self.select_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+)) @@ -123,8 +164,22 @@ macro_rules! sql_method { )) } }; + (async $id:ident($($arg:ident: $arg_type:ty),+) -> Result>: $sql:expr) => { + pub async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result> { + use $crate::anyhow::Context; + + self.write(|connection| { + connection.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), + )) + }).await + } + }; ($id:ident() -> Result>: $sql:expr) => { - pub fn $id(&self) -> $crate::sqlez::anyhow::Result> { + pub fn $id(&self) -> $crate::anyhow::Result> { use $crate::anyhow::Context; self.select_row::<$return_type>($sql)?() @@ -135,8 +190,22 @@ macro_rules! sql_method { )) } }; + (async $id:ident() -> Result>: $sql:expr) => { + pub async fn $id(&self) -> $crate::anyhow::Result> { + use $crate::anyhow::Context; + + self.write(|connection| { + connection.select_row::<$return_type>($sql)?() + .context(::std::format!( + "Error in {}, select_row failed to execute or parse for: {}", + ::std::stringify!($id), + ::std::stringify!($sql), + )) + }).await + } + }; ($id:ident($($arg:ident: $arg_type:ty),+) -> Result>: $sql:expr) => { - pub fn $id(&self, $($arg: $arg_type),+) -> $crate::sqlez::anyhow::Result> { + pub fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result> { use $crate::anyhow::Context; self.select_row_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+)) @@ -148,8 +217,22 @@ macro_rules! sql_method { } }; + (async $id:ident($($arg:ident: $arg_type:ty),+) -> Result>: $sql:expr) => { + pub async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result> { + use $crate::anyhow::Context; + + self.write(|connection| { + connection.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), + )) + }).await + } + }; ($id:ident() -> Result<$return_type:ty>: $sql:expr) => { - pub fn $id(&self) -> $crate::sqlez::anyhow::Result<$return_type> { + pub fn $id(&self) -> $crate::anyhow::Result<$return_type> { use $crate::anyhow::Context; self.select_row::<$return_type>($sql)?() @@ -165,8 +248,27 @@ macro_rules! sql_method { )) } }; + (async $id:ident() -> Result<$return_type:ty>: $sql:expr) => { + pub async fn $id(&self) -> $crate::anyhow::Result<$return_type> { + use $crate::anyhow::Context; + + self.write(|connection| { + connection.select_row::<$return_type>($sql)?() + .context(::std::format!( + "Error in {}, select_row_bound failed to execute or parse for: {}", + ::std::stringify!($id), + ::std::stringify!($sql), + ))? + .context(::std::format!( + "Error in {}, select_row_bound expected single row result but found none for: {}", + ::std::stringify!($id), + ::std::stringify!($sql), + )) + }).await + } + }; ($id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty>: $sql:expr) => { - pub fn $id(&self, $($arg: $arg_type),+) -> $crate::sqlez::anyhow::Result<$return_type> { + pub fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> { use $crate::anyhow::Context; self.select_row_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+)) @@ -182,4 +284,23 @@ macro_rules! sql_method { )) } }; + (async $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty>: $sql:expr) => { + pub async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> { + use $crate::anyhow::Context; + + self.write(|connection| { + connection.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), + ))? + .context(::std::format!( + "Error in {}, select_row_bound expected single row result but found none for: {}", + ::std::stringify!($id), + ::std::stringify!($sql), + )) + }).await + } + }; } diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index 3cdcd99016..dd82c17615 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -61,7 +61,7 @@ mod tests { #[test] fn test_kvp() -> Result<()> { - let db = KeyValueStore(crate::open_memory_db(Some("test_kvp"))); + let db = KeyValueStore(crate::open_memory_db("test_kvp")); assert_eq!(db.read_kvp("key-1").unwrap(), None); diff --git a/crates/db/test.db b/crates/db/test.db deleted file mode 100644 index cedefe5f832586d90e62e9a50c9e8c7506cf81e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40960 zcmWFz^vNtqRY=P(%1ta$FlG>7U}R))P*7lCVBlk5VBlgv01%%A!DV1XV&h`+G3doh z@$$DYaB|o&@b~aN<`3pw!?%um3zs^F9p@ih+D64kLtr!nMnhmU1V%$(Gz1232sB%B zu#1a}Gq#16BqrsgW|pMp7J~^+=O9Hj$qTuNl;_B`i zq~PZtqTmC5jyj>$TkR70@5ajCS8szHd>>8{9mncS44Z##wGZ#BJqFal>5e;%fF__>$iD8LwICBwS$5H%4Elg z7^nm!dSLdzlQ-4lMj-g3q{I23`~=Q&7_kn@N%4t!$r<@Y;4lT}9Y|2B zJLQ*@gS!Qu&Qcfx?X@QM!%FoY9P0RxadP!z( zYH>+oZUNW^1s7L0$55XT1?SM9AXmST_z+KD*WeIG-vD@t3@@>aS=q$h6>*hVaPJkz z8zR_jc}(o$va*bg`ruH@E{iWN$uCNU(ab0z1`~yZS9WS8#EBTeRF;@inhKGF)nIT( z>NB&6JIms7B+Q0*Ll_Ixr{)0F@kNZ_=!H5oH#5B`5mW;eBUnr*jzx$oK-`;>pPQJO z2X+=DB^H;Y7Qmwc9QJTMU{ze2ybKHsES#$t_!skS;*Q~51#-tI9u0xf5Eu=C(GVC7 zfzc2c4S~@R7z`oMtk1$K%Fz}H>X4%}13?1CkOm-#32NKJI^D3gDYywfI=TWMu>tp! zL1QRLgEb&AYL2d0bF~FCvx;)`DuPT#i7}ACXm5Oo_r|TcxG*9WnhnA6i!{iI(S3yX zPC=O!+!2T8RB$g1ZL|p9g#(RijO?*5Yp%vTCRTZlW?y(1qQnAR6dDz9K0Kr`A_F{T zF=&T8thu5YS%oLtr!nMnhmU1V%$( zGz3ONU^E0qLtr!nMnhmU1O`P2fad?1_&XW+_w#oS3I~lkVl)IsLtr!nMnhmU1V%$( zGz3ONU^E0qLtr!nMnhmU1cq}6m@u)L$Z{CN4%Gu43IOu1eo1Zt?1%*;10y3{LqlBy zGX+BfD?<}2LsNT3RzFEjVLqL*+71X20)}fbTW@Rv8f#gZ;j1ySGWc, searchable: bool, cursor_shape: CursorShape, + workspace_id: Option, keymap_context_layers: BTreeMap, input_enabled: bool, leader_replica_id: Option, @@ -1137,31 +1138,6 @@ impl Editor { cx: &mut ViewContext, ) -> 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::>() - // // .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(); - // } - // } - Self::new(EditorMode::Full, buffer, project, None, cx) } @@ -1262,6 +1238,7 @@ impl Editor { searchable: true, override_text_style: None, cursor_shape: Default::default(), + workspace_id: None, keymap_context_layers: Default::default(), input_enabled: true, leader_replica_id: None, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index aea0d8b437..e724156fae 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -17,7 +17,7 @@ use std::{ path::{Path, PathBuf}, }; use text::Selection; -use util::TryFutureExt; +use util::{ResultExt, TryFutureExt}; use workspace::{ item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, @@ -554,6 +554,43 @@ impl Item for Editor { Some(breadcrumbs) } + fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { + let workspace_id = workspace.database_id(); + let item_id = cx.view_id(); + + fn serialize( + buffer: ModelHandle, + workspace_id: WorkspaceId, + item_id: ItemId, + cx: &mut MutableAppContext, + ) { + if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { + let path = file.abs_path(cx); + + cx.background() + .spawn(async move { + DB.save_path(item_id, workspace_id, path.clone()) + .await + .log_err() + }) + .detach(); + } + } + + if let Some(buffer) = self.buffer().read(cx).as_singleton() { + serialize(buffer.clone(), workspace_id, item_id, cx); + + cx.subscribe(&buffer, |this, buffer, event, cx| { + if let Some(workspace_id) = this.workspace_id { + if let language::Event::FileHandleChanged = event { + serialize(buffer, workspace_id, cx.view_id(), cx); + } + } + }) + .detach(); + } + } + fn serialized_item_kind() -> Option<&'static str> { Some("Editor") } diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index a77eec7fd1..b2f76294aa 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use db::{connection, sql_method}; use indoc::indoc; @@ -39,7 +39,7 @@ impl EditorDb { } sql_method! { - save_path(item_id: ItemId, workspace_id: WorkspaceId, path: &Path) -> Result<()>: + async save_path(item_id: ItemId, workspace_id: WorkspaceId, path: PathBuf) -> Result<()>: indoc! {" INSERT OR REPLACE INTO editors(item_id, workspace_id, path) VALUES (?, ?, ?)"} diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 9cf305ad37..b65b09cf17 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -66,6 +66,7 @@ impl<'a> EditorLspTestContext<'a> { let (window_id, workspace) = cx.add_window(|cx| { Workspace::new( Default::default(), + 0, project.clone(), |_, _| unimplemented!(), cx, diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index b0016002fa..5122a46c2c 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -317,7 +317,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); cx.dispatch_action(window_id, Toggle); @@ -373,7 +373,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); let (_, finder) = cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx)); @@ -449,7 +449,7 @@ mod tests { ) .await; let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); let (_, finder) = cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx)); @@ -475,7 +475,7 @@ mod tests { ) .await; let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); let (_, finder) = cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx)); @@ -529,7 +529,7 @@ mod tests { ) .await; let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); let (_, finder) = cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx)); @@ -569,7 +569,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); let (_, finder) = cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx)); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index dae1f70aae..e88f3004eb 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1396,6 +1396,7 @@ mod tests { let (_, workspace) = cx.add_window(|cx| { Workspace::new( Default::default(), + 0, project.clone(), |_, _| unimplemented!(), cx, @@ -1495,6 +1496,7 @@ mod tests { let (_, workspace) = cx.add_window(|cx| { Workspace::new( Default::default(), + 0, project.clone(), |_, _| unimplemented!(), cx, diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml index cbb4504a04..cab1af7d6c 100644 --- a/crates/sqlez/Cargo.toml +++ b/crates/sqlez/Cargo.toml @@ -9,4 +9,7 @@ edition = "2021" anyhow = { version = "1.0.38", features = ["backtrace"] } indoc = "1.0.7" libsqlite3-sys = { version = "0.25.2", features = ["bundled"] } -thread_local = "1.1.4" \ No newline at end of file +thread_local = "1.1.4" +lazy_static = "1.4" +parking_lot = "0.11.1" +futures = "0.3" \ No newline at end of file diff --git a/crates/sqlez/src/bindable.rs b/crates/sqlez/src/bindable.rs index 51f67dd03f..ffef7814f9 100644 --- a/crates/sqlez/src/bindable.rs +++ b/crates/sqlez/src/bindable.rs @@ -322,6 +322,18 @@ impl Bind for &Path { } } +impl Bind for Arc { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + self.as_ref().bind(statement, start_index) + } +} + +impl Bind for PathBuf { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + (self.as_ref() as &Path).bind(statement, start_index) + } +} + impl Column for PathBuf { fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { let blob = statement.column_blob(start_index)?; diff --git a/crates/sqlez/src/connection.rs b/crates/sqlez/src/connection.rs index 5a71cefb52..4beddb4fed 100644 --- a/crates/sqlez/src/connection.rs +++ b/crates/sqlez/src/connection.rs @@ -10,16 +10,18 @@ use libsqlite3_sys::*; pub struct Connection { pub(crate) sqlite3: *mut sqlite3, persistent: bool, - phantom: PhantomData, + pub(crate) write: bool, + _sqlite: PhantomData, } unsafe impl Send for Connection {} impl Connection { - fn open(uri: &str, persistent: bool) -> Result { + pub(crate) fn open(uri: &str, persistent: bool) -> Result { let mut connection = Self { sqlite3: 0 as *mut _, persistent, - phantom: PhantomData, + write: true, + _sqlite: PhantomData, }; let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_READWRITE; @@ -60,6 +62,10 @@ impl Connection { self.persistent } + pub fn can_write(&self) -> bool { + self.write + } + pub fn backup_main(&self, destination: &Connection) -> Result<()> { unsafe { let backup = sqlite3_backup_init( diff --git a/crates/sqlez/src/lib.rs b/crates/sqlez/src/lib.rs index c5d2658666..a22cfff2b3 100644 --- a/crates/sqlez/src/lib.rs +++ b/crates/sqlez/src/lib.rs @@ -1,5 +1,3 @@ -pub use anyhow; - pub mod bindable; pub mod connection; pub mod domain; @@ -8,3 +6,6 @@ pub mod savepoint; pub mod statement; pub mod thread_safe_connection; pub mod typed_statements; +mod util; + +pub use anyhow; diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index 23af04bbf4..6c0aafaf20 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -11,46 +11,48 @@ use crate::connection::Connection; impl Connection { pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> { - // Setup the migrations table unconditionally - self.exec(indoc! {" - CREATE TABLE IF NOT EXISTS migrations ( + self.with_savepoint("migrating", || { + // Setup the migrations table unconditionally + self.exec(indoc! {" + CREATE TABLE IF NOT EXISTS migrations ( domain TEXT, step INTEGER, migration TEXT - )"})?()?; + )"})?()?; - let completed_migrations = - self.select_bound::<&str, (String, usize, String)>(indoc! {" + let completed_migrations = + self.select_bound::<&str, (String, usize, String)>(indoc! {" SELECT domain, step, migration FROM migrations WHERE domain = ? ORDER BY step - "})?(domain)?; + "})?(domain)?; - let mut store_completed_migration = - self.exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?; + let mut store_completed_migration = self + .exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?; - for (index, migration) in migrations.iter().enumerate() { - if let Some((_, _, completed_migration)) = completed_migrations.get(index) { - if completed_migration != migration { - return Err(anyhow!(formatdoc! {" - Migration changed for {} at step {} - - Stored migration: - {} - - Proposed migration: - {}", domain, index, completed_migration, migration})); - } else { - // Migration already run. Continue - continue; + for (index, migration) in migrations.iter().enumerate() { + if let Some((_, _, completed_migration)) = completed_migrations.get(index) { + if completed_migration != migration { + return Err(anyhow!(formatdoc! {" + Migration changed for {} at step {} + + Stored migration: + {} + + Proposed migration: + {}", domain, index, completed_migration, migration})); + } else { + // Migration already run. Continue + continue; + } } + + self.exec(migration)?()?; + store_completed_migration((domain, index, *migration))?; } - self.exec(migration)?()?; - store_completed_migration((domain, index, *migration))?; - } - - Ok(()) + Ok(()) + }) } } diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs index 0a7305c6ed..86035f5d0a 100644 --- a/crates/sqlez/src/statement.rs +++ b/crates/sqlez/src/statement.rs @@ -2,7 +2,7 @@ use std::ffi::{c_int, CStr, CString}; use std::marker::PhantomData; use std::{ptr, slice, str}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use libsqlite3_sys::*; use crate::bindable::{Bind, Column}; @@ -57,12 +57,21 @@ impl<'a> Statement<'a> { &mut raw_statement, &mut remaining_sql_ptr, ); + remaining_sql = CStr::from_ptr(remaining_sql_ptr); statement.raw_statements.push(raw_statement); connection.last_error().with_context(|| { format!("Prepare call failed for query:\n{}", query.as_ref()) })?; + + if !connection.can_write() && sqlite3_stmt_readonly(raw_statement) == 0 { + let sql = CStr::from_ptr(sqlite3_sql(raw_statement)); + + bail!( + "Write statement prepared with connection that is not write capable. SQL:\n{} ", + sql.to_str()?) + } } } diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index 7c5bf6388c..5402c6b5e1 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -1,36 +1,41 @@ -use std::{marker::PhantomData, ops::Deref, sync::Arc}; - -use connection::Connection; +use futures::{Future, FutureExt}; +use lazy_static::lazy_static; +use parking_lot::RwLock; +use std::{collections::HashMap, marker::PhantomData, ops::Deref, sync::Arc, thread}; use thread_local::ThreadLocal; use crate::{ - connection, + connection::Connection, domain::{Domain, Migrator}, + util::UnboundedSyncSender, }; +type QueuedWrite = Box; + +lazy_static! { + static ref QUEUES: RwLock, UnboundedSyncSender>> = + Default::default(); +} + pub struct ThreadSafeConnection { - uri: Option>, + uri: Arc, persistent: bool, initialize_query: Option<&'static str>, - connection: Arc>, - _pd: PhantomData, + connections: Arc>, + _migrator: PhantomData, } unsafe impl Send for ThreadSafeConnection {} unsafe impl Sync for ThreadSafeConnection {} impl ThreadSafeConnection { - pub fn new(uri: Option<&str>, persistent: bool) -> Self { - if persistent == true && uri == None { - // This panic is securing the unwrap in open_file(), don't remove it! - panic!("Cannot create a persistent connection without a URI") - } + pub fn new(uri: &str, persistent: bool) -> Self { Self { - uri: uri.map(|str| Arc::from(str)), + uri: Arc::from(uri), persistent, initialize_query: None, - connection: Default::default(), - _pd: PhantomData, + connections: Default::default(), + _migrator: PhantomData, } } @@ -46,13 +51,13 @@ impl ThreadSafeConnection { /// If opening fails, the connection falls back to a shared memory connection fn open_file(&self) -> Connection { // This unwrap is secured by a panic in the constructor. Be careful if you remove it! - Connection::open_file(self.uri.as_ref().unwrap()) + Connection::open_file(self.uri.as_ref()) } /// Opens a shared memory connection using the file path as the identifier. This unwraps /// as we expect it always to succeed fn open_shared_memory(&self) -> Connection { - Connection::open_memory(self.uri.as_ref().map(|str| str.deref())) + Connection::open_memory(Some(self.uri.as_ref())) } // Open a new connection for the given domain, leaving this @@ -62,10 +67,74 @@ impl ThreadSafeConnection { uri: self.uri.clone(), persistent: self.persistent, initialize_query: self.initialize_query, - connection: Default::default(), - _pd: PhantomData, + connections: Default::default(), + _migrator: PhantomData, } } + + pub fn write( + &self, + callback: impl 'static + Send + FnOnce(&Connection) -> T, + ) -> impl Future { + // Startup write thread for this database if one hasn't already + // been started and insert a channel to queue work for it + if !QUEUES.read().contains_key(&self.uri) { + use std::sync::mpsc::channel; + + let (sender, reciever) = channel::(); + let mut write_connection = self.create_connection(); + // Enable writes for this connection + write_connection.write = true; + thread::spawn(move || { + while let Ok(write) = reciever.recv() { + write(&write_connection) + } + }); + + let mut queues = QUEUES.write(); + queues.insert(self.uri.clone(), UnboundedSyncSender::new(sender)); + } + + // Grab the queue for this database + let queues = QUEUES.read(); + let write_channel = queues.get(&self.uri).unwrap(); + + // Create a one shot channel for the result of the queued write + // so we can await on the result + let (sender, reciever) = futures::channel::oneshot::channel(); + write_channel + .send(Box::new(move |connection| { + sender.send(callback(connection)).ok(); + })) + .expect("Could not send write action to background thread"); + + reciever.map(|response| response.expect("Background thread unexpectedly closed")) + } + + pub(crate) fn create_connection(&self) -> Connection { + let mut connection = if self.persistent { + self.open_file() + } else { + self.open_shared_memory() + }; + + // Enable writes for the migrations and initialization queries + connection.write = true; + + if let Some(initialize_query) = self.initialize_query { + connection.exec(initialize_query).expect(&format!( + "Initialize query failed to execute: {}", + initialize_query + ))() + .unwrap(); + } + + M::migrate(&connection).expect("Migrations failed"); + + // Disable db writes for normal thread local connection + connection.write = false; + connection + } } impl Clone for ThreadSafeConnection { @@ -74,8 +143,8 @@ impl Clone for ThreadSafeConnection { uri: self.uri.clone(), persistent: self.persistent, initialize_query: self.initialize_query.clone(), - connection: self.connection.clone(), - _pd: PhantomData, + connections: self.connections.clone(), + _migrator: PhantomData, } } } @@ -88,25 +157,7 @@ impl Deref for ThreadSafeConnection { type Target = Connection; fn deref(&self) -> &Self::Target { - self.connection.get_or(|| { - let connection = if self.persistent { - self.open_file() - } else { - self.open_shared_memory() - }; - - if let Some(initialize_query) = self.initialize_query { - connection.exec(initialize_query).expect(&format!( - "Initialize query failed to execute: {}", - initialize_query - ))() - .unwrap(); - } - - M::migrate(&connection).expect("Migrations failed"); - - connection - }) + self.connections.get_or(|| self.create_connection()) } } @@ -151,7 +202,7 @@ mod test { } } - let _ = ThreadSafeConnection::::new(None, false) + let _ = ThreadSafeConnection::::new("wild_zed_lost_failure", false) .with_initialize_query("PRAGMA FOREIGN_KEYS=true") .deref(); } diff --git a/crates/sqlez/src/util.rs b/crates/sqlez/src/util.rs new file mode 100644 index 0000000000..b5366cffc4 --- /dev/null +++ b/crates/sqlez/src/util.rs @@ -0,0 +1,28 @@ +use std::ops::Deref; +use std::sync::mpsc::Sender; + +use parking_lot::Mutex; +use thread_local::ThreadLocal; + +pub struct UnboundedSyncSender { + clonable_sender: Mutex>, + local_senders: ThreadLocal>, +} + +impl UnboundedSyncSender { + pub fn new(sender: Sender) -> Self { + Self { + clonable_sender: Mutex::new(sender), + local_senders: ThreadLocal::new(), + } + } +} + +impl Deref for UnboundedSyncSender { + type Target = Sender; + + fn deref(&self) -> &Self::Target { + self.local_senders + .get_or(|| self.clonable_sender.lock().clone()) + } +} diff --git a/crates/terminal/src/persistence.rs b/crates/terminal/src/persistence.rs index 07bca0c66f..1e9b846f38 100644 --- a/crates/terminal/src/persistence.rs +++ b/crates/terminal/src/persistence.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use db::{connection, indoc, sql_method, sqlez::domain::Domain}; @@ -17,7 +17,7 @@ impl Domain for Terminal { &[indoc! {" CREATE TABLE terminals ( workspace_id INTEGER, - item_id INTEGER, + item_id INTEGER UNIQUE, working_directory BLOB, PRIMARY KEY(workspace_id, item_id), FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) @@ -29,21 +29,35 @@ impl Domain for Terminal { impl TerminalDb { sql_method! { - save_working_directory(item_id: ItemId, - workspace_id: WorkspaceId, - working_directory: &Path) -> Result<()>: - indoc!{" - INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory) - VALUES (?1, ?2, ?3) + async update_workspace_id( + new_id: WorkspaceId, + old_id: WorkspaceId, + item_id: ItemId + ) -> Result<()>: + indoc! {" + UPDATE terminals + SET workspace_id = ? + WHERE workspace_id = ? AND item_id = ? "} } + sql_method! { + async save_working_directory( + item_id: ItemId, + workspace_id: WorkspaceId, + working_directory: PathBuf) -> Result<()>: + indoc!{" + INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory) + VALUES (?1, ?2, ?3) + "} + } + sql_method! { get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result>: - indoc!{" - SELECT working_directory - FROM terminals - WHERE item_id = ? AND workspace_id = ? - "} + indoc!{" + SELECT working_directory + FROM terminals + WHERE item_id = ? AND workspace_id = ? + "} } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index b5192b6876..0cbb6d36b1 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -57,7 +57,8 @@ use gpui::{ geometry::vector::{vec2f, Vector2F}, keymap::Keystroke, scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp}, - ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, Task, + AppContext, ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, + MutableAppContext, Task, }; use crate::mappings::{ @@ -585,7 +586,8 @@ impl Terminal { cx.background() .spawn(async move { TERMINAL_CONNECTION - .save_working_directory(item_id, workspace_id, cwd.as_path()) + .save_working_directory(item_id, workspace_id, cwd) + .await .log_err(); }) .detach(); @@ -1192,6 +1194,21 @@ impl Terminal { } } + pub fn set_workspace_id(&mut self, id: WorkspaceId, cx: &AppContext) { + let old_workspace_id = self.workspace_id; + let item_id = self.item_id; + cx.background() + .spawn(async move { + TERMINAL_CONNECTION + .update_workspace_id(id, old_workspace_id, item_id) + .await + .log_err() + }) + .detach(); + + self.workspace_id = id; + } + pub fn find_matches( &mut self, query: project::search::SearchQuery, diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs index fdda388642..a6c28d4baf 100644 --- a/crates/terminal/src/terminal_container_view.rs +++ b/crates/terminal/src/terminal_container_view.rs @@ -400,6 +400,14 @@ impl Item for TerminalContainer { ) }))) } + + fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { + if let Some(connected) = self.connected() { + let id = workspace.database_id(); + let terminal_handle = connected.read(cx).terminal().clone(); + terminal_handle.update(cx, |terminal, cx| terminal.set_workspace_id(id, cx)) + } + } } impl SearchableItem for TerminalContainer { diff --git a/crates/terminal/src/tests/terminal_test_context.rs b/crates/terminal/src/tests/terminal_test_context.rs index 352ce4a0d2..67ebb55805 100644 --- a/crates/terminal/src/tests/terminal_test_context.rs +++ b/crates/terminal/src/tests/terminal_test_context.rs @@ -31,6 +31,7 @@ impl<'a> TerminalTestContext<'a> { let (_, workspace) = self.cx.add_window(|cx| { Workspace::new( Default::default(), + 0, project.clone(), |_, _| unimplemented!(), cx, diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 68c08f2f7a..e0d972896f 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -44,6 +44,7 @@ impl<'a> VimTestContext<'a> { let (window_id, workspace) = cx.add_window(|cx| { Workspace::new( Default::default(), + 0, project.clone(), |_, _| unimplemented!(), cx, diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index fb28571172..0879166bbe 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -575,7 +575,7 @@ mod tests { cx.update(|cx| init(cx)); let project = Project::test(fs, [], cx).await; let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, default_item_factory, cx) + Workspace::new(Default::default(), 0, project, default_item_factory, cx) }); workspace.update(cx, |workspace, cx| { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index b990ba20a2..e44e7ca09d 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -119,6 +119,8 @@ pub trait Item: View { None } + fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext) {} + fn serialized_item_kind() -> Option<&'static str>; fn deserialize( @@ -267,7 +269,10 @@ impl ItemHandle for ViewHandle { cx: &mut ViewContext, ) { let history = pane.read(cx).nav_history_for_item(self); - self.update(cx, |this, cx| this.set_nav_history(history, cx)); + 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) { @@ -426,6 +431,10 @@ impl ItemHandle for ViewHandle { }) .detach(); } + + cx.defer(|workspace, cx| { + workspace.serialize_workspace(cx); + }); } fn deactivated(&self, cx: &mut MutableAppContext) { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 5db8d6feec..428865ec3b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1647,7 +1647,7 @@ mod tests { let project = Project::test(fs, None, cx).await; let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); @@ -1737,7 +1737,7 @@ mod tests { let project = Project::test(fs, None, cx).await; let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); @@ -1815,7 +1815,7 @@ mod tests { let project = Project::test(fs, None, cx).await; let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); @@ -1926,7 +1926,7 @@ mod tests { let project = Project::test(fs, None, cx).await; let (_, workspace) = - cx.add_window(|cx| Workspace::new(None, 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); diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 477e5a4960..66b3622119 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -5,7 +5,7 @@ pub mod model; use std::path::Path; use anyhow::{anyhow, bail, Context, Result}; -use db::{connection, sql_method}; +use db::{connection, sql_method, sqlez::connection::Connection}; use gpui::Axis; use indoc::indoc; @@ -138,60 +138,71 @@ impl WorkspaceDb { /// Saves a workspace using the worktree roots. Will garbage collect any workspaces /// that used this workspace previously - 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;"})?(workspace.id) - .context("Clearing old panes")?; + 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(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;"})?(workspace.id) + .context("Clearing old panes")?; - self.exec_bound(indoc! {" - DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ? - "})?((&workspace.location, workspace.id)) - .context("clearing out old locations")?; - - // Upsert - self.exec_bound(indoc! { - "INSERT INTO - workspaces(workspace_id, workspace_location, dock_visible, dock_anchor, timestamp) - VALUES - (?1, ?2, ?3, ?4, CURRENT_TIMESTAMP) - ON CONFLICT DO UPDATE SET - workspace_location = ?2, dock_visible = ?3, dock_anchor = ?4, timestamp = CURRENT_TIMESTAMP" - })?((workspace.id, &workspace.location, workspace.dock_position)) - .context("Updating workspace")?; + conn.exec_bound(indoc! {" + DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?"})?( + ( + &workspace.location, + workspace.id.clone(), + ) + ) + .context("clearing out old locations")?; - // Save center pane group and dock pane - self.save_pane_group(workspace.id, &workspace.center_group, None) - .context("save pane group in save workspace")?; + // Upsert + conn.exec_bound(indoc! {" + INSERT INTO workspaces( + workspace_id, + workspace_location, + dock_visible, + dock_anchor, + timestamp + ) + VALUES (?1, ?2, ?3, ?4, CURRENT_TIMESTAMP) + ON CONFLICT DO + UPDATE SET + workspace_location = ?2, + dock_visible = ?3, + dock_anchor = ?4, + timestamp = CURRENT_TIMESTAMP + "})?(( + workspace.id, + &workspace.location, + workspace.dock_position, + )) + .context("Updating workspace")?; - let dock_id = self - .save_pane(workspace.id, &workspace.dock_pane, None, true) - .context("save pane in save 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")?; - // Complete workspace initialization - self.exec_bound(indoc! {" - UPDATE workspaces - SET dock_pane = ? - WHERE workspace_id = ?"})?((dock_id, workspace.id)) - .context("Finishing initialization with dock pane")?; + let dock_id = Self::save_pane(conn, workspace.id, &workspace.dock_pane, None, true) + .context("save pane in save workspace")?; - Ok(()) + // Complete workspace initialization + conn.exec_bound(indoc! {" + UPDATE workspaces + SET dock_pane = ? + WHERE workspace_id = ?"})?((dock_id, workspace.id)) + .context("Finishing initialization with dock pane")?; + + Ok(()) + }) + .log_err(); }) - .with_context(|| { - format!( - "Update workspace with roots {:?} and id {:?} failed.", - workspace.location.paths(), - workspace.id - ) - }) - .log_err(); + .await; } - sql_method!{ - next_id() -> Result: + sql_method! { + async next_id() -> Result: "INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id" } @@ -276,7 +287,7 @@ impl WorkspaceDb { } fn save_pane_group( - &self, + conn: &Connection, workspace_id: WorkspaceId, pane_group: &SerializedPaneGroup, parent: Option<(GroupId, usize)>, @@ -285,7 +296,7 @@ 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 = conn.select_row_bound::<_, i64>(indoc! {" INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) VALUES (?, ?, ?, ?) RETURNING group_id"})?(( @@ -297,13 +308,13 @@ impl WorkspaceDb { .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)))? + Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))? } Ok(()) } SerializedPaneGroup::Pane(pane) => { - self.save_pane(workspace_id, &pane, parent, false)?; + Self::save_pane(conn, workspace_id, &pane, parent, false)?; Ok(()) } } @@ -325,13 +336,13 @@ impl WorkspaceDb { } fn save_pane( - &self, + conn: &Connection, workspace_id: WorkspaceId, pane: &SerializedPane, parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane dock: bool, ) -> Result { - let pane_id = self.select_row_bound::<_, i64>(indoc! {" + let pane_id = conn.select_row_bound::<_, i64>(indoc! {" INSERT INTO panes(workspace_id, active) VALUES (?, ?) RETURNING pane_id"})?((workspace_id, pane.active))? @@ -339,13 +350,12 @@ impl WorkspaceDb { if !dock { let (parent_id, order) = unzip_option(parent); - self.exec_bound(indoc! {" + conn.exec_bound(indoc! {" INSERT INTO center_panes(pane_id, parent_group_id, position) VALUES (?, ?, ?)"})?((pane_id, parent_id, order))?; } - self.save_items(workspace_id, pane_id, &pane.children) - .context("Saving items")?; + Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?; Ok(pane_id) } @@ -358,12 +368,12 @@ impl WorkspaceDb { } fn save_items( - &self, + conn: &Connection, workspace_id: WorkspaceId, pane_id: PaneId, items: &[SerializedItem], ) -> Result<()> { - let mut insert = self.exec_bound( + let mut insert = conn.exec_bound( "INSERT INTO items(workspace_id, pane_id, position, kind, item_id) VALUES (?, ?, ?, ?, ?)", ).context("Preparing insertion")?; for (position, item) in items.iter().enumerate() { @@ -384,32 +394,44 @@ mod tests { use super::*; - #[test] - fn test_next_id_stability() { + #[gpui::test] + async fn test_next_id_stability() { env_logger::try_init().ok(); - let db = WorkspaceDb(open_memory_db(Some("test_workspace_id_stability"))); + let db = WorkspaceDb(open_memory_db("test_next_id_stability")); - db.migrate( - "test_table", - &["CREATE TABLE test_table( - text TEXT, - workspace_id INTEGER, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT;"], - ) - .unwrap(); - - let id = db.next_id().unwrap(); + db.write(|conn| { + conn.migrate( + "test_table", + &[indoc! {" + 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:: - ("SELECT workspace_id FROM workspaces WHERE workspace_id = ?").unwrap() - (id).unwrap()); - - db.exec_bound("INSERT INTO test_table(text, workspace_id) VALUES (?, ?)") - .unwrap()(("test-text-1", id)) - .unwrap(); + assert_eq!( + Some(id), + db.select_row_bound::( + "SELECT workspace_id FROM workspaces WHERE workspace_id = ?" + ) + .unwrap()(id) + .unwrap() + ); + + db.write(move |conn| { + conn.exec_bound("INSERT INTO test_table(text, workspace_id) VALUES (?, ?)") + .unwrap()(("test-text-1", id)) + .unwrap() + }) + .await; let test_text_1 = db .select_row_bound::<_, String>("SELECT text FROM test_table WHERE workspace_id = ?") @@ -418,22 +440,27 @@ mod tests { .unwrap(); assert_eq!(test_text_1, "test-text-1"); } - - #[test] - fn test_workspace_id_stability() { + + #[gpui::test] + async fn test_workspace_id_stability() { env_logger::try_init().ok(); - let db = WorkspaceDb(open_memory_db(Some("test_workspace_id_stability"))); + let db = WorkspaceDb(open_memory_db("test_workspace_id_stability")); - db.migrate( - "test_table", - &["CREATE TABLE test_table( - text TEXT, - workspace_id INTEGER, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT;"], - ) + db.write(|conn| { + conn.migrate( + "test_table", + &[indoc! {" + 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 { @@ -452,27 +479,33 @@ mod tests { dock_pane: Default::default(), }; - db.save_workspace(&workspace_1); + db.save_workspace(workspace_1.clone()).await; - db.exec_bound("INSERT INTO test_table(text, workspace_id) VALUES (?, ?)") - .unwrap()(("test-text-1", 1)) - .unwrap(); + db.write(|conn| { + conn.exec_bound("INSERT INTO test_table(text, workspace_id) VALUES (?, ?)") + .unwrap()(("test-text-1", 1)) + .unwrap(); + }) + .await; - db.save_workspace(&workspace_2); + db.save_workspace(workspace_2.clone()).await; - db.exec_bound("INSERT INTO test_table(text, workspace_id) VALUES (?, ?)") - .unwrap()(("test-text-2", 2)) - .unwrap(); + db.write(|conn| { + conn.exec_bound("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); - db.save_workspace(&workspace_1); + 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, }); - db.save_workspace(&workspace_2); + db.save_workspace(workspace_2).await; let test_text_2 = db .select_row_bound::<_, String>("SELECT text FROM test_table WHERE workspace_id = ?") @@ -489,11 +522,11 @@ mod tests { assert_eq!(test_text_1, "test-text-1"); } - #[test] - fn test_full_workspace_serialization() { + #[gpui::test] + async fn test_full_workspace_serialization() { env_logger::try_init().ok(); - let db = WorkspaceDb(open_memory_db(Some("test_full_workspace_serialization"))); + let db = WorkspaceDb(open_memory_db("test_full_workspace_serialization")); let dock_pane = crate::persistence::model::SerializedPane { children: vec![ @@ -550,24 +583,24 @@ mod tests { dock_pane, }; - db.save_workspace(&workspace); + 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); - db.save_workspace(&workspace); + 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()); } - #[test] - fn test_workspace_assignment() { + #[gpui::test] + async fn test_workspace_assignment() { env_logger::try_init().ok(); - let db = WorkspaceDb(open_memory_db(Some("test_basic_functionality"))); + let db = WorkspaceDb(open_memory_db("test_basic_functionality")); let workspace_1 = SerializedWorkspace { id: 1, @@ -585,8 +618,8 @@ mod tests { dock_pane: Default::default(), }; - db.save_workspace(&workspace_1); - db.save_workspace(&workspace_2); + db.save_workspace(workspace_1.clone()).await; + db.save_workspace(workspace_2.clone()).await; // Test that paths are treated as a set assert_eq!( @@ -605,7 +638,7 @@ mod tests { // Test 'mutate' case of updating a pre-existing id workspace_2.location = (["/tmp", "/tmp2"]).into(); - db.save_workspace(&workspace_2); + db.save_workspace(workspace_2.clone()).await; assert_eq!( db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), workspace_2 @@ -620,7 +653,7 @@ mod tests { dock_pane: Default::default(), }; - db.save_workspace(&workspace_3); + db.save_workspace(workspace_3.clone()).await; assert_eq!( db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), workspace_3 @@ -628,7 +661,7 @@ mod tests { // Make sure that updating paths differently also works workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into(); - db.save_workspace(&workspace_3); + 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"]) @@ -655,11 +688,11 @@ mod tests { } } - #[test] - fn test_basic_dock_pane() { + #[gpui::test] + async fn test_basic_dock_pane() { env_logger::try_init().ok(); - let db = WorkspaceDb(open_memory_db(Some("basic_dock_pane"))); + let db = WorkspaceDb(open_memory_db("basic_dock_pane")); let dock_pane = crate::persistence::model::SerializedPane::new( vec![ @@ -673,18 +706,18 @@ mod tests { let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default()); - db.save_workspace(&workspace); + db.save_workspace(workspace.clone()).await; let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap(); assert_eq!(workspace.dock_pane, new_workspace.dock_pane); } - #[test] - fn test_simple_split() { + #[gpui::test] + async fn test_simple_split() { env_logger::try_init().ok(); - let db = WorkspaceDb(open_memory_db(Some("simple_split"))); + let db = WorkspaceDb(open_memory_db("simple_split")); // ----------------- // | 1,2 | 5,6 | @@ -725,18 +758,18 @@ mod tests { let workspace = default_workspace(&["/tmp"], Default::default(), ¢er_pane); - db.save_workspace(&workspace); + db.save_workspace(workspace.clone()).await; let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap(); assert_eq!(workspace.center_group, new_workspace.center_group); } - #[test] - fn test_cleanup_panes() { + #[gpui::test] + async fn test_cleanup_panes() { env_logger::try_init().ok(); - let db = WorkspaceDb(open_memory_db(Some("test_cleanup_panes"))); + let db = WorkspaceDb(open_memory_db("test_cleanup_panes")); let center_pane = SerializedPaneGroup::Group { axis: gpui::Axis::Horizontal, @@ -774,7 +807,7 @@ mod tests { let mut workspace = default_workspace(id, Default::default(), ¢er_pane); - db.save_workspace(&workspace); + db.save_workspace(workspace.clone()).await; workspace.center_group = SerializedPaneGroup::Group { axis: gpui::Axis::Vertical, @@ -796,7 +829,7 @@ mod tests { ], }; - db.save_workspace(&workspace); + db.save_workspace(workspace.clone()).await; let new_workspace = db.workspace_for_roots(id).unwrap(); diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 2f0bc050d2..dc6d8ba8ee 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -58,7 +58,7 @@ impl Column for WorkspaceLocation { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct SerializedWorkspace { pub id: WorkspaceId, pub location: WorkspaceLocation, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 155c95e4e8..9755c2c6ca 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -539,6 +539,7 @@ pub struct Workspace { impl Workspace { pub fn new( serialized_workspace: Option, + workspace_id: WorkspaceId, project: ModelHandle, dock_default_factory: DefaultItemFactory, cx: &mut ViewContext, @@ -558,7 +559,6 @@ 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(cx); } project::Event::DisconnectedFromHost => { @@ -633,12 +633,6 @@ impl Workspace { active_call = Some((call, subscriptions)); } - let database_id = serialized_workspace - .as_ref() - .map(|ws| ws.id) - .or_else(|| DB.next_id().log_err()) - .unwrap_or(0); - let mut this = Workspace { modal: None, weak_self: weak_handle.clone(), @@ -666,7 +660,7 @@ impl Workspace { last_leaders_by_pane: Default::default(), window_edited: false, active_call, - database_id, + database_id: workspace_id, _observe_current_user, }; this.project_remote_id_changed(project.read(cx).remote_id(), cx); @@ -699,10 +693,17 @@ impl Workspace { ); cx.spawn(|mut cx| async move { + let serialized_workspace = persistence::DB.workspace_for_roots(&abs_paths.as_slice()); + + let paths_to_open = serialized_workspace + .as_ref() + .map(|workspace| workspace.location.paths()) + .unwrap_or(Arc::new(abs_paths)); + // Get project paths for all of the abs_paths let mut worktree_roots: HashSet> = Default::default(); let mut project_paths = Vec::new(); - for path in abs_paths.iter() { + for path in paths_to_open.iter() { if let Some((worktree, project_entry)) = cx .update(|cx| { Workspace::project_path_for_path(project_handle.clone(), &path, true, cx) @@ -717,14 +718,17 @@ impl Workspace { } } - // Use the resolved worktree roots to get the serialized_db from the database - let serialized_workspace = persistence::DB - .workspace_for_roots(&Vec::from_iter(worktree_roots.into_iter())[..]); + let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() { + serialized_workspace.id + } else { + DB.next_id().await.unwrap_or(0) + }; // Use the serialized workspace to construct the new window let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { let mut workspace = Workspace::new( serialized_workspace, + workspace_id, project_handle, app_state.default_item_factory, cx, @@ -735,8 +739,8 @@ impl Workspace { // Call open path for each of the project paths // (this will bring them to the front if they were in the serialized workspace) - debug_assert!(abs_paths.len() == project_paths.len()); - let tasks = abs_paths + debug_assert!(paths_to_open.len() == project_paths.len()); + let tasks = paths_to_open .iter() .cloned() .zip(project_paths.into_iter()) @@ -1327,7 +1331,6 @@ impl Workspace { pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { let active_pane = self.active_pane().clone(); Pane::add_item(self, &active_pane, item, true, true, None, cx); - self.serialize_workspace(cx); } pub fn open_path( @@ -1532,10 +1535,11 @@ impl Workspace { entry.remove(); } } - self.serialize_workspace(cx); } _ => {} } + + self.serialize_workspace(cx); } else if self.dock.visible_pane().is_none() { error!("pane {} not found", pane_id); } @@ -2342,9 +2346,7 @@ impl Workspace { }; cx.background() - .spawn(async move { - persistence::DB.save_workspace(&serialized_workspace); - }) + .spawn(persistence::DB.save_workspace(serialized_workspace)) .detach(); } @@ -2642,9 +2644,13 @@ pub fn open_paths( fn open_new(app_state: &Arc, cx: &mut MutableAppContext) -> Task<()> { let task = Workspace::new_local(Vec::new(), app_state.clone(), cx); cx.spawn(|mut cx| async move { - let (workspace, _) = task.await; + let (workspace, opened_paths) = task.await; - workspace.update(&mut cx, |_, cx| cx.dispatch_action(NewFile)) + workspace.update(&mut cx, |_, cx| { + if opened_paths.is_empty() { + cx.dispatch_action(NewFile); + } + }) }) } @@ -2677,6 +2683,7 @@ mod tests { let (_, workspace) = cx.add_window(|cx| { Workspace::new( Default::default(), + 0, project.clone(), default_item_factory, cx, @@ -2748,6 +2755,7 @@ mod tests { let (window_id, workspace) = cx.add_window(|cx| { Workspace::new( Default::default(), + 0, project.clone(), default_item_factory, cx, @@ -2851,6 +2859,7 @@ mod tests { let (window_id, workspace) = cx.add_window(|cx| { Workspace::new( Default::default(), + 0, project.clone(), default_item_factory, cx, @@ -2895,8 +2904,9 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (window_id, workspace) = cx - .add_window(|cx| Workspace::new(Default::default(), project, default_item_factory, cx)); + let (window_id, workspace) = cx.add_window(|cx| { + Workspace::new(Default::default(), 0, project, default_item_factory, cx) + }); let item1 = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); @@ -2991,8 +3001,9 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; - let (window_id, workspace) = cx - .add_window(|cx| Workspace::new(Default::default(), project, default_item_factory, cx)); + let (window_id, workspace) = cx.add_window(|cx| { + Workspace::new(Default::default(), 0, project, default_item_factory, cx) + }); // Create several workspace items with single project entries, and two // workspace items with multiple project entries. @@ -3093,8 +3104,9 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; - let (window_id, workspace) = cx - .add_window(|cx| Workspace::new(Default::default(), project, default_item_factory, cx)); + let (window_id, workspace) = cx.add_window(|cx| { + Workspace::new(Default::default(), 0, project, default_item_factory, cx) + }); let item = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); @@ -3211,8 +3223,9 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; - let (_, workspace) = cx - .add_window(|cx| Workspace::new(Default::default(), project, default_item_factory, cx)); + let (_, workspace) = cx.add_window(|cx| { + Workspace::new(Default::default(), 0, project, default_item_factory, cx) + }); let item = cx.add_view(&workspace, |_| { let mut item = TestItem::new(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0abcbeac48..3693a5e580 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -809,7 +809,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); let entries = cx.read(|cx| workspace.file_project_paths(cx)); @@ -930,7 +930,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await; let (_, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); // Open a file within an existing worktree. @@ -1091,7 +1091,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); // Open a file within an existing worktree. @@ -1135,7 +1135,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; project.update(cx, |project, _| project.languages().add(rust_lang())); let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap()); @@ -1226,7 +1226,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), [], cx).await; project.update(cx, |project, _| project.languages().add(rust_lang())); let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); // Create a new untitled buffer @@ -1281,7 +1281,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new(Default::default(), project, |_, _| unimplemented!(), cx) + Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx) }); let entries = cx.read(|cx| workspace.file_project_paths(cx)); @@ -1359,6 +1359,7 @@ mod tests { let (_, workspace) = cx.add_window(|cx| { Workspace::new( Default::default(), + 0, project.clone(), |_, _| unimplemented!(), cx, @@ -1630,6 +1631,7 @@ mod tests { let (_, workspace) = cx.add_window(|cx| { Workspace::new( Default::default(), + 0, project.clone(), |_, _| unimplemented!(), cx,