diff --git a/src/terminal/page.zig b/src/terminal/page.zig index dae019c1f..a06a1dae7 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -2239,6 +2239,12 @@ pub const Cell = packed struct(u64) { /// struct T, used for masked compares of raw backing-integer values /// (e.g. `Row.Backing`, `Cell.Backing`). This is an implementation /// detail of `Mask`, which is the public API built on top of this. +/// +/// A field may be a dot-separated path (e.g. "content.codepoint.data") +/// to cover only a nested field of a packed struct or packed union +/// member. This allows a mask to be more precise than a whole +/// top-level field, e.g. covering the codepoint bits of a cell without +/// its padding. fn fieldMask( comptime T: type, comptime fields: []const []const u8, @@ -2246,16 +2252,33 @@ fn fieldMask( // Backing int of the packed struct const Int = @typeInfo(T).@"struct".backing_integer.?; - var mask: Int = 0; - inline for (fields) |field| { + comptime var mask: Int = 0; + inline for (fields) |path| { + // Walk the path to find the total bit offset and the type of + // the (possibly nested) field. + comptime var offset = 0; + comptime var Field = T; + comptime var it = std.mem.splitScalar(u8, path, '.'); + inline while (comptime it.next()) |name| { + offset += switch (@typeInfo(Field)) { + .@"struct" => @bitOffsetOf(Field, name), + + // Packed union members all share bit offset zero. + .@"union" => |u| offset: { + comptime assert(u.layout == .@"packed"); + break :offset 0; + }, + + else => @compileError("invalid field path: " ++ path), + }; + Field = @FieldType(Field, name); + } + // The type that fits all the bits we need to set. - const Ones = std.meta.Int( - .unsigned, - @bitSizeOf(@FieldType(T, field)), - ); + const Ones = std.meta.Int(.unsigned, @bitSizeOf(Field)); // Mask out the ones - mask |= @as(Int, std.math.maxInt(Ones)) << @bitOffsetOf(T, field); + mask |= @as(Int, std.math.maxInt(Ones)) << offset; } return mask; @@ -2378,6 +2401,21 @@ pub fn Mask( return pattern(v) == expected; } + /// Returns true if any value in the group of group_len values + /// starting at index i has masked fields equal to the expected + /// pattern (see `pattern`). This is the "any" counterpart to + /// `eql`: use it to detect the presence of a specific value + /// within a group, e.g. a run scan that must stop when it + /// encounters a sentinel codepoint anywhere in the group. + pub inline fn eqlAny( + values: []const T, + i: usize, + expected: Backing, + ) bool { + const masked = load(values, i) & @as(Group, @splat(mask)); + return @reduce(.Or, masked == @as(Group, @splat(expected))); + } + /// Like `eql` but returns the number of leading values whose /// masked fields equal the expected pattern, i.e. group_len if /// the entire group matches. This is useful for early-exit run @@ -2497,6 +2535,59 @@ test "Mask" { try testing.expectEqual(M.strip(styled), M.strip(styled_other)); try testing.expect(M.strip(styled) != M.strip(styled2)); } + + // eqlAny: presence of a matching value anywhere in the group + { + const expected = M.pattern(styled); + var cells: [4]Cell = .{ plain, plain, plain, plain }; + try testing.expect(!M.eqlAny(&cells, 0, expected)); + + cells[2] = styled; + try testing.expect(M.eqlAny(&cells, 0, expected)); + + // Masked compare: same masked fields with a different + // codepoint still matches. + cells[2] = styled2; + try testing.expect(M.eqlAny(&cells, 0, expected)); + } +} + +test "Mask nested field path" { + // Mask only the codepoint data bits of the content field, not + // the padding next to it or any other field. + const M = Mask(Cell, &.{"content.codepoint.data"}, 4); + + const a: Cell = .init('A'); + var b: Cell = .init('A'); + b.style_id = 5; + b.wide = .wide; + const c: Cell = .init('C'); + + // Same codepoint matches regardless of other fields. + const expected = M.pattern(a); + try testing.expect(M.eqlScalar(b, expected)); + try testing.expect(!M.eqlScalar(c, expected)); + + // The mask must cover exactly the codepoint data bits. + const cp_offset = @bitOffsetOf(Cell, "content"); + try testing.expectEqual( + @as(u64, std.math.maxInt(u21)) << cp_offset, + comptime fieldMask(Cell, &.{"content.codepoint.data"}), + ); + + // Group variants + { + var cells: [4]Cell = .{ a, b, a, b }; + try testing.expect(M.eql(&cells, 0, expected)); + try testing.expect(M.eqlAny(&cells, 0, expected)); + + cells[1] = c; + try testing.expect(!M.eql(&cells, 0, expected)); + try testing.expect(M.eqlAny(&cells, 0, expected)); + + const none: [4]Cell = .{ c, c, c, c }; + try testing.expect(!M.eqlAny(&none, 0, expected)); + } } // Uncomment this when you want to do some math.