Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion plugins/text/src/Document.zig
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,11 @@ pub fn save(self: *Document) !void {
/// The bytes `save` would write, for a host that does the writing itself (a mounted drive).
/// Remembers which edit those bytes stand for: the write lands later, and edits typed in the
/// meantime must still count as unsaved when it does.
///
/// Not a save in itself, so the run of typing stays open: the host takes these bytes for a crash
/// checkpoint too (`KeptDocuments`), every few seconds while someone types, and never writes them
/// over the file. The save's undo stop is made when its write lands (`written`).
pub fn savedBytes(self: *Document, allocator: std.mem.Allocator) ![]u8 {
self.history.closeGroup();
self.serialized_op_id = self.history.topOpId();
return allocator.dupe(u8, self.text.items);
}
Expand All @@ -418,6 +421,10 @@ pub fn written(self: *Document, path: []const u8) !void {
self.unsaved = false;
}
self.clean_op_id = self.serialized_op_id;
// The state written is an undo stop, as `save` makes it, while it is still the top. Typed
// while the write was in flight, a keystroke has already joined that step: the step is newer
// than the file, so the document stays dirty, and undo takes it back past the saved state.
if (self.history.topOpId() == self.serialized_op_id) self.history.closeGroup();
self.notifyContentChanged();
}

Expand Down
27 changes: 25 additions & 2 deletions tests/integration.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8651,10 +8651,16 @@ test "headless: the whole editor comes up, opens a file, plays the tour and goes

const a = try newDoc(editor, host, was.id);
rec.doc = a.id;
// Typed a keystroke at a time: one step, joined.
for ([_][]const u8{ "a", "b" }) |key| {
// Typed a keystroke at a time: one step, joined. A crash checkpoint between them (one is
// taken every few seconds of typing, by the wall clock: `Checkpoint`) is no save, so the
// run stays open; taken here, it is between them however long a frame takes.
for ([_][]const u8{ "a", "b" }, 0..) |key, i| {
try dvui.testing.writeText(key);
try dvui.testing.settle(headlessFrame);
if (i == 0) {
var kept = try fizzy.Editor.KeptDocuments.capture(editor, null);
kept.deinit();
}
}
try std.testing.expect(host.callCommand("text.insert", ".{ .text = \"X\", .line = 1 }", host.arena()) == .ok);
try editor.undo();
Expand All @@ -8680,6 +8686,23 @@ test "headless: the whole editor comes up, opens a file, plays the tour and goes
try std.testing.expectError(error.StepDoesNotApply, b.owner.vtable.applyStep.?(b.owner.state, b, rec.effects.items[3], "", 0));
try std.testing.expectEqualStrings("Yab", try textOf(host, b));

// A save is an undo stop, made when its write lands (`documentWritten`): typing after it
// is a step of its own, and undoing that step comes back to the saved, clean document.
const c = try newDoc(editor, host, b.id);
try dvui.testing.writeText("a");
try dvui.testing.settle(headlessFrame);
const bytes = (try c.owner.documentBytes(c, c.owner.documentPath(c), gpa)) orelse return error.TestUnexpectedResult;
gpa.free(bytes);
try c.owner.documentWritten(c, c.owner.documentPath(c));
try std.testing.expect(!c.owner.isDirty(c));
try dvui.testing.writeText("b");
try dvui.testing.settle(headlessFrame);
try editor.undo();
try dvui.testing.settle(headlessFrame);
try std.testing.expectEqualStrings("a", try textOf(host, c));
try std.testing.expect(!c.owner.isDirty(c));

try editor.rawCloseFileID(c.id);
try editor.rawCloseFileID(b.id);
try editor.rawCloseFileID(a.id);
editor.workbench.setActiveDocIndex(editor.app.open_files.getIndex(was.id) orelse return error.TestUnexpectedResult);
Expand Down
Loading