piecetable impl with undo

This commit is contained in:
2026-06-17 00:43:43 +12:00
commit a795a38cfe
3 changed files with 556 additions and 0 deletions

319
src/root.zig Normal file
View File

@@ -0,0 +1,319 @@
//! By convention, root.zig is the root source file when making a package.
const std = @import("std");
const Io = std.Io;
const Piece = struct {
// each piece could just be a slice however this make it easier to write
start: usize,
length: usize,
text: *const []const u8,
const nothing: []const u8 = "";
pub const none = Piece{
.start = 0,
.length = 0,
.text = &nothing,
};
pub fn format(self: Piece, writer: *std.Io.Writer) !void {
try writer.print(
"piece {{src[{},{}] : \"{s}\"}}",
.{ self.start, self.length, self.text.* },
);
}
};
const ActionType = enum {
append,
insert,
insert_splits_piece,
delete,
};
const Action = struct {
piece_table_index: usize,
old_start: usize,
old_length: usize,
};
const ActionGroup = union(ActionType) {
/// this will only occur when the table was already empty so boof it
append: void,
/// happens when a piece is cleanly inserted in to the table
insert: usize,
/// 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),
};
/// abstracted so i can implement redo
const ActionList = struct {
const Self = @This();
actions: std.ArrayList(ActionGroup) = .empty,
pub const empty = ActionList{};
pub fn append(self: *Self, alloc: std.mem.Allocator, action_group: ActionGroup) !void {
try self.actions.append(alloc, action_group);
}
pub fn pop(self: *Self) ?ActionGroup {
return self.actions.pop();
}
pub fn deinit(self: *Self, alloc: std.mem.Allocator) void {
for (self.actions.items) |*act| {
switch (act.*) {
.delete => act.delete.deinit(alloc),
else => continue,
}
}
self.actions.deinit(alloc);
}
};
/// piece table with built in undo
/// since the piece table is going in a text editor
/// if you dont have undo is not going to be a very
/// good text editor we aint trying to be ed here
const PieceTable = struct {
const Self = @This();
const Alloc = std.mem.Allocator;
pieces: std.ArrayList(Piece) = .empty,
actions: ActionList = .empty,
// i have this here because its easy
total_len: usize = 0,
pub fn init() Self {
return Self{};
}
/// self : piece table that is being operated on
/// alloc : allocator used for adding to the arraylists
/// 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 {
var currnet_start: usize = 0;
for (self.pieces.items, 0..) |p, i| {
// find the index where the split occurs
if (currnet_start + p.length > index) {
// we gotta split the piece
const pieces: [3]Piece = [3]Piece{
Piece{
.text = p.text,
.start = p.start,
.length = index - currnet_start,
},
piece,
Piece{
.text = p.text,
.start = p.start + (index - currnet_start),
.length = p.length - (index - currnet_start),
},
};
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,
},
});
return;
}
if (currnet_start + p.length == index) {
try self.pieces.insert(alloc, i + 1, piece);
try self.actions.append(alloc, .{ .insert = i + 1 });
return;
} // just append after
if (currnet_start + p.length < index) currnet_start += p.length;
}
// if there are no items this runs
try self.pieces.append(alloc, piece);
try self.actions.append(alloc, .append);
}
/// 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;
// 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;
}
// 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) {
.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);
},
.insert_splits_piece => |action| {
// remove the 2 addtional pieces
self.pieces.orderedRemoveMany(&.{
action.piece_table_index + 1,
action.piece_table_index + 2,
});
const first_half_of_split_piece = &self.pieces.items[action.piece_table_index];
first_half_of_split_piece.length = action.old_length;
first_half_of_split_piece.start = action.old_start;
},
.delete => |deletes| {
for (deletes.items) |action| {
const piece = &self.pieces.items[action.piece_table_index];
piece.length = action.old_length;
piece.start = action.old_start;
}
action_to_undo.delete.deinit(alloc);
},
}
}
pub fn format(self: Self, writer: *std.Io.Writer) !void {
_ = try writer.write("table : \"");
for (self.pieces.items) |p| {
_ = try writer.write(p.text.*[p.start .. p.start + p.length]);
}
_ = try writer.write("\"");
}
// pub fn undo(self: Self) !void {}
pub fn deinit(self: *Self, alloc: std.mem.Allocator) void {
self.actions.deinit(alloc);
self.pieces.deinit(alloc);
}
};
test "PieceTable insert" {
const orignal_text: []const u8 = "damn these are some cool toads";
const new_text: []const u8 = "frogs and ";
const even_newer_text: []const u8 = "cats but not ";
var piece_table = PieceTable.init();
try piece_table.insert(std.testing.allocator, .{
.start = 0,
.length = orignal_text.len,
.text = &orignal_text,
}, 0);
std.debug.print("{f}\n", .{piece_table});
try piece_table.insert(std.testing.allocator, .{
.text = &new_text,
.start = 0,
.length = new_text.len,
}, 20);
std.debug.print("{f}\n", .{piece_table});
try piece_table.delete(std.testing.allocator, 20, 20 + new_text.len);
std.debug.print("{f}\n", .{piece_table});
try piece_table.insert(std.testing.allocator, .{
.text = &even_newer_text,
.start = 0,
.length = even_newer_text.len,
}, 20);
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);
}