diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..895f4e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +zig-out/ +zig-pkg/ +.zig-cache/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a3c343e --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..47356e4 --- /dev/null +++ b/README.md @@ -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, +}); +``` diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..779079a --- /dev/null +++ b/build.zig @@ -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); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..08a2509 --- /dev/null +++ b/build.zig.zon @@ -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", + }, +} diff --git a/src/iter.zig b/src/iter.zig new file mode 100644 index 0000000..871c454 --- /dev/null +++ b/src/iter.zig @@ -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; + } + }; +} diff --git a/src/ordering.zig b/src/ordering.zig new file mode 100644 index 0000000..481e53c --- /dev/null +++ b/src/ordering.zig @@ -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); + } +}; diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..cbecc88 --- /dev/null +++ b/src/root.zig @@ -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}); + }, + } +} diff --git a/src/rope/array-vec.zig b/src/rope/array-vec.zig new file mode 100644 index 0000000..95a5a16 --- /dev/null +++ b/src/rope/array-vec.zig @@ -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; + } + }; +} diff --git a/src/rope/chunk.zig b/src/rope/chunk.zig new file mode 100644 index 0000000..101226f --- /dev/null +++ b/src/rope/chunk.zig @@ -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; +} diff --git a/src/rope/point.zig b/src/rope/point.zig new file mode 100644 index 0000000..ea68ff2 --- /dev/null +++ b/src/rope/point.zig @@ -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); +} diff --git a/src/rope/sum-tree.zig b/src/rope/sum-tree.zig new file mode 100644 index 0000000..d6d0026 --- /dev/null +++ b/src/rope/sum-tree.zig @@ -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); + } + }; +}