Properly store editor restoration data (#28296)

We cannot compare versions and anchors between different `Buffer`s with
different `BufferId`s.

Release Notes:

- Fixed Zed panicking on editor reopen

Co-authored-by: Conrad Irwin <conrad@zed.dev>
This commit is contained in:
Kirill Bulatov 2025-04-07 19:25:43 -06:00 committed by GitHub
parent bfe08e449f
commit 1264e7a200
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 111 additions and 80 deletions

View file

@ -6,7 +6,6 @@ use crate::{
scroll::ScrollAnchor,
};
use anyhow::{Context as _, Result, anyhow};
use clock::Global;
use collections::{HashMap, HashSet};
use file_icons::FileIcons;
use futures::future::try_join_all;
@ -16,12 +15,12 @@ use gpui::{
ParentElement, Pixels, SharedString, Styled, Task, WeakEntity, Window, point,
};
use language::{
Bias, Buffer, CharKind, DiskState, Point, SelectionGoal,
Bias, Buffer, BufferRow, CharKind, DiskState, LocalFile, Point, SelectionGoal,
proto::serialize_anchor as serialize_text_anchor,
};
use lsp::DiagnosticSeverity;
use project::{
Project, ProjectEntryId, ProjectItem as _, ProjectPath, lsp_store::FormatTrigger,
Project, ProjectItem as _, ProjectPath, lsp_store::FormatTrigger,
project_settings::ProjectSettings, search::SearchQuery,
};
use rpc::proto::{self, PeerId, update_view};
@ -30,13 +29,12 @@ use std::{
any::TypeId,
borrow::Cow,
cmp::{self, Ordering},
collections::hash_map,
iter,
ops::Range,
path::Path,
path::{Path, PathBuf},
sync::Arc,
};
use text::{BufferId, Selection};
use text::{BufferId, BufferSnapshot, Selection};
use theme::{Theme, ThemeSettings};
use ui::{IconDecorationKind, prelude::*};
use util::{ResultExt, TryFutureExt, paths::PathExt};
@ -1243,26 +1241,14 @@ impl SerializableItem for Editor {
#[derive(Debug, Default)]
struct EditorRestorationData {
entries: HashMap<ProjectEntryId, RestorationData>,
entries: HashMap<PathBuf, RestorationData>,
}
#[derive(Debug)]
#[derive(Default, Debug)]
pub struct RestorationData {
pub scroll_anchor: ScrollAnchor,
pub folds: Vec<Range<Anchor>>,
pub selections: Vec<Range<Anchor>>,
pub buffer_version: Global,
}
impl Default for RestorationData {
fn default() -> Self {
Self {
scroll_anchor: ScrollAnchor::new(),
folds: Vec::new(),
selections: Vec::new(),
buffer_version: Global::default(),
}
}
pub scroll_position: (BufferRow, gpui::Point<f32>),
pub folds: Vec<Range<Point>>,
pub selections: Vec<Range<Point>>,
}
impl ProjectItem for Editor {
@ -1280,21 +1266,37 @@ impl ProjectItem for Editor {
cx: &mut Context<Self>,
) -> Self {
let mut editor = Self::for_buffer(buffer.clone(), Some(project), window, cx);
if WorkspaceSettings::get(None, cx).restore_on_file_reopen {
if let Some(restoration_data) = Self::project_item_kind()
.and_then(|kind| pane.project_item_restoration_data.get(&kind))
.and_then(|data| data.downcast_ref::<EditorRestorationData>())
.and_then(|data| data.entries.get(&buffer.read(cx).entry_id(cx)?))
.filter(|data| !buffer.read(cx).version.changed_since(&data.buffer_version))
{
editor.fold_ranges(restoration_data.folds.clone(), false, window, cx);
if !restoration_data.selections.is_empty() {
editor.change_selections(None, window, cx, |s| {
s.select_ranges(restoration_data.selections.clone());
});
if let Some((excerpt_id, buffer_id, snapshot)) =
editor.buffer().read(cx).snapshot(cx).as_singleton()
{
if WorkspaceSettings::get(None, cx).restore_on_file_reopen {
if let Some(restoration_data) = Self::project_item_kind()
.and_then(|kind| pane.project_item_restoration_data.get(&kind))
.and_then(|data| data.downcast_ref::<EditorRestorationData>())
.and_then(|data| {
let file = project::File::from_dyn(buffer.read(cx).file())?;
data.entries.get(&file.abs_path(cx))
})
{
editor.fold_ranges(
clip_ranges(&restoration_data.folds, &snapshot),
false,
window,
cx,
);
if !restoration_data.selections.is_empty() {
editor.change_selections(None, window, cx, |s| {
s.select_ranges(clip_ranges(&restoration_data.selections, &snapshot));
});
}
let (top_row, offset) = restoration_data.scroll_position;
let anchor = Anchor::in_buffer(
*excerpt_id,
buffer_id,
snapshot.anchor_before(Point::new(top_row, 0)),
);
editor.set_scroll_anchor(ScrollAnchor { anchor, offset }, window, cx);
}
editor.set_scroll_anchor(restoration_data.scroll_anchor, window, cx);
}
}
@ -1302,6 +1304,19 @@ impl ProjectItem for Editor {
}
}
fn clip_ranges<'a>(
original: impl IntoIterator<Item = &'a Range<Point>> + 'a,
snapshot: &'a BufferSnapshot,
) -> Vec<Range<Point>> {
original
.into_iter()
.map(|range| {
snapshot.clip_point(range.start, Bias::Left)
..snapshot.clip_point(range.end, Bias::Right)
})
.collect()
}
impl EventEmitter<SearchEvent> for Editor {}
impl Editor {
@ -1320,8 +1335,7 @@ impl Editor {
let kind = Editor::project_item_kind()?;
let pane = editor.workspace()?.read(cx).pane_for(&cx.entity())?;
let buffer = editor.buffer().read(cx).as_singleton()?;
let entry_id = buffer.read(cx).entry_id(cx)?;
let buffer_version = buffer.read(cx).version();
let file_abs_path = project::File::from_dyn(buffer.read(cx).file())?.abs_path(cx);
pane.update(cx, |pane, _| {
let data = pane
.project_item_restoration_data
@ -1336,17 +1350,8 @@ impl Editor {
}
};
let data = match data.entries.entry(entry_id) {
hash_map::Entry::Occupied(o) => {
if buffer_version.changed_since(&o.get().buffer_version) {
return None;
}
o.into_mut()
}
hash_map::Entry::Vacant(v) => v.insert(RestorationData::default()),
};
let data = data.entries.entry(file_abs_path).or_default();
write(data);
data.buffer_version = buffer_version;
Some(())
})
});