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
+3
View File
@@ -0,0 +1,3 @@
zig-out/
zig-pkg/
.zig-cache/
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2026 Yves Biener
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+26
View File
@@ -0,0 +1,26 @@
# Rope
Implementation of a [rope datastructure](https://en.wikipedia.org/wiki/Rope_(data_structure)) in zig.
Based on the MIT-Licensed implementation of noahbald: https://github.com/noahbald/noe
Please check out his excelent [write up](https://www.noahwbaldwin.me/blogs/writing-an-editor) about the implementation of an editor in zig.
Another great resource for ropes has been created by [Zed](https://zed.dev/blog/zed-decoded-rope-sumtree), it is worth reading to further understand the implementation details of this implementation.
## Usage
To add or update `rope` as a dependency in your project run the following command:
```sh
zig fetch --save git+https://gitea.yves-biener.de/yves-biener/rope
```
Add the dependency to your module as follows in your *build.zig*:
```zig
const zterm: *Dependency = b.dependency("rope", .{
.target = target,
.optimize = optimize,
});
```
+21
View File
@@ -0,0 +1,21 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const mod = b.addModule("rope", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
const mod_tests = b.addTest(.{
.root_module = mod,
});
const run_mod_tests = b.addRunArtifact(mod_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
}
+14
View File
@@ -0,0 +1,14 @@
.{
.name = .rope,
.version = "0.0.0",
.fingerprint = 0xb11ed7373b0875fc, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"LICENSE",
"README.md",
},
}
+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);
}
};
}