widgets(text_input): use internal GapBuffer impl

Use a tiny GapBuffer implementation internal to the library.

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
Tim Culverhouse 2024-08-12 20:18:41 -05:00
parent 7190fde166
commit 4fbbebbf0c
4 changed files with 197 additions and 141 deletions

View file

@ -3,13 +3,11 @@ const std = @import("std");
pub fn build(b: *std.Build) void {
const include_libxev = b.option(bool, "libxev", "Enable support for libxev library (default: true)") orelse true;
const include_images = b.option(bool, "images", "Enable support for images (default: true)") orelse true;
const include_text_input = b.option(bool, "text_input", "Enable support for the TextInput widget (default: true)") orelse true;
const include_aio = b.option(bool, "aio", "Enable support for zig-aio library (default: false)") orelse false;
const options = b.addOptions();
options.addOption(bool, "libxev", include_libxev);
options.addOption(bool, "images", include_images);
options.addOption(bool, "text_input", include_text_input);
options.addOption(bool, "aio", include_aio);
const options_mod = options.createModule();
@ -27,10 +25,6 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
.target = target,
}) else null;
const gap_buffer_dep = if (include_text_input) b.lazyDependency("gap_buffer", .{
.optimize = optimize,
.target = target,
}) else null;
const xev_dep = if (include_libxev) b.lazyDependency("libxev", .{
.optimize = optimize,
.target = target,
@ -50,7 +44,6 @@ pub fn build(b: *std.Build) void {
vaxis_mod.addImport("grapheme", zg_dep.module("grapheme"));
vaxis_mod.addImport("DisplayWidth", zg_dep.module("DisplayWidth"));
if (zigimg_dep) |dep| vaxis_mod.addImport("zigimg", dep.module("zigimg"));
if (gap_buffer_dep) |dep| vaxis_mod.addImport("gap_buffer", dep.module("gap_buffer"));
if (xev_dep) |dep| vaxis_mod.addImport("xev", dep.module("xev"));
if (aio_dep) |dep| vaxis_mod.addImport("aio", dep.module("aio"));
if (aio_dep) |dep| vaxis_mod.addImport("coro", dep.module("coro"));
@ -100,7 +93,6 @@ pub fn build(b: *std.Build) void {
tests.root_module.addImport("grapheme", zg_dep.module("grapheme"));
tests.root_module.addImport("DisplayWidth", zg_dep.module("DisplayWidth"));
if (zigimg_dep) |dep| tests.root_module.addImport("zigimg", dep.module("zigimg"));
if (gap_buffer_dep) |dep| tests.root_module.addImport("gap_buffer", dep.module("gap_buffer"));
tests.root_module.addImport("build_options", options_mod);
const tests_run = b.addRunArtifact(tests);

View file

@ -8,10 +8,6 @@
.hash = "1220dd654ef941fc76fd96f9ec6adadf83f69b9887a0d3f4ee5ac0a1a3e11be35cf5",
.lazy = true,
},
.gap_buffer = .{
.url = "git+https://github.com/ryleelyman/GapBuffer.zig#9039708e09fc3eb5f698ab5694a436afe503c6a6",
.hash = "1220f525973ae804ec0284556bfc47db7b6a8dc86464a853956ef859d6e0fb5fa93b",
},
.zg = .{
.url = "git+https://codeberg.org/dude_the_builder/zg?ref=master#689ab6b83d08c02724b99d199d650ff731250998",
.hash = "12200d1ce5f9733a9437415d85665ad5fbc85a4d27689fd337fecad8014acffe3aa5",

View file

@ -11,7 +11,4 @@ pub const LineNumbers = @import("widgets/LineNumbers.zig");
pub const TextView = @import("widgets/TextView.zig");
pub const CodeView = @import("widgets/CodeView.zig");
pub const Terminal = @import("widgets/terminal/Terminal.zig");
// Widgets with dependencies
pub const TextInput = if (opts.text_input) @import("widgets/TextInput.zig") else undefined;
pub const TextInput = @import("widgets/TextInput.zig");

View file

@ -3,7 +3,6 @@ const assert = std.debug.assert;
const Key = @import("../Key.zig");
const Cell = @import("../Cell.zig");
const Window = @import("../Window.zig");
const GapBuffer = @import("gap_buffer").GapBuffer;
const Unicode = @import("../Unicode.zig");
const TextInput = @This();
@ -18,7 +17,7 @@ const ellipsis: Cell.Character = .{ .grapheme = "…", .width = 1 };
// Index of our cursor
cursor_idx: usize = 0,
grapheme_count: usize = 0,
buf: GapBuffer(u8),
buf: Buffer,
/// the number of graphemes to skip when drawing. Used for horizontal scrolling
draw_offset: usize = 0,
@ -33,7 +32,7 @@ unicode: *const Unicode,
pub fn init(alloc: std.mem.Allocator, unicode: *const Unicode) TextInput {
return TextInput{
.buf = GapBuffer(u8).init(alloc),
.buf = Buffer.init(alloc),
.unicode = unicode,
};
}
@ -47,37 +46,36 @@ pub fn update(self: *TextInput, event: Event) !void {
.key_press => |key| {
if (key.matches(Key.backspace, .{})) {
if (self.cursor_idx == 0) return;
try self.deleteBeforeCursor();
self.deleteBeforeCursor();
} else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) {
if (self.cursor_idx == self.grapheme_count) return;
try self.deleteAtCursor();
self.deleteAfterCursor();
} else if (key.matches(Key.left, .{}) or key.matches('b', .{ .ctrl = true })) {
if (self.cursor_idx > 0) self.cursor_idx -= 1;
self.cursorLeft();
} else if (key.matches(Key.right, .{}) or key.matches('f', .{ .ctrl = true })) {
if (self.cursor_idx < self.grapheme_count) self.cursor_idx += 1;
self.cursorRight();
} else if (key.matches('a', .{ .ctrl = true }) or key.matches(Key.home, .{})) {
self.buf.moveGapLeft(self.buf.firstHalf().len);
self.cursor_idx = 0;
} else if (key.matches('e', .{ .ctrl = true }) or key.matches(Key.end, .{})) {
self.buf.moveGapRight(self.buf.secondHalf().len);
self.cursor_idx = self.grapheme_count;
} else if (key.matches('k', .{ .ctrl = true })) {
try self.deleteToEnd();
self.deleteToEnd();
} else if (key.matches('u', .{ .ctrl = true })) {
try self.deleteToStart();
self.deleteToStart();
} else if (key.text) |text| {
try self.buf.insertSliceBefore(self.byteOffsetToCursor(), text);
self.cursor_idx += 1;
self.grapheme_count += 1;
try self.insertSliceAtCursor(text);
}
},
}
}
/// insert text at the cursor position
pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) !void {
pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) std.mem.Allocator.Error!void {
var iter = self.unicode.graphemeIterator(data);
var byte_offset_to_cursor = self.byteOffsetToCursor();
while (iter.next()) |text| {
try self.buf.insertSliceBefore(byte_offset_to_cursor, text.bytes(data));
try self.buf.insertSliceAtCursor(text.bytes(data));
byte_offset_to_cursor += text.len;
self.cursor_idx += 1;
self.grapheme_count += 1;
@ -85,24 +83,16 @@ pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) !void {
}
pub fn sliceToCursor(self: *TextInput, buf: []u8) []const u8 {
const offset = self.byteOffsetToCursor();
assert(offset <= buf.len); // provided buf was too small
if (offset <= self.buf.items.len) {
@memcpy(buf[0..offset], self.buf.items[0..offset]);
} else {
@memcpy(buf[0..self.buf.items.len], self.buf.items);
const second_half = self.buf.secondHalf();
const copy_len = offset - self.buf.items.len;
@memcpy(buf[self.buf.items.len .. self.buf.items.len + copy_len], second_half[0..copy_len]);
}
return buf[0..offset];
assert(buf.len >= self.buf.cursor);
@memcpy(buf[0..self.buf.cursor], self.buf.firstHalf());
return buf[0..self.buf.cursor];
}
/// calculates the display width from the draw_offset to the cursor
fn widthToCursor(self: *TextInput, win: Window) usize {
var width: usize = 0;
var first_iter = self.unicode.graphemeIterator(self.buf.items);
const first_half = self.buf.firstHalf();
var first_iter = self.unicode.graphemeIterator(first_half);
var i: usize = 0;
while (first_iter.next()) |grapheme| {
defer i += 1;
@ -110,23 +100,32 @@ fn widthToCursor(self: *TextInput, win: Window) usize {
continue;
}
if (i == self.cursor_idx) return width;
const g = grapheme.bytes(self.buf.items);
width += win.gwidth(g);
}
const second_half = self.buf.secondHalf();
var second_iter = self.unicode.graphemeIterator(second_half);
while (second_iter.next()) |grapheme| {
defer i += 1;
if (i < self.draw_offset) {
continue;
}
if (i == self.cursor_idx) return width;
const g = grapheme.bytes(second_half);
const g = grapheme.bytes(first_half);
width += win.gwidth(g);
}
return width;
}
fn cursorLeft(self: *TextInput) void {
if (self.cursor_idx == 0) return;
// We need to find the size of the last grapheme in the first half
var iter = self.unicode.graphemeIterator(self.buf.firstHalf());
var len: usize = 0;
while (iter.next()) |grapheme| {
len = grapheme.len;
}
self.buf.moveGapLeft(len);
self.cursor_idx -= 1;
}
fn cursorRight(self: *TextInput) void {
if (self.cursor_idx >= self.grapheme_count) return;
var iter = self.unicode.graphemeIterator(self.buf.secondHalf());
const grapheme = iter.next() orelse return;
self.buf.moveGapRight(grapheme.len);
self.cursor_idx += 1;
}
pub fn draw(self: *TextInput, win: Window) void {
if (self.cursor_idx < self.draw_offset) self.draw_offset = self.cursor_idx;
if (win.width == 0) return;
@ -143,7 +142,8 @@ pub fn draw(self: *TextInput, win: Window) void {
// assumption!! the gap is never within a grapheme
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
var first_iter = self.unicode.graphemeIterator(self.buf.items);
const first_half = self.buf.firstHalf();
var first_iter = self.unicode.graphemeIterator(first_half);
var col: usize = 0;
var i: usize = 0;
while (first_iter.next()) |grapheme| {
@ -151,7 +151,7 @@ pub fn draw(self: *TextInput, win: Window) void {
i += 1;
continue;
}
const g = grapheme.bytes(self.buf.items);
const g = grapheme.bytes(first_half);
const w = win.gwidth(g);
if (col + w >= win.width) {
win.writeCell(win.width - 1, 0, .{ .char = ellipsis });
@ -220,98 +220,40 @@ fn reset(self: *TextInput) void {
}
// returns the number of bytes before the cursor
// (since GapBuffers are strictly speaking not contiguous, this is a number in 0..realLength()
// which would need to be fed to realIndex() to get an actual offset into self.buf.items.ptr)
pub fn byteOffsetToCursor(self: TextInput) usize {
// assumption! the gap is never in the middle of a grapheme
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
var iter = self.unicode.graphemeIterator(self.buf.items);
var offset: usize = 0;
var i: usize = 0;
while (iter.next()) |grapheme| {
if (i == self.cursor_idx) break;
offset += grapheme.len;
i += 1;
} else {
var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf());
while (second_iter.next()) |grapheme| {
if (i == self.cursor_idx) break;
offset += grapheme.len;
i += 1;
}
}
return offset;
return self.buf.cursor;
}
fn deleteToEnd(self: *TextInput) !void {
const offset = self.byteOffsetToCursor();
try self.buf.replaceRangeAfter(offset, self.buf.realLength() - offset, &.{});
fn deleteToEnd(self: *TextInput) void {
self.buf.growGapRight(self.buf.secondHalf().len);
self.grapheme_count = self.cursor_idx;
}
fn deleteToStart(self: *TextInput) !void {
const offset = self.byteOffsetToCursor();
try self.buf.replaceRangeBefore(0, offset, &.{});
fn deleteToStart(self: *TextInput) void {
self.buf.growGapLeft(self.buf.cursor);
self.grapheme_count -= self.cursor_idx;
self.cursor_idx = 0;
}
fn deleteBeforeCursor(self: *TextInput) !void {
// assumption! the gap is never in the middle of a grapheme
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
var iter = self.unicode.graphemeIterator(self.buf.items);
var offset: usize = 0;
var i: usize = 1;
fn deleteBeforeCursor(self: *TextInput) void {
if (self.cursor_idx == 0) return;
// We need to find the size of the last grapheme in the first half
var iter = self.unicode.graphemeIterator(self.buf.firstHalf());
var len: usize = 0;
while (iter.next()) |grapheme| {
if (i == self.cursor_idx) {
try self.buf.replaceRangeBefore(offset, grapheme.len, &.{});
self.cursor_idx -= 1;
self.grapheme_count -= 1;
return;
}
offset += grapheme.len;
i += 1;
} else {
var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf());
while (second_iter.next()) |grapheme| {
if (i == self.cursor_idx) {
try self.buf.replaceRangeBefore(offset, grapheme.len, &.{});
self.cursor_idx -= 1;
self.grapheme_count -= 1;
return;
}
offset += grapheme.len;
i += 1;
}
len = grapheme.len;
}
self.buf.growGapLeft(len);
self.cursor_idx -= 1;
self.grapheme_count -= 1;
}
fn deleteAtCursor(self: *TextInput) !void {
// assumption! the gap is never in the middle of a grapheme
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
var iter = self.unicode.graphemeIterator(self.buf.items);
var offset: usize = 0;
var i: usize = 1;
while (iter.next()) |grapheme| {
if (i == self.cursor_idx + 1) {
try self.buf.replaceRangeAfter(offset, grapheme.len, &.{});
self.grapheme_count -= 1;
return;
}
offset += grapheme.len;
i += 1;
} else {
var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf());
while (second_iter.next()) |grapheme| {
if (i == self.cursor_idx + 1) {
try self.buf.replaceRangeAfter(offset, grapheme.len, &.{});
self.grapheme_count -= 1;
return;
}
offset += grapheme.len;
i += 1;
}
}
fn deleteAfterCursor(self: *TextInput) void {
if (self.cursor_idx == self.grapheme_count) return;
var iter = self.unicode.graphemeIterator(self.buf.secondHalf());
const grapheme = iter.next() orelse return;
self.buf.growGapRight(grapheme.len);
self.grapheme_count -= 1;
}
test "assertion" {
@ -337,10 +279,139 @@ test "sliceToCursor" {
var input = init(alloc, &unicode);
defer input.deinit();
try input.insertSliceAtCursor("hello, world");
input.cursor_idx = 2;
input.cursorLeft();
input.cursorLeft();
input.cursorLeft();
var buf: [32]u8 = undefined;
try std.testing.expectEqualStrings("he", input.sliceToCursor(&buf));
input.buf.moveGap(3);
input.cursor_idx = 5;
try std.testing.expectEqualStrings("hello", input.sliceToCursor(&buf));
try std.testing.expectEqualStrings("hello, wo", input.sliceToCursor(&buf));
input.cursorRight();
try std.testing.expectEqualStrings("hello, wor", input.sliceToCursor(&buf));
}
const Buffer = struct {
allocator: std.mem.Allocator,
buffer: []u8,
cursor: usize,
gap_size: usize,
fn init(allocator: std.mem.Allocator) Buffer {
return .{
.allocator = allocator,
.buffer = &.{},
.cursor = 0,
.gap_size = 0,
};
}
fn deinit(self: *Buffer) void {
self.allocator.free(self.buffer);
}
fn firstHalf(self: Buffer) []const u8 {
return self.buffer[0..self.cursor];
}
fn secondHalf(self: Buffer) []const u8 {
return self.buffer[self.cursor + self.gap_size ..];
}
fn grow(self: *Buffer, n: usize) std.mem.Allocator.Error!void {
// Always grow by 512 bytes
const new_size = self.buffer.len + n + 512;
// Allocate the new memory
const new_memory = try self.allocator.alloc(u8, new_size);
// Copy the first half
@memcpy(new_memory[0..self.cursor], self.firstHalf());
// Copy the second half
const second_half = self.secondHalf();
@memcpy(new_memory[new_size - second_half.len ..], second_half);
self.allocator.free(self.buffer);
self.buffer = new_memory;
self.gap_size = new_size - second_half.len - self.cursor;
}
fn insertSliceAtCursor(self: *Buffer, slice: []const u8) std.mem.Allocator.Error!void {
if (slice.len == 0) return;
if (self.gap_size <= slice.len) try self.grow(slice.len);
@memcpy(self.buffer[self.cursor .. self.cursor + slice.len], slice);
self.cursor += slice.len;
self.gap_size -= slice.len;
}
/// Move the gap n bytes to the left
fn moveGapLeft(self: *Buffer, n: usize) void {
const new_idx = self.cursor -| n;
const dst = self.buffer[new_idx + self.gap_size ..];
const src = self.buffer[new_idx..self.cursor];
std.mem.copyForwards(u8, dst, src);
self.cursor = new_idx;
}
fn moveGapRight(self: *Buffer, n: usize) void {
const new_idx = self.cursor + n;
const dst = self.buffer[self.cursor..];
const src = self.buffer[self.cursor + self.gap_size .. new_idx + self.gap_size];
std.mem.copyForwards(u8, dst, src);
self.cursor = new_idx;
}
/// grow the gap by moving the cursor n bytes to the left
fn growGapLeft(self: *Buffer, n: usize) void {
// gap grows by the delta
self.gap_size += n;
self.cursor -|= n;
}
/// grow the gap by removing n bytes after the cursor
fn growGapRight(self: *Buffer, n: usize) void {
self.gap_size = @min(self.gap_size + n, self.buffer.len - self.cursor);
}
fn clearAndFree(self: *Buffer) void {
self.cursor = 0;
self.allocator.free(self.buffer);
self.buffer = &.{};
self.gap_size = 0;
}
fn clearRetainingCapacity(self: *Buffer) void {
self.cursor = 0;
self.gap_size = self.buffer.len;
}
fn toOwnedSlice(self: *Buffer) std.mem.Allocator.Error![]const u8 {
const first_half = self.firstHalf();
const second_half = self.secondHalf();
const buf = try self.allocator.alloc(u8, first_half.len + second_half.len);
@memcpy(buf[0..first_half.len], first_half);
@memcpy(buf[first_half.len..], second_half);
self.clearAndFree();
}
};
test "TextInput.zig: Buffer" {
var gap_buf = Buffer.init(std.testing.allocator);
defer gap_buf.deinit();
try gap_buf.insertSliceAtCursor("abc");
try std.testing.expectEqualStrings("abc", gap_buf.firstHalf());
try std.testing.expectEqualStrings("", gap_buf.secondHalf());
gap_buf.moveGapLeft(1);
try std.testing.expectEqualStrings("ab", gap_buf.firstHalf());
try std.testing.expectEqualStrings("c", gap_buf.secondHalf());
try gap_buf.insertSliceAtCursor(" ");
try std.testing.expectEqualStrings("ab ", gap_buf.firstHalf());
try std.testing.expectEqualStrings("c", gap_buf.secondHalf());
gap_buf.growGapLeft(1);
try std.testing.expectEqualStrings("ab", gap_buf.firstHalf());
try std.testing.expectEqualStrings("c", gap_buf.secondHalf());
try std.testing.expectEqual(2, gap_buf.cursor);
gap_buf.growGapRight(1);
try std.testing.expectEqualStrings("ab", gap_buf.firstHalf());
try std.testing.expectEqualStrings("", gap_buf.secondHalf());
try std.testing.expectEqual(2, gap_buf.cursor);
}