diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/datastruct | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/datastruct')
| -rw-r--r-- | vendor/libghostty-vt/src/datastruct/array_list_collection.zig | 44 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/datastruct/blocking_queue.zig | 248 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/datastruct/cache_table.zig | 181 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/datastruct/circ_buf.zig | 866 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/datastruct/comparison.zig | 430 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/datastruct/intrusive_linked_list.zig | 181 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/datastruct/lru.zig | 349 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/datastruct/main.zig | 24 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/datastruct/message_data.zig | 124 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/datastruct/segmented_pool.zig | 95 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/datastruct/split_tree.zig | 2367 |
11 files changed, 4909 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/datastruct/array_list_collection.zig b/vendor/libghostty-vt/src/datastruct/array_list_collection.zig new file mode 100644 index 00000000..d3fbddb1 --- /dev/null +++ b/vendor/libghostty-vt/src/datastruct/array_list_collection.zig @@ -0,0 +1,44 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +/// A collection of ArrayLists with methods for bulk operations. +pub fn ArrayListCollection(comptime T: type) type { + return struct { + const Self = ArrayListCollection(T); + const ArrayListT = std.ArrayListUnmanaged(T); + + // An array containing the lists that belong to this collection. + lists: []ArrayListT, + + // The collection will be initialized with empty ArrayLists. + pub fn init( + alloc: Allocator, + list_count: usize, + initial_capacity: usize, + ) Allocator.Error!Self { + const self: Self = .{ + .lists = try alloc.alloc(ArrayListT, list_count), + }; + + for (self.lists) |*list| { + list.* = try .initCapacity(alloc, initial_capacity); + } + + return self; + } + + pub fn deinit(self: *Self, alloc: Allocator) void { + for (self.lists) |*list| { + list.deinit(alloc); + } + alloc.free(self.lists); + } + + /// Clear all lists in the collection, retaining capacity. + pub fn reset(self: *Self) void { + for (self.lists) |*list| { + list.clearRetainingCapacity(); + } + } + }; +} diff --git a/vendor/libghostty-vt/src/datastruct/blocking_queue.zig b/vendor/libghostty-vt/src/datastruct/blocking_queue.zig new file mode 100644 index 00000000..3185d98d --- /dev/null +++ b/vendor/libghostty-vt/src/datastruct/blocking_queue.zig @@ -0,0 +1,248 @@ +//! Blocking queue implementation aimed primarily for message passing +//! between threads. + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +/// Returns a blocking queue implementation for type T. +/// +/// This is tailor made for ghostty usage so it isn't meant to be maximally +/// generic, but I'm happy to make it more generic over time. Traits of this +/// queue that are specific to our usage: +/// +/// - Fixed size. We expect our queue to quickly drain and also not be +/// too large so we prefer a fixed size queue for now. +/// - No blocking pop. We use an external event loop mechanism such as +/// eventfd to notify our waiter that there is no data available so +/// we don't need to implement a blocking pop. +/// - Drain function. Most queues usually pop one at a time. We have +/// a mechanism for draining since on every IO loop our TTY drains +/// the full queue so we can get rid of the overhead of a ton of +/// locks and bounds checking and do a one-time drain. +/// +/// One key usage pattern is that our blocking queues are single producer +/// single consumer (SPSC). This should let us do some interesting optimizations +/// in the future. At the time of writing this, the blocking queue implementation +/// is purposely naive to build something quickly, but we should benchmark +/// and make this more optimized as necessary. +pub fn BlockingQueue( + comptime T: type, + comptime capacity: usize, +) type { + return struct { + const Self = @This(); + + // The type we use for queue size types. We can optimize this + // in the future to be the correct bit-size for our preallocated + // size for this queue. + pub const Size = u32; + + // The bounds of this queue. We recast this to Size so we can do math. + const bounds: Size = @intCast(capacity); + + /// Specifies the timeout for an operation. + pub const Timeout = union(enum) { + /// Fail instantly (non-blocking). + instant: void, + + /// Run forever or until interrupted + forever: void, + + /// Nanoseconds + ns: u64, + }; + + /// Our data. The values are undefined until they are written. + data: [bounds]T = undefined, + + /// The next location to write (next empty loc) and next location + /// to read (next non-empty loc). The number of written elements. + write: Size = 0, + read: Size = 0, + len: Size = 0, + + /// The big mutex that must be held to read/write. + mutex: std.Thread.Mutex = .{}, + + /// A CV for being notified when the queue is no longer full. This is + /// used for writing. Note we DON'T have a CV for waiting on the + /// queue not being EMPTY because we use external notifiers for that. + cond_not_full: std.Thread.Condition = .{}, + not_full_waiters: usize = 0, + + /// Allocate the blocking queue on the heap. + pub fn create(alloc: Allocator) Allocator.Error!*Self { + const ptr = try alloc.create(Self); + errdefer alloc.destroy(ptr); + + ptr.* = .{ + .data = undefined, + .len = 0, + .write = 0, + .read = 0, + .mutex = .{}, + .cond_not_full = .{}, + .not_full_waiters = 0, + }; + + return ptr; + } + + /// Free all the resources for this queue. This should only be + /// called once all producers and consumers have quit. + pub fn destroy(self: *Self, alloc: Allocator) void { + self.* = undefined; + alloc.destroy(self); + } + + /// Push a value to the queue. This returns the total size of the + /// queue (unread items) after the push. A return value of zero + /// means that the push failed. + pub fn push(self: *Self, value: T, timeout: Timeout) Size { + self.mutex.lock(); + defer self.mutex.unlock(); + + // The + if (self.full()) { + switch (timeout) { + // If we're not waiting, then we failed to write. + .instant => return 0, + + .forever => { + self.not_full_waiters += 1; + defer self.not_full_waiters -= 1; + self.cond_not_full.wait(&self.mutex); + }, + + .ns => |ns| { + self.not_full_waiters += 1; + defer self.not_full_waiters -= 1; + self.cond_not_full.timedWait(&self.mutex, ns) catch return 0; + }, + } + + // If we're still full, then we failed to write. This can + // happen in situations where we are interrupted. + if (self.full()) return 0; + } + + // Add our data and update our accounting + self.data[self.write] = value; + self.write += 1; + if (self.write >= bounds) self.write -= bounds; + self.len += 1; + + return self.len; + } + + /// Pop a value from the queue without blocking. + pub fn pop(self: *Self) ?T { + self.mutex.lock(); + defer self.mutex.unlock(); + + // If we're empty we have nothing + if (self.len == 0) return null; + + // Get the index we're going to read data from and do some + // accounting. We don't copy the value here to avoid copying twice. + const n = self.read; + self.read += 1; + if (self.read >= bounds) self.read -= bounds; + self.len -= 1; + + // If we have consumers waiting on a full queue, notify. + if (self.not_full_waiters > 0) self.cond_not_full.signal(); + + return self.data[n]; + } + + /// Pop all values from the queue. This will hold the big mutex + /// until `deinit` is called on the return value. This is used if + /// you know you're going to "pop" and utilize all the values + /// quickly to avoid many locks, bounds checks, and cv signals. + pub fn drain(self: *Self) DrainIterator { + self.mutex.lock(); + return .{ .queue = self }; + } + + pub const DrainIterator = struct { + queue: *Self, + + pub fn next(self: *DrainIterator) ?T { + if (self.queue.len == 0) return null; + + // Read and account + const n = self.queue.read; + self.queue.read += 1; + if (self.queue.read >= bounds) self.queue.read -= bounds; + self.queue.len -= 1; + + return self.queue.data[n]; + } + + pub fn deinit(self: *DrainIterator) void { + // If we have consumers waiting on a full queue, notify. + if (self.queue.not_full_waiters > 0) self.queue.cond_not_full.signal(); + + // Unlock + self.queue.mutex.unlock(); + } + }; + + /// Returns true if the queue is full. This is not public because + /// it requires the lock to be held. + inline fn full(self: *Self) bool { + return self.len == bounds; + } + }; +} + +test "basic push and pop" { + const testing = std.testing; + const alloc = testing.allocator; + + const Q = BlockingQueue(u64, 4); + const q = try Q.create(alloc); + defer q.destroy(alloc); + + // Should have no values + try testing.expect(q.pop() == null); + + // Push until we're full + try testing.expectEqual(@as(Q.Size, 1), q.push(1, .{ .instant = {} })); + try testing.expectEqual(@as(Q.Size, 2), q.push(2, .{ .instant = {} })); + try testing.expectEqual(@as(Q.Size, 3), q.push(3, .{ .instant = {} })); + try testing.expectEqual(@as(Q.Size, 4), q.push(4, .{ .instant = {} })); + try testing.expectEqual(@as(Q.Size, 0), q.push(5, .{ .instant = {} })); + + // Pop! + try testing.expect(q.pop().? == 1); + try testing.expect(q.pop().? == 2); + try testing.expect(q.pop().? == 3); + try testing.expect(q.pop().? == 4); + try testing.expect(q.pop() == null); + + // Drain does nothing + var it = q.drain(); + try testing.expect(it.next() == null); + it.deinit(); + + // Verify we can still push + try testing.expectEqual(@as(Q.Size, 1), q.push(1, .{ .instant = {} })); +} + +test "timed push" { + const testing = std.testing; + const alloc = testing.allocator; + + const Q = BlockingQueue(u64, 1); + const q = try Q.create(alloc); + defer q.destroy(alloc); + + // Push + try testing.expectEqual(@as(Q.Size, 1), q.push(1, .{ .instant = {} })); + try testing.expectEqual(@as(Q.Size, 0), q.push(2, .{ .instant = {} })); + + // Timed push should fail + try testing.expectEqual(@as(Q.Size, 0), q.push(2, .{ .ns = 1000 })); +} diff --git a/vendor/libghostty-vt/src/datastruct/cache_table.zig b/vendor/libghostty-vt/src/datastruct/cache_table.zig new file mode 100644 index 00000000..49172398 --- /dev/null +++ b/vendor/libghostty-vt/src/datastruct/cache_table.zig @@ -0,0 +1,181 @@ +const fastmem = @import("../fastmem.zig"); + +const std = @import("std"); +const assert = @import("../quirks.zig").inlineAssert; + +/// An associative data structure used for efficiently storing and +/// retrieving values which are able to be recomputed if necessary. +/// +/// This structure is effectively a hash table with fixed-sized buckets. +/// +/// When inserting an item in to a full bucket, the least recently used +/// item is replaced. +/// +/// To achieve this, when an item is accessed, it's moved to the end of +/// the bucket, and the rest of the items are moved over to fill the gap. +/// +/// This should provide very good query performance and keep frequently +/// accessed items cached indefinitely. +/// +/// Parameters: +/// +/// `Context` +/// A type containing methods to define CacheTable behaviors. +/// - `fn hash(*Context, K) u64` - Return a hash for a key. +/// - `fn eql(*Context, K, K) bool` - Check two keys for equality. +/// +/// - `fn evicted(*Context, K, V) void` - [OPTIONAL] Eviction callback. +/// If present, called whenever an item is evicted from the cache. +/// +/// `bucket_count` +/// Should ideally be close to the median number of important items that +/// you expect to be cached at any given point. This is required to be a +/// power of 2 since performance suffers if it's not and there's no good +/// reason to allow it to be anything else. +/// +/// `bucket_size` +/// should be larger if you expect a large number of unimportant items to +/// enter the cache at a time. Having larger buckets will avoid important +/// items being dropped from the cache prematurely. +/// +pub fn CacheTable( + comptime K: type, + comptime V: type, + comptime Context: type, + comptime bucket_count: usize, + comptime bucket_size: u8, +) type { + return struct { + const Self = @This(); + + const KV = struct { + key: K, + value: V, + }; + + comptime { + assert(std.math.isPowerOfTwo(bucket_count)); + assert(bucket_count <= std.math.maxInt(usize)); + } + + /// `bucket_count` buckets containing `bucket_size` KV pairs each. + /// + /// We don't need to initialize this memory because we don't use it + /// unless it's within a bucket's stored length, which will guarantee + /// that we put actual items there. + buckets: [bucket_count][bucket_size]KV = undefined, + + /// We use this array to keep track of how many slots in each bucket + /// have actual items in them. Once all the buckets fill up this will + /// become a pointless check, but hopefully branch prediction picks + /// up on it at that point. The memory cost isn't too bad since it's + /// just bytes, so should be a fraction the size of the main table. + lengths: [bucket_count]u8 = @splat(0), + + /// An instance of the context structure. + /// Must be initialized before calling any operations. + context: Context, + + /// Adds an item to the cache table. If an old value was removed to + /// make room then it is returned in a struct with its key and value. + pub fn put(self: *Self, key: K, value: V) ?KV { + const kv: KV = .{ .key = key, .value = value }; + const idx: usize = @intCast(self.context.hash(key) % bucket_count); + + // If we have space available in the bucket then we just append + if (self.lengths[idx] < bucket_size) { + self.buckets[idx][self.lengths[idx]] = kv; + self.lengths[idx] += 1; + return null; + } + assert(self.lengths[idx] == bucket_size); + + // Append our new item and return the oldest + const evicted = fastmem.rotateIn(KV, &self.buckets[idx], kv); + + // The Context is allowed to register an eviction hook. + if (comptime @hasDecl(Context, "evicted")) self.context.evicted( + evicted.key, + evicted.value, + ); + + return evicted; + } + + /// Retrieves an item from the cache table. + /// + /// Returns null if no item is found with the provided key. + pub fn get(self: *Self, key: K) ?V { + const idx: usize = @intCast(self.context.hash(key) % bucket_count); + const len = self.lengths[idx]; + var i: usize = len; + while (i > 0) { + i -= 1; + if (self.context.eql(key, self.buckets[idx][i].key)) { + defer fastmem.rotateOnce(KV, self.buckets[idx][i..len]); + return self.buckets[idx][i].value; + } + } + + return null; + } + + /// Removes all items from the cache table. + /// + /// If your `Context` has an `evicted` method, + /// it will be called with all removed items. + pub fn clear(self: *Self) void { + if (comptime @hasDecl(Context, "evicted")) { + for (self.buckets, self.lengths) |b, l| { + for (b[0..l]) |kv| { + self.context.evicted(kv.key, kv.value); + } + } + } + + @memset(&self.lengths, 0); + } + }; +} + +/// Creates a Context automatically for the given key type. This uses the +/// same logic as std.hash_map.AutoContext today since the API matches. +fn AutoContext(comptime K: type) type { + return std.hash_map.AutoContext(K); +} + +test CacheTable { + const testing = std.testing; + + // Construct a table that purposely has a predictable hash so we can + // test all edge cases. + const T = CacheTable(u32, u32, struct { + pub fn hash(self: *const @This(), key: u32) u64 { + _ = self; + return @intCast(key); + } + + pub fn eql(self: *const @This(), a: u32, b: u32) bool { + _ = self; + return a == b; + } + }, 2, 2); + var t: T = .{ .context = .{} }; + + // Fill the table + try testing.expect(t.put(0, 0) == null); + try testing.expect(t.put(1, 0) == null); + try testing.expect(t.put(2, 0) == null); + try testing.expect(t.put(3, 0) == null); + + // It should now be full, so any insert should evict the oldest item. + // NOTE: For the sake of this test, we're assuming that the first item + // is evicted but we don't need to promise this. + try testing.expectEqual(T.KV{ + .key = 0, + .value = 0, + }, t.put(4, 0).?); + + // The first item should now be gone + try testing.expect(t.get(0) == null); +} diff --git a/vendor/libghostty-vt/src/datastruct/circ_buf.zig b/vendor/libghostty-vt/src/datastruct/circ_buf.zig new file mode 100644 index 00000000..3e373cb9 --- /dev/null +++ b/vendor/libghostty-vt/src/datastruct/circ_buf.zig @@ -0,0 +1,866 @@ +const std = @import("std"); +const assert = @import("../quirks.zig").inlineAssert; +const Allocator = std.mem.Allocator; +const fastmem = @import("../fastmem.zig"); + +/// Returns a circular buffer containing type T. +pub fn CircBuf(comptime T: type, comptime default: T) type { + return struct { + const Self = @This(); + + // Implementation note: there's a lot of unsafe addition of usize + // here in this implementation that can technically overflow. If someone + // wants to fix this and make it overflow safe (use subtractions for + // checks prior to additions) then I welcome it. In reality, we'd + // have to be a really, really large terminal screen to even worry + // about this so I'm punting it. + + storage: []T, + head: usize, + tail: usize, + + // We could remove this and just use math with head/tail to figure + // it out, but our usage of circular buffers stores so much data that + // this minor overhead is not worth optimizing out. + full: bool, + + pub const Iterator = struct { + buf: Self, + idx: usize, + direction: Direction, + + pub const Direction = enum { forward, reverse }; + + pub fn next(self: *Iterator) ?*T { + if (self.idx >= self.buf.len()) return null; + + // Get our index from the tail + const tail_idx = switch (self.direction) { + .forward => self.idx, + .reverse => self.buf.len() - self.idx - 1, + }; + + // Translate the tail index to a storage index + const storage_idx = (self.buf.tail + tail_idx) % self.buf.capacity(); + self.idx += 1; + return &self.buf.storage[storage_idx]; + } + + /// Seek the iterator by a given amount. This will clamp + /// the values to the bounds of the buffer so overflows are + /// not possible. + pub fn seekBy(self: *Iterator, amount: isize) void { + if (amount > 0) { + self.idx +|= @intCast(amount); + } else { + self.idx -|= @intCast(@abs(amount)); + } + } + + /// Reset the iterator back to the first value. + pub fn reset(self: *Iterator) void { + self.idx = 0; + } + }; + + /// Initialize a new circular buffer that can store size elements. + pub fn init(alloc: Allocator, size: usize) Allocator.Error!Self { + const buf = try alloc.alloc(T, size); + @memset(buf, default); + + return Self{ + .storage = buf, + .head = 0, + .tail = 0, + .full = size == 0, + }; + } + + pub fn deinit(self: *Self, alloc: Allocator) void { + alloc.free(self.storage); + self.* = undefined; + } + + /// Append a single value to the buffer. If the buffer is full, + /// an error will be returned. + pub fn append(self: *Self, v: T) Allocator.Error!void { + if (self.full) return error.OutOfMemory; + self.storage[self.head] = v; + self.head += 1; + if (self.head >= self.storage.len) self.head = 0; + self.full = self.head == self.tail; + } + + /// Append a single value to the buffer, assuming there is capacity. + pub fn appendAssumeCapacity(self: *Self, v: T) void { + assert(!self.full); + self.storage[self.head] = v; + self.head += 1; + if (self.head >= self.storage.len) self.head = 0; + self.full = self.head == self.tail; + } + + /// Append a slice to the buffer. + pub fn appendSliceAssumeCapacity( + self: *Self, + slice: []const T, + ) void { + const storage = self.getPtrSlice( + self.len(), + slice.len, + ); + fastmem.copy(T, storage[0], slice[0..storage[0].len]); + fastmem.copy(T, storage[1], slice[storage[0].len..]); + } + + /// Clear the buffer. + pub fn clear(self: *Self) void { + self.head = 0; + self.tail = 0; + self.full = false; + } + + /// Iterate over the circular buffer. + pub fn iterator(self: Self, direction: Iterator.Direction) Iterator { + return Iterator{ + .buf = self, + .idx = 0, + .direction = direction, + }; + } + + /// Get the first (oldest) value in the buffer. + pub fn first(self: Self) ?*T { + // Note: this can be more efficient by not using the + // iterator, but this was an easy way to implement it. + var it = self.iterator(.forward); + return it.next(); + } + + /// Get the last (newest) value in the buffer. + pub fn last(self: Self) ?*T { + // Note: this can be more efficient by not using the + // iterator, but this was an easy way to implement it. + var it = self.iterator(.reverse); + return it.next(); + } + + /// Ensures that there is enough capacity to store amount more + /// items via append. + pub fn ensureUnusedCapacity( + self: *Self, + alloc: Allocator, + amount: usize, + ) Allocator.Error!void { + const new_cap = self.len() + amount; + if (new_cap <= self.capacity()) return; + try self.resize(alloc, new_cap); + } + + /// Resize the buffer to the given size (larger or smaller). + /// If larger, new values will be set to the default value. + pub fn resize(self: *Self, alloc: Allocator, size: usize) Allocator.Error!void { + // Rotate to zero so it is aligned. + try self.rotateToZero(); + + // Reallocate, this adds to the end so we're ready to go. + const prev_len = self.len(); + const prev_cap = self.storage.len; + self.storage = try alloc.realloc(self.storage, size); + + // If we grew, we need to set our new defaults. We can add it + // at the end since we rotated to start. + if (size > prev_cap) { + @memset(self.storage[prev_cap..], default); + + // Fix up our head/tail + if (self.full) { + self.head = prev_len; + self.full = false; + } + } + } + + /// Rotate the data so that it is zero-aligned. + fn rotateToZero(self: *Self) Allocator.Error!void { + // If we're already at zero then do nothing. + if (self.tail == 0) return; + + // We use std.mem.rotate to rotate our storage in-place. + std.mem.rotate(T, self.storage, self.tail); + + // Then fix up our head and tail. + self.head = self.len() % self.storage.len; + self.tail = 0; + } + + /// Returns if the buffer is currently empty. To check if its + /// full, just check the "full" attribute. + pub fn empty(self: Self) bool { + return !self.full and self.head == self.tail; + } + + /// Returns the total capacity allocated for this buffer. + pub fn capacity(self: Self) usize { + return self.storage.len; + } + + /// Returns the length in elements that are used. + pub fn len(self: Self) usize { + if (self.full) return self.storage.len; + if (self.head >= self.tail) return self.head - self.tail; + return self.storage.len - (self.tail - self.head); + } + + /// Delete the oldest n values from the buffer. If there are less + /// than n values in the buffer, it'll delete everything. + pub fn deleteOldest(self: *Self, n: usize) void { + assert(n <= self.storage.len); + + // Special case n == 0 otherwise we will accidentally break + // our circular buffer. + if (n == 0) { + @branchHint(.cold); + return; + } + + // Clear the values back to default + const slices = self.getPtrSlice(0, n); + inline for (slices) |slice| @memset(slice, default); + + // If we're not full, we can just advance the tail. We know + // it'll be less than the length because otherwise we'd be full. + self.tail += @min(self.len(), n); + if (self.tail >= self.storage.len) self.tail -= self.storage.len; + self.full = false; + } + + /// Returns a pointer to the value at offset with the given length, + /// and considers this full amount of data "written" if it is beyond + /// the end of our buffer. This never "rotates" the buffer because + /// the offset can only be within the size of the buffer. + pub fn getPtrSlice(self: *Self, offset: usize, slice_len: usize) [2][]T { + // Special case the empty slice fast-path. + if (slice_len == 0) { + @branchHint(.cold); + return .{ &.{}, &.{} }; + } + + // Note: this assertion is very important, it hints the compiler + // which generates ~10% faster code than without it. + assert(offset + slice_len <= self.capacity()); + + // End offset is the last offset (exclusive) for our slice. + // We use exclusive because it makes the math easier and it + // matches Zigs slicing parameterization. + const end_offset = offset + slice_len; + + // If our slice can't fit it in our length, then we need to advance. + if (end_offset > self.len()) self.advance(end_offset - self.len()); + + // Our start and end indexes into the storage buffer + const start_idx = self.storageOffset(offset); + const end_idx = self.storageOffset(end_offset - 1); + // std.log.warn("A={} B={}", .{ start_idx, end_idx }); + + // Optimistically, our data fits in one slice + if (end_idx >= start_idx) { + return .{ + self.storage[start_idx .. end_idx + 1], + self.storage[0..0], // So there is an empty slice + }; + } + + return .{ + self.storage[start_idx..], + self.storage[0 .. end_idx + 1], + }; + } + + /// Advances the head/tail so that we can store amount. + fn advance(self: *Self, amount: usize) void { + assert(amount <= self.storage.len - self.len()); + + // Optimistically add our amount + self.head += amount; + + // If we exceeded the length of the buffer, wrap around. + if (self.head >= self.storage.len) self.head = self.head - self.storage.len; + + // If we're full, we have to keep tail lined up. + if (self.full) self.tail = self.head; + + // We're full if the head reached the tail. The head can never + // pass the tail because advance asserts amount is only in + // available space left + self.full = self.head == self.tail; + } + + /// For a given offset from zero, this returns the offset in the + /// storage buffer where this data can be found. + fn storageOffset(self: Self, offset: usize) usize { + assert(offset < self.storage.len); + + // This should be subtraction ideally to avoid overflows but + // it would take a really, really, huge buffer to overflow. + const fits_offset = self.tail + offset; + if (fits_offset < self.storage.len) return fits_offset; + return fits_offset - self.storage.len; + } + }; +} + +test { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 12); + defer buf.deinit(alloc); + + try testing.expect(buf.empty()); + try testing.expectEqual(@as(usize, 0), buf.len()); +} + +test "CircBuf append" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 3); + defer buf.deinit(alloc); + + try buf.append(1); + try buf.append(2); + try buf.append(3); + try testing.expectError(error.OutOfMemory, buf.append(4)); + buf.deleteOldest(1); + try buf.append(4); + try testing.expectError(error.OutOfMemory, buf.append(5)); +} + +test "CircBuf forward iterator" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 3); + defer buf.deinit(alloc); + + // Empty + { + var it = buf.iterator(.forward); + try testing.expect(it.next() == null); + } + + // Partially full + try buf.append(1); + try buf.append(2); + { + var it = buf.iterator(.forward); + try testing.expect(it.next().?.* == 1); + try testing.expect(it.next().?.* == 2); + try testing.expect(it.next() == null); + } + + // Full + try buf.append(3); + { + var it = buf.iterator(.forward); + try testing.expect(it.next().?.* == 1); + try testing.expect(it.next().?.* == 2); + try testing.expect(it.next().?.* == 3); + try testing.expect(it.next() == null); + } + + // Delete and add + buf.deleteOldest(1); + try buf.append(4); + { + var it = buf.iterator(.forward); + try testing.expect(it.next().?.* == 2); + try testing.expect(it.next().?.* == 3); + try testing.expect(it.next().?.* == 4); + try testing.expect(it.next() == null); + } +} + +test "CircBuf reverse iterator" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 3); + defer buf.deinit(alloc); + + // Empty + { + var it = buf.iterator(.reverse); + try testing.expect(it.next() == null); + } + + // Partially full + try buf.append(1); + try buf.append(2); + { + var it = buf.iterator(.reverse); + try testing.expect(it.next().?.* == 2); + try testing.expect(it.next().?.* == 1); + try testing.expect(it.next() == null); + } + + // Full + try buf.append(3); + { + var it = buf.iterator(.reverse); + try testing.expect(it.next().?.* == 3); + try testing.expect(it.next().?.* == 2); + try testing.expect(it.next().?.* == 1); + try testing.expect(it.next() == null); + } + + // Delete and add + buf.deleteOldest(1); + try buf.append(4); + { + var it = buf.iterator(.reverse); + try testing.expect(it.next().?.* == 4); + try testing.expect(it.next().?.* == 3); + try testing.expect(it.next().?.* == 2); + try testing.expect(it.next() == null); + } +} + +test "CircBuf first/last" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 3); + defer buf.deinit(alloc); + + try buf.append(1); + try buf.append(2); + try buf.append(3); + try testing.expectEqual(3, buf.last().?.*); + try testing.expectEqual(1, buf.first().?.*); +} + +test "CircBuf first/last empty" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 0); + defer buf.deinit(alloc); + + try testing.expect(buf.first() == null); + try testing.expect(buf.last() == null); +} + +test "CircBuf first/last empty with cap" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 3); + defer buf.deinit(alloc); + + try testing.expect(buf.first() == null); + try testing.expect(buf.last() == null); +} + +test "CircBuf append slice" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 5); + defer buf.deinit(alloc); + + buf.appendSliceAssumeCapacity("hello"); + { + var it = buf.iterator(.forward); + try testing.expect(it.next().?.* == 'h'); + try testing.expect(it.next().?.* == 'e'); + try testing.expect(it.next().?.* == 'l'); + try testing.expect(it.next().?.* == 'l'); + try testing.expect(it.next().?.* == 'o'); + try testing.expect(it.next() == null); + } +} + +test "CircBuf append slice with wrap" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 4); + defer buf.deinit(alloc); + + // Fill the buffer + _ = buf.getPtrSlice(0, buf.capacity()); + try testing.expect(buf.full); + try testing.expectEqual(@as(usize, 4), buf.len()); + + // Delete + buf.deleteOldest(2); + try testing.expect(!buf.full); + try testing.expectEqual(@as(usize, 2), buf.len()); + + buf.appendSliceAssumeCapacity("AB"); + { + var it = buf.iterator(.forward); + try testing.expect(it.next().?.* == 0); + try testing.expect(it.next().?.* == 0); + try testing.expect(it.next().?.* == 'A'); + try testing.expect(it.next().?.* == 'B'); + try testing.expect(it.next() == null); + } +} + +test "CircBuf getPtrSlice fits" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 12); + defer buf.deinit(alloc); + + const slices = buf.getPtrSlice(0, 11); + try testing.expectEqual(@as(usize, 11), slices[0].len); + try testing.expectEqual(@as(usize, 0), slices[1].len); + try testing.expectEqual(@as(usize, 11), buf.len()); +} + +test "CircBuf getPtrSlice wraps" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 4); + defer buf.deinit(alloc); + + // Fill the buffer + _ = buf.getPtrSlice(0, buf.capacity()); + try testing.expect(buf.full); + try testing.expectEqual(@as(usize, 4), buf.len()); + + // Delete + buf.deleteOldest(2); + try testing.expect(!buf.full); + try testing.expectEqual(@as(usize, 2), buf.len()); + + // Get a slice that doesn't grow + { + const slices = buf.getPtrSlice(0, 2); + try testing.expectEqual(@as(usize, 2), slices[0].len); + try testing.expectEqual(@as(usize, 0), slices[1].len); + try testing.expectEqual(@as(usize, 2), buf.len()); + slices[0][0] = 1; + slices[0][1] = 2; + } + + // Get a slice that does grow, and forces wrap + { + const slices = buf.getPtrSlice(2, 2); + try testing.expectEqual(@as(usize, 2), slices[0].len); + try testing.expectEqual(@as(usize, 0), slices[1].len); + try testing.expectEqual(@as(usize, 4), buf.len()); + + // should be empty + try testing.expectEqual(@as(u8, 0), slices[0][0]); + try testing.expectEqual(@as(u8, 0), slices[0][1]); + slices[0][0] = 3; + slices[0][1] = 4; + } + + // Get a slice across boundaries + { + const slices = buf.getPtrSlice(0, 4); + try testing.expectEqual(@as(usize, 2), slices[0].len); + try testing.expectEqual(@as(usize, 2), slices[1].len); + try testing.expectEqual(@as(usize, 4), buf.len()); + + try testing.expectEqual(@as(u8, 1), slices[0][0]); + try testing.expectEqual(@as(u8, 2), slices[0][1]); + try testing.expectEqual(@as(u8, 3), slices[1][0]); + try testing.expectEqual(@as(u8, 4), slices[1][1]); + } +} + +test "CircBuf rotateToZero" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 12); + defer buf.deinit(alloc); + + _ = buf.getPtrSlice(0, 11); + try buf.rotateToZero(); +} + +test "CircBuf rotateToZero offset" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 4); + defer buf.deinit(alloc); + + // Fill the buffer + _ = buf.getPtrSlice(0, 3); + try testing.expectEqual(@as(usize, 3), buf.len()); + + // Delete + buf.deleteOldest(2); + try testing.expect(!buf.full); + try testing.expectEqual(@as(usize, 1), buf.len()); + try testing.expect(buf.tail > 0 and buf.head >= buf.tail); + + // Rotate to zero + try buf.rotateToZero(); + try testing.expectEqual(@as(usize, 0), buf.tail); + try testing.expectEqual(@as(usize, 1), buf.head); +} + +test "CircBuf rotateToZero wraps" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 4); + defer buf.deinit(alloc); + + // Fill the buffer + _ = buf.getPtrSlice(0, 3); + try testing.expectEqual(@as(usize, 3), buf.len()); + try testing.expect(buf.tail == 0 and buf.head == 3); + + // Delete all + buf.deleteOldest(3); + try testing.expectEqual(@as(usize, 0), buf.len()); + try testing.expect(buf.tail == 3 and buf.head == 3); + + // Refill to force a wrap + { + const slices = buf.getPtrSlice(0, 3); + slices[0][0] = 1; + slices[1][0] = 2; + slices[1][1] = 3; + try testing.expectEqual(@as(usize, 3), buf.len()); + try testing.expect(buf.tail == 3 and buf.head == 2); + } + + // Rotate to zero + try buf.rotateToZero(); + try testing.expectEqual(@as(usize, 0), buf.tail); + try testing.expectEqual(@as(usize, 3), buf.head); + { + const slices = buf.getPtrSlice(0, 3); + try testing.expectEqual(@as(u8, 1), slices[0][0]); + try testing.expectEqual(@as(u8, 2), slices[0][1]); + try testing.expectEqual(@as(u8, 3), slices[0][2]); + } +} + +test "CircBuf rotateToZero full no wrap" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 4); + defer buf.deinit(alloc); + + // Fill the buffer + _ = buf.getPtrSlice(0, 3); + + // Delete all + buf.deleteOldest(3); + + // Refill to force a wrap + { + const slices = buf.getPtrSlice(0, 4); + try testing.expect(buf.full); + slices[0][0] = 1; + slices[1][0] = 2; + slices[1][1] = 3; + slices[1][2] = 4; + } + + // Rotate to zero + try buf.rotateToZero(); + try testing.expect(buf.full); + try testing.expectEqual(@as(usize, 0), buf.tail); + try testing.expectEqual(@as(usize, 0), buf.head); + { + const slices = buf.getPtrSlice(0, 4); + try testing.expectEqual(@as(u8, 1), slices[0][0]); + try testing.expectEqual(@as(u8, 2), slices[0][1]); + try testing.expectEqual(@as(u8, 3), slices[0][2]); + try testing.expectEqual(@as(u8, 4), slices[0][3]); + } +} + +test "CircBuf resize grow from zero" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 0); + defer buf.deinit(alloc); + try testing.expect(buf.full); + + // Resize + try buf.resize(alloc, 2); + try testing.expect(!buf.full); + try testing.expectEqual(@as(usize, 0), buf.len()); + try testing.expectEqual(@as(usize, 2), buf.capacity()); + + try buf.append(1); + try buf.append(2); + + { + const slices = buf.getPtrSlice(0, 2); + try testing.expectEqual(@as(u8, 1), slices[0][0]); + try testing.expectEqual(@as(u8, 2), slices[0][1]); + } +} + +test "CircBuf resize grow" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 4); + defer buf.deinit(alloc); + + // Fill and write + { + const slices = buf.getPtrSlice(0, 4); + try testing.expect(buf.full); + slices[0][0] = 1; + slices[0][1] = 2; + slices[0][2] = 3; + slices[0][3] = 4; + } + + // Resize + try buf.resize(alloc, 6); + try testing.expect(!buf.full); + try testing.expectEqual(@as(usize, 4), buf.len()); + try testing.expectEqual(@as(usize, 6), buf.capacity()); + + { + const slices = buf.getPtrSlice(0, 4); + try testing.expectEqual(@as(u8, 1), slices[0][0]); + try testing.expectEqual(@as(u8, 2), slices[0][1]); + try testing.expectEqual(@as(u8, 3), slices[0][2]); + try testing.expectEqual(@as(u8, 4), slices[0][3]); + } +} + +test "CircBuf resize shrink" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 4); + defer buf.deinit(alloc); + + // Fill and write + { + const slices = buf.getPtrSlice(0, 4); + try testing.expect(buf.full); + slices[0][0] = 1; + slices[0][1] = 2; + slices[0][2] = 3; + slices[0][3] = 4; + } + + // Resize + try buf.resize(alloc, 3); + try testing.expect(buf.full); + try testing.expectEqual(@as(usize, 3), buf.len()); + try testing.expectEqual(@as(usize, 3), buf.capacity()); + + { + const slices = buf.getPtrSlice(0, 3); + try testing.expectEqual(@as(u8, 1), slices[0][0]); + try testing.expectEqual(@as(u8, 2), slices[0][1]); + try testing.expectEqual(@as(u8, 3), slices[0][2]); + } +} + +test "CircBuf append empty slice" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 5); + defer buf.deinit(alloc); + + // Appending an empty slice to empty buffer should be a no-op + buf.appendSliceAssumeCapacity(""); + try testing.expectEqual(@as(usize, 0), buf.len()); + try testing.expect(!buf.full); + + // Buffer should still work normally after appending empty slice + buf.appendSliceAssumeCapacity("hi"); + try testing.expectEqual(@as(usize, 2), buf.len()); + + // Appending an empty slice to non-empty buffer should also be a no-op + buf.appendSliceAssumeCapacity(""); + try testing.expectEqual(@as(usize, 2), buf.len()); +} + +test "CircBuf getPtrSlice zero length" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 5); + defer buf.deinit(alloc); + + // getPtrSlice with zero length on empty buffer should return empty slices + const slices = buf.getPtrSlice(0, 0); + try testing.expectEqual(@as(usize, 0), slices[0].len); + try testing.expectEqual(@as(usize, 0), slices[1].len); + try testing.expectEqual(@as(usize, 0), buf.len()); + + // Fill buffer partially + buf.appendSliceAssumeCapacity("abc"); + try testing.expectEqual(@as(usize, 3), buf.len()); + + // getPtrSlice with zero length on non-empty buffer should also work + const slices2 = buf.getPtrSlice(0, 0); + try testing.expectEqual(@as(usize, 0), slices2[0].len); + try testing.expectEqual(@as(usize, 0), slices2[1].len); + try testing.expectEqual(@as(usize, 3), buf.len()); +} + +test "CircBuf deleteOldest zero" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 5); + defer buf.deinit(alloc); + + // deleteOldest(0) on empty buffer should be a no-op + buf.deleteOldest(0); + try testing.expectEqual(@as(usize, 0), buf.len()); + + // Fill buffer + buf.appendSliceAssumeCapacity("hello"); + try testing.expectEqual(@as(usize, 5), buf.len()); + + // deleteOldest(0) on non-empty buffer should be a no-op + buf.deleteOldest(0); + try testing.expectEqual(@as(usize, 5), buf.len()); + + // Verify data is unchanged + var it = buf.iterator(.forward); + try testing.expectEqual(@as(u8, 'h'), it.next().?.*); +} diff --git a/vendor/libghostty-vt/src/datastruct/comparison.zig b/vendor/libghostty-vt/src/datastruct/comparison.zig new file mode 100644 index 00000000..6a862bb9 --- /dev/null +++ b/vendor/libghostty-vt/src/datastruct/comparison.zig @@ -0,0 +1,430 @@ +// The contents of this file is largely based on testing.zig from the Zig 0.15.1 +// stdlib, distributed under the MIT license, copyright (c) Zig contributors +const std = @import("std"); +const testing = std.testing; + +/// A deep equality comparison function that works for most types. We +/// add types as necessary. It defers to `equal` decls on types that support +/// decls. +pub fn deepEqual(comptime T: type, old: T, new: T) bool { + // Do known named types first + switch (T) { + inline []const u8, + [:0]const u8, + => return std.mem.eql(u8, old, new), + + []const [:0]const u8, + => { + if (old.len != new.len) return false; + for (old, new) |a, b| { + if (!std.mem.eql(u8, a, b)) return false; + } + + return true; + }, + + else => {}, + } + + // Back into types of types + switch (@typeInfo(T)) { + .void => return true, + + inline .bool, + .int, + .float, + .@"enum", + => return old == new, + + .optional => |info| { + if (old == null and new == null) return true; + if (old == null or new == null) return false; + return deepEqual(info.child, old.?, new.?); + }, + + .array => |info| for (old, new) |old_elem, new_elem| { + if (!deepEqual( + info.child, + old_elem, + new_elem, + )) return false; + } else return true, + + .@"struct" => |info| { + if (@hasDecl(T, "equal")) return old.equal(new); + + // If a struct doesn't declare an "equal" function, we fall back + // to a recursive field-by-field compare. + inline for (info.fields) |field_info| { + if (!deepEqual( + field_info.type, + @field(old, field_info.name), + @field(new, field_info.name), + )) return false; + } + return true; + }, + + .@"union" => |info| { + if (@hasDecl(T, "equal")) return old.equal(new); + + const tag_type = info.tag_type.?; + const old_tag = std.meta.activeTag(old); + const new_tag = std.meta.activeTag(new); + if (old_tag != new_tag) return false; + + inline for (info.fields) |field_info| { + if (@field(tag_type, field_info.name) == old_tag) { + return deepEqual( + field_info.type, + @field(old, field_info.name), + @field(new, field_info.name), + ); + } + } + + unreachable; + }, + + else => { + @compileLog(T); + @compileError("unsupported field type"); + }, + } +} + +/// Generic, recursive equality testing utility using approximate comparison for +/// floats and equality for everything else +/// +/// Based on `testing.expectEqual` and `testing.expectEqualSlices`. +/// +/// The relative tolerance is currently hardcoded to `sqrt(eps(float_type))`. +pub inline fn expectApproxEqual(expected: anytype, actual: anytype) !void { + const T = @TypeOf(expected, actual); + return expectApproxEqualInner(T, expected, actual); +} + +fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { + switch (@typeInfo(T)) { + // check approximate equality for floats + .float => { + const sqrt_eps = comptime std.math.sqrt(std.math.floatEps(T)); + if (!std.math.approxEqRel(T, expected, actual, sqrt_eps)) { + print("expected approximately {any}, found {any}\n", .{ expected, actual }); + return error.TestExpectedApproxEqual; + } + }, + + // recurse into containers + .array => { + const diff_index: usize = diff_index: { + const shortest = @min(expected.len, actual.len); + var index: usize = 0; + while (index < shortest) : (index += 1) { + expectApproxEqual(actual[index], expected[index]) catch break :diff_index index; + } + break :diff_index if (expected.len == actual.len) return else shortest; + }; + print("slices not approximately equal. first significant difference occurs at index {d} (0x{X})\n", .{ diff_index, diff_index }); + return error.TestExpectedApproxEqual; + }, + .vector => |info| { + var i: usize = 0; + while (i < info.len) : (i += 1) { + expectApproxEqual(expected[i], actual[i]) catch { + print("index {d} incorrect. expected approximately {any}, found {any}\n", .{ + i, expected[i], actual[i], + }); + return error.TestExpectedApproxEqual; + }; + } + }, + .@"struct" => |structType| { + inline for (structType.fields) |field| { + try expectApproxEqual(@field(expected, field.name), @field(actual, field.name)); + } + }, + + // unwrap unions, optionals, and error unions + .@"union" => |union_info| { + if (union_info.tag_type == null) { + // untagged unions can only be compared bitwise, + // so expectEqual is all we need + testing.expectEqual(expected, actual) catch { + return error.TestExpectedApproxEqual; + }; + } + + const Tag = std.meta.Tag(@TypeOf(expected)); + + const expectedTag = @as(Tag, expected); + const actualTag = @as(Tag, actual); + + testing.expectEqual(expectedTag, actualTag) catch { + return error.TestExpectedApproxEqual; + }; + + // we only reach this switch if the tags are equal + switch (expected) { + inline else => |val, tag| try expectApproxEqual(val, @field(actual, @tagName(tag))), + } + }, + .optional, .error_union => { + if (expected) |expected_payload| if (actual) |actual_payload| { + return expectApproxEqual(expected_payload, actual_payload); + }; + // we only reach this point if there's at least one null or error, + // in which case expectEqual is all we need + testing.expectEqual(expected, actual) catch { + return error.TestExpectedApproxEqual; + }; + }, + + // fall back to expectEqual for everything else + else => testing.expectEqual(expected, actual) catch { + return error.TestExpectedApproxEqual; + }, + } +} + +/// Copy of testing.print (not public) +fn print(comptime fmt: []const u8, args: anytype) void { + if (@inComptime()) { + @compileError(std.fmt.comptimePrint(fmt, args)); + } else if (testing.backend_can_print) { + std.debug.print(fmt, args); + } +} + +// Tests based on the `expectEqual` tests in the Zig stdlib +test "expectApproxEqual.union(enum)" { + const T = union(enum) { + a: i32, + b: f32, + }; + + const b10 = T{ .b = 10.0 }; + const b10plus = T{ .b = 10.000001 }; + + try expectApproxEqual(b10, b10plus); +} + +test "expectApproxEqual nested array" { + const a = [2][2]f32{ + [_]f32{ 1.0, 0.0 }, + [_]f32{ 0.0, 1.0 }, + }; + + const b = [2][2]f32{ + [_]f32{ 1.000001, 0.0 }, + [_]f32{ 0.0, 0.999999 }, + }; + + try expectApproxEqual(a, b); +} + +test "expectApproxEqual vector" { + const a: @Vector(4, f32) = @splat(4.0); + const b: @Vector(4, f32) = @splat(4.000001); + + try expectApproxEqual(a, b); +} + +test "expectApproxEqual struct" { + const a = .{ 1, @as(f32, 1.0) }; + const b = .{ 1, @as(f32, 0.999999) }; + + try expectApproxEqual(a, b); +} + +test "deepEqual void" { + try testing.expect(deepEqual(void, {}, {})); +} + +test "deepEqual bool" { + try testing.expect(deepEqual(bool, true, true)); + try testing.expect(deepEqual(bool, false, false)); + try testing.expect(!deepEqual(bool, true, false)); + try testing.expect(!deepEqual(bool, false, true)); +} + +test "deepEqual int" { + try testing.expect(deepEqual(i32, 42, 42)); + try testing.expect(deepEqual(i32, -100, -100)); + try testing.expect(!deepEqual(i32, 42, 43)); + try testing.expect(deepEqual(u64, 0, 0)); + try testing.expect(!deepEqual(u64, 0, 1)); +} + +test "deepEqual float" { + try testing.expect(deepEqual(f32, 1.0, 1.0)); + try testing.expect(!deepEqual(f32, 1.0, 1.1)); + try testing.expect(deepEqual(f64, 3.14159, 3.14159)); + try testing.expect(!deepEqual(f64, 3.14159, 3.14158)); +} + +test "deepEqual enum" { + const Color = enum { red, green, blue }; + try testing.expect(deepEqual(Color, .red, .red)); + try testing.expect(deepEqual(Color, .blue, .blue)); + try testing.expect(!deepEqual(Color, .red, .green)); + try testing.expect(!deepEqual(Color, .green, .blue)); +} + +test "deepEqual []const u8" { + try testing.expect(deepEqual([]const u8, "hello", "hello")); + try testing.expect(deepEqual([]const u8, "", "")); + try testing.expect(!deepEqual([]const u8, "hello", "world")); + try testing.expect(!deepEqual([]const u8, "hello", "hell")); + try testing.expect(!deepEqual([]const u8, "hello", "hello!")); +} + +test "deepEqual [:0]const u8" { + try testing.expect(deepEqual([:0]const u8, "foo", "foo")); + try testing.expect(!deepEqual([:0]const u8, "foo", "bar")); + try testing.expect(!deepEqual([:0]const u8, "foo", "fo")); +} + +test "deepEqual []const [:0]const u8" { + const a: []const [:0]const u8 = &.{ "one", "two", "three" }; + const b: []const [:0]const u8 = &.{ "one", "two", "three" }; + const c: []const [:0]const u8 = &.{ "one", "two" }; + const d: []const [:0]const u8 = &.{ "one", "two", "four" }; + const e: []const [:0]const u8 = &.{}; + + try testing.expect(deepEqual([]const [:0]const u8, a, b)); + try testing.expect(!deepEqual([]const [:0]const u8, a, c)); + try testing.expect(!deepEqual([]const [:0]const u8, a, d)); + try testing.expect(deepEqual([]const [:0]const u8, e, e)); + try testing.expect(!deepEqual([]const [:0]const u8, a, e)); +} + +test "deepEqual optional" { + try testing.expect(deepEqual(?i32, null, null)); + try testing.expect(deepEqual(?i32, 42, 42)); + try testing.expect(!deepEqual(?i32, null, 42)); + try testing.expect(!deepEqual(?i32, 42, null)); + try testing.expect(!deepEqual(?i32, 42, 43)); +} + +test "deepEqual optional nested" { + const Nested = struct { x: i32, y: i32 }; + try testing.expect(deepEqual(?Nested, null, null)); + try testing.expect(deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 2 })); + try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 3 })); + try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, null)); +} + +test "deepEqual array" { + try testing.expect(deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 3 })); + try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 4 })); + try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 0, 2, 3 })); + try testing.expect(deepEqual([0]i32, .{}, .{})); +} + +test "deepEqual nested array" { + const a = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } }; + const b = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } }; + const c = [2][2]i32{ .{ 1, 2 }, .{ 3, 5 } }; + + try testing.expect(deepEqual([2][2]i32, a, b)); + try testing.expect(!deepEqual([2][2]i32, a, c)); +} + +test "deepEqual struct" { + const Point = struct { x: i32, y: i32 }; + try testing.expect(deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 20 })); + try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 21 })); + try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 11, .y = 20 })); +} + +test "deepEqual struct nested" { + const Inner = struct { value: i32 }; + const Outer = struct { a: Inner, b: Inner }; + + const x = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } }; + const y = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } }; + const z = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 3 } }; + + try testing.expect(deepEqual(Outer, x, y)); + try testing.expect(!deepEqual(Outer, x, z)); +} + +test "deepEqual struct with equal decl" { + const Custom = struct { + value: i32, + + pub fn equal(self: @This(), other: @This()) bool { + return @mod(self.value, 10) == @mod(other.value, 10); + } + }; + + try testing.expect(deepEqual(Custom, .{ .value = 5 }, .{ .value = 15 })); + try testing.expect(deepEqual(Custom, .{ .value = 100 }, .{ .value = 0 })); + try testing.expect(!deepEqual(Custom, .{ .value = 5 }, .{ .value = 6 })); +} + +test "deepEqual union" { + const Value = union(enum) { + int: i32, + float: f32, + none, + }; + + try testing.expect(deepEqual(Value, .{ .int = 42 }, .{ .int = 42 })); + try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .int = 43 })); + try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .float = 42.0 })); + try testing.expect(deepEqual(Value, .none, .none)); + try testing.expect(!deepEqual(Value, .none, .{ .int = 0 })); +} + +test "deepEqual union with equal decl" { + const Value = union(enum) { + num: i32, + str: []const u8, + + pub fn equal(self: @This(), other: @This()) bool { + return switch (self) { + .num => |n| switch (other) { + .num => |m| @mod(n, 10) == @mod(m, 10), + else => false, + }, + .str => |s| switch (other) { + .str => |t| s.len == t.len, + else => false, + }, + }; + } + }; + + try testing.expect(deepEqual(Value, .{ .num = 5 }, .{ .num = 25 })); + try testing.expect(!deepEqual(Value, .{ .num = 5 }, .{ .num = 6 })); + try testing.expect(deepEqual(Value, .{ .str = "abc" }, .{ .str = "xyz" })); + try testing.expect(!deepEqual(Value, .{ .str = "abc" }, .{ .str = "ab" })); +} + +test "deepEqual array of structs" { + const Item = struct { id: i32, name: []const u8 }; + const a = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } }; + const b = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } }; + const c = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "TWO" } }; + + try testing.expect(deepEqual([2]Item, a, b)); + try testing.expect(!deepEqual([2]Item, a, c)); +} + +test "deepEqual struct with optional field" { + const Config = struct { name: []const u8, port: ?u16 }; + + try testing.expect(deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8080 })); + try testing.expect(deepEqual(Config, .{ .name = "app", .port = null }, .{ .name = "app", .port = null })); + try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = null })); + try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8081 })); +} + +test "deepEqual struct with array field" { + const Data = struct { values: [3]i32 }; + + try testing.expect(deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 3 } })); + try testing.expect(!deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 4 } })); +} diff --git a/vendor/libghostty-vt/src/datastruct/intrusive_linked_list.zig b/vendor/libghostty-vt/src/datastruct/intrusive_linked_list.zig new file mode 100644 index 00000000..734b82ff --- /dev/null +++ b/vendor/libghostty-vt/src/datastruct/intrusive_linked_list.zig @@ -0,0 +1,181 @@ +const std = @import("std"); +const testing = std.testing; + +/// An intrusive doubly-linked list. The type T must have a "next" and "prev" +/// field pointing to itself. +/// +/// This is an adaptation of the DoublyLinkedList from the Zig standard +/// library, which is MIT licensed. I've removed functionality that I don't +/// need. +pub fn DoublyLinkedList(comptime T: type) type { + return struct { + const Self = @This(); + + /// The type of the node in the list. This makes it easy to get the + /// node type from the list type. + pub const Node = T; + + first: ?*Node = null, + last: ?*Node = null, + + /// Insert a new node after an existing one. + /// + /// Arguments: + /// node: Pointer to a node in the list. + /// new_node: Pointer to the new node to insert. + pub inline fn insertAfter(list: *Self, node: *Node, new_node: *Node) void { + new_node.prev = node; + if (node.next) |next_node| { + // Intermediate node. + new_node.next = next_node; + next_node.prev = new_node; + } else { + // Last element of the list. + new_node.next = null; + list.last = new_node; + } + node.next = new_node; + } + + /// Insert a new node before an existing one. + /// + /// Arguments: + /// node: Pointer to a node in the list. + /// new_node: Pointer to the new node to insert. + pub inline fn insertBefore(list: *Self, node: *Node, new_node: *Node) void { + new_node.next = node; + if (node.prev) |prev_node| { + // Intermediate node. + new_node.prev = prev_node; + prev_node.next = new_node; + } else { + // First element of the list. + new_node.prev = null; + list.first = new_node; + } + node.prev = new_node; + } + + /// Insert a new node at the end of the list. + /// + /// Arguments: + /// new_node: Pointer to the new node to insert. + pub inline fn append(list: *Self, new_node: *Node) void { + if (list.last) |last| { + // Insert after last. + list.insertAfter(last, new_node); + } else { + // Empty list. + list.prepend(new_node); + } + } + + /// Insert a new node at the beginning of the list. + /// + /// Arguments: + /// new_node: Pointer to the new node to insert. + pub inline fn prepend(list: *Self, new_node: *Node) void { + if (list.first) |first| { + // Insert before first. + list.insertBefore(first, new_node); + } else { + // Empty list. + list.first = new_node; + list.last = new_node; + new_node.prev = null; + new_node.next = null; + } + } + + /// Remove a node from the list. + /// + /// Arguments: + /// node: Pointer to the node to be removed. + pub inline fn remove(list: *Self, node: *Node) void { + if (node.prev) |prev_node| { + // Intermediate node. + prev_node.next = node.next; + } else { + // First element of the list. + list.first = node.next; + } + + if (node.next) |next_node| { + // Intermediate node. + next_node.prev = node.prev; + } else { + // Last element of the list. + list.last = node.prev; + } + } + + /// Remove and return the last node in the list. + /// + /// Returns: + /// A pointer to the last node in the list. + pub inline fn pop(list: *Self) ?*Node { + const last = list.last orelse return null; + list.remove(last); + return last; + } + + /// Remove and return the first node in the list. + /// + /// Returns: + /// A pointer to the first node in the list. + pub inline fn popFirst(list: *Self) ?*Node { + const first = list.first orelse return null; + list.remove(first); + return first; + } + }; +} + +test "basic DoublyLinkedList test" { + const Node = struct { + data: u32, + prev: ?*@This() = null, + next: ?*@This() = null, + }; + const L = DoublyLinkedList(Node); + var list: L = .{}; + + var one: Node = .{ .data = 1 }; + var two: Node = .{ .data = 2 }; + var three: Node = .{ .data = 3 }; + var four: Node = .{ .data = 4 }; + var five: Node = .{ .data = 5 }; + + list.append(&two); // {2} + list.append(&five); // {2, 5} + list.prepend(&one); // {1, 2, 5} + list.insertBefore(&five, &four); // {1, 2, 4, 5} + list.insertAfter(&two, &three); // {1, 2, 3, 4, 5} + + // Traverse forwards. + { + var it = list.first; + var index: u32 = 1; + while (it) |node| : (it = node.next) { + try testing.expect(node.data == index); + index += 1; + } + } + + // Traverse backwards. + { + var it = list.last; + var index: u32 = 1; + while (it) |node| : (it = node.prev) { + try testing.expect(node.data == (6 - index)); + index += 1; + } + } + + _ = list.popFirst(); // {2, 3, 4, 5} + _ = list.pop(); // {2, 3, 4} + list.remove(&three); // {2, 4} + + try testing.expect(list.first.?.data == 2); + try testing.expect(list.last.?.data == 4); +} diff --git a/vendor/libghostty-vt/src/datastruct/lru.zig b/vendor/libghostty-vt/src/datastruct/lru.zig new file mode 100644 index 00000000..83d2cf8e --- /dev/null +++ b/vendor/libghostty-vt/src/datastruct/lru.zig @@ -0,0 +1,349 @@ +const std = @import("std"); +const assert = @import("../quirks.zig").inlineAssert; +const Allocator = std.mem.Allocator; + +/// Create a HashMap for a key type that can be automatically hashed. +/// If you want finer-grained control, use HashMap directly. +pub fn AutoHashMap(comptime K: type, comptime V: type) type { + return HashMap( + K, + V, + std.hash_map.AutoContext(K), + std.hash_map.default_max_load_percentage, + ); +} + +/// HashMap implementation that supports least-recently-used eviction. +/// +/// Beware of the Zig bug where a hashmap gets slower over time +/// (https://github.com/ziglang/zig/issues/17851). This LRU uses a hashmap +/// and evictions will cause this issue to appear. Callers should keep +/// track of eviction counts and periodically reinitialize the LRU to +/// avoid this issue. The LRU itself can't do this because it doesn't +/// know how to free values. +/// +/// Note: This is a really elementary CS101 version of an LRU right now. +/// This is done initially to get something working. Once we have it working, +/// we can benchmark and improve if this ends up being a source of slowness. +pub fn HashMap( + comptime K: type, + comptime V: type, + comptime Context: type, + comptime max_load_percentage: u64, +) type { + return struct { + const Self = @This(); + const Queue = std.DoublyLinkedList; + const Map = std.HashMapUnmanaged( + K, + *Entry, + Context, + max_load_percentage, + ); + + /// Map to maintain our entries. + map: Map, + + /// Queue to maintain LRU order. + queue: Queue, + + /// The capacity of our map. If this capacity is reached, cache + /// misses will begin evicting entries. + capacity: Map.Size, + + const Entry = struct { + data: KV, + node: Queue.Node, + + fn fromNode(node: *Queue.Node) *Entry { + return @fieldParentPtr("node", node); + } + }; + + pub const KV = struct { + key: K, + value: V, + }; + + /// The result of a getOrPut operation. + pub const GetOrPutResult = struct { + /// The entry that was retrieved. If found_existing is false, + /// then this is a pointer to allocated space to store a V. + /// If found_existing is true, the pointer value is valid, but + /// can be overwritten. + value_ptr: *V, + + /// Whether an existing value was found or not. + found_existing: bool, + + /// If another entry had to be evicted to make space for this + /// put operation, then this is the value that was evicted. + evicted: ?KV, + }; + + pub fn init(capacity: Map.Size) Self { + return .{ + .map = .{}, + .queue = .{}, + .capacity = capacity, + }; + } + + pub fn deinit(self: *Self, alloc: Allocator) void { + // Important: use our queue as a source of truth for dealloc + // because we might keep items in the queue around that aren't + // present in our LRU anymore to prevent future allocations. + var it = self.queue.first; + while (it) |node| { + it = node.next; + alloc.destroy(Entry.fromNode(node)); + } + + self.map.deinit(alloc); + self.* = undefined; + } + + /// Get or put a value for a key. See GetOrPutResult on how to check + /// if an existing value was found, if an existing value was evicted, + /// etc. + pub fn getOrPut(self: *Self, allocator: Allocator, key: K) Allocator.Error!GetOrPutResult { + if (@sizeOf(Context) != 0) + @compileError("Cannot infer context " ++ @typeName(Context) ++ ", call getOrPutContext instead."); + return self.getOrPutContext(allocator, key, undefined); + } + + /// See getOrPut + pub fn getOrPutContext( + self: *Self, + alloc: Allocator, + key: K, + ctx: Context, + ) Allocator.Error!GetOrPutResult { + const map_gop = try self.map.getOrPutContext(alloc, key, ctx); + if (map_gop.found_existing) { + // Move to end to mark as most recently used + self.queue.remove(&map_gop.value_ptr.*.node); + self.queue.append(&map_gop.value_ptr.*.node); + + return GetOrPutResult{ + .found_existing = true, + .value_ptr = &map_gop.value_ptr.*.data.value, + .evicted = null, + }; + } + errdefer _ = self.map.remove(key); + + // We're evicting if our map insertion increased our capacity. + const evict = self.map.count() > self.capacity; + + // Get our entry. If we're not evicting then we allocate a new + // entry. If we are evicting then we avoid allocation by just + // reusing the entry we would've evicted. + const entry: *Entry = if (!evict) try alloc.create(Entry) else entry: { + // Our first node is the least recently used. + const least_used_node = self.queue.popFirst().?; + const least_used_entry: *Entry = .fromNode(least_used_node); + + // Remove the least used from the map + _ = self.map.remove(least_used_entry.data.key); + + break :entry least_used_entry; + }; + errdefer if (!evict) alloc.destroy(entry); + + // Store our entry in the map. + map_gop.value_ptr.* = entry; + + // Mark the entry as most recently used + self.queue.append(&entry.node); + + // Set our key + entry.data.key = key; + + return .{ + .found_existing = map_gop.found_existing, + .value_ptr = &entry.data.value, + .evicted = if (!evict) null else entry.data, + }; + } + + /// Get a value for a key. + pub fn get(self: *const Self, key: K) ?V { + if (@sizeOf(Context) != 0) { + @compileError("getContext must be used."); + } + return self.getContext(key, undefined); + } + + /// See get + pub fn getContext(self: *const Self, key: K, ctx: Context) ?V { + const node = self.map.getContext(key, ctx) orelse return null; + return node.data.value; + } + + /// Resize the LRU. If this shrinks the LRU then LRU items will be + /// deallocated. The deallocated items are returned in the slice. This + /// slice must be freed by the caller. + pub fn resize(self: *Self, alloc: Allocator, capacity: Map.Size) Allocator.Error!?[]V { + // Fastest + if (capacity >= self.capacity) { + self.capacity = capacity; + return null; + } + + // If we're shrinking but we're smaller than the new capacity, + // then we don't have to do anything. + if (self.map.count() <= capacity) { + self.capacity = capacity; + return null; + } + + // We're shrinking and we have more items than the new capacity + const delta = self.map.count() - capacity; + var evicted = try alloc.alloc(V, delta); + + var i: Map.Size = 0; + while (i < delta) : (i += 1) { + const node = self.queue.popFirst().?; + const entry: *Entry = .fromNode(node); + evicted[i] = entry.data.value; + self.queue.remove(node); + _ = self.map.remove(entry.data.key); + alloc.destroy(entry); + } + + self.capacity = capacity; + assert(self.map.count() == capacity); + + return evicted; + } + }; +} + +test "getOrPut" { + const testing = std.testing; + const alloc = testing.allocator; + + const Map = AutoHashMap(u32, u8); + var m = Map.init(2); + defer m.deinit(alloc); + + // Insert cap values, should be hits + { + const gop = try m.getOrPut(alloc, 1); + try testing.expect(!gop.found_existing); + try testing.expect(gop.evicted == null); + gop.value_ptr.* = 1; + } + { + const gop = try m.getOrPut(alloc, 2); + try testing.expect(!gop.found_existing); + try testing.expect(gop.evicted == null); + gop.value_ptr.* = 2; + } + + // 1 is LRU + try testing.expect((try m.getOrPut(alloc, 1)).found_existing); + try testing.expect((try m.getOrPut(alloc, 2)).found_existing); + + // Next should evict + { + const gop = try m.getOrPut(alloc, 3); + try testing.expect(!gop.found_existing); + try testing.expect(gop.evicted != null); + try testing.expect(gop.evicted.?.value == 1); + gop.value_ptr.* = 3; + } + + // Currently: 2 is LRU, let's make 3 LRU + try testing.expect((try m.getOrPut(alloc, 2)).found_existing); + + // Next should evict + { + const gop = try m.getOrPut(alloc, 4); + try testing.expect(!gop.found_existing); + try testing.expect(gop.evicted != null); + try testing.expect(gop.evicted.?.value == 3); + gop.value_ptr.* = 4; + } +} + +test "get" { + const testing = std.testing; + const alloc = testing.allocator; + + const Map = AutoHashMap(u32, u8); + var m = Map.init(2); + defer m.deinit(alloc); + + // Insert cap values, should be hits + { + const gop = try m.getOrPut(alloc, 1); + try testing.expect(!gop.found_existing); + try testing.expect(gop.evicted == null); + gop.value_ptr.* = 1; + } + + try testing.expect(m.get(1) != null); + try testing.expect(m.get(1).? == 1); + try testing.expect(m.get(2) == null); +} + +test "resize shrink without removal" { + const testing = std.testing; + const alloc = testing.allocator; + + const Map = AutoHashMap(u32, u8); + var m = Map.init(2); + defer m.deinit(alloc); + + // Insert cap values, LRU is 1 + { + const gop = try m.getOrPut(alloc, 1); + try testing.expect(!gop.found_existing); + try testing.expect(gop.evicted == null); + gop.value_ptr.* = 1; + } + + // Shrink + const evicted = try m.resize(alloc, 1); + try testing.expect(evicted == null); + { + const gop = try m.getOrPut(alloc, 1); + try testing.expect(gop.found_existing); + } +} + +test "resize shrink and remove" { + const testing = std.testing; + const alloc = testing.allocator; + + const Map = AutoHashMap(u32, u8); + var m = Map.init(2); + defer m.deinit(alloc); + + // Insert cap values, LRU is 1 + { + const gop = try m.getOrPut(alloc, 1); + try testing.expect(!gop.found_existing); + try testing.expect(gop.evicted == null); + gop.value_ptr.* = 1; + } + { + const gop = try m.getOrPut(alloc, 2); + try testing.expect(!gop.found_existing); + try testing.expect(gop.evicted == null); + gop.value_ptr.* = 2; + } + + // Shrink + const evicted = try m.resize(alloc, 1); + defer alloc.free(evicted.?); + try testing.expectEqual(@as(usize, 1), evicted.?.len); + { + const gop = try m.getOrPut(alloc, 1); + try testing.expect(!gop.found_existing); + try testing.expect(gop.evicted.?.value == 2); + gop.value_ptr.* = 1; + } +} diff --git a/vendor/libghostty-vt/src/datastruct/main.zig b/vendor/libghostty-vt/src/datastruct/main.zig new file mode 100644 index 00000000..bfee2342 --- /dev/null +++ b/vendor/libghostty-vt/src/datastruct/main.zig @@ -0,0 +1,24 @@ +//! The datastruct package contains data structures or anything closely +//! related to data structures. + +const blocking_queue = @import("blocking_queue.zig"); +const cache_table = @import("cache_table.zig"); +const circ_buf = @import("circ_buf.zig"); +const intrusive_linked_list = @import("intrusive_linked_list.zig"); +const segmented_pool = @import("segmented_pool.zig"); +const split_tree = @import("split_tree.zig"); + +pub const lru = @import("lru.zig"); +pub const BlockingQueue = blocking_queue.BlockingQueue; +pub const CacheTable = cache_table.CacheTable; +pub const CircBuf = circ_buf.CircBuf; +pub const IntrusiveDoublyLinkedList = intrusive_linked_list.DoublyLinkedList; +pub const MessageData = @import("message_data.zig").MessageData; +pub const SegmentedPool = segmented_pool.SegmentedPool; +pub const SplitTree = split_tree.SplitTree; + +test { + @import("std").testing.refAllDecls(@This()); + + _ = @import("comparison.zig"); +} diff --git a/vendor/libghostty-vt/src/datastruct/message_data.zig b/vendor/libghostty-vt/src/datastruct/message_data.zig new file mode 100644 index 00000000..3e5cdae6 --- /dev/null +++ b/vendor/libghostty-vt/src/datastruct/message_data.zig @@ -0,0 +1,124 @@ +const std = @import("std"); +const assert = @import("../quirks.zig").inlineAssert; +const Allocator = std.mem.Allocator; + +/// Creates a union that can be used to accommodate data that fit within an array, +/// are a stable pointer, or require deallocation. This is helpful for thread +/// messaging utilities. +pub fn MessageData(comptime Elem: type, comptime small_size: comptime_int) type { + return union(enum) { + pub const Self = @This(); + + pub const Small = struct { + pub const Max = small_size; + pub const Array = [Max]Elem; + pub const Len = std.math.IntFittingRange(0, small_size); + data: Array = undefined, + len: Len = 0, + }; + + pub const Alloc = struct { + alloc: Allocator, + data: []Elem, + }; + + pub const Stable = []const Elem; + + /// A small write where the data fits into this union size. + small: Small, + + /// A stable pointer so we can just pass the slice directly through. + /// This is useful i.e. for const data. + stable: Stable, + + /// Allocated and must be freed with the provided allocator. This + /// should be rarely used. + alloc: Alloc, + + /// Initializes the union for a given data type. This will + /// attempt to fit into a small value if possible, otherwise + /// will allocate and put into alloc. + /// + /// This can't and will never detect stable pointers. + pub fn init(alloc: Allocator, data: anytype) !Self { + switch (@typeInfo(@TypeOf(data))) { + .pointer => |info| { + assert(info.size == .slice); + assert(info.child == Elem); + + // If it fits in our small request, do that. + if (data.len <= Small.Max) { + var buf: Small.Array = undefined; + @memcpy(buf[0..data.len], data); + return Self{ + .small = .{ + .data = buf, + .len = @intCast(data.len), + }, + }; + } + + // Otherwise, allocate + const buf = try alloc.dupe(Elem, data); + errdefer alloc.free(buf); + return Self{ + .alloc = .{ + .alloc = alloc, + .data = buf, + }, + }; + }, + + else => unreachable, + } + } + + pub fn deinit(self: Self) void { + switch (self) { + .small, .stable => {}, + .alloc => |v| v.alloc.free(v.data), + } + } + + /// Returns a const slice of the data pointed to by this request. + pub fn slice(self: *const Self) []const Elem { + return switch (self.*) { + .small => |*v| v.data[0..v.len], + .stable => |v| v, + .alloc => |v| v.data, + }; + } + }; +} + +test "MessageData init small" { + const testing = std.testing; + const alloc = testing.allocator; + + const Data = MessageData(u8, 10); + const input = "hello!"; + const io = try Data.init(alloc, @as([]const u8, input)); + try testing.expect(io == .small); +} + +test "MessageData init alloc" { + const testing = std.testing; + const alloc = testing.allocator; + + const Data = MessageData(u8, 10); + const input = "hello! " ** 100; + const io = try Data.init(alloc, @as([]const u8, input)); + try testing.expect(io == .alloc); + io.alloc.alloc.free(io.alloc.data); +} + +test "MessageData small fits non-u8 sized data" { + const testing = std.testing; + const alloc = testing.allocator; + + const len = 500; + const Data = MessageData(u8, len); + const input: []const u8 = "X" ** len; + const io = try Data.init(alloc, input); + try testing.expect(io == .small); +} diff --git a/vendor/libghostty-vt/src/datastruct/segmented_pool.zig b/vendor/libghostty-vt/src/datastruct/segmented_pool.zig new file mode 100644 index 00000000..328eb239 --- /dev/null +++ b/vendor/libghostty-vt/src/datastruct/segmented_pool.zig @@ -0,0 +1,95 @@ +const std = @import("std"); +const assert = @import("../quirks.zig").inlineAssert; +const Allocator = std.mem.Allocator; +const testing = std.testing; + +/// A data structure where you can get stable (never copied) pointers to +/// a type that automatically grows if necessary. The values can be "put back" +/// but are expected to be put back IN ORDER. +/// +/// This is implemented specifically for libuv write requests, since the +/// write requests must have a stable pointer and are guaranteed to be processed +/// in order for a single stream. +/// +/// This is NOT thread safe. +pub fn SegmentedPool(comptime T: type, comptime prealloc: usize) type { + return struct { + const Self = @This(); + + i: usize = 0, + available: usize = prealloc, + list: std.SegmentedList(T, prealloc) = .{ .len = prealloc }, + + pub fn deinit(self: *Self, alloc: Allocator) void { + self.list.deinit(alloc); + self.* = undefined; + } + + /// Get the next available value out of the list. This will not + /// grow the list. + pub fn get(self: *Self) !*T { + // Error to not have any + if (self.available == 0) return error.OutOfValues; + + // The index we grab is just i % len, so we wrap around to the front. + const i = @mod(self.i, self.list.len); + self.i +%= 1; // Wrapping addition to swe go back to 0 + self.available -= 1; + return self.list.at(i); + } + + /// Get the next available value out of the list and grow the list + /// if necessary. + pub fn getGrow(self: *Self, alloc: Allocator) !*T { + if (self.available == 0) try self.grow(alloc); + return try self.get(); + } + + fn grow(self: *Self, alloc: Allocator) !void { + try self.list.growCapacity(alloc, self.list.len * 2); + self.i = self.list.len; + self.available = self.list.len; + self.list.len *= 2; + } + + /// Put a value back. The value put back is expected to be the + /// in order of get. + pub fn put(self: *Self) void { + self.available += 1; + assert(self.available <= self.list.len); + } + }; +} + +test "SegmentedPool" { + var list: SegmentedPool(u8, 2) = .{}; + defer list.deinit(testing.allocator); + try testing.expectEqual(@as(usize, 2), list.available); + + // Get to capacity + const v1 = try list.get(); + const v2 = try list.get(); + try testing.expect(v1 != v2); + try testing.expectError(error.OutOfValues, list.get()); + + // Test writing for later + v1.* = 42; + + // Put a value back + list.put(); + const temp = try list.get(); + try testing.expect(v1 == temp); + try testing.expect(temp.* == 42); + try testing.expectError(error.OutOfValues, list.get()); + + // Grow + const v3 = try list.getGrow(testing.allocator); + try testing.expect(v1 != v3 and v2 != v3); + _ = try list.get(); + try testing.expectError(error.OutOfValues, list.get()); + + // Put a value back + list.put(); + try testing.expect(v1 == try list.get()); + try testing.expectError(error.OutOfValues, list.get()); +} diff --git a/vendor/libghostty-vt/src/datastruct/split_tree.zig b/vendor/libghostty-vt/src/datastruct/split_tree.zig new file mode 100644 index 00000000..c7da8ad9 --- /dev/null +++ b/vendor/libghostty-vt/src/datastruct/split_tree.zig @@ -0,0 +1,2367 @@ +const std = @import("std"); +const assert = @import("../quirks.zig").inlineAssert; +const build_config = @import("../build_config.zig"); +const ArenaAllocator = std.heap.ArenaAllocator; +const Allocator = std.mem.Allocator; + +/// SplitTree represents a tree of view types that can be divided. +/// +/// Concretely for Ghostty, it represents a tree of terminal views. In +/// its basic state, there are no splits and it is a single full-sized +/// terminal. However, it can be split arbitrarily many times among two +/// axes (horizontal and vertical) to create a tree of terminal views. +/// +/// This is an immutable tree structure, meaning all operations on it +/// will return a new tree with the operation applied. This allows us to +/// store versions of the tree in a history for easy undo/redo. To facilitate +/// this, the stored View type must implement reference counting; this is left +/// as an implementation detail of the View type. +/// +/// The View type will be stored as a pointer within the tree and must +/// implement a number of functions to work properly: +/// +/// - `fn ref(*View, Allocator) Allocator.Error!*View` - Increase a +/// reference count of the view. The Allocator will be the allocator provided +/// to the tree operation. This is allowed to copy the value if it wants to; +/// the returned value is expected to be a new reference (but that may +/// just be a copy). +/// +/// - `fn unref(*View, Allocator) void` - Decrease the reference count of a +/// view. The Allocator will be the allocator provided to the tree +/// operation. +/// +/// - `fn eql(*const View, *const View) bool` - Check if two views are equal. +/// +/// Optionally the following functions can also be implemented: +/// +/// - `fn splitTreeLabel(*const View) []const u8` - Return a label that is used +/// for the debug view. If this isn't specified then the node handle +/// will be used. +/// +/// Note: for both the ref and unref functions, the allocator is optional. +/// If the functions take less arguments, then the allocator will not be +/// passed. +pub fn SplitTree(comptime V: type) type { + return struct { + const Self = @This(); + + /// The view that this tree contains. + pub const View = V; + + /// The arena allocator used for all allocations in the tree. + /// Since the tree is an immutable structure, this lets us + /// cleanly free all memory when the tree is deinitialized. + arena: ArenaAllocator, + + /// All the nodes in the tree. Node at index 0 is always the root. + nodes: []const Node, + + /// The handle of the zoomed node. A "zoomed" node is one that is + /// expected to be made the full size of the split tree. Various + /// operations may unzoom (e.g. resize). + zoomed: ?Node.Handle, + + /// An empty tree. + pub const empty: Self = .{ + // Arena can be undefined because we have zero allocated nodes. + // If our nodes are empty our deinit function doesn't touch the + // arena. + .arena = undefined, + .nodes = &.{}, + .zoomed = null, + }; + + pub const Node = union(enum) { + leaf: *View, + split: Split, + + /// A handle into the nodes array. This lets us keep track of + /// nodes with 16-bit handles rather than full pointer-width + /// values. + pub const Handle = enum(Backing) { + root = 0, + _, + + pub const Backing = u16; + + pub inline fn idx(self: Handle) usize { + return @intFromEnum(self); + } + + /// Offset the handle by a given amount. + pub fn offset(self: Handle, v: usize) Handle { + const self_usize: usize = @intCast(@intFromEnum(self)); + const final = self_usize + v; + assert(final < std.math.maxInt(Backing)); + return @enumFromInt(final); + } + }; + }; + + pub const Split = struct { + layout: Layout, + ratio: f16, + left: Node.Handle, + right: Node.Handle, + + pub const Layout = enum { horizontal, vertical }; + pub const Direction = enum { left, right, down, up }; + }; + + /// Initialize a new tree with a single view. + pub fn init(gpa: Allocator, view: *View) Allocator.Error!Self { + var arena = ArenaAllocator.init(gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + const nodes = try alloc.alloc(Node, 1); + nodes[0] = .{ .leaf = try viewRef(view, gpa) }; + errdefer viewUnref(view, gpa); + + return .{ + .arena = arena, + .nodes = nodes, + .zoomed = null, + }; + } + + pub fn deinit(self: *Self) void { + // Important: only free memory if we have memory to free, + // because we use an undefined arena for empty trees. + if (self.nodes.len > 0) { + // Unref all our views + const gpa: Allocator = self.arena.child_allocator; + for (self.nodes) |node| switch (node) { + .leaf => |view| viewUnref(view, gpa), + .split => {}, + }; + self.arena.deinit(); + } + + self.* = undefined; + } + + /// Clone this tree, returning a new tree with the same nodes. + pub fn clone(self: *const Self, gpa: Allocator) Allocator.Error!Self { + // If we're empty then return an empty tree. + if (self.isEmpty()) return .empty; + + // Create a new arena allocator for the clone. + var arena = ArenaAllocator.init(gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Allocate a new nodes array and copy the existing nodes into it. + const nodes = try alloc.dupe(Node, self.nodes); + + // Increase the reference count of all the views in the nodes. + try refNodes(gpa, nodes); + + return .{ + .arena = arena, + .nodes = nodes, + .zoomed = self.zoomed, + }; + } + + /// Returns true if this is an empty tree. + pub fn isEmpty(self: *const Self) bool { + // An empty tree has no nodes. + return self.nodes.len == 0; + } + + /// Returns true if this tree has more than one split (i.e., the root + /// is a split node). This is useful for determining if actions like + /// resize_split or toggle_split_zoom are performable. + pub fn isSplit(self: *const Self) bool { + // An empty tree is not split. + if (self.isEmpty()) return false; + // The root node is at index 0. If it's a split, we have multiple splits. + return switch (self.nodes[0]) { + .split => true, + .leaf => false, + }; + } + + /// An iterator over all the views in the tree. + pub fn iterator( + self: *const Self, + ) Iterator { + return .{ .nodes = self.nodes }; + } + + pub const ViewEntry = struct { + handle: Node.Handle, + view: *View, + }; + + pub const Iterator = struct { + i: Node.Handle = .root, + nodes: []const Node, + + pub fn next(self: *Iterator) ?ViewEntry { + // If we have no nodes, return null. + if (@intFromEnum(self.i) >= self.nodes.len) return null; + + // Get the current node and increment the index. + const handle = self.i; + self.i = @enumFromInt(handle.idx() + 1); + const node = self.nodes[handle.idx()]; + + return switch (node) { + .leaf => |v| .{ .handle = handle, .view = v }, + .split => self.next(), + }; + } + }; + + /// Change the zoomed state to the given node. Assumes the handle + /// is valid. + pub fn zoom(self: *Self, handle: ?Node.Handle) void { + if (handle) |v| { + assert(@intFromEnum(v) >= 0); + assert(@intFromEnum(v) < self.nodes.len); + } + self.zoomed = handle; + } + + pub const Goto = union(enum) { + /// Previous view, null if we're the first view. + previous, + + /// Next view, null if we're the last view. + next, + + /// Previous view, but wrapped around to the last view. May + /// return the same view if this is the first view. + previous_wrapped, + + /// Next view, but wrapped around to the first view. May return + /// the same view if this is the last view. + next_wrapped, + + /// A spatial direction. "Spatial" means that the direction is + /// based on the nearest surface in the given direction visually + /// as the surfaces are laid out on a 2D grid. + spatial: Spatial.Direction, + }; + + /// Goto a view from a certain point in the split tree. Returns null + /// if the direction results in no visitable view. + /// + /// Allocator is only used for temporary state for spatial navigation. + pub fn goto( + self: *const Self, + alloc: Allocator, + from: Node.Handle, + to: Goto, + ) Allocator.Error!?Node.Handle { + return switch (to) { + .previous => self.previous(from), + .next => self.next(from), + .previous_wrapped => self.previous(from) orelse self.deepest(.right, .root), + .next_wrapped => self.next(from) orelse self.deepest(.left, .root), + .spatial => |d| spatial: { + // Get our spatial representation. + var sp = try self.spatial(alloc); + defer sp.deinit(alloc); + break :spatial self.nearestWrapped(sp, from, d); + }, + }; + } + + pub const Side = enum { left, right }; + + /// Returns the deepest view in the tree in the given direction. + /// This can be used to find the leftmost/rightmost surface within + /// a given split structure. + pub fn deepest( + self: *const Self, + side: Side, + from: Node.Handle, + ) Node.Handle { + var current: Node.Handle = from; + while (true) { + switch (self.nodes[current.idx()]) { + .leaf => return current, + .split => |s| current = switch (side) { + .left => s.left, + .right => s.right, + }, + } + } + } + + /// Returns the previous view from the given node handle (which itself + /// doesn't need to be a view). If there is no previous (this is the + /// most previous view) then this will return null. + /// + /// "Previous" is defined as the previous node in an in-order + /// traversal of the tree. This isn't a perfect definition and we + /// may want to change this to something that better matches a + /// spatial view of the tree later. + fn previous(self: *const Self, from: Node.Handle) ?Node.Handle { + return switch (self.previousBacktrack(from, .root)) { + .result => |v| v, + .backtrack, .deadend => null, + }; + } + + /// Same as `previous`, but returns the next view instead. + fn next(self: *const Self, from: Node.Handle) ?Node.Handle { + return switch (self.nextBacktrack(from, .root)) { + .result => |v| v, + .backtrack, .deadend => null, + }; + } + + // Design note: we use a recursive backtracking search because + // split trees are never that deep, so we can abuse the stack as + // a safe allocator (stack overflow unlikely unless the kernel is + // tuned in some really weird way). + const Backtrack = union(enum) { + deadend, + backtrack, + result: Node.Handle, + }; + + fn previousBacktrack( + self: *const Self, + from: Node.Handle, + current: Node.Handle, + ) Backtrack { + // If we reached the point that we're trying to find the previous + // value of, then we need to backtrack from here. + if (from == current) return .backtrack; + + return switch (self.nodes[current.idx()]) { + // If we hit a leaf that isn't our target, then deadend. + .leaf => .deadend, + + .split => |s| switch (self.previousBacktrack(from, s.left)) { + .result => |v| .{ .result = v }, + + // Backtrack from the left means we have to continue + // backtracking because we can't see what's before the left. + .backtrack => .backtrack, + + // If we hit a deadend on the left then let's move right. + .deadend => switch (self.previousBacktrack(from, s.right)) { + .result => |v| .{ .result = v }, + + // Deadend means its not in this split at all since + // we already tracked the left. + .deadend => .deadend, + + // Backtrack means that its in our left view because + // we can see the immediate previous and there MUST + // be leaves (we can't have split-only leaves). + .backtrack => .{ .result = self.deepest(.right, s.left) }, + }, + }, + }; + } + + // See previousBacktrack for detailed comments. This is a mirror + // of that. + fn nextBacktrack( + self: *const Self, + from: Node.Handle, + current: Node.Handle, + ) Backtrack { + if (from == current) return .backtrack; + return switch (self.nodes[current.idx()]) { + .leaf => .deadend, + .split => |s| switch (self.nextBacktrack(from, s.right)) { + .result => |v| .{ .result = v }, + .backtrack => .backtrack, + .deadend => switch (self.nextBacktrack(from, s.left)) { + .result => |v| .{ .result = v }, + .deadend => .deadend, + .backtrack => .{ .result = self.deepest(.left, s.right) }, + }, + }, + }; + } + + /// Returns the nearest leaf node (view) in the given direction. + /// This does not handle wrapping and will return null if there + /// is no node in that direction. + fn nearest( + self: *const Self, + sp: Spatial, + from: Node.Handle, + direction: Spatial.Direction, + target: Spatial.Slot, + ) ?Node.Handle { + var result: ?struct { + handle: Node.Handle, + distance: f16, + } = null; + for (sp.slots, 0..) |slot, handle| { + // Never match ourself + if (handle == from.idx()) continue; + + // Only match leaves + switch (self.nodes[handle]) { + .leaf => {}, + .split => continue, + } + + // Ensure it is in the proper direction + if (!switch (direction) { + .left => slot.maxX() <= target.x, + .right => slot.x >= target.maxX(), + .up => slot.maxY() <= target.y, + .down => slot.y >= target.maxY(), + }) continue; + + // Track our distance + const dx = slot.x - target.x; + const dy = slot.y - target.y; + const distance = @sqrt(dx * dx + dy * dy); + + // If we have a nearest it must be closer. + if (result) |n| { + if (distance >= n.distance) continue; + } + result = .{ + .handle = @enumFromInt(handle), + .distance = distance, + }; + } + + return if (result) |n| n.handle else null; + } + + /// Same as nearest but supports wrapping. + fn nearestWrapped( + self: *const Self, + sp: Spatial, + from: Node.Handle, + direction: Spatial.Direction, + ) ?Node.Handle { + // If we can find a nearest value without wrapping, then + // use that. + var target = sp.slots[from.idx()]; + if (self.nearest( + sp, + from, + direction, + target, + )) |v| return v; + + // The spatial grid is normalized to 1x1, so wrapping is modeled + // by shifting the target slot by one full grid in the opposite + // direction and reusing the same nearest distance logic. + // We don't actually modify the grid or spatial representation, + // this just fakes it. + assert(target.x >= 0 and target.y >= 0); + assert(target.maxX() <= 1 and target.maxY() <= 1); + switch (direction) { + .left => target.x += 1, + .right => target.x -= 1, + .up => target.y += 1, + .down => target.y -= 1, + } + + return self.nearest( + sp, + from, + direction, + target, + ); + } + + /// Resize the given node in place. The node MUST be a split (asserted). + /// + /// In general, this is an immutable data structure so this is + /// heavily discouraged. However, this is provided for convenience + /// and performance reasons where its very important for GUIs to + /// update the ratio during a live resize than to redraw the entire + /// widget tree. + pub fn resizeInPlace( + self: *Self, + at: Node.Handle, + ratio: f16, + ) void { + // Let's talk about this constCast. Our member are const but + // we actually always own their memory. We don't want consumers + // who directly access the nodes to be able to modify them + // (without nasty stuff like this), but given this is internal + // usage its perfectly fine to modify the node in-place. + const s: *Split = @constCast(&self.nodes[at.idx()].split); + s.ratio = ratio; + } + + /// Insert another tree into this tree at the given node in the + /// specified direction. The other tree will be inserted in the + /// new direction. For example, if the direction is "right" then + /// `insert` is inserted right of the existing node. + /// + /// The allocator will be used for the newly created tree. + /// The previous trees will not be freed, but reference counts + /// for the views will be increased accordingly for the new tree. + pub fn split( + self: *const Self, + gpa: Allocator, + at: Node.Handle, + direction: Split.Direction, + ratio: f16, + insert: *const Self, + ) Allocator.Error!Self { + // The new arena for our new tree. + var arena = ArenaAllocator.init(gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // We know we're going to need the sum total of the nodes + // between the two trees plus one for the new split node. + const nodes = try alloc.alloc(Node, self.nodes.len + insert.nodes.len + 1); + if (nodes.len > std.math.maxInt(Node.Handle.Backing)) return error.OutOfMemory; + + // We can copy our nodes exactly as they are, since they're + // mostly not changing (only `at` is changing). + @memcpy(nodes[0..self.nodes.len], self.nodes); + + // We can copy the destination nodes as well directly next to + // the source nodes. We just have to go through and offset + // all the handles in the destination tree to account for + // the shift. + const nodes_inserted = nodes[self.nodes.len..][0..insert.nodes.len]; + @memcpy(nodes_inserted, insert.nodes); + for (nodes_inserted) |*node| switch (node.*) { + .leaf => {}, + .split => |*s| { + // We need to offset the handles in the split + s.left = s.left.offset(self.nodes.len); + s.right = s.right.offset(self.nodes.len); + }, + }; + + // Determine our split layout and if we're on the left + const layout: Split.Layout, const left: bool = switch (direction) { + .left => .{ .horizontal, true }, + .right => .{ .horizontal, false }, + .up => .{ .vertical, true }, + .down => .{ .vertical, false }, + }; + + // Copy our previous value to the end of the nodes list and + // create our new split node. + nodes[nodes.len - 1] = nodes[at.idx()]; + nodes[at.idx()] = .{ .split = .{ + .layout = layout, + .ratio = ratio, + .left = @enumFromInt(if (left) self.nodes.len else nodes.len - 1), + .right = @enumFromInt(if (left) nodes.len - 1 else self.nodes.len), + } }; + + // We need to increase the reference count of all the nodes. + try refNodes(gpa, nodes); + + return .{ + .arena = arena, + .nodes = nodes, + // Splitting always resets zoom state. + .zoomed = null, + }; + } + + /// Remove a node from the tree. + pub fn remove( + self: *Self, + gpa: Allocator, + at: Node.Handle, + ) Allocator.Error!Self { + assert(at.idx() < self.nodes.len); + + // If we're removing node zero then we're clearing the tree. + if (at == .root) return .empty; + + // The new arena for our new tree. + var arena = ArenaAllocator.init(gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Allocate our new nodes list with the number of nodes we'll + // need after the removal. + const nodes = try alloc.alloc(Node, self.countAfterRemoval( + .root, + at, + 0, + )); + + var result: Self = .{ + .arena = arena, + .nodes = nodes, + .zoomed = null, + }; + + // Traverse the tree and copy all our nodes into place. + assert(self.removeNode( + &result, + 0, + .root, + at, + ) != 0); + + // Increase the reference count of all the nodes. + try refNodes(gpa, nodes); + + return result; + } + + fn removeNode( + old: *Self, + new: *Self, + new_offset: usize, + current: Node.Handle, + target: Node.Handle, + ) usize { + assert(current != target); + + // If we have a zoomed node and this is it then we migrate it. + if (old.zoomed) |v| { + if (v == current) { + assert(new.zoomed == null); + new.zoomed = @enumFromInt(new_offset); + } + } + + // Let's talk about this constCast. Our member are const but + // we actually always own their memory. We don't want consumers + // who directly access the nodes to be able to modify them + // (without nasty stuff like this), but given this is internal + // usage its perfectly fine to modify the node in-place. + const new_nodes: []Node = @constCast(new.nodes); + + switch (old.nodes[current.idx()]) { + // Leaf is simple, just copy it over. We don't ref anything + // yet because it'd make undo (errdefer) harder. We do that + // all at once later. + .leaf => |view| { + new_nodes[new_offset] = .{ .leaf = view }; + return 1; + }, + + .split => |s| { + // If we're removing one of the split node sides then + // we remove the split node itself as well and only add + // the other (non-removed) side. + if (s.left == target) return old.removeNode( + new, + new_offset, + s.right, + target, + ); + if (s.right == target) return old.removeNode( + new, + new_offset, + s.left, + target, + ); + + // Neither side is being directly removed, so we traverse. + const left = old.removeNode( + new, + new_offset + 1, + s.left, + target, + ); + assert(left != 0); + const right = old.removeNode( + new, + new_offset + left + 1, + s.right, + target, + ); + assert(right != 0); + new_nodes[new_offset] = .{ .split = .{ + .layout = s.layout, + .ratio = s.ratio, + .left = @enumFromInt(new_offset + 1), + .right = @enumFromInt(new_offset + 1 + left), + } }; + + return left + right + 1; + }, + } + } + + /// Returns the number of nodes that would be needed to store + /// the tree if the target node is removed. + fn countAfterRemoval( + self: *Self, + current: Node.Handle, + target: Node.Handle, + acc: usize, + ) usize { + assert(current != target); + + return switch (self.nodes[current.idx()]) { + // Leaf is simple, always takes one node. + .leaf => acc + 1, + + // Split is slightly more complicated. If either side is the + // target to remove, then we remove the split node as well + // so our count is just the count of the other side. + // + // If neither side is the target, then we count both sides + // and add one to account for the split node itself. + .split => |s| if (s.left == target) self.countAfterRemoval( + s.right, + target, + acc, + ) else if (s.right == target) self.countAfterRemoval( + s.left, + target, + acc, + ) else self.countAfterRemoval( + s.left, + target, + acc, + ) + self.countAfterRemoval( + s.right, + target, + acc, + ) + 1, + }; + } + + /// Reference all the nodes in the given slice, handling unref if + /// any fail. This should be called LAST so you don't have to undo + /// the refs at any further point after this. + fn refNodes(gpa: Allocator, nodes: []Node) Allocator.Error!void { + // We need to increase the reference count of all the nodes. + // Careful accounting here so that we properly unref on error + // only the nodes we referenced. + var reffed: usize = 0; + errdefer for (0..reffed) |i| { + switch (nodes[i]) { + .split => {}, + .leaf => |view| viewUnref(view, gpa), + } + }; + for (0..nodes.len) |i| { + switch (nodes[i]) { + .split => {}, + .leaf => |view| nodes[i] = .{ .leaf = try viewRef(view, gpa) }, + } + reffed = i; + } + assert(reffed == nodes.len - 1); + } + + /// Equalize this node and all its children, returning a new node with splits + /// adjusted so that each split's ratio is based on the relative weight + /// (number of leaves) of its children. + pub fn equalize( + self: *const Self, + gpa: Allocator, + ) Allocator.Error!Self { + if (self.isEmpty()) return .empty; + + // Create a new arena allocator for the clone. + var arena = ArenaAllocator.init(gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Allocate a new nodes array and copy the existing nodes into it. + const nodes = try alloc.dupe(Node, self.nodes); + + // Go through and equalize our ratios based on weights. + for (nodes) |*node| switch (node.*) { + .leaf => {}, + .split => |*s| { + const weight_left = self.weight(s.left, s.layout, 0); + const weight_right = self.weight(s.right, s.layout, 0); + assert(weight_left > 0); + assert(weight_right > 0); + const total_f16: f16 = @floatFromInt(weight_left + weight_right); + const weight_left_f16: f16 = @floatFromInt(weight_left); + s.ratio = weight_left_f16 / total_f16; + }, + }; + + // Increase the reference count of all the views in the nodes. + try refNodes(gpa, nodes); + + return .{ + .arena = arena, + .nodes = nodes, + .zoomed = self.zoomed, + }; + } + + fn weight( + self: *const Self, + from: Node.Handle, + layout: Split.Layout, + acc: usize, + ) usize { + return switch (self.nodes[from.idx()]) { + .leaf => acc + 1, + .split => |s| if (s.layout == layout) + self.weight(s.left, layout, acc) + + self.weight(s.right, layout, acc) + else + 1, + }; + } + + /// Resize the nearest split matching the layout by the given ratio. + /// Positive is right and down. + /// + /// The ratio is a signed delta representing the percentage to move + /// the divider. The percentage is of the entire grid size, not just + /// the specific split size. + /// We use the entire grid size because that's what Ghostty's + /// `resize_split` keybind does, because it maps to a general human + /// understanding of moving a split relative to the entire window + /// (generally). + /// + /// For example, a ratio of 0.1 and a layout of `vertical` will find + /// the nearest vertical split and move the divider down by 10% of + /// the total grid height. + /// + /// If no matching split is found, this does nothing, but will always + /// still return a cloned tree. + pub fn resize( + self: *const Self, + gpa: Allocator, + from: Node.Handle, + layout: Split.Layout, + ratio: f16, + ) Allocator.Error!Self { + assert(ratio >= -1 and ratio <= 1); + assert(!std.math.isNan(ratio)); + assert(!std.math.isInf(ratio)); + + // Fast path empty trees. + if (self.isEmpty()) return .empty; + + // From this point forward worst case we return a clone. + var result = try self.clone(gpa); + errdefer result.deinit(); + + // Find our nearest parent split node matching the layout. + const parent_handle = switch (self.findParentSplit( + layout, + from, + .root, + )) { + .deadend, .backtrack => return result, + .result => |v| v, + }; + + // Get our spatial layout, because we need the dimensions of this + // split with regards to the entire grid. + var sp = try result.spatial(gpa); + defer sp.deinit(gpa); + + // Get the ratio of the split relative to the full grid. + const full_ratio = full_ratio: { + // Our scale is the amount we need to multiply our individual + // ratio by to get the full ratio. Its actually a ratio on its + // own but I'm trying to avoid that word: its the ratio of + // our spatial width/height to the total. + const scale = switch (layout) { + .horizontal => sp.slots[parent_handle.idx()].width / sp.slots[0].width, + .vertical => sp.slots[parent_handle.idx()].height / sp.slots[0].height, + }; + + const current = result.nodes[parent_handle.idx()].split.ratio; + break :full_ratio current * scale; + }; + + // Set the final new ratio, clamping it to [0, 1] + result.resizeInPlace( + parent_handle, + @min(@max(full_ratio + ratio, 0), 1), + ); + return result; + } + + fn findParentSplit( + self: *const Self, + layout: Split.Layout, + from: Node.Handle, + current: Node.Handle, + ) Backtrack { + if (from == current) return .backtrack; + return switch (self.nodes[current.idx()]) { + .leaf => .deadend, + .split => |s| switch (self.findParentSplit( + layout, + from, + s.left, + )) { + .result => |v| .{ .result = v }, + .backtrack => if (s.layout == layout) + .{ .result = current } + else + .backtrack, + .deadend => switch (self.findParentSplit( + layout, + from, + s.right, + )) { + .deadend => .deadend, + .result => |v| .{ .result = v }, + .backtrack => if (s.layout == layout) + .{ .result = current } + else + .backtrack, + }, + }, + }; + } + + /// Spatial representation of the split tree. See spatial. + pub const Spatial = struct { + /// The slots of the spatial representation in the same order + /// as the tree it was created from. + slots: []const Slot, + + pub const empty: Spatial = .{ .slots = &.{} }; + + pub const Direction = enum { left, right, down, up }; + + const Slot = struct { + x: f16, + y: f16, + width: f16, + height: f16, + + fn maxX(self: *const Slot) f16 { + return self.x + self.width; + } + + fn maxY(self: *const Slot) f16 { + return self.y + self.height; + } + }; + + pub fn deinit(self: *Spatial, alloc: Allocator) void { + alloc.free(self.slots); + self.* = undefined; + } + }; + + /// Spatial representation of the split tree. This can be used to + /// better understand the layout of the tree in a 2D space. + /// + /// The bounds of the representation are always based on the total + /// 2D space being 1x1. The x/y coordinates and width/height dimensions + /// of each individual split and leaf are relative to this. + /// This means that the spatial representation is a normalized + /// representation of the actual space. + /// + /// The top-left corner of the tree is always (0, 0). + /// + /// We use a normalized form because we can calculate it without + /// accessing to the actual rendered view sizes. These actual sizes + /// may not be available at various times because GUI toolkits often + /// only make them available once they're part of a widget tree and + /// a SplitTree can represent views that aren't currently visible. + pub fn spatial( + self: *const Self, + alloc: Allocator, + ) Allocator.Error!Spatial { + // No nodes, empty spatial representation. + if (self.nodes.len == 0) return .empty; + + // Get our total dimensions. + const dim = self.dimensions(.root); + + // Create our slots which will match our nodes exactly. + const slots = try alloc.alloc(Spatial.Slot, self.nodes.len); + errdefer alloc.free(slots); + slots[0] = .{ + .x = 0, + .y = 0, + .width = @floatFromInt(dim.width), + .height = @floatFromInt(dim.height), + }; + self.fillSpatialSlots(slots, .root); + + // Normalize the dimensions to 1x1 grid. + for (slots) |*slot| { + slot.x /= @floatFromInt(dim.width); + slot.y /= @floatFromInt(dim.height); + slot.width /= @floatFromInt(dim.width); + slot.height /= @floatFromInt(dim.height); + } + + return .{ .slots = slots }; + } + + fn fillSpatialSlots( + self: *const Self, + slots: []Spatial.Slot, + current_: Node.Handle, + ) void { + const current = current_.idx(); + assert(slots[current].width >= 0 and slots[current].height >= 0); + switch (self.nodes[current]) { + // Leaf node, current slot is already filled by caller. + .leaf => {}, + + .split => |s| { + switch (s.layout) { + .horizontal => { + slots[s.left.idx()] = .{ + .x = slots[current].x, + .y = slots[current].y, + .width = slots[current].width * s.ratio, + .height = slots[current].height, + }; + slots[s.right.idx()] = .{ + .x = slots[current].x + slots[current].width * s.ratio, + .y = slots[current].y, + .width = slots[current].width * (1 - s.ratio), + .height = slots[current].height, + }; + }, + + .vertical => { + slots[s.left.idx()] = .{ + .x = slots[current].x, + .y = slots[current].y, + .width = slots[current].width, + .height = slots[current].height * s.ratio, + }; + slots[s.right.idx()] = .{ + .x = slots[current].x, + .y = slots[current].y + slots[current].height * s.ratio, + .width = slots[current].width, + .height = slots[current].height * (1 - s.ratio), + }; + }, + } + + self.fillSpatialSlots(slots, s.left); + self.fillSpatialSlots(slots, s.right); + }, + } + } + + /// Get the dimensions of the tree starting from the given node. + /// + /// This creates relative dimensions (see Spatial) by assuming each + /// leaf is exactly 1x1 unit in size. + fn dimensions(self: *const Self, current: Node.Handle) struct { + width: u16, + height: u16, + } { + return switch (self.nodes[current.idx()]) { + .leaf => .{ .width = 1, .height = 1 }, + .split => |s| split: { + const left = self.dimensions(s.left); + const right = self.dimensions(s.right); + break :split switch (s.layout) { + .horizontal => .{ + .width = left.width + right.width, + .height = @max(left.height, right.height), + }, + + .vertical => .{ + .width = @max(left.width, right.width), + .height = left.height + right.height, + }, + }; + }, + }; + } + + /// Format the tree in a human-readable format. By default this will + /// output a diagram followed by a textual representation. + pub fn format( + self: *const Self, + writer: *std.Io.Writer, + ) !void { + if (self.nodes.len == 0) { + try writer.writeAll("empty"); + return; + } + self.formatDiagram(writer) catch {}; + try self.formatText(writer); + } + + pub fn formatText(self: Self, writer: *std.Io.Writer) std.Io.Writer.Error!void { + if (self.nodes.len == 0) { + try writer.writeAll("empty"); + return; + } + try self.formatTextInner(writer, .root, 0); + } + + fn formatTextInner( + self: Self, + writer: *std.Io.Writer, + current: Node.Handle, + depth: usize, + ) std.Io.Writer.Error!void { + for (0..depth) |_| try writer.writeAll(" "); + + if (self.zoomed) |zoomed| if (zoomed == current) { + try writer.writeAll("(zoomed) "); + }; + + switch (self.nodes[current.idx()]) { + .leaf => |v| if (@hasDecl(View, "splitTreeLabel")) + try writer.print("leaf: {s}\n", .{v.splitTreeLabel()}) + else + try writer.print("leaf: {d}\n", .{current}), + + .split => |s| { + try writer.print("split (layout: {t}, ratio: {d:.2})\n", .{ + s.layout, + s.ratio, + }); + try self.formatTextInner(writer, s.left, depth + 1); + try self.formatTextInner(writer, s.right, depth + 1); + }, + } + } + + pub fn formatDiagram( + self: Self, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + if (self.nodes.len == 0) { + try writer.writeAll("empty"); + return; + } + + // Use our arena's GPA to allocate some intermediate memory. + // Requiring allocation for formatting is nasty but this is really + // only used for debugging and testing and shouldn't hit OOM + // scenarios. + var arena: ArenaAllocator = .init(self.arena.child_allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + // Get our spatial representation. + const sp = spatial: { + const sp = self.spatial(alloc) catch return error.WriteFailed; + + // Scale our spatial representation to have minimum width/height 1. + var min_w: f16 = 1; + var min_h: f16 = 1; + for (sp.slots) |slot| { + if (slot.width > 0) min_w = @min(min_w, slot.width); + if (slot.height > 0) min_h = @min(min_h, slot.height); + } + + const ratio_w: f16 = 1 / min_w; + const ratio_h: f16 = 1 / min_h; + const slots = alloc.dupe(Spatial.Slot, sp.slots) catch return error.WriteFailed; + for (slots) |*slot| { + slot.x *= ratio_w; + slot.y *= ratio_h; + slot.width *= ratio_w; + slot.height *= ratio_h; + } + + break :spatial .{ .slots = slots }; + }; + + // The width we need for the largest label. + const max_label_width: usize = max_label_width: { + if (!@hasDecl(View, "splitTreeLabel")) { + break :max_label_width std.math.log10(sp.slots.len) + 1; + } + + var max: usize = 0; + for (self.nodes) |node| switch (node) { + .split => {}, + .leaf => |view| { + const label = view.splitTreeLabel(); + max = @max(max, label.len); + }, + }; + + break :max_label_width max; + }; + + // We need space for whitespace and ASCII art so add that. + // We need to accommodate the leaf handle, whitespace, and + // then the border. + const cell_width = cell_width: { + // Border + whitespace + label + whitespace + border. + break :cell_width 2 + max_label_width + 2; + }; + const cell_height = cell_height: { + // Border + label + border. No whitespace needed on the + // vertical axis. + break :cell_height 1 + 1 + 1; + }; + + // Make a grid that can fit our entire ASCII diagram. We know + // the width/height based on node 0. + const grid = grid: { + // Get our initial width/height. Each leaf is 1x1 in this. + // We round up for this because partial widths/heights should + // take up an extra cell. + var width: usize = @intFromFloat(@ceil(sp.slots[0].width)); + var height: usize = @intFromFloat(@ceil(sp.slots[0].height)); + + // We need space for whitespace and ASCII art so add that. + // We need to accommodate the leaf handle, whitespace, and + // then the border. + width *= cell_width; + height *= cell_height; + + const rows = alloc.alloc([]u8, height) catch return error.WriteFailed; + for (0..rows.len) |y| { + rows[y] = alloc.alloc(u8, width + 1) catch return error.WriteFailed; + @memset(rows[y], ' '); + rows[y][width] = '\n'; + } + break :grid rows; + }; + + // Draw each node + for (sp.slots, 0..) |slot, handle| { + // We only draw leaf nodes. Splits are only used for layout. + const node = self.nodes[handle]; + switch (node) { + .leaf => {}, + .split => continue, + } + + // If our width/height is zero then we skip this. + if (slot.width == 0 or slot.height == 0) continue; + + var x: usize = @intFromFloat(@floor(slot.x)); + var y: usize = @intFromFloat(@floor(slot.y)); + var width: usize = @intFromFloat(@max(@floor(slot.width), 1)); + var height: usize = @intFromFloat(@max(@floor(slot.height), 1)); + x *= cell_width; + y *= cell_height; + width *= cell_width; + height *= cell_height; + + // Top border + { + const top = grid[y][x..][0..width]; + top[0] = '+'; + for (1..width - 1) |i| top[i] = '-'; + top[width - 1] = '+'; + } + + // Bottom border + { + const bottom = grid[y + height - 1][x..][0..width]; + bottom[0] = '+'; + for (1..width - 1) |i| bottom[i] = '-'; + bottom[width - 1] = '+'; + } + + // Left border + for (y + 1..y + height - 1) |y_cur| grid[y_cur][x] = '|'; + for (y + 1..y + height - 1) |y_cur| grid[y_cur][x + width - 1] = '|'; + + // Get our label text + var buf: [10]u8 = undefined; + const label: []const u8 = if (@hasDecl(View, "splitTreeLabel")) + node.leaf.splitTreeLabel() + else + std.fmt.bufPrint(&buf, "{d}", .{handle}) catch return error.WriteFailed; + + // Draw the handle in the center + const x_mid = width / 2 + x; + const y_mid = height / 2 + y; + const label_width = label.len; + const label_start = x_mid - label_width / 2; + const row = grid[y_mid][label_start..]; + _ = std.fmt.bufPrint(row, "{s}", .{label}) catch return error.WriteFailed; + } + + // Output every row + for (grid) |row| { + // We currently have a bug in our height calculation that + // results in trailing blank lines. Ignore those. We should + // really fix our height calculation instead. If someone wants + // to do that just remove this line and see the tests that fail + // and go from there. + if (row[0] == ' ') break; + try writer.writeAll(row); + } + } + + fn viewRef(view: *View, gpa: Allocator) Allocator.Error!*View { + const func = @typeInfo(@TypeOf(View.ref)).@"fn"; + return switch (func.params.len) { + 1 => view.ref(), + 2 => try view.ref(gpa), + else => @compileError("invalid view ref function"), + }; + } + + fn viewUnref(view: *View, gpa: Allocator) void { + const func = @typeInfo(@TypeOf(View.unref)).@"fn"; + switch (func.params.len) { + 1 => view.unref(), + 2 => view.unref(gpa), + else => @compileError("invalid view unref function"), + } + } + + /// Make this a valid gobject if we're in a GTK environment. + pub const getGObjectType = switch (build_config.app_runtime) { + .gtk => @import("gobject").ext.defineBoxed( + Self, + .{ + // To get the type name we get the non-qualified type name + // of the view and append that to `GhosttySplitTree`. + .name = name: { + const type_name = @typeName(View); + const last = if (std.mem.lastIndexOfScalar( + u8, + type_name, + '.', + )) |idx| + type_name[idx + 1 ..] + else + type_name; + assert(last.len > 0); + break :name "GhosttySplitTree" ++ last; + }, + + .funcs = .{ + .copy = &struct { + fn copy(self: *Self) callconv(.c) *Self { + const ptr = @import("glib").ext.create(Self); + ptr.* = if (self.nodes.len == 0) + .empty + else + self.clone(self.arena.child_allocator) catch @panic("oom"); + return ptr; + } + }.copy, + .free = &struct { + fn free(self: *Self) callconv(.c) void { + self.deinit(); + @import("glib").ext.destroy(self); + } + }.free, + }, + }, + ), + + .none => void, + }; + }; +} + +const TestTree = SplitTree(TestView); + +const TestView = struct { + const Self = @This(); + + label: []const u8, + + pub fn ref(self: *Self, alloc: Allocator) Allocator.Error!*Self { + const ptr = try alloc.create(Self); + ptr.* = self.*; + return ptr; + } + + pub fn unref(self: *Self, alloc: Allocator) void { + alloc.destroy(self); + } + + pub fn splitTreeLabel(self: *const Self) []const u8 { + return self.label; + } +}; + +test "SplitTree: isSplit" { + const testing = std.testing; + const alloc = testing.allocator; + + // Empty tree should not be split + var empty: TestTree = .empty; + defer empty.deinit(); + try testing.expect(!empty.isSplit()); + + // Single node tree should not be split + var v1: TestView = .{ .label = "A" }; + var single: TestTree = try TestTree.init(alloc, &v1); + defer single.deinit(); + try testing.expect(!single.isSplit()); + + // Split tree should be split + var v2: TestView = .{ .label = "B" }; + var tree2: TestTree = try TestTree.init(alloc, &v2); + defer tree2.deinit(); + var split = try single.split( + alloc, + .root, + .right, + 0.5, + &tree2, + ); + defer split.deinit(); + try testing.expect(split.isSplit()); +} + +test "SplitTree: empty tree" { + const testing = std.testing; + const alloc = testing.allocator; + var t: TestTree = .empty; + defer t.deinit(); + + const str = try std.fmt.allocPrint(alloc, "{f}", .{t}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\empty + ); +} + +test "SplitTree: single node" { + const testing = std.testing; + const alloc = testing.allocator; + var v: TestTree.View = .{ .label = "A" }; + var t: TestTree = try .init(alloc, &v); + defer t.deinit(); + + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---+ + \\| A | + \\+---+ + \\ + ); +} + +test "SplitTree: split horizontal" { + const testing = std.testing; + const alloc = testing.allocator; + + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); + defer t1.deinit(); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); + defer t2.deinit(); + var t3 = try t1.split( + alloc, + .root, // at root + .right, // split right + 0.5, + &t2, // insert t2 + ); + defer t3.deinit(); + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{t3}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---++---+ + \\| A || B | + \\+---++---+ + \\split (layout: horizontal, ratio: 0.50) + \\ leaf: A + \\ leaf: B + \\ + ); + } + + // Split right at B + var vC: TestTree.View = .{ .label = "C" }; + var tC: TestTree = try .init(alloc, &vC); + defer tC.deinit(); + var it = t3.iterator(); + var t4 = try t3.split( + alloc, + while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "B")) { + break entry.handle; + } + } else return error.NotFound, + .right, + 0.5, + &tC, + ); + defer t4.deinit(); + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{t4}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+--------++---++---+ + \\| A || B || C | + \\+--------++---++---+ + \\split (layout: horizontal, ratio: 0.50) + \\ leaf: A + \\ split (layout: horizontal, ratio: 0.50) + \\ leaf: B + \\ leaf: C + \\ + ); + } + + // Split right at C + var vD: TestTree.View = .{ .label = "D" }; + var tD: TestTree = try .init(alloc, &vD); + defer tD.deinit(); + it = t4.iterator(); + var t5 = try t4.split( + alloc, + while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "C")) { + break entry.handle; + } + } else return error.NotFound, + .right, + 0.5, + &tD, + ); + defer t5.deinit(); + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{t5}); + defer alloc.free(str); + try testing.expectEqualStrings( + \\+------------------++--------++---++---+ + \\| A || B || C || D | + \\+------------------++--------++---++---+ + \\split (layout: horizontal, ratio: 0.50) + \\ leaf: A + \\ split (layout: horizontal, ratio: 0.50) + \\ leaf: B + \\ split (layout: horizontal, ratio: 0.50) + \\ leaf: C + \\ leaf: D + \\ + , str); + } + + // Find "previous" from D back. + { + var current: u8 = 'D'; + while (current != 'A') : (current -= 1) { + it = t5.iterator(); + const handle = t5.previous( + while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, &.{current})) { + break entry.handle; + } + } else return error.NotFound, + ).?; + + const entry = t5.nodes[handle.idx()].leaf; + try testing.expectEqualStrings( + entry.label, + &.{current - 1}, + ); + } + + it = t5.iterator(); + try testing.expect(t5.previous( + while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, &.{current})) { + break entry.handle; + } + } else return error.NotFound, + ) == null); + } + + // Find "next" from A forward. + { + var current: u8 = 'A'; + while (current != 'D') : (current += 1) { + it = t5.iterator(); + const handle = t5.next( + while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, &.{current})) { + break entry.handle; + } + } else return error.NotFound, + ).?; + + const entry = t5.nodes[handle.idx()].leaf; + try testing.expectEqualStrings( + entry.label, + &.{current + 1}, + ); + } + + it = t5.iterator(); + try testing.expect(t5.next( + while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, &.{current})) { + break entry.handle; + } + } else return error.NotFound, + ) == null); + } +} + +test "SplitTree: split vertical" { + const testing = std.testing; + const alloc = testing.allocator; + + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); + defer t1.deinit(); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); + defer t2.deinit(); + + var t3 = try t1.split( + alloc, + .root, // at root + .down, // split down + 0.5, + &t2, // insert t2 + ); + defer t3.deinit(); + + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t3, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---+ + \\| A | + \\+---+ + \\+---+ + \\| B | + \\+---+ + \\ + ); +} + +test "SplitTree: split horizontal with zero ratio" { + const testing = std.testing; + const alloc = testing.allocator; + + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); + defer t1.deinit(); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); + defer t2.deinit(); + + // A | B horizontal + var splitAB = try t1.split( + alloc, + .root, // at root + .right, // split right + 0, + &t2, // insert t2 + ); + defer splitAB.deinit(); + const split = splitAB; + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---+ + \\| B | + \\+---+ + \\ + ); + } +} + +test "SplitTree: split vertical with zero ratio" { + const testing = std.testing; + const alloc = testing.allocator; + + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); + defer t1.deinit(); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); + defer t2.deinit(); + + // A | B horizontal + var splitAB = try t1.split( + alloc, + .root, // at root + .down, // split right + 0, + &t2, // insert t2 + ); + defer splitAB.deinit(); + const split = splitAB; + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---+ + \\| B | + \\+---+ + \\ + ); + } +} + +test "SplitTree: split horizontal with full width" { + const testing = std.testing; + const alloc = testing.allocator; + + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); + defer t1.deinit(); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); + defer t2.deinit(); + + // A | B horizontal + var splitAB = try t1.split( + alloc, + .root, // at root + .right, // split right + 1, + &t2, // insert t2 + ); + defer splitAB.deinit(); + const split = splitAB; + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---+ + \\| A | + \\+---+ + \\ + ); + } +} + +test "SplitTree: split vertical with full width" { + const testing = std.testing; + const alloc = testing.allocator; + + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); + defer t1.deinit(); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); + defer t2.deinit(); + + // A | B horizontal + var splitAB = try t1.split( + alloc, + .root, // at root + .down, // split right + 1, + &t2, // insert t2 + ); + defer splitAB.deinit(); + const split = splitAB; + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---+ + \\| A | + \\+---+ + \\ + ); + } +} + +test "SplitTree: remove leaf" { + const testing = std.testing; + const alloc = testing.allocator; + + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); + defer t1.deinit(); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); + defer t2.deinit(); + var t3 = try t1.split( + alloc, + .root, // at root + .right, // split right + 0.5, + &t2, // insert t2 + ); + defer t3.deinit(); + + // Remove "A" + var it = t3.iterator(); + var t4 = try t3.remove( + alloc, + while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "A")) { + break entry.handle; + } + } else return error.NotFound, + ); + defer t4.deinit(); + + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(t4, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---+ + \\| B | + \\+---+ + \\ + ); +} + +test "SplitTree: split twice, remove intermediary" { + const testing = std.testing; + const alloc = testing.allocator; + + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); + defer t1.deinit(); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); + defer t2.deinit(); + var v3: TestTree.View = .{ .label = "C" }; + var t3: TestTree = try .init(alloc, &v3); + defer t3.deinit(); + + // A | B horizontal. + var split1 = try t1.split( + alloc, + .root, // at root + .right, // split right + 0.5, + &t2, // insert t2 + ); + defer split1.deinit(); + + // Insert C below that. + var split2 = try split1.split( + alloc, + .root, // at root + .down, // split down + 0.5, + &t3, // insert t3 + ); + defer split2.deinit(); + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split2, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---++---+ + \\| A || B | + \\+---++---+ + \\+--------+ + \\| C | + \\+--------+ + \\ + ); + } + + // Remove "B" + var it = split2.iterator(); + var split3 = try split2.remove( + alloc, + while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "B")) { + break entry.handle; + } + } else return error.NotFound, + ); + defer split3.deinit(); + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split3, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---+ + \\| A | + \\+---+ + \\+---+ + \\| C | + \\+---+ + \\ + ); + } + + // Remove every node from split2 (our most complex one), which should + // never crash. We don't test the result is correct, this just verifies + // we don't hit any assertion failures. + for (0..split2.nodes.len) |i| { + var t = try split2.remove(alloc, @enumFromInt(i)); + t.deinit(); + } +} + +test "SplitTree: spatial goto" { + const testing = std.testing; + const alloc = testing.allocator; + + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); + defer t1.deinit(); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); + defer t2.deinit(); + var v3: TestTree.View = .{ .label = "C" }; + var t3: TestTree = try .init(alloc, &v3); + defer t3.deinit(); + var v4: TestTree.View = .{ .label = "D" }; + var t4: TestTree = try .init(alloc, &v4); + defer t4.deinit(); + + // A | B horizontal + var splitAB = try t1.split( + alloc, + .root, // at root + .right, // split right + 0.5, + &t2, // insert t2 + ); + defer splitAB.deinit(); + + // A | C vertical + var splitAC = try splitAB.split( + alloc, + at: { + var it = splitAB.iterator(); + break :at while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "A")) { + break entry.handle; + } + } else return error.NotFound; + }, + .down, // split down + 0.8, + &t3, // insert t3 + ); + defer splitAC.deinit(); + + // B | D vertical + var splitBD = try splitAC.split( + alloc, + at: { + var it = splitAB.iterator(); + break :at while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "B")) { + break entry.handle; + } + } else return error.NotFound; + }, + .down, // split down + 0.3, + &t4, // insert t4 + ); + defer splitBD.deinit(); + const split = splitBD; + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---++---+ + \\| || B | + \\| |+---+ + \\| |+---+ + \\| A || | + \\| || | + \\| || | + \\| || D | + \\+---+| | + \\+---+| | + \\| C || | + \\+---++---+ + \\ + ); + } + + // Spatial C => right + { + const target = (try split.goto( + alloc, + from: { + var it = split.iterator(); + break :from while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "C")) { + break entry.handle; + } + } else return error.NotFound; + }, + .{ .spatial = .right }, + )).?; + const view = split.nodes[target.idx()].leaf; + try testing.expectEqualStrings(view.label, "D"); + } + + // Spatial D => left + { + const target = (try split.goto( + alloc, + from: { + var it = split.iterator(); + break :from while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "D")) { + break entry.handle; + } + } else return error.NotFound; + }, + .{ .spatial = .left }, + )).?; + const view = split.nodes[target.idx()].leaf; + try testing.expectEqualStrings("A", view.label); + } + + // Spatial A => left (wrapped) + { + const target = (try split.goto( + alloc, + from: { + var it = split.iterator(); + break :from while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "A")) { + break entry.handle; + } + } else return error.NotFound; + }, + .{ .spatial = .left }, + )).?; + const view = split.nodes[target.idx()].leaf; + try testing.expectEqualStrings("B", view.label); + } + + // Spatial B => right (wrapped) + { + const target = (try split.goto( + alloc, + from: { + var it = split.iterator(); + break :from while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "B")) { + break entry.handle; + } + } else return error.NotFound; + }, + .{ .spatial = .right }, + )).?; + const view = split.nodes[target.idx()].leaf; + try testing.expectEqualStrings("A", view.label); + } + + // Spatial C => down (wrapped) + { + const target = (try split.goto( + alloc, + from: { + var it = split.iterator(); + break :from while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "C")) { + break entry.handle; + } + } else return error.NotFound; + }, + .{ .spatial = .down }, + )).?; + const view = split.nodes[target.idx()].leaf; + try testing.expectEqualStrings("A", view.label); + } + + // Equalize + var equal = try split.equalize(alloc); + defer equal.deinit(); + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(equal, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---++---+ + \\| A || B | + \\+---++---+ + \\+---++---+ + \\| C || D | + \\+---++---+ + \\ + ); + } +} + +test "SplitTree: resize" { + const testing = std.testing; + const alloc = testing.allocator; + + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); + defer t1.deinit(); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); + defer t2.deinit(); + + // A | B horizontal + var split = try t1.split( + alloc, + .root, // at root + .right, // split right + 0.5, + &t2, // insert t2 + ); + defer split.deinit(); + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---++---+ + \\| A || B | + \\+---++---+ + \\ + ); + } + + // Resize + { + var resized = try split.resize( + alloc, + at: { + var it = split.iterator(); + break :at while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "B")) { + break entry.handle; + } + } else return error.NotFound; + }, + .horizontal, // resize right + 0.25, + ); + defer resized.deinit(); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(resized, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+-------------++---+ + \\| A || B | + \\+-------------++---+ + \\ + ); + } + + // Resize the other direction (negative ratio) + { + var resized = try split.resize( + alloc, + at: { + var it = split.iterator(); + break :at while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "B")) { + break entry.handle; + } + } else return error.NotFound; + }, + .horizontal, // resize left + -0.25, + ); + defer resized.deinit(); + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(resized, .formatDiagram)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\+---++-------------+ + \\| A || B | + \\+---++-------------+ + \\ + ); + } +} + +test "SplitTree: clone empty tree" { + const testing = std.testing; + const alloc = testing.allocator; + var t: TestTree = .empty; + defer t.deinit(); + + var t2 = try t.clone(alloc); + defer t2.deinit(); + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{t2}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\empty + ); + } +} + +test "SplitTree: zoom" { + const testing = std.testing; + const alloc = testing.allocator; + + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); + defer t1.deinit(); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); + defer t2.deinit(); + + // A | B horizontal + var split = try t1.split( + alloc, + .root, // at root + .right, // split right + 0.5, + &t2, // insert t2 + ); + defer split.deinit(); + split.zoom(at: { + var it = split.iterator(); + break :at while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "B")) { + break entry.handle; + } + } else return error.NotFound; + }); + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatText)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\split (layout: horizontal, ratio: 0.50) + \\ leaf: A + \\ (zoomed) leaf: B + \\ + ); + } + + // Clone preserves zoom + var clone = try split.clone(alloc); + defer clone.deinit(); + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(clone, .formatText)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\split (layout: horizontal, ratio: 0.50) + \\ leaf: A + \\ (zoomed) leaf: B + \\ + ); + } +} + +test "SplitTree: split resets zoom" { + const testing = std.testing; + const alloc = testing.allocator; + + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); + defer t1.deinit(); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); + defer t2.deinit(); + + // Zoom A + t1.zoom(at: { + var it = t1.iterator(); + break :at while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "A")) { + break entry.handle; + } + } else return error.NotFound; + }); + + // A | B horizontal + var split = try t1.split( + alloc, + .root, // at root + .right, // split right + 0.5, + &t2, // insert t2 + ); + defer split.deinit(); + + { + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(split, .formatText)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\split (layout: horizontal, ratio: 0.50) + \\ leaf: A + \\ leaf: B + \\ + ); + } +} + +test "SplitTree: remove and zoom" { + const testing = std.testing; + const alloc = testing.allocator; + + var v1: TestTree.View = .{ .label = "A" }; + var t1: TestTree = try .init(alloc, &v1); + defer t1.deinit(); + var v2: TestTree.View = .{ .label = "B" }; + var t2: TestTree = try .init(alloc, &v2); + defer t2.deinit(); + + // A | B horizontal + var split = try t1.split( + alloc, + .root, // at root + .right, // split right + 0.5, + &t2, // insert t2 + ); + defer split.deinit(); + split.zoom(at: { + var it = split.iterator(); + break :at while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "A")) { + break entry.handle; + } + } else return error.NotFound; + }); + + // Remove A, should unzoom + { + var removed = try split.remove( + alloc, + at: { + var it = split.iterator(); + break :at while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "A")) { + break entry.handle; + } + } else return error.NotFound; + }, + ); + defer removed.deinit(); + try testing.expect(removed.zoomed == null); + + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(removed, .formatText)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\leaf: B + \\ + ); + } + + // Remove B, should keep zoom + { + var removed = try split.remove( + alloc, + at: { + var it = split.iterator(); + break :at while (it.next()) |entry| { + if (std.mem.eql(u8, entry.view.label, "B")) { + break entry.handle; + } + } else return error.NotFound; + }, + ); + defer removed.deinit(); + + const str = try std.fmt.allocPrint(alloc, "{f}", .{std.fmt.alt(removed, .formatText)}); + defer alloc.free(str); + try testing.expectEqualStrings(str, + \\(zoomed) leaf: A + \\ + ); + } +} |
