piecetable impl with undo
This commit is contained in:
319
src/root.zig
Normal file
319
src/root.zig
Normal 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 = ¬hing,
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user