Repository navigation
text: a crash checkpoint no longer splits a run of typing - #601
Merged
Merged
Conversation
The crash checkpoint (`Checkpoint`) captures every dirty document every few seconds of typing, and for its unsaved bytes asks the owner for the bytes a save would write (`documentBytes`). The text plugin's `savedBytes` closed the undo group there, as if it were the save, so a run of typing was cut into a new undo step each time a checkpoint landed in it. It is also what made the tour test's step check flaky: on a slow runner a checkpoint fell between the two keystrokes it types, and the second came as `.added`, not `.joined`. The save's undo stop now comes when its write lands (`written`), which only a real save reports, and only while the written state is still the top step. The test takes a checkpoint between its keystrokes itself, so it holds whatever a frame takes, and checks that a save still stops the run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 of 8 tasks
foxnne
marked this pull request as ready for review
October 19, 2026 01:33
foxnne
added a commit
that referenced
this pull request
Oct 19, 2026
#606) Stacked on #601 and based on its branch, so the diff is this change alone; GitHub retargets it to `main` when #601 merges. It takes up the follow-ups that PR lists. ## What changes **For plugin authors:** a new optional `Plugin.VTable.documentBytes(state, doc, path, allocator) → []u8`. It returns the bytes a save to `path` would write, for a copy that is **not** a save, with none of a save's bookkeeping or side effects: no format on save, no edit remembered for `documentWritten`, no undo stop. fizzy takes these bytes: - for the crash checkpoint (`KeptDocuments.capture` from `Checkpoint`, every few seconds of typing) - for a restart or a plugin reload carrying unsaved work - for a tape's file (`Tracks.fileAt`) - for a `fizzy.document.changed` listener that asks for the unsaved text An owner without the hook keeps working as before: `Plugin.documentBytes` falls back to `documentSnapshot` + `encodeSnapshot`, bookkeeping included. Saves keep the snapshot: `DocumentIo.save`, and the web download (`Editor.downloadDocument`), which now calls the new `Plugin.saveBytes` (both halves of a save at once, followed by `documentWritten`). **Why:** `documentSnapshot` is "the storage-agnostic half of saving". It runs at a save's start, so it formats on save and records which edit the bytes stand for. Every non-save copy did the same. In the text plugin that meant: 1. With `format_on_save` on (off by default), each checkpoint ran the formatter on a dirty document while the person was typing in it. 2. A checkpoint taken while a save's write was in flight overwrote `serialized_op_id`. If an edit came in between, the write landed and `documentWritten` marked the document clean at an edit newer than the file on disk. **Implemented in:** - **text**: the hook returns the text as it stands. - **archive**: it had the same in-flight bug through `serialized_generation`, so it gets the hook too (`doc.pack`, with no generation recorded). `Document.savedBytes`'s comment now says only a save takes it. `docs/PLUGINS.md` names the hook, and its storage-agnostic section now names the snapshot pair, which is what a save uses. **Tests** (`tests/integration.zig`, the tour test's "A document's steps" block, after #601's save check): - **Format on save on, with a stand-in formatter:** a `KeptDocuments.capture` keeps the text as typed and leaves the document unformatted. A real save (`saveBytes`) still formats, which shows the formatter was live. - **A checkpoint while a save is in flight:** take a save's snapshot at "a", type "b", take a checkpoint, then land the write (`documentWritten`). The document is still dirty. Each check fails without the text plugin's hook: the first with `formatted` where `as typed` was expected, the second with the document clean. ## SDK impact - [ ] None - [ ] Core-only or additive: reaches plugins at the next SDK release - [x] Fingerprint moved: recorded in `sdk/src/version.zig`, `sdk_version` untouched, PR labelled `sdk` - [ ] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the last `sdk-v*` tag A new optional vtable field, null by default. An existing plugin builds unchanged and gets the fallback, which behaves as before. **pixi** (`fizzyedit/pixi`) implements `documentSnapshot` and has both problems in its own form: - `File.saveSnapshot` sets `editor.saving_bookmark = history.bookmark`, so a checkpoint during a save moves the bookmark that `written` cleans to. - It also calls `finishReveal()`, so a checkpoint ends a reveal in progress. pixi would need a `documentBytes` that builds the same `Snapshot` content without those two lines, encodes it in place (`File.encode`), and destroys it. The `.pixi` zip encode then runs on the UI thread at each checkpoint, as it already does through the fallback today. ## Verified - [x] macOS: `zig build test-integration` (80/80 steps; the two new checks each fail with the text hook removed), `zig build`, `zig build test` (113/113), `check-web` (42/42), `test-sdk-version` (35/35). `scripts/check-fmt.sh` flags only `backend/src/platform/win32_titlebar.zig`, which this branch's base (#601's) predates `main`'s fix for (#597); untouched here. - [ ] Windows: - [ ] Linux: CI - [ ] Web: `check-web` builds; the download path was not run ## Follow-ups - pixi's `documentBytes` is written and builds against this branch's SDK, with pixi's `zig build test` passing. It's a local change for the pixi repo, pushed at the repin that brings this SDK, since SDK 0.2.30 has no such field. It splits `saveSnapshot` into the save's bookkeeping around a `takeSnapshot` that changes nothing. A drawing still being revealed is completed in the copy, as `captureState` does; a flat image is the one case that still finishes the reveal, because its composite is read back from the GPU. - The checkpoint still encodes on the UI thread for every owner. If a large document shows up in a frame, the seam is to snapshot through the hook and encode on `DocumentIo`'s worker. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the flaky step check in the integration tour test ("Integration tests (Linux)" on #583, 2026-10-18:
.addedwhere.joinedwas expected).What changes
Typing in a document with unsaved changes stays one undo step, however long you type. Before, the crash checkpoint split it every few seconds.
The cause wasn't a join window. The text plugin joins keystrokes by run of operation (VSCode's rule), with no timer. What cut the run was the crash checkpoint (
src/editor/Checkpoint.zig). It runs on the wall clock, at least 3 s after the last checkpoint and once there has been input, and captures every dirty document throughKeptDocuments.capture. For the unsaved bytes, that callsdocumentBytes, which goesdocumentSnapshot→Document.savedBytes, andsavedBytescalledhistory.closeGroup()as though the save had happened. On a slow runner, a checkpoint landed between the test's two keystrokes. Reproduced locally with a 4 s sleep between them (gives the CI failure exactly); a 1.5 s sleep doesn't, because it stays inside the checkpoint's spacing.The fix:
savedBytesstill remembers which edit the bytes stand for, but no longer closes the group.written(thedocumentWrittenhook, which only a real save reports:DocumentIoand the web download) makes the undo stop, while the written state is still the top step. The synchronoussavepath is unchanged.One behavior moves: a keystroke typed while a save's write is in flight (milliseconds, off-thread) now joins the step the save captured. The document stays dirty, which is correct because that step is newer than the file, but undo goes past the saved state instead of stopping on it. Before, the snapshot closed the group up front.
The test doesn't touch the clock. It takes a checkpoint's capture itself (
KeptDocuments.capture) between "a" and "b" and still expects.joined, so it covers the bug however long a frame takes. A new check covers the save side: type, save through the host's path (documentBytes+documentWritten), type, undo. That should give back the saved text, clean. Each check fails when its half of the fix is removed (verified by revertingsavedBytes, and separately by dropping thecloseGroupinwritten).SDK impact
sdk/src/version.zig,sdk_versionuntouched, PR labelledsdksdk_version, lists thesdkPRs since the lastsdk-v*tagVerified
zig build test-integration(the tour test with the new checks; it fails with either half of the fix reverted), pluszig build,test,check-web,test-sdk-version,scripts/check-fmt.shcheck-webbuilds; not runFollow-ups
The root of this is the host using the save's snapshot for things that aren't saves: the crash checkpoint, a restart or plugin reload (
KeptDocuments), and Tracks' share export. This PR fixes the undo split inside the text plugin. Two problems stay, and want a host-side seam (bytes as a save would write them, without a save's bookkeeping):documentSnapshotformats before taking the bytes).serialized_op_id. If an edit came between the two, the save lands and marks the document clean at an edit newer than what was written.🤖 Generated with Claude Code