summary refs log tree commit diff
path: root/src/Buffer.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/Buffer.zig')
-rw-r--r--src/Buffer.zig202
1 files changed, 202 insertions, 0 deletions
diff --git a/src/Buffer.zig b/src/Buffer.zig
new file mode 100644
index 0000000..faee5c4
--- /dev/null
+++ b/src/Buffer.zig
@@ -0,0 +1,202 @@
+// Gap buffer
+// SPDX-FileCopyrightText: 2025 Nguyễn Gia Phong
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+const assert = std.debug.assert;
+const eql = std.meta.eql;
+const std = @import("std");
+
+const Input = tree_sitter.Input;
+const Parser = tree_sitter.Parser;
+const Point = tree_sitter.Point;
+const Tree = tree_sitter.Tree;
+const tree_sitter = @import("tree-sitter");
+
+const Grapheme = Graphemes.Grapheme;
+const Graphemes = vaxis.Graphemes;
+const vaxis = @import("vaxis");
+
+const janet = zsanett.janet;
+const zsanett = @import("zsanett");
+
+const Selection = @import("Selection.zig");
+const languages = @import("languages");
+const graphemes = &@import("main.zig").graphemes;
+
+const initial_gap_size = std.atomic.cache_line;
+
+const Buffer = @This();
+/// File content with gap, owned by Janet runtime.
+data: []u8,
+gap_position: u32 = 0,
+gap_size: u32 = initial_gap_size,
+parser: *Parser,
+tree: *Tree, // TODO: polyglot
+scroll_row: u32 = 0,
+selection: Selection,
+paste_direction: enum { before, after } = .before,
+
+fn read(payload: ?*anyopaque, byte_index: u32, _: Point,
+        bytes_read: *u32) callconv(.C) [*c]const u8 {
+    const buffer: *Buffer = @alignCast(@ptrCast(payload.?));
+    if (byte_index < buffer.gap_position) {
+        bytes_read.* = @intCast(buffer.gap_position - byte_index);
+        return buffer.data[byte_index..buffer.gap_position].ptr;
+    }
+    const index_with_gap = byte_index + buffer.gap_size;
+    if (index_with_gap < buffer.data.len) {
+        bytes_read.* = @intCast(buffer.data.len - index_with_gap);
+        return buffer.data[index_with_gap..].ptr;
+    } else {
+        bytes_read.* = 0;
+        return "";
+    }
+}
+
+pub fn open(text: []u8) !Buffer {
+    const data = gap: {
+        const n: i32 = @intCast(text.len);
+        const buffer = janet.pointerBufferUnsafe(@ptrCast(text.ptr), n, n);
+        const capacity: usize = @intCast(n + initial_gap_size);
+        if (janet.realloc(buffer.*.data, capacity)) |data| {
+            janet.gcpressure(initial_gap_size);
+            buffer.*.data = @ptrCast(data);
+            buffer.*.capacity = @intCast(capacity);
+            buffer.*.count = buffer.*.capacity;
+            // TODO: use @memmove in Zig 0.15
+            var i = capacity;
+            while (i >= initial_gap_size) : (i -= 1)
+                buffer.*.data[i] = buffer.*.data[i - initial_gap_size];
+            break :gap buffer.*.data[0..capacity];
+        } else return error.OutOfMemory;
+    };
+
+    const parser = Parser.create();
+    errdefer parser.destroy();
+    const language = languages.c();
+    errdefer language.destroy();
+    try parser.setLanguage(language);
+    var result = Buffer{
+        .data = data,
+        .parser = parser,
+        .tree = undefined,
+        .selection = undefined,
+    };
+    result.tree = parser.parse(.{
+        .payload = &result,
+        .read = Buffer.read,
+    }, null).?;
+    result.selection = .{
+        .head = .{ .node = result.tree.rootNode() },
+        .tail = .{ .node = result.tree.rootNode() },
+    };
+    return result;
+}
+
+pub fn close(buffer: Buffer) void {
+    buffer.tree.getLanguage().destroy();
+    buffer.tree.destroy();
+    buffer.parser.destroy();
+}
+
+fn skipLines(text: []const u8, from: u32, n: u32) union(enum) {
+    done: u32,
+    left: u32,
+} {
+    assert(n > 0);
+    var lines: u32 = 0;
+    for (text[from..], 1..) |c, i|
+        if (c == '\n') {
+            lines += 1;
+            if (lines == n)
+                return .{ .done = @intCast(from + i) };
+        };
+    return .{ .left = n - lines };
+}
+
+pub fn iterate(buffer: Buffer) struct {
+    const Iterator = @This();
+    graphemes_iterator: Graphemes.Iterator,
+    data: []const u8,
+    offset: u32,
+    before_gap: bool,
+    gap_start: u32,
+    gap_end: u32,
+
+    pub fn next(iterator: *Iterator) ?Grapheme {
+        if (iterator.graphemes_iterator.next()) |grapheme| {
+            return .{
+                .offset = grapheme.offset + iterator.offset,
+                .len = grapheme.len,
+            };
+        } else if (iterator.before_gap) {
+            const buffer_after_gap = iterator.data[iterator.gap_end..];
+            iterator.graphemes_iterator = graphemes.iterator(buffer_after_gap);
+            iterator.offset = iterator.gap_start;
+            iterator.before_gap = false;
+            return iterator.next();
+        } else return null;
+    }
+} {
+    const root_node = buffer.tree.rootNode();
+    const point = Point{ .row = buffer.scroll_row, .column = 0 };
+    const node = root_node.descendantForPointRange(point, point).?;
+    const start_point = node.startPoint();
+    assert(start_point.row < point.row or eql(start_point, point));
+
+    const start_byte = node.startByte();
+    const start = if (eql(start_point, point))
+        start_byte
+    else if (start_byte >= buffer.gap_position)
+        skipLines(buffer.data[buffer.gap_size..], start_byte,
+                  point.row - start_point.row).done
+    else switch (skipLines(buffer.data[0..buffer.gap_position], start_byte,
+                           point.row - start_point.row)) {
+        .done => |offset| offset,
+        .left => |left| skipLines(buffer.data[buffer.gap_size..],
+                                  buffer.gap_position, left).done,
+    };
+    if (start < buffer.gap_position) {
+        const buffer_before_gap = buffer.data[start..buffer.gap_position];
+        return .{
+            .graphemes_iterator = graphemes.iterator(buffer_before_gap),
+            .data = buffer.data,
+            .offset = start,
+            .before_gap = true,
+            .gap_start = buffer.gap_position,
+            .gap_end = buffer.gap_position + buffer.gap_size,
+        };
+    }
+    const offset = start + buffer.gap_size;
+    return .{
+        .graphemes_iterator = graphemes.iterator(buffer.data[offset..]),
+        .data = undefined,
+        .offset = start,
+        .before_gap = true,
+        .gap_start = undefined,
+        .gap_end = undefined,
+    };
+}
+
+pub fn bytes(buffer: Buffer, grapheme: Grapheme) []const u8 {
+    if (grapheme.offset < buffer.gap_position)
+        return grapheme.bytes(buffer.data);
+    const start = grapheme.offset + buffer.gap_size;
+    const end = start + grapheme.len;
+    return buffer.data[start..end];
+}
+
+pub fn selectedBytes(buffer: Buffer) []const u8 {
+    const start = buffer.selection.startByte();
+    const end = buffer.selection.endByte();
+    if (end <= buffer.gap_position)
+        return buffer.data[start..end];
+    if (start >= buffer.gap_position)
+        return buffer.data[buffer.gap_size..][start..end];
+    unreachable;
+}
+
+pub fn paste(buffer: Buffer, data: []const u8) Buffer {
+    _ = data;
+    return buffer;
+}