fix: store redo metadata along with undo metadata and restore it on redo

This commit refactors undo storage and handling significantly.

The undo/redo chaining in Buffer is much simpler and clearer.

The metadata generated by Editor now contains the pre and post change
states. The pre-state is restored on undo and the post-state is restored
on redo.

closes #348
This commit is contained in:
CJ van den Berg 2025-11-04 15:32:43 +01:00
parent 76952a7d1b
commit 5e292e75b5
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
2 changed files with 99 additions and 65 deletions

View file

@ -46,9 +46,8 @@ auto_save: bool = false,
meta: ?[]const u8 = null, meta: ?[]const u8 = null,
lsp_version: usize = 1, lsp_version: usize = 1,
undo_history: ?*UndoNode = null, undo_head: ?*UndoNode = null,
redo_history: ?*UndoNode = null, redo_head: ?*UndoNode = null,
curr_history: ?*UndoNode = null,
mtime: i64, mtime: i64,
utime: i64, utime: i64,
@ -62,14 +61,15 @@ pub const EolModeTag = @typeInfo(EolMode).@"enum".tag_type;
const UndoNode = struct { const UndoNode = struct {
root: Root, root: Root,
next: ?*UndoNode = null, next_undo: ?*UndoNode = null,
next_redo: ?*UndoNode = null,
branches: ?*UndoBranch = null, branches: ?*UndoBranch = null,
meta: []const u8, meta: []const u8,
file_eol_mode: EolMode, file_eol_mode: EolMode,
}; };
const UndoBranch = struct { const UndoBranch = struct {
redo: *UndoNode, redo_head: *UndoNode,
next: ?*UndoBranch, next: ?*UndoBranch,
}; };
@ -1511,9 +1511,8 @@ pub fn update(self: *Self, root: Root) void {
} }
pub fn store_undo(self: *Self, meta: []const u8) error{OutOfMemory}!void { pub fn store_undo(self: *Self, meta: []const u8) error{OutOfMemory}!void {
self.push_undo(try self.create_undo(self.root, meta));
self.curr_history = null;
try self.push_redo_branch(); try self.push_redo_branch();
self.push_undo(try self.create_undo(self.root, meta));
} }
fn create_undo(self: *const Self, root: Root, meta_: []const u8) error{OutOfMemory}!*UndoNode { fn create_undo(self: *const Self, root: Root, meta_: []const u8) error{OutOfMemory}!*UndoNode {
@ -1527,54 +1526,65 @@ fn create_undo(self: *const Self, root: Root, meta_: []const u8) error{OutOfMemo
return h; return h;
} }
fn push_undo(self: *Self, h: *UndoNode) void { fn push_undo(self: *Self, node: *UndoNode) void {
const next = self.undo_history; node.next_undo = self.undo_head;
self.undo_history = h; self.undo_head = node;
h.next = next;
} }
fn push_redo(self: *Self, h: *UndoNode) void { fn pop_undo(self: *Self) ?*UndoNode {
const next = self.redo_history; const node = self.undo_head orelse return null;
self.redo_history = h; self.undo_head = node.next_undo;
h.next = next; return node;
}
fn push_redo(self: *Self, node: *UndoNode) void {
node.next_redo = self.redo_head;
self.redo_head = node;
}
fn pop_redo(self: *Self) ?*UndoNode {
const node = self.redo_head orelse return null;
self.redo_head = node.next_redo;
return node;
} }
fn push_redo_branch(self: *Self) !void { fn push_redo_branch(self: *Self) !void {
const r = self.redo_history orelse return; const redo_head = self.redo_head orelse return;
const u = self.undo_history orelse return; const undo_head = self.undo_head orelse return;
const next = u.branches; const branch = try self.allocator.create(UndoBranch);
const b = try self.allocator.create(UndoBranch); branch.* = .{
b.* = .{ .redo_head = redo_head,
.redo = r, .next = undo_head.branches,
.next = next,
}; };
u.branches = b; undo_head.branches = branch;
self.redo_history = null; self.redo_head = null;
} }
pub fn undo(self: *Self, meta: []const u8) error{Stop}![]const u8 { pub fn undo(self: *Self) error{Stop}![]const u8 {
const r = self.curr_history orelse self.create_undo(self.root, meta) catch return error.Stop; const node = self.pop_undo() orelse return error.Stop;
const h = self.undo_history orelse return error.Stop; if (self.redo_head == null) blk: {
self.undo_history = h.next; self.push_redo(self.create_undo(self.root, &.{}) catch break :blk);
self.curr_history = h; }
self.root = h.root; self.push_redo(node);
self.file_eol_mode = h.file_eol_mode; self.root = node.root;
self.push_redo(r); self.file_eol_mode = node.file_eol_mode;
self.mtime = std.time.milliTimestamp(); self.mtime = std.time.milliTimestamp();
return h.meta; return node.meta;
} }
pub fn redo(self: *Self) error{Stop}![]const u8 { pub fn redo(self: *Self) error{Stop}![]const u8 {
const u = self.curr_history orelse return error.Stop; if (self.redo_head) |redo_head| if (self.root != redo_head.root)
const h = self.redo_history orelse return error.Stop; return error.Stop;
if (u.root != self.root) return error.Stop; const node = self.pop_redo() orelse return error.Stop;
self.redo_history = h.next; self.push_undo(node);
self.curr_history = h; if (self.redo_head) |head| {
self.root = h.root; self.root = head.root;
self.file_eol_mode = h.file_eol_mode; self.file_eol_mode = head.file_eol_mode;
self.push_undo(u); if (head.next_redo == null)
self.redo_head = null;
}
self.mtime = std.time.milliTimestamp(); self.mtime = std.time.milliTimestamp();
return h.meta; return node.meta;
} }
pub fn write_state(self: *const Self, writer: *std.Io.Writer) error{ Stop, OutOfMemory, WriteFailed }!void { pub fn write_state(self: *const Self, writer: *std.Io.Writer) error{ Stop, OutOfMemory, WriteFailed }!void {

View file

@ -409,6 +409,22 @@ pub const Editor = struct {
if (self.buffer) |p| p.set_meta(meta.written()) catch {}; if (self.buffer) |p| p.set_meta(meta.written()) catch {};
} }
fn count_cursels(self: *const Self) usize {
var count: usize = 0;
for (self.cursels.items) |*cursel_| if (cursel_.*) |_| {
count += 1;
};
return count;
}
fn count_cursels_saved(self: *const Self) usize {
var count: usize = 0;
for (self.cursels_saved.items) |*cursel_| if (cursel_.*) |_| {
count += 1;
};
return count;
}
pub fn write_state(self: *const Self, writer: *std.Io.Writer) !void { pub fn write_state(self: *const Self, writer: *std.Io.Writer) !void {
try cbor.writeArrayHeader(writer, 10); try cbor.writeArrayHeader(writer, 10);
try cbor.writeValue(writer, self.file_path orelse ""); try cbor.writeValue(writer, self.file_path orelse "");
@ -427,11 +443,7 @@ pub const Editor = struct {
} }
try self.view.write(writer); try self.view.write(writer);
var count_cursels: usize = 0; try cbor.writeArrayHeader(writer, self.count_cursels());
for (self.cursels.items) |*cursel_| if (cursel_.*) |_| {
count_cursels += 1;
};
try cbor.writeArrayHeader(writer, count_cursels);
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
try cursel.write(writer); try cursel.write(writer);
}; };
@ -806,16 +818,17 @@ pub const Editor = struct {
fn store_undo_meta(self: *Self, allocator: Allocator) ![]u8 { fn store_undo_meta(self: *Self, allocator: Allocator) ![]u8 {
var meta: std.Io.Writer.Allocating = .init(allocator); var meta: std.Io.Writer.Allocating = .init(allocator);
defer meta.deinit(); defer meta.deinit();
try cbor.writeArrayHeader(&meta.writer, 2);
try cbor.writeArrayHeader(&meta.writer, self.count_cursels_saved());
for (self.cursels_saved.items) |*cursel_| if (cursel_.*) |*cursel| for (self.cursels_saved.items) |*cursel_| if (cursel_.*) |*cursel|
try cursel.write(&meta.writer); try cursel.write(&meta.writer);
return meta.toOwnedSlice();
}
fn store_current_undo_meta(self: *Self, allocator: Allocator) ![]u8 { try cbor.writeArrayHeader(&meta.writer, self.count_cursels());
var meta: std.Io.Writer.Allocating = .init(allocator);
defer meta.deinit();
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel|
try cursel.write(&meta.writer); try cursel.write(&meta.writer);
return meta.toOwnedSlice(); return meta.toOwnedSlice();
} }
@ -839,15 +852,30 @@ pub const Editor = struct {
try self.send_editor_modified(); try self.send_editor_modified();
} }
fn restore_undo_redo_meta(self: *Self, meta: []const u8) !void { fn restore_cursels_array(self: *Self, iter: *[]const u8) !void {
var len = cbor.decodeArrayHeader(iter) catch return error.UndoMetaSyntaxCurSelsError;
while (len > 0) : (len -= 1) {
var cursel: CurSel = .{};
if (!try cursel.extract(iter)) return error.UndoMetaSyntaxCurSelError;
(try self.cursels.addOne(self.allocator)).* = cursel;
}
}
fn restore_undo_meta(self: *Self, meta: []const u8) !void {
if (meta.len > 0) if (meta.len > 0)
self.clear_all_cursors(); self.clear_all_cursors();
var iter = meta; var iter = meta;
while (iter.len > 0) { if ((cbor.decodeArrayHeader(&iter) catch return error.UndoMetaSyntaxError) != 2) return error.UndoMetaSyntaxError;
var cursel: CurSel = .{}; return self.restore_cursels_array(&iter);
if (!try cursel.extract(&iter)) return error.SyntaxError;
(try self.cursels.addOne(self.allocator)).* = cursel;
} }
fn restore_redo_meta(self: *Self, meta: []const u8) !void {
if (meta.len > 0)
self.clear_all_cursors();
var iter = meta;
if ((cbor.decodeArrayHeader(&iter) catch return error.UndoMetaSyntaxError) != 2) return error.UndoMetaSyntaxError;
try cbor.skipValue(&iter); // first array is pre-operation cursels
return self.restore_cursels_array(&iter); // second array is post-operation cursels
} }
fn restore_undo(self: *Self) !void { fn restore_undo(self: *Self) !void {
@ -856,18 +884,14 @@ pub const Editor = struct {
if (self.buffer) |b_mut| { if (self.buffer) |b_mut| {
try self.send_editor_jump_source(); try self.send_editor_jump_source();
self.cancel_all_matches(); self.cancel_all_matches();
var sfa = std.heap.stackFallback(512, self.allocator); const meta = b_mut.undo() catch |e| switch (e) {
const sfa_allocator = sfa.get();
const redo_metadata = try self.store_current_undo_meta(sfa_allocator);
defer sfa_allocator.free(redo_metadata);
const meta = b_mut.undo(redo_metadata) catch |e| switch (e) {
error.Stop => { error.Stop => {
self.logger.print("nothing to undo", .{}); self.logger.print("nothing to undo", .{});
return; return;
}, },
else => return e, else => return e,
}; };
try self.restore_undo_redo_meta(meta); try self.restore_undo_meta(meta);
try self.send_editor_jump_destination(); try self.send_editor_jump_destination();
} }
} }
@ -883,7 +907,7 @@ pub const Editor = struct {
}, },
else => return e, else => return e,
}; };
try self.restore_undo_redo_meta(meta); try self.restore_redo_meta(meta);
try self.send_editor_jump_destination(); try self.send_editor_jump_destination();
} }
} }