initial commit
Zig Project Action / Lint, Spell-check and test zig project (push) Has been cancelled

based on sources of https://github.com/noahbald/noe which are MIT-licensed
This commit is contained in:
2026-06-06 20:48:35 +02:00
parent d2836931a7
commit ae666cb436
12 changed files with 2116 additions and 0 deletions
+260
View File
@@ -0,0 +1,260 @@
//! A collection of iterator types.
const std = @import("std");
// --- OPAQUE ---
pub fn SliceIter(T: type) type {
return struct {
buffer: []const T,
i: usize = 0,
const Self = @This();
pub const Item = *const T;
pub fn next(self: *Self) ?Item {
if (self.peek()) |result| {
self.i += 1;
return result;
} else {
return null;
}
}
pub fn peek(self: *const Self) ?Item {
if (self.i < self.buffer.len) {
return &self.buffer[self.i];
} else {
return null;
}
}
pub fn len(self: *const Self) usize {
return if (self.i < self.buffer.len) self.buffer.len - self.i else 0;
}
};
}
pub fn ParallelSliceIter(T: type) type {
return struct {
inner: SliceIter(T),
lock: std.Thread.Mutex = .{},
const Self = @This();
pub fn parNext(self: *Self) ?.{ T, usize } {
self.lock.lock();
const i = self.inner.i;
const result = self.inner.next();
self.lock.unlock();
return .{ result, i };
}
pub fn len(self: *const Self) usize {
return self.inner.len();
}
};
}
// --- TRANSPARENT ---
pub fn Map(T: type, R: type, C: type) type {
return struct {
inner: T,
context: C,
f: fn (T, C) R,
const Self = @This();
pub fn next(self: *Self) ?R {
return self.f(self.inner.next().?, self.context);
}
};
}
pub fn Flatten(T: type) type {
return struct {
inner: T,
current: ?T.Item = null,
const Self = @This();
pub fn next(self: *Self) ?T.Item.Item {
if (self.current) |current| {
if (current) |item| {
return item;
} else {
self.current = null;
return self.next();
}
} else {
if (self.inner.next()) |current| {
self.current = current;
return self.next();
} else {
return null;
}
}
}
};
}
pub fn Take(T: type) type {
return struct {
inner: T,
remainder: usize,
const Self = @This();
pub fn next(self: *Self) ?T.Item {
if (self.remainder == 0) {
return null;
} else {
return self.inner.next();
}
}
};
}
pub fn ParallelMap(T: type, R: type, C: type) type {
return struct {
inner: T,
context: C,
f: fn (T, C) R,
const Self = @This();
pub fn parNext(self: *Self) ?.{ T, usize } {
const inner = self.inner.parNext().?;
return .{ self.f(inner[0], self.context), inner[1] };
}
};
}
// --- BASE ---
pub fn Iter(T: type) type {
return struct {
impl: T,
const Self = @This();
/// Advances the iterator and returns the next value.
///
/// Returns `null` once the iterator is finished.
pub fn next(self: *Self) ?T.Item {
return self.impl.next();
}
/// Advances the iterator by a number of steps, usually without
/// processing the intermediate values.
pub fn skip(self: *Self) void {
self.impl.skip();
}
/// Returns the exact remaining length of the iterator.
///
/// This method is optional by implementors, and may result in a compile error
/// if not provided by the implementor.
pub fn len(self: *const Self) usize {
return self.impl.len();
}
/// For an iterator of an iterator, returns an iterator over the
/// nested items.
pub fn flatten(self: Self) Iter(Flatten(T)) {
return .{ .impl = .{ .inner = self.impl } };
}
/// Takes a function and creates an iterator that calls the function on each element.
///
/// Since closures are unavailable in zig, `mapScope` can be used to provide a scope
/// or context that's passed to the function.
pub fn map(self: Self, R: type, f: fn (T, void) R) Map(Self, R, void) {
return self.mapScope(self, R, f, void{});
}
/// Takes a function and context and creates an iterator that calls the function on each element.
///
/// The same context will be passed along with each item to the function.
pub fn mapScope(self: Self, R: type, C: type, f: fn (T, C) R, context: C) Map(Self, R, C) {
return .{ .inner = self, .f = f, .context = context };
}
/// Returns an iterator that will continue to iterate until `n` items
/// have been yielded.
pub fn take(self: Self, n: usize) Iter(Take(T)) {
return .{ .impl = .{ .inner = self.impl, .remainder = n } };
}
};
}
pub fn ParallelIter(T: type) type {
return struct {
impl: T,
pool: []std.Thread = undefined,
poolSize: usize,
sink: []T = undefined,
sinkIndex: usize = 0,
sinkIndexLock: std.Thread.Mutex = .{},
const Self = @This();
pub fn init(impl: T) Self {
const cpuCount = try std.Thread.getCpuCount() catch return .{
.impl = impl,
// If cpu-count fails, assume single threaded
.poolSize = 0,
};
const poolSize = (impl.len() + cpuCount - 1) / cpuCount;
return .{
.impl = impl,
.poolSize = poolSize,
};
}
pub fn deinit(self: *Self, gpa: std.mem.Allocator) void {
gpa.free(self.pool);
gpa.free(self.sink);
}
/// Spawn threads to process each element of the iterator
pub fn fork(self: *Self, gpa: std.mem.Allocator) void {
self.pool = try gpa.alloc(std.Thread, self.poolSize);
self.sink = try gpa.alloc(T, self.impl.len());
if (self.poolSize == 0) {
return; // Assume threads are unavailable, complete syncronously
} else {
for (self.pool) |*thread| {
thread.* = try std.Thread.spawn(.{}, threadHandle, .{&self});
}
}
}
pub fn join(self: *Self) []T {
if (self.pool.len == 0) {
// Assume threads are unavailable, complete syncronously
while (true) {
self.handle() orelse break;
}
} else {
for (self.pool) |thread| {
thread.join();
}
}
return self.sink;
}
fn threadHandle(self: *Self) void {
for (0..self.poolSize) |_| {
self.handle() orelse break;
}
}
fn handle(self: *Self) ?void {
var item: T = undefined;
var i: usize = undefined;
item, i = self.impl.parNext().?;
self.sink[i] = item;
}
};
}
+21
View File
@@ -0,0 +1,21 @@
pub const Ordering = enum(u2) {
less = 0,
equal = 1,
greater = 2,
pub fn cmp(a: anytype, b: anytype) Ordering {
if (a < b) return .less else if (a == b) return .equal else return .greater;
}
pub fn gte(self: Ordering) bool {
return @intFromEnum(self) > 1;
}
pub fn lte(self: Ordering) bool {
return @intFromEnum(self) < 1;
}
pub fn is(self: Ordering, other: Ordering) bool {
return @intFromEnum(self) == @intFromEnum(other);
}
};
+911
View File
@@ -0,0 +1,911 @@
//! A rope is a data-structure that can be efficiently edited, representing a string of u8 bytes.
//!
//! The rope is represented by a sum-tree which provides efficient extraction and navigation of
//! the the tree and it's metadata.
const std = @import("std");
const arrayVec = @import("rope/array-vec.zig");
const Chunk = @import("rope/chunk.zig");
const Point = @import("rope/point.zig");
const sumTree = @import("rope/sum-tree.zig");
const iter = @import("iter.zig");
const Rope = @This();
const SumTree = sumTree.SumTree;
pub const Tree = SumTree(Chunk);
const Error = sumTree.Error || arrayVec.Error || std.mem.Allocator.Error;
/// The sum-tree of the rope's contents, stored as chunks of text.
tree: *Tree,
gpa: std.mem.Allocator,
pub fn init(gpa: std.mem.Allocator) std.mem.Allocator.Error!Rope {
return .{
.tree = try .init(gpa, void{}),
.gpa = gpa,
};
}
pub fn deinit(self: *Rope) void {
self.tree.deinit(self.gpa);
}
/// Take a reference to a reader and from it, generates a rope containing the reader's output.
pub fn read(self: *Rope, reader: *std.Io.Reader) (std.Io.Reader.ShortError || Error)!void {
var buffer: [Chunk.MAX_BASE]u8 = undefined;
while (true) {
const bytes = try reader.readSliceShort(&buffer);
try self.push(buffer[0..bytes]);
if (bytes < buffer.len) break;
}
}
pub fn slice(self: *const Rope, start: usize, end: usize) Slice {
std.debug.assert(start <= end);
var c = self.cursor(start);
return c.slice(end);
}
pub fn sliceRows(self: *const Rope, start: usize, end: usize) Slice {
std.debug.assert(start <= end);
const startByte = self.pointToOffset(.init(start, 0));
const endByte = self.pointToOffset(.init(end + 1, 0));
if (endByte == 0 or startByte == endByte)
return self.slice(startByte, startByte);
if (endByte == self.len())
return self.slice(startByte, endByte);
return self.slice(startByte, endByte - 1);
}
pub fn sliceLine(self: *const Rope, line: usize) Slice {
return self.sliceRows(line, line);
}
/// Pushes the text to the end of the rope and rebalances the tree.
pub fn push(self: *Rope, text: []const u8) Error!void {
var offset: usize = 0;
while (offset < text.len) {
// Split into chunks
const chunkSize = @min(Chunk.MAX_BASE, text.len - offset);
const chunkText = text[offset .. offset + chunkSize];
const chunk: Chunk = try .init(chunkText);
offset += chunkSize;
// Push chunk and handle split
if (try self.tree.push(self.gpa, chunk, {})) |right| {
const root = try self.gpa.create(Tree);
const left = self.tree;
root.* = .{ .internal = .{
.height = left.height() + 1,
.summary = left.summary().add(&right.summary(), {}),
.childSummaries = .init,
.childTrees = .init,
} };
try root.internal.childTrees.push(left);
try root.internal.childTrees.push(right);
try root.internal.childSummaries.push(left.summary());
try root.internal.childSummaries.push(right.summary());
self.tree = root;
}
}
}
/// Mutates the rope by inserting the text at the given offset
pub fn insert(self: *Rope, offset: usize, text: []const u8) Error!void {
if (offset >= self.len()) {
return self.push(text);
} else if (offset == 0) {
var left = try init(self.gpa);
errdefer left.deinit();
try left.push(text);
std.mem.swap(Tree, self.tree, left.tree); // left is now right
return;
}
var right = try self.split(offset);
var middle = try init(self.gpa);
try middle.push(text);
try self.join(&middle);
if (right) |*r| {
try self.join(r);
}
}
pub fn remove(self: *Rope, offset: usize) Error!void {
if (offset >= self.len()) return;
var right = try self.split(offset);
if (right) |*r| {
if (r.len() <= 1) return;
defer r.deinit();
var rest = try r.split(1);
if (rest) |*r2| {
try self.join(r2);
}
}
self.tree.checkInvariants(void{});
}
/// Splits the rope by mutation at the given offset, popping and returning the remaining rope.
pub fn split(self: *Rope, offset: usize) Error!?Rope {
self.tree.checkInvariants(void{});
var right: Rope = .{ .tree = undefined, .gpa = self.gpa };
right.tree = try self.splitTree(offset, 0) orelse return null;
self.tree.checkInvariants(void{});
right.tree.checkInvariants(void{});
return right;
}
fn splitTree(self: *Rope, offset: usize, baseOffset: usize) Error!?*Tree {
switch (self.tree.*) {
.internal => |*internal| {
// Find split boundary
var currentOffset = baseOffset;
var splitIndex: ?usize = null;
for (internal.childSummaries.slice(), 0..) |sum, i| {
if (currentOffset + sum.len > offset) {
splitIndex = i;
break;
}
currentOffset += sum.len;
}
const i = splitIndex orelse return null;
// Split boundary point
var middle: Rope = .{ .tree = undefined, .gpa = self.gpa };
middle.tree = internal.childTrees.slice()[i];
const rightStart = try middle.splitTree(offset, currentOffset);
internal.childSummaries.sliceMut()[i] = internal.childTrees.slice()[i].summary();
const hasRemainingChildren = i + 1 < internal.childTrees.slice().len;
const hasRightContent = rightStart != null or hasRemainingChildren;
if (hasRightContent) {
// Build right tree
var right = try self.gpa.create(Tree);
errdefer right.deinit(self.gpa);
right.* = .{ .internal = .{
.height = internal.height,
.summary = Chunk.Summary.zero(void{}),
.childSummaries = .init,
.childTrees = .init,
} };
if (rightStart) |tree| {
try right.internal.childTrees.push(tree);
const sum = tree.summary();
try right.internal.childSummaries.push(sum);
right.internal.summary = sum;
}
for (internal.childTrees.slice()[i + 1 ..], internal.childSummaries.slice()[i + 1 ..]) |child, sum| {
try right.internal.childTrees.push(child);
try right.internal.childSummaries.push(sum);
right.internal.summary = right.internal.summary.add(&sum, void{});
}
// Slice left tree
if (middle.summary().len == 0) {
internal.childTrees.slice()[i].deinit(self.gpa);
internal.childTrees.len = i;
internal.childSummaries.len = i;
} else {
internal.childTrees.len = i + 1;
internal.childSummaries.len = i + 1;
}
internal.summary = Chunk.Summary.zero(void{});
for (internal.childSummaries.slice()) |sum| {
internal.summary = internal.summary.add(&sum, void{});
}
self.tree.checkInvariants(void{});
right.checkInvariants(void{});
return right;
} else {
return null;
}
},
.leaf => |*leaf| {
// Find split boundary
var currentOffset = baseOffset;
var splitIndex: ?usize = null;
var splitText: usize = 0;
for (leaf.items.slice(), 0..) |chunk, i| {
if (currentOffset + chunk.text.len > offset) {
splitIndex = i;
splitText = offset - currentOffset;
break;
}
currentOffset += chunk.text.len;
}
const i = splitIndex orelse return null;
// Split boundary point
var middle = leaf.items.slice()[i];
const rightStart: Chunk = try .init(middle.slice()[splitText..]);
// Build right tree
var right = try self.gpa.create(Tree);
errdefer right.deinit(self.gpa);
right.* = .{ .leaf = .{
.summary = rightStart.summary(void{}),
.items = .init,
.itemSummaries = .init,
} };
try right.leaf.items.push(rightStart);
try right.leaf.itemSummaries.push(rightStart.summary(void{}));
for (leaf.items.slice()[i + 1 ..]) |item| {
try right.leaf.items.push(item);
const sum = item.summary(void{});
try right.leaf.itemSummaries.push(sum);
right.leaf.summary = right.leaf.summary.add(&sum, void{});
}
// Slice left tree
if (splitText == 0) {
leaf.items.len = i;
leaf.itemSummaries.len = i;
} else {
leaf.items.sliceMut()[i] = try .init(middle.text.slice()[0..splitText]);
leaf.itemSummaries.sliceMut()[i] = leaf.items.slice()[i].summary(void{});
leaf.items.len = i + 1;
leaf.itemSummaries.len = i + 1;
}
leaf.summary = Chunk.Summary.zero(void{});
for (leaf.items.slice()) |item| {
leaf.summary = leaf.summary.add(&item.summary(void{}), void{});
}
self.tree.checkInvariants(void{});
right.checkInvariants(void{});
return right;
},
}
}
/// Joins another rope to the end of this rope.
pub fn join(self: *Rope, other: *Rope) Error!void {
if (other.len() == 0) {
other.deinit();
return;
}
if (self.len() == 0) {
std.mem.swap(Tree, self.tree, other.tree);
other.deinit();
return;
}
try self.joinTree(other);
self.tree.checkInvariants(void{});
}
fn joinTree(self: *Rope, other: *Rope) Error!void {
const leftHeight = self.tree.height();
const rightHeight = other.tree.height();
if (leftHeight == rightHeight) {
const parent = try self.gpa.create(Tree);
errdefer parent.deinit(self.gpa);
parent.* = .{ .internal = .{
.height = leftHeight + 1,
.summary = self.tree.summary().add(&other.tree.summary(), void{}),
.childSummaries = .init,
.childTrees = .init,
} };
try parent.internal.childTrees.push(self.tree);
try parent.internal.childTrees.push(other.tree);
try parent.internal.childSummaries.push(self.summary());
try parent.internal.childSummaries.push(other.summary());
self.tree = parent;
} else if (leftHeight > rightHeight) {
try self.joinTreeEdge(false, other);
} else {
std.mem.swap(Tree, self.tree, other.tree);
try self.joinTreeEdge(true, other);
}
other.tree = undefined;
}
fn joinTreeEdge(self: *Rope, comptime left: bool, other: *Rope) Error!void {
switch (self.tree.*) {
.internal => |*internal| {
const i = if (left) 0 else internal.childTrees.len - 1;
const child = internal.childTrees.slice()[i];
const childHeight = child.height();
const otherHeight = other.tree.height();
if (childHeight > otherHeight) {
var childRope: Rope = .{ .tree = child, .gpa = self.gpa };
try childRope.joinTreeEdge(left, other);
internal.childSummaries.sliceMut()[i] = child.summary();
internal.summary = Chunk.Summary.zero(void{});
for (internal.childSummaries.slice()) |sum| {
internal.summary = internal.summary.add(&sum, void{});
}
} else if (childHeight == otherHeight) {
if (left) {
try internal.childTrees.insert(0, other.tree);
try internal.childSummaries.insert(0, other.summary());
} else {
try internal.childTrees.push(other.tree);
try internal.childSummaries.push(other.summary());
}
internal.summary = internal.summary.add(&other.summary(), void{});
} else {
const parent = try self.gpa.create(Tree);
errdefer parent.deinit(self.gpa);
parent.* = .{ .internal = .{
.height = self.tree.height() + 1,
.summary = other.summary().add(&child.summary(), void{}),
.childSummaries = .init,
.childTrees = .init,
} };
const first = if (left) other.tree else child;
const second = if (left) child else other.tree;
try parent.internal.childTrees.push(first);
try parent.internal.childTrees.push(second);
try parent.internal.childSummaries.push(first.summary());
try parent.internal.childSummaries.push(second.summary());
internal.childTrees.sliceMut()[i] = parent;
internal.childSummaries.sliceMut()[i] = parent.summary();
internal.summary = Chunk.Summary.zero(void{});
for (internal.childSummaries.slice()) |sum| {
internal.summary = internal.summary.add(&sum, void{});
}
}
},
.leaf => {
const parent = try self.gpa.create(Tree);
errdefer parent.deinit(self.gpa);
parent.* = .{ .internal = .{
.height = @max(self.tree.height(), other.tree.height()) + 1,
.summary = self.tree.summary().add(&other.tree.summary(), void{}),
.childSummaries = .init,
.childTrees = .init,
} };
const first = if (left) other.tree else self.tree;
const second = if (left) self.tree else other.tree;
try parent.internal.childTrees.push(first);
try parent.internal.childTrees.push(second);
try parent.internal.childSummaries.push(first.summary());
try parent.internal.childSummaries.push(second.summary());
self.tree = parent;
},
}
}
/// Returns the summary of the rope's contents.
pub fn summary(self: *const Rope) Chunk.Summary {
return self.tree.summary();
}
/// Returns the length of the rope's contents in bytes.
pub fn len(self: *const Rope) usize {
return self.summary().len;
}
/// Returns the number of newline characters in the rope.
pub fn lines(self: *const Rope) usize {
return self.summary().lines.row;
}
pub fn pointToOffset(self: *const Rope, point: Point) usize {
const selfSummary = self.summary();
if (point.cmp(&selfSummary.lines).gte()) return selfSummary.len;
const result = self.tree.find(sumTree.Dimensions(Point, Point.USize, null), Point, void{}, &point, .left);
const start = result.start;
const item = result.item;
const overshoot = point.sub(&start.d1);
return start.d2.inner + if (item) |chunk| chunk.asSlice().pointToOffset(overshoot) else 0;
}
pub fn offsetToPoint(self: *const Rope, offset: usize) Point {
const sum = self.summary();
if (offset > sum.len) {
return sum.lines;
}
const target: Point.USize = .{ .inner = offset };
const result = self.tree.find(sumTree.Dimensions(Point.USize, Point, null), Point.USize, void{}, &target, .left);
var start = result.start;
const item = result.item;
const overshoot = offset - start.d1.inner;
return start.d2.add(if (item) |chunk| &chunk.asSlice().offsetToPoint(overshoot) else &Point.zero(void{}));
}
/// Writes the contents of the rope to the writer, without flushing it.
pub fn format(self: *const Rope, writer: *std.Io.Writer) std.Io.Writer.Error!void {
const visitor: Tree.ThisVisitor(*std.Io.Writer, std.Io.Writer.Error) = .{
.visitInternal = null,
.visitLeaf = formatVisitor,
};
try visitor.visit(self.tree, writer);
}
fn formatVisitor(leaf: *const Tree.Leaf, context: *std.Io.Writer) std.Io.Writer.Error!void {
for (leaf.items.slice()) |chunk| {
const s = chunk.slice();
_ = try context.write(s);
}
}
/// Allocates and returns a string with the contents of the rope written to the string.
///
/// Deallocates the string upon error.
pub fn toString(
self: *const Rope,
gpa: std.mem.Allocator,
) (std.mem.Allocator.Error || std.Io.Writer.Error)![]u8 {
var buffer = try gpa.alloc(u8, self.len());
errdefer gpa.free(buffer);
var stream = std.Io.Writer.fixed(&buffer);
self.format(&stream);
return buffer;
}
pub fn cursor(self: *const Rope, start: usize) Cursor {
return .{ .rope = self.tree, .start = start };
}
pub const Cursor = struct {
rope: *const Tree,
start: usize = 0,
pub fn slice(self: *Cursor, end: usize) Slice {
std.debug.assert(end <= self.rope.summary().len);
var current = self.rope;
var currentStart: usize = 0;
var currentEnd = self.rope.summary().len;
current: while (true) {
// Find the shallowest node containing `offset..endOffset`.
switch (current.*) {
.internal => |*internal| {
var thisStart = currentStart;
for (internal.childTrees.slice(), internal.childSummaries.slice()) |t, s| {
const thisEnd = thisStart + s.len;
if (thisStart <= self.start and thisEnd >= end) {
currentStart = thisStart;
currentEnd = thisEnd;
current = t;
continue :current;
}
thisStart = thisEnd;
}
return .{
.tree = current,
.trimStart = self.start - currentStart,
.trimEnd = currentEnd - end,
};
},
.leaf => {
return .{
.tree = current,
.trimStart = self.start - currentStart,
.trimEnd = currentEnd - end,
};
},
}
}
}
};
pub const Slice = struct {
tree: *const Tree,
trimStart: usize,
trimEnd: usize,
pub fn len(self: *const Slice) usize {
return self.tree.summary().len -| self.trimStart -| self.trimEnd;
}
pub fn visit(
self: *const Slice,
comptime C: type,
comptime E: ?type,
f: fn ([]const u8, C) if (E) |_E| _E!void else void,
context: C,
) if (E) |_E| _E!void else void {
return self.visitDirection(false, C, E, f, context);
}
pub fn visitReverse(
self: *const Slice,
comptime C: type,
comptime E: ?type,
f: fn ([]const u8, C) if (E) |_E| _E!void else void,
context: C,
) if (E) |_E| _E!void else void {
return self.visitDirection(true, C, E, f, context);
}
pub fn visitDirection(
self: *const Slice,
comptime reverse: bool,
comptime C: type,
comptime E: ?type,
f: fn ([]const u8, C) if (E) |_E| _E!void else void,
context: C,
) if (E) |_E| _E!void else void {
const Visit = struct {
const Context = struct {
context: C,
trim: usize,
remainder: usize,
};
pub fn visitLeaf(leaf: *const Tree.Leaf, c: *Context) if (E) |_E| _E!void else void {
// Start by finding chunk bounds
var chunks = leaf.items.slice();
var firstChunk: usize = 0;
var lastChunk = chunks.len - 1;
var trimStart: usize = 0;
var trimEnd: usize = 0;
// Seek to first bound
const firstChunkTerminal = if (reverse) &lastChunk else &firstChunk;
const firstTrimTerminal = if (reverse) &trimEnd else &trimStart;
for (firstChunk..lastChunk + 1) |i| {
const index = if (reverse) chunks.len - i - 1 else i;
const chunk = chunks[index].slice();
if (c.trim < chunk.len) {
firstChunkTerminal.* = index;
firstTrimTerminal.* = c.trim;
c.trim = 0;
break;
}
c.trim -= chunk.len;
}
// Seek to last bound
const lastChunkTerminal = if (reverse) &firstChunk else &lastChunk;
const lastTrimTerminal = if (reverse) &trimStart else &trimEnd;
for (firstChunk..lastChunk + 1) |i| {
const index = if (reverse) lastChunk - i - 1 else i;
const chunk = chunks[index].slice();
var length = chunk.len;
if (i == firstChunk) {
length -= firstTrimTerminal.*;
}
if (c.remainder < length) {
lastChunkTerminal.* = index;
lastTrimTerminal.* = length - c.remainder;
c.remainder = 0;
break;
}
c.remainder -= length;
}
// Then iterate through chunk bounds in given order
chunks = chunks[firstChunk .. lastChunk + 1];
for (0..chunks.len) |i| {
const index = if (reverse) chunks.len - i - 1 else i;
var chunk = chunks[index].slice();
if (index == 0) {
chunk = chunk[trimStart..];
}
if (index == chunks.len - 1) {
chunk = chunk[0 .. chunk.len - trimEnd];
}
if (E) |_| {
try f(chunk, c.context);
} else {
f(chunk, c.context);
}
}
}
};
const visitor: Tree.ThisVisitor(*Visit.Context, E) = .{
.visitInternal = null,
.visitLeaf = Visit.visitLeaf,
};
var c: Visit.Context = .{
.context = context,
.trim = if (reverse) self.trimEnd else self.trimStart,
.remainder = self.len(),
};
return visitor.visitDirection(reverse, self.tree, &c);
}
/// Writes the contents of the rope to the writer, without flushing it.
pub fn format(self: *const Slice, writer: *std.Io.Writer) std.Io.Writer.Error!void {
try self.visit(*std.Io.Writer, std.Io.Writer.Error, sliceFormatVisitor, writer);
}
fn sliceFormatVisitor(chunk: []const u8, context: *std.Io.Writer) std.Io.Writer.Error!void {
_ = try context.write(chunk);
}
};
test read {
// Arrange
var gpa = std.testing.allocator_instance;
const alloc = gpa.allocator();
// input
const source = "The quick brown fox\njumps\nover\nthe\nlazy\ndog.";
var reader = std.Io.Reader.fixed(source);
// output
var buffer: [44]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
var rope: Rope = try .init(alloc);
// Act
try rope.read(&reader);
// Assert
try writer.print("{f}", .{rope});
try std.testing.expect(source.len > Chunk.MAX_BASE);
try std.testing.expectEqualStrings(source, &buffer);
rope.deinit();
try std.testing.expectEqual(std.heap.Check.ok, gpa.deinit());
}
test slice {
// Arrange
var gpa = std.testing.allocator_instance;
const alloc = gpa.allocator();
// input
const source = "The quick brown fox\njumps\nover\nthe\nlazy\ndog.";
var reader = std.Io.Reader.fixed(source);
// output
var buffer: [15]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
var rope: Rope = try .init(alloc);
try rope.read(&reader);
// Act
const ropeSlice = rope.slice(10, 25);
// Assert
try writer.print("{f}", .{ropeSlice});
try std.testing.expectEqualStrings("brown fox\njumps", &buffer);
rope.deinit();
try std.testing.expectEqual(std.heap.Check.ok, gpa.deinit());
}
test sliceRows {
// Arrange
var gpa = std.testing.allocator_instance;
const alloc = gpa.allocator();
// input
const source = "The quick brown fox\njumps\nover\nthe\nlazy\ndog.";
var reader = std.Io.Reader.fixed(source);
// output
var buffer: [10]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
var rope: Rope = try .init(alloc);
try rope.read(&reader);
// Act
const ropeSlice = rope.sliceRows(1, 2);
// Assert
try writer.print("{f}", .{ropeSlice});
try std.testing.expectEqualStrings("jumps\nover", &buffer);
rope.deinit();
try std.testing.expectEqual(std.heap.Check.ok, gpa.deinit());
}
test sliceLine {
// Arrange
var gpa = std.testing.allocator_instance;
const alloc = gpa.allocator();
// input
const source = "The quick brown fox\njumps\nover\nthe\nlazy\ndog.";
var reader = std.Io.Reader.fixed(source);
// output
var buffer: [5]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
var rope: Rope = try .init(alloc);
try rope.read(&reader);
// Act
const ropeSlice = rope.sliceLine(1);
// Assert
try writer.print("{f}", .{ropeSlice});
try std.testing.expectEqualStrings("jumps", &buffer);
rope.deinit();
try std.testing.expectEqual(std.heap.Check.ok, gpa.deinit());
}
test pointToOffset {
var gpa = std.testing.allocator_instance;
const alloc = gpa.allocator();
// input
// 0123456789012345678 901234 5678
// 0:0 1:0 2:0
// 2:2
const source = "The quick brown fox\njumps\nover\nthe\nlazy\ndog.";
// -----------------------------------------^ row 2 (26 chars)
// -------------------------------------------^ row 2, column 2 (28 chars)
var reader = std.Io.Reader.fixed(source);
var rope: Rope = try .init(alloc);
try rope.read(&reader);
const offset = rope.pointToOffset(.{ .column = 2, .row = 2 });
try std.testing.expectEqual(28, offset);
}
test offsetToPoint {
var gpa = std.testing.allocator_instance;
const alloc = gpa.allocator();
// input
// 0123456789012345678 901234 5678
// 0:0 1:0 2:0
// 2:2
const source = "The quick brown fox\njumps\nover\nthe\nlazy\ndog.";
// -----------------------------------------^ row 2 (26 chars)
// -------------------------------------------^ row 2, column 2 (28 chars)
var reader = std.Io.Reader.fixed(source);
var rope: Rope = try .init(alloc);
try rope.read(&reader);
const point = rope.offsetToPoint(28);
try std.testing.expectEqual(2, point.column);
try std.testing.expectEqual(2, point.row);
}
test split {
var gpa = std.testing.allocator_instance;
const alloc = gpa.allocator();
// input
// 0123456789012345678 901234 5
const source = "The quick brown fox\njumps\nover\nthe\nlazy\ndog.";
var reader = std.Io.Reader.fixed(source);
// output
var bufferLeft: [26]u8 = undefined;
var writerLeft = std.Io.Writer.fixed(&bufferLeft);
var bufferRight: [18]u8 = undefined;
var writerRight = std.Io.Writer.fixed(&bufferRight);
var rope: Rope = try .init(alloc);
try rope.read(&reader);
// Act
var right = (try rope.split(26)).?;
// Assert
try writerLeft.print("{f}", .{rope});
try std.testing.expectEqualStrings("The quick brown fox\njumps\n", &bufferLeft);
try writerRight.print("{f}", .{right});
try std.testing.expectEqualStrings("over\nthe\nlazy\ndog.", &bufferRight);
rope.deinit();
right.deinit();
try std.testing.expectEqual(std.heap.Check.ok, gpa.deinit());
}
test join {
// Arrange
var gpa = std.testing.allocator_instance;
const alloc = gpa.allocator();
// input
const leftSource = "The quick brown fox\njumps\n";
const rightSource = "over\nthe\nlazy\ndog.";
var reader: std.Io.Reader = .fixed(leftSource);
var leftRope: Rope = try .init(alloc);
try leftRope.read(&reader);
reader = .fixed(rightSource);
var rightRope: Rope = try .init(alloc);
try rightRope.read(&reader);
// output
var buffer: [44]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
// Act
try leftRope.join(&rightRope);
// Assert
try writer.print("{f}", .{leftRope});
try std.testing.expectEqualStrings("The quick brown fox\njumps\nover\nthe\nlazy\ndog.", &buffer);
leftRope.deinit();
try std.testing.expectEqual(std.heap.Check.ok, gpa.deinit());
}
test insert {
var gpa = std.testing.allocator_instance;
const alloc = gpa.allocator();
// input
// 0123456789012345678 901234 5
const source = "The quick brown fox\njumps\nover\nthe\nlazy\ndog.";
var reader = std.Io.Reader.fixed(source);
var buffer: [49]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
var rope: Rope = try .init(alloc);
try rope.read(&reader);
try rope.insert(10, "cool ");
try writer.print("{f}", .{rope});
try std.testing.expectEqualStrings("The quick cool brown fox\njumps\nover\nthe\nlazy\ndog.", &buffer);
rope.deinit();
try std.testing.expectEqual(std.heap.Check.ok, gpa.deinit());
}
test remove {
// Arrange
var gpa = std.testing.allocator_instance;
const alloc = gpa.allocator();
// input
// 01234567890123456789
const source = "The quick brown fox\njumps\nover\nthe\nlazy\ndog.";
var reader = std.Io.Reader.fixed(source);
// output
var buffer: [43]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
var rope: Rope = try .init(alloc);
try rope.read(&reader);
// Act
try rope.remove(19);
// Assert
try writer.print("{f}", .{rope});
try std.testing.expectEqualStrings("The quick brown foxjumps\nover\nthe\nlazy\ndog.", &buffer);
rope.deinit();
try std.testing.expectEqual(std.heap.Check.ok, gpa.deinit());
}
test "post edit" {
// Arrange
var gpa = std.testing.allocator_instance;
const alloc = gpa.allocator();
// input
// 01234567890123456789
const source = "The quick brown fox\njumps\nover\nthe\nlazy\ndog.";
var reader = std.Io.Reader.fixed(source);
// output
var buffer: [42]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
var rope: Rope = try .init(alloc);
try rope.read(&reader);
// Act
try rope.remove(Chunk.MAX_BASE);
rope.tree.checkInvariants(void{});
try rope.remove(Chunk.MAX_BASE);
rope.tree.checkInvariants(void{});
const ropeSlice = rope.sliceRows(0, 6);
// Assert
try std.testing.expectEqual(5, rope.sliceLine(1).len());
try writer.print("{f}", .{ropeSlice});
try std.testing.expectEqualStrings("The quick brown x\njumps\nover\nthe\nlazy\ndog.", &buffer);
rope.deinit();
try std.testing.expectEqual(std.heap.Check.ok, gpa.deinit());
}
fn debugSummary(tree: *const Tree, indent: usize) void {
if (indent == 0) {
std.debug.print("summary:\n", .{});
}
for (0..indent) |_| {
std.debug.print(" ", .{});
}
const sum = tree.summary();
std.debug.print("len: {d}, lines: {any}, type: ", .{ sum.len, sum.lines });
switch (tree.*) {
.internal => |*internal| {
std.debug.print("internal\n", .{});
for (internal.childTrees.slice()) |t| {
debugSummary(t, indent + 1);
}
},
.leaf => |*leaf| {
std.debug.print("leaf\n", .{});
for (0..indent) |_| {
std.debug.print(" ", .{});
}
var n: usize = 0;
for (leaf.items.slice()) |item| {
n += item.text.len;
std.debug.print("'", .{});
std.debug.print("{s}", .{item.text.slice()});
std.debug.print("' ({d} bytes, {b:0>16} newlines bitmap slice) ", .{ item.text.len, item.asSlice().sliceBitmap(item.newlines) });
}
std.debug.print(" ({d} bytes total)\n", .{n});
},
}
}
+100
View File
@@ -0,0 +1,100 @@
const std = @import("std");
const iterator = @import("../iter.zig");
pub const Error = error{
OutOfRange,
};
/// An array vector is a fixed-capacity array.
pub fn ArrayVec(comptime T: type, comptime capacity: usize) type {
return struct {
const Private = struct { buffer: [capacity]T = undefined };
len: usize = 0,
private: Private = .{},
const Self = @This();
pub const init: Self = .{};
pub inline fn isFull(self: *const Self) bool {
return self.len >= capacity;
}
/// Pushes the item to the end of the array and increments the length.
pub fn push(self: *Self, item: T) Error!void {
if (self.isFull()) return Error.OutOfRange;
self.private.buffer[self.len] = item;
self.len += 1;
}
/// Inserts the item at the given index, moving all the following
/// along the vec to make space.
pub fn insert(self: *Self, index: usize, item: T) Error!void {
if (self.isFull()) return Error.OutOfRange;
@memcpy(
self.private.buffer[index + 1 .. self.len + 1],
self.private.buffer[index..self.len],
);
self.private.buffer[index] = item;
}
/// Pushes the items to the end of the array and updates the length.
pub fn extend(self: *Self, items: []const T) Error!void {
if (self.len + items.len > capacity) return Error.OutOfRange;
@memcpy(self.private.buffer[self.len .. self.len + items.len], items);
self.len += items.len;
}
pub fn from(items: []const T) Error!Self {
var result: Self = .init;
try result.extend(items);
return result;
}
/// Returns a copy of the last element in the array and decrements the length.
pub fn pop(self: *Self) ?T {
if (self.len == 0) return null;
self.len -= 1;
return self.private.buffer[self.len];
}
pub fn iter(self: *const Self) iterator.Iter(iterator.SliceIter(T)) {
return .{ .impl = .{ .buffer = self.slice() } };
}
/// Returns a copy of the element at the given index.
pub fn get(self: *const Self, index: usize) ?T {
if (self.isFull()) return null;
return self.private.buffer[index];
}
/// Returns a slice of the underlying array.
pub fn slice(self: *const Self) []const T {
return self.private.buffer[0..self.len];
}
/// Returns a mutable slice of the underlying array.
pub fn sliceMut(self: *Self) []T {
return self.private.buffer[0..self.len];
}
/// Returns a copy of the last element of the array.
pub fn last(self: *const Self) ?T {
if (self.len == 0) return null;
return self.private.buffer[self.len - 1];
}
/// Returns a mutable reference of the last element of the array.
pub fn lastMut(self: *Self) ?*T {
if (self.len == 0) return null;
return &self.private.buffer[self.len - 1];
}
/// Removes all items from the array.
pub fn clear(self: *Self) void {
self.len = 0;
}
};
}
+271
View File
@@ -0,0 +1,271 @@
//! A chunk is a small set of bytes of a text string.
const builtin = @import("builtin");
const std = @import("std");
const iters = @import("../iter.zig");
const arrayVec = @import("array-vec.zig");
const Point = @import("point.zig");
const ArrayVec = arrayVec.ArrayVec;
const Chunk = @This();
const Bitmap = if (builtin.is_test) u16 else u128;
const Bitsize = std.math.Log2Int(Bitmap);
// const Bitmap = u128;
pub const MAX_BASE = @bitSizeOf(Bitmap);
pub const MIN_BASE = MAX_BASE / 2;
/// Each bit indicates the start of a UTF-8 character.
chars: Bitmap,
/// The sum of the bits if the number of Utf16 code units it would take to represent the text.
charsUtf16: Bitmap,
/// Each bit indicates a `\n` character.
newlines: Bitmap,
/// Each bit indicates a `\t` character.
tabs: Bitmap,
/// The string of bytes.
text: ArrayVec(u8, MAX_BASE),
/// A summary of a string of text.
pub const Summary = struct {
/// The length in bytes.
len: usize,
/// A point one character to the right of the last character's position.
///
/// So the `row` field is the number of lines and `column` is the length of the last line.
lines: Point,
pub const Context = void;
/// Creates a summary matching that of `""`.
pub fn zero(cx: Context) Summary {
return .{ .len = 0, .lines = .zero(cx) };
}
/// Returns the addition of each field within the summaries.
pub fn add(self: *const Summary, other: *const Summary, _: Context) Summary {
return .{
.len = self.len + other.len,
.lines = self.lines.add(&other.lines),
};
}
/// Returns the subtraction of the each field within the summaries.
pub fn sub(self: *const Summary, other: *const Summary, _: Context) Summary {
return .{
.len = self.len - other.len,
.lines = self.lines.sub(&other.lines),
};
}
pub fn eq(self: *const Summary, other: *const Summary, _: Context) bool {
return self.len == other.len and self.lines.cmp(&other.lines) == .equal;
}
};
pub fn init(text: []const u8) arrayVec.Error!Chunk {
const CHUNK_SIZE = 8;
var charsBytes = std.mem.zeroes([MAX_BASE / CHUNK_SIZE]u8);
var newlinesBytes = std.mem.zeroes([MAX_BASE / CHUNK_SIZE]u8);
var tabsBytes = std.mem.zeroes([MAX_BASE / CHUNK_SIZE]u8);
var charsUtf16Bytes = std.mem.zeroes([MAX_BASE / CHUNK_SIZE]u8);
var chunkIx: u8 = 0;
var bytes = text;
while (bytes.len > 0) {
const chunk = bytes[0..@min(bytes.len, CHUNK_SIZE)];
bytes = bytes[@min(bytes.len, CHUNK_SIZE)..];
var chars: u8 = 0;
var newlines: u8 = 0;
var tabs: u8 = 0;
var charsUtf16: u8 = 0;
for (0.., chunk) |i, b| {
const ix: u3 = @intCast(i);
const char: u8 = @intCast(@intFromBool(isUtf8CharBoundary(b)));
chars |= char << ix;
const newline: u8 = @intCast(@intFromBool(b == '\n'));
newlines |= newline << ix;
const tab: u8 = @intCast(@intFromBool(b == '\t'));
tabs |= tab << ix;
const charUtf16: u8 = @intCast(@intFromBool(b >= 240));
charsUtf16 |= charUtf16 << ix;
}
charsBytes[chunkIx] = chars;
newlinesBytes[chunkIx] = newlines;
tabsBytes[chunkIx] = tabs;
charsUtf16Bytes[chunkIx] = charsUtf16;
chunkIx += 1;
}
const chars = std.mem.readInt(Bitmap, &charsBytes, .little);
return .{
.text = try .from(text),
.chars = chars,
.charsUtf16 = (std.mem.readInt(Bitmap, &charsUtf16Bytes, .little) >> 1) | chars,
.newlines = (std.mem.readInt(Bitmap, &newlinesBytes, .little)),
.tabs = (std.mem.readInt(Bitmap, &tabsBytes, .little)),
};
}
pub fn format(self: *const Chunk, writer: *std.Io.Writer) std.Io.Writer.Error!void {
try std.zig.stringEscape(self.text.slice(), writer);
}
/// Returns the summary of the chunk's contents.
pub fn summary(self: *const Chunk, _: Summary.Context) Summary {
return self.asSlice().textSummary();
}
/// Returns the contents of the chunk as bytes.
pub fn slice(self: *const Chunk) []const u8 {
return self.text.slice();
}
pub fn iter(self: *const Chunk) iters.Iter(iters.SliceIter(u8)) {
return self.text.iter();
}
pub const Slice = struct {
text: []const u8,
inner: *const Chunk,
/// Returns a bitmap with a popcount and leading zeroes outside of the text range
/// omitted. Ensures correct `@popcout` and `@clz` after the chunk may have been shortened.
/// Invalidates `@ctz`.
pub fn sliceBitmap(self: *const Slice, bitmap: Bitmap) Bitmap {
return bitmap << @as(std.math.Log2Int(Bitmap), @intCast(@max(MAX_BASE, self.text.len) - self.text.len));
}
pub fn textSummary(self: *const Slice) Summary {
return .{
.len = self.len(),
.lines = self.lines(),
};
}
/// Returns the point representing the last row and column of the chunk.
pub fn lines(self: *const Slice) Point {
const newlines = self.sliceBitmap(self.inner.newlines);
// The row is the number of positive bits.
const row = @popCount(newlines);
// The column is the number of zero bits from the start of the chunk (NOTE: The bitmap is little endian).
// If `@popCount` is zero, then `newlines` is zero and `@clz` will need to be clamped to `self.text.len`.
const column = @min(@clz(newlines), self.text.len);
return .{ .row = row, .column = column };
}
pub fn len(self: *const Slice) usize {
return self.text.len;
}
pub fn pointToOffset(self: *const Slice, point: Point) usize {
if (point.row > self.lines().row) {
return self.len();
}
const rowOffsetRange = self.offsetRangeForRow(point.row);
if (point.column > (rowOffsetRange.end - rowOffsetRange.start)) {
return rowOffsetRange.end;
} else {
return rowOffsetRange.start + point.column;
}
}
pub fn offsetToPoint(self: *const Slice, offset: usize) Point {
const mask = (@as(Bitmap, 1) <<| offset) -% 1;
const newlines = self.inner.newlines;
const row = @popCount(newlines & mask);
const newline = @bitSizeOf(Bitmap) - @clz(newlines & mask);
const column = offset - newline;
return .{ .row = row, .column = column };
}
pub fn offsetRangeForRow(self: *const Slice, row: usize) struct { start: usize, end: usize } {
const newlines = self.inner.newlines;
var rowStart: usize = 0;
if (row > 0) {
const newlinesWide: u128 = if (builtin.is_test) @intCast(newlines) else newlines;
rowStart = nthSetBit(newlinesWide, row) + 1;
}
var rowLen: usize = 0;
if (rowStart != MAX_BASE) {
rowLen = @min(@ctz(newlines >> @truncate(rowStart)), self.text.len - rowStart);
}
return .{ .start = rowStart, .end = rowStart + rowLen };
}
};
pub fn asSlice(self: *const Chunk) Slice {
return .{
.text = self.text.slice(),
.inner = self,
};
}
fn nthSetBit(v: u128, n: usize) usize {
const low: u64 = @truncate(v);
const high: u64 = @truncate(v >> 64);
const lowCount = @popCount(low);
if (n > lowCount) {
return 64 + nthSetBitU64(high, n - lowCount);
} else {
return nthSetBitU64(low, n);
}
}
fn nthSetBitU64(vBase: u64, nBase: u64) u64 {
// https://zed.dev/blog/zed-decoded-rope-optimizations-part-1
const v = @bitReverse(vBase);
var n = nBase;
var s: u64 = 64;
const U64_MAX = std.math.maxInt(u64);
// Parallel bit count intermediates
const a = v - ((v >> 1) & (U64_MAX / 3));
const b = (a & (U64_MAX / 5)) + ((a >> 2) & (U64_MAX / 5));
const c = (b + (b >> 4)) & (U64_MAX / 0x11);
const d = (c + (c >> 8)) & (U64_MAX / 0x101);
// Branchless select
var t = (d >> 32) + (d >> 48);
s -= ((t -% n) & 256) >> 3;
n -= t & ((t -% n) >> 8);
t = (d >> @intCast(s - 16)) & 0xff;
s -= ((t -% n) & 256) >> 4;
n -= t & ((t -% n) >> 8);
t = (c >> @intCast(s - 8)) & 0xf;
s -= ((t -% n) & 256) >> 5;
n -= t & ((t -% n) >> 8);
t = (b >> @intCast(s - 4)) & 0x7;
s -= ((t -% n) & 256) >> 6;
n -= t & ((t -% n) >> 8);
t = (a >> @intCast(s - 2)) & 0x3;
s -= ((t -% n) & 256) >> 7;
n -= t & ((t -% n) >> 8);
t = (v >> @intCast(s - 1)) & 0x1;
s -= ((t -% n) & 256) >> 8;
return 65 - s - 1;
}
fn isUtf8CharBoundary(@"u8": u8) bool {
// This is bit magic equivalent to: b < 128 || b >= 192
const @"i8": i8 = @intCast(@"u8");
return @"i8" >= -0x40;
}
+84
View File
@@ -0,0 +1,84 @@
const Ordering = @import("../ordering.zig").Ordering;
const Chunk = @import("chunk.zig");
const sumTree = @import("sum-tree.zig");
const Point = @This();
row: usize,
column: usize,
pub const Summary = Chunk.Summary;
pub const Context = Summary.Context;
pub fn init(row: usize, column: usize) Point {
return .{ .row = row, .column = column };
}
pub fn clone(self: *const Point) Point {
return .{ .row = self.row, .column = self.column };
}
pub fn add(self: *const Point, other: *const Point) Point {
if (other.row == 0) {
return .{ .row = self.row, .column = self.column + other.column };
} else {
return .{ .row = self.row + other.row, .column = other.column };
}
}
pub fn sub(self: *const Point, other: *const Point) Point {
if (self.row == other.row) {
return .{ .row = 0, .column = self.column - other.column };
} else {
return .{ .row = self.row - other.row, .column = self.column };
}
}
pub fn cmp(self: *const Point, other: *const Point) Ordering {
return switch (Ordering.cmp(self.row, other.row)) {
.equal => Ordering.cmp(self.column, other.column),
else => |ordering| ordering,
};
}
pub const USize = struct {
inner: usize = 0,
pub const Summary = Chunk.Summary;
pub const Context = void;
pub fn zero(_: USize.Context) USize {
return .{};
}
pub fn clone(self: *const USize) USize {
return .{ .inner = self.inner };
}
pub fn addSummary(self: *USize, sum: *const USize.Summary, _: USize.Context) void {
self.inner += sum.len;
}
pub fn cmp(self: *const USize, other: *const USize) Ordering {
return Ordering.cmp(self.inner, other.inner);
}
pub fn cmpSeekTarget(self: *const USize, cursorLocation: anytype, _: USize.Context) Ordering {
return self.cmp(&cursorLocation.d1);
}
};
pub fn cmpSeekTarget(
self: *const Point,
cursorLocation: *const sumTree.Dimensions(Point, USize, null),
_: Summary.Context,
) Ordering {
return self.cmp(&cursorLocation.d1);
}
pub fn zero(_: Summary.Context) Point {
return .{ .row = 0, .column = 0 };
}
pub fn addSummary(self: *Point, summary: *const Summary, _: Summary.Context) void {
self.* = self.add(&summary.lines);
}
+396
View File
@@ -0,0 +1,396 @@
const std = @import("std");
const builtin = @import("builtin");
const iters = @import("../iter.zig");
const Ordering = @import("../ordering.zig").Ordering;
const arrayVec = @import("array-vec.zig");
const ArrayVec = arrayVec.ArrayVec;
pub const TREE_BASE = 6;
pub const TREE_MAX = TREE_BASE * 2;
pub const Error = error{
EmptyInternal,
};
pub const Bias = enum {
left,
right,
pub const default: Bias = .left;
};
/// A visitor is a type which can travel across the tree in a recursive (i.e. depth-first) fashion.
///
/// An error can provided as a type of "break" or as a legitimate error to prematurely end the
/// visitor.
pub fn Visitor(comptime T: type, comptime C: type, comptime E: ?type) type {
const R = if (E) |_E| _E!void else void;
return struct {
/// A function that will be called with each internal node of the sum-tree.
visitInternal: ?*const fn (*const SumTree(T).Internal, C) R = null,
/// A function that will be called with each leaf node of the sum-tree.
visitLeaf: ?*const fn (*const SumTree(T).Leaf, C) R = null,
const Self = @This();
/// Initiates the visitor against the node and all it's descendants.
pub fn visit(self: *const Self, node: *const SumTree(T), context: C) R {
return self.visitDirection(false, node, context);
}
pub fn visitReverse(self: *const Self, node: *const SumTree(T), context: C) R {
return self.visitDirection(true, node, context);
}
pub fn visitDirection(self: *const Self, comptime reverse: bool, node: *const SumTree(T), context: C) R {
switch (node.*) {
.internal => |*internal| {
if (self.visitInternal) |f| {
if (E) |_| {
try f(internal, context);
} else {
f(internal, context);
}
}
const children = internal.childTrees.slice();
for (0..children.len) |i| {
const child = if (reverse) children[children.len - i - 1] else children[i];
if (E) |_| {
try self.visit(child, context);
} else {
self.visit(child, context);
}
}
},
.leaf => |*leaf| {
if (self.visitLeaf) |f| {
if (E) |_| {
try f(leaf, context);
} else {
f(leaf, context);
}
}
},
}
}
};
}
pub fn Dimensions(comptime D1: type, comptime D2: type, comptime D3: ?type) type {
return struct {
d1: D1,
d2: D2,
d3: if (D3) |d| d else void,
const Self = @This();
pub fn zero(cx: D1.Context) Self {
return .{ .d1 = .zero(cx), .d2 = .zero(cx), .d3 = if (D3) |_| .zero(cx) else void{} };
}
pub fn clone(self: *const Self) Self {
return .{ .d1 = self.d1.clone(), .d2 = self.d2.clone(), .d3 = if (D3) |_| self.d3.clone() else void{} };
}
pub fn withAddedSummary(self: Self, summary: *const D1.Summary, cx: D1.Summary.Context) Self {
var result = self;
result.addSummary(summary, cx);
return result;
}
pub fn addSummary(self: *Self, summary: *const D1.Summary, cx: D1.Summary.Context) void {
self.d1.addSummary(summary, cx);
self.d2.addSummary(summary, cx);
if (D3) |_| self.d3.addSummary(summary, cx);
}
};
}
pub fn Cursor(comptime T: type) type {
return struct {
tree: *const SumTree(T),
};
}
/// A sum-tree is a tree which contains a summary of all it's child nodes, where each child
/// node similarly contains a summary of it's children.
///
/// This provides a data structure that is efficient for navigation by binary searching against
/// the summary's heuristics.
pub fn SumTree(comptime T: type) type {
return union(enum) {
pub const Internal = struct {
height: usize,
summary: T.Summary,
childSummaries: ArrayVec(T.Summary, TREE_MAX),
childTrees: ArrayVec(*SumTree(T), TREE_MAX),
};
pub const Leaf = struct {
summary: T.Summary,
itemSummaries: ArrayVec(T.Summary, TREE_MAX),
items: ArrayVec(T, TREE_MAX),
pub fn iter(self: *const Leaf) iters.Iter(iters.SliceIter(T)) {
return self.items.iter();
}
};
/// A node that contains child nodes.
internal: Internal,
/// A node that contains data.
leaf: Leaf,
const Self = @This();
/// Returns a visitor type with this sum-tree's data.
pub fn ThisVisitor(comptime C: type, comptime E: ?type) type {
return Visitor(T, C, E);
}
pub fn init(gpa: std.mem.Allocator, context: T.Summary.Context) std.mem.Allocator.Error!*Self {
const self = try gpa.create(Self);
self.* = .{ .leaf = .{
.summary = .zero(context),
.itemSummaries = .init,
.items = .init,
} };
return self;
}
pub fn deinit(self: *Self, gpa: std.mem.Allocator) void {
switch (self.*) {
.internal => |internal| {
for (internal.childTrees.slice()) |child| {
child.deinit(gpa);
}
},
.leaf => {},
}
gpa.destroy(self);
}
/// Returns a summary of the node and any descendants.
pub fn summary(self: *const Self) T.Summary {
return switch (self.*) {
.internal => |internal| internal.summary,
.leaf => |leaf| leaf.summary,
};
}
/// Returns the number of generations withing the current node.
pub fn height(self: *const Self) usize {
return switch (self.*) {
.internal => |internal| internal.height,
.leaf => 0,
};
}
pub fn cursor(self: *const Self) Cursor(T) {
return .{ .tree = self };
}
pub fn FindResult(comptime D: type) type {
return struct { start: D, end: D, item: ?*const T };
}
pub fn find(
self: *const Self,
comptime D: type,
comptime Target: type,
cx: T.Summary.Context,
target: *const Target,
bias: Bias,
) FindResult(D) {
const treeEnd = D.zero(cx).withAddedSummary(&self.summary(), cx);
const comparison = target.cmpSeekTarget(&treeEnd, cx);
const isEnd = switch (comparison) {
.greater => true,
.equal => bias == .right,
.less => false,
};
if (isEnd) {
return .{ .start = treeEnd, .end = treeEnd, .item = null };
}
var position = D.zero(cx);
var current = self;
outer: while (true) {
switch (current.*) {
.internal => |*internal| {
const childSummaries = internal.childSummaries.slice();
const childTrees = internal.childTrees.slice();
for (childSummaries, childTrees) |childSummary, childTree| {
const childEnd = position.withAddedSummary(&childSummary, cx);
const cmp = target.cmpSeekTarget(&childEnd, cx);
const targetInChild = cmp == .less or (cmp == .equal and bias == .left);
if (targetInChild) {
current = childTree;
continue :outer;
}
position = childEnd;
}
return .{ .start = position, .end = position, .item = null };
},
.leaf => |*leaf| {
const itemSummaries = leaf.itemSummaries.slice();
const items = leaf.items.slice();
for (itemSummaries, items) |itemSummary, *item| {
const itemEnd = position.withAddedSummary(&itemSummary, cx);
const cmp = target.cmpSeekTarget(&itemEnd, cx);
const itemFound = cmp == .less or (cmp == .equal and bias == .left);
if (itemFound) {
return .{ .start = position, .end = itemEnd, .item = item };
}
position = itemEnd;
}
return .{ .start = position, .end = position, .item = null };
},
}
}
}
/// Inserts the item to the last possible position within the tree.
///
/// If the item won't fit into the current tree, it will return a
/// dangling node for the caller to use in rebalancing the tree.
pub fn push(
self: *Self,
gpa: std.mem.Allocator,
item: T,
context: T.Summary.Context,
) (arrayVec.Error || std.mem.Allocator.Error || Error)!?*Self {
switch (self.*) {
.leaf => |*leaf| {
const itemSummary = item.summary(context);
// If there's room, add it
if (leaf.items.len < TREE_MAX) {
try leaf.items.push(item);
try leaf.itemSummaries.push(itemSummary);
leaf.summary = leaf.summary.add(&itemSummary, context);
return null;
}
// Otherwise, split it
const newLeaf = try gpa.create(Self);
newLeaf.* = .{ .leaf = .{
.summary = T.Summary.zero(context),
.items = .init,
.itemSummaries = .init,
} };
try newLeaf.leaf.items.push(item);
try newLeaf.leaf.itemSummaries.push(itemSummary);
newLeaf.leaf.summary = newLeaf.leaf.summary.add(&itemSummary, context);
// Return leaf to be handled by parent
self.checkInvariants(context);
return newLeaf;
},
.internal => |*internal| {
const lastChild = internal.childTrees.lastMut() orelse return Error.EmptyInternal;
// Push and handle split
if (try lastChild.*.push(gpa, item, context)) |newChild| {
// If child can fit into this node, push it
if (internal.childTrees.len < TREE_MAX) {
try internal.childTrees.push(newChild);
try internal.childSummaries.push(newChild.summary());
internal.summary = internal.summary.add(&newChild.summary(), context);
self.checkInvariants(context);
return null;
}
// Otherwise, create a new node and propogate to parent
const newInternal = try gpa.create(Self);
newInternal.* = .{ .internal = .{
.height = internal.height,
.summary = newChild.summary(),
.childSummaries = .init,
.childTrees = .init,
} };
try newInternal.internal.childTrees.push(newChild);
try newInternal.internal.childSummaries.push(newChild.summary());
self.checkInvariants(context);
return newInternal;
} else {
// Update our summary to match updated child
const lastChildSummary = lastChild.*.summary();
const internalLastChildSummary = internal.childSummaries.lastMut().?;
const difference = lastChildSummary.sub(internalLastChildSummary, context);
internalLastChildSummary.* = lastChildSummary;
internal.summary = internal.summary.add(&difference, context);
self.checkInvariants(context);
return null;
}
},
}
}
const CheckInvariantsContext = struct {
cx: T.Summary.Context,
pub fn visitInternal(internal: *const Internal, cx: *@This()) void {
var computed = T.Summary.zero(cx.cx);
for (internal.childSummaries.slice()) |childSummary| {
computed = computed.add(&childSummary, cx.cx);
}
if (!computed.eq(&internal.summary, cx.cx)) {
std.debug.panic(
\\Incorrect internal summary!
\\ Current: {any}
\\ Computed: {any}
\\
, .{ internal.summary, computed });
}
for (internal.childTrees.slice(), internal.childSummaries.slice(), 0..) |child, childSummary, i| {
const sum = child.summary();
if (!sum.eq(&childSummary, cx.cx)) {
std.debug.panic(
\\Incorrect child summary at {d}!
\\ Current: {any}
\\ Computed: {any}
\\
, .{ i, childSummary, sum });
}
}
}
pub fn visitLeaf(leaf: *const Leaf, cx: *@This()) void {
var computed = T.Summary.zero(cx.cx);
for (leaf.itemSummaries.slice()) |itemSummary| {
computed = computed.add(&itemSummary, cx.cx);
}
if (!computed.eq(&leaf.summary, cx.cx)) {
std.debug.panic(
\\Incorrect leaf summary!
\\ Current: {any}
\\ Computed: {any}
\\
, .{ leaf.summary, computed });
}
for (leaf.items.slice(), leaf.itemSummaries.slice(), 0..) |item, itemSummary, i| {
const sum = item.summary(cx.cx);
if (!sum.eq(&itemSummary, cx.cx)) {
std.debug.panic(
\\Incorrect item summary at {d}!
\\ Current: {any}
\\ Computed: {any}
\\
, .{ i, leaf.summary, computed });
}
}
}
};
pub fn checkInvariants(self: *const Self, context: T.Summary.Context) void {
comptime if (builtin.mode != .Debug) return;
var cx: CheckInvariantsContext = .{ .cx = context };
const visitor: ThisVisitor(*CheckInvariantsContext, null) = .{
.visitInternal = CheckInvariantsContext.visitInternal,
.visitLeaf = CheckInvariantsContext.visitLeaf,
};
visitor.visit(self, &cx);
}
};
}