This commit is contained in:
2026-06-17 01:27:09 +12:00
parent a795a38cfe
commit 317c8c3ad2
2 changed files with 202 additions and 102 deletions

1
README.md Normal file
View File

@@ -0,0 +1 @@
piece table with both undo and redo features.

View File

@@ -38,19 +38,38 @@ const Action = struct {
old_length: usize,
};
const ActionGroup = union(ActionType) {
/// this will only occur when the table was already empty so boof it
append: void,
const ActionGroup = struct {
const PieceRedo = struct {
piece: Piece,
index: usize,
};
/// happens when a piece is cleanly inserted in to the table
insert: usize,
/// holds the min amount of info inorder to restore a change
undo_action: union(ActionType) {
/// this will only occur when the table was already empty so boof it
append: void,
/// happens when the piece requires to be split before indexing
insert_splits_piece: Action,
/// happens when a piece is cleanly inserted in to the table
insert: usize,
/// happens when a piece is deletes (length set to zero)
/// appends all modifed pieces to be restored
delete: std.ArrayList(Action),
/// happens when the piece requires to be split before indexing
insert_splits_piece: Action,
/// happens when a piece is deletes (length set to zero)
/// appends all modifed pieces to be restored
delete: std.ArrayList(Action),
},
/// just keeps the function args, best way to redo something is to redo it
redo_action: union(ActionType) {
append: Piece,
insert: PieceRedo,
insert_splits_piece: PieceRedo,
delete: struct {
from: usize,
to: usize,
},
},
};
/// abstracted so i can implement redo
@@ -59,20 +78,36 @@ const ActionList = struct {
actions: std.ArrayList(ActionGroup) = .empty,
/// offsets from the last index i down to zero
undo_offset: usize = 0,
pub const empty = ActionList{};
pub fn append(self: *Self, alloc: std.mem.Allocator, action_group: ActionGroup) !void {
if (self.undo_offset != 0) {
try self.actions.resize(alloc, self.actions.items.len - 1 - self.undo_offset);
self.undo_offset = 0;
}
try self.actions.append(alloc, action_group);
}
pub fn pop(self: *Self) ?ActionGroup {
return self.actions.pop();
pub fn undo(self: *Self) ?ActionGroup {
if (self.undo_offset == self.actions.items.len) return null;
defer self.undo_offset += 1;
return self.actions.items[self.actions.items.len - 1 - self.undo_offset];
}
pub fn redo(self: *Self) ?ActionGroup {
if (self.undo_offset == 0) return null;
self.undo_offset -= 1;
return self.actions.items[self.actions.items.len - 1 - self.undo_offset];
}
pub fn deinit(self: *Self, alloc: std.mem.Allocator) void {
for (self.actions.items) |*act| {
switch (act.*) {
.delete => act.delete.deinit(alloc),
switch (act.undo_action) {
.delete => act.undo_action.delete.deinit(alloc),
else => continue,
}
}
@@ -103,6 +138,92 @@ const PieceTable = struct {
/// piece : the piece to add
/// index : the index in the text where the new piece will be added
pub fn insert(self: *Self, alloc: Alloc, piece: Piece, index: usize) !void {
return self.insertWActionDisable(alloc, piece, index, false);
}
/// will delete all from (index) to (index)
/// self : piece table that is being operated on
/// alloc : allocator used for adding the actions to the action list.
/// from : the index to be deleted from
/// to : the index the deletion will stop
pub fn delete(self: *Self, alloc: std.mem.Allocator, from: usize, to: usize) !void {
return self.deleteWActionDisable(alloc, from, to, false);
}
fn deleteWActionDisable(self: *Self, alloc: std.mem.Allocator, from: usize, to: usize, action_disabled: bool) !void {
var currnet_index: usize = 0;
var edited_piece_index: usize = 0;
var delete_actions: std.ArrayList(Action) = .empty;
// from set
for (self.pieces.items, 0..) |p, i| {
// find the index where the split occurs
if (currnet_index + p.length > from) {
// we gotta split the piece
var current_piece = &self.pieces.items[i];
if (!action_disabled) try delete_actions.append(
alloc,
Action{
.piece_table_index = i,
.old_start = current_piece.start,
.old_length = current_piece.length,
},
);
current_piece.length -= from - currnet_index;
edited_piece_index = i;
break;
}
if (currnet_index + p.length <= from) currnet_index += p.length;
}
// to set
for (self.pieces.items, edited_piece_index..) |p, i| {
if (currnet_index + p.length < to) {
var current_piece = &self.pieces.items[i];
if (!action_disabled) try delete_actions.append(
alloc,
Action{
.piece_table_index = i,
.old_start = current_piece.start,
.old_length = current_piece.length,
},
);
current_piece.length = 0;
currnet_index += p.length;
}
if (currnet_index + p.length >= to) {
var current_piece = &self.pieces.items[i];
if (!action_disabled) try delete_actions.append(
alloc,
Action{
.piece_table_index = i,
.old_start = current_piece.start,
.old_length = current_piece.length,
},
);
// shrink it
current_piece.start += to - currnet_index;
current_piece.length -= to - currnet_index;
break;
}
}
if (!action_disabled) try self.actions.append(alloc, ActionGroup{
.undo_action = .{ .delete = delete_actions },
.redo_action = .{ .delete = .{ .from = from, .to = to } },
});
}
fn insertWActionDisable(self: *Self, alloc: Alloc, piece: Piece, index: usize, action_disabled: bool) !void {
var currnet_start: usize = 0;
for (self.pieces.items, 0..) |p, i| {
@@ -126,18 +247,30 @@ const PieceTable = struct {
};
try self.pieces.replaceRange(alloc, i, 1, &pieces);
try self.actions.append(alloc, .{
.insert_splits_piece = Action{
.piece_table_index = i,
.old_length = p.length,
.old_start = p.start,
if (!action_disabled) try self.actions.append(alloc, ActionGroup{
.undo_action = .{
.insert_splits_piece = Action{
.piece_table_index = i,
.old_length = p.length,
.old_start = p.start,
},
},
.redo_action = .{ .insert_splits_piece = .{
.piece = piece,
.index = index,
} },
});
return;
}
if (currnet_start + p.length == index) {
try self.pieces.insert(alloc, i + 1, piece);
try self.actions.append(alloc, .{ .insert = i + 1 });
if (!action_disabled) try self.actions.append(alloc, ActionGroup{
.undo_action = .{ .insert = i + 1 },
.redo_action = .{ .insert = .{
.piece = piece,
.index = index,
} },
});
return;
} // just append after
if (currnet_start + p.length < index) currnet_start += p.length;
@@ -145,99 +278,37 @@ const PieceTable = struct {
// if there are no items this runs
try self.pieces.append(alloc, piece);
try self.actions.append(alloc, .append);
if (!action_disabled) try self.actions.append(alloc, ActionGroup{
.undo_action = .append,
.redo_action = .{ .append = piece },
});
}
/// will delete all from (index) to (index)
/// self : piece table that is being operated on
/// alloc : allocator used for adding the actions to the action list.
/// from : the index to be deleted from
/// to : the index the deletion will stop
pub fn delete(self: *Self, alloc: std.mem.Allocator, from: usize, to: usize) !void {
var currnet_index: usize = 0;
var edited_piece_index: usize = 0;
var delete_actions: std.ArrayList(Action) = .empty;
pub fn redo(self: *Self, alloc: std.mem.Allocator) !void {
const action_group = self.actions.redo() orelse return;
// from set
for (self.pieces.items, 0..) |p, i| {
// find the index where the split occurs
if (currnet_index + p.length > from) {
// we gotta split the piece
var current_piece = &self.pieces.items[i];
try delete_actions.append(
alloc,
Action{
.piece_table_index = i,
.old_start = current_piece.start,
.old_length = current_piece.length,
},
);
current_piece.length -= from - currnet_index;
edited_piece_index = i;
break;
}
if (currnet_index + p.length <= from) currnet_index += p.length;
switch (action_group.redo_action) {
.append => |piece| {
try self.insertWActionDisable(alloc, piece, 0, true);
},
.insert, .insert_splits_piece => |action| {
try self.insertWActionDisable(alloc, action.piece, action.index, true);
},
.delete => |deletes| {
try self.deleteWActionDisable(alloc, deletes.from, deletes.to, true);
},
}
// to set
for (self.pieces.items, edited_piece_index..) |p, i| {
if (currnet_index + p.length < to) {
var current_piece = &self.pieces.items[i];
try delete_actions.append(
alloc,
Action{
.piece_table_index = i,
.old_start = current_piece.start,
.old_length = current_piece.length,
},
);
current_piece.length = 0;
currnet_index += p.length;
}
if (currnet_index + p.length >= to) {
var current_piece = &self.pieces.items[i];
try delete_actions.append(
alloc,
Action{
.piece_table_index = i,
.old_start = current_piece.start,
.old_length = current_piece.length,
},
);
// shrink it
current_piece.start += to - currnet_index;
current_piece.length -= to - currnet_index;
break;
}
}
try self.actions.append(alloc, .{ .delete = delete_actions });
}
pub fn undo(self: *Self, alloc: std.mem.Allocator) !void {
var action_to_undo = self.actions.pop() orelse return;
switch (action_to_undo) {
var action_group = self.actions.undo() orelse return;
switch (action_group.undo_action) {
.append => {
self.pieces.deinit(alloc);
self.pieces = .empty;
},
.insert => |insert_index| {
// for the small price of one alloc you too can just swap nothing in
// might be faster, dont know should test
// [TODO] test if this is faster
try self.pieces.append(alloc, .none);
_ = self.pieces.swapRemove(insert_index);
_ = self.pieces.orderedRemove(insert_index);
},
.insert_splits_piece => |action| {
@@ -258,7 +329,7 @@ const PieceTable = struct {
piece.start = action.old_start;
}
action_to_undo.delete.deinit(alloc);
action_group.undo_action.delete.deinit(alloc);
},
}
}
@@ -315,5 +386,33 @@ test "PieceTable insert" {
std.debug.print("{f}\n", .{piece_table});
try piece_table.redo(std.testing.allocator);
std.debug.print("{f}\n", .{piece_table});
for (0..5) |_| {
try piece_table.insert(std.testing.allocator, .{
.text = &"a",
.start = 0,
.length = 1,
}, 20);
}
std.debug.print("{f}\n", .{piece_table});
try piece_table.undo(std.testing.allocator);
std.debug.print("{f}\n", .{piece_table});
try piece_table.undo(std.testing.allocator);
std.debug.print("{f}\n", .{piece_table});
try piece_table.undo(std.testing.allocator);
std.debug.print("{f}\n", .{piece_table});
try piece_table.undo(std.testing.allocator);
std.debug.print("{f}\n", .{piece_table});
try piece_table.undo(std.testing.allocator);
std.debug.print("{f}\n", .{piece_table});
try piece_table.undo(std.testing.allocator);
std.debug.print("{f}\n", .{piece_table});
// try piece_table.undo(std.testing.allocator);
// std.debug.print("{f}\n", .{piece_table});
piece_table.deinit(std.testing.allocator);
}