const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const cwd = std.fs.cwd; const Self = @This(); const max_imbalance = 7; pub const Root = *const Node; pub const unicode = @import("unicode.zig"); pub const Cursor = @import("Cursor.zig"); pub const View = @import("View.zig"); pub const Selection = @import("Selection.zig"); pub const MetaWriter = std.ArrayList(u8).Writer; pub const Metrics = struct { ctx: *const anyopaque, egc_length: egc_length_func, egc_chunk_width: egc_chunk_width_func, pub const egc_length_func = *const fn (ctx: *const anyopaque, egcs: []const u8, colcount: *c_int, abs_col: usize) usize; pub const egc_chunk_width_func = *const fn (self: *const anyopaque, chunk_: []const u8, abs_col_: usize) usize; }; arena: std.heap.ArenaAllocator, allocator: Allocator, external_allocator: Allocator, root: Root, leaves_buf: ?[]Node = null, file_buf: ?[]const u8 = null, file_path: []const u8 = "", last_save: ?Root = null, file_exists: bool = true, file_eol_mode: EolMode = .lf, undo_history: ?*UndoNode = null, redo_history: ?*UndoNode = null, curr_history: ?*UndoNode = null, pub const EolMode = enum { lf, crlf }; pub const EolModeTag = @typeInfo(EolMode).Enum.tag_type; const UndoNode = struct { root: Root, next: ?*UndoNode = null, branches: ?*UndoBranch = null, meta: []const u8, }; const UndoBranch = struct { redo: *UndoNode, next: ?*UndoBranch, }; pub const WalkerMut = struct { keep_walking: bool = false, found: bool = false, replace: ?Root = null, err: ?anyerror = null, pub const keep_walking = WalkerMut{ .keep_walking = true }; pub const stop = WalkerMut{ .keep_walking = false }; pub const found = WalkerMut{ .found = true }; const F = *const fn (ctx: *anyopaque, leaf: *const Leaf, metrics: Metrics) WalkerMut; }; pub const Walker = struct { keep_walking: bool = false, found: bool = false, err: ?anyerror = null, pub const keep_walking = Walker{ .keep_walking = true }; pub const stop = Walker{ .keep_walking = false }; pub const found = Walker{ .found = true }; const F = *const fn (ctx: *anyopaque, leaf: *const Leaf, metrics: Metrics) Walker; }; pub const Weights = struct { bols: u32 = 0, eols: u32 = 0, len: u32 = 0, depth: u32 = 1, fn add(self: *Weights, other: Weights) void { self.bols += other.bols; self.eols += other.eols; self.len += other.len; self.depth = @max(self.depth, other.depth); } }; pub const Branch = struct { left: *const Node, right: *const Node, weights: Weights, weights_sum: Weights, const walker = *const fn (ctx: *anyopaque, branch: *const Branch) WalkerMut; fn is_balanced(self: *const Branch) bool { const left: isize = @intCast(self.left.weights_sum().depth); const right: isize = @intCast(self.right.weights_sum().depth); return @abs(left - right) < max_imbalance; } fn merge_results_const(_: *const Branch, left: Walker, right: Walker) Walker { var result = Walker{}; result.err = if (left.err) |_| left.err else right.err; result.keep_walking = left.keep_walking and right.keep_walking; result.found = left.found or right.found; return result; } fn merge_results(self: *const Branch, allocator: Allocator, left: WalkerMut, right: WalkerMut) WalkerMut { var result = WalkerMut{}; result.err = if (left.err) |_| left.err else right.err; if (left.replace != null or right.replace != null) { const new_left = left.replace orelse self.left; const new_right = right.replace orelse self.right; result.replace = if (new_left.is_empty()) new_right else if (new_right.is_empty()) new_left else Node.new(allocator, new_left, new_right) catch |e| return .{ .err = e }; } result.keep_walking = left.keep_walking and right.keep_walking; result.found = left.found or right.found; return result; } }; pub const Leaf = struct { buf: []const u8, bol: bool = true, eol: bool = true, fn new(allocator: Allocator, piece: []const u8, bol: bool, eol: bool) !*const Node { if (piece.len == 0) return if (!bol and !eol) &empty_leaf else if (bol and !eol) &empty_bol_leaf else if (!bol and eol) &empty_eol_leaf else &empty_line_leaf; const node = try allocator.create(Node); node.* = .{ .leaf = .{ .buf = piece, .bol = bol, .eol = eol } }; return node; } inline fn weights(self: *const Leaf) Weights { var len = self.buf.len; if (self.eol) len += 1; return .{ .bols = if (self.bol) 1 else 0, .eols = if (self.eol) 1 else 0, .len = @intCast(len) }; } inline fn is_empty(self: *const Leaf) bool { return self.buf.len == 0 and !self.bol and !self.eol; } fn pos_to_width(self: *const Leaf, pos: *usize, abs_col_: usize, metrics: Metrics) usize { var col: usize = 0; var abs_col = abs_col_; var cols: c_int = 0; var buf = self.buf; while (buf.len > 0 and pos.* > 0) { if (buf[0] == '\t') { cols = @intCast(8 - (abs_col % 8)); buf = buf[1..]; pos.* -= 1; } else { const bytes = metrics.egc_length(metrics.ctx, buf, &cols, abs_col); buf = buf[bytes..]; pos.* -= bytes; } col += @intCast(cols); abs_col += @intCast(cols); } return col; } fn width(self: *const Leaf, abs_col: usize, metrics: Metrics) usize { var pos: usize = std.math.maxInt(usize); return self.pos_to_width(&pos, abs_col, metrics); } inline fn width_to_pos(self: *const Leaf, col_: usize, abs_col_: usize, metrics: Metrics) !usize { var abs_col = abs_col_; var col = col_; var cols: c_int = 0; var buf = self.buf; return while (buf.len > 0) { if (col == 0) break @intFromPtr(buf.ptr) - @intFromPtr(self.buf.ptr); const bytes = metrics.egc_length(metrics.ctx, buf, &cols, abs_col); buf = buf[bytes..]; if (col < cols) break @intFromPtr(buf.ptr) - @intFromPtr(self.buf.ptr); col -= @intCast(cols); abs_col += @intCast(cols); } else error.BufferUnderrun; } inline fn dump(self: *const Leaf, l: *ArrayList(u8), abs_col: usize, metrics: Metrics) !void { var buf: [16]u8 = undefined; const wcwidth = try std.fmt.bufPrint(&buf, "{d}", .{self.width(abs_col, metrics)}); if (self.bol) try l.appendSlice("BOL "); try l.appendSlice(wcwidth); try l.append('"'); try debug_render_chunk(self.buf, l, metrics); try l.appendSlice("\" "); if (self.eol) try l.appendSlice("EOL "); } fn debug_render_chunk(chunk: []const u8, l: *ArrayList(u8), metrics: Metrics) !void { var cols: c_int = 0; var buf = chunk; while (buf.len > 0) { switch (buf[0]) { '\x00'...(' ' - 1) => { const control = unicode.control_code_to_unicode(buf[0]); try l.appendSlice(control); buf = buf[1..]; }, else => { const bytes = metrics.egc_length(metrics.ctx, buf, &cols, 0); var buf_: [4096]u8 = undefined; try l.appendSlice(try std.fmt.bufPrint(&buf_, "{s}", .{std.fmt.fmtSliceEscapeLower(buf[0..bytes])})); buf = buf[bytes..]; }, } } } }; const empty_leaf: Node = .{ .leaf = .{ .buf = "", .bol = false, .eol = false } }; const empty_bol_leaf: Node = .{ .leaf = .{ .buf = "", .bol = true, .eol = false } }; const empty_eol_leaf: Node = .{ .leaf = .{ .buf = "", .bol = false, .eol = true } }; const empty_line_leaf: Node = .{ .leaf = .{ .buf = "", .bol = true, .eol = true } }; const Node = union(enum) { node: Branch, leaf: Leaf, const walker = *const fn (ctx: *anyopaque, node: *const Node) WalkerMut; fn new(allocator: Allocator, l: *const Node, r: *const Node) !*const Node { const node = try allocator.create(Node); const l_weights_sum = l.weights_sum(); var weights_sum_ = Weights{}; weights_sum_.add(l_weights_sum); weights_sum_.add(r.weights_sum()); weights_sum_.depth += 1; node.* = .{ .node = .{ .left = l, .right = r, .weights = l_weights_sum, .weights_sum = weights_sum_ } }; return node; } fn weights_sum(self: *const Node) Weights { return switch (self.*) { .node => |*n| n.weights_sum, .leaf => |*l| l.weights(), }; } fn depth(self: *const Node) usize { return self.weights_sum().depth; } pub fn lines(self: *const Node) usize { return self.weights_sum().bols; } pub fn length(self: *const Node) usize { return self.weights_sum().len; } pub fn is_balanced(self: *const Node) bool { return switch (self.*) { .node => |*n| n.is_balanced(), .leaf => |_| true, }; } pub fn rebalance(self: *const Node, allocator: Allocator, tmp_allocator: Allocator) !Root { return if (self.is_balanced()) self else bal: { const leaves = try self.collect_leaves(tmp_allocator); defer tmp_allocator.free(leaves); break :bal self.merge(leaves, allocator); }; } fn merge(self: *const Node, leaves: []*const Node, allocator: Allocator) !Root { const len = leaves.len; if (len == 1) { return leaves[0]; } if (len == 2) { return Node.new(allocator, leaves[0], leaves[1]); } const mid = len / 2; return Node.new(allocator, try self.merge(leaves[0..mid], allocator), try self.merge(leaves[mid..], allocator)); } fn is_empty(self: *const Node) bool { return switch (self.*) { .node => |*n| n.left.is_empty() and n.right.is_empty(), .leaf => |*l| if (self == &empty_leaf) true else l.is_empty(), }; } fn collect(self: *const Node, l: *ArrayList(*const Node)) !void { switch (self.*) { .node => |*node| { try node.left.collect(l); try node.right.collect(l); }, .leaf => (try l.addOne()).* = self, } } fn collect_leaves(self: *const Node, allocator: Allocator) ![]*const Node { var leaves = ArrayList(*const Node).init(allocator); try leaves.ensureTotalCapacity(self.lines()); try self.collect(&leaves); return leaves.toOwnedSlice(); } fn walk_const(self: *const Node, f: Walker.F, ctx: *anyopaque, metrics: Metrics) Walker { switch (self.*) { .node => |*node| { const left = node.left.walk_const(f, ctx, metrics); if (!left.keep_walking) { var result = Walker{}; result.err = left.err; result.found = left.found; return result; } const right = node.right.walk_const(f, ctx, metrics); return node.merge_results_const(left, right); }, .leaf => |*l| return f(ctx, l, metrics), } } fn walk(self: *const Node, allocator: Allocator, f: WalkerMut.F, ctx: *anyopaque, metrics: Metrics) WalkerMut { switch (self.*) { .node => |*node| { const left = node.left.walk(allocator, f, ctx, metrics); if (!left.keep_walking) { var result = WalkerMut{}; result.err = left.err; result.found = left.found; if (left.replace) |p| { result.replace = Node.new(allocator, p, node.right) catch |e| return .{ .err = e }; } return result; } const right = node.right.walk(allocator, f, ctx, metrics); return node.merge_results(allocator, left, right); }, .leaf => |*l| return f(ctx, l, metrics), } } fn walk_from_line_begin_const_internal(self: *const Node, line: usize, f: Walker.F, ctx: *anyopaque, metrics: Metrics) Walker { switch (self.*) { .node => |*node| { const left_bols = node.weights.bols; if (line >= left_bols) return node.right.walk_from_line_begin_const_internal(line - left_bols, f, ctx, metrics); const left_result = node.left.walk_from_line_begin_const_internal(line, f, ctx, metrics); const right_result = if (left_result.found and left_result.keep_walking) node.right.walk_const(f, ctx, metrics) else Walker{}; return node.merge_results_const(left_result, right_result); }, .leaf => |*l| { if (line == 0) { var result = f(ctx, l, metrics); if (result.err) |_| return result; result.found = true; return result; } return Walker.keep_walking; }, } } pub fn walk_from_line_begin_const(self: *const Node, line: usize, f: Walker.F, ctx: *anyopaque, metrics: Metrics) !bool { const result = self.walk_from_line_begin_const_internal(line, f, ctx, metrics); if (result.err) |e| return e; return result.found; } fn walk_from_line_begin_internal(self: *const Node, allocator: Allocator, line: usize, f: WalkerMut.F, ctx: *anyopaque, metrics: Metrics) WalkerMut { switch (self.*) { .node => |*node| { const left_bols = node.weights.bols; if (line >= left_bols) { const right_result = node.right.walk_from_line_begin_internal(allocator, line - left_bols, f, ctx, metrics); if (right_result.replace) |p| { var result = WalkerMut{}; result.err = right_result.err; result.found = right_result.found; result.keep_walking = right_result.keep_walking; result.replace = if (p.is_empty()) node.left else Node.new(allocator, node.left, p) catch |e| return .{ .err = e }; return result; } else { return right_result; } } const left_result = node.left.walk_from_line_begin_internal(allocator, line, f, ctx, metrics); const right_result = if (left_result.found and left_result.keep_walking) node.right.walk(allocator, f, ctx, metrics) else WalkerMut{}; return node.merge_results(allocator, left_result, right_result); }, .leaf => |*l| { if (line == 0) { var result = f(ctx, l, metrics); if (result.err) |_| { result.replace = null; return result; } result.found = true; return result; } return WalkerMut.keep_walking; }, } } pub fn walk_from_line_begin(self: *const Node, allocator: Allocator, line: usize, f: WalkerMut.F, ctx: *anyopaque, metrics: Metrics) !struct { bool, ?Root } { const result = self.walk_from_line_begin_internal(allocator, line, f, ctx, metrics); if (result.err) |e| return e; return .{ result.found, result.replace }; } fn find_line_node(self: *const Node, line: usize) ?*const Node { switch (self.*) { .node => |*node| { if (node.weights_sum.bols == 1) return self; const left_bols = node.weights.bols; if (line >= left_bols) return node.right.find_line_node(line - left_bols); return node.left.find_line_node(line); }, .leaf => |*l| { return if (l.bol) self else null; }, } } fn debug_render_tree(self: *const Node, l: *ArrayList(u8), d: usize) void { switch (self.*) { .node => |*node| { l.append('(') catch {}; node.left.debug_render_tree(l, d + 1); l.append(' ') catch {}; node.right.debug_render_tree(l, d + 1); l.append(')') catch {}; }, .leaf => |*leaf| { l.append('"') catch {}; l.appendSlice(leaf.buf) catch {}; if (leaf.eol) l.appendSlice("\\n") catch {}; l.append('"') catch {}; }, } } const EgcF = *const fn (ctx: *anyopaque, egc: []const u8, wcwidth: usize, metrics: Metrics) Walker; pub fn walk_egc_forward(self: *const Node, line: usize, walker_f: EgcF, walker_ctx: *anyopaque, metrics_: Metrics) !void { const Ctx = struct { walker_f: EgcF, walker_ctx: @TypeOf(walker_ctx), abs_col: usize = 0, fn walker(ctx_: *anyopaque, leaf: *const Self.Leaf, metrics: Metrics) Walker { const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); var buf: []const u8 = leaf.buf; while (buf.len > 0) { var cols: c_int = undefined; const bytes = metrics.egc_length(metrics.ctx, buf, &cols, ctx.abs_col); const ret = ctx.walker_f(ctx.walker_ctx, buf[0..bytes], @intCast(cols), metrics); if (ret.err) |e| return .{ .err = e }; buf = buf[bytes..]; ctx.abs_col += @intCast(cols); if (!ret.keep_walking) return Walker.stop; } if (leaf.eol) { const ret = ctx.walker_f(ctx.walker_ctx, "\n", 1, metrics); if (ret.err) |e| return .{ .err = e }; if (!ret.keep_walking) return Walker.stop; ctx.abs_col = 0; } return Walker.keep_walking; } }; var ctx: Ctx = .{ .walker_f = walker_f, .walker_ctx = walker_ctx }; const found = try self.walk_from_line_begin_const(line, Ctx.walker, &ctx, metrics_); if (!found) return error.NotFound; } pub fn ecg_at(self: *const Node, line: usize, col: usize, metrics: Metrics) error{NotFound}!struct { []const u8, usize, usize } { const ctx_ = struct { col: usize, at: ?[]const u8 = null, wcwidth: usize = 0, fn walker(ctx_: *anyopaque, egc: []const u8, wcwidth: usize, _: Metrics) Walker { const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); ctx.at = egc; ctx.wcwidth = wcwidth; if (ctx.col == 0 or egc[0] == '\n' or ctx.col < wcwidth) return Walker.stop; ctx.col -= wcwidth; return Walker.keep_walking; } }; var ctx: ctx_ = .{ .col = col }; self.walk_egc_forward(line, ctx_.walker, &ctx, metrics) catch return .{ "?", 1, 0 }; return if (ctx.at) |at| .{ at, ctx.wcwidth, ctx.col } else error.NotFound; } pub fn test_at(self: *const Node, pred: *const fn (c: []const u8) bool, line: usize, col: usize, metrics: Metrics) bool { const ecg, _, _ = self.ecg_at(line, col, metrics) catch return false; return pred(ecg); } pub fn get_line_width_map(self: *const Node, line: usize, map: *ArrayList(usize), metrics: Metrics) error{ Stop, NoSpaceLeft }!void { const Ctx = struct { map: *ArrayList(usize), wcwidth: usize = 0, fn walker(ctx_: *anyopaque, egc: []const u8, wcwidth: usize, _: Metrics) Walker { const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); var n = egc.len; while (n > 0) : (n -= 1) { const p = ctx.map.addOne() catch |e| return .{ .err = e }; p.* = ctx.wcwidth; } ctx.wcwidth += wcwidth; return if (egc[0] == '\n') Walker.stop else Walker.keep_walking; } }; var ctx: Ctx = .{ .map = map }; self.walk_egc_forward(line, Ctx.walker, &ctx, metrics) catch |e| return switch (e) { error.NoSpaceLeft => error.NoSpaceLeft, else => error.Stop, }; } pub fn get_range(self: *const Node, sel: Selection, copy_buf: ?[]u8, size: ?*usize, wcwidth_: ?*usize, metrics_: Metrics) error{ Stop, NoSpaceLeft }!?[]u8 { const Ctx = struct { col: usize = 0, sel: Selection, out: ?[]u8, bytes: usize = 0, wcwidth: usize = 0, fn walker(ctx_: *anyopaque, egc: []const u8, wcwidth: usize, _: Metrics) Walker { const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); if (ctx.col < ctx.sel.begin.col) { ctx.col += wcwidth; return Walker.keep_walking; } if (ctx.out) |out| { if (egc.len > out.len) return .{ .err = error.NoSpaceLeft }; @memcpy(out[0..egc.len], egc); ctx.out = out[egc.len..]; } ctx.bytes += egc.len; ctx.wcwidth += wcwidth; if (egc[0] == '\n') { ctx.col = 0; ctx.sel.begin.col = 0; ctx.sel.begin.row += 1; } else { ctx.col += wcwidth; ctx.sel.begin.col += wcwidth; } return if (ctx.sel.begin.eql(ctx.sel.end) or ctx.sel.begin.right_of(ctx.sel.end)) Walker.stop else Walker.keep_walking; } }; var ctx: Ctx = .{ .sel = sel, .out = copy_buf }; ctx.sel.normalize(); if (ctx.sel.begin.eql(ctx.sel.end)) return if (copy_buf) |_| "" else null; self.walk_egc_forward(ctx.sel.begin.row, Ctx.walker, &ctx, metrics_) catch |e| return switch (e) { error.NoSpaceLeft => error.NoSpaceLeft, error.Stop => if (copy_buf) |buf_| buf_[0..ctx.bytes] else null, else => error.Stop, }; if (size) |p| p.* = ctx.bytes; if (wcwidth_) |p| p.* = ctx.wcwidth; return if (copy_buf) |buf_| buf_[0..ctx.bytes] else null; } pub fn get_from_pos(self: *const Node, start: Cursor, result_buf: []u8, metrics: Metrics) []const u8 { var end: Cursor = .{}; end.move_buffer_end(self, metrics); const result = self.get_range(.{ .begin = start, .end = end }, result_buf, null, null, metrics) catch |e| switch (e) { error.NoSpaceLeft => result_buf, else => @panic("buffer overflow in get_from_start_pos"), }; return result orelse ""; } pub fn delete_range(self: *const Node, sel: Selection, allocator: Allocator, size: ?*usize, metrics: Metrics) error{Stop}!Root { var wcwidth: usize = 0; _ = self.get_range(sel, null, size, &wcwidth, metrics) catch return error.Stop; return self.del_chars(sel.begin.row, sel.begin.col, wcwidth, allocator, metrics) catch return error.Stop; } pub fn del_chars(self: *const Node, line: usize, col: usize, count: usize, allocator: Allocator, metrics_: Metrics) !Root { const Ctx = struct { allocator: Allocator, col: usize, abs_col: usize = 0, count: usize, delete_next_bol: bool = false, fn walker(Ctx: *anyopaque, leaf: *const Leaf, metrics: Metrics) WalkerMut { const ctx = @as(*@This(), @ptrCast(@alignCast(Ctx))); var result = WalkerMut.keep_walking; if (ctx.delete_next_bol and ctx.count == 0) { result.replace = Leaf.new(ctx.allocator, leaf.buf, false, leaf.eol) catch |e| return .{ .err = e }; result.keep_walking = false; ctx.delete_next_bol = false; return result; } const leaf_wcwidth = leaf.width(ctx.abs_col, metrics); const leaf_bol = leaf.bol and !ctx.delete_next_bol; ctx.delete_next_bol = false; const base_col = ctx.abs_col; ctx.abs_col += leaf_wcwidth; if (ctx.col > leaf_wcwidth) { // next node ctx.col -= leaf_wcwidth; if (leaf.eol) ctx.col -= 1; } else { // this node if (ctx.col == 0) { if (ctx.count > leaf_wcwidth) { ctx.count -= leaf_wcwidth; result.replace = Leaf.new(ctx.allocator, "", leaf_bol, false) catch |e| return .{ .err = e }; if (leaf.eol) { ctx.count -= 1; ctx.delete_next_bol = true; } } else if (ctx.count == leaf_wcwidth) { result.replace = Leaf.new(ctx.allocator, "", leaf_bol, leaf.eol) catch |e| return .{ .err = e }; ctx.count = 0; } else { const pos = leaf.width_to_pos(ctx.count, base_col, metrics) catch |e| return .{ .err = e }; result.replace = Leaf.new(ctx.allocator, leaf.buf[pos..], leaf_bol, leaf.eol) catch |e| return .{ .err = e }; ctx.count = 0; } } else if (ctx.col == leaf_wcwidth) { if (leaf.eol) { ctx.count -= 1; result.replace = Leaf.new(ctx.allocator, leaf.buf, leaf_bol, false) catch |e| return .{ .err = e }; ctx.delete_next_bol = true; } ctx.col -= leaf_wcwidth; } else { if (ctx.col + ctx.count >= leaf_wcwidth) { ctx.count -= leaf_wcwidth - ctx.col; const pos = leaf.width_to_pos(ctx.col, base_col, metrics) catch |e| return .{ .err = e }; const leaf_eol = if (leaf.eol and ctx.count > 0) leaf_eol: { ctx.count -= 1; ctx.delete_next_bol = true; break :leaf_eol false; } else leaf.eol; result.replace = Leaf.new(ctx.allocator, leaf.buf[0..pos], leaf_bol, leaf_eol) catch |e| return .{ .err = e }; ctx.col = 0; } else { const pos = leaf.width_to_pos(ctx.col, base_col, metrics) catch |e| return .{ .err = e }; const pos_end = leaf.width_to_pos(ctx.col + ctx.count, base_col, metrics) catch |e| return .{ .err = e }; const left = Leaf.new(ctx.allocator, leaf.buf[0..pos], leaf_bol, false) catch |e| return .{ .err = e }; const right = Leaf.new(ctx.allocator, leaf.buf[pos_end..], false, leaf.eol) catch |e| return .{ .err = e }; result.replace = Node.new(ctx.allocator, left, right) catch |e| return .{ .err = e }; ctx.count = 0; } } if (ctx.count == 0 and !ctx.delete_next_bol) result.keep_walking = false; } return result; } }; var ctx: Ctx = .{ .allocator = allocator, .col = col, .count = count }; const found, const root = try self.walk_from_line_begin(allocator, line, Ctx.walker, &ctx, metrics_); return if (found) (root orelse error.Stop) else error.NotFound; } fn merge_in_place(leaves: []const Node, allocator: Allocator) !Root { const len = leaves.len; if (len == 1) { return &leaves[0]; } if (len == 2) { return Node.new(allocator, &leaves[0], &leaves[1]); } const mid = len / 2; return Node.new(allocator, try merge_in_place(leaves[0..mid], allocator), try merge_in_place(leaves[mid..], allocator)); } pub fn get_line(self: *const Node, line: usize, result: *ArrayList(u8), metrics: Metrics) !void { const Ctx = struct { line: *ArrayList(u8), fn walker(ctx_: *anyopaque, leaf: *const Leaf, _: Metrics) Walker { const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); ctx.line.appendSlice(leaf.buf) catch |e| return .{ .err = e }; return if (!leaf.eol) Walker.keep_walking else Walker.stop; } }; var ctx: Ctx = .{ .line = result }; const found = self.walk_from_line_begin_const(line, Ctx.walker, &ctx, metrics) catch false; return if (!found) error.NotFound; } pub fn line_width(self: *const Node, line: usize, metrics_: Metrics) !usize { const do = struct { result: usize = 0, fn walker(ctx: *anyopaque, leaf: *const Leaf, metrics: Metrics) Walker { const do = @as(*@This(), @ptrCast(@alignCast(ctx))); do.result += leaf.width(do.result, metrics); return if (!leaf.eol) Walker.keep_walking else Walker.stop; } }; var ctx: do = .{}; const found = self.walk_from_line_begin_const(line, do.walker, &ctx, metrics_) catch true; return if (found) ctx.result else error.NotFound; } pub fn pos_to_width(self: *const Node, line: usize, pos: usize, metrics_: Metrics) !usize { const do = struct { result: usize = 0, pos: usize, fn walker(ctx: *anyopaque, leaf: *const Leaf, metrics: Metrics) Walker { const do = @as(*@This(), @ptrCast(@alignCast(ctx))); do.result += leaf.pos_to_width(&do.pos, do.result, metrics); return if (!(leaf.eol or do.pos == 0)) Walker.keep_walking else Walker.stop; } }; var ctx: do = .{ .pos = pos }; const found = self.walk_from_line_begin_const(line, do.walker, &ctx, metrics_) catch true; return if (found) ctx.result else error.NotFound; } pub fn insert_chars( self_: *const Node, line_: usize, col_: usize, s: []const u8, allocator: Allocator, metrics_: Metrics, ) !struct { usize, usize, Root } { var self = self_; const Ctx = struct { allocator: Allocator, col: usize, abs_col: usize = 0, s: []const u8, eol: bool, fn walker(ctx: *anyopaque, leaf: *const Leaf, metrics: Metrics) WalkerMut { const Ctx = @as(*@This(), @ptrCast(@alignCast(ctx))); const leaf_wcwidth = leaf.width(Ctx.abs_col, metrics); const base_col = Ctx.abs_col; Ctx.abs_col += leaf_wcwidth; if (Ctx.col == 0) { const left = Leaf.new(Ctx.allocator, Ctx.s, leaf.bol, Ctx.eol) catch |e| return .{ .err = e }; const right = Leaf.new(Ctx.allocator, leaf.buf, Ctx.eol, leaf.eol) catch |e| return .{ .err = e }; return .{ .replace = Node.new(Ctx.allocator, left, right) catch |e| return .{ .err = e } }; } if (leaf_wcwidth == Ctx.col) { if (leaf.eol and Ctx.eol and Ctx.s.len == 0) { const left = Leaf.new(Ctx.allocator, leaf.buf, leaf.bol, true) catch |e| return .{ .err = e }; const right = Leaf.new(Ctx.allocator, Ctx.s, true, true) catch |e| return .{ .err = e }; return .{ .replace = Node.new(Ctx.allocator, left, right) catch |e| return .{ .err = e } }; } const left = Leaf.new(Ctx.allocator, leaf.buf, leaf.bol, false) catch |e| return .{ .err = e }; if (Ctx.eol) { const middle = Leaf.new(Ctx.allocator, Ctx.s, false, Ctx.eol) catch |e| return .{ .err = e }; const right = Leaf.new(Ctx.allocator, "", Ctx.eol, leaf.eol) catch |e| return .{ .err = e }; return .{ .replace = Node.new( Ctx.allocator, left, Node.new(Ctx.allocator, middle, right) catch |e| return .{ .err = e }, ) catch |e| return .{ .err = e } }; } else { const right = Leaf.new(Ctx.allocator, Ctx.s, false, leaf.eol) catch |e| return .{ .err = e }; return .{ .replace = Node.new(Ctx.allocator, left, right) catch |e| return .{ .err = e } }; } } if (leaf_wcwidth > Ctx.col) { const pos = leaf.width_to_pos(Ctx.col, base_col, metrics) catch |e| return .{ .err = e }; if (Ctx.eol and Ctx.s.len == 0) { const left = Leaf.new(Ctx.allocator, leaf.buf[0..pos], leaf.bol, Ctx.eol) catch |e| return .{ .err = e }; const right = Leaf.new(Ctx.allocator, leaf.buf[pos..], Ctx.eol, leaf.eol) catch |e| return .{ .err = e }; return .{ .replace = Node.new(Ctx.allocator, left, right) catch |e| return .{ .err = e } }; } const left = Leaf.new(Ctx.allocator, leaf.buf[0..pos], leaf.bol, false) catch |e| return .{ .err = e }; const middle = Leaf.new(Ctx.allocator, Ctx.s, false, Ctx.eol) catch |e| return .{ .err = e }; const right = Leaf.new(Ctx.allocator, leaf.buf[pos..], Ctx.eol, leaf.eol) catch |e| return .{ .err = e }; return .{ .replace = Node.new( Ctx.allocator, left, Node.new(Ctx.allocator, middle, right) catch |e| return .{ .err = e }, ) catch |e| return .{ .err = e } }; } Ctx.col -= leaf_wcwidth; return if (leaf.eol) WalkerMut.stop else WalkerMut.keep_walking; } }; if (s.len == 0) return error.Stop; var rest = try allocator.dupe(u8, s); var chunk = rest; var line = line_; var col = col_; var need_eol = false; while (rest.len > 0) { if (std.mem.indexOfScalar(u8, rest, '\n')) |eol| { chunk = rest[0..eol]; rest = rest[eol + 1 ..]; need_eol = true; } else { chunk = rest; rest = &[_]u8{}; need_eol = false; } var ctx: Ctx = .{ .allocator = allocator, .col = col, .s = chunk, .eol = need_eol }; const found, const replace = try self.walk_from_line_begin(allocator, line, Ctx.walker, &ctx, metrics_); if (!found) return error.NotFound; if (replace) |root| self = root; if (need_eol) { line += 1; col = 0; } else { col += metrics_.egc_chunk_width(metrics_.ctx, chunk, col); } } return .{ line, col, self }; } pub fn store(self: *const Node, writer: anytype, eol_mode: EolMode) !void { switch (self.*) { .node => |*node| { try node.left.store(writer, eol_mode); try node.right.store(writer, eol_mode); }, .leaf => |*leaf| { _ = try writer.write(leaf.buf); if (leaf.eol) switch (eol_mode) { .lf => _ = try writer.write("\n"), .crlf => _ = try writer.write("\r\n"), }; }, } } pub const FindAllCallback = fn (data: *anyopaque, begin_row: usize, begin_col: usize, end_row: usize, end_col: usize) error{Stop}!void; pub fn find_all_ranges(self: *const Node, pattern: []const u8, data: *anyopaque, callback: *const FindAllCallback, allocator: Allocator) !void { const Ctx = struct { pattern: []const u8, data: *anyopaque, callback: *const FindAllCallback, line: usize = 0, pos: usize = 0, buf: []u8, rest: []u8 = "", const Ctx = @This(); const Writer = std.io.Writer(*Ctx, error{Stop}, write); fn write(ctx: *Ctx, bytes: []const u8) error{Stop}!usize { var input = bytes; while (true) { const input_consume_size = @min(ctx.buf.len - ctx.rest.len, input.len); @memcpy(ctx.buf[ctx.rest.len .. ctx.rest.len + input_consume_size], input[0..input_consume_size]); ctx.rest = ctx.buf[0 .. ctx.rest.len + input_consume_size]; input = input[input_consume_size..]; if (ctx.rest.len < ctx.pattern.len) return bytes.len - input.len; var i: usize = 0; const end = ctx.rest.len - ctx.pattern.len; while (i <= end) { if (std.mem.eql(u8, ctx.rest[i .. i + ctx.pattern.len], ctx.pattern)) { const begin_row = ctx.line + 1; const begin_pos = ctx.pos; ctx.skip(&i, ctx.pattern.len); const end_row = ctx.line + 1; const end_pos = ctx.pos; try ctx.callback(ctx.data, begin_row, begin_pos, end_row, end_pos); } else { ctx.skip(&i, 1); } } std.mem.copyForwards(u8, ctx.buf, ctx.rest[i..]); ctx.rest = ctx.buf[0 .. ctx.rest.len - i]; if (input.len == 0) break; if (ctx.rest.len == ctx.buf.len) unreachable; } return bytes.len - input.len; } fn skip(ctx: *Ctx, i: *usize, n_: usize) void { var n = n_; while (n > 0) : (n -= 1) { if (ctx.rest[i.*] == '\n') { ctx.line += 1; ctx.pos = 0; } else { ctx.pos += 1; } i.* += 1; } } fn writer(ctx: *Ctx) Writer { return .{ .context = ctx }; } }; var ctx: Ctx = .{ .pattern = pattern, .data = data, .callback = callback, .buf = try allocator.alloc(u8, pattern.len * 2), }; defer allocator.free(ctx.buf); return self.store(ctx.writer(), .lf); } pub fn get_byte_pos(self: *const Node, pos_: Cursor, metrics_: Metrics, eol_mode: EolMode) !usize { const Ctx = struct { line: usize = 0, abs_col: usize = 0, pos: Cursor, byte_pos: usize = 0, metrics: Metrics, const Ctx = @This(); const Writer = std.io.Writer(*Ctx, error{Stop}, write); fn write(ctx: *Ctx, bytes: []const u8) error{Stop}!usize { if (ctx.line >= ctx.pos.row) { return ctx.get_col_bytes(bytes, bytes.len); } else for (bytes, 1..) |char, i| { ctx.byte_pos += 1; if (char == '\n') { ctx.line += 1; if (ctx.line >= ctx.pos.row) return ctx.get_col_bytes(bytes[i..], bytes.len); } } return bytes.len; } fn get_col_bytes(ctx: *Ctx, bytes: []const u8, result: usize) error{Stop}!usize { var buf: []const u8 = bytes; while (buf.len > 0) { if (ctx.abs_col >= ctx.pos.col) return error.Stop; if (buf[0] == '\n') return error.Stop; var cols: c_int = undefined; const egc_bytes = ctx.metrics.egc_length(ctx.metrics.ctx, buf, &cols, ctx.abs_col); ctx.abs_col += @intCast(cols); ctx.byte_pos += egc_bytes; buf = buf[egc_bytes..]; } return result; } fn writer(ctx: *Ctx) Writer { return .{ .context = ctx }; } }; var ctx: Ctx = .{ .pos = pos_, .metrics = metrics_, }; self.store(ctx.writer(), eol_mode) catch |e| switch (e) { error.Stop => return ctx.byte_pos, }; return error.NotFound; } pub fn debug_render_chunks(self: *const Node, line: usize, output: *ArrayList(u8), metrics_: Metrics) !void { const ctx_ = struct { l: *ArrayList(u8), wcwidth: usize = 0, fn walker(ctx_: *anyopaque, leaf: *const Leaf, metrics: Metrics) Walker { const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); leaf.dump(ctx.l, ctx.wcwidth, metrics) catch |e| return .{ .err = e }; ctx.wcwidth += leaf.width(ctx.wcwidth, metrics); return if (!leaf.eol) Walker.keep_walking else Walker.stop; } }; var ctx: ctx_ = .{ .l = output }; const found = self.walk_from_line_begin_const(line, ctx_.walker, &ctx, metrics_) catch true; if (!found) return error.NotFound; var buf: [16]u8 = undefined; const wcwidth = try std.fmt.bufPrint(&buf, "{d}", .{ctx.wcwidth}); try output.appendSlice(wcwidth); } pub fn debug_line_render_tree(self: *const Node, line: usize, l: *ArrayList(u8)) !void { return if (self.find_line_node(line)) |n| n.debug_render_tree(l, 0) else error.NotFound; } }; pub fn create(allocator: Allocator) !*Self { const self = try allocator.create(Self); const arena_a = if (builtin.is_test) allocator else std.heap.page_allocator; self.* = .{ .arena = std.heap.ArenaAllocator.init(arena_a), .allocator = self.arena.allocator(), .external_allocator = allocator, .root = try Node.new(self.allocator, &empty_leaf, &empty_leaf), }; return self; } pub fn deinit(self: *Self) void { if (self.file_buf) |buf| self.external_allocator.free(buf); if (self.leaves_buf) |buf| self.external_allocator.free(buf); self.arena.deinit(); self.external_allocator.destroy(self); } fn new_file(self: *const Self, file_exists: *bool) !Root { file_exists.* = false; return Leaf.new(self.allocator, "", true, false); } pub fn load(self: *const Self, reader: anytype, size: usize, eol_mode: *EolMode) !Root { const lf = '\n'; const cr = '\r'; var buf = try self.external_allocator.alloc(u8, size); const self_ = @constCast(self); self_.file_buf = buf; const read_size = try reader.readAll(buf); if (read_size != size) return error.BufferUnderrun; const final_read = try reader.read(buf); if (final_read != 0) @panic("unexpected data in final read"); eol_mode.* = .lf; var leaf_count: usize = 1; for (0..buf.len) |i| { if (buf[i] == lf) { leaf_count += 1; if (i > 0 and buf[i - 1] == cr) eol_mode.* = .crlf; } } var leaves = try self.external_allocator.alloc(Node, leaf_count); self_.leaves_buf = leaves; var cur_leaf: usize = 0; var b: usize = 0; for (0..buf.len) |i| { if (buf[i] == lf) { const line_end = if (i > 0 and buf[i - 1] == cr) i - 1 else i; const line = buf[b..line_end]; leaves[cur_leaf] = .{ .leaf = .{ .buf = line, .bol = true, .eol = true } }; cur_leaf += 1; b = i + 1; } } const line = buf[b..]; leaves[cur_leaf] = .{ .leaf = .{ .buf = line, .bol = true, .eol = false } }; if (leaves.len != cur_leaf + 1) return error.Unexpected; return Node.merge_in_place(leaves, self.allocator); } pub fn load_from_string(self: *const Self, s: []const u8, eol_mode: *EolMode) !Root { var stream = std.io.fixedBufferStream(s); return self.load(stream.reader(), s.len, eol_mode); } pub fn load_from_string_and_update(self: *Self, file_path: []const u8, s: []const u8) !void { self.root = try self.load_from_string(s, &self.file_eol_mode); self.file_path = try self.allocator.dupe(u8, file_path); self.last_save = self.root; self.file_exists = false; } pub fn load_from_file(self: *const Self, file_path: []const u8, file_exists: *bool, eol_mode: *EolMode) !Root { const file = cwd().openFile(file_path, .{ .mode = .read_only }) catch |e| switch (e) { error.FileNotFound => return self.new_file(file_exists), else => return e, }; file_exists.* = true; defer file.close(); const stat = try file.stat(); return self.load(file.reader(), @intCast(stat.size), eol_mode); } pub fn load_from_file_and_update(self: *Self, file_path: []const u8) !void { var file_exists: bool = false; var eol_mode: EolMode = .lf; self.root = try self.load_from_file(file_path, &file_exists, &eol_mode); self.file_path = try self.allocator.dupe(u8, file_path); self.last_save = self.root; self.file_exists = file_exists; self.file_eol_mode = eol_mode; } pub fn store_to_string(self: *const Self, allocator: Allocator) ![]u8 { var s = try ArrayList(u8).initCapacity(allocator, self.root.weights_sum().len); try self.root.store(s.writer()); return s.toOwnedSlice(); } fn store_to_file_const(self: *const Self, file: anytype) !void { const buffer_size = 4096 * 16; // 64KB const BufferedWriter = std.io.BufferedWriter(buffer_size, std.fs.File.Writer); const Writer = std.io.Writer(*BufferedWriter, BufferedWriter.Error, BufferedWriter.write); const file_writer: std.fs.File.Writer = file.writer(); var buffered_writer: BufferedWriter = .{ .unbuffered_writer = file_writer }; try self.root.store(Writer{ .context = &buffered_writer }, self.file_eol_mode); try buffered_writer.flush(); } pub fn store_to_existing_file_const(self: *const Self, file_path: []const u8) !void { const stat = try cwd().statFile(file_path); var atomic = try cwd().atomicFile(file_path, .{ .mode = stat.mode }); defer atomic.deinit(); try self.store_to_file_const(atomic.file); try atomic.finish(); } pub fn store_to_new_file_const(self: *const Self, file_path: []const u8) !void { const file = try cwd().createFile(file_path, .{ .read = true, .truncate = true }); defer file.close(); try self.store_to_file_const(file); } pub fn store_to_file_and_clean(self: *Self, file_path: []const u8) !void { self.store_to_existing_file_const(file_path) catch |e| switch (e) { error.FileNotFound => try self.store_to_new_file_const(file_path), else => return e, }; self.last_save = self.root; self.file_exists = true; } pub fn is_dirty(self: *const Self) bool { return if (!self.file_exists) self.root.length() > 0 else if (self.last_save) |p| self.root != p else true; } pub fn version(self: *const Self) usize { return @intFromPtr(self.root); } pub fn update(self: *Self, root: Root) void { self.root = root; } pub fn store_undo(self: *Self, meta: []const u8) !void { self.push_undo(try self.create_undo(self.root, meta)); self.curr_history = null; try self.push_redo_branch(); } fn create_undo(self: *const Self, root: Root, meta_: []const u8) !*UndoNode { const h = try self.allocator.create(UndoNode); const meta = try self.allocator.dupe(u8, meta_); h.* = UndoNode{ .root = root, .meta = meta, }; return h; } fn push_undo(self: *Self, h: *UndoNode) void { const next = self.undo_history; self.undo_history = h; h.next = next; } fn push_redo(self: *Self, h: *UndoNode) void { const next = self.redo_history; self.redo_history = h; h.next = next; } fn push_redo_branch(self: *Self) !void { const r = self.redo_history orelse return; const u = self.undo_history orelse return; const next = u.branches; const b = try self.allocator.create(UndoBranch); b.* = .{ .redo = r, .next = next, }; u.branches = b; self.redo_history = null; } pub fn undo(self: *Self, meta: []const u8) error{Stop}![]const u8 { const r = self.curr_history orelse self.create_undo(self.root, meta) catch return error.Stop; const h = self.undo_history orelse return error.Stop; self.undo_history = h.next; self.curr_history = h; self.root = h.root; self.push_redo(r); return h.meta; } pub fn redo(self: *Self) error{Stop}![]const u8 { const u = self.curr_history orelse return error.Stop; const h = self.redo_history orelse return error.Stop; if (u.root != self.root) return error.Stop; self.redo_history = h.next; self.curr_history = h; self.root = h.root; self.push_undo(u); return h.meta; }