mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00

In multiple tests we create 1 or more pages by growing them 1 row at a time, which results in an integrity check of the page for each row grown which is just... horrible. By simply pausing integrity checks while growing these pages (since growing them is not the point of the test) we MASSIVELY speed up all of these tests. Also reduced grapheme bytes during testing and made the Terminal "glitch text" test actually assert what it intends to achieve, rather than just blindly assuming 100 copies of the text will be the right amount -- this lets us stop a lot earlier, making it take practically no time.
3180 lines
113 KiB
Zig
3180 lines
113 KiB
Zig
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const build_config = @import("../build_config.zig");
|
|
const Allocator = std.mem.Allocator;
|
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
const assert = std.debug.assert;
|
|
const testing = std.testing;
|
|
const posix = std.posix;
|
|
const fastmem = @import("../fastmem.zig");
|
|
const color = @import("color.zig");
|
|
const hyperlink = @import("hyperlink.zig");
|
|
const kitty = @import("kitty.zig");
|
|
const sgr = @import("sgr.zig");
|
|
const style = @import("style.zig");
|
|
const size = @import("size.zig");
|
|
const getOffset = size.getOffset;
|
|
const Offset = size.Offset;
|
|
const OffsetBuf = size.OffsetBuf;
|
|
const BitmapAllocator = @import("bitmap_allocator.zig").BitmapAllocator;
|
|
const hash_map = @import("hash_map.zig");
|
|
const AutoOffsetHashMap = hash_map.AutoOffsetHashMap;
|
|
const alignForward = std.mem.alignForward;
|
|
const alignBackward = std.mem.alignBackward;
|
|
|
|
const log = std.log.scoped(.page);
|
|
|
|
/// The allocator to use for multi-codepoint grapheme data. We use
|
|
/// a chunk size of 4 codepoints. It'd be best to set this empirically
|
|
/// but it is currently set based on vibes. My thinking around 4 codepoints
|
|
/// is that most skin-tone emoji are <= 4 codepoints, letter combiners
|
|
/// are usually <= 4 codepoints, and 4 codepoints is a nice power of two
|
|
/// for alignment.
|
|
const grapheme_chunk_len = 4;
|
|
const grapheme_chunk = grapheme_chunk_len * @sizeOf(u21);
|
|
const GraphemeAlloc = BitmapAllocator(grapheme_chunk);
|
|
const grapheme_count_default = GraphemeAlloc.bitmap_bit_size;
|
|
const grapheme_bytes_default = grapheme_count_default * grapheme_chunk;
|
|
const GraphemeMap = AutoOffsetHashMap(Offset(Cell), Offset(u21).Slice);
|
|
|
|
/// The allocator used for shared utf8-encoded strings within a page.
|
|
/// Note the chunk size below is the minimum size of a single allocation
|
|
/// and requires a single bit of metadata in our bitmap allocator. Therefore
|
|
/// it should be tuned carefully (too small and we waste metadata, too large
|
|
/// and we have fragmentation). We can probably use a better allocation
|
|
/// strategy in the future.
|
|
///
|
|
/// At the time of writing this, the strings table is only used for OSC8
|
|
/// IDs and URIs. IDs are usually short and URIs are usually longer. I chose
|
|
/// 32 bytes as a compromise between these two since it represents single
|
|
/// domain links quite well and is not too wasteful for short IDs. We can
|
|
/// continue to tune this as we see how it's used.
|
|
const string_chunk_len = 32;
|
|
const string_chunk = string_chunk_len * @sizeOf(u8);
|
|
const StringAlloc = BitmapAllocator(string_chunk);
|
|
const string_count_default = StringAlloc.bitmap_bit_size;
|
|
const string_bytes_default = string_count_default * string_chunk;
|
|
|
|
/// Default number of hyperlinks we support.
|
|
///
|
|
/// The cell multiplier is the number of cells per hyperlink entry that
|
|
/// we support. A hyperlink can be longer than this multiplier; the multiplier
|
|
/// just sets the total capacity to simplify adjustable size metrics.
|
|
const hyperlink_count_default = 4;
|
|
const hyperlink_bytes_default = hyperlink_count_default * @sizeOf(hyperlink.Set.Item);
|
|
const hyperlink_cell_multiplier = 16;
|
|
|
|
/// 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
|
|
/// serialized, copied, etc. as a convenient way to represent a section
|
|
/// of the screen.
|
|
///
|
|
/// This property is useful for renderers which want to copy just the pages
|
|
/// for the visible portion of the screen, or for infinite scrollback where
|
|
/// we may want to serialize and store pages that are sufficiently far
|
|
/// away from the current viewport.
|
|
///
|
|
/// Pages are always backed by a single contiguous block of memory that is
|
|
/// aligned on a page boundary. This makes it easy and fast to copy pages
|
|
/// around. Within the contiguous block of memory, the contents of a page are
|
|
/// thoughtfully laid out to optimize primarily for terminal IO (VT streams)
|
|
/// and to minimize memory usage.
|
|
pub const Page = struct {
|
|
comptime {
|
|
// The alignment of our members. We want to ensure that the page
|
|
// alignment is always divisible by this.
|
|
assert(std.mem.page_size % @max(
|
|
@alignOf(Row),
|
|
@alignOf(Cell),
|
|
style.Set.base_align,
|
|
) == 0);
|
|
}
|
|
|
|
/// The backing memory for the page. A page is always made up of a
|
|
/// a single contiguous block of memory that is aligned on a page
|
|
/// boundary and is a multiple of the system page size.
|
|
memory: []align(std.mem.page_size) u8,
|
|
|
|
/// The array of rows in the page. The rows are always in row order
|
|
/// (i.e. index 0 is the top row, index 1 is the row below that, etc.)
|
|
rows: Offset(Row),
|
|
|
|
/// The array of cells in the page. The cells are NOT in row order,
|
|
/// but they are in column order. To determine the mapping of cells
|
|
/// to row, you must use the `rows` field. From the pointer to the
|
|
/// first column, all cells in that row are laid out in column order.
|
|
cells: Offset(Cell),
|
|
|
|
/// The string allocator for this page used for shared utf-8 encoded
|
|
/// strings. Liveness of strings and memory management is deferred to
|
|
/// the individual use case.
|
|
string_alloc: StringAlloc,
|
|
|
|
/// The multi-codepoint grapheme data for this page. This is where
|
|
/// any cell that has more than one codepoint will be stored. This is
|
|
/// relatively rare (typically only emoji) so this defaults to a very small
|
|
/// size and we force page realloc when it grows.
|
|
grapheme_alloc: GraphemeAlloc,
|
|
|
|
/// The mapping of cell to grapheme data. The exact mapping is the
|
|
/// cell offset to the grapheme data offset. Therefore, whenever a
|
|
/// cell is moved (i.e. `erase`) then the grapheme data must be updated.
|
|
/// Grapheme data is relatively rare so this is considered a slow
|
|
/// path.
|
|
grapheme_map: GraphemeMap,
|
|
|
|
/// The available set of styles in use on this page.
|
|
styles: style.Set,
|
|
|
|
/// The structures used for tracking hyperlinks within the page.
|
|
/// The map maps cell offsets to hyperlink IDs and the IDs are in
|
|
/// the ref counted set. The strings within the hyperlink structures
|
|
/// are allocated in the string allocator.
|
|
hyperlink_map: hyperlink.Map,
|
|
hyperlink_set: hyperlink.Set,
|
|
|
|
/// The offset to the first mask of dirty bits in the page.
|
|
///
|
|
/// The dirty bits is a contiguous array of usize where each bit represents
|
|
/// a row in the page, in order. If the bit is set, then the row is dirty
|
|
/// and requires a redraw. Dirty status is only ever meant to convey that
|
|
/// a cell has changed visually. A cell which changes in a way that doesn't
|
|
/// affect the visual representation may not be marked as dirty.
|
|
///
|
|
/// Dirty tracking may have false positives but should never have false
|
|
/// negatives. A false negative would result in a visual artifact on the
|
|
/// screen.
|
|
///
|
|
/// Dirty bits are only ever unset by consumers of a page. The page
|
|
/// structure itself does not unset dirty bits since the page does not
|
|
/// know when a cell has been redrawn.
|
|
///
|
|
/// As implementation background: it may seem that dirty bits should be
|
|
/// stored elsewhere and not on the page itself, because the only data
|
|
/// that could possibly change is in the active area of a terminal
|
|
/// historically and that area is small compared to the typical scrollback.
|
|
/// My original thinking was to put the dirty bits on Screen instead and
|
|
/// have them only track the active area. However, I decided to put them
|
|
/// into the page directly for a few reasons:
|
|
///
|
|
/// 1. It's simpler. The page is a self-contained unit and it's nice
|
|
/// to have all the data for a page in one place.
|
|
///
|
|
/// 2. It's cheap. Even a very large page might have 1000 rows and
|
|
/// that's only ~128 bytes of 64-bit integers to track all the dirty
|
|
/// bits. Compared to the hundreds of kilobytes a typical page
|
|
/// consumes, this is nothing.
|
|
///
|
|
/// 3. It's more flexible. If we ever want to implement new terminal
|
|
/// features that allow non-active area to be dirty, we can do that
|
|
/// with minimal dirty-tracking work.
|
|
///
|
|
dirty: Offset(usize),
|
|
|
|
/// The current dimensions of the page. The capacity may be larger
|
|
/// than this. This allows us to allocate a larger page than necessary
|
|
/// and also to resize a page smaller without reallocating.
|
|
size: Size,
|
|
|
|
/// The capacity of this page. This is the full size of the backing
|
|
/// memory and is fixed at page creation time.
|
|
capacity: Capacity,
|
|
|
|
/// If this is true then verifyIntegrity will do nothing. This is
|
|
/// only present with runtime safety enabled.
|
|
pause_integrity_checks: if (build_config.slow_runtime_safety) usize else void =
|
|
if (build_config.slow_runtime_safety) 0 else {},
|
|
|
|
/// Initialize a new page, allocating the required backing memory.
|
|
/// The size of the initialized page defaults to the full capacity.
|
|
///
|
|
/// The backing memory is always allocated using mmap directly.
|
|
/// You cannot use custom allocators with this structure because
|
|
/// it is critical to performance that we use mmap.
|
|
pub fn init(cap: Capacity) !Page {
|
|
const l = layout(cap);
|
|
|
|
// We use mmap directly to avoid Zig allocator overhead
|
|
// (small but meaningful for this path) and because a private
|
|
// anonymous mmap is guaranteed on Linux and macOS to be zeroed,
|
|
// which is a critical property for us.
|
|
assert(l.total_size % std.mem.page_size == 0);
|
|
const backing = try posix.mmap(
|
|
null,
|
|
l.total_size,
|
|
posix.PROT.READ | posix.PROT.WRITE,
|
|
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
|
|
-1,
|
|
0,
|
|
);
|
|
errdefer posix.munmap(backing);
|
|
|
|
const buf = OffsetBuf.init(backing);
|
|
return initBuf(buf, l);
|
|
}
|
|
|
|
/// Initialize a new page using the given backing memory.
|
|
/// It is up to the caller to not call deinit on these pages.
|
|
pub fn initBuf(buf: OffsetBuf, l: Layout) Page {
|
|
const cap = l.capacity;
|
|
const rows = buf.member(Row, l.rows_start);
|
|
const cells = buf.member(Cell, l.cells_start);
|
|
|
|
// We need to go through and initialize all the rows so that
|
|
// they point to a valid offset into the cells, since the rows
|
|
// zero-initialized aren't valid.
|
|
const cells_ptr = cells.ptr(buf)[0 .. cap.cols * cap.rows];
|
|
for (rows.ptr(buf)[0..cap.rows], 0..) |*row, y| {
|
|
const start = y * cap.cols;
|
|
row.* = .{
|
|
.cells = getOffset(Cell, buf, &cells_ptr[start]),
|
|
};
|
|
}
|
|
|
|
return .{
|
|
.memory = @alignCast(buf.start()[0..l.total_size]),
|
|
.rows = rows,
|
|
.cells = cells,
|
|
.dirty = buf.member(usize, l.dirty_start),
|
|
.styles = style.Set.init(
|
|
buf.add(l.styles_start),
|
|
l.styles_layout,
|
|
.{},
|
|
),
|
|
.string_alloc = StringAlloc.init(
|
|
buf.add(l.string_alloc_start),
|
|
l.string_alloc_layout,
|
|
),
|
|
.grapheme_alloc = GraphemeAlloc.init(
|
|
buf.add(l.grapheme_alloc_start),
|
|
l.grapheme_alloc_layout,
|
|
),
|
|
.grapheme_map = GraphemeMap.init(
|
|
buf.add(l.grapheme_map_start),
|
|
l.grapheme_map_layout,
|
|
),
|
|
.hyperlink_map = hyperlink.Map.init(
|
|
buf.add(l.hyperlink_map_start),
|
|
l.hyperlink_map_layout,
|
|
),
|
|
.hyperlink_set = hyperlink.Set.init(
|
|
buf.add(l.hyperlink_set_start),
|
|
l.hyperlink_set_layout,
|
|
.{},
|
|
),
|
|
.size = .{ .cols = cap.cols, .rows = cap.rows },
|
|
.capacity = cap,
|
|
};
|
|
}
|
|
|
|
/// Deinitialize the page, freeing any backing memory. Do NOT call
|
|
/// this if you allocated the backing memory yourself (i.e. you used
|
|
/// initBuf).
|
|
pub fn deinit(self: *Page) void {
|
|
posix.munmap(self.memory);
|
|
self.* = undefined;
|
|
}
|
|
|
|
/// Reinitialize the page with the same capacity.
|
|
pub fn reinit(self: *Page) void {
|
|
// We zero the page memory as u64 instead of u8 because
|
|
// we can and it's empirically quite a bit faster.
|
|
@memset(@as([*]u64, @ptrCast(self.memory))[0 .. self.memory.len / 8], 0);
|
|
self.* = initBuf(OffsetBuf.init(self.memory), layout(self.capacity));
|
|
}
|
|
|
|
pub const IntegrityError = error{
|
|
ZeroRowCount,
|
|
ZeroColCount,
|
|
UnmarkedGraphemeRow,
|
|
MissingGraphemeData,
|
|
InvalidGraphemeCount,
|
|
UnmarkedGraphemeCell,
|
|
MissingStyle,
|
|
UnmarkedStyleRow,
|
|
MismatchedStyleRef,
|
|
InvalidStyleCount,
|
|
MissingHyperlinkData,
|
|
MismatchedHyperlinkRef,
|
|
UnmarkedHyperlinkCell,
|
|
UnmarkedHyperlinkRow,
|
|
InvalidSpacerTailLocation,
|
|
InvalidSpacerHeadLocation,
|
|
UnwrappedSpacerHead,
|
|
};
|
|
|
|
/// Temporarily pause integrity checks. This is useful when you are
|
|
/// doing a lot of operations that would trigger integrity check
|
|
/// violations but you know the page will end up in a consistent state.
|
|
pub fn pauseIntegrityChecks(self: *Page, v: bool) void {
|
|
if (build_config.slow_runtime_safety) {
|
|
if (v) {
|
|
self.pause_integrity_checks += 1;
|
|
} else {
|
|
self.pause_integrity_checks -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A helper that can be used to assert the integrity of the page
|
|
/// when runtime safety is enabled. This is a no-op when runtime
|
|
/// safety is disabled. This uses the libc allocator.
|
|
pub fn assertIntegrity(self: *const Page) void {
|
|
if (comptime build_config.slow_runtime_safety) {
|
|
self.verifyIntegrity(std.heap.c_allocator) catch |err| {
|
|
log.err("page integrity violation, crashing. err={}", .{err});
|
|
@panic("page integrity violation");
|
|
};
|
|
}
|
|
}
|
|
|
|
/// Verifies the integrity of the page data. This is not fast,
|
|
/// but it is useful for assertions, deserialization, etc. The
|
|
/// allocator is only used for temporary allocations -- all memory
|
|
/// is freed before this function returns.
|
|
///
|
|
/// Integrity errors are also logged as warnings.
|
|
pub fn verifyIntegrity(self: *const Page, alloc_gpa: Allocator) !void {
|
|
// Some things that seem like we should check but do not:
|
|
//
|
|
// - We do not check that the style ref count is exact, only that
|
|
// it is at least what we see. We do this because some fast paths
|
|
// trim rows without clearing data.
|
|
// - We do not check that styles seen is exactly the same as the
|
|
// styles count in the page for the same reason as above.
|
|
// - We only check that we saw less graphemes than the total memory
|
|
// used for the same reason as styles above.
|
|
//
|
|
|
|
if (build_config.slow_runtime_safety) {
|
|
if (self.pause_integrity_checks > 0) return;
|
|
}
|
|
|
|
if (self.size.rows == 0) {
|
|
log.warn("page integrity violation zero row count", .{});
|
|
return IntegrityError.ZeroRowCount;
|
|
}
|
|
if (self.size.cols == 0) {
|
|
log.warn("page integrity violation zero col count", .{});
|
|
return IntegrityError.ZeroColCount;
|
|
}
|
|
|
|
var arena = ArenaAllocator.init(alloc_gpa);
|
|
defer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
|
|
var graphemes_seen: usize = 0;
|
|
var styles_seen = std.AutoHashMap(style.Id, usize).init(alloc);
|
|
defer styles_seen.deinit();
|
|
var hyperlinks_seen = std.AutoHashMap(hyperlink.Id, usize).init(alloc);
|
|
defer hyperlinks_seen.deinit();
|
|
|
|
const grapheme_count = self.graphemeCount();
|
|
|
|
const rows = self.rows.ptr(self.memory)[0..self.size.rows];
|
|
for (rows, 0..) |*row, y| {
|
|
const graphemes_start = graphemes_seen;
|
|
const cells = row.cells.ptr(self.memory)[0..self.size.cols];
|
|
for (cells, 0..) |*cell, x| {
|
|
if (cell.hasGrapheme()) {
|
|
// If a cell has grapheme data, it must be present in
|
|
// the grapheme map.
|
|
_ = self.lookupGrapheme(cell) orelse {
|
|
log.warn(
|
|
"page integrity violation y={} x={} grapheme data missing",
|
|
.{ y, x },
|
|
);
|
|
return IntegrityError.MissingGraphemeData;
|
|
};
|
|
|
|
graphemes_seen += 1;
|
|
} else if (grapheme_count > 0) {
|
|
// It should not have grapheme data if it isn't marked.
|
|
// The grapheme_count check above is just an optimization
|
|
// to speed up integrity checks.
|
|
if (self.lookupGrapheme(cell) != null) {
|
|
log.warn(
|
|
"page integrity violation y={} x={} cell not marked as grapheme",
|
|
.{ y, x },
|
|
);
|
|
return IntegrityError.UnmarkedGraphemeCell;
|
|
}
|
|
}
|
|
|
|
if (cell.style_id != style.default_id) {
|
|
// If a cell has a style, it must be present in the styles
|
|
// set. Accessing it with `get` asserts that.
|
|
_ = self.styles.get(
|
|
self.memory,
|
|
cell.style_id,
|
|
);
|
|
|
|
if (!row.styled) {
|
|
log.warn(
|
|
"page integrity violation y={} x={} row not marked as styled",
|
|
.{ y, x },
|
|
);
|
|
return IntegrityError.UnmarkedStyleRow;
|
|
}
|
|
|
|
const gop = try styles_seen.getOrPut(cell.style_id);
|
|
if (!gop.found_existing) gop.value_ptr.* = 0;
|
|
gop.value_ptr.* += 1;
|
|
}
|
|
|
|
if (cell.hyperlink) {
|
|
const id = self.lookupHyperlink(cell) orelse {
|
|
log.warn(
|
|
"page integrity violation y={} x={} hyperlink data missing",
|
|
.{ y, x },
|
|
);
|
|
return IntegrityError.MissingHyperlinkData;
|
|
};
|
|
|
|
if (!row.hyperlink) {
|
|
log.warn(
|
|
"page integrity violation y={} x={} row not marked as hyperlink",
|
|
.{ y, x },
|
|
);
|
|
return IntegrityError.UnmarkedHyperlinkRow;
|
|
}
|
|
|
|
const gop = try hyperlinks_seen.getOrPut(id);
|
|
if (!gop.found_existing) gop.value_ptr.* = 0;
|
|
gop.value_ptr.* += 1;
|
|
|
|
// Hyperlink ID should be valid. This just straight crashes
|
|
// if this fails due to assertions.
|
|
_ = self.hyperlink_set.get(self.memory, id);
|
|
} else {
|
|
// It should not have hyperlink data if it isn't marked
|
|
if (self.lookupHyperlink(cell) != null) {
|
|
log.warn(
|
|
"page integrity violation y={} x={} cell not marked as hyperlink",
|
|
.{ y, x },
|
|
);
|
|
return IntegrityError.UnmarkedHyperlinkCell;
|
|
}
|
|
}
|
|
|
|
switch (cell.wide) {
|
|
.narrow => {},
|
|
.wide => {},
|
|
|
|
.spacer_tail => {
|
|
// Spacer tails can't be at the start because they follow
|
|
// a wide char.
|
|
if (x == 0) {
|
|
log.warn(
|
|
"page integrity violation y={} x={} spacer tail at start",
|
|
.{ y, x },
|
|
);
|
|
return IntegrityError.InvalidSpacerTailLocation;
|
|
}
|
|
|
|
// Spacer tails must follow a wide char
|
|
const prev = cells[x - 1];
|
|
if (prev.wide != .wide) {
|
|
log.warn(
|
|
"page integrity violation y={} x={} spacer tail not following wide",
|
|
.{ y, x },
|
|
);
|
|
return IntegrityError.InvalidSpacerTailLocation;
|
|
}
|
|
},
|
|
|
|
.spacer_head => {
|
|
// Spacer heads must be at the end
|
|
if (x != self.size.cols - 1) {
|
|
log.warn(
|
|
"page integrity violation y={} x={} spacer head not at end",
|
|
.{ y, x },
|
|
);
|
|
return IntegrityError.InvalidSpacerHeadLocation;
|
|
}
|
|
|
|
// The row must be wrapped
|
|
if (!row.wrap) {
|
|
log.warn(
|
|
"page integrity violation y={} spacer head not wrapped",
|
|
.{y},
|
|
);
|
|
return IntegrityError.UnwrappedSpacerHead;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
// Check row grapheme data
|
|
if (graphemes_seen > graphemes_start) {
|
|
// If a cell in a row has grapheme data, the row must
|
|
// be marked as having grapheme data.
|
|
if (!row.grapheme) {
|
|
log.warn(
|
|
"page integrity violation y={} grapheme data but row not marked",
|
|
.{y},
|
|
);
|
|
return IntegrityError.UnmarkedGraphemeRow;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Our graphemes seen should exactly match the grapheme count
|
|
if (graphemes_seen > self.graphemeCount()) {
|
|
log.warn(
|
|
"page integrity violation grapheme count mismatch expected={} actual={}",
|
|
.{ graphemes_seen, self.graphemeCount() },
|
|
);
|
|
return IntegrityError.InvalidGraphemeCount;
|
|
}
|
|
|
|
// Verify all our styles have the correct ref count.
|
|
{
|
|
var it = styles_seen.iterator();
|
|
while (it.next()) |entry| {
|
|
const ref_count = self.styles.refCount(self.memory, entry.key_ptr.*);
|
|
if (ref_count < entry.value_ptr.*) {
|
|
log.warn(
|
|
"page integrity violation style ref count mismatch id={} expected={} actual={}",
|
|
.{ entry.key_ptr.*, entry.value_ptr.*, ref_count },
|
|
);
|
|
return IntegrityError.MismatchedStyleRef;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify all our hyperlinks have the correct ref count.
|
|
{
|
|
var it = hyperlinks_seen.iterator();
|
|
while (it.next()) |entry| {
|
|
const ref_count = self.hyperlink_set.refCount(self.memory, entry.key_ptr.*);
|
|
if (ref_count < entry.value_ptr.*) {
|
|
log.warn(
|
|
"page integrity violation hyperlink ref count mismatch id={} expected={} actual={}",
|
|
.{ entry.key_ptr.*, entry.value_ptr.*, ref_count },
|
|
);
|
|
return IntegrityError.MismatchedHyperlinkRef;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify there are no zombie styles, that is, styles in the
|
|
// set with ref counts > 0, which are not present in the page.
|
|
{
|
|
const styles_table = self.styles.table.ptr(self.memory)[0..self.styles.layout.table_cap];
|
|
const styles_items = self.styles.items.ptr(self.memory)[0..self.styles.layout.cap];
|
|
|
|
var zombies: usize = 0;
|
|
|
|
for (styles_table) |id| {
|
|
if (id == 0) continue;
|
|
const item = styles_items[id];
|
|
if (item.meta.ref == 0) continue;
|
|
|
|
const expected = styles_seen.get(id) orelse 0;
|
|
if (expected > 0) continue;
|
|
|
|
if (item.meta.ref > expected) {
|
|
zombies += 1;
|
|
}
|
|
}
|
|
|
|
// NOTE: This is currently disabled because @qwerasd says that
|
|
// certain fast paths can cause this but its okay.
|
|
// Just 1 zombie style might be the cursor style, so ignore it.
|
|
// if (zombies > 1) {
|
|
// log.warn(
|
|
// "page integrity violation zombie styles count={}",
|
|
// .{zombies},
|
|
// );
|
|
// return IntegrityError.ZombieStyles;
|
|
// }
|
|
}
|
|
}
|
|
|
|
/// Clone the contents of this page. This will allocate new memory
|
|
/// using the page allocator. If you want to manage memory manually,
|
|
/// use cloneBuf.
|
|
pub fn clone(self: *const Page) !Page {
|
|
const backing = try posix.mmap(
|
|
null,
|
|
self.memory.len,
|
|
posix.PROT.READ | posix.PROT.WRITE,
|
|
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
|
|
-1,
|
|
0,
|
|
);
|
|
errdefer posix.munmap(backing);
|
|
return self.cloneBuf(backing);
|
|
}
|
|
|
|
/// Clone the entire contents of this page.
|
|
///
|
|
/// The buffer must be at least the size of self.memory.
|
|
pub fn cloneBuf(self: *const Page, buf: []align(std.mem.page_size) u8) Page {
|
|
assert(buf.len >= self.memory.len);
|
|
|
|
// The entire concept behind a page is that everything is stored
|
|
// as offsets so we can do a simple linear copy of the backing
|
|
// memory and copy all the offsets and everything will work.
|
|
var result = self.*;
|
|
result.memory = buf[0..self.memory.len];
|
|
|
|
// This is a memcpy. We may want to investigate if there are
|
|
// faster ways to do this (i.e. copy-on-write tricks) but I suspect
|
|
// they'll be slower. I haven't experimented though.
|
|
// std.log.warn("copy bytes={}", .{self.memory.len});
|
|
fastmem.copy(u8, result.memory, self.memory);
|
|
|
|
return result;
|
|
}
|
|
|
|
pub const StyleSetError = error{
|
|
StyleSetOutOfMemory,
|
|
StyleSetNeedsRehash,
|
|
};
|
|
|
|
pub const HyperlinkError = error{
|
|
StringAllocOutOfMemory,
|
|
HyperlinkSetOutOfMemory,
|
|
HyperlinkSetNeedsRehash,
|
|
HyperlinkMapOutOfMemory,
|
|
};
|
|
|
|
pub const GraphemeError = error{
|
|
GraphemeMapOutOfMemory,
|
|
GraphemeAllocOutOfMemory,
|
|
};
|
|
|
|
pub const CloneFromError =
|
|
StyleSetError ||
|
|
HyperlinkError ||
|
|
GraphemeError;
|
|
|
|
/// Clone the contents of another page into this page. The capacities
|
|
/// can be different, but the size of the other page must fit into
|
|
/// this page.
|
|
///
|
|
/// The y_start and y_end parameters allow you to clone only a portion
|
|
/// of the other page. This is useful for splitting a page into two
|
|
/// or more pages.
|
|
///
|
|
/// The column count of this page will always be the same as this page.
|
|
/// If the other page has more columns, the extra columns will be
|
|
/// truncated. If the other page has fewer columns, the extra columns
|
|
/// will be zeroed.
|
|
pub fn cloneFrom(
|
|
self: *Page,
|
|
other: *const Page,
|
|
y_start: usize,
|
|
y_end: usize,
|
|
) CloneFromError!void {
|
|
assert(y_start <= y_end);
|
|
assert(y_end <= other.size.rows);
|
|
assert(y_end - y_start <= self.size.rows);
|
|
|
|
const other_rows = other.rows.ptr(other.memory)[y_start..y_end];
|
|
const rows = self.rows.ptr(self.memory)[0 .. y_end - y_start];
|
|
const other_dirty_set = other.dirtyBitSet();
|
|
var dirty_set = self.dirtyBitSet();
|
|
for (rows, 0.., other_rows, y_start..) |*dst_row, dst_y, *src_row, src_y| {
|
|
try self.cloneRowFrom(other, dst_row, src_row);
|
|
if (other_dirty_set.isSet(src_y)) dirty_set.set(dst_y);
|
|
}
|
|
|
|
// We should remain consistent
|
|
self.assertIntegrity();
|
|
}
|
|
|
|
/// Clone a single row from another page into this page.
|
|
pub fn cloneRowFrom(
|
|
self: *Page,
|
|
other: *const Page,
|
|
dst_row: *Row,
|
|
src_row: *const Row,
|
|
) CloneFromError!void {
|
|
try self.clonePartialRowFrom(
|
|
other,
|
|
dst_row,
|
|
src_row,
|
|
0,
|
|
self.size.cols,
|
|
);
|
|
}
|
|
|
|
/// Clone a single row from another page into this page, supporting
|
|
/// partial copy. cloneRowFrom calls this.
|
|
pub fn clonePartialRowFrom(
|
|
self: *Page,
|
|
other: *const Page,
|
|
dst_row: *Row,
|
|
src_row: *const Row,
|
|
x_start: usize,
|
|
x_end_req: usize,
|
|
) CloneFromError!void {
|
|
// This whole operation breaks integrity until the end.
|
|
self.pauseIntegrityChecks(true);
|
|
defer {
|
|
self.pauseIntegrityChecks(false);
|
|
self.assertIntegrity();
|
|
}
|
|
|
|
const cell_len = @min(self.size.cols, other.size.cols);
|
|
const x_end = @min(x_end_req, cell_len);
|
|
assert(x_start <= x_end);
|
|
const other_cells = src_row.cells.ptr(other.memory)[x_start..x_end];
|
|
const cells = dst_row.cells.ptr(self.memory)[x_start..x_end];
|
|
|
|
// If our destination has styles or graphemes then we need to
|
|
// clear some state. This will free up the managed memory as well.
|
|
if (dst_row.managedMemory()) self.clearCells(dst_row, x_start, x_end);
|
|
|
|
// Copy all the row metadata but keep our cells offset
|
|
dst_row.* = copy: {
|
|
var copy = src_row.*;
|
|
|
|
// If we're not copying the full row then we want to preserve
|
|
// some original state from our dst row.
|
|
if ((x_end - x_start) < self.size.cols) {
|
|
copy.wrap = dst_row.wrap;
|
|
copy.wrap_continuation = dst_row.wrap_continuation;
|
|
copy.grapheme = dst_row.grapheme;
|
|
copy.hyperlink = dst_row.hyperlink;
|
|
copy.styled = dst_row.styled;
|
|
}
|
|
|
|
// Our cell offset remains the same
|
|
copy.cells = dst_row.cells;
|
|
|
|
break :copy copy;
|
|
};
|
|
|
|
// If we have no managed memory in the source, then we can just
|
|
// copy it directly.
|
|
if (!src_row.managedMemory()) {
|
|
// This is an integrity check: if the row claims it doesn't
|
|
// have managed memory then all cells must also not have
|
|
// managed memory.
|
|
if (build_config.slow_runtime_safety) {
|
|
for (other_cells) |cell| {
|
|
assert(!cell.hasGrapheme());
|
|
assert(!cell.hyperlink);
|
|
assert(cell.style_id == style.default_id);
|
|
}
|
|
}
|
|
|
|
fastmem.copy(Cell, cells, other_cells);
|
|
} else {
|
|
// We have managed memory, so we have to do a slower copy to
|
|
// get all of that right.
|
|
for (cells, other_cells) |*dst_cell, *src_cell| {
|
|
dst_cell.* = src_cell.*;
|
|
|
|
// Reset any managed memory markers on the cell so that we don't
|
|
// hit an integrity check if we have to return an error because
|
|
// the page can't fit the new memory.
|
|
dst_cell.hyperlink = false;
|
|
dst_cell.style_id = style.default_id;
|
|
if (dst_cell.content_tag == .codepoint_grapheme) {
|
|
dst_cell.content_tag = .codepoint;
|
|
}
|
|
|
|
if (src_cell.hasGrapheme()) {
|
|
// To prevent integrity checks flipping. This will
|
|
// get fixed up when we check the style id below.
|
|
if (build_config.slow_runtime_safety) {
|
|
dst_cell.style_id = style.default_id;
|
|
}
|
|
|
|
// Copy the grapheme codepoints
|
|
const cps = other.lookupGrapheme(src_cell).?;
|
|
|
|
// Safe to use setGraphemes because we cleared all
|
|
// managed memory for our destination cell range.
|
|
try self.setGraphemes(dst_row, dst_cell, cps);
|
|
}
|
|
if (src_cell.hyperlink) hyperlink: {
|
|
const id = other.lookupHyperlink(src_cell).?;
|
|
|
|
// Fast-path: same page we can add with the same id.
|
|
if (other == self) {
|
|
self.hyperlink_set.use(self.memory, id);
|
|
try self.setHyperlink(dst_row, dst_cell, id);
|
|
break :hyperlink;
|
|
}
|
|
|
|
// Slow-path: get the hyperlink from the other page,
|
|
// add it, and migrate.
|
|
|
|
// If our page can't support an additional cell with
|
|
// a hyperlink then we have to return an error.
|
|
if (self.hyperlinkCount() >= self.hyperlinkCapacity()) {
|
|
// The hyperlink map capacity needs to be increased.
|
|
return error.HyperlinkMapOutOfMemory;
|
|
}
|
|
|
|
const other_link = other.hyperlink_set.get(other.memory, id);
|
|
const dst_id = dst_id: {
|
|
// First check if the link already exists in our page,
|
|
// and increment its refcount if so, since we're about
|
|
// to use it.
|
|
if (self.hyperlink_set.lookupContext(
|
|
self.memory,
|
|
other_link.*,
|
|
|
|
// `lookupContext` uses the context for hashing, and
|
|
// that doesn't write to the page, so this constCast
|
|
// is completely safe.
|
|
.{ .page = @constCast(other) },
|
|
)) |i| {
|
|
self.hyperlink_set.use(self.memory, i);
|
|
break :dst_id i;
|
|
}
|
|
|
|
// If we don't have this link in our page yet then
|
|
// we need to clone it over and add it to our set.
|
|
|
|
// Clone the link.
|
|
const dst_link = other_link.dupe(other, self) catch |e| {
|
|
comptime assert(@TypeOf(e) == error{OutOfMemory});
|
|
// The string alloc capacity needs to be increased.
|
|
return error.StringAllocOutOfMemory;
|
|
};
|
|
|
|
// Add it, preferring to use the same ID as the other
|
|
// page, since this *probably* speeds up full-page
|
|
// clones.
|
|
//
|
|
// TODO(qwerasd): verify the assumption that `addWithId`
|
|
// is ever actually useful, I think it may not be.
|
|
break :dst_id self.hyperlink_set.addWithIdContext(
|
|
self.memory,
|
|
dst_link,
|
|
id,
|
|
.{ .page = self },
|
|
) catch |e| switch (e) {
|
|
// The hyperlink set capacity needs to be increased.
|
|
error.OutOfMemory => return error.HyperlinkSetOutOfMemory,
|
|
|
|
// The hyperlink set needs to be rehashed.
|
|
error.NeedsRehash => return error.HyperlinkSetNeedsRehash,
|
|
} orelse id;
|
|
};
|
|
|
|
try self.setHyperlink(dst_row, dst_cell, dst_id);
|
|
}
|
|
if (src_cell.style_id != style.default_id) style: {
|
|
dst_row.styled = true;
|
|
|
|
if (other == self) {
|
|
// If it's the same page we don't have to worry about
|
|
// copying the style, we can use the style ID directly.
|
|
dst_cell.style_id = src_cell.style_id;
|
|
self.styles.use(self.memory, dst_cell.style_id);
|
|
break :style;
|
|
}
|
|
|
|
// Slow path: Get the style from the other
|
|
// page and add it to this page's style set.
|
|
const other_style = other.styles.get(other.memory, src_cell.style_id);
|
|
dst_cell.style_id = self.styles.addWithId(
|
|
self.memory,
|
|
other_style.*,
|
|
src_cell.style_id,
|
|
) catch |e| switch (e) {
|
|
// The style set capacity needs to be increased.
|
|
error.OutOfMemory => return error.StyleSetOutOfMemory,
|
|
|
|
// The style set needs to be rehashed.
|
|
error.NeedsRehash => return error.StyleSetNeedsRehash,
|
|
} orelse src_cell.style_id;
|
|
}
|
|
if (src_cell.codepoint() == kitty.graphics.unicode.placeholder) {
|
|
dst_row.kitty_virtual_placeholder = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we are growing columns, then we need to ensure spacer heads
|
|
// are cleared.
|
|
if (self.size.cols > other.size.cols) {
|
|
const last = &cells[other.size.cols - 1];
|
|
if (last.wide == .spacer_head) {
|
|
last.wide = .narrow;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Get a single row. y must be valid.
|
|
pub fn getRow(self: *const Page, y: usize) *Row {
|
|
assert(y < self.size.rows);
|
|
return &self.rows.ptr(self.memory)[y];
|
|
}
|
|
|
|
/// Get the cells for a row.
|
|
pub fn getCells(self: *const Page, row: *Row) []Cell {
|
|
if (build_config.slow_runtime_safety) {
|
|
const rows = self.rows.ptr(self.memory);
|
|
const cells = self.cells.ptr(self.memory);
|
|
assert(@intFromPtr(row) >= @intFromPtr(rows));
|
|
assert(@intFromPtr(row) < @intFromPtr(cells));
|
|
}
|
|
|
|
const cells = row.cells.ptr(self.memory);
|
|
return cells[0..self.size.cols];
|
|
}
|
|
|
|
/// Get the row and cell for the given X/Y within this page.
|
|
pub fn getRowAndCell(self: *const Page, x: usize, y: usize) struct {
|
|
row: *Row,
|
|
cell: *Cell,
|
|
} {
|
|
assert(y < self.size.rows);
|
|
assert(x < self.size.cols);
|
|
|
|
const rows = self.rows.ptr(self.memory);
|
|
const row = &rows[y];
|
|
const cell = &row.cells.ptr(self.memory)[x];
|
|
|
|
return .{ .row = row, .cell = cell };
|
|
}
|
|
|
|
/// Move a cell from one location to another. This will replace the
|
|
/// previous contents with a blank cell. Because this is a move, this
|
|
/// doesn't allocate and can't fail.
|
|
pub fn moveCells(
|
|
self: *Page,
|
|
src_row: *Row,
|
|
src_left: usize,
|
|
dst_row: *Row,
|
|
dst_left: usize,
|
|
len: usize,
|
|
) void {
|
|
defer self.assertIntegrity();
|
|
|
|
const src_cells = src_row.cells.ptr(self.memory)[src_left .. src_left + len];
|
|
const dst_cells = dst_row.cells.ptr(self.memory)[dst_left .. dst_left + len];
|
|
|
|
// Clear our destination now matter what
|
|
self.clearCells(dst_row, dst_left, dst_left + len);
|
|
|
|
// If src has no managed memory, this is very fast.
|
|
if (!src_row.managedMemory()) {
|
|
fastmem.copy(Cell, dst_cells, src_cells);
|
|
} else {
|
|
// Source has graphemes or hyperlinks...
|
|
for (src_cells, dst_cells) |*src, *dst| {
|
|
dst.* = src.*;
|
|
if (src.hasGrapheme()) {
|
|
// Required for moveGrapheme assertions
|
|
dst.content_tag = .codepoint;
|
|
self.moveGrapheme(src, dst);
|
|
src.content_tag = .codepoint;
|
|
dst.content_tag = .codepoint_grapheme;
|
|
dst_row.grapheme = true;
|
|
}
|
|
if (src.hyperlink) {
|
|
dst.hyperlink = false;
|
|
self.moveHyperlink(src, dst);
|
|
dst.hyperlink = true;
|
|
dst_row.hyperlink = true;
|
|
}
|
|
if (src.codepoint() == kitty.graphics.unicode.placeholder) {
|
|
dst_row.kitty_virtual_placeholder = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The destination row has styles if any of the cells are styled
|
|
if (!dst_row.styled) dst_row.styled = styled: for (dst_cells) |c| {
|
|
if (c.style_id != style.default_id) break :styled true;
|
|
} else false;
|
|
|
|
// Clear our source row now that the copy is complete. We can NOT
|
|
// use clearCells here because clearCells will garbage collect our
|
|
// styles and graphames but we moved them above.
|
|
//
|
|
// Zero the cells as u64s since empirically this seems
|
|
// to be a bit faster than using @memset(src_cells, .{})
|
|
@memset(@as([]u64, @ptrCast(src_cells)), 0);
|
|
if (src_cells.len == self.size.cols) {
|
|
src_row.grapheme = false;
|
|
src_row.hyperlink = false;
|
|
src_row.styled = false;
|
|
src_row.kitty_virtual_placeholder = false;
|
|
}
|
|
}
|
|
|
|
/// Swap two cells within the same row as quickly as possible.
|
|
pub fn swapCells(
|
|
self: *Page,
|
|
src: *Cell,
|
|
dst: *Cell,
|
|
) void {
|
|
defer self.assertIntegrity();
|
|
|
|
// Graphemes are keyed by cell offset so we do have to move them.
|
|
// We do this first so that all our grapheme state is correct.
|
|
if (src.hasGrapheme() or dst.hasGrapheme()) {
|
|
if (src.hasGrapheme() and !dst.hasGrapheme()) {
|
|
self.moveGrapheme(src, dst);
|
|
} else if (!src.hasGrapheme() and dst.hasGrapheme()) {
|
|
self.moveGrapheme(dst, src);
|
|
} else {
|
|
// Both had graphemes, so we have to manually swap
|
|
const src_offset = getOffset(Cell, self.memory, src);
|
|
const dst_offset = getOffset(Cell, self.memory, dst);
|
|
var map = self.grapheme_map.map(self.memory);
|
|
const src_entry = map.getEntry(src_offset).?;
|
|
const dst_entry = map.getEntry(dst_offset).?;
|
|
const src_value = src_entry.value_ptr.*;
|
|
const dst_value = dst_entry.value_ptr.*;
|
|
src_entry.value_ptr.* = dst_value;
|
|
dst_entry.value_ptr.* = src_value;
|
|
}
|
|
}
|
|
|
|
// Hyperlinks are keyed by cell offset.
|
|
if (src.hyperlink or dst.hyperlink) {
|
|
if (src.hyperlink and !dst.hyperlink) {
|
|
self.moveHyperlink(src, dst);
|
|
} else if (!src.hyperlink and dst.hyperlink) {
|
|
self.moveHyperlink(dst, src);
|
|
} else {
|
|
// Both had hyperlinks, so we have to manually swap
|
|
const src_offset = getOffset(Cell, self.memory, src);
|
|
const dst_offset = getOffset(Cell, self.memory, dst);
|
|
var map = self.hyperlink_map.map(self.memory);
|
|
const src_entry = map.getEntry(src_offset).?;
|
|
const dst_entry = map.getEntry(dst_offset).?;
|
|
const src_value = src_entry.value_ptr.*;
|
|
const dst_value = dst_entry.value_ptr.*;
|
|
src_entry.value_ptr.* = dst_value;
|
|
dst_entry.value_ptr.* = src_value;
|
|
}
|
|
}
|
|
|
|
// Copy the metadata. Note that we do NOT have to worry about
|
|
// styles because styles are keyed by ID and we're preserving the
|
|
// exact ref count and row state here.
|
|
const old_dst = dst.*;
|
|
dst.* = src.*;
|
|
src.* = old_dst;
|
|
}
|
|
|
|
/// Clear the cells in the given row. This will reclaim memory used
|
|
/// by graphemes and styles. Note that if the style cleared is still
|
|
/// active, Page cannot know this and it will still be ref counted down.
|
|
/// The best solution for this is to artificially increment the ref count
|
|
/// prior to calling this function.
|
|
pub fn clearCells(
|
|
self: *Page,
|
|
row: *Row,
|
|
left: usize,
|
|
end: usize,
|
|
) void {
|
|
defer self.assertIntegrity();
|
|
|
|
const cells = row.cells.ptr(self.memory)[left..end];
|
|
|
|
if (row.grapheme) {
|
|
for (cells) |*cell| {
|
|
if (cell.hasGrapheme()) self.clearGrapheme(row, cell);
|
|
}
|
|
}
|
|
|
|
if (row.hyperlink) {
|
|
for (cells) |*cell| {
|
|
if (cell.hyperlink) self.clearHyperlink(row, cell);
|
|
}
|
|
}
|
|
|
|
if (row.styled) {
|
|
for (cells) |*cell| {
|
|
if (cell.style_id == style.default_id) continue;
|
|
|
|
self.styles.release(self.memory, cell.style_id);
|
|
}
|
|
|
|
if (cells.len == self.size.cols) row.styled = false;
|
|
}
|
|
|
|
if (row.kitty_virtual_placeholder and
|
|
cells.len == self.size.cols)
|
|
{
|
|
for (cells) |c| {
|
|
if (c.codepoint() == kitty.graphics.unicode.placeholder) {
|
|
break;
|
|
}
|
|
} else row.kitty_virtual_placeholder = false;
|
|
}
|
|
|
|
// Zero the cells as u64s since empirically this seems
|
|
// to be a bit faster than using @memset(cells, .{})
|
|
@memset(@as([]u64, @ptrCast(cells)), 0);
|
|
}
|
|
|
|
/// Returns the hyperlink ID for the given cell.
|
|
pub fn lookupHyperlink(self: *const Page, cell: *const Cell) ?hyperlink.Id {
|
|
const cell_offset = getOffset(Cell, self.memory, cell);
|
|
const map = self.hyperlink_map.map(self.memory);
|
|
return map.get(cell_offset);
|
|
}
|
|
|
|
/// Clear the hyperlink from the given cell.
|
|
pub fn clearHyperlink(self: *Page, row: *Row, cell: *Cell) void {
|
|
defer self.assertIntegrity();
|
|
|
|
// Get our ID
|
|
const cell_offset = getOffset(Cell, self.memory, cell);
|
|
var map = self.hyperlink_map.map(self.memory);
|
|
const entry = map.getEntry(cell_offset) orelse return;
|
|
|
|
// Release our usage of this, free memory, unset flag
|
|
self.hyperlink_set.release(self.memory, entry.value_ptr.*);
|
|
map.removeByPtr(entry.key_ptr);
|
|
cell.hyperlink = false;
|
|
|
|
// Mark that we no longer have hyperlinks, also search the row
|
|
// to make sure its state is correct.
|
|
const cells = row.cells.ptr(self.memory)[0..self.size.cols];
|
|
for (cells) |c| if (c.hyperlink) return;
|
|
row.hyperlink = false;
|
|
}
|
|
|
|
pub const InsertHyperlinkError = error{
|
|
/// string_alloc errors
|
|
StringsOutOfMemory,
|
|
|
|
/// hyperlink_set errors
|
|
SetOutOfMemory,
|
|
SetNeedsRehash,
|
|
};
|
|
|
|
/// Convert a hyperlink into a page entry, returning the ID.
|
|
///
|
|
/// This does not de-dupe any strings, so if the URI, explicit ID,
|
|
/// etc. is already in the strings table this will duplicate it.
|
|
///
|
|
/// To release the memory associated with the given hyperlink,
|
|
/// release the ID from the `hyperlink_set`. If the refcount reaches
|
|
/// zero and the slot is needed then the context will reap the
|
|
/// memory.
|
|
pub fn insertHyperlink(
|
|
self: *Page,
|
|
link: hyperlink.Hyperlink,
|
|
) InsertHyperlinkError!hyperlink.Id {
|
|
// Insert our URI into the page strings table.
|
|
const page_uri: Offset(u8).Slice = uri: {
|
|
const buf = self.string_alloc.alloc(
|
|
u8,
|
|
self.memory,
|
|
link.uri.len,
|
|
) catch |err| switch (err) {
|
|
error.OutOfMemory => return error.StringsOutOfMemory,
|
|
};
|
|
errdefer self.string_alloc.free(self.memory, buf);
|
|
@memcpy(buf, link.uri);
|
|
|
|
break :uri .{
|
|
.offset = size.getOffset(u8, self.memory, &buf[0]),
|
|
.len = link.uri.len,
|
|
};
|
|
};
|
|
errdefer self.string_alloc.free(
|
|
self.memory,
|
|
page_uri.offset.ptr(self.memory)[0..page_uri.len],
|
|
);
|
|
|
|
// Allocate an ID for our page memory if we have to.
|
|
const page_id: hyperlink.PageEntry.Id = switch (link.id) {
|
|
.explicit => |id| explicit: {
|
|
const buf = self.string_alloc.alloc(
|
|
u8,
|
|
self.memory,
|
|
id.len,
|
|
) catch |err| switch (err) {
|
|
error.OutOfMemory => return error.StringsOutOfMemory,
|
|
};
|
|
errdefer self.string_alloc.free(self.memory, buf);
|
|
@memcpy(buf, id);
|
|
|
|
break :explicit .{
|
|
.explicit = .{
|
|
.offset = size.getOffset(u8, self.memory, &buf[0]),
|
|
.len = id.len,
|
|
},
|
|
};
|
|
},
|
|
|
|
.implicit => |id| .{ .implicit = id },
|
|
};
|
|
errdefer switch (page_id) {
|
|
.implicit => {},
|
|
.explicit => |slice| self.string_alloc.free(
|
|
self.memory,
|
|
slice.offset.ptr(self.memory)[0..slice.len],
|
|
),
|
|
};
|
|
|
|
// Build our entry
|
|
const entry: hyperlink.PageEntry = .{
|
|
.id = page_id,
|
|
.uri = page_uri,
|
|
};
|
|
|
|
// Put our hyperlink into the hyperlink set to get an ID
|
|
const id = self.hyperlink_set.addContext(
|
|
self.memory,
|
|
entry,
|
|
.{ .page = self },
|
|
) catch |err| switch (err) {
|
|
error.OutOfMemory => return error.SetOutOfMemory,
|
|
error.NeedsRehash => return error.SetNeedsRehash,
|
|
};
|
|
errdefer self.hyperlink_set.release(self.memory, id);
|
|
|
|
return id;
|
|
}
|
|
|
|
/// Set the hyperlink for the given cell. If the cell already has a
|
|
/// hyperlink, then this will handle memory management and refcount
|
|
/// update for the prior hyperlink.
|
|
///
|
|
/// DOES NOT increment the reference count for the new hyperlink!
|
|
///
|
|
/// Caller is responsible for updating the refcount in the hyperlink
|
|
/// set as necessary by calling `use` if the id was not acquired with
|
|
/// `add`.
|
|
pub fn setHyperlink(self: *Page, row: *Row, cell: *Cell, id: hyperlink.Id) error{HyperlinkMapOutOfMemory}!void {
|
|
defer self.assertIntegrity();
|
|
|
|
const cell_offset = getOffset(Cell, self.memory, cell);
|
|
var map = self.hyperlink_map.map(self.memory);
|
|
const gop = map.getOrPut(cell_offset) catch |e| {
|
|
comptime assert(@TypeOf(e) == error{OutOfMemory});
|
|
// The hyperlink map capacity needs to be increased.
|
|
return error.HyperlinkMapOutOfMemory;
|
|
};
|
|
|
|
if (gop.found_existing) {
|
|
// Always release the old hyperlink, because even if it's actually
|
|
// the same as the one we're setting, we'd end up double-counting
|
|
// if we left the reference count be, because the caller does not
|
|
// know whether it's the same and will have increased the count
|
|
// outside of this function.
|
|
self.hyperlink_set.release(self.memory, gop.value_ptr.*);
|
|
|
|
// If the hyperlink matches then we don't need to do anything.
|
|
if (gop.value_ptr.* == id) {
|
|
// It is possible for cell hyperlink to be false but row
|
|
// must never be false. The cell hyperlink can be false because
|
|
// in Terminal.print we clear the hyperlink for the cursor cell
|
|
// before writing the cell again, so if someone prints over
|
|
// a cell with a matching hyperlink this state can happen.
|
|
// This is tested in Terminal.zig.
|
|
assert(row.hyperlink);
|
|
cell.hyperlink = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Set the hyperlink on the cell and in the map.
|
|
gop.value_ptr.* = id;
|
|
cell.hyperlink = true;
|
|
row.hyperlink = true;
|
|
}
|
|
|
|
/// Move the hyperlink from one cell to another. This can't fail
|
|
/// because we avoid any allocations since we're just moving data.
|
|
/// Destination must NOT have a hyperlink.
|
|
fn moveHyperlink(self: *Page, src: *Cell, dst: *Cell) void {
|
|
assert(src.hyperlink);
|
|
assert(!dst.hyperlink);
|
|
|
|
const src_offset = getOffset(Cell, self.memory, src);
|
|
const dst_offset = getOffset(Cell, self.memory, dst);
|
|
var map = self.hyperlink_map.map(self.memory);
|
|
const entry = map.getEntry(src_offset).?;
|
|
const value = entry.value_ptr.*;
|
|
map.removeByPtr(entry.key_ptr);
|
|
map.putAssumeCapacity(dst_offset, value);
|
|
|
|
// NOTE: We must not set src/dst.hyperlink here because this
|
|
// function is used in various cases where we swap cell contents
|
|
// and its unsafe. The flip side: the caller must be careful
|
|
// to set the proper cell state to represent the move.
|
|
}
|
|
|
|
/// Returns the number of hyperlinks in the page. This isn't the byte
|
|
/// size but the total number of unique cells that have hyperlink data.
|
|
pub fn hyperlinkCount(self: *const Page) usize {
|
|
return self.hyperlink_map.map(self.memory).count();
|
|
}
|
|
|
|
/// Returns the hyperlink capacity for the page. This isn't the byte
|
|
/// size but the number of unique cells that can have hyperlink data.
|
|
pub fn hyperlinkCapacity(self: *const Page) usize {
|
|
return self.hyperlink_map.map(self.memory).capacity();
|
|
}
|
|
|
|
/// Set the graphemes for the given cell. This asserts that the cell
|
|
/// has no graphemes set, and only contains a single codepoint.
|
|
pub fn setGraphemes(self: *Page, row: *Row, cell: *Cell, cps: []u21) GraphemeError!void {
|
|
defer self.assertIntegrity();
|
|
|
|
assert(cell.codepoint() > 0);
|
|
assert(cell.content_tag == .codepoint);
|
|
|
|
const cell_offset = getOffset(Cell, self.memory, cell);
|
|
var map = self.grapheme_map.map(self.memory);
|
|
|
|
const slice = self.grapheme_alloc.alloc(u21, self.memory, cps.len) catch |e| {
|
|
comptime assert(@TypeOf(e) == error{OutOfMemory});
|
|
// The grapheme alloc capacity needs to be increased.
|
|
return error.GraphemeAllocOutOfMemory;
|
|
};
|
|
errdefer self.grapheme_alloc.free(self.memory, slice);
|
|
@memcpy(slice, cps);
|
|
|
|
map.putNoClobber(cell_offset, .{
|
|
.offset = getOffset(u21, self.memory, @ptrCast(slice.ptr)),
|
|
.len = slice.len,
|
|
}) catch |e| {
|
|
comptime assert(@TypeOf(e) == error{OutOfMemory});
|
|
// The grapheme map capacity needs to be increased.
|
|
return error.GraphemeMapOutOfMemory;
|
|
};
|
|
errdefer map.remove(cell_offset);
|
|
|
|
cell.content_tag = .codepoint_grapheme;
|
|
row.grapheme = true;
|
|
|
|
return;
|
|
}
|
|
|
|
/// Append a codepoint to the given cell as a grapheme.
|
|
pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) Allocator.Error!void {
|
|
defer self.assertIntegrity();
|
|
|
|
if (build_config.slow_runtime_safety) assert(cell.codepoint() != 0);
|
|
|
|
const cell_offset = getOffset(Cell, self.memory, cell);
|
|
var map = self.grapheme_map.map(self.memory);
|
|
|
|
// If this cell has no graphemes, we can go faster by knowing we
|
|
// need to allocate a new grapheme slice and update the map.
|
|
if (cell.content_tag != .codepoint_grapheme) {
|
|
const cps = try self.grapheme_alloc.alloc(u21, self.memory, 1);
|
|
errdefer self.grapheme_alloc.free(self.memory, cps);
|
|
cps[0] = cp;
|
|
|
|
try map.putNoClobber(cell_offset, .{
|
|
.offset = getOffset(u21, self.memory, @ptrCast(cps.ptr)),
|
|
.len = 1,
|
|
});
|
|
errdefer map.remove(cell_offset);
|
|
|
|
cell.content_tag = .codepoint_grapheme;
|
|
row.grapheme = true;
|
|
|
|
return;
|
|
}
|
|
|
|
// The cell already has graphemes. We need to append to the existing
|
|
// grapheme slice and update the map.
|
|
assert(row.grapheme);
|
|
|
|
const slice = map.getPtr(cell_offset).?;
|
|
|
|
// If our slice len doesn't divide evenly by the grapheme chunk
|
|
// length then we can utilize the additional chunk space.
|
|
if (slice.len % grapheme_chunk_len != 0) {
|
|
const cps = slice.offset.ptr(self.memory);
|
|
cps[slice.len] = cp;
|
|
slice.len += 1;
|
|
return;
|
|
}
|
|
|
|
// We are out of chunk space. There is no fast path here. We need
|
|
// to allocate a larger chunk. This is a very slow path. We expect
|
|
// most graphemes to fit within our chunk size.
|
|
const cps = try self.grapheme_alloc.alloc(u21, self.memory, slice.len + 1);
|
|
errdefer self.grapheme_alloc.free(self.memory, cps);
|
|
const old_cps = slice.offset.ptr(self.memory)[0..slice.len];
|
|
fastmem.copy(u21, cps[0..old_cps.len], old_cps);
|
|
cps[slice.len] = cp;
|
|
slice.* = .{
|
|
.offset = getOffset(u21, self.memory, @ptrCast(cps.ptr)),
|
|
.len = slice.len + 1,
|
|
};
|
|
|
|
// Free our old chunk
|
|
self.grapheme_alloc.free(self.memory, old_cps);
|
|
}
|
|
|
|
/// Returns the codepoints for the given cell. These are the codepoints
|
|
/// in addition to the first codepoint. The first codepoint is NOT
|
|
/// included since it is on the cell itself.
|
|
pub fn lookupGrapheme(self: *const Page, cell: *const Cell) ?[]u21 {
|
|
const cell_offset = getOffset(Cell, self.memory, cell);
|
|
const map = self.grapheme_map.map(self.memory);
|
|
const slice = map.get(cell_offset) orelse return null;
|
|
return slice.offset.ptr(self.memory)[0..slice.len];
|
|
}
|
|
|
|
/// Move the graphemes from one cell to another. This can't fail
|
|
/// because we avoid any allocations since we're just moving data.
|
|
///
|
|
/// WARNING: This will NOT change the content_tag on the cells because
|
|
/// there are scenarios where we want to move graphemes without changing
|
|
/// the content tag. Callers beware but assertIntegrity should catch this.
|
|
fn moveGrapheme(self: *Page, src: *Cell, dst: *Cell) void {
|
|
if (build_config.slow_runtime_safety) {
|
|
assert(src.hasGrapheme());
|
|
assert(!dst.hasGrapheme());
|
|
}
|
|
|
|
const src_offset = getOffset(Cell, self.memory, src);
|
|
const dst_offset = getOffset(Cell, self.memory, dst);
|
|
var map = self.grapheme_map.map(self.memory);
|
|
const entry = map.getEntry(src_offset).?;
|
|
const value = entry.value_ptr.*;
|
|
map.removeByPtr(entry.key_ptr);
|
|
map.putAssumeCapacity(dst_offset, value);
|
|
}
|
|
|
|
/// Clear the graphemes for a given cell.
|
|
pub fn clearGrapheme(self: *Page, row: *Row, cell: *Cell) void {
|
|
defer self.assertIntegrity();
|
|
if (build_config.slow_runtime_safety) assert(cell.hasGrapheme());
|
|
|
|
// Get our entry in the map, which must exist
|
|
const cell_offset = getOffset(Cell, self.memory, cell);
|
|
var map = self.grapheme_map.map(self.memory);
|
|
const entry = map.getEntry(cell_offset).?;
|
|
|
|
// Free our grapheme data
|
|
const cps = entry.value_ptr.offset.ptr(self.memory)[0..entry.value_ptr.len];
|
|
self.grapheme_alloc.free(self.memory, cps);
|
|
|
|
// Remove the entry
|
|
map.removeByPtr(entry.key_ptr);
|
|
|
|
// Mark that we no longer have graphemes, also search the row
|
|
// to make sure its state is correct.
|
|
cell.content_tag = .codepoint;
|
|
const cells = row.cells.ptr(self.memory)[0..self.size.cols];
|
|
for (cells) |c| if (c.hasGrapheme()) return;
|
|
row.grapheme = false;
|
|
}
|
|
|
|
/// Returns the number of graphemes in the page. This isn't the byte
|
|
/// size but the total number of unique cells that have grapheme data.
|
|
pub fn graphemeCount(self: *const Page) usize {
|
|
return self.grapheme_map.map(self.memory).count();
|
|
}
|
|
|
|
/// Returns the grapheme capacity for the page. This isn't the byte
|
|
/// size but the number of unique cells that can have grapheme data.
|
|
pub fn graphemeCapacity(self: *const Page) usize {
|
|
return self.grapheme_map.map(self.memory).capacity();
|
|
}
|
|
|
|
/// Options for encoding the page as UTF-8.
|
|
pub const EncodeUtf8Options = struct {
|
|
/// The range of rows to encode. If end_y is null, then it will
|
|
/// encode to the end of the page.
|
|
start_y: size.CellCountInt = 0,
|
|
end_y: ?size.CellCountInt = null,
|
|
|
|
/// If true, this will unwrap soft-wrapped lines. If false, this will
|
|
/// dump the screen as it is visually seen in a rendered window.
|
|
unwrap: bool = true,
|
|
|
|
/// Preceding state from encoding the prior page. Used to preserve
|
|
/// blanks properly across multiple pages.
|
|
preceding: TrailingUtf8State = .{},
|
|
|
|
/// If non-null, this will be cleared and filled with the x/y
|
|
/// coordinates of each byte in the UTF-8 encoded output.
|
|
/// The index in the array is the byte offset in the output
|
|
/// where 0 is the cursor of the writer when the function is
|
|
/// called.
|
|
cell_map: ?*CellMap = null,
|
|
|
|
/// Trailing state for UTF-8 encoding.
|
|
pub const TrailingUtf8State = struct {
|
|
rows: usize = 0,
|
|
cells: usize = 0,
|
|
};
|
|
};
|
|
|
|
/// See cell_map
|
|
pub const CellMap = std.ArrayList(CellMapEntry);
|
|
|
|
/// The x/y coordinate of a single cell in the cell map.
|
|
pub const CellMapEntry = struct {
|
|
y: size.CellCountInt,
|
|
x: size.CellCountInt,
|
|
};
|
|
|
|
/// Encode the page contents as UTF-8.
|
|
///
|
|
/// If preceding is non-null, then it will be used to initialize our
|
|
/// blank rows/cells count so that we can accumulate blanks across
|
|
/// multiple pages.
|
|
///
|
|
/// Note: Many tests for this function are done via Screen.dumpString
|
|
/// tests since that function is a thin wrapper around this one and
|
|
/// it makes it easier to test input contents.
|
|
pub fn encodeUtf8(
|
|
self: *const Page,
|
|
writer: anytype,
|
|
opts: EncodeUtf8Options,
|
|
) anyerror!EncodeUtf8Options.TrailingUtf8State {
|
|
var blank_rows: usize = opts.preceding.rows;
|
|
var blank_cells: usize = opts.preceding.cells;
|
|
|
|
const start_y: size.CellCountInt = opts.start_y;
|
|
const end_y: size.CellCountInt = opts.end_y orelse self.size.rows;
|
|
|
|
// We can probably avoid this by doing the logic below in a different
|
|
// way. The reason this exists is so that when we end a non-blank
|
|
// line with a newline, we can correctly map the cell map over to
|
|
// the correct x value.
|
|
//
|
|
// For example "A\nB". The cell map for "\n" should be (1, 0).
|
|
// This is tested in Screen.zig so feel free to refactor this.
|
|
var last_x: size.CellCountInt = 0;
|
|
|
|
for (start_y..end_y) |y_usize| {
|
|
const y: size.CellCountInt = @intCast(y_usize);
|
|
const row: *Row = self.getRow(y);
|
|
const cells: []const Cell = self.getCells(row);
|
|
|
|
// If this row is blank, accumulate to avoid a bunch of extra
|
|
// work later. If it isn't blank, make sure we dump all our
|
|
// blanks.
|
|
if (!Cell.hasTextAny(cells)) {
|
|
blank_rows += 1;
|
|
continue;
|
|
}
|
|
for (1..blank_rows + 1) |i| {
|
|
try writer.writeByte('\n');
|
|
|
|
// This is tested in Screen.zig, i.e. one test is
|
|
// "cell map with newlines"
|
|
if (opts.cell_map) |cell_map| {
|
|
try cell_map.append(.{
|
|
.x = last_x,
|
|
.y = @intCast(y - blank_rows + i - 1),
|
|
});
|
|
last_x = 0;
|
|
}
|
|
}
|
|
blank_rows = 0;
|
|
|
|
// If we're not wrapped, we always add a newline so after
|
|
// the row is printed we can add a newline.
|
|
if (!row.wrap or !opts.unwrap) blank_rows += 1;
|
|
|
|
// If the row doesn't continue a wrap then we need to reset
|
|
// our blank cell count.
|
|
if (!row.wrap_continuation or !opts.unwrap) blank_cells = 0;
|
|
|
|
// Go through each cell and print it
|
|
for (cells, 0..) |*cell, x_usize| {
|
|
const x: size.CellCountInt = @intCast(x_usize);
|
|
|
|
// Skip spacers
|
|
switch (cell.wide) {
|
|
.narrow, .wide => {},
|
|
.spacer_head, .spacer_tail => continue,
|
|
}
|
|
|
|
// If we have a zero value, then we accumulate a counter. We
|
|
// only want to turn zero values into spaces if we have a non-zero
|
|
// char sometime later.
|
|
if (!cell.hasText()) {
|
|
blank_cells += 1;
|
|
continue;
|
|
}
|
|
if (blank_cells > 0) {
|
|
try writer.writeByteNTimes(' ', blank_cells);
|
|
if (opts.cell_map) |cell_map| {
|
|
for (0..blank_cells) |i| try cell_map.append(.{
|
|
.x = @intCast(x - blank_cells + i),
|
|
.y = y,
|
|
});
|
|
}
|
|
|
|
blank_cells = 0;
|
|
}
|
|
|
|
switch (cell.content_tag) {
|
|
.codepoint => {
|
|
try writer.print("{u}", .{cell.content.codepoint});
|
|
if (opts.cell_map) |cell_map| {
|
|
last_x = x + 1;
|
|
try cell_map.append(.{
|
|
.x = x,
|
|
.y = y,
|
|
});
|
|
}
|
|
},
|
|
|
|
.codepoint_grapheme => {
|
|
try writer.print("{u}", .{cell.content.codepoint});
|
|
if (opts.cell_map) |cell_map| {
|
|
last_x = x + 1;
|
|
try cell_map.append(.{
|
|
.x = x,
|
|
.y = y,
|
|
});
|
|
}
|
|
|
|
for (self.lookupGrapheme(cell).?) |cp| {
|
|
try writer.print("{u}", .{cp});
|
|
if (opts.cell_map) |cell_map| try cell_map.append(.{
|
|
.x = x,
|
|
.y = y,
|
|
});
|
|
}
|
|
},
|
|
|
|
// Unreachable since we do hasText() above
|
|
.bg_color_palette,
|
|
.bg_color_rgb,
|
|
=> unreachable,
|
|
}
|
|
}
|
|
}
|
|
|
|
return .{ .rows = blank_rows, .cells = blank_cells };
|
|
}
|
|
|
|
/// Returns the bitset for the dirty bits on this page.
|
|
///
|
|
/// The returned value is a DynamicBitSetUnmanaged but it is NOT
|
|
/// actually dynamic; do NOT call resize on this. It is safe to
|
|
/// read and write but do not resize it.
|
|
pub fn dirtyBitSet(self: *const Page) std.DynamicBitSetUnmanaged {
|
|
return .{
|
|
.bit_length = self.capacity.rows,
|
|
.masks = self.dirty.ptr(self.memory),
|
|
};
|
|
}
|
|
|
|
/// Returns true if the given row is dirty. This is NOT very
|
|
/// efficient if you're checking many rows and you should use
|
|
/// dirtyBitSet directly instead.
|
|
pub fn isRowDirty(self: *const Page, y: usize) bool {
|
|
return self.dirtyBitSet().isSet(y);
|
|
}
|
|
|
|
/// Returns true if this page is dirty at all. If you plan on
|
|
/// checking any additional rows, you should use dirtyBitSet and
|
|
/// check this on your own so you have the set available.
|
|
pub fn isDirty(self: *const Page) bool {
|
|
return self.dirtyBitSet().findFirstSet() != null;
|
|
}
|
|
|
|
pub const Layout = struct {
|
|
total_size: usize,
|
|
rows_start: usize,
|
|
rows_size: usize,
|
|
cells_start: usize,
|
|
cells_size: usize,
|
|
dirty_start: usize,
|
|
dirty_size: usize,
|
|
styles_start: usize,
|
|
styles_layout: style.Set.Layout,
|
|
grapheme_alloc_start: usize,
|
|
grapheme_alloc_layout: GraphemeAlloc.Layout,
|
|
grapheme_map_start: usize,
|
|
grapheme_map_layout: GraphemeMap.Layout,
|
|
string_alloc_start: usize,
|
|
string_alloc_layout: StringAlloc.Layout,
|
|
hyperlink_map_start: usize,
|
|
hyperlink_map_layout: hyperlink.Map.Layout,
|
|
hyperlink_set_start: usize,
|
|
hyperlink_set_layout: hyperlink.Set.Layout,
|
|
capacity: Capacity,
|
|
};
|
|
|
|
/// The memory layout for a page given a desired minimum cols
|
|
/// and rows size.
|
|
pub fn layout(cap: Capacity) Layout {
|
|
const rows_count: usize = @intCast(cap.rows);
|
|
const rows_start = 0;
|
|
const rows_end: usize = rows_start + (rows_count * @sizeOf(Row));
|
|
|
|
const cells_count: usize = @intCast(cap.cols * cap.rows);
|
|
const cells_start = alignForward(usize, rows_end, @alignOf(Cell));
|
|
const cells_end = cells_start + (cells_count * @sizeOf(Cell));
|
|
|
|
// The division below cannot fail because our row count cannot
|
|
// exceed the maximum value of usize.
|
|
const dirty_bit_length: usize = rows_count;
|
|
const dirty_usize_length: usize = std.math.divCeil(
|
|
usize,
|
|
dirty_bit_length,
|
|
@bitSizeOf(usize),
|
|
) catch unreachable;
|
|
const dirty_start = alignForward(usize, cells_end, @alignOf(usize));
|
|
const dirty_end: usize = dirty_start + (dirty_usize_length * @sizeOf(usize));
|
|
|
|
const styles_layout = style.Set.layout(cap.styles);
|
|
const styles_start = alignForward(usize, dirty_end, style.Set.base_align);
|
|
const styles_end = styles_start + styles_layout.total_size;
|
|
|
|
const grapheme_alloc_layout = GraphemeAlloc.layout(cap.grapheme_bytes);
|
|
const grapheme_alloc_start = alignForward(usize, styles_end, GraphemeAlloc.base_align);
|
|
const grapheme_alloc_end = grapheme_alloc_start + grapheme_alloc_layout.total_size;
|
|
|
|
const grapheme_count = @divFloor(cap.grapheme_bytes, grapheme_chunk);
|
|
const grapheme_map_layout = GraphemeMap.layout(@intCast(grapheme_count));
|
|
const grapheme_map_start = alignForward(usize, grapheme_alloc_end, GraphemeMap.base_align);
|
|
const grapheme_map_end = grapheme_map_start + grapheme_map_layout.total_size;
|
|
|
|
const string_layout = StringAlloc.layout(cap.string_bytes);
|
|
const string_start = alignForward(usize, grapheme_map_end, StringAlloc.base_align);
|
|
const string_end = string_start + string_layout.total_size;
|
|
|
|
const hyperlink_count = @divFloor(cap.hyperlink_bytes, @sizeOf(hyperlink.Set.Item));
|
|
const hyperlink_set_layout = hyperlink.Set.layout(@intCast(hyperlink_count));
|
|
const hyperlink_set_start = alignForward(usize, string_end, hyperlink.Set.base_align);
|
|
const hyperlink_set_end = hyperlink_set_start + hyperlink_set_layout.total_size;
|
|
|
|
const hyperlink_map_count: u32 = count: {
|
|
if (hyperlink_count == 0) break :count 0;
|
|
const mult = std.math.cast(
|
|
u32,
|
|
hyperlink_count * hyperlink_cell_multiplier,
|
|
) orelse break :count std.math.maxInt(u32);
|
|
break :count std.math.ceilPowerOfTwoAssert(u32, mult);
|
|
};
|
|
const hyperlink_map_layout = hyperlink.Map.layout(hyperlink_map_count);
|
|
const hyperlink_map_start = alignForward(usize, hyperlink_set_end, hyperlink.Map.base_align);
|
|
const hyperlink_map_end = hyperlink_map_start + hyperlink_map_layout.total_size;
|
|
|
|
const total_size = alignForward(usize, hyperlink_map_end, std.mem.page_size);
|
|
|
|
return .{
|
|
.total_size = total_size,
|
|
.rows_start = rows_start,
|
|
.rows_size = rows_end - rows_start,
|
|
.cells_start = cells_start,
|
|
.cells_size = cells_end - cells_start,
|
|
.dirty_start = dirty_start,
|
|
.dirty_size = dirty_end - dirty_start,
|
|
.styles_start = styles_start,
|
|
.styles_layout = styles_layout,
|
|
.grapheme_alloc_start = grapheme_alloc_start,
|
|
.grapheme_alloc_layout = grapheme_alloc_layout,
|
|
.grapheme_map_start = grapheme_map_start,
|
|
.grapheme_map_layout = grapheme_map_layout,
|
|
.string_alloc_start = string_start,
|
|
.string_alloc_layout = string_layout,
|
|
.hyperlink_map_start = hyperlink_map_start,
|
|
.hyperlink_map_layout = hyperlink_map_layout,
|
|
.hyperlink_set_start = hyperlink_set_start,
|
|
.hyperlink_set_layout = hyperlink_set_layout,
|
|
.capacity = cap,
|
|
};
|
|
}
|
|
};
|
|
|
|
/// The standard capacity for a page that doesn't have special
|
|
/// requirements. This is enough to support a very large number of cells.
|
|
/// The standard capacity is chosen as the fast-path for allocation since
|
|
/// pages of standard capacity use a pooled allocator instead of single-use
|
|
/// mmaps.
|
|
pub const std_capacity: Capacity = .{
|
|
.cols = 215,
|
|
.rows = 215,
|
|
.styles = 128,
|
|
.grapheme_bytes = if (builtin.is_test) 512 else 8192,
|
|
};
|
|
|
|
/// The size of this page.
|
|
pub const Size = struct {
|
|
cols: size.CellCountInt,
|
|
rows: size.CellCountInt,
|
|
};
|
|
|
|
/// Capacity of this page.
|
|
pub const Capacity = struct {
|
|
/// Number of columns and rows we can know about.
|
|
cols: size.CellCountInt,
|
|
rows: size.CellCountInt,
|
|
|
|
/// Number of unique styles that can be used on this page.
|
|
styles: usize = 16,
|
|
|
|
/// Number of bytes to allocate for hyperlink data. Note that the
|
|
/// amount of data used for hyperlinks in total is more than this because
|
|
/// hyperlinks use string data as well as a small amount of lookup metadata.
|
|
/// This number is a rough approximation.
|
|
hyperlink_bytes: usize = hyperlink_bytes_default,
|
|
|
|
/// Number of bytes to allocate for grapheme data.
|
|
grapheme_bytes: usize = grapheme_bytes_default,
|
|
|
|
/// Number of bytes to allocate for strings.
|
|
string_bytes: usize = string_bytes_default,
|
|
|
|
pub const Adjustment = struct {
|
|
cols: ?size.CellCountInt = null,
|
|
};
|
|
|
|
/// Adjust the capacity parameters while retaining the same total size.
|
|
/// Adjustments always happen by limiting the rows in the page. Everything
|
|
/// else can grow. If it is impossible to achieve the desired adjustment,
|
|
/// OutOfMemory is returned.
|
|
pub fn adjust(self: Capacity, req: Adjustment) Allocator.Error!Capacity {
|
|
var adjusted = self;
|
|
if (req.cols) |cols| {
|
|
// The math below only works if there is no alignment gap between
|
|
// the end of the rows array and the start of the cells array.
|
|
//
|
|
// To guarantee this, we assert that Row's size is a multiple of
|
|
// Cell's alignment, so that any length array of Rows will end on
|
|
// a valid alignment for the start of the Cell array.
|
|
assert(@sizeOf(Row) % @alignOf(Cell) == 0);
|
|
|
|
const layout = Page.layout(self);
|
|
|
|
// In order to determine the amount of space in the page available
|
|
// for rows & cells (which will allow us to calculate the number of
|
|
// rows we can fit at a certain column width) we need to layout the
|
|
// "meta" members of the page (i.e. everything else) from the end.
|
|
const hyperlink_map_start = alignBackward(usize, layout.total_size - layout.hyperlink_map_layout.total_size, hyperlink.Map.base_align);
|
|
const hyperlink_set_start = alignBackward(usize, hyperlink_map_start - layout.hyperlink_set_layout.total_size, hyperlink.Set.base_align);
|
|
const string_alloc_start = alignBackward(usize, hyperlink_set_start - layout.string_alloc_layout.total_size, StringAlloc.base_align);
|
|
const grapheme_map_start = alignBackward(usize, string_alloc_start - layout.grapheme_map_layout.total_size, GraphemeMap.base_align);
|
|
const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - layout.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align);
|
|
const styles_start = alignBackward(usize, grapheme_alloc_start - layout.styles_layout.total_size, style.Set.base_align);
|
|
|
|
// The size per row is:
|
|
// - The row metadata itself
|
|
// - The cells per row (n=cols)
|
|
// - 1 bit for dirty tracking
|
|
const bits_per_row: usize = size: {
|
|
var bits: usize = @bitSizeOf(Row); // Row metadata
|
|
bits += @bitSizeOf(Cell) * @as(usize, @intCast(cols)); // Cells (n=cols)
|
|
bits += 1; // The dirty bit
|
|
break :size bits;
|
|
};
|
|
const available_bits: usize = styles_start * 8;
|
|
const new_rows: usize = @divFloor(available_bits, bits_per_row);
|
|
|
|
// If our rows go to zero then we can't fit any row metadata
|
|
// for the desired number of columns.
|
|
if (new_rows == 0) return error.OutOfMemory;
|
|
|
|
adjusted.cols = cols;
|
|
adjusted.rows = @intCast(new_rows);
|
|
}
|
|
|
|
return adjusted;
|
|
}
|
|
};
|
|
|
|
pub const Row = packed struct(u64) {
|
|
/// The cells in the row offset from the page.
|
|
cells: Offset(Cell),
|
|
|
|
/// True if this row is soft-wrapped. The first cell of the next
|
|
/// row is a continuation of this row.
|
|
wrap: bool = false,
|
|
|
|
/// True if the previous row to this one is soft-wrapped and
|
|
/// this row is a continuation of that row.
|
|
wrap_continuation: bool = false,
|
|
|
|
/// True if any of the cells in this row have multi-codepoint
|
|
/// grapheme clusters. If this is true, some fast paths are not
|
|
/// possible because erasing for example may need to clear existing
|
|
/// grapheme data.
|
|
grapheme: bool = false,
|
|
|
|
/// True if any of the cells in this row have a ref-counted style.
|
|
/// This can have false positives but never a false negative. Meaning:
|
|
/// this will be set to true the first time a style is used, but it
|
|
/// will not be set to false if the style is no longer used, because
|
|
/// checking for that condition is too expensive.
|
|
///
|
|
/// Why have this weird false positive flag at all? This makes VT operations
|
|
/// that erase cells (such as insert lines, delete lines, erase chars,
|
|
/// etc.) MUCH MUCH faster in the case that the row was never styled.
|
|
/// At the time of writing this, the speed difference is around 4x.
|
|
styled: bool = false,
|
|
|
|
/// True if any of the cells in this row are part of a hyperlink.
|
|
/// This is similar to styled: it can have false positives but never
|
|
/// false negatives. This is used to optimize hyperlink operations.
|
|
hyperlink: bool = false,
|
|
|
|
/// The semantic prompt type for this row as specified by the
|
|
/// running program, or "unknown" if it was never set.
|
|
semantic_prompt: SemanticPrompt = .unknown,
|
|
|
|
/// True if this row contains a virtual placeholder for the Kitty
|
|
/// graphics protocol. (U+10EEEE)
|
|
kitty_virtual_placeholder: bool = false,
|
|
|
|
_padding: u23 = 0,
|
|
|
|
/// Semantic prompt type.
|
|
pub const SemanticPrompt = enum(u3) {
|
|
/// Unknown, the running application didn't tell us for this line.
|
|
unknown = 0,
|
|
|
|
/// This is a prompt line, meaning it only contains the shell prompt.
|
|
/// For poorly behaving shells, this may also be the input.
|
|
prompt = 1,
|
|
prompt_continuation = 2,
|
|
|
|
/// This line contains the input area. We don't currently track
|
|
/// where this actually is in the line, so we just assume it is somewhere.
|
|
input = 3,
|
|
|
|
/// This line is the start of command output.
|
|
command = 4,
|
|
|
|
/// True if this is a prompt or input line.
|
|
pub fn promptOrInput(self: SemanticPrompt) bool {
|
|
return self == .prompt or self == .prompt_continuation or self == .input;
|
|
}
|
|
};
|
|
|
|
/// Returns true if this row has any managed memory outside of the
|
|
/// row structure (graphemes, styles, etc.)
|
|
fn managedMemory(self: Row) bool {
|
|
return self.grapheme or self.styled or self.hyperlink;
|
|
}
|
|
};
|
|
|
|
/// A cell represents a single terminal grid cell.
|
|
///
|
|
/// The zero value of this struct must be a valid cell representing empty,
|
|
/// since we zero initialize the backing memory for a page.
|
|
pub const Cell = packed struct(u64) {
|
|
/// The content tag dictates the active tag in content and possibly
|
|
/// some other behaviors.
|
|
content_tag: ContentTag = .codepoint,
|
|
|
|
/// The content of the cell. This is a union based on content_tag.
|
|
content: packed union {
|
|
/// The codepoint that this cell contains. If `grapheme` is false,
|
|
/// then this is the only codepoint in the cell. If `grapheme` is
|
|
/// true, then this is the first codepoint in the grapheme cluster.
|
|
codepoint: u21,
|
|
|
|
/// The content is an empty cell with a background color.
|
|
color_palette: u8,
|
|
color_rgb: RGB,
|
|
} = .{ .codepoint = 0 },
|
|
|
|
/// The style ID to use for this cell within the style map. Zero
|
|
/// is always the default style so no lookup is required.
|
|
style_id: style.Id = 0,
|
|
|
|
/// The wide property of this cell, for wide characters. Characters in
|
|
/// a terminal grid can only be 1 or 2 cells wide. A wide character
|
|
/// is always next to a spacer. This is used to determine both the width
|
|
/// and spacer properties of a cell.
|
|
wide: Wide = .narrow,
|
|
|
|
/// Whether this was written with the protection flag set.
|
|
protected: bool = false,
|
|
|
|
/// Whether this cell is a hyperlink. If this is true then you must
|
|
/// look up the hyperlink ID in the page hyperlink_map and the ID in
|
|
/// the hyperlink_set to get the actual hyperlink data.
|
|
hyperlink: bool = false,
|
|
|
|
_padding: u18 = 0,
|
|
|
|
pub const ContentTag = enum(u2) {
|
|
/// A single codepoint, could be zero to be empty cell.
|
|
codepoint = 0,
|
|
|
|
/// A codepoint that is part of a multi-codepoint grapheme cluster.
|
|
/// The codepoint tag is active in content, but also expect more
|
|
/// codepoints in the grapheme data.
|
|
codepoint_grapheme = 1,
|
|
|
|
/// The cell has no text but only a background color. This is an
|
|
/// optimization so that cells with only backgrounds don't take up
|
|
/// style map space and also don't require a style map lookup.
|
|
bg_color_palette = 2,
|
|
bg_color_rgb = 3,
|
|
};
|
|
|
|
pub const RGB = packed struct {
|
|
r: u8,
|
|
g: u8,
|
|
b: u8,
|
|
};
|
|
|
|
pub const Wide = enum(u2) {
|
|
/// Not a wide character, cell width 1.
|
|
narrow = 0,
|
|
|
|
/// Wide character, cell width 2.
|
|
wide = 1,
|
|
|
|
/// Spacer after wide character. Do not render.
|
|
spacer_tail = 2,
|
|
|
|
/// Spacer at the end of a soft-wrapped line to indicate that a wide
|
|
/// character is continued on the next line.
|
|
spacer_head = 3,
|
|
};
|
|
|
|
/// Helper to make a cell that just has a codepoint.
|
|
pub fn init(cp: u21) Cell {
|
|
return .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = cp },
|
|
};
|
|
}
|
|
|
|
pub fn isZero(self: Cell) bool {
|
|
return @as(u64, @bitCast(self)) == 0;
|
|
}
|
|
|
|
/// Returns true if this cell represents a cell with text to render.
|
|
///
|
|
/// Cases this returns false:
|
|
/// - Cell text is blank
|
|
/// - Cell is styled but only with a background color and no text
|
|
/// - Cell has a unicode placeholder for Kitty graphics protocol
|
|
pub fn hasText(self: Cell) bool {
|
|
return switch (self.content_tag) {
|
|
.codepoint,
|
|
.codepoint_grapheme,
|
|
=> self.content.codepoint != 0,
|
|
|
|
.bg_color_palette,
|
|
.bg_color_rgb,
|
|
=> false,
|
|
};
|
|
}
|
|
|
|
pub fn codepoint(self: Cell) u21 {
|
|
return switch (self.content_tag) {
|
|
.codepoint,
|
|
.codepoint_grapheme,
|
|
=> self.content.codepoint,
|
|
|
|
.bg_color_palette,
|
|
.bg_color_rgb,
|
|
=> 0,
|
|
};
|
|
}
|
|
|
|
/// The width in grid cells that this cell takes up.
|
|
pub fn gridWidth(self: Cell) u2 {
|
|
return switch (self.wide) {
|
|
.narrow, .spacer_head, .spacer_tail => 1,
|
|
.wide => 2,
|
|
};
|
|
}
|
|
|
|
pub fn hasStyling(self: Cell) bool {
|
|
return self.style_id != style.default_id;
|
|
}
|
|
|
|
/// Returns true if the cell has no text or styling.
|
|
pub fn isEmpty(self: Cell) bool {
|
|
return switch (self.content_tag) {
|
|
// Textual cells are empty if they have no text and are narrow.
|
|
// The "narrow" requirement is because wide spacers are meaningful.
|
|
.codepoint,
|
|
.codepoint_grapheme,
|
|
=> !self.hasText() and self.wide == .narrow,
|
|
|
|
.bg_color_palette,
|
|
.bg_color_rgb,
|
|
=> false,
|
|
};
|
|
}
|
|
|
|
pub fn hasGrapheme(self: Cell) bool {
|
|
return self.content_tag == .codepoint_grapheme;
|
|
}
|
|
|
|
/// Returns true if the set of cells has text in it.
|
|
pub fn hasTextAny(cells: []const Cell) bool {
|
|
for (cells) |cell| {
|
|
if (cell.hasText()) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Uncomment this when you want to do some math.
|
|
// test "Page size calculator" {
|
|
// const total_size = alignForward(
|
|
// usize,
|
|
// Page.layout(.{
|
|
// .cols = 250,
|
|
// .rows = 250,
|
|
// .styles = 128,
|
|
// .grapheme_bytes = 1024,
|
|
// }).total_size,
|
|
// std.mem.page_size,
|
|
// );
|
|
//
|
|
// std.log.warn("total_size={} pages={}", .{
|
|
// total_size,
|
|
// total_size / std.mem.page_size,
|
|
// });
|
|
// }
|
|
//
|
|
// test "Page std size" {
|
|
// // We want to ensure that the standard capacity is what we
|
|
// // expect it to be. Changing this is fine but should be done with care
|
|
// // so we fail a test if it changes.
|
|
// const total_size = Page.layout(std_capacity).total_size;
|
|
// try testing.expectEqual(@as(usize, 524_288), total_size); // 512 KiB
|
|
// //const pages = total_size / std.mem.page_size;
|
|
// }
|
|
|
|
test "Cell is zero by default" {
|
|
const cell = Cell.init(0);
|
|
const cell_int: u64 = @bitCast(cell);
|
|
try std.testing.expectEqual(@as(u64, 0), cell_int);
|
|
}
|
|
|
|
test "Page capacity adjust cols down" {
|
|
const original = std_capacity;
|
|
const original_size = Page.layout(original).total_size;
|
|
const adjusted = try original.adjust(.{ .cols = original.cols / 2 });
|
|
const adjusted_size = Page.layout(adjusted).total_size;
|
|
try testing.expectEqual(original_size, adjusted_size);
|
|
// If we layout a page with 1 more row and it's still the same size
|
|
// then adjust is not producing enough rows.
|
|
var bigger = adjusted;
|
|
bigger.rows += 1;
|
|
const bigger_size = Page.layout(bigger).total_size;
|
|
try testing.expect(bigger_size > original_size);
|
|
}
|
|
|
|
test "Page capacity adjust cols down to 1" {
|
|
const original = std_capacity;
|
|
const original_size = Page.layout(original).total_size;
|
|
const adjusted = try original.adjust(.{ .cols = 1 });
|
|
const adjusted_size = Page.layout(adjusted).total_size;
|
|
try testing.expectEqual(original_size, adjusted_size);
|
|
// If we layout a page with 1 more row and it's still the same size
|
|
// then adjust is not producing enough rows.
|
|
var bigger = adjusted;
|
|
bigger.rows += 1;
|
|
const bigger_size = Page.layout(bigger).total_size;
|
|
try testing.expect(bigger_size > original_size);
|
|
}
|
|
|
|
test "Page capacity adjust cols up" {
|
|
const original = std_capacity;
|
|
const original_size = Page.layout(original).total_size;
|
|
const adjusted = try original.adjust(.{ .cols = original.cols * 2 });
|
|
const adjusted_size = Page.layout(adjusted).total_size;
|
|
try testing.expectEqual(original_size, adjusted_size);
|
|
// If we layout a page with 1 more row and it's still the same size
|
|
// then adjust is not producing enough rows.
|
|
var bigger = adjusted;
|
|
bigger.rows += 1;
|
|
const bigger_size = Page.layout(bigger).total_size;
|
|
try testing.expect(bigger_size > original_size);
|
|
}
|
|
|
|
test "Page capacity adjust cols sweep" {
|
|
var cap = std_capacity;
|
|
const original_cols = cap.cols;
|
|
const original_size = Page.layout(cap).total_size;
|
|
for (1..original_cols * 2) |c| {
|
|
cap = try cap.adjust(.{ .cols = @as(u16, @intCast(c)) });
|
|
const adjusted_size = Page.layout(cap).total_size;
|
|
try testing.expectEqual(original_size, adjusted_size);
|
|
// If we layout a page with 1 more row and it's still the same size
|
|
// then adjust is not producing enough rows.
|
|
var bigger = cap;
|
|
bigger.rows += 1;
|
|
const bigger_size = Page.layout(bigger).total_size;
|
|
try testing.expect(bigger_size > original_size);
|
|
}
|
|
}
|
|
|
|
test "Page capacity adjust cols too high" {
|
|
const original = std_capacity;
|
|
try testing.expectError(
|
|
error.OutOfMemory,
|
|
original.adjust(.{ .cols = std.math.maxInt(size.CellCountInt) }),
|
|
);
|
|
}
|
|
|
|
test "Page init" {
|
|
var page = try Page.init(.{
|
|
.cols = 120,
|
|
.rows = 80,
|
|
.styles = 32,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Dirty set should be empty
|
|
const dirty = page.dirtyBitSet();
|
|
try std.testing.expectEqual(@as(usize, 0), dirty.count());
|
|
}
|
|
|
|
test "Page read and write cells" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(y) },
|
|
};
|
|
}
|
|
|
|
// Read it again
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
|
|
}
|
|
}
|
|
|
|
test "Page appendGrapheme small" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
const rac = page.getRowAndCell(0, 0);
|
|
rac.cell.* = Cell.init(0x09);
|
|
|
|
// One
|
|
try page.appendGrapheme(rac.row, rac.cell, 0x0A);
|
|
try testing.expect(rac.row.grapheme);
|
|
try testing.expect(rac.cell.hasGrapheme());
|
|
try testing.expectEqualSlices(u21, &.{0x0A}, page.lookupGrapheme(rac.cell).?);
|
|
|
|
// Two
|
|
try page.appendGrapheme(rac.row, rac.cell, 0x0B);
|
|
try testing.expect(rac.row.grapheme);
|
|
try testing.expect(rac.cell.hasGrapheme());
|
|
try testing.expectEqualSlices(u21, &.{ 0x0A, 0x0B }, page.lookupGrapheme(rac.cell).?);
|
|
|
|
// Clear it
|
|
page.clearGrapheme(rac.row, rac.cell);
|
|
try testing.expect(!rac.row.grapheme);
|
|
try testing.expect(!rac.cell.hasGrapheme());
|
|
}
|
|
|
|
test "Page appendGrapheme larger than chunk" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
const rac = page.getRowAndCell(0, 0);
|
|
rac.cell.* = Cell.init(0x09);
|
|
|
|
const count = grapheme_chunk_len * 10;
|
|
for (0..count) |i| {
|
|
try page.appendGrapheme(rac.row, rac.cell, @intCast(0x0A + i));
|
|
}
|
|
|
|
const cps = page.lookupGrapheme(rac.cell).?;
|
|
try testing.expectEqual(@as(usize, count), cps.len);
|
|
for (0..count) |i| {
|
|
try testing.expectEqual(@as(u21, @intCast(0x0A + i)), cps[i]);
|
|
}
|
|
}
|
|
|
|
test "Page clearGrapheme not all cells" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
const rac = page.getRowAndCell(0, 0);
|
|
rac.cell.* = Cell.init(0x09);
|
|
try page.appendGrapheme(rac.row, rac.cell, 0x0A);
|
|
|
|
const rac2 = page.getRowAndCell(1, 0);
|
|
rac2.cell.* = Cell.init(0x09);
|
|
try page.appendGrapheme(rac2.row, rac2.cell, 0x0A);
|
|
|
|
// Clear it
|
|
page.clearGrapheme(rac.row, rac.cell);
|
|
try testing.expect(rac.row.grapheme);
|
|
try testing.expect(!rac.cell.hasGrapheme());
|
|
try testing.expect(rac2.cell.hasGrapheme());
|
|
}
|
|
|
|
test "Page clone" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Write
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(y) },
|
|
};
|
|
}
|
|
|
|
// Clone
|
|
var page2 = try page.clone();
|
|
defer page2.deinit();
|
|
try testing.expectEqual(page2.capacity, page.capacity);
|
|
|
|
// Read it again
|
|
for (0..page2.capacity.rows) |y| {
|
|
const rac = page2.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
|
|
}
|
|
|
|
// Write again
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = 0 },
|
|
};
|
|
}
|
|
|
|
// Read it again, should be unchanged
|
|
for (0..page2.capacity.rows) |y| {
|
|
const rac = page2.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
|
|
}
|
|
|
|
// Read the original
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint);
|
|
}
|
|
}
|
|
|
|
test "Page cloneFrom" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Write
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(y) },
|
|
};
|
|
}
|
|
|
|
// Clone
|
|
var page2 = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page2.deinit();
|
|
try page2.cloneFrom(&page, 0, page.size.rows);
|
|
|
|
// Read it again
|
|
for (0..page2.capacity.rows) |y| {
|
|
const rac = page2.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
|
|
}
|
|
|
|
// Write again
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = 0 },
|
|
};
|
|
}
|
|
|
|
// Read it again, should be unchanged
|
|
for (0..page2.capacity.rows) |y| {
|
|
const rac = page2.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
|
|
}
|
|
|
|
// Read the original
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint);
|
|
}
|
|
}
|
|
|
|
test "Page cloneFrom shrink columns" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Write
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(y) },
|
|
};
|
|
}
|
|
|
|
// Clone
|
|
var page2 = try Page.init(.{
|
|
.cols = 5,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page2.deinit();
|
|
try page2.cloneFrom(&page, 0, page.size.rows);
|
|
try testing.expectEqual(@as(size.CellCountInt, 5), page2.size.cols);
|
|
|
|
// Read it again
|
|
for (0..page2.capacity.rows) |y| {
|
|
const rac = page2.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
|
|
}
|
|
}
|
|
|
|
test "Page cloneFrom partial" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Write
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(y) },
|
|
};
|
|
}
|
|
|
|
// Clone
|
|
var page2 = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page2.deinit();
|
|
try page2.cloneFrom(&page, 0, 5);
|
|
|
|
// Read it again
|
|
for (0..5) |y| {
|
|
const rac = page2.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint);
|
|
}
|
|
for (5..page2.size.rows) |y| {
|
|
const rac = page2.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint);
|
|
}
|
|
}
|
|
|
|
test "Page cloneFrom hyperlinks exact capacity" {
|
|
var page = try Page.init(.{
|
|
.cols = 50,
|
|
.rows = 50,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Ensure our page can accommodate the capacity.
|
|
const hyperlink_cap = page.hyperlinkCapacity();
|
|
try testing.expect(hyperlink_cap <= page.size.cols * page.size.rows);
|
|
|
|
// Create a hyperlink.
|
|
const hyperlink_id = try page.insertHyperlink(.{
|
|
.id = .{ .implicit = 0 },
|
|
.uri = "https://example.com",
|
|
});
|
|
|
|
// Fill the exact cap with cells.
|
|
fill: for (0..page.size.cols) |x| {
|
|
for (0..page.size.rows) |y| {
|
|
const rac = page.getRowAndCell(x, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = 42 },
|
|
};
|
|
try page.setHyperlink(rac.row, rac.cell, hyperlink_id);
|
|
page.hyperlink_set.use(page.memory, hyperlink_id);
|
|
|
|
if (page.hyperlinkCount() == hyperlink_cap) {
|
|
break :fill;
|
|
}
|
|
}
|
|
}
|
|
try testing.expectEqual(page.hyperlinkCount(), page.hyperlinkCapacity());
|
|
|
|
// Clone the full page
|
|
var page2 = try Page.init(page.capacity);
|
|
defer page2.deinit();
|
|
try page2.cloneFrom(&page, 0, page.size.rows);
|
|
|
|
// We should have the same number of hyperlinks
|
|
try testing.expectEqual(page2.hyperlinkCount(), page.hyperlinkCount());
|
|
}
|
|
|
|
test "Page cloneFrom graphemes" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Write
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(y + 1) },
|
|
};
|
|
try page.appendGrapheme(rac.row, rac.cell, 0x0A);
|
|
}
|
|
|
|
// Clone
|
|
var page2 = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page2.deinit();
|
|
try page2.cloneFrom(&page, 0, page.size.rows);
|
|
|
|
// Read it again
|
|
for (0..page2.capacity.rows) |y| {
|
|
const rac = page2.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, @intCast(y + 1)), rac.cell.content.codepoint);
|
|
try testing.expect(rac.row.grapheme);
|
|
try testing.expect(rac.cell.hasGrapheme());
|
|
try testing.expectEqualSlices(u21, &.{0x0A}, page2.lookupGrapheme(rac.cell).?);
|
|
}
|
|
|
|
// Write again
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
page.clearGrapheme(rac.row, rac.cell);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = 0 },
|
|
};
|
|
}
|
|
|
|
// Read it again, should be unchanged
|
|
for (0..page2.capacity.rows) |y| {
|
|
const rac = page2.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, @intCast(y + 1)), rac.cell.content.codepoint);
|
|
try testing.expect(rac.row.grapheme);
|
|
try testing.expect(rac.cell.hasGrapheme());
|
|
try testing.expectEqualSlices(u21, &.{0x0A}, page2.lookupGrapheme(rac.cell).?);
|
|
}
|
|
|
|
// Read the original
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint);
|
|
}
|
|
}
|
|
|
|
test "Page cloneFrom frees dst graphemes" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
for (0..page.capacity.rows) |y| {
|
|
const rac = page.getRowAndCell(1, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(y + 1) },
|
|
};
|
|
}
|
|
|
|
// Clone
|
|
var page2 = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page2.deinit();
|
|
for (0..page2.capacity.rows) |y| {
|
|
const rac = page2.getRowAndCell(1, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(y + 1) },
|
|
};
|
|
try page2.appendGrapheme(rac.row, rac.cell, 0x0A);
|
|
}
|
|
|
|
// Clone from page which has no graphemes.
|
|
try page2.cloneFrom(&page, 0, page.size.rows);
|
|
|
|
// Read it again
|
|
for (0..page2.capacity.rows) |y| {
|
|
const rac = page2.getRowAndCell(1, y);
|
|
try testing.expectEqual(@as(u21, @intCast(y + 1)), rac.cell.content.codepoint);
|
|
try testing.expect(!rac.row.grapheme);
|
|
try testing.expect(!rac.cell.hasGrapheme());
|
|
}
|
|
try testing.expectEqual(@as(usize, 0), page2.graphemeCount());
|
|
}
|
|
|
|
test "Page cloneRowFrom partial" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Write
|
|
{
|
|
const y = 0;
|
|
for (0..page.size.cols) |x| {
|
|
const rac = page.getRowAndCell(x, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(x + 1) },
|
|
};
|
|
}
|
|
}
|
|
|
|
// Clone
|
|
var page2 = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page2.deinit();
|
|
try page2.clonePartialRowFrom(
|
|
&page,
|
|
page2.getRow(0),
|
|
page.getRow(0),
|
|
2,
|
|
8,
|
|
);
|
|
|
|
// Read it again
|
|
{
|
|
const y = 0;
|
|
for (0..page2.size.cols) |x| {
|
|
const expected: u21 = if (x >= 2 and x < 8) @intCast(x + 1) else 0;
|
|
const rac = page2.getRowAndCell(x, y);
|
|
try testing.expectEqual(expected, rac.cell.content.codepoint);
|
|
}
|
|
}
|
|
}
|
|
|
|
test "Page cloneRowFrom partial grapheme in non-copied source region" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Write
|
|
{
|
|
const y = 0;
|
|
for (0..page.size.cols) |x| {
|
|
const rac = page.getRowAndCell(x, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(x + 1) },
|
|
};
|
|
}
|
|
{
|
|
const rac = page.getRowAndCell(0, y);
|
|
try page.appendGrapheme(rac.row, rac.cell, 0x0A);
|
|
}
|
|
{
|
|
const rac = page.getRowAndCell(9, y);
|
|
try page.appendGrapheme(rac.row, rac.cell, 0x0A);
|
|
}
|
|
}
|
|
try testing.expectEqual(@as(usize, 2), page.graphemeCount());
|
|
|
|
// Clone
|
|
var page2 = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page2.deinit();
|
|
try page2.clonePartialRowFrom(
|
|
&page,
|
|
page2.getRow(0),
|
|
page.getRow(0),
|
|
2,
|
|
8,
|
|
);
|
|
|
|
// Read it again
|
|
{
|
|
const y = 0;
|
|
for (0..page2.size.cols) |x| {
|
|
const expected: u21 = if (x >= 2 and x < 8) @intCast(x + 1) else 0;
|
|
const rac = page2.getRowAndCell(x, y);
|
|
try testing.expectEqual(expected, rac.cell.content.codepoint);
|
|
try testing.expect(!rac.cell.hasGrapheme());
|
|
}
|
|
{
|
|
const rac = page2.getRowAndCell(9, y);
|
|
try testing.expect(!rac.row.grapheme);
|
|
}
|
|
}
|
|
try testing.expectEqual(@as(usize, 0), page2.graphemeCount());
|
|
}
|
|
|
|
test "Page cloneRowFrom partial grapheme in non-copied dest region" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Write
|
|
{
|
|
const y = 0;
|
|
for (0..page.size.cols) |x| {
|
|
const rac = page.getRowAndCell(x, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(x + 1) },
|
|
};
|
|
}
|
|
}
|
|
try testing.expectEqual(@as(usize, 0), page.graphemeCount());
|
|
|
|
// Clone
|
|
var page2 = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page2.deinit();
|
|
{
|
|
const y = 0;
|
|
for (0..page2.size.cols) |x| {
|
|
const rac = page2.getRowAndCell(x, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = 0xBB },
|
|
};
|
|
}
|
|
{
|
|
const rac = page2.getRowAndCell(0, y);
|
|
try page2.appendGrapheme(rac.row, rac.cell, 0x0A);
|
|
}
|
|
{
|
|
const rac = page2.getRowAndCell(9, y);
|
|
try page2.appendGrapheme(rac.row, rac.cell, 0x0A);
|
|
}
|
|
}
|
|
try page2.clonePartialRowFrom(
|
|
&page,
|
|
page2.getRow(0),
|
|
page.getRow(0),
|
|
2,
|
|
8,
|
|
);
|
|
|
|
// Read it again
|
|
{
|
|
const y = 0;
|
|
for (0..page2.size.cols) |x| {
|
|
const expected: u21 = if (x >= 2 and x < 8) @intCast(x + 1) else 0xBB;
|
|
const rac = page2.getRowAndCell(x, y);
|
|
try testing.expectEqual(expected, rac.cell.content.codepoint);
|
|
}
|
|
{
|
|
const rac = page2.getRowAndCell(9, y);
|
|
try testing.expect(rac.row.grapheme);
|
|
}
|
|
}
|
|
try testing.expectEqual(@as(usize, 2), page2.graphemeCount());
|
|
}
|
|
|
|
test "Page cloneRowFrom partial hyperlink in same page copy" {
|
|
var page = try Page.init(.{ .cols = 10, .rows = 10 });
|
|
defer page.deinit();
|
|
|
|
// We need to create a hyperlink.
|
|
const hyperlink_id = try page.hyperlink_set.addContext(
|
|
page.memory,
|
|
.{ .id = .{ .implicit = 0 }, .uri = .{} },
|
|
.{ .page = &page },
|
|
);
|
|
|
|
// Write
|
|
{
|
|
const y = 0;
|
|
for (0..page.size.cols) |x| {
|
|
const rac = page.getRowAndCell(x, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(x + 1) },
|
|
};
|
|
}
|
|
|
|
// Hyperlink in a single cell
|
|
{
|
|
const rac = page.getRowAndCell(7, y);
|
|
try page.setHyperlink(rac.row, rac.cell, hyperlink_id);
|
|
}
|
|
}
|
|
try testing.expectEqual(@as(usize, 1), page.hyperlinkCount());
|
|
|
|
// Clone into the same page
|
|
try page.clonePartialRowFrom(
|
|
&page,
|
|
page.getRow(1),
|
|
page.getRow(0),
|
|
2,
|
|
8,
|
|
);
|
|
|
|
// Read it again
|
|
{
|
|
const y = 1;
|
|
for (0..page.size.cols) |x| {
|
|
const expected: u21 = if (x >= 2 and x < 8) @intCast(x + 1) else 0;
|
|
const rac = page.getRowAndCell(x, y);
|
|
try testing.expectEqual(expected, rac.cell.content.codepoint);
|
|
}
|
|
{
|
|
const rac = page.getRowAndCell(7, y);
|
|
try testing.expect(rac.row.hyperlink);
|
|
try testing.expect(rac.cell.hyperlink);
|
|
}
|
|
}
|
|
try testing.expectEqual(@as(usize, 2), page.hyperlinkCount());
|
|
}
|
|
|
|
test "Page cloneRowFrom partial hyperlink in same page omit" {
|
|
var page = try Page.init(.{ .cols = 10, .rows = 10 });
|
|
defer page.deinit();
|
|
|
|
// We need to create a hyperlink.
|
|
const hyperlink_id = try page.hyperlink_set.addContext(
|
|
page.memory,
|
|
.{ .id = .{ .implicit = 0 }, .uri = .{} },
|
|
.{ .page = &page },
|
|
);
|
|
|
|
// Write
|
|
{
|
|
const y = 0;
|
|
for (0..page.size.cols) |x| {
|
|
const rac = page.getRowAndCell(x, y);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(x + 1) },
|
|
};
|
|
}
|
|
|
|
// Hyperlink in a single cell
|
|
{
|
|
const rac = page.getRowAndCell(7, y);
|
|
try page.setHyperlink(rac.row, rac.cell, hyperlink_id);
|
|
}
|
|
}
|
|
try testing.expectEqual(@as(usize, 1), page.hyperlinkCount());
|
|
|
|
// Clone into the same page
|
|
try page.clonePartialRowFrom(
|
|
&page,
|
|
page.getRow(1),
|
|
page.getRow(0),
|
|
2,
|
|
6,
|
|
);
|
|
|
|
// Read it again
|
|
{
|
|
const y = 1;
|
|
for (0..page.size.cols) |x| {
|
|
const expected: u21 = if (x >= 2 and x < 6) @intCast(x + 1) else 0;
|
|
const rac = page.getRowAndCell(x, y);
|
|
try testing.expectEqual(expected, rac.cell.content.codepoint);
|
|
}
|
|
{
|
|
const rac = page.getRowAndCell(7, y);
|
|
try testing.expect(!rac.row.hyperlink);
|
|
try testing.expect(!rac.cell.hyperlink);
|
|
}
|
|
}
|
|
try testing.expectEqual(@as(usize, 1), page.hyperlinkCount());
|
|
}
|
|
|
|
test "Page moveCells text-only" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Write
|
|
for (0..page.capacity.cols) |x| {
|
|
const rac = page.getRowAndCell(x, 0);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(x + 1) },
|
|
};
|
|
}
|
|
|
|
const src = page.getRow(0);
|
|
const dst = page.getRow(1);
|
|
page.moveCells(src, 0, dst, 0, page.capacity.cols);
|
|
|
|
// New rows should have text
|
|
for (0..page.capacity.cols) |x| {
|
|
const rac = page.getRowAndCell(x, 1);
|
|
try testing.expectEqual(
|
|
@as(u21, @intCast(x + 1)),
|
|
rac.cell.content.codepoint,
|
|
);
|
|
}
|
|
|
|
// Old row should be blank
|
|
for (0..page.capacity.cols) |x| {
|
|
const rac = page.getRowAndCell(x, 0);
|
|
try testing.expectEqual(
|
|
@as(u21, 0),
|
|
rac.cell.content.codepoint,
|
|
);
|
|
}
|
|
}
|
|
|
|
test "Page moveCells graphemes" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Write
|
|
for (0..page.size.cols) |x| {
|
|
const rac = page.getRowAndCell(x, 0);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(x + 1) },
|
|
};
|
|
try page.appendGrapheme(rac.row, rac.cell, 0x0A);
|
|
}
|
|
const original_count = page.graphemeCount();
|
|
|
|
const src = page.getRow(0);
|
|
const dst = page.getRow(1);
|
|
page.moveCells(src, 0, dst, 0, page.size.cols);
|
|
try testing.expectEqual(original_count, page.graphemeCount());
|
|
|
|
// New rows should have text
|
|
for (0..page.size.cols) |x| {
|
|
const rac = page.getRowAndCell(x, 1);
|
|
try testing.expectEqual(
|
|
@as(u21, @intCast(x + 1)),
|
|
rac.cell.content.codepoint,
|
|
);
|
|
try testing.expectEqualSlices(
|
|
u21,
|
|
&.{0x0A},
|
|
page.lookupGrapheme(rac.cell).?,
|
|
);
|
|
}
|
|
|
|
// Old row should be blank
|
|
for (0..page.size.cols) |x| {
|
|
const rac = page.getRowAndCell(x, 0);
|
|
try testing.expectEqual(
|
|
@as(u21, 0),
|
|
rac.cell.content.codepoint,
|
|
);
|
|
}
|
|
}
|
|
|
|
test "Page verifyIntegrity graphemes good" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Write
|
|
for (0..page.size.cols) |x| {
|
|
const rac = page.getRowAndCell(x, 0);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(x + 1) },
|
|
};
|
|
try page.appendGrapheme(rac.row, rac.cell, 0x0A);
|
|
}
|
|
|
|
try page.verifyIntegrity(testing.allocator);
|
|
}
|
|
|
|
test "Page verifyIntegrity grapheme row not marked" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Write
|
|
for (0..page.size.cols) |x| {
|
|
const rac = page.getRowAndCell(x, 0);
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(x + 1) },
|
|
};
|
|
try page.appendGrapheme(rac.row, rac.cell, 0x0A);
|
|
}
|
|
|
|
// Make invalid by unmarking the row
|
|
page.getRow(0).grapheme = false;
|
|
|
|
try testing.expectError(
|
|
Page.IntegrityError.UnmarkedGraphemeRow,
|
|
page.verifyIntegrity(testing.allocator),
|
|
);
|
|
}
|
|
|
|
test "Page verifyIntegrity styles good" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Upsert a style we'll use
|
|
const id = try page.styles.add(page.memory, .{ .flags = .{
|
|
.bold = true,
|
|
} });
|
|
|
|
// Write
|
|
for (0..page.size.cols) |x| {
|
|
const rac = page.getRowAndCell(x, 0);
|
|
rac.row.styled = true;
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(x + 1) },
|
|
.style_id = id,
|
|
};
|
|
page.styles.use(page.memory, id);
|
|
}
|
|
|
|
// The original style add would have incremented the
|
|
// ref count too, so release it to balance that out.
|
|
page.styles.release(page.memory, id);
|
|
|
|
try page.verifyIntegrity(testing.allocator);
|
|
}
|
|
|
|
test "Page verifyIntegrity styles ref count mismatch" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
|
|
// Upsert a style we'll use
|
|
const id = try page.styles.add(page.memory, .{ .flags = .{
|
|
.bold = true,
|
|
} });
|
|
|
|
// Write
|
|
for (0..page.size.cols) |x| {
|
|
const rac = page.getRowAndCell(x, 0);
|
|
rac.row.styled = true;
|
|
rac.cell.* = .{
|
|
.content_tag = .codepoint,
|
|
.content = .{ .codepoint = @intCast(x + 1) },
|
|
.style_id = id,
|
|
};
|
|
page.styles.use(page.memory, id);
|
|
}
|
|
|
|
// The original style add would have incremented the
|
|
// ref count too, so release it to balance that out.
|
|
page.styles.release(page.memory, id);
|
|
|
|
// Miss a ref
|
|
page.styles.release(page.memory, id);
|
|
|
|
try testing.expectError(
|
|
Page.IntegrityError.MismatchedStyleRef,
|
|
page.verifyIntegrity(testing.allocator),
|
|
);
|
|
}
|
|
|
|
test "Page verifyIntegrity zero rows" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
page.size.rows = 0;
|
|
try testing.expectError(
|
|
Page.IntegrityError.ZeroRowCount,
|
|
page.verifyIntegrity(testing.allocator),
|
|
);
|
|
}
|
|
|
|
test "Page verifyIntegrity zero cols" {
|
|
var page = try Page.init(.{
|
|
.cols = 10,
|
|
.rows = 10,
|
|
.styles = 8,
|
|
});
|
|
defer page.deinit();
|
|
page.size.cols = 0;
|
|
try testing.expectError(
|
|
Page.IntegrityError.ZeroColCount,
|
|
page.verifyIntegrity(testing.allocator),
|
|
);
|
|
}
|