Restore editor state on reopen (#27672)

Closes https://github.com/zed-industries/zed/issues/11626
Part of https://github.com/zed-industries/zed/issues/12853

`"restore_on_file_reopen": true` in workspace settings can now be used
to enable and disable editor data between file reopens in the same pane:


https://github.com/user-attachments/assets/8d938ee1-d854-42a8-bbc3-2a4e4d7d5933

The settings are generic and panes' data store can be extended for
further entities, beyond editors.

---------------
Impl details: 

Currently, the project entry IDs seem to be stable across file reopens,
unlike BufferIds, so those were used.
Originally, the DB data was considered over in-memory one as editors
serialize their state anyway, but managing and exposing PaneIds out of
the DB is quite tedious and joining the DB data otherwise is not
possible.


Release Notes:

- Started to restore editor state on reopen
This commit is contained in:
Kirill Bulatov 2025-03-29 00:04:16 +02:00 committed by GitHub
parent bbd1e628f0
commit e11e7df724
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 611 additions and 48 deletions

View file

@ -1597,6 +1597,17 @@ impl Editor {
}
this.tasks_update_task = Some(this.refresh_runnables(window, cx));
this._subscriptions.extend(project_subscriptions);
this._subscriptions
.push(cx.subscribe_self(|editor, e: &EditorEvent, cx| {
if let EditorEvent::SelectionsChanged { local } = e {
if *local {
let new_anchor = editor.scroll_manager.anchor();
editor.update_restoration_data(cx, move |data| {
data.scroll_anchor = new_anchor;
});
}
}
}));
this.end_selection(window, cx);
this.scroll_manager.show_scrollbars(window, cx);
@ -2317,18 +2328,24 @@ impl Editor {
if selections.len() == 1 {
cx.emit(SearchEvent::ActiveMatchChanged)
}
if local
&& self.is_singleton(cx)
&& WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None
{
if let Some(workspace_id) = self.workspace.as_ref().and_then(|workspace| workspace.1) {
let background_executor = cx.background_executor().clone();
let editor_id = cx.entity().entity_id().as_u64() as ItemId;
let snapshot = self.buffer().read(cx).snapshot(cx);
let selections = selections.clone();
self.serialize_selections = cx.background_spawn(async move {
if local && self.is_singleton(cx) {
let inmemory_selections = selections.iter().map(|s| s.range()).collect();
self.update_restoration_data(cx, |data| {
data.selections = inmemory_selections;
});
if WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None
{
if let Some(workspace_id) =
self.workspace.as_ref().and_then(|workspace| workspace.1)
{
let snapshot = self.buffer().read(cx).snapshot(cx);
let selections = selections.clone();
let background_executor = cx.background_executor().clone();
let editor_id = cx.entity().entity_id().as_u64() as ItemId;
self.serialize_selections = cx.background_spawn(async move {
background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
let selections = selections
let db_selections = selections
.iter()
.map(|selection| {
(
@ -2338,11 +2355,12 @@ impl Editor {
})
.collect();
DB.save_editor_selections(editor_id, workspace_id, selections)
DB.save_editor_selections(editor_id, workspace_id, db_selections)
.await
.with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}"))
.log_err();
});
}
}
}
@ -2356,13 +2374,24 @@ impl Editor {
return;
}
let snapshot = self.buffer().read(cx).snapshot(cx);
let inmemory_folds = self.display_map.update(cx, |display_map, cx| {
display_map
.snapshot(cx)
.folds_in_range(0..snapshot.len())
.map(|fold| fold.range.deref().clone())
.collect()
});
self.update_restoration_data(cx, |data| {
data.folds = inmemory_folds;
});
let Some(workspace_id) = self.workspace.as_ref().and_then(|workspace| workspace.1) else {
return;
};
let background_executor = cx.background_executor().clone();
let editor_id = cx.entity().entity_id().as_u64() as ItemId;
let snapshot = self.buffer().read(cx).snapshot(cx);
let folds = self.display_map.update(cx, |display_map, cx| {
let db_folds = self.display_map.update(cx, |display_map, cx| {
display_map
.snapshot(cx)
.folds_in_range(0..snapshot.len())
@ -2376,7 +2405,7 @@ impl Editor {
});
self.serialize_folds = cx.background_spawn(async move {
background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
DB.save_editor_folds(editor_id, workspace_id, folds)
DB.save_editor_folds(editor_id, workspace_id, db_folds)
.await
.with_context(|| format!("persisting editor folds for editor {editor_id}, workspace {workspace_id:?}"))
.log_err();
@ -17454,19 +17483,6 @@ impl Editor {
{
let buffer_snapshot = OnceCell::new();
if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() {
if !selections.is_empty() {
let snapshot =
buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx));
self.change_selections(None, window, cx, |s| {
s.select_ranges(selections.into_iter().map(|(start, end)| {
snapshot.clip_offset(start, Bias::Left)
..snapshot.clip_offset(end, Bias::Right)
}));
});
}
};
if let Some(folds) = DB.get_editor_folds(item_id, workspace_id).log_err() {
if !folds.is_empty() {
let snapshot =
@ -17485,6 +17501,19 @@ impl Editor {
);
}
}
if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() {
if !selections.is_empty() {
let snapshot =
buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx));
self.change_selections(None, window, cx, |s| {
s.select_ranges(selections.into_iter().map(|(start, end)| {
snapshot.clip_offset(start, Bias::Left)
..snapshot.clip_offset(end, Bias::Right)
}));
});
}
};
}
self.read_scroll_position_from_db(item_id, workspace_id, window, cx);