Restore unsaved buffers on restart (#13546)
This adds the ability for Zed to restore unsaved buffers on restart. The user is no longer prompted to save/discard/cancel when trying to close a Zed window with dirty buffers in it. Instead those dirty buffers are stored and restored on restart. It does this by saving the contents of dirty buffers to the internal SQLite database in which Zed stores other data too. On restart, if there are dirty buffers in the database, they are restored. On certain events (buffer changed, file saved, ...) Zed will serialize these buffers, throttled to a 100ms, so that we don't overload the machine by saving on every keystroke. When Zed quits, it waits until all the buffers are serialized. ### Current limitations - It does not persist undo-history (right now we don't persist/restore undo-history regardless of dirty buffers or not) - It does not restore buffers in windows without projects/worktrees. Example: if you open a new window with `cmd-shift-n` and type something in a buffer, this will _not_ be stored and you will be asked whether to save/discard on quit. In the future, we want to fix this by also restoring windows without projects/worktrees. ### Demo https://github.com/user-attachments/assets/45c63237-8848-471f-8575-ac05496bba19 ### Related tickets I'm unsure about closing them, without also fixing the 2nd limitation: restoring of worktree-less windows. So let's wait until that. - https://github.com/zed-industries/zed/issues/4985 - https://github.com/zed-industries/zed/issues/4683 ### Note on performance - Serializing editing buffer (asynchronously on background thread) with 500k lines takes ~200ms on M3 Max. That's an extreme case and that performance seems acceptable. Release Notes: - Added automatic restoring of unsaved buffers. Zed can now be closed even if there are unsaved changes in buffers. One current limitation is that this only works when having projects open, not single files or empty windows with unsaved buffers. The feature can be turned off by setting `{"session": {"restore_unsaved_buffers": false}}`. --------- Co-authored-by: Bennet <bennet@zed.dev> Co-authored-by: Antonio <antonio@zed.dev>
This commit is contained in:
parent
8e9e94de22
commit
9241b11e1f
20 changed files with 1111 additions and 340 deletions
|
@ -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
|
||||
|
|
|
@ -9,9 +9,8 @@ use ui::prelude::*;
|
|||
|
||||
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,
|
||||
};
|
||||
|
||||
|
@ -90,49 +89,6 @@ impl Item for ImageView {
|
|||
.into_any_element()
|
||||
}
|
||||
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
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();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn serialized_item_kind() -> Option<&'static str> {
|
||||
Some(IMAGE_VIEWER_KIND)
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
_project: Model<Project>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
workspace_id: WorkspaceId,
|
||||
item_id: ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<anyhow::Result<View<Self>>> {
|
||||
cx.spawn(|_pane, mut cx| async move {
|
||||
let image_path = IMAGE_VIEWER
|
||||
.get_image_path(item_id, workspace_id)?
|
||||
.ok_or_else(|| anyhow::anyhow!("No image path found"))?;
|
||||
|
||||
cx.new_view(|cx| ImageView {
|
||||
path: image_path,
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
|
@ -148,6 +104,62 @@ impl Item for ImageView {
|
|||
}
|
||||
}
|
||||
|
||||
impl SerializableItem for ImageView {
|
||||
fn serialized_item_kind() -> &'static str {
|
||||
IMAGE_VIEWER_KIND
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
_project: Model<Project>,
|
||||
_workspace: WeakView<Workspace>,
|
||||
workspace_id: WorkspaceId,
|
||||
item_id: ItemId,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
) -> Task<gpui::Result<View<Self>>> {
|
||||
cx.spawn(|_pane, mut cx| async move {
|
||||
let image_path = IMAGE_VIEWER
|
||||
.get_image_path(item_id, workspace_id)?
|
||||
.ok_or_else(|| anyhow::anyhow!("No image path found"))?;
|
||||
|
||||
cx.new_view(|cx| ImageView {
|
||||
path: image_path,
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn cleanup(
|
||||
workspace_id: WorkspaceId,
|
||||
alive_items: Vec<ItemId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<gpui::Result<()>> {
|
||||
cx.spawn(|_| IMAGE_VIEWER.delete_unloaded_items(workspace_id, alive_items))
|
||||
}
|
||||
|
||||
fn serialize(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
item_id: ItemId,
|
||||
_closing: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Task<gpui::Result<()>>> {
|
||||
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
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn should_serialize(&self, _event: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for ImageView {}
|
||||
impl FocusableView for ImageView {
|
||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||
|
@ -242,13 +254,14 @@ impl ProjectItem for ImageView {
|
|||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
workspace::register_project_item::<ImageView>(cx);
|
||||
workspace::register_deserializable_item::<ImageView>(cx)
|
||||
workspace::register_serializable_item::<ImageView>(cx)
|
||||
}
|
||||
|
||||
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! {
|
||||
|
@ -298,5 +311,29 @@ mod persistence {
|
|||
WHERE item_id = ? AND workspace_id = ?
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_unloaded_items(
|
||||
&self,
|
||||
workspace: WorkspaceId,
|
||||
alive_items: Vec<ItemId>,
|
||||
) -> Result<()> {
|
||||
let placeholders = alive_items
|
||||
.iter()
|
||||
.map(|_| "?")
|
||||
.collect::<Vec<&str>>()
|
||||
.join(", ");
|
||||
|
||||
let query = format!("DELETE FROM image_viewers WHERE workspace_id = ? AND item_id NOT IN ({placeholders})");
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue