From 81921571ed132d859d1b530edae07c0741d290e1 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 26 Jun 2024 17:26:25 +0200 Subject: [PATCH 01/38] WIP: Backup unpersisted changes in in buffers Co-authored-by: Bennet --- Cargo.lock | 8 +- Cargo.toml | 1 + crates/editor/src/editor.rs | 2 + crates/editor/src/items.rs | 160 +++++++++++++++++++++++-------- crates/editor/src/persistence.rs | 95 ++++++++++++++++++ crates/workspace/Cargo.toml | 2 +- crates/zed/src/zed.rs | 27 +++--- 7 files changed, 240 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2eed789433accc..1aef2b435e928c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10880,18 +10880,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 3e46369976d785..ae16a913cf5ff0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -284,6 +284,7 @@ async-trait = "0.1" async-watch = "0.3.1" async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] } base64 = "0.13" +bincode = "1.2.1" bitflags = "2.6.0" blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" } blade-macros = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c9f25d8a4ec767..d3c7717c1aee43 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -566,6 +566,7 @@ pub struct Editor { previous_search_ranges: Option]>>, file_header_size: u8, breadcrumb_header: Option, + serialize_unsaved_buffer_debounce: Arc>, } #[derive(Clone)] @@ -1901,6 +1902,7 @@ impl Editor { linked_edit_ranges: Default::default(), previous_search_ranges: None, breadcrumb_header: None, + serialize_unsaved_buffer_debounce: Arc::new(Mutex::new(DebouncedDelay::new())), }; this.tasks_update_task = Some(this.refresh_runnables(cx)); this._subscriptions.extend(project_subscriptions); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 5289923c0434d4..6ad99147de171e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -29,6 +29,7 @@ use std::{ ops::Range, path::Path, sync::Arc, + time::Duration, }; use text::{BufferId, Selection}; use theme::{Theme, ThemeSettings}; @@ -873,18 +874,58 @@ impl Item for Editor { } } + fn serialize_edited_buffer( + buffer: Model, + workspace_id: WorkspaceId, + item_id: ItemId, + cx: &mut AppContext, + ) -> Task<()> { + let snapshot = buffer.read(cx).snapshot(); + cx.background_executor().spawn(async move { + let contents = snapshot.text(); + DB.save_contents(item_id, workspace_id, contents) + .await + .log_err(); + }) + } + 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((_, Some(workspace_id))) = this.workspace.as_ref() { - if let language::Event::FileHandleChanged = event { - serialize( - buffer, - *workspace_id, - cx.view().item_id().as_u64() as ItemId, - cx, - ); + match event { + language::Event::FileHandleChanged => { + serialize( + buffer, + *workspace_id, + cx.view().item_id().as_u64() as ItemId, + cx, + ); + } + language::Event::Edited => { + let workspace_id = *workspace_id; + let item_id = cx.view().item_id().as_u64() as ItemId; + this.serialize_unsaved_buffer_debounce.lock().fire_new( + Duration::from_millis(100), + cx, + move |_, cx| { + serialize_edited_buffer(buffer, workspace_id, item_id, cx) + }, + ); + } + language::Event::Saved => { + let item_id = cx.view().item_id().as_u64() as ItemId; + cx.background_executor() + .spawn({ + let workspace_id = *workspace_id; + async move { + DB.delete_contents(workspace_id, item_id).await.log_err() + } + }) + .detach(); + } + _ => {} } } }) @@ -937,41 +978,84 @@ impl Item for Editor { item_id: ItemId, cx: &mut ViewContext, ) -> Task>> { - let project_item: Result<_> = 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)? - .context("No path stored for this editor")?; - - let (worktree, path) = project - .find_local_worktree(&path, cx) - .with_context(|| format!("No worktree for path: {path:?}"))?; - let project_path = ProjectPath { - worktree_id: worktree.read(cx).id(), - path: path.into(), - }; + // Look up the path with this key associated, create a self with that path + let path = match DB + .get_path(item_id, workspace_id) + .context("Failed to query editor state") + { + Ok(path) => path, + Err(error) => { + return Task::ready(Err(error)); + } + }; - Ok(project.open_path(project_path, cx)) - }); + let contents = match DB + .get_contents(item_id, workspace_id) + .context("Failed to query editor content") + { + Ok(contents) => contents, + Err(error) => { + return Task::ready(Err(error)); + } + }; + + match (path, contents) { + (None, Some(contents)) => { + let buffer = cx.new_model(|cx| { + let mut buffer = Buffer::local("", cx); + buffer.set_text(contents, cx); + buffer + }); + let view = cx.new_view(|cx| { + let mut editor = Editor::for_buffer(buffer, Some(project), cx); + editor.read_scroll_position_from_db(item_id, workspace_id, cx); + editor + }); + Task::ready(Ok(view)) + } + (Some(path), contents) => { + let project_item = project.update(cx, |project, cx| { + let (worktree, path) = project + .find_local_worktree(&path, cx) + .with_context(|| format!("No worktree for path: {path:?}"))?; + let project_path = ProjectPath { + worktree_id: worktree.read(cx).id(), + path: path.into(), + }; + + Ok(project.open_path(project_path, cx)) + }); + + project_item + .map(|project_item| { + cx.spawn(|pane, mut cx| async move { + let (_, project_item) = project_item.await?; + let buffer = project_item.downcast::().map_err(|_| { + anyhow!("Project item at stored path was not a buffer") + })?; + + // TODO: This is a bit wasteful: we're loading the whole buffer from + // disk and then overwrite + if let Some(contents) = contents { + buffer.update(&mut cx, |buffer, cx| { + buffer.set_text(contents, cx); + })?; + } + + pane.update(&mut cx, |_, cx| { + cx.new_view(|cx| { + let mut editor = Editor::for_buffer(buffer, Some(project), cx); - project_item - .map(|project_item| { - cx.spawn(|pane, mut cx| async move { - let (_, project_item) = project_item.await?; - let buffer = project_item - .downcast::() - .map_err(|_| anyhow!("Project item at stored path was not a buffer"))?; - pane.update(&mut cx, |_, cx| { - cx.new_view(|cx| { - let mut editor = Editor::for_buffer(buffer, Some(project), cx); - - editor.read_scroll_position_from_db(item_id, workspace_id, cx); - editor + editor.read_scroll_position_from_db(item_id, workspace_id, cx); + editor + }) + }) }) }) - }) - }) - .unwrap_or_else(|error| Task::ready(Err(error))) + .unwrap_or_else(|error| Task::ready(Err(error))) + } + (None, None) => Task::ready(Err(anyhow!("No path or contents found for buffer"))), + } } } diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index 6e37735c1371ff..6686e91557206d 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +use anyhow::Result; use db::sqlez_macros::sql; use db::{define_connection, query}; @@ -31,6 +32,17 @@ define_connection!( ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0; ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0; ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0; + ), + sql! ( + CREATE TABLE editor_contents ( + item_id INTEGER NOT NULL, + workspace_id INTEGER NOT NULL, + contents TEXT NOT NULL, + PRIMARY KEY(item_id, workspace_id), + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; )]; ); @@ -55,6 +67,61 @@ impl EditorDb { } } + query! { + pub fn get_contents_metadata(workspace: WorkspaceId) -> Result> { + SELECT item_id, workspace_id + FROM editor_contents + WHERE workspace_id = ? + } + } + + query! { + pub fn get_contents(item_id: ItemId, workspace: WorkspaceId) -> Result> { + SELECT contents + FROM editor_contents + WHERE item_id = ?1 + AND workspace_id = ? + AND item_id = ? + } + } + + query! { + pub async fn save_contents(item_id: ItemId, workspace: WorkspaceId, contents: String) -> Result<()> { + INSERT INTO editor_contents + (item_id, workspace_id, contents) + VALUES + (?1, ?2, ?3) + ON CONFLICT DO UPDATE SET + item_id = ?1, + workspace_id = ?2, + contents = ?3 + } + } + + query! { + pub async fn delete_contents(workspace: WorkspaceId, item_id: ItemId) -> Result<()> { + DELETE FROM editor_contents + WHERE workspace_id = ? + AND item_id = ? + } + } + + //TODO pass Vec instead of String, unsure how to do that with sqlez + pub fn delete_outdated_contents( + &self, + workspace: WorkspaceId, + item_ids: Vec, + ) -> Result<()> { + let ids_string = item_ids + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(", "); + + let query = format!("DELETE FROM editor_contents WHERE workspace_id = {workspace:?} AND item_id NOT IN ({ids_string})"); + self.exec(&query).unwrap()() + } + // Returns the scroll top row, and offset query! { pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { @@ -81,3 +148,31 @@ impl EditorDb { } } } + +// pub struct BufferVersion(pub clock::Global); + +// impl StaticColumnCount for BufferVersion {} + +// impl Bind for BufferVersion { +// fn bind(&self, statement: &Statement, start_index: i32) -> Result { +// let data: Vec = self.0.clone().into(); +// statement.bind(&bincode::serialize(&data)?, start_index) +// } +// } + +// impl Column for BufferVersion { +// fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { +// let version_blob = statement.column_blob(start_index)?; + +// let version: Vec = if version_blob.is_empty() { +// Default::default() +// } else { +// bincode::deserialize(version_blob).context("Bincode deserialization of paths failed")? +// }; + +// Ok(( +// BufferVersion(clock::Global::from(version.as_slice())), +// start_index + 1, +// )) +// } +// } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 2b178d50dde168..080f96f7dbf335 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -29,7 +29,7 @@ test-support = [ anyhow.workspace = true any_vec.workspace = true async-recursion.workspace = true -bincode = "1.2.1" +bincode.workspace = true call.workspace = true client.workspace = true clock.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 28eee430ee25b1..c2e547738886f9 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -571,19 +571,22 @@ fn quit(_: &Quit, cx: &mut AppContext) { } } + // TODO: Ensure that all workspace items and unpersisted + // changes are persisted + // If the user cancels any save prompt, then keep the app open. - for window in workspace_windows { - if let Some(should_close) = window - .update(&mut cx, |workspace, cx| { - workspace.prepare_to_close(true, cx) - }) - .log_err() - { - if !should_close.await? { - return Ok(()); - } - } - } + // for window in workspace_windows { + // if let Some(should_close) = window + // .update(&mut cx, |workspace, cx| { + // workspace.prepare_to_close(true, cx) + // }) + // .log_err() + // { + // if !should_close.await? { + // return Ok(()); + // } + // } + // } cx.update(|cx| cx.quit())?; anyhow::Ok(()) }) From 1c1488b82743edc4ee6fdedbf7d426b88732afa7 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 26 Jun 2024 17:43:07 +0200 Subject: [PATCH 02/38] Fix moving of dependency in Cargo.toml --- crates/workspace/Cargo.toml | 2 +- crates/workspace/src/workspace.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 080f96f7dbf335..2b178d50dde168 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -29,7 +29,7 @@ test-support = [ anyhow.workspace = true any_vec.workspace = true async-recursion.workspace = true -bincode.workspace = true +bincode = "1.2.1" call.workspace = true client.workspace = true clock.workspace = true diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 636198bdacce41..b715e91d25001a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3834,6 +3834,8 @@ impl Workspace { }) .ok(); + // TODO: Start background task to delete all editor_contents + Ok(opened_items) }) } From 1e2a4efe4bf4699186b8d0defd801127f4155592 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 27 Jun 2024 09:17:06 +0200 Subject: [PATCH 03/38] Fix wrong query --- crates/editor/src/persistence.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index 6686e91557206d..40055e9a75e496 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -80,8 +80,7 @@ impl EditorDb { SELECT contents FROM editor_contents WHERE item_id = ?1 - AND workspace_id = ? - AND item_id = ? + AND workspace_id = ?2 } } From 2f9a1381c18aedc8a3189ed6f270024b3c86ec4a Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 11 Jul 2024 09:52:22 +0200 Subject: [PATCH 04/38] Remove bincode after rebase --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ae16a913cf5ff0..7a118fc9231acd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -285,10 +285,10 @@ async-watch = "0.3.1" async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] } base64 = "0.13" bincode = "1.2.1" -bitflags = "2.6.0" -blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" } -blade-macros = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" } -blade-util = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" } +bitflags = "2.4.2" +blade-graphics = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" } +blade-macros = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" } +blade-util = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" } cap-std = "3.0" cargo_toml = "0.20" chrono = { version = "0.4", features = ["serde"] } From a1cc0ed75b46e55d99637f6eb0a2d2932e3dcee6 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 11 Jul 2024 11:41:59 +0200 Subject: [PATCH 05/38] workspace: Wait for items to be serialized on close --- crates/editor/src/items.rs | 47 +++++++++++++++++++++++ crates/workspace/src/item.rs | 24 ++++++++++++ crates/workspace/src/workspace.rs | 62 +++++++++++++++++++++---------- crates/zed/src/zed.rs | 24 ++++++------ 4 files changed, 125 insertions(+), 32 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 6ad99147de171e..8e03126e7276d3 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1057,6 +1057,53 @@ impl Item for Editor { (None, None) => Task::ready(Err(anyhow!("No path or contents found for buffer"))), } } + + fn can_serialize(&self, cx: &AppContext) -> bool { + // TODO: Here we could check a setting. + self.buffer().read(cx).as_singleton().is_some() + } + + fn serialize( + &self, + workspace: &mut Workspace, + item_id: ItemId, + cx: &mut WindowContext, + ) -> Task> { + let Some(workspace_id) = workspace.database_id() else { + return Task::ready(Ok(())); + }; + + let Some(buffer) = self.buffer().read(cx).as_singleton() else { + return Task::ready(Ok(())); + }; + + let path = buffer + .read(cx) + .file() + .and_then(|file| file.as_local()) + .map(|file| file.abs_path(cx)); + + let snapshot = buffer.read(cx).snapshot(); + + cx.spawn(|cx| async move { + if let Some(path) = path { + cx.background_executor() + .spawn(async move { DB.save_path(item_id, workspace_id, path.clone()).await }) + .await + .context("failed to save path of buffer")? + } + + cx.background_executor() + .spawn(async move { + let contents = snapshot.text(); + DB.save_contents(item_id, workspace_id, contents).await + }) + .await + .context("failed to save contents of buffer")?; + + Ok(()) + }) + } } impl ProjectItem for Editor { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 48b8fb1d1e165a..dcc3ebf32f3df7 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -261,6 +261,14 @@ pub trait Item: FocusableView + EventEmitter { fn pixel_position_of_cursor(&self, _: &AppContext) -> Option> { None } + + fn can_serialize(&self, _: &AppContext) -> bool { + false + } + + fn serialize(&self, _: &mut Workspace, _: ItemId, _: &mut WindowContext) -> Task> { + Task::ready(Ok(())) + } } pub trait ItemHandle: 'static + Send { @@ -332,6 +340,9 @@ pub trait ItemHandle: 'static + Send { fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option>; fn downgrade_item(&self) -> Box; fn workspace_settings<'a>(&self, cx: &'a AppContext) -> &'a WorkspaceSettings; + + fn can_serialize(&self, cx: &AppContext) -> bool; + fn serialize(&self, workspace: &mut Workspace, cx: &mut WindowContext) -> Task>; } pub trait WeakItemHandle: Send + Sync { @@ -722,6 +733,19 @@ impl ItemHandle for View { fn downgrade_item(&self) -> Box { Box::new(self.downgrade()) } + + fn can_serialize(&self, cx: &AppContext) -> bool { + self.read(cx).can_serialize(cx) + } + + fn serialize( + &self, + workspace: &mut Workspace, + cx: &mut WindowContext, + ) -> Task> { + let item_id = self.entity_id().as_u64() as ItemId; + self.update(cx, |item, cx| item.serialize(workspace, item_id, cx)) + } } impl From> for AnyView { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b715e91d25001a..3cb961a6ffdf5d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1525,27 +1525,49 @@ impl Workspace { let project = self.project.clone(); cx.spawn(|workspace, mut cx| async move { - // Override save mode and display "Save all files" prompt - if save_intent == SaveIntent::Close && dirty_items.len() > 1 { - let answer = workspace.update(&mut cx, |_, cx| { - let (prompt, detail) = Pane::file_names_for_prompt( - &mut dirty_items.iter().map(|(_, handle)| handle), - dirty_items.len(), - cx, - ); - cx.prompt( - PromptLevel::Warning, - &prompt, - Some(&detail), - &["Save all", "Discard all", "Cancel"], - ) - })?; - match answer.await.log_err() { - Some(0) => save_intent = SaveIntent::SaveAll, - Some(1) => save_intent = SaveIntent::Skip, - _ => {} + let dirty_items = if save_intent == SaveIntent::Close && dirty_items.len() > 1 { + let (serialize_tasks, remaining_dirty_items) = + workspace.update(&mut cx, |workspace, cx| { + let mut remaining_dirty_items = Vec::new(); + let mut serialize_tasks = Vec::new(); + for (pane, item) in dirty_items { + if item.can_serialize(cx) { + serialize_tasks.push(item.serialize(workspace, cx)); + } else { + remaining_dirty_items.push((pane, item)); + } + } + (serialize_tasks, remaining_dirty_items) + })?; + + futures::future::try_join_all(serialize_tasks).await?; + + if remaining_dirty_items.len() > 1 { + let answer = workspace.update(&mut cx, |_, cx| { + let (prompt, detail) = Pane::file_names_for_prompt( + &mut remaining_dirty_items.iter().map(|(_, handle)| handle), + remaining_dirty_items.len(), + cx, + ); + cx.prompt( + PromptLevel::Warning, + &prompt, + Some(&detail), + &["Save all", "Discard all", "Cancel"], + ) + })?; + match answer.await.log_err() { + Some(0) => save_intent = SaveIntent::SaveAll, + Some(1) => save_intent = SaveIntent::Skip, + _ => {} + } } - } + + remaining_dirty_items + } else { + dirty_items + }; + for (pane, item) in dirty_items { let (singleton, project_entry_ids) = cx.update(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c2e547738886f9..011996738bed6c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -575,18 +575,18 @@ fn quit(_: &Quit, cx: &mut AppContext) { // changes are persisted // If the user cancels any save prompt, then keep the app open. - // for window in workspace_windows { - // if let Some(should_close) = window - // .update(&mut cx, |workspace, cx| { - // workspace.prepare_to_close(true, cx) - // }) - // .log_err() - // { - // if !should_close.await? { - // return Ok(()); - // } - // } - // } + for window in workspace_windows { + if let Some(should_close) = window + .update(&mut cx, |workspace, cx| { + workspace.prepare_to_close(true, cx) + }) + .log_err() + { + if !should_close.await? { + return Ok(()); + } + } + } cx.update(|cx| cx.quit())?; anyhow::Ok(()) }) From f9128f7f9ccecaa98bff178752c83f6d23aae1f4 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 11 Jul 2024 13:03:33 +0200 Subject: [PATCH 06/38] Clean up unloaded items on start --- crates/editor/src/persistence.rs | 45 --------------------------- crates/workspace/src/persistence.rs | 19 +++++++++++- crates/workspace/src/workspace.rs | 47 +++++++++++++++++++++++------ 3 files changed, 55 insertions(+), 56 deletions(-) diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index 40055e9a75e496..ee312a2ace6c67 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,6 +1,5 @@ use std::path::PathBuf; -use anyhow::Result; use db::sqlez_macros::sql; use db::{define_connection, query}; @@ -105,22 +104,6 @@ impl EditorDb { } } - //TODO pass Vec instead of String, unsure how to do that with sqlez - pub fn delete_outdated_contents( - &self, - workspace: WorkspaceId, - item_ids: Vec, - ) -> Result<()> { - let ids_string = item_ids - .iter() - .map(|id| id.to_string()) - .collect::>() - .join(", "); - - let query = format!("DELETE FROM editor_contents WHERE workspace_id = {workspace:?} AND item_id NOT IN ({ids_string})"); - self.exec(&query).unwrap()() - } - // Returns the scroll top row, and offset query! { pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { @@ -147,31 +130,3 @@ impl EditorDb { } } } - -// pub struct BufferVersion(pub clock::Global); - -// impl StaticColumnCount for BufferVersion {} - -// impl Bind for BufferVersion { -// fn bind(&self, statement: &Statement, start_index: i32) -> Result { -// let data: Vec = self.0.clone().into(); -// statement.bind(&bincode::serialize(&data)?, start_index) -// } -// } - -// impl Column for BufferVersion { -// fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { -// let version_blob = statement.column_blob(start_index)?; - -// let version: Vec = if version_blob.is_empty() { -// Default::default() -// } else { -// bincode::deserialize(version_blob).context("Bincode deserialization of paths failed")? -// }; - -// Ok(( -// BufferVersion(clock::Global::from(version.as_slice())), -// start_index + 1, -// )) -// } -// } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 8fcadcf4f5f4d3..7c7e5a5ed6089a 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -16,7 +16,7 @@ use ui::px; use util::ResultExt; use uuid::Uuid; -use crate::WorkspaceId; +use crate::{ItemId, WorkspaceId}; use model::{ GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, @@ -979,6 +979,23 @@ impl WorkspaceDb { WHERE workspace_id = ?1 } } + + pub async fn delete_unloaded_items( + &self, + workspace: WorkspaceId, + item_ids: Vec, + ) -> Result<()> { + let ids_string = item_ids + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(", "); + + let workspace_id = workspace.0; + // TODO: This is hacky because we're reaching into `editor_contents` from this struct + let query = format!("DELETE FROM editor_contents WHERE workspace_id = {workspace_id} AND item_id NOT IN ({ids_string})"); + self.write(move |conn| conn.exec(&query).unwrap()()).await + } } #[cfg(test)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3cb961a6ffdf5d..0001e3b4ab9fe6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3795,16 +3795,19 @@ impl Workspace { center_group = Some((group, active_pane)) } - let mut items_by_project_path = cx.update(|cx| { - center_items + let mut items_by_project_path = HashMap::default(); + let mut item_ids = Vec::default(); + cx.update(|cx| { + for item in center_items .unwrap_or_default() .into_iter() - .filter_map(|item| { - let item = item?; - let project_path = item.project_path(cx)?; - Some((project_path, item)) - }) - .collect::>() + .filter_map(|item| item) + { + item_ids.push(item.item_id().as_u64() as ItemId); + if let Some(project_path) = item.project_path(cx) { + items_by_project_path.insert(project_path, item); + } + } })?; let opened_items = paths_to_open @@ -3849,6 +3852,16 @@ impl Workspace { cx.notify(); })?; + // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means + // after loading the items, we might have different items and in order to avoid + // the database filling up, we delete items that haven't been loaded now. + workspace + .update(&mut cx, |workspace, cx| { + workspace.delete_unloaded_items(item_ids, cx) + })? + .await + .context("cleaning unloaded workspace items from database failed")?; + // Serialize ourself to make sure our timestamps and any pane / item changes are replicated workspace .update(&mut cx, |workspace, cx| { @@ -3856,12 +3869,26 @@ impl Workspace { }) .ok(); - // TODO: Start background task to delete all editor_contents - Ok(opened_items) }) } + fn delete_unloaded_items( + &self, + loaded_items: Vec, + cx: &mut WindowContext, + ) -> Task> { + let Some(database_id) = self.database_id() else { + return Task::ready(Ok(())); + }; + + cx.spawn(|_| async move { + persistence::DB + .delete_unloaded_items(database_id, loaded_items) + .await + }) + } + fn actions(&self, div: Div, cx: &mut ViewContext) -> Div { self.add_workspace_actions_listeners(div, cx) .on_action(cx.listener(Self::close_inactive_items_and_panes)) From 7e6790412cead7f25d3905f4c8083810d98ef395 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 11 Jul 2024 13:10:32 +0200 Subject: [PATCH 07/38] Remove unused method --- crates/editor/src/persistence.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index ee312a2ace6c67..99c81c324aad7c 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -66,14 +66,6 @@ impl EditorDb { } } - query! { - pub fn get_contents_metadata(workspace: WorkspaceId) -> Result> { - SELECT item_id, workspace_id - FROM editor_contents - WHERE workspace_id = ? - } - } - query! { pub fn get_contents(item_id: ItemId, workspace: WorkspaceId) -> Result> { SELECT contents From 76b605f6b701cbb2203d808080617808041a156f Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 11 Jul 2024 14:31:25 +0200 Subject: [PATCH 08/38] Delete serialized contents when discarding a file --- crates/editor/src/items.rs | 19 +++++++++++++++++++ crates/workspace/src/item.rs | 25 +++++++++++++++++++++++++ crates/workspace/src/pane.rs | 36 ++++++++++++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 8e03126e7276d3..34309f7b30b3c4 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1104,6 +1104,25 @@ impl Item for Editor { Ok(()) }) } + + fn delete_serialized( + &self, + workspace: &mut Workspace, + item_id: ItemId, + cx: &mut WindowContext, + ) -> Task> { + let Some(workspace_id) = workspace.database_id() else { + return Task::ready(Ok(())); + }; + + println!("deleting serialized content of item {:?}", item_id); + cx.spawn(|cx| async move { + cx.background_executor() + .spawn(DB.delete_contents(workspace_id, item_id)) + .await + .context("failed to save contents of buffer") + }) + } } impl ProjectItem for Editor { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index dcc3ebf32f3df7..a89effacd4a5de 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -269,6 +269,15 @@ pub trait Item: FocusableView + EventEmitter { fn serialize(&self, _: &mut Workspace, _: ItemId, _: &mut WindowContext) -> Task> { Task::ready(Ok(())) } + + fn delete_serialized( + &self, + _: &mut Workspace, + _: ItemId, + _: &mut WindowContext, + ) -> Task> { + Task::ready(Ok(())) + } } pub trait ItemHandle: 'static + Send { @@ -343,6 +352,11 @@ pub trait ItemHandle: 'static + Send { fn can_serialize(&self, cx: &AppContext) -> bool; fn serialize(&self, workspace: &mut Workspace, cx: &mut WindowContext) -> Task>; + fn delete_serialized( + &self, + workspace: &mut Workspace, + cx: &mut WindowContext, + ) -> Task>; } pub trait WeakItemHandle: Send + Sync { @@ -746,6 +760,17 @@ impl ItemHandle for View { let item_id = self.entity_id().as_u64() as ItemId; self.update(cx, |item, cx| item.serialize(workspace, item_id, cx)) } + + fn delete_serialized( + &self, + workspace: &mut Workspace, + cx: &mut WindowContext, + ) -> Task> { + let item_id = self.entity_id().as_u64() as ItemId; + self.update(cx, |item, cx| { + item.delete_serialized(workspace, item_id, cx) + }) + } } impl From> for AnyView { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 087cfd6bef662c..9d9d96f49bede1 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1370,6 +1370,9 @@ impl Pane { "This file has changed on disk since you started editing it. Do you want to overwrite it?"; if save_intent == SaveIntent::Skip { + if let Some(deletion_task) = Self::delete_serialized_content(pane, item, cx) { + deletion_task.await.map(|_| true)?; + } return Ok(true); } @@ -1452,8 +1455,16 @@ impl Pane { })?; match answer { Ok(0) => {} - Ok(1) => return Ok(true), // Don't save this file - _ => return Ok(false), // Cancel + Ok(1) => { + // Don't save this file + if let Some(deletion_task) = + Self::delete_serialized_content(pane, item, cx) + { + deletion_task.await.map(|_| true)?; + } + return Ok(true); + } + _ => return Ok(false), // Cancel } } else { return Ok(false); @@ -1477,6 +1488,7 @@ impl Pane { } } } + Ok(true) } @@ -1485,6 +1497,26 @@ impl Pane { item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted } + fn delete_serialized_content( + pane: &WeakView, + item: &dyn ItemHandle, + cx: &mut AsyncWindowContext, + ) -> Option>> { + pane.update(cx, |pane, cx| { + if item.can_serialize(cx) { + let task = pane + .workspace + .update(cx, |workspace, cx| item.delete_serialized(workspace, cx)) + .ok()?; + Some(task) + } else { + None + } + }) + .ok() + .flatten() + } + pub fn autosave_item( item: &dyn ItemHandle, project: Model, From c2c5975e4e122979e63714ea486d5a8e6096bc58 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 11 Jul 2024 15:05:43 +0200 Subject: [PATCH 09/38] Fix save-or-discard prompt appearing with 1 item left --- crates/workspace/src/workspace.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0001e3b4ab9fe6..591520aa5020fd 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1525,7 +1525,7 @@ impl Workspace { let project = self.project.clone(); cx.spawn(|workspace, mut cx| async move { - let dirty_items = if save_intent == SaveIntent::Close && dirty_items.len() > 1 { + let dirty_items = if save_intent == SaveIntent::Close && dirty_items.len() > 0 { let (serialize_tasks, remaining_dirty_items) = workspace.update(&mut cx, |workspace, cx| { let mut remaining_dirty_items = Vec::new(); From 63e4789f2a769589509e8349824e5c8a9215fa7a Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 11 Jul 2024 17:27:39 +0200 Subject: [PATCH 10/38] WIP: Do not serialize content in unnamed workspaces --- crates/editor/src/items.rs | 15 ++++++------- crates/workspace/src/item.rs | 35 ++++++++++++++++++++----------- crates/workspace/src/pane.rs | 27 ++++++++++++------------ crates/workspace/src/workspace.rs | 12 +++++++++-- 4 files changed, 55 insertions(+), 34 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 34309f7b30b3c4..39a5e7fca44a88 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -903,7 +903,7 @@ impl Item for Editor { cx, ); } - language::Event::Edited => { + language::Event::Edited if this.can_serialize_content(cx) => { let workspace_id = *workspace_id; let item_id = cx.view().item_id().as_u64() as ItemId; this.serialize_unsaved_buffer_debounce.lock().fire_new( @@ -1058,12 +1058,14 @@ impl Item for Editor { } } - fn can_serialize(&self, cx: &AppContext) -> bool { - // TODO: Here we could check a setting. - self.buffer().read(cx).as_singleton().is_some() + fn can_serialize_content(&self, cx: &AppContext) -> bool { + let workspace_can_deserialize = self.workspace().map_or(false, |workspace| { + workspace.read(cx).can_deserialize_content(cx) + }); + workspace_can_deserialize && self.buffer().read(cx).as_singleton().is_some() } - fn serialize( + fn serialize_content( &self, workspace: &mut Workspace, item_id: ItemId, @@ -1105,7 +1107,7 @@ impl Item for Editor { }) } - fn delete_serialized( + fn delete_serialized_content( &self, workspace: &mut Workspace, item_id: ItemId, @@ -1115,7 +1117,6 @@ impl Item for Editor { return Task::ready(Ok(())); }; - println!("deleting serialized content of item {:?}", item_id); cx.spawn(|cx| async move { cx.background_executor() .spawn(DB.delete_contents(workspace_id, item_id)) diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index a89effacd4a5de..d599a7f1c9fafe 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -262,15 +262,20 @@ pub trait Item: FocusableView + EventEmitter { None } - fn can_serialize(&self, _: &AppContext) -> bool { + fn can_serialize_content(&self, _: &AppContext) -> bool { false } - fn serialize(&self, _: &mut Workspace, _: ItemId, _: &mut WindowContext) -> Task> { + fn serialize_content( + &self, + _: &mut Workspace, + _: ItemId, + _: &mut WindowContext, + ) -> Task> { Task::ready(Ok(())) } - fn delete_serialized( + fn delete_serialized_content( &self, _: &mut Workspace, _: ItemId, @@ -350,9 +355,13 @@ pub trait ItemHandle: 'static + Send { fn downgrade_item(&self) -> Box; fn workspace_settings<'a>(&self, cx: &'a AppContext) -> &'a WorkspaceSettings; - fn can_serialize(&self, cx: &AppContext) -> bool; - fn serialize(&self, workspace: &mut Workspace, cx: &mut WindowContext) -> Task>; - fn delete_serialized( + fn can_serialize_content(&self, cx: &AppContext) -> bool; + fn serialize_content( + &self, + workspace: &mut Workspace, + cx: &mut WindowContext, + ) -> Task>; + fn delete_serialized_content( &self, workspace: &mut Workspace, cx: &mut WindowContext, @@ -748,27 +757,29 @@ impl ItemHandle for View { Box::new(self.downgrade()) } - fn can_serialize(&self, cx: &AppContext) -> bool { - self.read(cx).can_serialize(cx) + fn can_serialize_content(&self, cx: &AppContext) -> bool { + self.read(cx).can_serialize_content(cx) } - fn serialize( + fn serialize_content( &self, workspace: &mut Workspace, cx: &mut WindowContext, ) -> Task> { let item_id = self.entity_id().as_u64() as ItemId; - self.update(cx, |item, cx| item.serialize(workspace, item_id, cx)) + self.update(cx, |item, cx| { + item.serialize_content(workspace, item_id, cx) + }) } - fn delete_serialized( + fn delete_serialized_content( &self, workspace: &mut Workspace, cx: &mut WindowContext, ) -> Task> { let item_id = self.entity_id().as_u64() as ItemId; self.update(cx, |item, cx| { - item.delete_serialized(workspace, item_id, cx) + item.delete_serialized_content(workspace, item_id, cx) }) } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 9d9d96f49bede1..abf4aa6aa38c7b 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1502,19 +1502,20 @@ impl Pane { item: &dyn ItemHandle, cx: &mut AsyncWindowContext, ) -> Option>> { - pane.update(cx, |pane, cx| { - if item.can_serialize(cx) { - let task = pane - .workspace - .update(cx, |workspace, cx| item.delete_serialized(workspace, cx)) - .ok()?; - Some(task) - } else { - None - } - }) - .ok() - .flatten() + let task = pane.update(cx, |pane, cx| { + pane.workspace.update(cx, |workspace, cx| { + if item.can_serialize_content(cx) { + Some(item.delete_serialized_content(workspace, cx)) + } else { + None + } + }) + }); + if let Ok(Ok(task)) = task { + task + } else { + None + } } pub fn autosave_item( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 591520aa5020fd..408c4c885074ea 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1531,8 +1531,8 @@ impl Workspace { let mut remaining_dirty_items = Vec::new(); let mut serialize_tasks = Vec::new(); for (pane, item) in dirty_items { - if item.can_serialize(cx) { - serialize_tasks.push(item.serialize(workspace, cx)); + if item.can_serialize_content(cx) { + serialize_tasks.push(item.serialize_content(workspace, cx)); } else { remaining_dirty_items.push((pane, item)); } @@ -3751,6 +3751,7 @@ impl Workspace { }; // don't save workspace state for the empty workspace. + println!("location: {:?}", location); if let Some(location) = location { let center_group = build_serialized_pane_group(&self.center.root, cx); let docks = build_serialized_docks(self, cx); @@ -3769,6 +3770,13 @@ impl Workspace { Task::ready(()) } + pub fn can_deserialize_content(&self, cx: &AppContext) -> bool { + // We only want to serialize content of workspace items if the workspace itself + // can also deserialize them again + self.local_paths(cx) + .map_or(false, |local_paths| !local_paths.is_empty()) + } + pub(crate) fn load_workspace( serialized_workspace: SerializedWorkspace, paths_to_open: Vec>, From 8630985ef28893a0a371d0c4ae5b4cb3a2445224 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 12 Jul 2024 12:55:17 +0200 Subject: [PATCH 11/38] Decentralize cleaning up of unloaded items --- crates/editor/src/editor.rs | 1 + crates/editor/src/items.rs | 18 +++++-- crates/editor/src/persistence.rs | 19 +++++++ crates/workspace/src/item.rs | 8 +++ crates/workspace/src/persistence.rs | 34 ++++++------- crates/workspace/src/workspace.rs | 79 ++++++++++++++++++----------- 6 files changed, 107 insertions(+), 52 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d3c7717c1aee43..513dda217ad756 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -273,6 +273,7 @@ pub fn init(cx: &mut AppContext) { workspace::register_project_item::(cx); workspace::register_followable_item::(cx); workspace::register_deserializable_item::(cx); + workspace::register_unloaded_items_cleaner::(cx); cx.observe_new_views( |workspace: &mut Workspace, _cx: &mut ViewContext| { workspace.register_action(Editor::new_file); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 39a5e7fca44a88..ea6bb09bd61957 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1058,11 +1058,21 @@ impl Item for Editor { } } + fn clean_unloaded_items( + workspace_id: WorkspaceId, + loaded_items: Vec, + cx: &mut WindowContext, + ) -> Task> { + cx.spawn(|_| DB.delete_unloaded_items(workspace_id, loaded_items)) + } + fn can_serialize_content(&self, cx: &AppContext) -> bool { - let workspace_can_deserialize = self.workspace().map_or(false, |workspace| { - workspace.read(cx).can_deserialize_content(cx) - }); - workspace_can_deserialize && self.buffer().read(cx).as_singleton().is_some() + // TODO: This is broken because the `workspace.read(cx)` panics when closing Zed + // let workspace_can_deserialize = self.workspace().map_or(false, |workspace| { + // workspace.read(cx).can_deserialize_content(cx) + // }); + // workspace_can_deserialize && self.buffer().read(cx).as_singleton().is_some() + self.buffer().read(cx).as_singleton().is_some() } fn serialize_content( diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index 99c81c324aad7c..36c7b89a814016 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use std::path::PathBuf; use db::sqlez_macros::sql; @@ -121,4 +122,22 @@ impl EditorDb { WHERE item_id = ?1 AND workspace_id = ?2 } } + + pub async fn delete_unloaded_items( + &self, + workspace: WorkspaceId, + loaded_item_ids: Vec, + ) -> Result<()> { + println!("Editor. delete_unloaded_items: ids: {:?}", loaded_item_ids); + let ids_string = loaded_item_ids + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(", "); + + let workspace_id: i64 = workspace.into(); + + let query = format!("DELETE FROM editor_contents WHERE workspace_id = {workspace_id} AND item_id NOT IN ({ids_string})"); + self.write(move |conn| conn.exec(&query).unwrap()()).await + } } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index d599a7f1c9fafe..2da59fb9af8e26 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -262,6 +262,14 @@ pub trait Item: FocusableView + EventEmitter { None } + fn clean_unloaded_items( + _workspace_id: WorkspaceId, + _loaded_items: Vec, + _cx: &mut WindowContext, + ) -> Task> { + Task::ready(Ok(())) + } + fn can_serialize_content(&self, _: &AppContext) -> bool { false } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 7c7e5a5ed6089a..775bc49382b34c 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -16,7 +16,7 @@ use ui::px; use util::ResultExt; use uuid::Uuid; -use crate::{ItemId, WorkspaceId}; +use crate::WorkspaceId; use model::{ GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, @@ -980,22 +980,22 @@ impl WorkspaceDb { } } - pub async fn delete_unloaded_items( - &self, - workspace: WorkspaceId, - item_ids: Vec, - ) -> Result<()> { - let ids_string = item_ids - .iter() - .map(|id| id.to_string()) - .collect::>() - .join(", "); - - let workspace_id = workspace.0; - // TODO: This is hacky because we're reaching into `editor_contents` from this struct - let query = format!("DELETE FROM editor_contents WHERE workspace_id = {workspace_id} AND item_id NOT IN ({ids_string})"); - self.write(move |conn| conn.exec(&query).unwrap()()).await - } + // pub async fn delete_unloaded_items( + // &self, + // workspace: WorkspaceId, + // item_ids: Vec, + // ) -> Result<()> { + // let ids_string = item_ids + // .iter() + // .map(|id| id.to_string()) + // .collect::>() + // .join(", "); + + // let workspace_id = workspace.0; + // // TODO: This is hacky because we're reaching into `editor_contents` from this struct + // let query = format!("DELETE FROM editor_contents WHERE workspace_id = {workspace_id} AND item_id NOT IN ({ids_string})"); + // self.write(move |conn| conn.exec(&query).unwrap()()).await + // } } #[cfg(test)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 408c4c885074ea..77bfa7f02f5124 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -85,7 +85,7 @@ use ui::{ ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _, WindowContext, }; -use util::{maybe, ResultExt}; +use util::{maybe, ResultExt, TryFutureExt}; use uuid::Uuid; pub use workspace_settings::{ AutosaveSetting, RestoreOnStartupBehaviour, TabBarSettings, WorkspaceSettings, @@ -280,6 +280,12 @@ impl Column for WorkspaceId { .with_context(|| format!("Failed to read WorkspaceId at index {start_index}")) } } +impl Into for WorkspaceId { + fn into(self) -> i64 { + self.0 + } +} + pub fn init_settings(cx: &mut AppContext) { WorkspaceSettings::register(cx); ItemSettings::register(cx); @@ -421,6 +427,25 @@ pub fn register_deserializable_item(cx: &mut AppContext) { } } +#[derive(Default, Deref, DerefMut)] +struct UnloadedItemsCleaners( + HashMap, fn(WorkspaceId, Vec, &mut WindowContext) -> Task>>, +); + +impl Global for UnloadedItemsCleaners {} + +pub fn register_unloaded_items_cleaner(cx: &mut AppContext) { + if let Some(serialized_item_kind) = I::serialized_item_kind() { + let cleaners = cx.default_global::(); + cleaners.insert( + Arc::from(serialized_item_kind), + |workspace_id, loaded_items, cx| { + I::clean_unloaded_items(workspace_id, loaded_items, cx) + }, + ); + } +} + pub struct AppState { pub languages: Arc, pub client: Arc, @@ -3751,7 +3776,6 @@ impl Workspace { }; // don't save workspace state for the empty workspace. - println!("location: {:?}", location); if let Some(location) = location { let center_group = build_serialized_pane_group(&self.center.root, cx); let docks = build_serialized_docks(self, cx); @@ -3804,14 +3828,16 @@ impl Workspace { } let mut items_by_project_path = HashMap::default(); - let mut item_ids = Vec::default(); + let mut item_ids_by_kind = HashMap::default(); cx.update(|cx| { - for item in center_items - .unwrap_or_default() - .into_iter() - .filter_map(|item| item) - { - item_ids.push(item.item_id().as_u64() as ItemId); + for item in center_items.unwrap_or_default().into_iter().flatten() { + if let Some(serialized_item_kind) = item.serialized_item_kind() { + item_ids_by_kind + .entry(serialized_item_kind) + .or_insert(Vec::new()) + .push(item.item_id().as_u64() as ItemId); + } + if let Some(project_path) = item.project_path(cx) { items_by_project_path.insert(project_path, item); } @@ -3863,12 +3889,19 @@ impl Workspace { // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means // after loading the items, we might have different items and in order to avoid // the database filling up, we delete items that haven't been loaded now. - workspace - .update(&mut cx, |workspace, cx| { - workspace.delete_unloaded_items(item_ids, cx) - })? - .await - .context("cleaning unloaded workspace items from database failed")?; + let clean_up_tasks = workspace.update(&mut cx, |_, cx| { + let database_id = serialized_workspace.id; + let mut tasks = vec![]; + for (item_kind, loaded_items) in item_ids_by_kind { + if let Some(clean_up) = cx.global::().get(item_kind) { + let task = clean_up(database_id, loaded_items, cx).log_err(); + tasks.push(task); + } + } + tasks + })?; + + futures::future::join_all(clean_up_tasks).await; // Serialize ourself to make sure our timestamps and any pane / item changes are replicated workspace @@ -3881,22 +3914,6 @@ impl Workspace { }) } - fn delete_unloaded_items( - &self, - loaded_items: Vec, - cx: &mut WindowContext, - ) -> Task> { - let Some(database_id) = self.database_id() else { - return Task::ready(Ok(())); - }; - - cx.spawn(|_| async move { - persistence::DB - .delete_unloaded_items(database_id, loaded_items) - .await - }) - } - fn actions(&self, div: Div, cx: &mut ViewContext) -> Div { self.add_workspace_actions_listeners(div, cx) .on_action(cx.listener(Self::close_inactive_items_and_panes)) From 33f8cba3d56ce96c206da560b727a48cb1dd0273 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 12 Jul 2024 13:48:44 +0200 Subject: [PATCH 12/38] Serialize loaded items again after loading --- crates/editor/src/items.rs | 9 ++++++++- crates/editor/src/persistence.rs | 1 - crates/workspace/src/item.rs | 4 +++- crates/workspace/src/workspace.rs | 26 ++++++++++++++++++++++---- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index ea6bb09bd61957..588f036733af4f 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -29,7 +29,7 @@ use std::{ ops::Range, path::Path, sync::Arc, - time::Duration, + time::{Duration, Instant}, }; use text::{BufferId, Selection}; use theme::{Theme, ThemeSettings}; @@ -880,12 +880,14 @@ impl Item for Editor { item_id: ItemId, cx: &mut AppContext, ) -> Task<()> { + let start = Instant::now(); let snapshot = buffer.read(cx).snapshot(); cx.background_executor().spawn(async move { let contents = snapshot.text(); DB.save_contents(item_id, workspace_id, contents) .await .log_err(); + println!("serialized edited buffer. took: {:?}", start.elapsed()); }) } @@ -1089,6 +1091,7 @@ impl Item for Editor { return Task::ready(Ok(())); }; + let start = Instant::now(); let path = buffer .read(cx) .file() @@ -1113,6 +1116,10 @@ impl Item for Editor { .await .context("failed to save contents of buffer")?; + println!( + "Editor.Item.serialize_content done. took: {:?}", + start.elapsed() + ); Ok(()) }) } diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index 36c7b89a814016..cfc9666295c4d5 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -128,7 +128,6 @@ impl EditorDb { workspace: WorkspaceId, loaded_item_ids: Vec, ) -> Result<()> { - println!("Editor. delete_unloaded_items: ids: {:?}", loaded_item_ids); let ids_string = loaded_item_ids .iter() .map(|id| id.to_string()) diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 2da59fb9af8e26..2ac7632e3fb508 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -280,7 +280,9 @@ pub trait Item: FocusableView + EventEmitter { _: ItemId, _: &mut WindowContext, ) -> Task> { - Task::ready(Ok(())) + unimplemented!( + "serialize_content() must be implemented if can_serialize_content() returns true" + ) } fn delete_serialized_content( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 77bfa7f02f5124..fa121bc3443914 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -74,7 +74,7 @@ use std::{ path::{Path, PathBuf}, rc::Rc, sync::{atomic::AtomicUsize, Arc, Weak}, - time::Duration, + time::{Duration, Instant}, }; use task::SpawnInTerminal; use theme::{ActiveTheme, SystemAppearance, ThemeSettings}; @@ -434,6 +434,12 @@ struct UnloadedItemsCleaners( impl Global for UnloadedItemsCleaners {} +impl UnloadedItemsCleaners { + pub fn global(cx: &AppContext) -> &Self { + cx.global::() + } +} + pub fn register_unloaded_items_cleaner(cx: &mut AppContext) { if let Some(serialized_item_kind) = I::serialized_item_kind() { let cleaners = cx.default_global::(); @@ -3829,6 +3835,7 @@ impl Workspace { let mut items_by_project_path = HashMap::default(); let mut item_ids_by_kind = HashMap::default(); + let mut all_deserialized_items = Vec::default(); cx.update(|cx| { for item in center_items.unwrap_or_default().into_iter().flatten() { if let Some(serialized_item_kind) = item.serialized_item_kind() { @@ -3839,8 +3846,9 @@ impl Workspace { } if let Some(project_path) = item.project_path(cx) { - items_by_project_path.insert(project_path, item); + items_by_project_path.insert(project_path, item.clone()); } + all_deserialized_items.push(item); } })?; @@ -3891,9 +3899,9 @@ impl Workspace { // the database filling up, we delete items that haven't been loaded now. let clean_up_tasks = workspace.update(&mut cx, |_, cx| { let database_id = serialized_workspace.id; - let mut tasks = vec![]; + let mut tasks = Vec::with_capacity(item_ids_by_kind.len()); for (item_kind, loaded_items) in item_ids_by_kind { - if let Some(clean_up) = cx.global::().get(item_kind) { + if let Some(clean_up) = UnloadedItemsCleaners::global(cx).get(item_kind) { let task = clean_up(database_id, loaded_items, cx).log_err(); tasks.push(task); } @@ -3903,6 +3911,16 @@ impl Workspace { futures::future::join_all(clean_up_tasks).await; + // If the item we loaded is dirty, it's possible that we might have just cleaned it up, + // because our IDs aren't stable. In that case, serialize its content again. + workspace.update(&mut cx, |workspace, cx| { + for item in all_deserialized_items { + if item.is_dirty(cx) && item.can_serialize_content(cx) { + item.serialize_content(workspace, cx).detach(); + } + } + })?; + // Serialize ourself to make sure our timestamps and any pane / item changes are replicated workspace .update(&mut cx, |workspace, cx| { From b87fea22195ba6e556119b09101a16bf9079a860 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 12 Jul 2024 14:53:12 +0200 Subject: [PATCH 13/38] Introduce SerializableItemRegistry --- crates/editor/src/editor.rs | 3 +- crates/image_viewer/src/image_viewer.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 4 +- crates/workspace/src/persistence/model.rs | 20 ++-- crates/workspace/src/workspace.rs | 126 +++++++++++++--------- 5 files changed, 92 insertions(+), 63 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 513dda217ad756..99321aedabec4b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -272,8 +272,7 @@ pub fn init(cx: &mut AppContext) { workspace::register_project_item::(cx); workspace::register_followable_item::(cx); - workspace::register_deserializable_item::(cx); - workspace::register_unloaded_items_cleaner::(cx); + workspace::register_serializable_item::(cx); cx.observe_new_views( |workspace: &mut Workspace, _cx: &mut ViewContext| { workspace.register_action(Editor::new_file); diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 8c03a5d5f2aa7a..938d418e788874 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -242,7 +242,7 @@ impl ProjectItem for ImageView { pub fn init(cx: &mut AppContext) { workspace::register_project_item::(cx); - workspace::register_deserializable_item::(cx) + workspace::register_serializable_item::(cx) } mod persistence { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 788e0575813588..d8580cc2baa321 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -31,7 +31,7 @@ use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, TabContentParams}, notifications::NotifyResultExt, - register_deserializable_item, + register_serializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, CloseActiveItem, NewCenterTerminal, OpenVisible, Pane, ToolbarItemLocation, Workspace, WorkspaceId, @@ -73,7 +73,7 @@ pub fn init(cx: &mut AppContext) { terminal_panel::init(cx); terminal::init(cx); - register_deserializable_item::(cx); + register_serializable_item::(cx); cx.observe_new_views(|workspace: &mut Workspace, _| { workspace.register_action(TerminalView::deploy); diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 339d3b3b8ecf6d..6cd2344056e708 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -1,5 +1,7 @@ use super::{SerializedAxis, SerializedWindowBounds}; -use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId}; +use crate::{ + item::ItemHandle, Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId, +}; use anyhow::{Context, Result}; use async_recursion::async_recursion; use client::DevServerProjectId; @@ -331,14 +333,14 @@ impl SerializedPane { for (index, item) in self.children.iter().enumerate() { let project = project.clone(); item_tasks.push(pane.update(cx, |_, cx| { - if let Some(deserializer) = cx.global::().get(&item.kind) { - deserializer(project, workspace.clone(), workspace_id, item.item_id, cx) - } else { - Task::ready(Err(anyhow::anyhow!( - "Deserializer does not exist for item kind: {}", - item.kind - ))) - } + SerializableItemRegistry::deserialize( + &item.kind, + project, + workspace.clone(), + workspace_id, + item.item_id, + cx, + ) })?); if item.active { active_item_index = Some(index); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index fa121bc3443914..4f72b560af1247 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -74,7 +74,7 @@ use std::{ path::{Path, PathBuf}, rc::Rc, sync::{atomic::AtomicUsize, Arc, Weak}, - time::{Duration, Instant}, + time::Duration, }; use task::SpawnInTerminal; use theme::{ActiveTheme, SystemAppearance, ThemeSettings}; @@ -397,58 +397,83 @@ pub fn register_followable_item(cx: &mut AppContext) { ); } +#[derive(Copy, Clone)] +struct SerializableItemDescriptor { + deserialize: fn( + Model, + WeakView, + WorkspaceId, + ItemId, + &mut ViewContext, + ) -> Task>>, + cleanup: fn(WorkspaceId, Vec, &mut WindowContext) -> Task>, +} + #[derive(Default, Deref, DerefMut)] -struct ItemDeserializers( - HashMap< - Arc, - fn( - Model, - WeakView, - WorkspaceId, - ItemId, - &mut ViewContext, - ) -> Task>>, - >, -); +struct SerializableItemRegistry(HashMap, SerializableItemDescriptor>); -impl Global for ItemDeserializers {} +impl Global for SerializableItemRegistry {} -pub fn register_deserializable_item(cx: &mut AppContext) { - if let Some(serialized_item_kind) = I::serialized_item_kind() { - let deserializers = cx.default_global::(); - deserializers.insert( - Arc::from(serialized_item_kind), - |project, workspace, workspace_id, item_id, cx| { - let task = I::deserialize(project, workspace, workspace_id, item_id, cx); - cx.foreground_executor() - .spawn(async { Ok(Box::new(task.await?) as Box<_>) }) - }, - ); +impl SerializableItemRegistry { + fn deserialize( + item_kind: &str, + project: Model, + workspace: WeakView, + workspace_id: WorkspaceId, + item_item: ItemId, + cx: &mut ViewContext, + ) -> Task>> { + let Some(descriptor) = Self::descriptor(item_kind, cx) else { + return Task::ready(Err(anyhow!( + "cannot deserialize {}, descriptor not found", + item_kind + ))); + }; + + (descriptor.deserialize)(project, workspace, workspace_id, item_item, cx) } -} -#[derive(Default, Deref, DerefMut)] -struct UnloadedItemsCleaners( - HashMap, fn(WorkspaceId, Vec, &mut WindowContext) -> Task>>, -); + fn cleanup( + item_kind: &str, + workspace_id: WorkspaceId, + loaded_items: Vec, + cx: &mut WindowContext, + ) -> Task> { + let Some(descriptor) = Self::descriptor(item_kind, cx) else { + return Task::ready(Err(anyhow!( + "cannot cleanup {}, descriptor not found", + item_kind + ))); + }; -impl Global for UnloadedItemsCleaners {} + (descriptor.cleanup)(workspace_id, loaded_items, cx) + } -impl UnloadedItemsCleaners { - pub fn global(cx: &AppContext) -> &Self { - cx.global::() + fn descriptor(item_kind: &str, cx: &AppContext) -> Option { + let this = cx.try_global::()?; + this.0.get(item_kind).copied() } } -pub fn register_unloaded_items_cleaner(cx: &mut AppContext) { +// NOTe: trait SerializableItem that derives from Item? +// NOTE: trait SerializableContentItem, that derives from Item? + +pub fn register_serializable_item(cx: &mut AppContext) { if let Some(serialized_item_kind) = I::serialized_item_kind() { - let cleaners = cx.default_global::(); - cleaners.insert( - Arc::from(serialized_item_kind), - |workspace_id, loaded_items, cx| { + let registry = cx.default_global::(); + let descriptor = SerializableItemDescriptor { + deserialize: |project, workspace, workspace_id, item_id, cx| { + let task = I::deserialize(project, workspace, workspace_id, item_id, cx); + cx.foreground_executor() + .spawn(async { Ok(Box::new(task.await?) as Box<_>) }) + }, + cleanup: |workspace_id, loaded_items, cx| { I::clean_unloaded_items(workspace_id, loaded_items, cx) }, - ); + }; + registry + .0 + .insert(Arc::from(serialized_item_kind), descriptor); } } @@ -3898,15 +3923,18 @@ impl Workspace { // after loading the items, we might have different items and in order to avoid // the database filling up, we delete items that haven't been loaded now. let clean_up_tasks = workspace.update(&mut cx, |_, cx| { - let database_id = serialized_workspace.id; - let mut tasks = Vec::with_capacity(item_ids_by_kind.len()); - for (item_kind, loaded_items) in item_ids_by_kind { - if let Some(clean_up) = UnloadedItemsCleaners::global(cx).get(item_kind) { - let task = clean_up(database_id, loaded_items, cx).log_err(); - tasks.push(task); - } - } - tasks + item_ids_by_kind + .into_iter() + .map(|(item_kind, loaded_items)| { + SerializableItemRegistry::cleanup( + item_kind, + serialized_workspace.id, + loaded_items, + cx, + ) + .log_err() + }) + .collect::>() })?; futures::future::join_all(clean_up_tasks).await; From 726cc0b3ef4d75ba232be2cbefb546b6d4a80c7c Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Fri, 12 Jul 2024 17:50:26 +0200 Subject: [PATCH 14/38] Introduce SerializableItem trait and rework structure Co-authored-by: Antonio --- crates/diagnostics/src/diagnostics.rs | 16 +- crates/editor/src/editor.rs | 2 - crates/editor/src/items.rs | 204 +++++------------- crates/image_viewer/src/image_viewer.rs | 57 +++-- crates/search/src/project_search.rs | 18 +- crates/terminal_view/src/terminal_panel.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 88 +++++--- crates/workspace/src/item.rs | 239 ++++++++++++++------- crates/workspace/src/pane.rs | 55 ++--- crates/workspace/src/persistence/model.rs | 2 +- crates/workspace/src/workspace.rs | 127 +++++------ 11 files changed, 398 insertions(+), 412 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 419e3d3fdcd6ac..a174e19032e04f 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -43,7 +43,7 @@ use ui::{h_flex, prelude::*, Icon, IconName, Label}; use util::ResultExt; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, - ItemNavHistory, Pane, ToolbarItemLocation, Workspace, + ItemNavHistory, ToolbarItemLocation, Workspace, }; actions!(diagnostics, [Deploy, ToggleWarnings]); @@ -779,20 +779,6 @@ impl Item for ProjectDiagnosticsEditor { self.editor .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx)); } - - fn serialized_item_kind() -> Option<&'static str> { - Some("diagnostics") - } - - fn deserialize( - project: Model, - workspace: WeakView, - _workspace_id: workspace::WorkspaceId, - _item_id: workspace::ItemId, - cx: &mut ViewContext, - ) -> Task>> { - Task::ready(Ok(cx.new_view(|cx| Self::new(project, workspace, cx)))) - } } const DIAGNOSTIC_HEADER: &'static str = "diagnostic header"; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 99321aedabec4b..0af73c26cdee4b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -566,7 +566,6 @@ pub struct Editor { previous_search_ranges: Option]>>, file_header_size: u8, breadcrumb_header: Option, - serialize_unsaved_buffer_debounce: Arc>, } #[derive(Clone)] @@ -1902,7 +1901,6 @@ impl Editor { linked_edit_ranges: Default::default(), previous_search_ranges: None, breadcrumb_header: None, - serialize_unsaved_buffer_debounce: Arc::new(Mutex::new(DebouncedDelay::new())), }; this.tasks_update_task = Some(this.refresh_runnables(cx)); this._subscriptions.extend(project_subscriptions); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 588f036733af4f..cc7acb61224f0d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -19,7 +19,7 @@ use multi_buffer::AnchorRangeExt; use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; use rpc::proto::{self, update_view, PeerId}; use settings::Settings; -use workspace::item::{ItemSettings, TabContentParams}; +use workspace::item::{ItemSettings, SerializableItem, TabContentParams}; use std::{ any::TypeId, @@ -29,7 +29,6 @@ use std::{ ops::Range, path::Path, sync::Arc, - time::{Duration, Instant}, }; use text::{BufferId, Selection}; use theme::{Theme, ThemeSettings}; @@ -37,7 +36,7 @@ use ui::{h_flex, prelude::*, Label}; use util::{paths::PathExt, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowEvent, FollowableItemHandle}; use workspace::{ - item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, + item::{FollowableItem, Item, ItemEvent, ProjectItem}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, ItemId, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, }; @@ -847,96 +846,8 @@ impl Item for Editor { Some(breadcrumbs) } - fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { + fn added_to_workspace(&mut self, workspace: &mut Workspace, _: &mut ViewContext) { self.workspace = Some((workspace.weak_handle(), workspace.database_id())); - let Some(workspace_id) = workspace.database_id() else { - return; - }; - - let item_id = cx.view().item_id().as_u64() as ItemId; - - fn serialize( - buffer: Model, - workspace_id: WorkspaceId, - item_id: ItemId, - cx: &mut AppContext, - ) { - if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { - let path = file.abs_path(cx); - - cx.background_executor() - .spawn(async move { - DB.save_path(item_id, workspace_id, path.clone()) - .await - .log_err() - }) - .detach(); - } - } - - fn serialize_edited_buffer( - buffer: Model, - workspace_id: WorkspaceId, - item_id: ItemId, - cx: &mut AppContext, - ) -> Task<()> { - let start = Instant::now(); - let snapshot = buffer.read(cx).snapshot(); - cx.background_executor().spawn(async move { - let contents = snapshot.text(); - DB.save_contents(item_id, workspace_id, contents) - .await - .log_err(); - println!("serialized edited buffer. took: {:?}", start.elapsed()); - }) - } - - 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((_, Some(workspace_id))) = this.workspace.as_ref() { - match event { - language::Event::FileHandleChanged => { - serialize( - buffer, - *workspace_id, - cx.view().item_id().as_u64() as ItemId, - cx, - ); - } - language::Event::Edited if this.can_serialize_content(cx) => { - let workspace_id = *workspace_id; - let item_id = cx.view().item_id().as_u64() as ItemId; - this.serialize_unsaved_buffer_debounce.lock().fire_new( - Duration::from_millis(100), - cx, - move |_, cx| { - serialize_edited_buffer(buffer, workspace_id, item_id, cx) - }, - ); - } - language::Event::Saved => { - let item_id = cx.view().item_id().as_u64() as ItemId; - cx.background_executor() - .spawn({ - let workspace_id = *workspace_id; - async move { - DB.delete_contents(workspace_id, item_id).await.log_err() - } - }) - .detach(); - } - _ => {} - } - } - }) - .detach(); - } - } - - fn serialized_item_kind() -> Option<&'static str> { - Some("Editor") } fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) { @@ -972,6 +883,20 @@ impl Item for Editor { _ => {} } } +} + +impl SerializableItem for Editor { + fn serialized_item_kind() -> &'static str { + "Editor" + } + + fn cleanup( + workspace_id: WorkspaceId, + alive_items: Vec, + cx: &mut WindowContext, + ) -> Task> { + cx.spawn(|_| DB.delete_unloaded_items(workspace_id, alive_items)) + } fn deserialize( project: Model, @@ -980,7 +905,6 @@ impl Item for Editor { item_id: ItemId, cx: &mut ViewContext, ) -> Task>> { - // Look up the path with this key associated, create a self with that path let path = match DB .get_path(item_id, workspace_id) .context("Failed to query editor state") @@ -1060,86 +984,60 @@ impl Item for Editor { } } - fn clean_unloaded_items( - workspace_id: WorkspaceId, - loaded_items: Vec, - cx: &mut WindowContext, - ) -> Task> { - cx.spawn(|_| DB.delete_unloaded_items(workspace_id, loaded_items)) - } - - fn can_serialize_content(&self, cx: &AppContext) -> bool { - // TODO: This is broken because the `workspace.read(cx)` panics when closing Zed - // let workspace_can_deserialize = self.workspace().map_or(false, |workspace| { - // workspace.read(cx).can_deserialize_content(cx) - // }); - // workspace_can_deserialize && self.buffer().read(cx).as_singleton().is_some() - self.buffer().read(cx).as_singleton().is_some() - } - - fn serialize_content( - &self, + fn serialize( + &mut self, workspace: &mut Workspace, item_id: ItemId, - cx: &mut WindowContext, - ) -> Task> { - let Some(workspace_id) = workspace.database_id() else { - return Task::ready(Ok(())); - }; + cx: &mut ViewContext, + ) -> Option>> { + let project = self.project.clone()?; + if project.read(cx).worktrees().next().is_none() { + // If we don't have a worktree, we don't serialize, because + // if we don't have a worktree, we can't deserialize ourselves. + return None; + } - let Some(buffer) = self.buffer().read(cx).as_singleton() else { - return Task::ready(Ok(())); - }; + let workspace_id = workspace.database_id()?; - let start = Instant::now(); + let buffer = self.buffer().read(cx).as_singleton()?; + + let is_dirty = buffer.read(cx).is_dirty(); let path = buffer .read(cx) .file() .and_then(|file| file.as_local()) .map(|file| file.abs_path(cx)); - let snapshot = buffer.read(cx).snapshot(); - cx.spawn(|cx| async move { - if let Some(path) = path { - cx.background_executor() - .spawn(async move { DB.save_path(item_id, workspace_id, path.clone()).await }) - .await - .context("failed to save path of buffer")? - } - + Some(cx.spawn(|_this, cx| async move { cx.background_executor() .spawn(async move { - let contents = snapshot.text(); - DB.save_contents(item_id, workspace_id, contents).await + if let Some(path) = path { + DB.save_path(item_id, workspace_id, path.clone()) + .await + .context("failed to save path of buffer")? + } + + if is_dirty { + let contents = snapshot.text(); + DB.save_contents(item_id, workspace_id, contents).await + } else { + // TODO: unspice + DB.delete_contents(workspace_id, item_id).await + } }) .await .context("failed to save contents of buffer")?; - println!( - "Editor.Item.serialize_content done. took: {:?}", - start.elapsed() - ); Ok(()) - }) + })) } - fn delete_serialized_content( - &self, - workspace: &mut Workspace, - item_id: ItemId, - cx: &mut WindowContext, - ) -> Task> { - let Some(workspace_id) = workspace.database_id() else { - return Task::ready(Ok(())); - }; - - cx.spawn(|cx| async move { - cx.background_executor() - .spawn(DB.delete_contents(workspace_id, item_id)) - .await - .context("failed to save contents of buffer") - }) + fn should_serialize(&self, event: &Self::Event) -> bool { + matches!( + event, + EditorEvent::Saved | EditorEvent::DirtyChanged | EditorEvent::BufferEdited + ) } } diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 938d418e788874..3c2c8e90881041 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -11,7 +11,7 @@ use project::{Project, ProjectEntryId, ProjectPath}; use std::{ffi::OsStr, path::PathBuf}; use util::ResultExt; use workspace::{ - item::{Item, ProjectItem, TabContentParams}, + item::{Item, ProjectItem, SerializableItem, TabContentParams}, ItemId, Pane, Workspace, WorkspaceId, }; @@ -110,8 +110,24 @@ impl Item for ImageView { } } - fn serialized_item_kind() -> Option<&'static str> { - Some(IMAGE_VIEWER_KIND) + fn clone_on_split( + &self, + _workspace_id: Option, + cx: &mut ViewContext, + ) -> Option> + where + Self: Sized, + { + Some(cx.new_view(|cx| Self { + path: self.path.clone(), + focus_handle: cx.focus_handle(), + })) + } +} + +impl SerializableItem for ImageView { + fn serialized_item_kind() -> &'static str { + IMAGE_VIEWER_KIND } fn deserialize( @@ -120,7 +136,7 @@ impl Item for ImageView { workspace_id: WorkspaceId, item_id: ItemId, cx: &mut ViewContext, - ) -> Task>> { + ) -> Task>> { cx.spawn(|_pane, mut cx| async move { let image_path = IMAGE_VIEWER .get_image_path(item_id, workspace_id)? @@ -133,19 +149,32 @@ impl Item for ImageView { }) } - fn clone_on_split( - &self, - _workspace_id: Option, + fn cleanup(_: WorkspaceId, _: Vec, _: &mut WindowContext) -> Task> { + Task::ready(Ok(())) + } + + fn serialize( + &mut self, + workspace: &mut Workspace, + item_id: ItemId, cx: &mut ViewContext, - ) -> Option> - where - Self: Sized, - { - Some(cx.new_view(|cx| Self { - path: self.path.clone(), - focus_handle: cx.focus_handle(), + ) -> Option>> { + let workspace_id = workspace.database_id()?; + + Some(cx.background_executor().spawn({ + let image_path = self.path.clone(); + async move { + IMAGE_VIEWER + .save_image_path(item_id, workspace_id, image_path) + .await + } })) } + + // TODO: Should we serialize on an "added to workspace" event? + fn should_serialize(&self, _event: &Self::Event) -> bool { + false + } } impl EventEmitter<()> for ImageView {} diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 82fc0a926cdcf9..df79986ee566ae 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -16,7 +16,7 @@ use gpui::{ EventEmitter, FocusHandle, FocusableView, FontStyle, Global, Hsla, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Point, Render, SharedString, Styled, Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, VisualContext, WeakModel, - WeakView, WhiteSpace, WindowContext, + WhiteSpace, WindowContext, }; use menu::Confirm; use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project, ProjectPath}; @@ -37,7 +37,7 @@ use util::paths::PathMatcher; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, searchable::{Direction, SearchableItem, SearchableItemHandle}, - DeploySearch, ItemNavHistory, NewSearch, Pane, ToolbarItemEvent, ToolbarItemLocation, + DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, }; @@ -506,20 +506,6 @@ impl Item for ProjectSearchView { fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { self.results_editor.breadcrumbs(theme, cx) } - - fn serialized_item_kind() -> Option<&'static str> { - None - } - - fn deserialize( - _project: Model, - _workspace: WeakView, - _workspace_id: workspace::WorkspaceId, - _item_id: workspace::ItemId, - _cx: &mut ViewContext, - ) -> Task>> { - unimplemented!() - } } impl ProjectSearchView { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 064adfaaf4e871..e194fcffb09d32 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -26,7 +26,7 @@ use ui::{ use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, - item::Item, + item::SerializableItem, pane, ui::IconName, DraggedTab, NewTerminal, Pane, ToggleZoom, Workspace, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index d8580cc2baa321..a68775dda9caa1 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -29,7 +29,7 @@ use terminal_element::{is_blank, TerminalElement}; use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip}; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ - item::{BreadcrumbText, Item, ItemEvent, TabContentParams}, + item::{BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams}, notifications::NotifyResultExt, register_serializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, @@ -612,22 +612,6 @@ fn subscribe_for_terminal_events( Event::TitleChanged => { cx.emit(ItemEvent::UpdateTab); - let terminal = this.terminal().read(cx); - if terminal.task().is_none() { - if let Some(cwd) = terminal.get_cwd() { - let item_id = cx.entity_id(); - if let Some(workspace_id) = this.workspace_id { - cx.background_executor() - .spawn(async move { - TERMINAL_DB - .save_working_directory(item_id.as_u64(), workspace_id, cwd) - .await - .log_err(); - }) - .detach(); - } - } - } } Event::NewNavigationTarget(maybe_navigation_target) => { @@ -1072,8 +1056,59 @@ impl Item for TerminalView { }]) } - fn serialized_item_kind() -> Option<&'static str> { - Some("Terminal") + fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { + if self.terminal().read(cx).task().is_none() { + if let Some((new_id, old_id)) = workspace.database_id().zip(self.workspace_id) { + cx.background_executor() + .spawn(TERMINAL_DB.update_workspace_id(new_id, old_id, cx.entity_id().as_u64())) + .detach(); + } + self.workspace_id = workspace.database_id(); + } + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + f(*event) + } +} + +impl SerializableItem for TerminalView { + fn serialized_item_kind() -> &'static str { + "Terminal" + } + + fn cleanup( + _workspace_id: WorkspaceId, + _alive_items: Vec, + _cx: &mut WindowContext, + ) -> Task> { + Task::ready(Ok(())) + } + + fn serialize( + &mut self, + _workspace: &mut Workspace, + item_id: workspace::ItemId, + cx: &mut ViewContext, + ) -> Option>> { + let terminal = self.terminal().read(cx); + if terminal.task().is_some() { + return None; + } + + if let Some((cwd, workspace_id)) = terminal.get_cwd().zip(self.workspace_id) { + Some(cx.background_executor().spawn(async move { + TERMINAL_DB + .save_working_directory(item_id, workspace_id, cwd) + .await + })) + } else { + None + } + } + + fn should_serialize(&self, event: &Self::Event) -> bool { + matches!(event, ItemEvent::UpdateTab) } fn deserialize( @@ -1116,21 +1151,6 @@ impl Item for TerminalView { }) }) } - - fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { - if self.terminal().read(cx).task().is_none() { - if let Some((new_id, old_id)) = workspace.database_id().zip(self.workspace_id) { - cx.background_executor() - .spawn(TERMINAL_DB.update_workspace_id(new_id, old_id, cx.entity_id().as_u64())) - .detach(); - } - self.workspace_id = workspace.database_id(); - } - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - f(*event) - } } impl SearchableItem for TerminalView { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 2ac7632e3fb508..e6f484b28d4256 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -3,8 +3,8 @@ use crate::{ persistence::model::ItemId, searchable::SearchableItemHandle, workspace_settings::{AutosaveSetting, WorkspaceSettings}, - DelayedDebouncedEditAction, FollowableItemBuilders, ItemNavHistory, ToolbarItemLocation, - ViewId, Workspace, WorkspaceId, + DelayedDebouncedEditAction, FollowableItemBuilders, ItemNavHistory, SerializableItemRegistry, + ToolbarItemLocation, ViewId, Workspace, WorkspaceId, }; use anyhow::Result; use client::{ @@ -32,6 +32,7 @@ use std::{ }; use theme::Theme; use ui::Element as _; +use util::ResultExt; pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200); @@ -240,9 +241,23 @@ pub trait Item: FocusableView + EventEmitter { fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext) {} - fn serialized_item_kind() -> Option<&'static str> { + fn show_toolbar(&self) -> bool { + true + } + + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option> { None } +} + +pub trait SerializableItem: Item { + fn serialized_item_kind() -> &'static str; + + fn cleanup( + workspace_id: WorkspaceId, + alive_items: Vec, + cx: &mut WindowContext, + ) -> Task>; fn deserialize( _project: Model, @@ -250,48 +265,50 @@ pub trait Item: FocusableView + EventEmitter { _workspace_id: WorkspaceId, _item_id: ItemId, _cx: &mut ViewContext, - ) -> Task>> { - unimplemented!( - "deserialize() must be implemented if serialized_item_kind() returns Some(_)" - ) - } - fn show_toolbar(&self) -> bool { - true - } - fn pixel_position_of_cursor(&self, _: &AppContext) -> Option> { - None - } + ) -> Task>>; - fn clean_unloaded_items( - _workspace_id: WorkspaceId, - _loaded_items: Vec, - _cx: &mut WindowContext, - ) -> Task> { - Task::ready(Ok(())) - } + fn serialize( + &mut self, + workspace: &mut Workspace, + item_id: ItemId, + cx: &mut ViewContext, + ) -> Option>>; - fn can_serialize_content(&self, _: &AppContext) -> bool { - false - } + fn should_serialize(&self, event: &Self::Event) -> bool; +} - fn serialize_content( +pub trait SerializableItemHandle: ItemHandle { + fn serialized_item_kind(&self) -> &'static str; + fn serialize( &self, - _: &mut Workspace, - _: ItemId, - _: &mut WindowContext, - ) -> Task> { - unimplemented!( - "serialize_content() must be implemented if can_serialize_content() returns true" - ) + workspace: &mut Workspace, + cx: &mut WindowContext, + ) -> Option>>; + fn should_serialize(&self, event: &dyn Any, cx: &AppContext) -> bool; +} + +impl SerializableItemHandle for View +where + T: SerializableItem, +{ + fn serialized_item_kind(&self) -> &'static str { + T::serialized_item_kind() } - fn delete_serialized_content( + fn serialize( &self, - _: &mut Workspace, - _: ItemId, - _: &mut WindowContext, - ) -> Task> { - Task::ready(Ok(())) + workspace: &mut Workspace, + cx: &mut WindowContext, + ) -> Option>> { + self.update(cx, |this, cx| { + this.serialize(workspace, cx.entity_id().as_u64(), cx) + }) + } + + fn should_serialize(&self, event: &dyn Any, cx: &AppContext) -> bool { + event + .downcast_ref::() + .map_or(false, |event| self.read(cx).should_serialize(event)) } } @@ -351,6 +368,10 @@ pub trait ItemHandle: 'static + Send { fn reload(&self, project: Model, cx: &mut WindowContext) -> Task>; fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; + fn to_serializable_item_handle( + &self, + cx: &AppContext, + ) -> Option>; fn on_release( &self, cx: &mut AppContext, @@ -359,23 +380,10 @@ pub trait ItemHandle: 'static + Send { fn to_searchable_item_handle(&self, cx: &AppContext) -> Option>; fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation; fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option>; - fn serialized_item_kind(&self) -> Option<&'static str>; fn show_toolbar(&self, cx: &AppContext) -> bool; fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option>; fn downgrade_item(&self) -> Box; fn workspace_settings<'a>(&self, cx: &'a AppContext) -> &'a WorkspaceSettings; - - fn can_serialize_content(&self, cx: &AppContext) -> bool; - fn serialize_content( - &self, - workspace: &mut Workspace, - cx: &mut WindowContext, - ) -> Task>; - fn delete_serialized_content( - &self, - workspace: &mut Workspace, - cx: &mut WindowContext, - ) -> Task>; } pub trait WeakItemHandle: Send + Sync { @@ -609,6 +617,43 @@ impl ItemHandle for View { } } + if let Some(item) = item.to_serializable_item_handle(cx) { + if item.should_serialize(event, cx) { + // send to channel on workspace + // items_to_serialize.send(item); + + // workspace.send_netw + + /* + + ready_chunks + + throttle isntead of debounce + + background_task.spawn({ + + let items_received = vec![]; + + recv().await; + try_recv a bunch to see what needs to be serialized + + group_by item.item_id() + + }) + + */ + + // let task = item.seriali + // todo!(""); + if let Some(task) = item.serialize(workspace, cx) { + println!("TODO: move all the serialization logic into one spot. publish here to wake up that spot."); + cx.background_executor() + .spawn(async move { task.await.log_err() }) + .detach(); + } + } + } + T::to_item_events(event, |event| match event { ItemEvent::CloseItem => { pane.update(cx, |pane, cx| { @@ -751,9 +796,9 @@ impl ItemHandle for View { self.read(cx).breadcrumbs(theme, cx) } - fn serialized_item_kind(&self) -> Option<&'static str> { - T::serialized_item_kind() - } + // fn serialized_item_kind(&self) -> Option<&'static str> { + // T::serialized_item_kind() + // } fn show_toolbar(&self, cx: &AppContext) -> bool { self.read(cx).show_toolbar() @@ -767,31 +812,38 @@ impl ItemHandle for View { Box::new(self.downgrade()) } - fn can_serialize_content(&self, cx: &AppContext) -> bool { - self.read(cx).can_serialize_content(cx) - } - - fn serialize_content( + fn to_serializable_item_handle( &self, - workspace: &mut Workspace, - cx: &mut WindowContext, - ) -> Task> { - let item_id = self.entity_id().as_u64() as ItemId; - self.update(cx, |item, cx| { - item.serialize_content(workspace, item_id, cx) - }) - } - - fn delete_serialized_content( - &self, - workspace: &mut Workspace, - cx: &mut WindowContext, - ) -> Task> { - let item_id = self.entity_id().as_u64() as ItemId; - self.update(cx, |item, cx| { - item.delete_serialized_content(workspace, item_id, cx) - }) - } + cx: &AppContext, + ) -> Option> { + SerializableItemRegistry::view_to_serializable_item_handle(self.to_any(), cx) + } + + // fn can_serialize_content(&self, cx: &AppContext) -> bool { + // self.read(cx).can_serialize_content(cx) + // } + + // fn serialize_content( + // &self, + // workspace: &mut Workspace, + // cx: &mut WindowContext, + // ) -> Task> { + // let item_id = self.entity_id().as_u64() as ItemId; + // self.update(cx, |item, cx| { + // item.serialize_content(workspace, item_id, cx) + // }) + // } + + // fn delete_serialized_content( + // &self, + // workspace: &mut Workspace, + // cx: &mut WindowContext, + // ) -> Task> { + // let item_id = self.entity_id().as_u64() as ItemId; + // self.update(cx, |item, cx| { + // item.delete_serialized_content(workspace, item_id, cx) + // }) + // } } impl From> for AnyView { @@ -952,7 +1004,7 @@ impl WeakFollowableItemHandle for WeakView { #[cfg(any(test, feature = "test-support"))] pub mod test { - use super::{Item, ItemEvent, TabContentParams}; + use super::{Item, ItemEvent, SerializableItem, TabContentParams}; use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId}; use gpui::{ AnyElement, AppContext, Context as _, EntityId, EventEmitter, FocusableView, @@ -1237,9 +1289,11 @@ pub mod test { self.is_dirty = false; Task::ready(Ok(())) } + } - fn serialized_item_kind() -> Option<&'static str> { - Some("TestItem") + impl SerializableItem for TestItem { + fn serialized_item_kind() -> &'static str { + "TestItem" } fn deserialize( @@ -1250,7 +1304,28 @@ pub mod test { cx: &mut ViewContext, ) -> Task>> { let view = cx.new_view(|cx| Self::new_deserialized(workspace_id, cx)); - Task::Ready(Some(anyhow::Ok(view))) + Task::ready(Ok(view)) + } + + fn cleanup( + _workspace_id: WorkspaceId, + _alive_items: Vec, + _cx: &mut ui::WindowContext, + ) -> Task> { + Task::ready(Ok(())) + } + + fn serialize( + &mut self, + _workspace: &mut Workspace, + _item_id: ItemId, + _cx: &mut ViewContext, + ) -> Option>> { + None + } + + fn should_serialize(&self, _event: &Self::Event) -> bool { + false } } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index abf4aa6aa38c7b..8dc0c28a9d50b8 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1370,9 +1370,6 @@ impl Pane { "This file has changed on disk since you started editing it. Do you want to overwrite it?"; if save_intent == SaveIntent::Skip { - if let Some(deletion_task) = Self::delete_serialized_content(pane, item, cx) { - deletion_task.await.map(|_| true)?; - } return Ok(true); } @@ -1455,16 +1452,8 @@ impl Pane { })?; match answer { Ok(0) => {} - Ok(1) => { - // Don't save this file - if let Some(deletion_task) = - Self::delete_serialized_content(pane, item, cx) - { - deletion_task.await.map(|_| true)?; - } - return Ok(true); - } - _ => return Ok(false), // Cancel + Ok(1) => return Ok(true), // Don't save this file + _ => return Ok(false), // Cancel } } else { return Ok(false); @@ -1497,26 +1486,26 @@ impl Pane { item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted } - fn delete_serialized_content( - pane: &WeakView, - item: &dyn ItemHandle, - cx: &mut AsyncWindowContext, - ) -> Option>> { - let task = pane.update(cx, |pane, cx| { - pane.workspace.update(cx, |workspace, cx| { - if item.can_serialize_content(cx) { - Some(item.delete_serialized_content(workspace, cx)) - } else { - None - } - }) - }); - if let Ok(Ok(task)) = task { - task - } else { - None - } - } + // fn delete_serialized_content( + // pane: &WeakView, + // item: &dyn ItemHandle, + // cx: &mut AsyncWindowContext, + // ) -> Option>> { + // let task = pane.update(cx, |pane, cx| { + // pane.workspace.update(cx, |workspace, cx| { + // if item.can_serialize_content(cx) { + // Some(item.delete_serialized_content(workspace, cx)) + // } else { + // None + // } + // }) + // }); + // if let Ok(Ok(task)) = task { + // task + // } else { + // None + // } + // } pub fn autosave_item( item: &dyn ItemHandle, diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 6cd2344056e708..38933d17ac9bdd 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -9,7 +9,7 @@ use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; -use gpui::{AsyncWindowContext, Model, Task, View, WeakView}; +use gpui::{AsyncWindowContext, Model, View, WeakView}; use project::Project; use serde::{Deserialize, Serialize}; use std::{ diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 4f72b560af1247..6d165fae561b85 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -37,7 +37,7 @@ use gpui::{ }; use item::{ FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings, - ProjectItem, + ProjectItem, SerializableItem, SerializableItemHandle, }; use itertools::Itertools; use language::{LanguageRegistry, Rope}; @@ -407,10 +407,14 @@ struct SerializableItemDescriptor { &mut ViewContext, ) -> Task>>, cleanup: fn(WorkspaceId, Vec, &mut WindowContext) -> Task>, + view_to_serializable_item: fn(AnyView) -> Box, } -#[derive(Default, Deref, DerefMut)] -struct SerializableItemRegistry(HashMap, SerializableItemDescriptor>); +#[derive(Default)] +struct SerializableItemRegistry { + descriptors_by_kind: HashMap, SerializableItemDescriptor>, + descriptors_by_type: HashMap, +} impl Global for SerializableItemRegistry {} @@ -449,32 +453,40 @@ impl SerializableItemRegistry { (descriptor.cleanup)(workspace_id, loaded_items, cx) } + fn view_to_serializable_item_handle( + item: AnyView, + cx: &AppContext, + ) -> Option> { + let this = cx.try_global::()?; + let descriptor = this.descriptors_by_type.get(&item.entity_type())?; + Some((descriptor.view_to_serializable_item)(item)) + } + fn descriptor(item_kind: &str, cx: &AppContext) -> Option { let this = cx.try_global::()?; - this.0.get(item_kind).copied() + this.descriptors_by_kind.get(item_kind).copied() } } -// NOTe: trait SerializableItem that derives from Item? -// NOTE: trait SerializableContentItem, that derives from Item? - -pub fn register_serializable_item(cx: &mut AppContext) { - if let Some(serialized_item_kind) = I::serialized_item_kind() { - let registry = cx.default_global::(); - let descriptor = SerializableItemDescriptor { - deserialize: |project, workspace, workspace_id, item_id, cx| { - let task = I::deserialize(project, workspace, workspace_id, item_id, cx); - cx.foreground_executor() - .spawn(async { Ok(Box::new(task.await?) as Box<_>) }) - }, - cleanup: |workspace_id, loaded_items, cx| { - I::clean_unloaded_items(workspace_id, loaded_items, cx) - }, - }; - registry - .0 - .insert(Arc::from(serialized_item_kind), descriptor); - } +pub fn register_serializable_item(cx: &mut AppContext) { + let serialized_item_kind = I::serialized_item_kind(); + + let registry = cx.default_global::(); + let descriptor = SerializableItemDescriptor { + deserialize: |project, workspace, workspace_id, item_id, cx| { + let task = I::deserialize(project, workspace, workspace_id, item_id, cx); + cx.foreground_executor() + .spawn(async { Ok(Box::new(task.await?) as Box<_>) }) + }, + cleanup: |workspace_id, loaded_items, cx| I::cleanup(workspace_id, loaded_items, cx), + view_to_serializable_item: |view| Box::new(view.downcast::().unwrap()), + }; + registry + .descriptors_by_kind + .insert(Arc::from(serialized_item_kind), descriptor); + registry + .descriptors_by_type + .insert(TypeId::of::(), descriptor); } pub struct AppState { @@ -1587,8 +1599,11 @@ impl Workspace { let mut remaining_dirty_items = Vec::new(); let mut serialize_tasks = Vec::new(); for (pane, item) in dirty_items { - if item.can_serialize_content(cx) { - serialize_tasks.push(item.serialize_content(workspace, cx)); + if let Some(task) = item + .to_serializable_item_handle(cx) + .and_then(|handle| handle.serialize(workspace, cx)) + { + serialize_tasks.push(task); } else { remaining_dirty_items.push((pane, item)); } @@ -3683,12 +3698,14 @@ impl Workspace { let active_item_id = pane.active_item().map(|item| item.item_id()); ( pane.items() - .filter_map(|item_handle| { + .filter_map(|handle| { + let handle = handle.to_serializable_item_handle(cx)?; + Some(SerializedItem { - kind: Arc::from(item_handle.serialized_item_kind()?), - item_id: item_handle.item_id().as_u64(), - active: Some(item_handle.item_id()) == active_item_id, - preview: pane.is_active_preview_item(item_handle.item_id()), + kind: Arc::from(handle.serialized_item_kind()), + item_id: handle.item_id().as_u64(), + active: Some(handle.item_id()) == active_item_id, + preview: pane.is_active_preview_item(handle.item_id()), }) }) .collect::>(), @@ -3825,13 +3842,6 @@ impl Workspace { Task::ready(()) } - pub fn can_deserialize_content(&self, cx: &AppContext) -> bool { - // We only want to serialize content of workspace items if the workspace itself - // can also deserialize them again - self.local_paths(cx) - .map_or(false, |local_paths| !local_paths.is_empty()) - } - pub(crate) fn load_workspace( serialized_workspace: SerializedWorkspace, paths_to_open: Vec>, @@ -3863,9 +3873,9 @@ impl Workspace { let mut all_deserialized_items = Vec::default(); cx.update(|cx| { for item in center_items.unwrap_or_default().into_iter().flatten() { - if let Some(serialized_item_kind) = item.serialized_item_kind() { + if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) { item_ids_by_kind - .entry(serialized_item_kind) + .entry(serializable_item_handle.serialized_item_kind()) .or_insert(Vec::new()) .push(item.item_id().as_u64() as ItemId); } @@ -3943,8 +3953,11 @@ impl Workspace { // because our IDs aren't stable. In that case, serialize its content again. workspace.update(&mut cx, |workspace, cx| { for item in all_deserialized_items { - if item.is_dirty(cx) && item.can_serialize_content(cx) { - item.serialize_content(workspace, cx).detach(); + if let Some(serializable_item) = item.to_serializable_item_handle(cx) { + // TODO: Maybe send this to serialize hcannel there + if let Some(task) = serializable_item.serialize(workspace, cx) { + task.detach(); + } } } })?; @@ -6307,7 +6320,6 @@ mod tests { use super::*; - const TEST_PNG_KIND: &str = "TestPngItemView"; // View struct TestPngItemView { focus_handle: FocusHandle, @@ -6339,10 +6351,6 @@ mod tests { impl Item for TestPngItemView { type Event = (); - - fn serialized_item_kind() -> Option<&'static str> { - Some(TEST_PNG_KIND) - } } impl EventEmitter<()> for TestPngItemView {} impl FocusableView for TestPngItemView { @@ -6374,7 +6382,6 @@ mod tests { } } - const TEST_IPYNB_KIND: &str = "TestIpynbItemView"; // View struct TestIpynbItemView { focus_handle: FocusHandle, @@ -6406,10 +6413,6 @@ mod tests { impl Item for TestIpynbItemView { type Event = (); - - fn serialized_item_kind() -> Option<&'static str> { - Some(TEST_IPYNB_KIND) - } } impl EventEmitter<()> for TestIpynbItemView {} impl FocusableView for TestIpynbItemView { @@ -6445,14 +6448,10 @@ mod tests { focus_handle: FocusHandle, } - const TEST_ALTERNATE_PNG_KIND: &str = "TestAlternatePngItemView"; impl Item for TestAlternatePngItemView { type Event = (); - - fn serialized_item_kind() -> Option<&'static str> { - Some(TEST_ALTERNATE_PNG_KIND) - } } + impl EventEmitter<()> for TestAlternatePngItemView {} impl FocusableView for TestAlternatePngItemView { fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { @@ -6519,7 +6518,10 @@ mod tests { .unwrap(); // Now we can check if the handle we got back errored or not - assert_eq!(handle.serialized_item_kind().unwrap(), TEST_PNG_KIND); + assert_eq!( + handle.to_any().entity_type(), + TypeId::of::() + ); let handle = workspace .update(cx, |workspace, cx| { @@ -6529,7 +6531,10 @@ mod tests { .await .unwrap(); - assert_eq!(handle.serialized_item_kind().unwrap(), TEST_IPYNB_KIND); + assert_eq!( + handle.to_any().entity_type(), + TypeId::of::() + ); let handle = workspace .update(cx, |workspace, cx| { @@ -6577,8 +6582,8 @@ mod tests { // This _must_ be the second item registered assert_eq!( - handle.serialized_item_kind().unwrap(), - TEST_ALTERNATE_PNG_KIND + handle.to_any().entity_type(), + TypeId::of::() ); let handle = workspace From 2e5f45a022a53429f90afbd1d9756ca1d19a80b3 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 15 Jul 2024 10:33:49 +0200 Subject: [PATCH 15/38] Update Cargo.lock after rebase --- Cargo.lock | 42 ++++++++++++++---------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1aef2b435e928c..d11ae44c5fb7dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1583,16 +1583,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec 0.6.3", -] - -[[package]] -name = "bit-set" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" -dependencies = [ - "bit-vec 0.7.0", + "bit-vec", ] [[package]] @@ -1601,12 +1592,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" -[[package]] -name = "bit-vec" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" - [[package]] name = "bit_field" version = "0.10.2" @@ -1649,7 +1634,7 @@ dependencies = [ [[package]] name = "blade-graphics" version = "0.4.0" -source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818" +source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" dependencies = [ "ash", "ash-window", @@ -1679,7 +1664,7 @@ dependencies = [ [[package]] name = "blade-macros" version = "0.2.1" -source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818" +source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" dependencies = [ "proc-macro2", "quote", @@ -1689,7 +1674,7 @@ dependencies = [ [[package]] name = "blade-util" version = "0.1.0" -source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818" +source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" dependencies = [ "blade-graphics", "bytemuck", @@ -4018,7 +4003,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7493d4c459da9f84325ad297371a6b2b8a162800873a22e3b6b6512e61d18c05" dependencies = [ - "bit-set 0.5.3", + "bit-set", "regex", ] @@ -6656,17 +6641,17 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "naga" -version = "0.20.0" -source = "git+https://github.com/gfx-rs/wgpu?rev=425526828f738c95ec50b016c6a761bc00d2fb25#425526828f738c95ec50b016c6a761bc00d2fb25" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae585df4b6514cf8842ac0f1ab4992edc975892704835b549cf818dc0191249e" dependencies = [ - "arrayvec", - "bit-set 0.6.0", + "bit-set", "bitflags 2.6.0", - "cfg_aliases", "codespan-reporting", "hexf-parse", "indexmap 2.2.6", "log", + "num-traits", "rustc-hash", "spirv", "termcolor", @@ -9927,11 +9912,12 @@ dependencies = [ [[package]] name = "spirv" -version = "0.3.0+sdk-1.3.268.0" +version = "0.2.0+1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" dependencies = [ - "bitflags 2.6.0", + "bitflags 1.3.2", + "num-traits", ] [[package]] From 58f6cead8e64f90f38216a86fa203af769fc8e54 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 15 Jul 2024 11:02:20 +0200 Subject: [PATCH 16/38] Bandaid get the tests to run before deciding on strategy --- crates/editor/src/items.rs | 7 +++++++ crates/recent_projects/src/recent_projects.rs | 5 +++-- crates/zed/src/zed.rs | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index cc7acb61224f0d..b38123ef66637c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1009,6 +1009,13 @@ impl SerializableItem for Editor { .map(|file| file.abs_path(cx)); let snapshot = buffer.read(cx).snapshot(); + #[cfg(feature = "test-support")] + if path.as_ref().map_or(false, |p| { + p.to_string_lossy().contains("does-not-serialize") + }) { + return None; + } + Some(cx.spawn(|_this, cx| async move { cx.background_executor() .spawn(async move { diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 9655c713048df2..1b7db4a4427e9c 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -700,6 +700,7 @@ mod tests { #[gpui::test] async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) { + let filename = "does-not-serialize-main.ts"; let app_state = init_test(cx); app_state .fs @@ -707,13 +708,13 @@ mod tests { .insert_tree( "/dir", json!({ - "main.ts": "a" + filename: "a" }), ) .await; cx.update(|cx| { open_paths( - &[PathBuf::from("/dir/main.ts")], + &[PathBuf::from(format!("/dir/{}", filename))], app_state, workspace::OpenOptions::default(), cx, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 011996738bed6c..d745a282afcedb 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1262,12 +1262,12 @@ mod tests { app_state .fs .as_fake() - .insert_tree("/root", json!({"a": "hey"})) + .insert_tree("/root", json!({"does-not-serialize": "hey"})) .await; cx.update(|cx| { open_paths( - &[PathBuf::from("/root/a")], + &[PathBuf::from("/root/does-not-serialize")], app_state.clone(), workspace::OpenOptions::default(), cx, From 43cf3ed39b4b064dba162afe611164d07af5c7c8 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 15 Jul 2024 11:09:15 +0200 Subject: [PATCH 17/38] Remove `bincode` in root Cargo.toml --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7a118fc9231acd..6779b7279bab5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -284,7 +284,6 @@ async-trait = "0.1" async-watch = "0.3.1" async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] } base64 = "0.13" -bincode = "1.2.1" bitflags = "2.4.2" blade-graphics = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" } blade-macros = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" } From 8db0d147f56ad3bd0ae3921f044261768eade278 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 15 Jul 2024 11:11:18 +0200 Subject: [PATCH 18/38] Change signature of function to unspice it --- crates/editor/src/items.rs | 3 +-- crates/editor/src/persistence.rs | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b38123ef66637c..88baf8844c72ba 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1029,8 +1029,7 @@ impl SerializableItem for Editor { let contents = snapshot.text(); DB.save_contents(item_id, workspace_id, contents).await } else { - // TODO: unspice - DB.delete_contents(workspace_id, item_id).await + DB.delete_contents(item_id, workspace_id).await } }) .await diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index cfc9666295c4d5..4792972e7161dc 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -90,10 +90,10 @@ impl EditorDb { } query! { - pub async fn delete_contents(workspace: WorkspaceId, item_id: ItemId) -> Result<()> { + pub async fn delete_contents(item_id: ItemId, workspace: WorkspaceId) -> Result<()> { DELETE FROM editor_contents - WHERE workspace_id = ? - AND item_id = ? + WHERE item_id = ?1 + AND workspace_id = ?2 } } From 62e559c3d8b4d9ede62a41edc96a9acd6f55e952 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 15 Jul 2024 11:32:29 +0200 Subject: [PATCH 19/38] remove dead, commented-out code --- crates/workspace/src/item.rs | 30 ----------------------------- crates/workspace/src/pane.rs | 21 -------------------- crates/workspace/src/persistence.rs | 17 ---------------- crates/workspace/src/workspace.rs | 6 +++--- crates/zed/src/zed.rs | 3 --- 5 files changed, 3 insertions(+), 74 deletions(-) diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index e6f484b28d4256..dd2b75d2341406 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -796,10 +796,6 @@ impl ItemHandle for View { self.read(cx).breadcrumbs(theme, cx) } - // fn serialized_item_kind(&self) -> Option<&'static str> { - // T::serialized_item_kind() - // } - fn show_toolbar(&self, cx: &AppContext) -> bool { self.read(cx).show_toolbar() } @@ -818,32 +814,6 @@ impl ItemHandle for View { ) -> Option> { SerializableItemRegistry::view_to_serializable_item_handle(self.to_any(), cx) } - - // fn can_serialize_content(&self, cx: &AppContext) -> bool { - // self.read(cx).can_serialize_content(cx) - // } - - // fn serialize_content( - // &self, - // workspace: &mut Workspace, - // cx: &mut WindowContext, - // ) -> Task> { - // let item_id = self.entity_id().as_u64() as ItemId; - // self.update(cx, |item, cx| { - // item.serialize_content(workspace, item_id, cx) - // }) - // } - - // fn delete_serialized_content( - // &self, - // workspace: &mut Workspace, - // cx: &mut WindowContext, - // ) -> Task> { - // let item_id = self.entity_id().as_u64() as ItemId; - // self.update(cx, |item, cx| { - // item.delete_serialized_content(workspace, item_id, cx) - // }) - // } } impl From> for AnyView { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 8dc0c28a9d50b8..026a4c37cff973 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1486,27 +1486,6 @@ impl Pane { item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted } - // fn delete_serialized_content( - // pane: &WeakView, - // item: &dyn ItemHandle, - // cx: &mut AsyncWindowContext, - // ) -> Option>> { - // let task = pane.update(cx, |pane, cx| { - // pane.workspace.update(cx, |workspace, cx| { - // if item.can_serialize_content(cx) { - // Some(item.delete_serialized_content(workspace, cx)) - // } else { - // None - // } - // }) - // }); - // if let Ok(Ok(task)) = task { - // task - // } else { - // None - // } - // } - pub fn autosave_item( item: &dyn ItemHandle, project: Model, diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 775bc49382b34c..8fcadcf4f5f4d3 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -979,23 +979,6 @@ impl WorkspaceDb { WHERE workspace_id = ?1 } } - - // pub async fn delete_unloaded_items( - // &self, - // workspace: WorkspaceId, - // item_ids: Vec, - // ) -> Result<()> { - // let ids_string = item_ids - // .iter() - // .map(|id| id.to_string()) - // .collect::>() - // .join(", "); - - // let workspace_id = workspace.0; - // // TODO: This is hacky because we're reaching into `editor_contents` from this struct - // let query = format!("DELETE FROM editor_contents WHERE workspace_id = {workspace_id} AND item_id NOT IN ({ids_string})"); - // self.write(move |conn| conn.exec(&query).unwrap()()).await - // } } #[cfg(test)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6d165fae561b85..a36d45b2537319 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -454,12 +454,12 @@ impl SerializableItemRegistry { } fn view_to_serializable_item_handle( - item: AnyView, + view: AnyView, cx: &AppContext, ) -> Option> { let this = cx.try_global::()?; - let descriptor = this.descriptors_by_type.get(&item.entity_type())?; - Some((descriptor.view_to_serializable_item)(item)) + let descriptor = this.descriptors_by_type.get(&view.entity_type())?; + Some((descriptor.view_to_serializable_item)(view)) } fn descriptor(item_kind: &str, cx: &AppContext) -> Option { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d745a282afcedb..94515632ad5be7 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -571,9 +571,6 @@ fn quit(_: &Quit, cx: &mut AppContext) { } } - // TODO: Ensure that all workspace items and unpersisted - // changes are persisted - // If the user cancels any save prompt, then keep the app open. for window in workspace_windows { if let Some(should_close) = window From 528609cf038708318bc6bdad6be9d0e87f0d5075 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 15 Jul 2024 14:13:10 +0200 Subject: [PATCH 20/38] Implement centralized serialization --- crates/image_viewer/src/image_viewer.rs | 19 ++------ crates/workspace/src/item.rs | 34 +------------- crates/workspace/src/workspace.rs | 61 ++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 50 deletions(-) diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 3c2c8e90881041..083fb22b23b04d 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -92,22 +92,10 @@ impl Item for ImageView { fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { let item_id = cx.entity_id().as_u64(); - let workspace_id = workspace.database_id(); - let image_path = self.path.clone(); - - if let Some(workspace_id) = workspace_id { - cx.background_executor() - .spawn({ - let image_path = image_path.clone(); - async move { - IMAGE_VIEWER - .save_image_path(item_id, workspace_id, image_path) - .await - .log_err(); - } - }) + if let Some(serialize_task) = self.serialize(workspace, item_id, cx) { + cx.spawn(|_, _| async move { serialize_task.await.log_err() }) .detach(); - } + }; } fn clone_on_split( @@ -171,7 +159,6 @@ impl SerializableItem for ImageView { })) } - // TODO: Should we serialize on an "added to workspace" event? fn should_serialize(&self, _event: &Self::Event) -> bool { false } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index dd2b75d2341406..94e17bf5904966 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -32,7 +32,6 @@ use std::{ }; use theme::Theme; use ui::Element as _; -use util::ResultExt; pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200); @@ -619,38 +618,7 @@ impl ItemHandle for View { if let Some(item) = item.to_serializable_item_handle(cx) { if item.should_serialize(event, cx) { - // send to channel on workspace - // items_to_serialize.send(item); - - // workspace.send_netw - - /* - - ready_chunks - - throttle isntead of debounce - - background_task.spawn({ - - let items_received = vec![]; - - recv().await; - try_recv a bunch to see what needs to be serialized - - group_by item.item_id() - - }) - - */ - - // let task = item.seriali - // todo!(""); - if let Some(task) = item.serialize(workspace, cx) { - println!("TODO: move all the serialization logic into one spot. publish here to wake up that spot."); - cx.background_executor() - .spawn(async move { task.await.log_err() }) - .detach(); - } + workspace.enqueue_item_serialization(item).ok(); } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a36d45b2537319..1e23eaa93b18aa 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -22,7 +22,10 @@ use collections::{hash_map, HashMap, HashSet}; use derive_more::{Deref, DerefMut}; use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle}; use futures::{ - channel::{mpsc, oneshot}, + channel::{ + mpsc::{self, UnboundedReceiver, UnboundedSender}, + oneshot, + }, future::try_join_all, Future, FutureExt, StreamExt, }; @@ -680,6 +683,8 @@ pub struct Workspace { on_prompt_for_new_path: Option, render_disconnected_overlay: Option) -> AnyElement>>, + serializable_items_tx: UnboundedSender>, + _items_serializer: Task>, } impl EventEmitter for Workspace {} @@ -860,6 +865,12 @@ impl Workspace { active_call = Some((call, subscriptions)); } + let (serializable_items_tx, serializable_items_rx) = + mpsc::unbounded::>(); + let _items_serializer = cx.spawn(|this, mut cx| async move { + Self::serialize_items(&this, serializable_items_rx, &mut cx).await + }); + let subscriptions = vec![ cx.observe_window_activation(Self::on_window_activation_changed), cx.observe_window_bounds(move |this, cx| { @@ -959,6 +970,8 @@ impl Workspace { bounds_save_task_queued: None, on_prompt_for_new_path: None, render_disconnected_overlay: None, + serializable_items_tx, + _items_serializer, } } @@ -3842,6 +3855,52 @@ impl Workspace { Task::ready(()) } + async fn serialize_items( + this: &WeakView, + items_rx: UnboundedReceiver>, + cx: &mut AsyncWindowContext, + ) -> Result<()> { + const CHUNK_SIZE: usize = 200; + const THROTTLE_TIME: Duration = Duration::from_millis(200); + + let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE); + + while let Some(items_received) = serializable_items.next().await { + let unique_items = + items_received + .into_iter() + .fold(HashMap::default(), |mut acc, item| { + acc.entry(item.serialized_item_kind().to_string()) + .or_insert(item); + acc + }); + + for (kind, item) in unique_items { + if let Ok(Some(task)) = + this.update(cx, |workspace, cx| item.serialize(workspace, cx)) + { + println!("serializing item of kind: {}", kind); + cx.background_executor() + .spawn(async move { task.await.log_err() }) + .detach(); + } + } + + cx.background_executor().timer(THROTTLE_TIME).await; + } + + Ok(()) + } + + pub(crate) fn enqueue_item_serialization( + &mut self, + item: Box, + ) -> Result<()> { + self.serializable_items_tx + .unbounded_send(item) + .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err)) + } + pub(crate) fn load_workspace( serialized_workspace: SerializedWorkspace, paths_to_open: Vec>, From 89eaf076a74110631df08274cd9f3da662037457 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 15 Jul 2024 14:54:51 +0200 Subject: [PATCH 21/38] Add clean_up for TerminalView/ImageViewer --- crates/image_viewer/src/image_viewer.rs | 26 ++++++++++++++++++++-- crates/terminal_view/src/persistence.rs | 18 +++++++++++++++ crates/terminal_view/src/terminal_panel.rs | 16 ++++++++++++- crates/terminal_view/src/terminal_view.rs | 8 +++---- crates/workspace/src/workspace.rs | 1 - 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 083fb22b23b04d..c8d32473beb99c 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -137,8 +137,12 @@ impl SerializableItem for ImageView { }) } - fn cleanup(_: WorkspaceId, _: Vec, _: &mut WindowContext) -> Task> { - Task::ready(Ok(())) + fn cleanup( + workspace_id: WorkspaceId, + alive_items: Vec, + cx: &mut WindowContext, + ) -> Task> { + cx.spawn(|_| IMAGE_VIEWER.delete_unloaded_items(workspace_id, alive_items)) } fn serialize( @@ -262,6 +266,7 @@ pub fn init(cx: &mut AppContext) { } mod persistence { + use anyhow::Result; use std::path::PathBuf; use db::{define_connection, query, sqlez_macros::sql}; @@ -314,5 +319,22 @@ mod persistence { WHERE item_id = ? AND workspace_id = ? } } + + pub async fn delete_unloaded_items( + &self, + workspace: WorkspaceId, + alive_items: Vec, + ) -> Result<()> { + let ids_string = alive_items + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(", "); + + let workspace_id: i64 = workspace.into(); + + let query = format!("DELETE FROM image_viewers WHERE workspace_id = {workspace_id} AND item_id NOT IN ({ids_string})"); + self.write(move |conn| conn.exec(&query).unwrap()()).await + } } } diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index 0da9ed47299d5b..110bd1211351d1 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use std::path::PathBuf; use db::{define_connection, query, sqlez_macros::sql}; @@ -68,4 +69,21 @@ impl TerminalDb { WHERE item_id = ? AND workspace_id = ? } } + + pub async fn delete_unloaded_items( + &self, + workspace: WorkspaceId, + alive_items: Vec, + ) -> Result<()> { + let ids_string = alive_items + .iter() + .map(|id| id.to_string()) + .collect::>() + .join(", "); + + let workspace_id: i64 = workspace.into(); + + let query = format!("DELETE FROM terminals WHERE workspace_id = {workspace_id} AND item_id NOT IN ({ids_string})"); + self.write(move |conn| conn.exec(&query).unwrap()()).await + } } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index e194fcffb09d32..7d50bdb12ea94c 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -29,7 +29,7 @@ use workspace::{ item::SerializableItem, pane, ui::IconName, - DraggedTab, NewTerminal, Pane, ToggleZoom, Workspace, + DraggedTab, ItemId, NewTerminal, Pane, ToggleZoom, Workspace, }; use anyhow::Result; @@ -278,6 +278,7 @@ impl TerminalPanel { let pane = pane.downgrade(); let items = futures::future::join_all(items).await; + let mut alive_item_ids = Vec::new(); pane.update(&mut cx, |pane, cx| { let active_item_id = serialized_panel .as_ref() @@ -287,6 +288,7 @@ impl TerminalPanel { if let Some(item) = item.log_err() { let item_id = item.entity_id().as_u64(); pane.add_item(Box::new(item), false, false, None, cx); + alive_item_ids.push(item_id as ItemId); if Some(item_id) == active_item_id { active_ix = Some(pane.items_len() - 1); } @@ -298,6 +300,18 @@ impl TerminalPanel { } })?; + // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace. + if let Some(workspace) = workspace.upgrade() { + let cleanup_task = workspace.update(&mut cx, |workspace, cx| { + workspace + .database_id() + .map(|workspace_id| TerminalView::cleanup(workspace_id, alive_item_ids, cx)) + })?; + if let Some(task) = cleanup_task { + task.await.log_err(); + } + } + Ok(panel) } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index a68775dda9caa1..864de028ef330e 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1078,11 +1078,11 @@ impl SerializableItem for TerminalView { } fn cleanup( - _workspace_id: WorkspaceId, - _alive_items: Vec, - _cx: &mut WindowContext, + workspace_id: WorkspaceId, + alive_items: Vec, + cx: &mut WindowContext, ) -> Task> { - Task::ready(Ok(())) + cx.spawn(|_| TERMINAL_DB.delete_unloaded_items(workspace_id, alive_items)) } fn serialize( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1e23eaa93b18aa..a7342dd242aadd 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3879,7 +3879,6 @@ impl Workspace { if let Ok(Some(task)) = this.update(cx, |workspace, cx| item.serialize(workspace, cx)) { - println!("serializing item of kind: {}", kind); cx.background_executor() .spawn(async move { task.await.log_err() }) .detach(); From 09e47adf420fab2dcbc4a9c7426f9a13ff469c46 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 15 Jul 2024 16:08:14 +0200 Subject: [PATCH 22/38] Fix warning and add comment --- crates/editor/src/items.rs | 7 +++++-- crates/workspace/src/workspace.rs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 88baf8844c72ba..b4d5c3e2803af7 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -960,8 +960,11 @@ impl SerializableItem for Editor { anyhow!("Project item at stored path was not a buffer") })?; - // TODO: This is a bit wasteful: we're loading the whole buffer from - // disk and then overwrite + // This is a bit wasteful: we're loading the whole buffer from + // disk and then overwrite the content. + // But for now, it keeps the implementation of the content serialization + // simple, because we don't have to persist all of the metadata that we get + // by loading the file (git diff base, mtime, ...). if let Some(contents) = contents { buffer.update(&mut cx, |buffer, cx| { buffer.set_text(contents, cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a7342dd242aadd..ef60577acc5401 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3875,7 +3875,7 @@ impl Workspace { acc }); - for (kind, item) in unique_items { + for (_, item) in unique_items { if let Ok(Some(task)) = this.update(cx, |workspace, cx| item.serialize(workspace, cx)) { From c27dd02f83bcbc2fb33bade0637d0752584e7c93 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 15 Jul 2024 16:17:24 +0200 Subject: [PATCH 23/38] Send serialization over channel --- crates/workspace/src/workspace.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ef60577acc5401..9e0bba33fa49e5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4012,13 +4012,11 @@ impl Workspace { workspace.update(&mut cx, |workspace, cx| { for item in all_deserialized_items { if let Some(serializable_item) = item.to_serializable_item_handle(cx) { - // TODO: Maybe send this to serialize hcannel there - if let Some(task) = serializable_item.serialize(workspace, cx) { - task.detach(); - } + workspace.enqueue_item_serialization(serializable_item)?; } } - })?; + anyhow::Ok(()) + })??; // Serialize ourself to make sure our timestamps and any pane / item changes are replicated workspace From 88b11506ad62b46797a5f0b6a4bbe4b803e1b3f5 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 15 Jul 2024 16:31:19 +0200 Subject: [PATCH 24/38] Put feature behind a feature flag --- crates/editor/src/editor.rs | 9 +++++- crates/editor/src/items.rs | 28 +++++++++++-------- crates/image_viewer/src/image_viewer.rs | 3 +- crates/project/src/project_settings.rs | 20 +++++++++++++ crates/recent_projects/src/recent_projects.rs | 5 ++-- crates/terminal_view/src/terminal_view.rs | 1 + crates/workspace/src/item.rs | 6 +++- crates/workspace/src/workspace.rs | 4 +-- crates/zed/src/zed.rs | 4 +-- 9 files changed, 58 insertions(+), 22 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0af73c26cdee4b..41740d2c6ce1c3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -550,6 +550,7 @@ pub struct Editor { show_git_blame_inline: bool, show_git_blame_inline_delay_task: Option>, git_blame_inline_enabled: bool, + serialize_dirty_buffers: bool, show_selection_menu: Option, blame: Option>, blame_subscription: Option, @@ -1874,6 +1875,9 @@ impl Editor { show_selection_menu: None, show_git_blame_inline_delay_task: None, git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), + serialize_dirty_buffers: ProjectSettings::get_global(cx) + .session + .restore_unsaved_sessions, blame: None, blame_subscription: None, file_header_size, @@ -11243,8 +11247,11 @@ impl Editor { self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin; self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs; + let project_settings = ProjectSettings::get_global(cx); + self.serialize_dirty_buffers = project_settings.session.restore_unsaved_sessions; + if self.mode == EditorMode::Full { - let inline_blame_enabled = ProjectSettings::get_global(cx).git.inline_blame_enabled(); + let inline_blame_enabled = project_settings.git.inline_blame_enabled(); if self.git_blame_inline_enabled != inline_blame_enabled { self.toggle_git_blame_inline_internal(false, cx); } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b4d5c3e2803af7..88c167a48b8cc6 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -991,12 +991,19 @@ impl SerializableItem for Editor { &mut self, workspace: &mut Workspace, item_id: ItemId, + closing: bool, cx: &mut ViewContext, ) -> Option>> { + let mut serialize_dirty_buffers = self.serialize_dirty_buffers; + let project = self.project.clone()?; if project.read(cx).worktrees().next().is_none() { // If we don't have a worktree, we don't serialize, because // if we don't have a worktree, we can't deserialize ourselves. + serialize_dirty_buffers = false; + } + + if closing && !serialize_dirty_buffers { return None; } @@ -1012,13 +1019,6 @@ impl SerializableItem for Editor { .map(|file| file.abs_path(cx)); let snapshot = buffer.read(cx).snapshot(); - #[cfg(feature = "test-support")] - if path.as_ref().map_or(false, |p| { - p.to_string_lossy().contains("does-not-serialize") - }) { - return None; - } - Some(cx.spawn(|_this, cx| async move { cx.background_executor() .spawn(async move { @@ -1028,12 +1028,16 @@ impl SerializableItem for Editor { .context("failed to save path of buffer")? } - if is_dirty { - let contents = snapshot.text(); - DB.save_contents(item_id, workspace_id, contents).await - } else { - DB.delete_contents(item_id, workspace_id).await + if serialize_dirty_buffers { + if is_dirty { + let contents = snapshot.text(); + DB.save_contents(item_id, workspace_id, contents).await?; + } else { + DB.delete_contents(item_id, workspace_id).await?; + } } + + anyhow::Ok(()) }) .await .context("failed to save contents of buffer")?; diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index c8d32473beb99c..d700fa1644aa52 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -92,7 +92,7 @@ impl Item for ImageView { fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { let item_id = cx.entity_id().as_u64(); - if let Some(serialize_task) = self.serialize(workspace, item_id, cx) { + if let Some(serialize_task) = self.serialize(workspace, item_id, false, cx) { cx.spawn(|_, _| async move { serialize_task.await.log_err() }) .detach(); }; @@ -149,6 +149,7 @@ impl SerializableItem for ImageView { &mut self, workspace: &mut Workspace, item_id: ItemId, + _closing: bool, cx: &mut ViewContext, ) -> Option>> { let workspace_id = workspace.database_id()?; diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index ecb2b31cb0f665..cc5614f932d58b 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -24,6 +24,10 @@ pub struct ProjectSettings { /// Configuration for how direnv configuration should be loaded #[serde(default)] pub load_direnv: DirenvSettings, + + /// Configuration for session-related features + #[serde(default)] + pub session: SessionSettings, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -107,6 +111,10 @@ const fn true_value() -> bool { true } +const fn false_value() -> bool { + false +} + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct BinarySettings { pub path: Option, @@ -122,6 +130,18 @@ pub struct LspSettings { pub settings: Option, } +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct SessionSettings { + /// Whether or not to restore unsaved buffers on restart. + /// + /// If this is true, user won't be prompted whether to save/discard + /// dirty files when closing the application. + /// + /// Default: false + #[serde(default = "false_value")] + pub restore_unsaved_sessions: bool, +} + impl Settings for ProjectSettings { const KEY: Option<&'static str> = None; diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 1b7db4a4427e9c..9655c713048df2 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -700,7 +700,6 @@ mod tests { #[gpui::test] async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) { - let filename = "does-not-serialize-main.ts"; let app_state = init_test(cx); app_state .fs @@ -708,13 +707,13 @@ mod tests { .insert_tree( "/dir", json!({ - filename: "a" + "main.ts": "a" }), ) .await; cx.update(|cx| { open_paths( - &[PathBuf::from(format!("/dir/{}", filename))], + &[PathBuf::from("/dir/main.ts")], app_state, workspace::OpenOptions::default(), cx, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 864de028ef330e..a74723a201d7f4 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1089,6 +1089,7 @@ impl SerializableItem for TerminalView { &mut self, _workspace: &mut Workspace, item_id: workspace::ItemId, + _closing: bool, cx: &mut ViewContext, ) -> Option>> { let terminal = self.terminal().read(cx); diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 94e17bf5904966..010c1c4dead92d 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -270,6 +270,7 @@ pub trait SerializableItem: Item { &mut self, workspace: &mut Workspace, item_id: ItemId, + closing: bool, cx: &mut ViewContext, ) -> Option>>; @@ -281,6 +282,7 @@ pub trait SerializableItemHandle: ItemHandle { fn serialize( &self, workspace: &mut Workspace, + closing: bool, cx: &mut WindowContext, ) -> Option>>; fn should_serialize(&self, event: &dyn Any, cx: &AppContext) -> bool; @@ -297,10 +299,11 @@ where fn serialize( &self, workspace: &mut Workspace, + closing: bool, cx: &mut WindowContext, ) -> Option>> { self.update(cx, |this, cx| { - this.serialize(workspace, cx.entity_id().as_u64(), cx) + this.serialize(workspace, cx.entity_id().as_u64(), closing, cx) }) } @@ -1257,6 +1260,7 @@ pub mod test { &mut self, _workspace: &mut Workspace, _item_id: ItemId, + _closing: bool, _cx: &mut ViewContext, ) -> Option>> { None diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9e0bba33fa49e5..91c28a099f3bea 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1614,7 +1614,7 @@ impl Workspace { for (pane, item) in dirty_items { if let Some(task) = item .to_serializable_item_handle(cx) - .and_then(|handle| handle.serialize(workspace, cx)) + .and_then(|handle| handle.serialize(workspace, true, cx)) { serialize_tasks.push(task); } else { @@ -3877,7 +3877,7 @@ impl Workspace { for (_, item) in unique_items { if let Ok(Some(task)) = - this.update(cx, |workspace, cx| item.serialize(workspace, cx)) + this.update(cx, |workspace, cx| item.serialize(workspace, false, cx)) { cx.background_executor() .spawn(async move { task.await.log_err() }) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 94515632ad5be7..28eee430ee25b1 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1259,12 +1259,12 @@ mod tests { app_state .fs .as_fake() - .insert_tree("/root", json!({"does-not-serialize": "hey"})) + .insert_tree("/root", json!({"a": "hey"})) .await; cx.update(|cx| { open_paths( - &[PathBuf::from("/root/does-not-serialize")], + &[PathBuf::from("/root/a")], app_state.clone(), workspace::OpenOptions::default(), cx, From 18a7ab5634fe3d11bef3464497bca76f5a6a243c Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 15 Jul 2024 17:23:57 +0200 Subject: [PATCH 25/38] Clean up that sqlez method --- crates/editor/src/persistence.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index 4792972e7161dc..ee4e2f8cdb6718 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use db::sqlez::statement::Statement; use std::path::PathBuf; use db::sqlez_macros::sql; @@ -128,15 +129,26 @@ impl EditorDb { workspace: WorkspaceId, loaded_item_ids: Vec, ) -> Result<()> { - let ids_string = loaded_item_ids + if loaded_item_ids.is_empty() { + return Ok(()); + } + + let placeholders = loaded_item_ids .iter() - .map(|id| id.to_string()) - .collect::>() + .map(|_| "?") + .collect::>() .join(", "); - let workspace_id: i64 = workspace.into(); + let query = format!("DELETE FROM editor_contents WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"); - let query = format!("DELETE FROM editor_contents WHERE workspace_id = {workspace_id} AND item_id NOT IN ({ids_string})"); - self.write(move |conn| conn.exec(&query).unwrap()()).await + self.write(move |conn| { + let mut statement = Statement::prepare(conn, query)?; + let mut next_index = statement.bind(&workspace, 1)?; + for id in loaded_item_ids { + next_index = statement.bind(&id, next_index)?; + } + statement.exec() + }) + .await } } From d49501cf2e50a40b96949676d7b787ce814f7329 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 15 Jul 2024 17:52:05 +0200 Subject: [PATCH 26/38] Change all where-not-in queries to use same pattern --- crates/editor/src/persistence.rs | 10 +++------- crates/image_viewer/src/image_viewer.rs | 21 ++++++++++++++------- crates/terminal_view/src/persistence.rs | 23 ++++++++++++++++------- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index ee4e2f8cdb6718..65195b8ea4d466 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -127,13 +127,9 @@ impl EditorDb { pub async fn delete_unloaded_items( &self, workspace: WorkspaceId, - loaded_item_ids: Vec, + alive_items: Vec, ) -> Result<()> { - if loaded_item_ids.is_empty() { - return Ok(()); - } - - let placeholders = loaded_item_ids + let placeholders = alive_items .iter() .map(|_| "?") .collect::>() @@ -144,7 +140,7 @@ impl EditorDb { self.write(move |conn| { let mut statement = Statement::prepare(conn, query)?; let mut next_index = statement.bind(&workspace, 1)?; - for id in loaded_item_ids { + for id in alive_items { next_index = statement.bind(&id, next_index)?; } statement.exec() diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index d700fa1644aa52..c540ab62d3f5a4 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -270,7 +270,7 @@ mod persistence { use anyhow::Result; use std::path::PathBuf; - use db::{define_connection, query, sqlez_macros::sql}; + use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; define_connection! { @@ -326,16 +326,23 @@ mod persistence { workspace: WorkspaceId, alive_items: Vec, ) -> Result<()> { - let ids_string = alive_items + let placeholders = alive_items .iter() - .map(|id| id.to_string()) - .collect::>() + .map(|_| "?") + .collect::>() .join(", "); - let workspace_id: i64 = workspace.into(); + let query = format!("DELETE FROM image_viewers WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"); - let query = format!("DELETE FROM image_viewers WHERE workspace_id = {workspace_id} AND item_id NOT IN ({ids_string})"); - self.write(move |conn| conn.exec(&query).unwrap()()).await + self.write(move |conn| { + let mut statement = Statement::prepare(conn, query)?; + let mut next_index = statement.bind(&workspace, 1)?; + for id in alive_items { + next_index = statement.bind(&id, next_index)?; + } + statement.exec() + }) + .await } } } diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index 110bd1211351d1..b8c31e05b014a2 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -1,7 +1,7 @@ use anyhow::Result; use std::path::PathBuf; -use db::{define_connection, query, sqlez_macros::sql}; +use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; define_connection! { @@ -75,15 +75,24 @@ impl TerminalDb { workspace: WorkspaceId, alive_items: Vec, ) -> Result<()> { - let ids_string = alive_items + let placeholders = alive_items .iter() - .map(|id| id.to_string()) - .collect::>() + .map(|_| "?") + .collect::>() .join(", "); - let workspace_id: i64 = workspace.into(); + let query = format!( + "DELETE FROM terminals WHERE workspace_id = ? AND item_id NOT IN ({placeholders})" + ); - let query = format!("DELETE FROM terminals WHERE workspace_id = {workspace_id} AND item_id NOT IN ({ids_string})"); - self.write(move |conn| conn.exec(&query).unwrap()()).await + self.write(move |conn| { + let mut statement = Statement::prepare(conn, query)?; + let mut next_index = statement.bind(&workspace, 1)?; + for id in alive_items { + next_index = statement.bind(&id, next_index)?; + } + statement.exec() + }) + .await } } From 549e4fd706012742970459fc8fa9849710d3c1e4 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 15 Jul 2024 17:53:10 +0200 Subject: [PATCH 27/38] Fix after rebase --- Cargo.lock | 42 ++++++++++++++++++++++++++++-------------- Cargo.toml | 8 ++++---- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d11ae44c5fb7dc..1aef2b435e928c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1583,7 +1583,16 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" +dependencies = [ + "bit-vec 0.7.0", ] [[package]] @@ -1592,6 +1601,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" + [[package]] name = "bit_field" version = "0.10.2" @@ -1634,7 +1649,7 @@ dependencies = [ [[package]] name = "blade-graphics" version = "0.4.0" -source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" +source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818" dependencies = [ "ash", "ash-window", @@ -1664,7 +1679,7 @@ dependencies = [ [[package]] name = "blade-macros" version = "0.2.1" -source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" +source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818" dependencies = [ "proc-macro2", "quote", @@ -1674,7 +1689,7 @@ dependencies = [ [[package]] name = "blade-util" version = "0.1.0" -source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" +source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818" dependencies = [ "blade-graphics", "bytemuck", @@ -4003,7 +4018,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7493d4c459da9f84325ad297371a6b2b8a162800873a22e3b6b6512e61d18c05" dependencies = [ - "bit-set", + "bit-set 0.5.3", "regex", ] @@ -6641,17 +6656,17 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "naga" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae585df4b6514cf8842ac0f1ab4992edc975892704835b549cf818dc0191249e" +version = "0.20.0" +source = "git+https://github.com/gfx-rs/wgpu?rev=425526828f738c95ec50b016c6a761bc00d2fb25#425526828f738c95ec50b016c6a761bc00d2fb25" dependencies = [ - "bit-set", + "arrayvec", + "bit-set 0.6.0", "bitflags 2.6.0", + "cfg_aliases", "codespan-reporting", "hexf-parse", "indexmap 2.2.6", "log", - "num-traits", "rustc-hash", "spirv", "termcolor", @@ -9912,12 +9927,11 @@ dependencies = [ [[package]] name = "spirv" -version = "0.2.0+1.5.4" +version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 1.3.2", - "num-traits", + "bitflags 2.6.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6779b7279bab5b..3e46369976d785 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -284,10 +284,10 @@ async-trait = "0.1" async-watch = "0.3.1" async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] } base64 = "0.13" -bitflags = "2.4.2" -blade-graphics = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" } -blade-macros = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" } -blade-util = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" } +bitflags = "2.6.0" +blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" } +blade-macros = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" } +blade-util = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" } cap-std = "3.0" cargo_toml = "0.20" chrono = { version = "0.4", features = ["serde"] } From a62af54497c281a6311db2cadc0b54a168c00a66 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 16 Jul 2024 10:31:46 +0200 Subject: [PATCH 28/38] Rename feature flag --- Cargo.lock | 1 + crates/editor/src/editor.rs | 4 ++-- crates/project/src/project_settings.rs | 10 +++------- crates/recent_projects/Cargo.toml | 1 + crates/recent_projects/src/recent_projects.rs | 14 ++++++++++++-- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1aef2b435e928c..48700530711ea0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8497,6 +8497,7 @@ dependencies = [ "rpc", "serde", "serde_json", + "settings", "smol", "task", "terminal_view", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 41740d2c6ce1c3..b86a604a0db42d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1877,7 +1877,7 @@ impl Editor { git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), serialize_dirty_buffers: ProjectSettings::get_global(cx) .session - .restore_unsaved_sessions, + .restore_unsaved_buffers, blame: None, blame_subscription: None, file_header_size, @@ -11248,7 +11248,7 @@ impl Editor { self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs; let project_settings = ProjectSettings::get_global(cx); - self.serialize_dirty_buffers = project_settings.session.restore_unsaved_sessions; + self.serialize_dirty_buffers = project_settings.session.restore_unsaved_buffers; if self.mode == EditorMode::Full { let inline_blame_enabled = project_settings.git.inline_blame_enabled(); diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index cc5614f932d58b..d5e172ba01ba5c 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -111,10 +111,6 @@ const fn true_value() -> bool { true } -const fn false_value() -> bool { - false -} - #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct BinarySettings { pub path: Option, @@ -137,9 +133,9 @@ pub struct SessionSettings { /// If this is true, user won't be prompted whether to save/discard /// dirty files when closing the application. /// - /// Default: false - #[serde(default = "false_value")] - pub restore_unsaved_sessions: bool, + /// Default: true + #[serde(default = "true_value")] + pub restore_unsaved_buffers: bool, } impl Settings for ProjectSettings { diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index c8975b15a80892..8f75d76f6b0056 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -39,5 +39,6 @@ workspace.workspace = true editor = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } +settings = { workspace = true, features = ["test-support"] } serde_json.workspace = true workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 9655c713048df2..652151f3bcb627 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -691,9 +691,10 @@ mod tests { use std::path::PathBuf; use editor::Editor; - use gpui::{TestAppContext, WindowHandle}; - use project::Project; + use gpui::{TestAppContext, UpdateGlobal, WindowHandle}; + use project::{project_settings::ProjectSettings, Project}; use serde_json::json; + use settings::SettingsStore; use workspace::{open_paths, AppState, LocalPaths}; use super::*; @@ -701,6 +702,15 @@ mod tests { #[gpui::test] async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) { let app_state = init_test(cx); + + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::(cx, |settings| { + settings.session.restore_unsaved_buffers = false + }); + }); + }); + app_state .fs .as_fake() From 497129e03a70e4653926c67b2aa08034798a9003 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 16 Jul 2024 11:53:07 +0200 Subject: [PATCH 29/38] Fix bug that threw away many item serializations --- crates/workspace/src/workspace.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 91c28a099f3bea..7576ceef017aa7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3870,8 +3870,7 @@ impl Workspace { items_received .into_iter() .fold(HashMap::default(), |mut acc, item| { - acc.entry(item.serialized_item_kind().to_string()) - .or_insert(item); + acc.entry(item.item_id()).or_insert(item); acc }); From 34770d3a83e9ece306d6140debc8bd52fd4fcb43 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 16 Jul 2024 11:58:46 +0200 Subject: [PATCH 30/38] Always serialize serializable items after adding to workspace --- crates/image_viewer/src/image_viewer.rs | 8 -------- crates/workspace/src/item.rs | 7 +++++++ crates/workspace/src/workspace.rs | 13 ++----------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index c540ab62d3f5a4..c8ac300192759c 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -90,14 +90,6 @@ impl Item for ImageView { .into_any_element() } - fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { - let item_id = cx.entity_id().as_u64(); - if let Some(serialize_task) = self.serialize(workspace, item_id, false, cx) { - cx.spawn(|_, _| async move { serialize_task.await.log_err() }) - .detach(); - }; - } - fn clone_on_split( &self, _workspace_id: Option, diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 010c1c4dead92d..e27c3d6c3521d8 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -32,6 +32,7 @@ use std::{ }; use theme::Theme; use ui::Element as _; +use util::ResultExt; pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200); @@ -526,6 +527,12 @@ impl ItemHandle for View { this.added_to_workspace(workspace, cx); }); + if let Some(serializable_item) = self.to_serializable_item_handle(cx) { + workspace + .enqueue_item_serialization(serializable_item) + .log_err(); + } + if let Some(followed_item) = self.to_followable_item_handle(cx) { if let Some(message) = followed_item.to_state_proto(cx) { workspace.update_followers( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7576ceef017aa7..2639c5ef27b81b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3989,6 +3989,8 @@ impl Workspace { // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means // after loading the items, we might have different items and in order to avoid // the database filling up, we delete items that haven't been loaded now. + // + // The items that have been loaded, have been saved after they've been added to the workspace. let clean_up_tasks = workspace.update(&mut cx, |_, cx| { item_ids_by_kind .into_iter() @@ -4006,17 +4008,6 @@ impl Workspace { futures::future::join_all(clean_up_tasks).await; - // If the item we loaded is dirty, it's possible that we might have just cleaned it up, - // because our IDs aren't stable. In that case, serialize its content again. - workspace.update(&mut cx, |workspace, cx| { - for item in all_deserialized_items { - if let Some(serializable_item) = item.to_serializable_item_handle(cx) { - workspace.enqueue_item_serialization(serializable_item)?; - } - } - anyhow::Ok(()) - })??; - // Serialize ourself to make sure our timestamps and any pane / item changes are replicated workspace .update(&mut cx, |workspace, cx| { From 4069debe03dac638793011e12cdca4dc526355ca Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 16 Jul 2024 14:03:15 +0200 Subject: [PATCH 31/38] Use single table to store editor contents/language --- crates/editor/src/items.rs | 79 ++++++++++++++++--------- crates/editor/src/persistence.rs | 56 +++++++++++------- crates/image_viewer/src/image_viewer.rs | 1 - crates/workspace/src/workspace.rs | 2 +- 4 files changed, 86 insertions(+), 52 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 88c167a48b8cc6..b88e70e95848d9 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -16,7 +16,10 @@ use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal, }; use multi_buffer::AnchorRangeExt; -use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; +use project::{ + project_settings::ProjectSettings, search::SearchQuery, FormatTrigger, Item as _, Project, + ProjectPath, +}; use rpc::proto::{self, update_view, PeerId}; use settings::Settings; use workspace::item::{ItemSettings, SerializableItem, TabContentParams}; @@ -905,41 +908,56 @@ impl SerializableItem for Editor { item_id: ItemId, cx: &mut ViewContext, ) -> Task>> { - let path = match DB - .get_path(item_id, workspace_id) + let path_content_language = match DB + .get_path_and_contents(item_id, workspace_id) .context("Failed to query editor state") { - Ok(path) => path, - Err(error) => { - return Task::ready(Err(error)); + Ok(Some((path, content, language))) => { + if ProjectSettings::get_global(cx) + .session + .restore_unsaved_buffers + { + (path, content, language) + } else { + (path, None, None) + } + } + Ok(None) => { + return Task::ready(Err(anyhow!("No path or contents found for buffer"))); } - }; - - let contents = match DB - .get_contents(item_id, workspace_id) - .context("Failed to query editor content") - { - Ok(contents) => contents, Err(error) => { return Task::ready(Err(error)); } }; - match (path, contents) { - (None, Some(contents)) => { - let buffer = cx.new_model(|cx| { - let mut buffer = Buffer::local("", cx); - buffer.set_text(contents, cx); - buffer - }); - let view = cx.new_view(|cx| { + match path_content_language { + (None, Some(content), language_name) => cx.spawn(|_, mut cx| async move { + let language = if let Some(language_name) = language_name { + let language_registry = + project.update(&mut cx, |project, _| project.languages().clone())?; + + Some(language_registry.language_for_name(&language_name).await?) + } else { + None + }; + + // First create the empty buffer + let buffer = project.update(&mut cx, |project, cx| { + project.create_local_buffer("", language, cx) + })?; + + // Then set the text so that the language is set correctly + buffer.update(&mut cx, |buffer, cx| { + buffer.set_text(content, cx); + })?; + + cx.new_view(|cx| { let mut editor = Editor::for_buffer(buffer, Some(project), cx); editor.read_scroll_position_from_db(item_id, workspace_id, cx); editor - }); - Task::ready(Ok(view)) - } - (Some(path), contents) => { + }) + }), + (Some(path), contents, _) => { let project_item = project.update(cx, |project, cx| { let (worktree, path) = project .find_local_worktree(&path, cx) @@ -965,9 +983,9 @@ impl SerializableItem for Editor { // But for now, it keeps the implementation of the content serialization // simple, because we don't have to persist all of the metadata that we get // by loading the file (git diff base, mtime, ...). - if let Some(contents) = contents { + if let Some(buffer_text) = contents { buffer.update(&mut cx, |buffer, cx| { - buffer.set_text(contents, cx); + buffer.set_text(buffer_text, cx); })?; } @@ -983,7 +1001,7 @@ impl SerializableItem for Editor { }) .unwrap_or_else(|error| Task::ready(Err(error))) } - (None, None) => Task::ready(Err(anyhow!("No path or contents found for buffer"))), + _ => Task::ready(Err(anyhow!("No path or contents found for buffer"))), } } @@ -1031,7 +1049,10 @@ impl SerializableItem for Editor { if serialize_dirty_buffers { if is_dirty { let contents = snapshot.text(); - DB.save_contents(item_id, workspace_id, contents).await?; + let language = snapshot.language().map(|lang| lang.name().to_string()); + println!("saving dirty buffer with language {:?}", language); + DB.save_contents(item_id, workspace_id, contents, language) + .await?; } else { DB.delete_contents(item_id, workspace_id).await?; } diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index 65195b8ea4d466..00997e5e93ddb9 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -12,10 +12,12 @@ define_connection!( // editors( // item_id: usize, // workspace_id: usize, - // path: PathBuf, + // path: Option, // scroll_top_row: usize, // scroll_vertical_offset: f32, // scroll_horizontal_offset: f32, + // content: Option, + // language: Option, // ) pub static ref DB: EditorDb = &[sql! ( @@ -35,22 +37,37 @@ define_connection!( ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0; ), sql! ( - CREATE TABLE editor_contents ( + // Since sqlite3 doesn't support ALTER TABLE, we create a new + // table, move the data over, drop the old table, rename new table. + CREATE TABLE new_editors_tmp ( item_id INTEGER NOT NULL, workspace_id INTEGER NOT NULL, - contents TEXT NOT NULL, + path BLOB, // <-- No longer "NOT NULL" + scroll_top_row INTEGER NOT NULL DEFAULT 0, + scroll_horizontal_offset REAL NOT NULL DEFAULT 0, + scroll_vertical_offset REAL NOT NULL DEFAULT 0, + contents TEXT, // New + language TEXT, // New PRIMARY KEY(item_id, workspace_id), FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ON UPDATE CASCADE ) STRICT; + + INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset) + SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset + FROM editors; + + DROP TABLE editors; + + ALTER TABLE new_editors_tmp RENAME TO editors; )]; ); impl EditorDb { query! { - pub fn get_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { - SELECT path FROM editors + pub fn get_path_and_contents(item_id: ItemId, workspace_id: WorkspaceId) -> Result, Option, Option)>> { + SELECT path, contents, language FROM editors WHERE item_id = ? AND workspace_id = ? } } @@ -69,30 +86,25 @@ impl EditorDb { } query! { - pub fn get_contents(item_id: ItemId, workspace: WorkspaceId) -> Result> { - SELECT contents - FROM editor_contents - WHERE item_id = ?1 - AND workspace_id = ?2 - } - } - - query! { - pub async fn save_contents(item_id: ItemId, workspace: WorkspaceId, contents: String) -> Result<()> { - INSERT INTO editor_contents - (item_id, workspace_id, contents) + pub async fn save_contents(item_id: ItemId, workspace: WorkspaceId, contents: String, language: Option) -> Result<()> { + INSERT INTO editors + (item_id, workspace_id, contents, language) VALUES - (?1, ?2, ?3) + (?1, ?2, ?3, ?4) ON CONFLICT DO UPDATE SET item_id = ?1, workspace_id = ?2, - contents = ?3 + contents = ?3, + language = ?4 } } query! { pub async fn delete_contents(item_id: ItemId, workspace: WorkspaceId) -> Result<()> { - DELETE FROM editor_contents + UPDATE editors + SET + contents = NULL, + language = NULL WHERE item_id = ?1 AND workspace_id = ?2 } @@ -135,7 +147,9 @@ impl EditorDb { .collect::>() .join(", "); - let query = format!("DELETE FROM editor_contents WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"); + let query = format!( + "DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})" + ); self.write(move |conn| { let mut statement = Statement::prepare(conn, query)?; diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index c8ac300192759c..d654141a11bebf 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -9,7 +9,6 @@ use ui::prelude::*; use project::{Project, ProjectEntryId, ProjectPath}; use std::{ffi::OsStr, path::PathBuf}; -use util::ResultExt; use workspace::{ item::{Item, ProjectItem, SerializableItem, TabContentParams}, ItemId, Pane, Workspace, WorkspaceId, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2639c5ef27b81b..1cf3d368935705 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3874,7 +3874,7 @@ impl Workspace { acc }); - for (_, item) in unique_items { + for (kind, item) in unique_items { if let Ok(Some(task)) = this.update(cx, |workspace, cx| item.serialize(workspace, false, cx)) { From aa2cb58ec05535aa0c4cc51da871c9cdb7d8fb1f Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Tue, 16 Jul 2024 14:13:49 +0200 Subject: [PATCH 32/38] Minor cleanup --- crates/editor/src/items.rs | 1 - crates/workspace/src/workspace.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b88e70e95848d9..19c52728ee4a6d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1050,7 +1050,6 @@ impl SerializableItem for Editor { if is_dirty { let contents = snapshot.text(); let language = snapshot.language().map(|lang| lang.name().to_string()); - println!("saving dirty buffer with language {:?}", language); DB.save_contents(item_id, workspace_id, contents, language) .await?; } else { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1cf3d368935705..f81833c6cd556c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3874,7 +3874,7 @@ impl Workspace { acc }); - for (kind, item) in unique_items { + for item in unique_items.values() { if let Ok(Some(task)) = this.update(cx, |workspace, cx| item.serialize(workspace, false, cx)) { From 77094464f2686c27979e9c625dfb545acc7e1cf5 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 17 Jul 2024 10:52:01 +0200 Subject: [PATCH 33/38] Fix things after rebase/merge --- crates/diagnostics/src/grouped_diagnostics.rs | 16 +--------------- crates/editor/src/items.rs | 8 ++++---- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/crates/diagnostics/src/grouped_diagnostics.rs b/crates/diagnostics/src/grouped_diagnostics.rs index db790e75f2ca8a..043e0f5825e017 100644 --- a/crates/diagnostics/src/grouped_diagnostics.rs +++ b/crates/diagnostics/src/grouped_diagnostics.rs @@ -40,7 +40,7 @@ use ui::{h_flex, prelude::*, Icon, IconName, Label}; use util::{debug_panic, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, - ItemNavHistory, Pane, ToolbarItemLocation, Workspace, + ItemNavHistory, ToolbarItemLocation, Workspace, }; use crate::project_diagnostics_settings::ProjectDiagnosticsSettings; @@ -603,20 +603,6 @@ impl Item for GroupedDiagnosticsEditor { self.editor .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx)); } - - fn serialized_item_kind() -> Option<&'static str> { - Some("diagnostics") - } - - fn deserialize( - project: Model, - workspace: WeakView, - _workspace_id: workspace::WorkspaceId, - _item_id: workspace::ItemId, - cx: &mut ViewContext, - ) -> Task>> { - Task::ready(Ok(cx.new_view(|cx| Self::new(project, workspace, cx)))) - } } fn compare_data_locations( diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index fabb513c631350..7f11be4f968822 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -937,7 +937,7 @@ impl SerializableItem for Editor { project.create_local_buffer("", language, cx) })?; - // Then set the text so that the language is set correctly + // Then set the text so that the dirty bit is set correctly buffer.update(&mut cx, |buffer, cx| { buffer.set_text(content, cx); })?; @@ -951,7 +951,7 @@ impl SerializableItem for Editor { (Some(path), contents, _) => { let project_item = project.update(cx, |project, cx| { let (worktree, path) = project - .find_local_worktree(&path, cx) + .find_worktree(&path, cx) .with_context(|| format!("No worktree for path: {path:?}"))?; let project_path = ProjectPath { worktree_id: worktree.read(cx).id(), @@ -1006,9 +1006,9 @@ impl SerializableItem for Editor { let mut serialize_dirty_buffers = self.serialize_dirty_buffers; let project = self.project.clone()?; - if project.read(cx).worktrees().next().is_none() { + if project.read(cx).visible_worktrees(cx).next().is_none() { // If we don't have a worktree, we don't serialize, because - // if we don't have a worktree, we can't deserialize ourselves. + // projects without worktrees aren't deserialized. serialize_dirty_buffers = false; } From 07f6ff08bb8660476167257b3de79e29f0313eb3 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 17 Jul 2024 11:37:07 +0200 Subject: [PATCH 34/38] Fix default value of restore_unsaved_buffers --- crates/project/src/project_settings.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index d5e172ba01ba5c..63bdc6da74c097 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -126,7 +126,7 @@ pub struct LspSettings { pub settings: Option, } -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct SessionSettings { /// Whether or not to restore unsaved buffers on restart. /// @@ -134,10 +134,17 @@ pub struct SessionSettings { /// dirty files when closing the application. /// /// Default: true - #[serde(default = "true_value")] pub restore_unsaved_buffers: bool, } +impl Default for SessionSettings { + fn default() -> Self { + Self { + restore_unsaved_buffers: true, + } + } +} + impl Settings for ProjectSettings { const KEY: Option<&'static str> = None; From a6c3352c48ff7c332408abfc37fdaf87e5632342 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 17 Jul 2024 14:56:04 +0200 Subject: [PATCH 35/38] Fix tests and add more tests to ensure it works --- crates/workspace/src/item.rs | 19 ++++- crates/workspace/src/workspace.rs | 44 ++++++++++- crates/zed/src/zed.rs | 122 +++++++++++++++++++++++++++++- 3 files changed, 178 insertions(+), 7 deletions(-) diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index da5af174be7fc8..74d8892e86f0bd 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -979,6 +979,7 @@ pub mod test { pub nav_history: Option, pub tab_descriptions: Option>, pub tab_detail: Cell>, + serialize: Option Option>>>>, focus_handle: gpui::FocusHandle, } @@ -1042,6 +1043,7 @@ pub mod test { tab_detail: Default::default(), workspace_id: Default::default(), focus_handle: cx.focus_handle(), + serialize: None, } } @@ -1077,6 +1079,14 @@ pub mod test { self } + pub fn with_serialize( + mut self, + serialize: impl Fn() -> Option>> + 'static, + ) -> Self { + self.serialize = Some(Box::new(serialize)); + self + } + pub fn set_state(&mut self, state: String, cx: &mut ViewContext) { self.push_to_nav_history(cx); self.state = state; @@ -1185,6 +1195,7 @@ pub mod test { tab_detail: Default::default(), workspace_id: self.workspace_id, focus_handle: cx.focus_handle(), + serialize: None, })) } @@ -1268,7 +1279,13 @@ pub mod test { _closing: bool, _cx: &mut ViewContext, ) -> Option>> { - None + if let Some(serialize) = self.serialize.take() { + let result = serialize(); + self.serialize = Some(serialize); + result + } else { + None + } } fn should_serialize(&self, _event: &Self::Event) -> bool { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 4fa3d65ec7d2fd..0aec91a622961b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4012,7 +4012,9 @@ impl Workspace { acc }); - for item in unique_items.values() { + // We use into_iter() here so that the references to the items are moved into + // the tasks and not kept alive while we're sleeping. + for (_, item) in unique_items.into_iter() { if let Ok(Some(task)) = this.update(cx, |workspace, cx| item.serialize(workspace, false, cx)) { @@ -4146,10 +4148,13 @@ impl Workspace { futures::future::join_all(clean_up_tasks).await; - // Serialize ourself to make sure our timestamps and any pane / item changes are replicated workspace .update(&mut cx, |workspace, cx| { + // Serialize ourself to make sure our timestamps and any pane / item changes are replicated workspace.serialize_workspace_internal(cx).detach(); + + // Ensure that we mark the window as edited if we did load dirty items + workspace.update_window_edited(cx); }) .ok(); @@ -5738,6 +5743,41 @@ mod tests { assert!(!task.await.unwrap()); } + #[gpui::test] + async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) { + init_test(cx); + + // Register TestItem as a serializable item + cx.update(|cx| { + register_serializable_item::(cx); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/root", json!({ "one": "" })).await; + + let project = Project::test(fs, ["root".as_ref()], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + + // When there are dirty untitled items, but they can serialize, then there is no prompt. + let item1 = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_serialize(|| Some(Task::ready(Ok(())))) + }); + let item2 = cx.new_view(|cx| { + TestItem::new(cx) + .with_dirty(true) + .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) + .with_serialize(|| Some(Task::ready(Ok(())))) + }); + workspace.update(cx, |w, cx| { + w.add_item_to_active_pane(Box::new(item1.clone()), None, cx); + w.add_item_to_active_pane(Box::new(item2.clone()), None, cx); + }); + let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); + assert!(task.await.unwrap()); + } + #[gpui::test] async fn test_close_pane_items(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f3e7a99f2e2783..c8beb5f7f74d38 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -964,13 +964,16 @@ mod tests { use editor::{display_map::DisplayRow, scroll::Autoscroll, DisplayPoint, Editor}; use gpui::{ actions, Action, AnyWindowHandle, AppContext, AssetSource, BorrowAppContext, Entity, - SemanticVersion, TestAppContext, VisualTestContext, WindowHandle, + SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, }; use language::{LanguageMatcher, LanguageRegistry}; - use project::{Project, ProjectPath, WorktreeSettings}; + use project::{project_settings::ProjectSettings, Project, ProjectPath, WorktreeSettings}; use serde_json::json; use settings::{handle_settings_file_changes, watch_config_file, SettingsStore}; - use std::path::{Path, PathBuf}; + use std::{ + path::{Path, PathBuf}, + time::Duration, + }; use task::{RevealStrategy, SpawnInTerminal}; use theme::{ThemeRegistry, ThemeSettings}; use workspace::{ @@ -1253,9 +1256,18 @@ mod tests { } #[gpui::test] - async fn test_window_edit_state(cx: &mut TestAppContext) { + async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) { let executor = cx.executor(); let app_state = init_test(cx); + + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::(cx, |settings| { + settings.session.restore_unsaved_buffers = false + }); + }); + }); + app_state .fs .as_fake() @@ -1363,6 +1375,7 @@ mod tests { editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); }) .unwrap(); + executor.run_until_parked(); assert!(window_is_edited(window, cx)); // Ensure closing the window via the mouse gets preempted due to the @@ -1377,6 +1390,105 @@ mod tests { assert_eq!(cx.update(|cx| cx.windows().len()), 0); } + #[gpui::test] + async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree("/root", json!({"a": "hey"})) + .await; + + cx.update(|cx| { + open_paths( + &[PathBuf::from("/root/a")], + app_state.clone(), + workspace::OpenOptions::default(), + cx, + ) + }) + .await + .unwrap(); + + assert_eq!(cx.update(|cx| cx.windows().len()), 1); + + // When opening the workspace, the window is not in a edited state. + let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + + let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { + cx.update(|cx| window.read(cx).unwrap().is_edited()) + }; + + let editor = window + .read_with(cx, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }) + .unwrap(); + + assert!(!window_is_edited(window, cx)); + + // Editing a buffer marks the window as edited. + window + .update(cx, |_, cx| { + editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); + }) + .unwrap(); + + assert!(window_is_edited(window, cx)); + cx.run_until_parked(); + + // Advance the clock to make sure the workspace is serialized + cx.executor().advance_clock(Duration::from_secs(1)); + + // When closing the window, no prompt shows up and the window is closed. + // buffer having unsaved changes. + assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close()); + cx.run_until_parked(); + assert_eq!(cx.update(|cx| cx.windows().len()), 0); + + // When we now reopen the window, the edited state and the edited buffer are back + cx.update(|cx| { + open_paths( + &[PathBuf::from("/root/a")], + app_state.clone(), + workspace::OpenOptions::default(), + cx, + ) + }) + .await + .unwrap(); + + assert_eq!(cx.update(|cx| cx.windows().len()), 1); + assert!(cx.update(|cx| cx.active_window().is_some())); + + // // Run to make sure all the deserializing etc. has run + // cx.run_until_parked(); + + // When opening the workspace, the window is not in a edited state. + let window = cx.update(|cx| cx.active_window().unwrap().downcast::().unwrap()); + assert!(window_is_edited(window, cx)); + + window + .update(cx, |workspace, cx| { + let editor = workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap(); + editor.update(cx, |editor, cx| { + assert_eq!(editor.text(cx), "EDIThey"); + assert!(editor.is_dirty(cx)); + }); + + editor + }) + .unwrap(); + } + #[gpui::test] async fn test_new_empty_workspace(cx: &mut TestAppContext) { let app_state = init_test(cx); @@ -2256,6 +2368,8 @@ mod tests { assert!(workspace.active_item(cx).is_none()); }) .unwrap(); + + cx.run_until_parked(); editor_1.assert_released(); editor_2.assert_released(); buffer.assert_released(); From 56a08340ddbfc779099df201a03d213c4b1398b9 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 17 Jul 2024 15:23:06 +0200 Subject: [PATCH 36/38] More test fixing --- crates/zed/src/zed.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c8beb5f7f74d38..2380dcf6267cb6 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1347,6 +1347,9 @@ mod tests { close.await.unwrap(); assert!(!window_is_edited(window, cx)); + // Advance the clock to ensure that the item has been serialized and dropped from the queue + cx.executor().advance_clock(Duration::from_secs(1)); + // Opening the buffer again doesn't impact the window's edited state. cx.update(|cx| { open_paths( @@ -1358,6 +1361,22 @@ mod tests { }) .await .unwrap(); + executor.run_until_parked(); + + window + .update(cx, |workspace, cx| { + let editor = workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap(); + + editor.update(cx, |editor, cx| { + assert_eq!(editor.text(cx), "hey"); + }); + }) + .unwrap(); + let editor = window .read_with(cx, |workspace, cx| { workspace @@ -1465,9 +1484,6 @@ mod tests { assert_eq!(cx.update(|cx| cx.windows().len()), 1); assert!(cx.update(|cx| cx.active_window().is_some())); - // // Run to make sure all the deserializing etc. has run - // cx.run_until_parked(); - // When opening the workspace, the window is not in a edited state. let window = cx.update(|cx| cx.active_window().unwrap().downcast::().unwrap()); assert!(window_is_edited(window, cx)); From 01def468a4e2af03af8f9a5b111b2dff93228032 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 17 Jul 2024 15:47:58 +0200 Subject: [PATCH 37/38] Remove now unused dependency --- Cargo.lock | 1 - crates/image_viewer/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d2d9b457cf8e5..d0fc48dd65f0e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5493,7 +5493,6 @@ dependencies = [ "gpui", "project", "ui", - "util", "workspace", ] diff --git a/crates/image_viewer/Cargo.toml b/crates/image_viewer/Cargo.toml index 7b9e0d9db34d85..70fe1426e2d2d5 100644 --- a/crates/image_viewer/Cargo.toml +++ b/crates/image_viewer/Cargo.toml @@ -17,6 +17,5 @@ anyhow.workspace = true db.workspace = true gpui.workspace = true ui.workspace = true -util.workspace = true workspace.workspace = true project.workspace = true From ac1e4986a9dcc93ce3415788e4d2d5204c3aec73 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 17 Jul 2024 16:21:52 +0200 Subject: [PATCH 38/38] Simplify persistence code a little bit --- crates/editor/src/items.rs | 12 ++++--- crates/editor/src/persistence.rs | 60 +++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7f11be4f968822..5f545f7a36c0b9 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1038,14 +1038,16 @@ impl SerializableItem for Editor { } if serialize_dirty_buffers { - if is_dirty { + let (contents, language) = if is_dirty { let contents = snapshot.text(); let language = snapshot.language().map(|lang| lang.name().to_string()); - DB.save_contents(item_id, workspace_id, contents, language) - .await?; + (Some(contents), language) } else { - DB.delete_contents(item_id, workspace_id).await?; - } + (None, None) + }; + + DB.save_contents(item_id, workspace_id, contents, language) + .await?; } anyhow::Ok(()) diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index 00997e5e93ddb9..ade605e99ebede 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -37,7 +37,7 @@ define_connection!( ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0; ), sql! ( - // Since sqlite3 doesn't support ALTER TABLE, we create a new + // Since sqlite3 doesn't support ALTER COLUMN, we create a new // table, move the data over, drop the old table, rename new table. CREATE TABLE new_editors_tmp ( item_id INTEGER NOT NULL, @@ -86,7 +86,7 @@ impl EditorDb { } query! { - pub async fn save_contents(item_id: ItemId, workspace: WorkspaceId, contents: String, language: Option) -> Result<()> { + pub async fn save_contents(item_id: ItemId, workspace: WorkspaceId, contents: Option, language: Option) -> Result<()> { INSERT INTO editors (item_id, workspace_id, contents, language) VALUES @@ -99,17 +99,6 @@ impl EditorDb { } } - query! { - pub async fn delete_contents(item_id: ItemId, workspace: WorkspaceId) -> Result<()> { - UPDATE editors - SET - contents = NULL, - language = NULL - WHERE item_id = ?1 - AND workspace_id = ?2 - } - } - // Returns the scroll top row, and offset query! { pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { @@ -162,3 +151,48 @@ impl EditorDb { .await } } + +#[cfg(test)] +mod tests { + use super::*; + use gpui; + + #[gpui::test] + async fn test_saving_content() { + env_logger::try_init().ok(); + + let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); + + // Sanity check: make sure there is no row in the `editors` table + assert_eq!(DB.get_path_and_contents(1234, workspace_id).unwrap(), None); + + // Save content/language + DB.save_contents( + 1234, + workspace_id, + Some("testing".into()), + Some("Go".into()), + ) + .await + .unwrap(); + + // Check that it can be read from DB + let path_and_contents = DB.get_path_and_contents(1234, workspace_id).unwrap(); + let (path, contents, language) = path_and_contents.unwrap(); + assert!(path.is_none()); + assert_eq!(contents, Some("testing".to_owned())); + assert_eq!(language, Some("Go".to_owned())); + + // Update it with NULL + DB.save_contents(1234, workspace_id, None, None) + .await + .unwrap(); + + // Check that it worked + let path_and_contents = DB.get_path_and_contents(1234, workspace_id).unwrap(); + let (path, contents, language) = path_and_contents.unwrap(); + assert!(path.is_none()); + assert!(contents.is_none()); + assert!(language.is_none()); + } +}