pinephone-test/examples/PieceTable.zig

462 lines
17 KiB
Zig

const std = @import("std");
const testing = std.testing;
/// Represents buffer and a set of changes to the buffer.
///
/// All insertions are copied internally. Deletions to the buffer will not free memory.
///
/// To initialize without text, use struct initialization syntax and specify an allocator
/// like so: `var table = PieceTable{ .allocator = allocator };`
pub const PieceTable = struct {
allocator: std.mem.Allocator,
original: []const u8 = &.{},
buffer: std.ArrayListUnmanaged(u8) = .{},
pieces: std.ArrayListUnmanaged(Piece) = .{},
pub const Piece = struct {
start: usize,
length: usize,
tag: Tag = .added,
pub const Tag = enum {
original,
added,
};
};
pub fn init(allocator: std.mem.Allocator, original: []const u8) !PieceTable {
if (original.len == 0) {
// An empty string was passed, skip making a copy of it
return .{
.allocator = allocator,
};
}
const original_copy = try allocator.dupe(u8, original);
// Create piece pointing to original text
var pieces = try std.ArrayListUnmanaged(Piece).initCapacity(allocator, 1);
pieces.appendAssumeCapacity(.{
.start = 0,
.length = original_copy.len,
.tag = .original,
});
return .{
.allocator = allocator,
.original = original_copy,
.buffer = .{},
.pieces = pieces,
};
}
pub fn deinit(table: *PieceTable) void {
table.allocator.free(table.original);
table.buffer.deinit(table.allocator);
table.pieces.deinit(table.allocator);
}
/// Inserts `new_text` into buffer at `index`.
///
/// `new_text` is owned by caller.
///
/// It is an error to insert outside of the bounds of the piece table. If the
/// table is empty, 0 is the only valid argument for index.
pub fn insert(table: *PieceTable, index: usize, text: []const u8) !void {
const start = table.buffer.items.len;
const length = text.len;
try table.buffer.appendSlice(table.allocator, text);
// Insert at the start of the file, catches empty tables
if (index == 0) {
try table.pieces.insert(table.allocator, 0, .{ .start = start, .length = length, .tag = .added });
return;
}
var p_i: usize = 0;
var b_i: usize = 0;
while (p_i < table.pieces.items.len) : (p_i += 1) {
const p = table.pieces.items[p_i];
if (index == b_i + p.length) {
if (p_i + 1 == table.pieces.items.len) {
// The new index is the end of the file
try table.pieces.append(table.allocator, .{ .start = start, .length = length, .tag = .added });
return;
} else {
// The new index is directly after an existing node, but not at the end of the file.
try table.pieces.insert(table.allocator, p_i + 1, .{ .start = start, .length = length, .tag = .added });
return;
}
} else if (index < b_i + p.length) {
// new piece is within another piece; split the old one into 2
// and insert the new piece between
// ignore the returned slice since we will also want the
// piece right before the insertion
_ = try table.pieces.addManyAt(table.allocator, p_i + 1, 2);
const pieces = table.pieces.items[p_i..][0..3];
const sub_i = index - b_i;
const start0 = pieces[0].start;
const length_original = pieces[0].length;
const length_rest = length_original - sub_i;
const length_first = length_original - length_rest;
std.debug.assert(pieces[0].length == length_first + length_rest);
pieces[0].length = length_first;
pieces[1].start = start;
pieces[1].length = length;
pieces[2].start = start0 + (length_first);
pieces[2].length = length_rest;
// set the tag for the pieces 1 and 2
// elide setting the tag for pieces[0], it should be correct already
pieces[1].tag = .added;
switch (p.tag) {
.original => pieces[2].tag = .original,
.added => pieces[2].tag = .added,
}
return;
} else {
b_i += p.length;
}
}
@panic("Impossible state while inserting into PieceTable");
}
/// Deletes the data from start to start+length from the piece table.
/// Will not free any memory.
pub fn delete(table: *PieceTable, start: usize, length: usize) !void {
if (length == 0) return error.InvalidLength;
const endi = start + length;
var b_i: usize = 0; // buffer index
const p_start, const start_subi, const start_piece = for (table.pieces.items, 0..) |piece, i| {
if (start < b_i + piece.length) {
// start found
break .{ i, start - b_i, piece };
}
b_i += piece.length;
} else return error.OutOfBounds;
// reuse b_i
const p_end, const end_subi, const end_piece = for (table.pieces.items[p_start..], p_start..) |piece, i| {
if (endi < b_i + piece.length) {
break .{ i, endi - b_i, piece };
}
b_i += piece.length;
} else .{ p_start, start_piece.length, start_piece };
// Removal cases:
// 1. the deletion starts on one piece boundary and ends on another piece boundary
// - Delete all pieces between start and end
// 2. the deletion starts within a piece and ends on a boundary
// - Delete all but the start piece
// - modify slice end in start piece
// 3. the deletion starts on a bondary and ends within a piece
// - Delete all but the end piece
// - modify slice start in end piece
// 4. the deletion starts within a piece and ends within a piece
// - Delet all the start and end pieces
// - modify slice end in start piece
// - modify slice end in end piece
const is_start_on_boundary = start_subi == 0;
const is_end_on_boundary = end_subi == end_piece.length;
const remove_len = (p_end + 1) - p_start;
if (is_start_on_boundary and is_end_on_boundary) {
table.pieces.replaceRange(table.allocator, p_start, remove_len, &.{}) catch unreachable;
} else {
if (is_start_on_boundary) {
const new = &[_]PieceTable.Piece{
.{ .start = end_piece.start + end_subi, .length = end_piece.length - end_subi, .tag = end_piece.tag },
};
table.pieces.replaceRange(table.allocator, p_start, remove_len, new) catch unreachable;
} else if (is_end_on_boundary) {
const new = &[_]PieceTable.Piece{
.{ .start = start_piece.start, .length = start_piece.length - start_subi, .tag = start_piece.tag },
};
table.pieces.replaceRange(table.allocator, p_start, remove_len, new) catch unreachable;
} else {
const new = &[_]PieceTable.Piece{
.{ .start = start_piece.start, .length = start_subi, .tag = start_piece.tag },
.{ .start = end_piece.start + end_subi, .length = end_piece.length - end_subi, .tag = end_piece.tag },
};
table.pieces.replaceRange(table.allocator, p_start, remove_len, new) catch unreachable;
}
}
}
pub fn getTotalSize(table: PieceTable) usize {
var length: usize = 0;
for (table.pieces.items) |piece| {
length += piece.length;
}
return length;
}
fn getPiece(table: PieceTable, piece: Piece) []const u8 {
return switch (piece.tag) {
.original => table.original[piece.start..][0..piece.length],
.added => table.buffer.items[piece.start..][0..piece.length],
};
}
pub fn writeAll(table: PieceTable, buffer: []u8) void {
std.debug.assert(table.getTotalSize() == buffer.len);
var current_buffer = buffer[0..];
for (table.pieces.items) |piece| {
const str = table.getPiece(piece);
@memcpy(current_buffer[0..piece.length], str);
current_buffer = current_buffer[piece.length..];
}
}
pub fn writeAllAlloc(table: PieceTable) ![]u8 {
const size = table.getTotalSize();
const buffer = try table.allocator.alloc(u8, size);
table.writeAll(buffer);
return buffer;
}
};
test "Init empty PieceTable" {
var table = try PieceTable.init(testing.allocator, "");
defer table.deinit();
var out_buf: [0]u8 = undefined;
table.writeAll(&out_buf);
try testing.expectEqualStrings("", &out_buf);
}
test "Insert into empty PieceTable" {
var table = try PieceTable.init(testing.allocator, "");
defer table.deinit();
try table.insert(0, "the quick brown fox\njumped over the lazy dog");
var out_buf: [44]u8 = undefined;
table.writeAll(&out_buf);
try testing.expectEqualStrings(
\\the quick brown fox
\\jumped over the lazy dog
, &out_buf);
}
test "Init Piecetable" {
const original = "the quick brown fox\njumped over the lazy dog";
var table = try PieceTable.init(testing.allocator, original);
defer table.deinit();
var out_buf: [44]u8 = undefined;
table.writeAll(&out_buf);
try testing.expectEqualStrings(
\\the quick brown fox
\\jumped over the lazy dog
, &out_buf);
}
test "Insert into PieceTable" {
const original = "the quick brown fox\njumped over the lazy dog";
var table = try PieceTable.init(testing.allocator, original);
defer table.deinit();
try table.insert(20, "went to the park and\n");
try testing.expectEqual(@as(usize, 3), table.pieces.items.len);
try testing.expectEqual(PieceTable.Piece.Tag.original, table.pieces.items[0].tag);
try testing.expectEqual(PieceTable.Piece.Tag.added, table.pieces.items[1].tag);
try testing.expectEqual(PieceTable.Piece.Tag.original, table.pieces.items[2].tag);
try testing.expectEqualStrings("the quick brown fox\n", table.getPiece(table.pieces.items[0]));
try testing.expectEqualStrings("went to the park and\n", table.getPiece(table.pieces.items[1]));
try testing.expectEqualStrings("jumped over the lazy dog", table.getPiece(table.pieces.items[2]));
try testing.expectEqual(@as(usize, 65), table.getTotalSize());
var out_buf: [65]u8 = undefined;
table.writeAll(&out_buf);
try testing.expectEqualStrings(
\\the quick brown fox
\\went to the park and
\\jumped over the lazy dog
, &out_buf);
}
test "Insert at end of Piece" {
const original = "the quick brown fox\njumped over the lazy dog";
var table = try PieceTable.init(testing.allocator, original);
defer table.deinit();
try table.insert(20, "went to the park and\n");
try table.insert(41, "ate a burger and\n");
try testing.expectEqual(@as(usize, 4), table.pieces.items.len);
try testing.expectEqual(PieceTable.Piece.Tag.original, table.pieces.items[0].tag);
try testing.expectEqual(PieceTable.Piece.Tag.added, table.pieces.items[1].tag);
try testing.expectEqual(PieceTable.Piece.Tag.added, table.pieces.items[2].tag);
try testing.expectEqual(PieceTable.Piece.Tag.original, table.pieces.items[3].tag);
// try testing.expectEqualStrings("the quick brown fox\n", table.pieces.items[0].slice);
// try testing.expectEqualStrings("went to the park and\n", table.pieces.items[1].slice);
// try testing.expectEqualStrings("ate a burger and\n", table.pieces.items[2].slice);
// try testing.expectEqualStrings("jumped over the lazy dog", table.pieces.items[3].slice);
try testing.expectEqual(@as(usize, 82), table.getTotalSize());
var out_buf: [82]u8 = undefined;
table.writeAll(&out_buf);
try testing.expectEqualStrings(
\\the quick brown fox
\\went to the park and
\\ate a burger and
\\jumped over the lazy dog
, &out_buf);
}
test "Insert at end of file" {
const original = "the quick brown fox";
var table = try PieceTable.init(testing.allocator, original);
defer table.deinit();
try table.insert(19, "\njumped over the lazy dog");
try testing.expectEqual(@as(usize, 2), table.pieces.items.len);
try testing.expectEqual(PieceTable.Piece.Tag.original, table.pieces.items[0].tag);
try testing.expectEqual(PieceTable.Piece.Tag.added, table.pieces.items[1].tag);
// try testing.expectEqualStrings("the quick brown fox", table.pieces.items[0].slice);
// try testing.expectEqualStrings("\njumped over the lazy dog", table.pieces.items[1].slice);
try testing.expectEqual(@as(usize, 44), table.getTotalSize());
var out_buf: [44]u8 = undefined;
table.writeAll(&out_buf);
try testing.expectEqualStrings(
\\the quick brown fox
\\jumped over the lazy dog
, &out_buf);
}
test "Delete one entire Piece" {
const original = "the quick brown fox";
var table = try PieceTable.init(testing.allocator, original);
defer table.deinit();
try table.delete(0, 19);
try testing.expectEqual(@as(usize, 0), table.pieces.items.len);
}
test "Delete multiple entire Pieces" {
const original = "the quick brown fox\njumped over the lazy dog";
var table = try PieceTable.init(testing.allocator, original);
defer table.deinit();
try table.insert(20, "went to the park and\n");
try table.insert(41, "ate a burger and\n");
try table.delete(20, 38);
try testing.expectEqual(@as(usize, 2), table.pieces.items.len);
}
test "Delete inside a Piece" {
const original = "the quick brown fox";
var table = try PieceTable.init(testing.allocator, original);
defer table.deinit();
// delete "brown "
try table.delete(10, 6);
try testing.expectEqual(@as(usize, 2), table.pieces.items.len);
// try testing.expectEqualStrings("the quick ", table.pieces.items[0].slice);
// try testing.expectEqualStrings("fox", table.pieces.items[1].slice);
try testing.expectEqual(@as(usize, 13), table.getTotalSize());
var out_buf: [13]u8 = undefined;
table.writeAll(&out_buf);
try testing.expectEqualStrings(
\\the quick fox
, &out_buf);
}
test "Delete from within one piece to within another piece" {
const original = "the quick brown fox\njumped over the lazy dog";
var table = try PieceTable.init(testing.allocator, original);
defer table.deinit();
try table.insert(20, "went to the park and\n");
try table.insert(41, "ate a burger and\n");
try table.delete(45, 13 + 12);
try testing.expectEqual(@as(usize, 4), table.pieces.items.len);
try testing.expectEqual(@as(usize, 57), table.getTotalSize());
var out_buf: [57]u8 = undefined;
table.writeAll(&out_buf);
try testing.expectEqualStrings(
\\the quick brown fox
\\went to the park and
\\ate the lazy dog
, &out_buf);
}
test "Delete from start of piece to within piece" {
const original = "the quick brown fox\njumped over the lazy dog";
var table = try PieceTable.init(testing.allocator, original);
defer table.deinit();
try table.insert(20, "went to the park and\n");
try table.insert(41, "ate a burger and\n");
try table.delete(41, 13);
try testing.expectEqual(@as(usize, 4), table.pieces.items.len);
try testing.expectEqual(@as(usize, 69), table.getTotalSize());
var out_buf: [69]u8 = undefined;
table.writeAll(&out_buf);
try testing.expectEqualStrings(
\\the quick brown fox
\\went to the park and
\\and
\\jumped over the lazy dog
, &out_buf);
}
test "Delete from within piece to end of piece" {
const original = "the quick brown fox\njumped over the lazy dog";
var table = try PieceTable.init(testing.allocator, original);
defer table.deinit();
try table.insert(20, "went to the park and\n");
try table.insert(41, "ate a burger and\n");
try table.delete(45, 13);
try testing.expectEqual(@as(usize, 4), table.pieces.items.len);
try testing.expectEqual(@as(usize, 69), table.getTotalSize());
var out_buf: [69]u8 = undefined;
table.writeAll(&out_buf);
try testing.expectEqualStrings(
\\the quick brown fox
\\went to the park and
\\ate jumped over the lazy dog
, &out_buf);
}