diff --git a/src/terminal/new/page.zig b/src/terminal/new/page.zig index 51851ec70..d37661f94 100644 --- a/src/terminal/new/page.zig +++ b/src/terminal/new/page.zig @@ -2,8 +2,11 @@ const std = @import("std"); const assert = std.debug.assert; const color = @import("../color.zig"); const sgr = @import("../sgr.zig"); +const style = @import("style.zig"); const size = @import("size.zig"); const Offset = size.Offset; +const hash_map = @import("hash_map.zig"); +const AutoOffsetHashMap = hash_map.AutoOffsetHashMap; /// A page represents a specific section of terminal screen. The primary /// idea of a page is that it is a fully self-contained unit that can be @@ -93,3 +96,14 @@ test { _ = Page; _ = Style; } + +// test { +// const testing = std.testing; +// const cap = try std.math.ceilPowerOfTwo(usize, 350); +// const StyleIdMap = AutoOffsetHashMap(size.CellCountInt, style.Style); +// const StyleMetadataMap = AutoOffsetHashMap(style.Style, style.Metadata); +// +// var len = StyleIdMap.bufferSize(@intCast(cap)); +// len += StyleMetadataMap.bufferSize(@intCast(cap)); +// try testing.expectEqual(@as(usize, 0), len); +// } diff --git a/src/terminal/new/size.zig b/src/terminal/new/size.zig index 47cf23edf..01a8bece1 100644 --- a/src/terminal/new/size.zig +++ b/src/terminal/new/size.zig @@ -30,7 +30,7 @@ pub fn Offset(comptime T: type) type { // our return type is naturally aligned. We COULD modify this // to return arbitrary alignment, but its not something we need. assert(@mod(self.offset, @alignOf(T)) == 0); - return @ptrFromInt(@intFromPtr(base) + self.offset); + return @ptrFromInt(intFromBase(base) + self.offset); } }; } @@ -42,12 +42,27 @@ pub fn getOffset( base: anytype, ptr: *const T, ) Offset(T) { - const base_int = @intFromPtr(base); + const base_int = intFromBase(base); const ptr_int = @intFromPtr(ptr); const offset = ptr_int - base_int; return .{ .offset = @intCast(offset) }; } +fn intFromBase(base: anytype) usize { + return switch (@typeInfo(@TypeOf(base))) { + .Pointer => |v| switch (v.size) { + .One, + .Many, + .C, + => @intFromPtr(base), + + .Slice => @intFromPtr(base.ptr), + }, + + else => @compileError("invalid base type"), + }; +} + test "Offset" { // This test is here so that if Offset changes, we can be very aware // of this effect and think about the implications of it. diff --git a/src/terminal/new/style.zig b/src/terminal/new/style.zig index 6fdb8c3e7..549972975 100644 --- a/src/terminal/new/style.zig +++ b/src/terminal/new/style.zig @@ -1,7 +1,11 @@ const std = @import("std"); +const assert = std.debug.assert; const color = @import("../color.zig"); const sgr = @import("../sgr.zig"); const size = @import("size.zig"); +const Offset = size.Offset; +const hash_map = @import("hash_map.zig"); +const AutoOffsetHashMap = hash_map.AutoOffsetHashMap; /// The unique identifier for a style. This is at most the number of cells /// that can fit into a terminal page. @@ -43,20 +47,144 @@ pub const Style = struct { } }; -/// Maps a style definition to metadata about that style. -pub const MetadataMap = std.AutoHashMapUnmanaged(Style, Metadata); +/// A set of styles. +pub const Set = struct { + /// The mapping of a style to associated metadata. This is + /// the map that contains the actual style definitions + /// (in the form of the key). + styles: MetadataMap, -/// Maps the unique style ID to the concrete style definition. -pub const IdMap = std.AutoHashMapUnmanaged(size.CellCountInt, Style); + /// The mapping from ID to style. + id_map: IdMap, + + /// The next ID to use for a style that isn't in the set. + /// When this overflows we'll begin returning an IdOverflow + /// error and the caller must manually compact the style + /// set. + next_id: Id = 1, + + /// Maps a style definition to metadata about that style. + const MetadataMap = AutoOffsetHashMap(Style, Metadata); + + /// Maps the unique style ID to the concrete style definition. + const IdMap = AutoOffsetHashMap(Id, Offset(Style)); + + /// Returns the memory layout for the given base offset and + /// desired capacity. The layout can be used by the caller to + /// determine how much memory to allocate, and the layout must + /// be used to initialize the set so that the set knows all + /// the offsets for the various buffers. + pub fn layoutForCapacity(base: usize, cap: usize) Layout { + const md_start = std.mem.alignForward(usize, base, MetadataMap.base_align); + const md_end = md_start + MetadataMap.bufferSize(@intCast(cap)); + + const id_start = std.mem.alignForward(usize, md_end, IdMap.base_align); + const id_end = id_start + IdMap.bufferSize(@intCast(cap)); + + const total_size = id_end - base; + + return .{ + .cap = cap, + .md_start = md_start, + .id_start = id_start, + .total_size = total_size, + }; + } + + pub const Layout = struct { + cap: usize, + md_start: usize, + id_start: usize, + total_size: usize, + }; + + pub fn init(base: []u8, layout: Layout) Set { + assert(base.len >= layout.total_size); + + var styles = MetadataMap.init(@intCast(layout.cap), base[layout.md_start..]); + styles.metadata.offset += @intCast(layout.md_start); + + var id_map = IdMap.init(@intCast(layout.cap), base[layout.id_start..]); + id_map.metadata.offset += @intCast(layout.id_start); + + return .{ + .styles = styles, + .id_map = id_map, + }; + } + + /// Upsert a style into the set and return a pointer to the metadata + /// for that style. The pointer is valid for the lifetime of the set + /// so long as the style is not removed. + pub fn upsert(self: *Set, base: anytype, style: Style) !*Metadata { + // If we already have the style in the map, this is fast. + var map = self.styles.map(base); + const gop = try map.getOrPut(style); + if (gop.found_existing) return gop.value_ptr; + + // New style, we need to setup all the metadata. First thing, + // let's get the ID we'll assign, because if we're out of space + // we need to fail early. + errdefer map.removeByPtr(gop.key_ptr); + const id = self.next_id; + self.next_id = try std.math.add(Id, self.next_id, 1); + errdefer self.next_id -= 1; + gop.value_ptr.* = .{ .id = id }; + + // Setup our ID mapping + var id_map = self.id_map.map(base); + const id_gop = try id_map.getOrPut(id); + errdefer id_map.removeByPtr(id_gop.key_ptr); + assert(!id_gop.found_existing); + id_gop.value_ptr.* = size.getOffset(Style, base, gop.key_ptr); + return gop.value_ptr; + } + + /// Lookup a style by its unique identifier. + pub fn lookupId(self: *const Set, base: anytype, id: Id) ?*Style { + const id_map = self.id_map.map(base); + const offset = id_map.get(id) orelse return null; + return @ptrCast(offset.ptr(base)); + } +}; /// Metadata about a style. This is used to track the reference count /// and the unique identifier for a style. The unique identifier is used /// to track the style in the full style map. pub const Metadata = struct { - ref: size.CellCountInt = 0, - id: size.CellCountInt = 0, + ref: size.CellCountInt = 1, + id: Id = 0, }; test { _ = Style; + _ = Set; +} + +test "Set basic usage" { + const testing = std.testing; + const alloc = testing.allocator; + const layout = Set.layoutForCapacity(0, 16); + const buf = try alloc.alloc(u8, layout.total_size); + defer alloc.free(buf); + + const style: Style = .{ .flags = .{ .bold = true } }; + + var set = Set.init(buf, layout); + + // Upsert + const meta = try set.upsert(buf, style); + try testing.expect(meta.id > 0); + + // Second upsert should return the same metadata. + { + const meta2 = try set.upsert(buf, style); + try testing.expectEqual(meta.id, meta2.id); + } + + // Look it up + { + const v = set.lookupId(buf, meta.id).?; + try testing.expect(v.flags.bold); + } }