Do not serialize buffers containing bundled files (#38102)

Fix bundled file persistence by introducing SerializationMode enum

Closes: #38094

What's the issue?

Opening bundled files like Default Key Bindings
(zed://settings/keymap-default.json) was causing SQLite foreign key
constraint errors. The editor was trying to save state for these
read-only assets just like regular files, but since bundled files don't
have database entries, the foreign key constraint would fail.

The fix

Replaced the boolean serialize_dirty_buffers flag with a type-safe
SerializationMode enum:

```rust
pub enum SerializationMode {
    Enabled,   // Regular files persist across sessions
    Disabled,  // Bundled files don't persist
}
```

This prevents serialization at the source: workspace_id() returns None
for disabled editors, serialize() bails early, and should_serialize()
returns false. When opening bundled files, we set the mode to Disabled
from the start, so they're treated as transient views that never
interact with the persistence layer.

Changes

- editor.rs: Added SerializationMode enum and updated serialization
methods to respect it
- items.rs: Guarded should_serialize() to prevent disabled editors from
being serialized
- zed.rs: Set SerializationMode::Disabled in open_bundled_file()

Result

Bundled files open cleanly without SQLite errors and don't persist
across workspace reloads (expected behavior). Regular file persistence
remains unaffected.

Release Notes: Fixed SQLite foreign key constraint errors when opening
bundled files like Default Key Bindings.

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
This commit is contained in:
ʟᴜɴᴇx
2025-11-06 13:30:03 +00:00
committed by GitHub
co-authored by MrSubidubi
parent a7163623e7
commit 7a37dd9433
3 changed files with 66 additions and 25 deletions
+51 -10
View File
@@ -812,6 +812,22 @@ impl MinimapVisibility {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BufferSerialization {
All,
NonDirtyBuffers,
}
impl BufferSerialization {
fn new(restore_unsaved_buffers: bool) -> Self {
if restore_unsaved_buffers {
Self::All
} else {
Self::NonDirtyBuffers
}
}
}
#[derive(Clone, Debug)]
struct RunnableTasks {
templates: Vec<(TaskSourceKind, TaskTemplate)>,
@@ -1129,7 +1145,7 @@ pub struct Editor {
show_git_blame_inline_delay_task: Option<Task<()>>,
git_blame_inline_enabled: bool,
render_diff_hunk_controls: RenderDiffHunkControlsFn,
serialize_dirty_buffers: bool,
buffer_serialization: Option<BufferSerialization>,
show_selection_menu: Option<bool>,
blame: Option<Entity<GitBlame>>,
blame_subscription: Option<Subscription>,
@@ -2205,10 +2221,13 @@ impl Editor {
git_blame_inline_enabled: full_mode
&& ProjectSettings::get_global(cx).git.inline_blame.enabled,
render_diff_hunk_controls: Arc::new(render_diff_hunk_controls),
serialize_dirty_buffers: !is_minimap
&& ProjectSettings::get_global(cx)
.session
.restore_unsaved_buffers,
buffer_serialization: is_minimap.not().then(|| {
BufferSerialization::new(
ProjectSettings::get_global(cx)
.session
.restore_unsaved_buffers,
)
}),
blame: None,
blame_subscription: None,
tasks: BTreeMap::default(),
@@ -2752,6 +2771,14 @@ impl Editor {
self.workspace.as_ref()?.0.upgrade()
}
/// Returns the workspace serialization ID if this editor should be serialized.
fn workspace_serialization_id(&self, _cx: &App) -> Option<WorkspaceId> {
self.workspace
.as_ref()
.filter(|_| self.should_serialize_buffer())
.and_then(|workspace| workspace.1)
}
pub fn title<'a>(&self, cx: &'a App) -> Cow<'a, str> {
self.buffer().read(cx).title(cx)
}
@@ -3000,6 +3027,20 @@ impl Editor {
self.auto_replace_emoji_shortcode = auto_replace;
}
pub fn set_should_serialize(&mut self, should_serialize: bool, cx: &App) {
self.buffer_serialization = should_serialize.then(|| {
BufferSerialization::new(
ProjectSettings::get_global(cx)
.session
.restore_unsaved_buffers,
)
})
}
fn should_serialize_buffer(&self) -> bool {
self.buffer_serialization.is_some()
}
pub fn toggle_edit_predictions(
&mut self,
_: &ToggleEditPrediction,
@@ -3216,8 +3257,7 @@ impl Editor {
});
if WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None
&& let Some(workspace_id) =
self.workspace.as_ref().and_then(|workspace| workspace.1)
&& let Some(workspace_id) = self.workspace_serialization_id(cx)
{
let snapshot = self.buffer().read(cx).snapshot(cx);
let selections = selections.clone();
@@ -3282,7 +3322,7 @@ impl Editor {
data.folds = inmemory_folds;
});
let Some(workspace_id) = self.workspace.as_ref().and_then(|workspace| workspace.1) else {
let Some(workspace_id) = self.workspace_serialization_id(cx) else {
return;
};
let background_executor = cx.background_executor().clone();
@@ -21179,8 +21219,9 @@ impl Editor {
}
let project_settings = ProjectSettings::get_global(cx);
self.serialize_dirty_buffers =
!self.mode.is_minimap() && project_settings.session.restore_unsaved_buffers;
self.buffer_serialization = self
.should_serialize_buffer()
.then(|| BufferSerialization::new(project_settings.session.restore_unsaved_buffers));
if self.mode.is_full() {
let show_inline_diagnostics = project_settings.diagnostics.inline.enabled;
+14 -15
View File
@@ -1,7 +1,7 @@
use crate::{
Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget,
MultiBuffer, MultiBufferSnapshot, NavigationData, ReportEditorEvent, SearchWithinRange,
SelectionEffects, ToPoint as _,
Anchor, Autoscroll, BufferSerialization, Editor, EditorEvent, EditorSettings, ExcerptId,
ExcerptRange, FormatTarget, MultiBuffer, MultiBufferSnapshot, NavigationData,
ReportEditorEvent, SearchWithinRange, SelectionEffects, ToPoint as _,
display_map::HighlightKey,
editor_settings::SeedQuerySetting,
persistence::{DB, SerializedEditor},
@@ -1256,17 +1256,15 @@ impl SerializableItem for Editor {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
if self.mode.is_minimap() {
return None;
}
let mut serialize_dirty_buffers = self.serialize_dirty_buffers;
let buffer_serialization = self.buffer_serialization?;
let project = self.project.clone()?;
if project.read(cx).visible_worktrees(cx).next().is_none() {
let serialize_dirty_buffers = match buffer_serialization {
// If we don't have a worktree, we don't serialize, because
// projects without worktrees aren't deserialized.
serialize_dirty_buffers = false;
}
BufferSerialization::All => project.read(cx).visible_worktrees(cx).next().is_some(),
BufferSerialization::NonDirtyBuffers => false,
};
if closing && !serialize_dirty_buffers {
return None;
@@ -1323,10 +1321,11 @@ impl SerializableItem for Editor {
}
fn should_serialize(&self, event: &Self::Event) -> bool {
matches!(
event,
EditorEvent::Saved | EditorEvent::DirtyChanged | EditorEvent::BufferEdited
)
self.should_serialize_buffer()
&& matches!(
event,
EditorEvent::Saved | EditorEvent::DirtyChanged | EditorEvent::BufferEdited
)
}
}
+1
View File
@@ -1922,6 +1922,7 @@ fn open_bundled_file(
let mut editor =
Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
editor.set_read_only(true);
editor.set_should_serialize(false, cx);
editor.set_breadcrumb_header(title.into());
editor
})),