// 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: []const u8, file: []const 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: []const u8, file: []const u8) !Buffer { const data = gap: { const unsafe_ptr: *anyopaque = @constCast(@ptrCast(text.ptr)); const n: i32 = @intCast(text.len); const buffer = janet.pointerBufferUnsafe(unsafe_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, .file = file, .parser = parser, .tree = undefined, .selection = undefined, }; result.tree = parser.parse(.{ .payload = &result, .read = Buffer.read, }, null).?; const span0 = Selection.Unit{ .span = .{ .tree = result.tree } }; result.selection = .{ .head = span0, .tail = span0 }; return result; } pub fn close(buffer: Buffer) void { buffer.tree.getLanguage().destroy(); buffer.tree.destroy(); buffer.parser.destroy(); } pub fn length(buffer: Buffer) u32 { return @intCast(buffer.data.len - buffer.gap_size); } pub fn graphemeAt(buffer: Buffer, n: u32) Grapheme { const text = if (n < buffer.gap_position) buffer.data[n..buffer.gap_position] else buffer.data[buffer.gap_size..][n..]; var iterator = graphemes.iterator(text); return .{ .offset = n, .len = iterator.peek().?.len }; } pub fn graphemeBefore(buffer: Buffer, n: u32) ?Grapheme { if (n == 0) return null; const text = if (n <= buffer.gap_position) buffer.data[0..n] else buffer.data[buffer.gap_size..][buffer.gap_position..n]; var iterator = graphemes.reverseIterator(text); const len = iterator.peek().?.len; return .{ .offset = n - len, .len = len }; } fn skipLines(text: []const u8, from: u32, n: u32) union(enum) { done: u32, left: u32, } { if (n == 0) return .{ .done = from }; 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 point = Point{ .row = buffer.scroll_row, .column = 0 }; const root_node = buffer.tree.rootNode(); const root_start = root_node.startPoint(); const start = if (root_start.row < point.row or eql(root_start, point)) skip: { const node = root_node.descendantForPointRange(point, point).?; const start_point = node.startPoint(); const start_byte = node.startByte(); if (eql(start_point, point)) break :skip start_byte; assert(start_point.row < point.row); break :skip 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, }; } else switch (skipLines(buffer.data[0..buffer.gap_position], 0, 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 = false, .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; }