Packages

A terminal UI framework for Elixir with a high-performance Zig NIF backend.

Current section

Files

Jump to
elixir_opentui zig opentui tests rope_test.zig
Raw

zig/opentui/tests/rope_test.zig

const std = @import("std");
const rope_mod = @import("../rope.zig");
const SimpleItem = struct {
value: u32,
pub fn empty() SimpleItem {
return .{ .value = 0 };
}
pub fn is_empty(self: *const SimpleItem) bool {
return self.value == 0;
}
};
const ItemWithMetrics = struct {
value: u32,
size: u32,
pub const Metrics = struct {
total_size: u32 = 0,
pub fn add(self: *Metrics, other: Metrics) void {
self.total_size += other.total_size;
}
};
pub fn measure(self: *const ItemWithMetrics) Metrics {
return .{ .total_size = self.size };
}
pub fn empty() ItemWithMetrics {
return .{ .value = 0, .size = 0 };
}
pub fn is_empty(self: *const ItemWithMetrics) bool {
return self.value == 0 and self.size == 0;
}
};
//===== Basic Rope Tests =====
test "Rope - can initialize with arena allocator" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.init(arena.allocator());
try std.testing.expectEqual(@as(u32, 0), rope.count()); // Sentinel filtered
}
test "Rope - from_item creates single item rope" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 42 });
try std.testing.expectEqual(@as(u32, 1), rope.count());
const item = rope.get(0);
try std.testing.expect(item != null);
try std.testing.expectEqual(@as(u32, 42), item.?.value);
}
test "Rope - from_slice creates rope from multiple items" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
try std.testing.expectEqual(@as(u32, 3), rope.count());
try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);
try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);
}
test "Rope - get with out of bounds returns null" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try std.testing.expect(rope.get(100) == null);
}
//===== Insert Tests =====
test "Rope - insert at beginning" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try rope.insert(0, .{ .value = 0 });
try std.testing.expectEqual(@as(u32, 2), rope.count());
try std.testing.expectEqual(@as(u32, 0), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 1), rope.get(1).?.value);
}
test "Rope - insert at end" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try rope.insert(1, .{ .value = 2 });
try std.testing.expectEqual(@as(u32, 2), rope.count());
try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);
}
test "Rope - multiple inserts" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.init(arena.allocator());
try rope.insert(0, .{ .value = 1 });
try rope.insert(1, .{ .value = 2 });
try rope.insert(2, .{ .value = 3 });
try std.testing.expectEqual(@as(u32, 3), rope.count()); // Sentinel filtered
}
//===== Delete Tests =====
test "Rope - delete at beginning" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
try rope.delete(0);
try std.testing.expectEqual(@as(u32, 2), rope.count());
try std.testing.expectEqual(@as(u32, 2), rope.get(0).?.value);
}
test "Rope - delete in middle" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
try rope.delete(1);
try std.testing.expectEqual(@as(u32, 2), rope.count());
try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 3), rope.get(1).?.value);
}
//===== Walk Tests =====
test "Rope - walk all items" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 10 },
.{ .value = 20 },
.{ .value = 30 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const Context = struct {
sum: u32 = 0,
fn walker(ctx: *anyopaque, data: *const SimpleItem, index: u32) RopeType.Node.WalkerResult {
_ = index;
const self = @as(*@This(), @ptrCast(@alignCast(ctx)));
self.sum += data.value;
return .{};
}
};
var ctx = Context{};
try rope.walk(&ctx, Context.walker);
try std.testing.expectEqual(@as(u32, 60), ctx.sum);
}
test "Rope - walk with early exit" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const Context = struct {
count: u32 = 0,
fn walker(ctx: *anyopaque, data: *const SimpleItem, index: u32) RopeType.Node.WalkerResult {
_ = index;
const self = @as(*@This(), @ptrCast(@alignCast(ctx)));
self.count += 1;
if (data.value == 2) {
return .{ .keep_walking = false };
}
return .{};
}
};
var ctx = Context{};
try rope.walk(&ctx, Context.walker);
try std.testing.expectEqual(@as(u32, 2), ctx.count);
}
//===== Metrics Tests =====
test "Rope - custom metrics are tracked" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(ItemWithMetrics);
const items = [_]ItemWithMetrics{
.{ .value = 1, .size = 10 },
.{ .value = 2, .size = 20 },
.{ .value = 3, .size = 30 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const metrics = rope.root.metrics();
try std.testing.expectEqual(@as(u32, 3), metrics.count);
try std.testing.expectEqual(@as(u32, 60), metrics.custom.total_size);
}
test "Rope - rebalance maintains data" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var items: [20]SimpleItem = undefined;
for (&items, 0..) |*item, i| {
item.* = .{ .value = @intCast(i) };
}
var rope = try RopeType.from_slice(arena.allocator(), &items);
try rope.rebalance(arena.allocator());
// Data should be preserved
try std.testing.expectEqual(@as(u32, 20), rope.count());
try std.testing.expectEqual(@as(u32, 0), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 10), rope.get(10).?.value);
try std.testing.expectEqual(@as(u32, 19), rope.get(19).?.value);
}
//===== Stress Tests =====
test "Rope - large number of items" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var items: [100]SimpleItem = undefined;
for (&items, 0..) |*item, i| {
item.* = .{ .value = @intCast(i) };
}
var rope = try RopeType.from_slice(arena.allocator(), &items);
try std.testing.expectEqual(@as(u32, 100), rope.count());
try std.testing.expectEqual(@as(u32, 0), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 50), rope.get(50).?.value);
try std.testing.expectEqual(@as(u32, 99), rope.get(99).?.value);
}
test "Rope - many insert operations" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.init(arena.allocator());
for (0..50) |i| {
try rope.insert(@intCast(i), .{ .value = @intCast(i) });
}
try std.testing.expectEqual(@as(u32, 50), rope.count()); // Sentinel filtered
}
//===== Nested Rope Tests (Lines→Chunks Pattern) =====
// Chunk type similar to what would be used in text buffer
const Chunk = struct {
data: []const u8,
width: u32,
pub const Metrics = struct {
total_width: u32 = 0,
total_bytes: u32 = 0,
pub fn add(self: *Metrics, other: Metrics) void {
self.total_width += other.total_width;
self.total_bytes += other.total_bytes;
}
};
pub fn measure(self: *const Chunk) Metrics {
return .{
.total_width = self.width,
.total_bytes = @intCast(self.data.len),
};
}
pub fn empty() Chunk {
return .{ .data = "", .width = 0 };
}
pub fn is_empty(self: *const Chunk) bool {
return self.data.len == 0;
}
};
// Static empty chunk rope node for Line.empty()
const empty_chunk_leaf_node = rope_mod.Rope(Chunk).Node{
.leaf = .{
.data = Chunk.empty(),
},
};
// Line type containing a rope of chunks
const Line = struct {
chunks: rope_mod.Rope(Chunk),
line_id: u32,
pub const Metrics = struct {
total_width: u32 = 0,
total_lines: u32 = 1,
pub fn add(self: *Metrics, other: Metrics) void {
self.total_width += other.total_width;
self.total_lines += other.total_lines;
}
};
pub fn measure(self: *const Line) Metrics {
const chunk_metrics = self.chunks.root.metrics();
return .{
.total_width = chunk_metrics.custom.total_width,
.total_lines = 1,
};
}
pub fn empty() Line {
// Use static empty chunk rope - safe because it's immutable
const ChunkRope = rope_mod.Rope(Chunk);
return .{
.chunks = .{
.root = &empty_chunk_leaf_node,
.allocator = undefined, // Never used for empty
.empty_leaf = &empty_chunk_leaf_node,
.marker_cache = ChunkRope.MarkerCache.init(undefined),
},
.line_id = 0,
};
}
pub fn is_empty(self: *const Line) bool {
return self.line_id == 0 and self.chunks.count() == 1;
}
};
test "Rope - replace item at index" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
try rope.replace(1, .{ .value = 20 });
try std.testing.expectEqual(@as(u32, 3), rope.count());
try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 20), rope.get(1).?.value);
try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);
}
test "Rope - append item" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try rope.append(.{ .value = 2 });
try rope.append(.{ .value = 3 });
try std.testing.expectEqual(@as(u32, 3), rope.count());
try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);
try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);
}
test "Rope - prepend item" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 3 });
try rope.prepend(.{ .value = 2 });
try rope.prepend(.{ .value = 1 });
try std.testing.expectEqual(@as(u32, 3), rope.count());
try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);
try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);
}
test "Rope - concatenate two ropes" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items1 = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
};
var rope1 = try RopeType.from_slice(arena.allocator(), &items1);
const items2 = [_]SimpleItem{
.{ .value = 3 },
.{ .value = 4 },
};
const rope2 = try RopeType.from_slice(arena.allocator(), &items2);
try rope1.concat(&rope2);
try std.testing.expectEqual(@as(u32, 4), rope1.count());
try std.testing.expectEqual(@as(u32, 1), rope1.get(0).?.value);
try std.testing.expectEqual(@as(u32, 2), rope1.get(1).?.value);
try std.testing.expectEqual(@as(u32, 3), rope1.get(2).?.value);
try std.testing.expectEqual(@as(u32, 4), rope1.get(3).?.value);
}
//===== Undo/Redo Tests =====
test "Rope - basic undo operation" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try rope.store_undo("initial");
try rope.insert(1, .{ .value = 2 });
try std.testing.expectEqual(@as(u32, 2), rope.count());
const meta = try rope.undo("before undo");
try std.testing.expectEqualStrings("initial", meta);
try std.testing.expectEqual(@as(u32, 1), rope.count());
}
test "Rope - basic redo operation" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try rope.store_undo("initial");
try rope.insert(1, .{ .value = 2 });
_ = try rope.undo("before undo");
try std.testing.expectEqual(@as(u32, 1), rope.count());
const meta = try rope.redo();
try std.testing.expectEqualStrings("before undo", meta);
try std.testing.expectEqual(@as(u32, 2), rope.count());
}
test "Rope - multiple undo/redo operations" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try rope.store_undo("state1");
try rope.append(.{ .value = 2 });
try rope.store_undo("state2");
try rope.append(.{ .value = 3 });
try rope.store_undo("state3");
try rope.append(.{ .value = 4 });
try std.testing.expectEqual(@as(u32, 4), rope.count());
_ = try rope.undo("current");
try std.testing.expectEqual(@as(u32, 3), rope.count());
_ = try rope.undo("current");
try std.testing.expectEqual(@as(u32, 2), rope.count());
_ = try rope.redo();
try std.testing.expectEqual(@as(u32, 3), rope.count());
}
test "Rope - undo/redo with delete operations" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
try rope.store_undo("before delete");
try rope.delete(1);
try std.testing.expectEqual(@as(u32, 2), rope.count());
try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 3), rope.get(1).?.value);
_ = try rope.undo("after delete");
try std.testing.expectEqual(@as(u32, 3), rope.count());
try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);
}
test "Rope - undo/redo with replace operations" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 10 });
try rope.store_undo("original");
try rope.replace(0, .{ .value = 20 });
try std.testing.expectEqual(@as(u32, 20), rope.get(0).?.value);
_ = try rope.undo("after replace");
try std.testing.expectEqual(@as(u32, 10), rope.get(0).?.value);
_ = try rope.redo();
try std.testing.expectEqual(@as(u32, 20), rope.get(0).?.value);
}
test "Rope - can_undo and can_redo" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try std.testing.expect(!rope.can_undo());
try std.testing.expect(!rope.can_redo());
try rope.store_undo("state1");
try std.testing.expect(rope.can_undo());
try std.testing.expect(!rope.can_redo());
_ = try rope.undo("current");
try std.testing.expect(!rope.can_undo()); // No more undo (only one state)
try std.testing.expect(rope.can_redo());
}
test "Rope - clear history" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try rope.store_undo("state1");
try rope.append(.{ .value = 2 });
try rope.store_undo("state2");
try std.testing.expect(rope.can_undo());
rope.clear_history();
try std.testing.expect(!rope.can_undo());
try std.testing.expect(!rope.can_redo());
}
test "Rope - undo fails when no history" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
// No history stored, undo should fail
const result = rope.undo("current");
try std.testing.expectError(error.Stop, result);
}
test "Rope - redo fails when no redo history" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
// No redo history, redo should fail
const result = rope.redo();
try std.testing.expectError(error.Stop, result);
}
test "Rope - complex undo/redo workflow" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.init(arena.allocator());
// Build up a sequence of operations
try rope.store_undo("empty");
try rope.insert(0, .{ .value = 1 });
try rope.store_undo("one item");
try rope.append(.{ .value = 2 });
try rope.store_undo("two items");
try rope.append(.{ .value = 3 });
try rope.store_undo("three items");
try rope.delete(1); // Remove middle
// State: [1, 3]
try std.testing.expectEqual(@as(u32, 2), rope.count()); // Sentinel filtered
// Undo delete
_ = try rope.undo("current");
try std.testing.expectEqual(@as(u32, 3), rope.count());
// Undo append
_ = try rope.undo("current");
try std.testing.expectEqual(@as(u32, 2), rope.count());
// Redo append
_ = try rope.redo();
try std.testing.expectEqual(@as(u32, 3), rope.count());
}
test "Rope - undo/redo with metadata tracking" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try rope.store_undo("insert operation");
try rope.append(.{ .value = 2 });
try rope.store_undo("delete operation");
try rope.delete(0);
// Undo and check metadata
const meta1 = try rope.undo("current state");
try std.testing.expectEqualStrings("delete operation", meta1);
const meta2 = try rope.undo("current state");
try std.testing.expectEqualStrings("insert operation", meta2);
}
test "Rope - undo invalidates redo after new operation" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try rope.store_undo("state1");
try rope.append(.{ .value = 2 });
try rope.store_undo("state2");
try rope.append(.{ .value = 3 });
// Undo once
_ = try rope.undo("current");
try std.testing.expect(rope.can_redo());
// Make a new change - this stores the old redo as a branch and clears redo
try rope.store_undo("new branch");
try rope.append(.{ .value = 99 });
// Redo should NOT work anymore (it was saved as a branch)
try std.testing.expect(!rope.can_redo());
// But we can still undo
try std.testing.expect(rope.can_undo());
}
test "Rope - undo/redo with nested ropes" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
const chunks1 = [_]Chunk{.{ .data = "Line 1", .width = 6 }};
const line1 = Line{
.chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks1),
.line_id = 1,
};
const RopeType = rope_mod.Rope(Line);
var rope = try RopeType.from_item(allocator, line1);
try rope.store_undo("before append");
const chunks2 = [_]Chunk{.{ .data = "Line 2", .width = 6 }};
const line2 = Line{
.chunks = try rope_mod.Rope(Chunk).from_slice(allocator, &chunks2),
.line_id = 2,
};
try rope.append(line2);
try std.testing.expectEqual(@as(u32, 2), rope.count());
// Undo
_ = try rope.undo("after append");
try std.testing.expectEqual(@as(u32, 1), rope.count());
// Redo
_ = try rope.redo();
try std.testing.expectEqual(@as(u32, 2), rope.count());
}
test "Rope - stress test undo/redo with many operations" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.init(arena.allocator());
// Perform 20 operations
for (0..20) |i| {
try rope.store_undo("operation");
try rope.append(.{ .value = @intCast(i) });
}
try std.testing.expectEqual(@as(u32, 20), rope.count()); // Sentinel filtered
// Undo 10 operations
for (0..10) |_| {
_ = try rope.undo("current");
}
try std.testing.expectEqual(@as(u32, 10), rope.count());
// Redo 5 operations
for (0..5) |_| {
_ = try rope.redo();
}
try std.testing.expectEqual(@as(u32, 15), rope.count());
}
//===== Bulk/Range Operations Tests =====
test "Rope - split at beginning" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const right = try rope.split(0);
try std.testing.expectEqual(@as(u32, 0), rope.count()); // Sentinel filtered
try std.testing.expectEqual(@as(u32, 3), right.count());
try std.testing.expectEqual(@as(u32, 1), right.get(0).?.value);
}
test "Rope - split at middle" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
.{ .value = 4 },
.{ .value = 5 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const right = try rope.split(2);
try std.testing.expectEqual(@as(u32, 2), rope.count());
try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);
try std.testing.expectEqual(@as(u32, 3), right.count());
try std.testing.expectEqual(@as(u32, 3), right.get(0).?.value);
try std.testing.expectEqual(@as(u32, 4), right.get(1).?.value);
try std.testing.expectEqual(@as(u32, 5), right.get(2).?.value);
}
test "Rope - split at end" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const right = try rope.split(3);
try std.testing.expectEqual(@as(u32, 3), rope.count());
try std.testing.expectEqual(@as(u32, 0), right.count()); // Sentinel filtered
}
test "Rope - slice full range" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const sliced = try rope.slice(0, 3, arena.allocator());
defer arena.allocator().free(sliced);
try std.testing.expectEqual(@as(usize, 3), sliced.len);
try std.testing.expectEqual(@as(u32, 1), sliced[0].value);
try std.testing.expectEqual(@as(u32, 2), sliced[1].value);
try std.testing.expectEqual(@as(u32, 3), sliced[2].value);
}
test "Rope - slice partial range" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 10 },
.{ .value = 20 },
.{ .value = 30 },
.{ .value = 40 },
.{ .value = 50 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const sliced = try rope.slice(1, 4, arena.allocator());
defer arena.allocator().free(sliced);
try std.testing.expectEqual(@as(usize, 3), sliced.len);
try std.testing.expectEqual(@as(u32, 20), sliced[0].value);
try std.testing.expectEqual(@as(u32, 30), sliced[1].value);
try std.testing.expectEqual(@as(u32, 40), sliced[2].value);
}
test "Rope - slice empty range" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const sliced = try rope.slice(1, 1, arena.allocator());
defer arena.allocator().free(sliced);
try std.testing.expectEqual(@as(usize, 0), sliced.len);
}
test "Rope - delete_range at beginning" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
.{ .value = 4 },
.{ .value = 5 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
try rope.delete_range(0, 2);
try std.testing.expectEqual(@as(u32, 3), rope.count());
try std.testing.expectEqual(@as(u32, 3), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 4), rope.get(1).?.value);
try std.testing.expectEqual(@as(u32, 5), rope.get(2).?.value);
}
test "Rope - delete_range in middle" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
.{ .value = 4 },
.{ .value = 5 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
try rope.delete_range(1, 4);
try std.testing.expectEqual(@as(u32, 2), rope.count());
try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 5), rope.get(1).?.value);
}
test "Rope - delete_range at end" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
try rope.delete_range(1, 3);
try std.testing.expectEqual(@as(u32, 1), rope.count());
try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);
}
test "Rope - delete_range empty (same indices)" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
try rope.delete_range(1, 1);
try std.testing.expectEqual(@as(u32, 2), rope.count());
}
test "Rope - insert_slice at beginning" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 3 });
const to_insert = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
};
try rope.insert_slice(0, &to_insert);
try std.testing.expectEqual(@as(u32, 3), rope.count());
try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);
try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);
}
test "Rope - insert_slice in middle" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 4 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const to_insert = [_]SimpleItem{
.{ .value = 2 },
.{ .value = 3 },
};
try rope.insert_slice(1, &to_insert);
try std.testing.expectEqual(@as(u32, 4), rope.count());
try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);
try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);
try std.testing.expectEqual(@as(u32, 4), rope.get(3).?.value);
}
test "Rope - insert_slice at end" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const to_insert = [_]SimpleItem{
.{ .value = 3 },
.{ .value = 4 },
};
try rope.insert_slice(2, &to_insert);
try std.testing.expectEqual(@as(u32, 4), rope.count());
try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);
try std.testing.expectEqual(@as(u32, 4), rope.get(3).?.value);
}
test "Rope - insert_slice empty array" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
const to_insert: []const SimpleItem = &[_]SimpleItem{};
try rope.insert_slice(0, to_insert);
try std.testing.expectEqual(@as(u32, 1), rope.count());
}
test "Rope - to_array with simple items" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 10 },
.{ .value = 20 },
.{ .value = 30 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const array = try rope.to_array(arena.allocator());
defer arena.allocator().free(array);
try std.testing.expectEqual(@as(usize, 3), array.len);
try std.testing.expectEqual(@as(u32, 10), array[0].value);
try std.testing.expectEqual(@as(u32, 20), array[1].value);
try std.testing.expectEqual(@as(u32, 30), array[2].value);
}
test "Rope - to_array empty rope" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.init(arena.allocator());
const array = try rope.to_array(arena.allocator());
defer arena.allocator().free(array);
try std.testing.expectEqual(@as(usize, 0), array.len); // Sentinel filtered
}
test "Rope - combined bulk operations" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
.{ .value = 4 },
.{ .value = 5 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
try rope.delete_range(2, 4);
try std.testing.expectEqual(@as(u32, 3), rope.count());
const to_insert = [_]SimpleItem{
.{ .value = 30 },
.{ .value = 40 },
};
try rope.insert_slice(1, &to_insert);
try std.testing.expectEqual(@as(u32, 5), rope.count());
const array = try rope.to_array(arena.allocator());
defer arena.allocator().free(array);
try std.testing.expectEqual(@as(u32, 1), array[0].value);
try std.testing.expectEqual(@as(u32, 30), array[1].value);
try std.testing.expectEqual(@as(u32, 40), array[2].value);
try std.testing.expectEqual(@as(u32, 2), array[3].value);
try std.testing.expectEqual(@as(u32, 5), array[4].value);
}
test "Rope - undo/redo with bulk operations" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
// Store state
try rope.store_undo("before bulk");
// Bulk insert
const to_insert = [_]SimpleItem{
.{ .value = 10 },
.{ .value = 20 },
};
try rope.insert_slice(1, &to_insert);
try std.testing.expectEqual(@as(u32, 5), rope.count());
// Undo
_ = try rope.undo("after bulk");
try std.testing.expectEqual(@as(u32, 3), rope.count());
// Redo
_ = try rope.redo();
try std.testing.expectEqual(@as(u32, 5), rope.count());
}
//===== Edge Case Tests =====
test "Rope - slice with start > end returns empty" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const sliced = try rope.slice(2, 1, arena.allocator());
defer arena.allocator().free(sliced);
try std.testing.expectEqual(@as(usize, 0), sliced.len);
}
test "Rope - slice beyond bounds" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
// Should only get items that exist
const sliced = try rope.slice(0, 100, arena.allocator());
defer arena.allocator().free(sliced);
try std.testing.expectEqual(@as(usize, 2), sliced.len);
}
test "Rope - delete_range with start > end does nothing" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
try rope.delete_range(2, 1);
try std.testing.expectEqual(@as(u32, 2), rope.count());
}
test "Rope - insert_slice beyond count appends" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
const to_insert = [_]SimpleItem{
.{ .value = 2 },
.{ .value = 3 },
};
try rope.insert_slice(100, &to_insert);
try std.testing.expectEqual(@as(u32, 3), rope.count());
try std.testing.expectEqual(@as(u32, 2), rope.get(1).?.value);
try std.testing.expectEqual(@as(u32, 3), rope.get(2).?.value);
}
test "Rope - replace at out of bounds does nothing" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try rope.replace(100, .{ .value = 999 });
try std.testing.expectEqual(@as(u32, 1), rope.count());
try std.testing.expectEqual(@as(u32, 1), rope.get(0).?.value);
}
test "Rope - delete at out of bounds" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
// This should handle gracefully (delete beyond bounds)
try rope.delete(100);
// Count unchanged
try std.testing.expectEqual(@as(u32, 1), rope.count());
}
test "Rope - split at zero creates empty left" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const right = try rope.split(0);
try std.testing.expectEqual(@as(u32, 0), rope.count()); // Sentinel filtered
try std.testing.expectEqual(@as(u32, 2), right.count());
}
test "Rope - split beyond count" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const right = try rope.split(100);
try std.testing.expectEqual(@as(u32, 2), rope.count());
try std.testing.expectEqual(@as(u32, 0), right.count()); // Sentinel filtered
}
test "Rope - multiple undo without operations" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try rope.store_undo("state1");
try rope.store_undo("state2");
// Two undos back to back
_ = try rope.undo("current");
_ = try rope.undo("current");
// Should fail on third
const result = rope.undo("current");
try std.testing.expectError(error.Stop, result);
}
test "Rope - stress test with 1000 items" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var items: [1000]SimpleItem = undefined;
for (&items, 0..) |*item, i| {
item.* = .{ .value = @intCast(i) };
}
var rope = try RopeType.from_slice(arena.allocator(), &items);
try std.testing.expectEqual(@as(u32, 1000), rope.count());
try std.testing.expectEqual(@as(u32, 0), rope.get(0).?.value);
try std.testing.expectEqual(@as(u32, 500), rope.get(500).?.value);
try std.testing.expectEqual(@as(u32, 999), rope.get(999).?.value);
const depth = rope.root.depth();
try std.testing.expect(depth < 20); // log2(1000) ≈ 10, allow some slack
}
test "Rope - delete_range entire rope" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
try rope.delete_range(0, 3);
// Should be empty (sentinel filtered)
try std.testing.expectEqual(@as(u32, 0), rope.count());
}
test "Rope - to_array single item" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 42 });
const array = try rope.to_array(arena.allocator());
defer arena.allocator().free(array);
try std.testing.expectEqual(@as(usize, 1), array.len);
try std.testing.expectEqual(@as(u32, 42), array[0].value);
}
test "Rope - concat with empty rope" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope1 = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
const rope2 = try RopeType.init(arena.allocator());
try rope1.concat(&rope2);
try std.testing.expectEqual(@as(u32, 1), rope1.count()); // original only (empty filtered)
}
test "Rope - redo after modifying tree fails" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
try rope.store_undo("state1");
try rope.append(.{ .value = 2 });
_ = try rope.undo("current");
// Manually modify the rope (breaking the redo contract)
try rope.append(.{ .value = 3 });
// Redo should fail because tree was modified
const result = rope.redo();
try std.testing.expectError(error.Stop, result);
}
test "Rope - rebalance extremely unbalanced tree" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.init(arena.allocator());
for (0..50) |i| {
try rope.append(.{ .value = @intCast(i) });
}
const depth_before = rope.root.depth();
// Rebalance
try rope.rebalance(arena.allocator());
const depth_after = rope.root.depth();
// Should be more balanced now
try std.testing.expect(depth_after <= depth_before);
try std.testing.expect(depth_after < 15); // log2(50) ≈ 6
// Data should be preserved
try std.testing.expectEqual(@as(u32, 50), rope.count());
try std.testing.expectEqual(@as(u32, 0), rope.get(0).?.value); // Fixed index
}
//===== Weight-aware Tests =====
// Type with custom weight for testing weight-based operations
const WeightedItem = struct {
value: u32,
weight: u32,
pub const Metrics = struct {
total_weight: u32 = 0,
pub fn add(self: *Metrics, other: Metrics) void {
self.total_weight += other.total_weight;
}
pub fn weight(self: *const Metrics) u32 {
return self.total_weight;
}
};
pub fn measure(self: *const WeightedItem) Metrics {
return .{ .total_weight = self.weight };
}
pub fn empty() WeightedItem {
return .{ .value = 0, .weight = 0 };
}
pub fn is_empty(self: *const WeightedItem) bool {
return self.value == 0 and self.weight == 0;
}
};
// Leaf split function for testing (callback format)
const WeightedRope = rope_mod.Rope(WeightedItem);
fn splitWeightedItemCallback(
ctx: ?*anyopaque,
allocator: std.mem.Allocator,
leaf: *const WeightedItem,
weight_in_leaf: u32,
) error{ OutOfBounds, OutOfMemory }!WeightedRope.Node.LeafSplitResult {
_ = ctx;
_ = allocator;
if (weight_in_leaf == 0) {
return .{
.left = WeightedItem.empty(),
.right = leaf.*,
};
} else if (weight_in_leaf >= leaf.weight) {
return .{
.left = leaf.*,
.right = WeightedItem.empty(),
};
}
// Split proportionally
return .{
.left = .{ .value = leaf.value, .weight = weight_in_leaf },
.right = .{ .value = leaf.value + 1000, .weight = leaf.weight - weight_in_leaf },
};
}
// Helper to create the callback struct
fn makeWeightedSplitter() WeightedRope.Node.LeafSplitFn {
return .{
.ctx = null,
.splitFn = splitWeightedItemCallback,
};
}
test "Rope - totalWeight returns correct weight" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const items = [_]WeightedItem{
.{ .value = 1, .weight = 10 },
.{ .value = 2, .weight = 20 },
.{ .value = 3, .weight = 30 },
};
var rope = try WeightedRope.from_slice(arena.allocator(), &items);
try std.testing.expectEqual(@as(u32, 60), rope.totalWeight());
}
test "Rope - split_at_weight at boundary" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const items = [_]WeightedItem{
.{ .value = 1, .weight = 10 },
.{ .value = 2, .weight = 20 },
.{ .value = 3, .weight = 30 },
};
const rope = try WeightedRope.from_slice(arena.allocator(), &items);
// Split at weight 30 (boundary between second and third item)
const splitter = makeWeightedSplitter();
const result = try WeightedRope.Node.split_at_weight(rope.root, 30, arena.allocator(), rope.empty_leaf, &splitter);
// Left should have weight 30 (first two items)
try std.testing.expectEqual(@as(u32, 30), result.left.metrics().weight());
// Right should have weight 30 (third item)
try std.testing.expectEqual(@as(u32, 30), result.right.metrics().weight());
}
test "Rope - split_at_weight inside leaf" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const rope = try WeightedRope.from_item(arena.allocator(), .{ .value = 1, .weight = 100 });
// Split at weight 40 (inside the single leaf)
const splitter = makeWeightedSplitter();
const result = try WeightedRope.Node.split_at_weight(rope.root, 40, arena.allocator(), rope.empty_leaf, &splitter);
// Left should have weight 40
try std.testing.expectEqual(@as(u32, 40), result.left.metrics().weight());
// Right should have weight 60
try std.testing.expectEqual(@as(u32, 60), result.right.metrics().weight());
}
test "Rope - splitByWeight" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const items = [_]WeightedItem{
.{ .value = 1, .weight = 10 },
.{ .value = 2, .weight = 20 },
.{ .value = 3, .weight = 30 },
};
var rope = try WeightedRope.from_slice(arena.allocator(), &items);
try std.testing.expectEqual(@as(u32, 60), rope.totalWeight());
// Split at weight 30
const splitter = makeWeightedSplitter();
const right_half = try rope.splitByWeight(30, &splitter);
// Left half should have weight 30
try std.testing.expectEqual(@as(u32, 30), rope.totalWeight());
// Right half should have weight 30
try std.testing.expectEqual(@as(u32, 30), right_half.totalWeight());
}
test "Rope - deleteRangeByWeight" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const items = [_]WeightedItem{
.{ .value = 1, .weight = 10 },
.{ .value = 2, .weight = 20 },
.{ .value = 3, .weight = 30 },
.{ .value = 4, .weight = 40 },
};
var rope = try WeightedRope.from_slice(arena.allocator(), &items);
try std.testing.expectEqual(@as(u32, 100), rope.totalWeight());
// Delete weight range [10, 30) - removes the second item (weight 20)
const splitter = makeWeightedSplitter();
try rope.deleteRangeByWeight(10, 30, &splitter);
// Should have removed weight 20
try std.testing.expectEqual(@as(u32, 80), rope.totalWeight());
}
test "Rope - insertSliceByWeight" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const items = [_]WeightedItem{
.{ .value = 1, .weight = 10 },
.{ .value = 3, .weight = 30 },
};
var rope = try WeightedRope.from_slice(arena.allocator(), &items);
try std.testing.expectEqual(@as(u32, 40), rope.totalWeight());
// Insert at weight 10 (after first item)
const insert_items = [_]WeightedItem{
.{ .value = 2, .weight = 20 },
};
const splitter = makeWeightedSplitter();
try rope.insertSliceByWeight(10, &insert_items, &splitter);
// Should have added weight 20
try std.testing.expectEqual(@as(u32, 60), rope.totalWeight());
}
test "Rope - findByWeight" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const items = [_]WeightedItem{
.{ .value = 1, .weight = 10 },
.{ .value = 2, .weight = 20 },
.{ .value = 3, .weight = 30 },
};
var rope = try WeightedRope.from_slice(arena.allocator(), &items);
// Find leaf containing weight 0 (first item)
const result0 = rope.findByWeight(0);
try std.testing.expect(result0 != null);
try std.testing.expectEqual(@as(u32, 1), result0.?.leaf.value);
try std.testing.expectEqual(@as(u32, 0), result0.?.start_weight);
// Find leaf containing weight 15 (second item)
const result15 = rope.findByWeight(15);
try std.testing.expect(result15 != null);
try std.testing.expectEqual(@as(u32, 2), result15.?.leaf.value);
try std.testing.expectEqual(@as(u32, 10), result15.?.start_weight);
// Find leaf containing weight 35 (third item)
const result35 = rope.findByWeight(35);
try std.testing.expect(result35 != null);
try std.testing.expectEqual(@as(u32, 3), result35.?.leaf.value);
try std.testing.expectEqual(@as(u32, 30), result35.?.start_weight);
// Out of bounds
const result100 = rope.findByWeight(100);
try std.testing.expect(result100 == null);
}
//===== Integrated Marker Tracking Tests (Union Types) =====
// Simple union type for testing automatic marker tracking
const TokenType = union(enum) {
word: u32,
space: u32,
newline: void, // Marker type
// Define which tags are markers (only track these!)
pub const MarkerTypes = &[_]std.meta.Tag(TokenType){.newline};
pub const Metrics = struct {
width: u32 = 0,
pub fn add(self: *Metrics, other: Metrics) void {
self.width += other.width;
}
pub fn weight(self: *const Metrics) u32 {
return self.width;
}
};
pub fn measure(self: *const TokenType) Metrics {
return switch (self.*) {
.word => |w| .{ .width = w },
.space => |s| .{ .width = s },
.newline => .{ .width = 0 },
};
}
pub fn empty() TokenType {
return .{ .space = 0 };
}
pub fn is_empty(self: *const TokenType) bool {
return switch (self.*) {
.space => |s| s == 0,
else => false,
};
}
};
test "Rope - automatic marker tracking with union type" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(TokenType);
// Create rope with marker tracking enabled
const tokens = [_]TokenType{
.{ .word = 5 }, // "Hello"
.{ .space = 1 }, // " "
.{ .word = 5 }, // "World"
.{ .newline = {} }, // Line break marker
.{ .word = 6 }, // "Second"
.{ .space = 1 }, // " "
.{ .word = 4 }, // "Line"
.{ .newline = {} }, // Line break marker
.{ .word = 5 }, // "Third"
};
var rope = try RopeType.from_slice(arena.allocator(), &tokens);
// O(1) lookup: find newline markers (only .newline is tracked, not .word or .space)
try std.testing.expectEqual(@as(u32, 2), rope.markerCount(.newline));
// Get first newline (end of line 0)
const nl0 = rope.getMarker(.newline, 0);
try std.testing.expect(nl0 != null);
try std.testing.expectEqual(@as(u32, 3), nl0.?.leaf_index); // After word, space, word
try std.testing.expectEqual(@as(u32, 11), nl0.?.global_weight); // 5 + 1 + 5
// Get second newline (end of line 1)
const nl1 = rope.getMarker(.newline, 1);
try std.testing.expect(nl1 != null);
try std.testing.expectEqual(@as(u32, 7), nl1.?.leaf_index);
try std.testing.expectEqual(@as(u32, 22), nl1.?.global_weight); // 11 + 6 + 1 + 4
// Word and space are NOT markers - should return 0
try std.testing.expectEqual(@as(u32, 0), rope.markerCount(.word));
try std.testing.expectEqual(@as(u32, 0), rope.markerCount(.space));
}
test "Rope - marker tracking with empty rope" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(TokenType);
var rope = try RopeType.init(arena.allocator());
try std.testing.expectEqual(@as(u32, 0), rope.markerCount(.newline));
try std.testing.expectEqual(@as(u32, 0), rope.markerCount(.word)); // Not a marker type
try std.testing.expect(rope.getMarker(.newline, 0) == null);
}
test "Rope - marker tracking requires rebuild" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(TokenType);
const tokens = [_]TokenType{
.{ .word = 5 },
.{ .newline = {} },
};
var rope = try RopeType.from_slice(arena.allocator(), &tokens);
// Markers are automatically tracked in the tree
try std.testing.expectEqual(@as(u32, 1), rope.markerCount(.newline));
try std.testing.expect(rope.getMarker(.newline, 0) != null);
}
test "Rope - marker tracking with many markers" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(TokenType);
// Create 100 lines
var tokens_array: [199]TokenType = undefined; // 100 words + 99 newlines
for (0..100) |i| {
if (i > 0) {
tokens_array[i * 2 - 1] = .{ .newline = {} };
}
tokens_array[i * 2] = .{ .word = 5 };
}
var rope = try RopeType.from_slice(arena.allocator(), &tokens_array);
// Should have 99 newlines (only newlines are tracked as markers)
try std.testing.expectEqual(@as(u32, 99), rope.markerCount(.newline));
// Test O(1) random access to specific lines
const nl50 = rope.getMarker(.newline, 50).?;
try std.testing.expectEqual(@as(u32, 101), nl50.leaf_index); // word, nl, word, nl, ... (50th newline is at index 101)
try std.testing.expectEqual(@as(u32, 255), nl50.global_weight); // 51 words * 5 width
const nl98 = rope.getMarker(.newline, 98).?;
try std.testing.expectEqual(@as(u32, 197), nl98.leaf_index);
}
//===== Debug toText Tests =====
test "Rope - toText shows basic structure" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const debug_text = try rope.toText(arena.allocator());
try std.testing.expect(std.mem.indexOf(u8, debug_text, "[root") != null);
try std.testing.expect(std.mem.indexOf(u8, debug_text, "branch") != null or std.mem.indexOf(u8, debug_text, "leaf") != null);
}
test "Rope - toText shows empty rope" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.init(arena.allocator());
const debug_text = try rope.toText(arena.allocator());
try std.testing.expect(std.mem.indexOf(u8, debug_text, "[root") != null);
try std.testing.expect(std.mem.indexOf(u8, debug_text, "[empty]") != null);
}
test "Rope - toText with union type shows tags" {
const TestSegment = union(enum) {
text: struct { width: u32 },
brk: void,
linestart: void,
pub const MarkerTypes = &[_]std.meta.Tag(@This()){ .brk, .linestart };
pub const Metrics = struct {
width: u32 = 0,
pub fn add(self: *Metrics, other: Metrics) void {
self.width += other.width;
}
pub fn weight(self: *const Metrics) u32 {
return self.width;
}
};
pub fn measure(self: *const @This()) Metrics {
return switch (self.*) {
.text => |t| Metrics{ .width = t.width },
.brk => Metrics{ .width = 1 },
.linestart => Metrics{ .width = 0 },
};
}
pub fn empty() @This() {
return .{ .text = .{ .width = 0 } };
}
pub fn is_empty(self: *const @This()) bool {
return switch (self.*) {
.text => |t| t.width == 0,
else => false,
};
}
};
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const TestRope = rope_mod.Rope(TestSegment);
var rope = try TestRope.from_slice(arena.allocator(), &[_]TestSegment{
.linestart,
.{ .text = .{ .width = 5 } },
.brk,
.linestart,
.{ .text = .{ .width = 10 } },
});
const debug_text = try rope.toText(arena.allocator());
try std.testing.expect(std.mem.indexOf(u8, debug_text, "text") != null);
try std.testing.expect(std.mem.indexOf(u8, debug_text, "brk") != null);
try std.testing.expect(std.mem.indexOf(u8, debug_text, "linestart") != null);
try std.testing.expect(std.mem.indexOf(u8, debug_text, "w5") != null);
try std.testing.expect(std.mem.indexOf(u8, debug_text, "w10") != null);
}
test "Rope - toText with nested structure" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
// Create a larger rope that will have branches
var items: [10]SimpleItem = undefined;
for (&items, 0..) |*item, i| {
item.* = .{ .value = @intCast(i) };
}
var rope = try RopeType.from_slice(arena.allocator(), &items);
const debug_text = try rope.toText(arena.allocator());
try std.testing.expect(std.mem.indexOf(u8, debug_text, "[root") != null);
try std.testing.expect(std.mem.indexOf(u8, debug_text, "[branch") != null);
}
test "Rope - toText after insertions shows updated structure" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
const before = try rope.toText(arena.allocator());
try std.testing.expect(std.mem.indexOf(u8, before, "[root") != null);
try rope.append(.{ .value = 2 });
try rope.append(.{ .value = 3 });
const after = try rope.toText(arena.allocator());
try std.testing.expect(std.mem.indexOf(u8, after, "[root") != null);
try std.testing.expect(after.len >= before.len);
}
test "Rope - toText with custom metrics shows width info" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(ItemWithMetrics);
const items = [_]ItemWithMetrics{
.{ .value = 1, .size = 100 },
.{ .value = 2, .size = 200 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
const debug_text = try rope.toText(arena.allocator());
try std.testing.expect(std.mem.indexOf(u8, debug_text, "[root") != null);
try std.testing.expect(std.mem.indexOf(u8, debug_text, "w") != null);
}
test "Rope - toText shows single leaf" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 42 });
const debug_text = try rope.toText(arena.allocator());
try std.testing.expect(std.mem.indexOf(u8, debug_text, "[root") != null);
try std.testing.expect(std.mem.indexOf(u8, debug_text, "[leaf") != null);
try std.testing.expect(std.mem.indexOf(u8, debug_text, "]") != null);
}
test "Rope - marker cache MUST update after delete operations" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(TokenType);
// Create: word(5) newline word(5) newline word(5)
// 3 lines total, 2 newlines
const tokens = [_]TokenType{
.{ .word = 5 },
.{ .newline = {} },
.{ .word = 5 },
.{ .newline = {} },
.{ .word = 5 },
};
var rope = try RopeType.from_slice(arena.allocator(), &tokens);
try std.testing.expectEqual(@as(u32, 2), rope.markerCount(.newline));
try rope.delete(4);
try std.testing.expectEqual(@as(u32, 2), rope.markerCount(.newline));
// The critical test: marker positions MUST be correct after delete!
const nl1_after = rope.getMarker(.newline, 1);
try std.testing.expect(nl1_after != null);
// After deleting the last word at index 4, the second newline should be at index 3
// (was at index 3 before, stays at 3 after deleting index 4)
try std.testing.expectEqual(@as(u32, 3), nl1_after.?.leaf_index);
}
test "Rope - marker cache MUST update after undo" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(TokenType);
// Create: word(10) newline word(5)
const tokens = [_]TokenType{
.{ .word = 10 },
.{ .newline = {} },
.{ .word = 5 },
};
var rope = try RopeType.from_slice(arena.allocator(), &tokens);
// Initial state: 1 newline at weight 10
const nl_before = rope.getMarker(.newline, 0);
try std.testing.expect(nl_before != null);
try std.testing.expectEqual(@as(u32, 10), nl_before.?.global_weight);
// Store undo point
try rope.store_undo("before delete");
// Delete part of first word: delete range [0, 1) removes first word
try rope.delete_range(0, 1);
// After delete: newline should be at weight 0 (no word before it)
const nl_after_delete = rope.getMarker(.newline, 0);
try std.testing.expect(nl_after_delete != null);
try std.testing.expectEqual(@as(u32, 0), nl_after_delete.?.global_weight);
// Undo the delete
_ = try rope.undo("after delete");
// CRITICAL: After undo, marker cache MUST be recalculated!
// Newline should be back at weight 10
const nl_after_undo = rope.getMarker(.newline, 0);
try std.testing.expect(nl_after_undo != null);
try std.testing.expectEqual(@as(u32, 10), nl_after_undo.?.global_weight);
}
test "Rope - marker cache MUST update after redo" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(TokenType);
const tokens = [_]TokenType{
.{ .word = 10 },
.{ .newline = {} },
.{ .word = 5 },
};
var rope = try RopeType.from_slice(arena.allocator(), &tokens);
try rope.store_undo("initial");
try rope.delete_range(0, 1);
const nl_after_delete = rope.getMarker(.newline, 0);
try std.testing.expectEqual(@as(u32, 0), nl_after_delete.?.global_weight);
// Undo
_ = try rope.undo("after delete");
const nl_after_undo = rope.getMarker(.newline, 0);
try std.testing.expectEqual(@as(u32, 10), nl_after_undo.?.global_weight);
// Redo
_ = try rope.redo();
// CRITICAL: After redo, marker cache MUST be recalculated!
const nl_after_redo = rope.getMarker(.newline, 0);
try std.testing.expect(nl_after_redo != null);
try std.testing.expectEqual(@as(u32, 0), nl_after_redo.?.global_weight);
}
test "Rope - marker cache survives multiple undo/redo cycles" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(TokenType);
var rope = try RopeType.from_slice(arena.allocator(), &[_]TokenType{
.{ .word = 5 },
.{ .newline = {} },
.{ .word = 5 },
});
try rope.store_undo("state1");
try rope.append(.{ .newline = {} });
try rope.append(.{ .word = 5 });
// Should have 2 newlines now
try std.testing.expectEqual(@as(u32, 2), rope.markerCount(.newline));
const nl1_orig = rope.getMarker(.newline, 1);
try std.testing.expectEqual(@as(u32, 10), nl1_orig.?.global_weight);
try rope.store_undo("state2");
try rope.delete(0); // Delete first word
// Markers should update: first newline now at weight 0
const nl0_after_delete = rope.getMarker(.newline, 0);
try std.testing.expectEqual(@as(u32, 0), nl0_after_delete.?.global_weight);
// Undo twice
_ = try rope.undo("current");
_ = try rope.undo("current");
// Back to original: 1 newline at weight 5
try std.testing.expectEqual(@as(u32, 1), rope.markerCount(.newline));
const nl_final = rope.getMarker(.newline, 0);
try std.testing.expectEqual(@as(u32, 5), nl_final.?.global_weight);
// Redo twice
_ = try rope.redo();
_ = try rope.redo();
// Should match the post-delete state
const nl0_redo = rope.getMarker(.newline, 0);
try std.testing.expectEqual(@as(u32, 0), nl0_redo.?.global_weight);
}
//===== Configurable Undo Depth Tests =====
test "Rope - weight-based balancing with custom weight function" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
// Create items with different sizes
const items = [_]WeightedItem{
.{ .value = 1, .weight = 100 }, // Large item
.{ .value = 2, .weight = 10 }, // Small item
.{ .value = 3, .weight = 200 }, // Very large item
.{ .value = 4, .weight = 50 }, // Medium item
};
var rope = try WeightedRope.from_slice(arena.allocator(), &items);
// Check that metrics are tracked
const metrics = rope.root.metrics();
try std.testing.expectEqual(@as(u32, 4), metrics.count);
try std.testing.expectEqual(@as(u32, 360), metrics.custom.total_weight);
try std.testing.expectEqual(@as(u32, 360), metrics.weight()); // Should use weight()
}
test "Rope - unlimited undo depth by default" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.init(arena.allocator());
// Store many undo states
for (0..100) |i| {
try rope.store_undo("state");
try rope.append(.{ .value = @intCast(i) });
}
// Should have all 100 states
try std.testing.expectEqual(@as(usize, 100), rope.undo_depth);
try std.testing.expect(rope.can_undo());
}
test "Rope - max_undo_depth limits history" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.initWithConfig(arena.allocator(), .{ .max_undo_depth = 10 });
// Store 20 undo states
for (0..20) |i| {
try rope.store_undo("state");
try rope.append(.{ .value = @intCast(i) });
}
// Should only keep 10 states
try std.testing.expectEqual(@as(usize, 10), rope.undo_depth);
try std.testing.expect(rope.can_undo());
// Can undo at most 10 times (may be less due to how history works)
var undo_count: usize = 0;
while (rope.can_undo()) : (undo_count += 1) {
_ = rope.undo("current") catch break;
}
// Should have undone at least some operations, but not more than 10
try std.testing.expect(undo_count > 0);
try std.testing.expect(undo_count <= 10);
}
test "Rope - trimUndoHistory works correctly" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.initWithConfig(arena.allocator(), .{ .max_undo_depth = 5 });
for (0..10) |i| {
try rope.store_undo("state");
try rope.append(.{ .value = @intCast(i) });
try std.testing.expect(rope.undo_depth <= 5);
}
try std.testing.expectEqual(@as(usize, 5), rope.undo_depth);
}
test "Rope - weight-based join_balanced respects weight ratio" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const left_items = [_]WeightedItem{
.{ .value = 1, .weight = 1000 },
.{ .value = 2, .weight = 1000 },
.{ .value = 3, .weight = 1000 },
};
var rope_left = try WeightedRope.from_slice(arena.allocator(), &left_items);
const right_items = [_]WeightedItem{
.{ .value = 4, .weight = 100 },
};
const rope_right = try WeightedRope.from_slice(arena.allocator(), &right_items);
try rope_left.concat(&rope_right);
try std.testing.expectEqual(@as(u32, 4), rope_left.count());
try std.testing.expect(rope_left.root.is_balanced());
}
test "Rope - integration weight-based balancing with history limits" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var rope = try WeightedRope.initWithConfig(arena.allocator(), .{ .max_undo_depth = 5 });
var expected_count: u32 = 0;
for (0..10) |i| {
try rope.store_undo("insert");
try rope.append(.{
.value = @intCast(i),
.weight = @intCast((i + 1) * 10),
});
expected_count += 1;
}
try std.testing.expectEqual(expected_count, rope.count());
try rope.insert(5, .{ .value = 999, .weight = 50 });
expected_count += 1;
try std.testing.expectEqual(expected_count, rope.count());
try std.testing.expect(rope.undo_depth <= 5);
try std.testing.expect(rope.root.is_balanced());
}
test "Rope - clear removes all items" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
const items = [_]SimpleItem{
.{ .value = 1 },
.{ .value = 2 },
.{ .value = 3 },
};
var rope = try RopeType.from_slice(arena.allocator(), &items);
try std.testing.expectEqual(@as(u32, 3), rope.count());
rope.clear();
try std.testing.expectEqual(@as(u32, 0), rope.count());
}
test "Rope - clear on empty rope" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.init(arena.allocator());
try std.testing.expectEqual(@as(u32, 0), rope.count());
rope.clear();
try std.testing.expectEqual(@as(u32, 0), rope.count());
}
test "Rope - clear then insert works" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.from_item(arena.allocator(), .{ .value = 1 });
rope.clear();
try std.testing.expectEqual(@as(u32, 0), rope.count());
try rope.append(.{ .value = 42 });
try std.testing.expectEqual(@as(u32, 1), rope.count());
try std.testing.expectEqual(@as(u32, 42), rope.get(0).?.value);
}
test "Rope - clear with markers resets marker cache" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(TokenType);
const tokens = [_]TokenType{
.{ .word = 5 },
.{ .newline = {} },
.{ .word = 5 },
.{ .newline = {} },
};
var rope = try RopeType.from_slice(arena.allocator(), &tokens);
try std.testing.expectEqual(@as(u32, 2), rope.markerCount(.newline));
rope.clear();
try std.testing.expectEqual(@as(u32, 0), rope.count());
try std.testing.expectEqual(@as(u32, 0), rope.markerCount(.newline));
}
test "Rope - integration all features working together" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const RopeType = rope_mod.Rope(SimpleItem);
var rope = try RopeType.initWithConfig(arena.allocator(), .{ .max_undo_depth = 3 });
try std.testing.expectEqual(@as(u32, 0), rope.count());
try rope.store_undo("empty");
try rope.append(.{ .value = 1 });
try rope.store_undo("one");
try rope.append(.{ .value = 2 });
try rope.store_undo("two");
try rope.append(.{ .value = 3 });
try rope.store_undo("three");
try rope.append(.{ .value = 4 });
try std.testing.expectEqual(@as(u32, 4), rope.count());
try std.testing.expectEqual(@as(usize, 3), rope.undo_depth);
const val = rope.get(2);
try std.testing.expectEqual(@as(u32, 3), val.?.value);
_ = try rope.undo("current");
try std.testing.expectEqual(@as(u32, 3), rope.count());
const Context = struct {
count: u32 = 0,
fn walker(ctx: *anyopaque, data: *const SimpleItem, index: u32) RopeType.Node.WalkerResult {
_ = data;
_ = index;
const self = @as(*@This(), @ptrCast(@alignCast(ctx)));
self.count += 1;
return .{};
}
};
var ctx = Context{};
try rope.walk(&ctx, Context.walker);
try std.testing.expectEqual(@as(u32, 3), ctx.count);
}