mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
terminal/new: graphemes
This commit is contained in:
@ -2327,6 +2327,7 @@ test "Terminal: print over wide spacer tail" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X
|
||||||
test "Terminal: VS15 to make narrow character" {
|
test "Terminal: VS15 to make narrow character" {
|
||||||
var t = try init(testing.allocator, 5, 5);
|
var t = try init(testing.allocator, 5, 5);
|
||||||
defer t.deinit(testing.allocator);
|
defer t.deinit(testing.allocator);
|
||||||
@ -2352,6 +2353,7 @@ test "Terminal: VS15 to make narrow character" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X
|
||||||
test "Terminal: VS16 to make wide character with mode 2027" {
|
test "Terminal: VS16 to make wide character with mode 2027" {
|
||||||
var t = try init(testing.allocator, 5, 5);
|
var t = try init(testing.allocator, 5, 5);
|
||||||
defer t.deinit(testing.allocator);
|
defer t.deinit(testing.allocator);
|
||||||
@ -2377,6 +2379,7 @@ test "Terminal: VS16 to make wide character with mode 2027" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X
|
||||||
test "Terminal: VS16 repeated with mode 2027" {
|
test "Terminal: VS16 repeated with mode 2027" {
|
||||||
var t = try init(testing.allocator, 5, 5);
|
var t = try init(testing.allocator, 5, 5);
|
||||||
defer t.deinit(testing.allocator);
|
defer t.deinit(testing.allocator);
|
||||||
@ -2492,6 +2495,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X
|
||||||
test "Terminal: print multicodepoint grapheme, mode 2027" {
|
test "Terminal: print multicodepoint grapheme, mode 2027" {
|
||||||
var t = try init(testing.allocator, 80, 80);
|
var t = try init(testing.allocator, 80, 80);
|
||||||
defer t.deinit(testing.allocator);
|
defer t.deinit(testing.allocator);
|
||||||
@ -2556,6 +2560,7 @@ test "Terminal: print invalid VS16 non-grapheme" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X
|
||||||
test "Terminal: print invalid VS16 grapheme" {
|
test "Terminal: print invalid VS16 grapheme" {
|
||||||
var t = try init(testing.allocator, 80, 80);
|
var t = try init(testing.allocator, 80, 80);
|
||||||
defer t.deinit(testing.allocator);
|
defer t.deinit(testing.allocator);
|
||||||
@ -2586,6 +2591,7 @@ test "Terminal: print invalid VS16 grapheme" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X
|
||||||
test "Terminal: print invalid VS16 with second char" {
|
test "Terminal: print invalid VS16 with second char" {
|
||||||
var t = try init(testing.allocator, 80, 80);
|
var t = try init(testing.allocator, 80, 80);
|
||||||
defer t.deinit(testing.allocator);
|
defer t.deinit(testing.allocator);
|
||||||
@ -2710,6 +2716,7 @@ test "Terminal: disabled wraparound with wide char and no space" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X
|
||||||
test "Terminal: disabled wraparound with wide grapheme and half space" {
|
test "Terminal: disabled wraparound with wide grapheme and half space" {
|
||||||
var t = try init(testing.allocator, 5, 5);
|
var t = try init(testing.allocator, 5, 5);
|
||||||
defer t.deinit(testing.allocator);
|
defer t.deinit(testing.allocator);
|
||||||
|
@ -227,8 +227,127 @@ pub fn print(self: *Terminal, c: u21) !void {
|
|||||||
// This is MUCH slower than the normal path so the conditional below is
|
// This is MUCH slower than the normal path so the conditional below is
|
||||||
// purposely ordered in least-likely to most-likely so we can drop out
|
// purposely ordered in least-likely to most-likely so we can drop out
|
||||||
// as quickly as possible.
|
// as quickly as possible.
|
||||||
if (c > 255 and self.modes.get(.grapheme_cluster) and self.screen.cursor.x > 0) {
|
if (c > 255 and
|
||||||
@panic("TODO: graphemes");
|
self.modes.get(.grapheme_cluster) and
|
||||||
|
self.screen.cursor.x > 0)
|
||||||
|
grapheme: {
|
||||||
|
// We need the previous cell to determine if we're at a grapheme
|
||||||
|
// break or not. If we are NOT, then we are still combining the
|
||||||
|
// same grapheme. Otherwise, we can stay in this cell.
|
||||||
|
const Prev = struct { cell: *Cell, left: size.CellCountInt };
|
||||||
|
const prev: Prev = prev: {
|
||||||
|
const left: size.CellCountInt = left: {
|
||||||
|
// If we have wraparound, then we always use the prev col
|
||||||
|
if (self.modes.get(.wraparound)) break :left 1;
|
||||||
|
|
||||||
|
// If we do not have wraparound, the logic is trickier. If
|
||||||
|
// we're not on the last column, then we just use the previous
|
||||||
|
// column. Otherwise, we need to check if there is text to
|
||||||
|
// figure out if we're attaching to the prev or current.
|
||||||
|
if (self.screen.cursor.x != right_limit - 1) break :left 1;
|
||||||
|
break :left @intFromBool(self.screen.cursor.page_cell.codepoint == 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the previous cell is a wide spacer tail, then we actually
|
||||||
|
// want to use the cell before that because that has the actual
|
||||||
|
// content.
|
||||||
|
const immediate = self.screen.cursorCellLeft(left);
|
||||||
|
break :prev switch (immediate.wide) {
|
||||||
|
else => .{ .cell = immediate, .left = left },
|
||||||
|
.spacer_tail => .{
|
||||||
|
.cell = self.screen.cursorCellLeft(left + 1),
|
||||||
|
.left = left + 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// If our cell has no content, then this is a new cell and
|
||||||
|
// necessarily a grapheme break.
|
||||||
|
if (prev.cell.codepoint == 0) break :grapheme;
|
||||||
|
|
||||||
|
const grapheme_break = brk: {
|
||||||
|
var state: unicode.GraphemeBreakState = .{};
|
||||||
|
var cp1: u21 = prev.cell.codepoint;
|
||||||
|
if (prev.cell.grapheme) {
|
||||||
|
const cps = self.screen.cursor.page_offset.page.data.lookupGrapheme(prev.cell).?;
|
||||||
|
for (cps) |cp2| {
|
||||||
|
// log.debug("cp1={x} cp2={x}", .{ cp1, cp2 });
|
||||||
|
assert(!unicode.graphemeBreak(cp1, cp2, &state));
|
||||||
|
cp1 = cp2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.debug("cp1={x} cp2={x} end", .{ cp1, c });
|
||||||
|
break :brk unicode.graphemeBreak(cp1, c, &state);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we can NOT break, this means that "c" is part of a grapheme
|
||||||
|
// with the previous char.
|
||||||
|
if (!grapheme_break) {
|
||||||
|
// If this is an emoji variation selector then we need to modify
|
||||||
|
// the cell width accordingly. VS16 makes the character wide and
|
||||||
|
// VS15 makes it narrow.
|
||||||
|
if (c == 0xFE0F or c == 0xFE0E) {
|
||||||
|
// This only applies to emoji
|
||||||
|
const prev_props = unicode.getProperties(prev.cell.codepoint);
|
||||||
|
const emoji = prev_props.grapheme_boundary_class == .extended_pictographic;
|
||||||
|
if (!emoji) return;
|
||||||
|
|
||||||
|
switch (c) {
|
||||||
|
0xFE0F => wide: {
|
||||||
|
if (prev.cell.wide == .wide) break :wide;
|
||||||
|
|
||||||
|
// Move our cursor back to the previous. We'll move
|
||||||
|
// the cursor within this block to the proper location.
|
||||||
|
self.screen.cursorLeft(prev.left);
|
||||||
|
|
||||||
|
// If we don't have space for the wide char, we need
|
||||||
|
// to insert spacers and wrap. Then we just print the wide
|
||||||
|
// char as normal.
|
||||||
|
if (self.screen.cursor.x == right_limit - 1) {
|
||||||
|
if (!self.modes.get(.wraparound)) return;
|
||||||
|
self.printCell(' ', .spacer_head);
|
||||||
|
try self.printWrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.printCell(prev.cell.codepoint, .wide);
|
||||||
|
|
||||||
|
// Write our spacer
|
||||||
|
self.screen.cursorRight(1);
|
||||||
|
self.printCell(' ', .spacer_tail);
|
||||||
|
|
||||||
|
// Move the cursor again so we're beyond our spacer
|
||||||
|
if (self.screen.cursor.x == right_limit - 1) {
|
||||||
|
self.screen.cursor.pending_wrap = true;
|
||||||
|
} else {
|
||||||
|
self.screen.cursorRight(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
0xFE0E => narrow: {
|
||||||
|
// Prev cell is no longer wide
|
||||||
|
if (prev.cell.wide != .wide) break :narrow;
|
||||||
|
prev.cell.wide = .narrow;
|
||||||
|
|
||||||
|
// Remove the wide spacer tail
|
||||||
|
const cell = self.screen.cursorCellLeft(prev.left - 1);
|
||||||
|
cell.wide = .narrow;
|
||||||
|
|
||||||
|
break :narrow;
|
||||||
|
},
|
||||||
|
|
||||||
|
else => unreachable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("c={x} grapheme attach to left={}", .{ c, prev.left });
|
||||||
|
try self.screen.cursor.page_offset.page.data.appendGrapheme(
|
||||||
|
self.screen.cursor.page_row,
|
||||||
|
prev.cell,
|
||||||
|
c,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the width of this character so we can handle
|
// Determine the width of this character so we can handle
|
||||||
@ -1045,6 +1164,204 @@ test "Terminal: print invalid VS16 non-grapheme" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Terminal: print multicodepoint grapheme, mode 2027" {
|
||||||
|
var t = try init(testing.allocator, 80, 80);
|
||||||
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
|
// Enable grapheme clustering
|
||||||
|
t.modes.set(.grapheme_cluster, true);
|
||||||
|
|
||||||
|
// https://github.com/mitchellh/ghostty/issues/289
|
||||||
|
// This is: 👨👩👧 (which may or may not render correctly)
|
||||||
|
try t.print(0x1F468);
|
||||||
|
try t.print(0x200D);
|
||||||
|
try t.print(0x1F469);
|
||||||
|
try t.print(0x200D);
|
||||||
|
try t.print(0x1F467);
|
||||||
|
|
||||||
|
// We should have 2 cells taken up. It is one character but "wide".
|
||||||
|
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
|
||||||
|
try testing.expectEqual(@as(usize, 2), t.screen.cursor.x);
|
||||||
|
|
||||||
|
// Assert various properties about our screen to verify
|
||||||
|
// we have all expected cells.
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expectEqual(@as(u21, 0x1F468), cell.codepoint);
|
||||||
|
try testing.expect(cell.grapheme);
|
||||||
|
try testing.expectEqual(Cell.Wide.wide, cell.wide);
|
||||||
|
const cps = list_cell.page.data.lookupGrapheme(cell).?;
|
||||||
|
try testing.expectEqual(@as(usize, 4), cps.len);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expectEqual(@as(u21, ' '), cell.codepoint);
|
||||||
|
try testing.expect(!cell.grapheme);
|
||||||
|
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: VS15 to make narrow character" {
|
||||||
|
var t = try init(testing.allocator, 5, 5);
|
||||||
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
|
// Enable grapheme clustering
|
||||||
|
t.modes.set(.grapheme_cluster, true);
|
||||||
|
|
||||||
|
try t.print(0x26C8); // Thunder cloud and rain
|
||||||
|
try t.print(0xFE0E); // VS15 to make narrow
|
||||||
|
|
||||||
|
{
|
||||||
|
const str = try t.plainString(testing.allocator);
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
try testing.expectEqualStrings("⛈︎", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expectEqual(@as(u21, 0x26C8), cell.codepoint);
|
||||||
|
try testing.expect(cell.grapheme);
|
||||||
|
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
|
||||||
|
const cps = list_cell.page.data.lookupGrapheme(cell).?;
|
||||||
|
try testing.expectEqual(@as(usize, 1), cps.len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: VS16 to make wide character with mode 2027" {
|
||||||
|
var t = try init(testing.allocator, 5, 5);
|
||||||
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
|
// Enable grapheme clustering
|
||||||
|
t.modes.set(.grapheme_cluster, true);
|
||||||
|
|
||||||
|
try t.print(0x2764); // Heart
|
||||||
|
try t.print(0xFE0F); // VS16 to make wide
|
||||||
|
|
||||||
|
{
|
||||||
|
const str = try t.plainString(testing.allocator);
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
try testing.expectEqualStrings("❤️", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expectEqual(@as(u21, 0x2764), cell.codepoint);
|
||||||
|
try testing.expect(cell.grapheme);
|
||||||
|
try testing.expectEqual(Cell.Wide.wide, cell.wide);
|
||||||
|
const cps = list_cell.page.data.lookupGrapheme(cell).?;
|
||||||
|
try testing.expectEqual(@as(usize, 1), cps.len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: VS16 repeated with mode 2027" {
|
||||||
|
var t = try init(testing.allocator, 5, 5);
|
||||||
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
|
// Enable grapheme clustering
|
||||||
|
t.modes.set(.grapheme_cluster, true);
|
||||||
|
|
||||||
|
try t.print(0x2764); // Heart
|
||||||
|
try t.print(0xFE0F); // VS16 to make wide
|
||||||
|
try t.print(0x2764); // Heart
|
||||||
|
try t.print(0xFE0F); // VS16 to make wide
|
||||||
|
|
||||||
|
{
|
||||||
|
const str = try t.plainString(testing.allocator);
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
try testing.expectEqualStrings("❤️❤️", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expectEqual(@as(u21, 0x2764), cell.codepoint);
|
||||||
|
try testing.expect(cell.grapheme);
|
||||||
|
try testing.expectEqual(Cell.Wide.wide, cell.wide);
|
||||||
|
const cps = list_cell.page.data.lookupGrapheme(cell).?;
|
||||||
|
try testing.expectEqual(@as(usize, 1), cps.len);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expectEqual(@as(u21, 0x2764), cell.codepoint);
|
||||||
|
try testing.expect(cell.grapheme);
|
||||||
|
try testing.expectEqual(Cell.Wide.wide, cell.wide);
|
||||||
|
const cps = list_cell.page.data.lookupGrapheme(cell).?;
|
||||||
|
try testing.expectEqual(@as(usize, 1), cps.len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: print invalid VS16 grapheme" {
|
||||||
|
var t = try init(testing.allocator, 80, 80);
|
||||||
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
|
// Enable grapheme clustering
|
||||||
|
t.modes.set(.grapheme_cluster, true);
|
||||||
|
|
||||||
|
// https://github.com/mitchellh/ghostty/issues/1482
|
||||||
|
try t.print('x');
|
||||||
|
try t.print(0xFE0F);
|
||||||
|
|
||||||
|
// We should have 2 cells taken up. It is one character but "wide".
|
||||||
|
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
|
||||||
|
try testing.expectEqual(@as(usize, 1), t.screen.cursor.x);
|
||||||
|
|
||||||
|
// Assert various properties about our screen to verify
|
||||||
|
// we have all expected cells.
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expectEqual(@as(u21, 'x'), cell.codepoint);
|
||||||
|
try testing.expect(!cell.grapheme);
|
||||||
|
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expectEqual(@as(u21, 0), cell.codepoint);
|
||||||
|
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: print invalid VS16 with second char" {
|
||||||
|
var t = try init(testing.allocator, 80, 80);
|
||||||
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
|
// Enable grapheme clustering
|
||||||
|
t.modes.set(.grapheme_cluster, true);
|
||||||
|
|
||||||
|
// https://github.com/mitchellh/ghostty/issues/1482
|
||||||
|
try t.print('x');
|
||||||
|
try t.print(0xFE0F);
|
||||||
|
try t.print('y');
|
||||||
|
|
||||||
|
// We should have 2 cells taken up. It is one character but "wide".
|
||||||
|
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
|
||||||
|
try testing.expectEqual(@as(usize, 2), t.screen.cursor.x);
|
||||||
|
|
||||||
|
// Assert various properties about our screen to verify
|
||||||
|
// we have all expected cells.
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expectEqual(@as(u21, 'x'), cell.codepoint);
|
||||||
|
try testing.expect(!cell.grapheme);
|
||||||
|
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expectEqual(@as(u21, 'y'), cell.codepoint);
|
||||||
|
try testing.expect(!cell.grapheme);
|
||||||
|
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "Terminal: soft wrap" {
|
test "Terminal: soft wrap" {
|
||||||
var t = try init(testing.allocator, 3, 80);
|
var t = try init(testing.allocator, 3, 80);
|
||||||
defer t.deinit(testing.allocator);
|
defer t.deinit(testing.allocator);
|
||||||
@ -1115,6 +1432,35 @@ test "Terminal: disabled wraparound with wide char and no space" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Terminal: disabled wraparound with wide grapheme and half space" {
|
||||||
|
var t = try init(testing.allocator, 5, 5);
|
||||||
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
|
t.modes.set(.grapheme_cluster, true);
|
||||||
|
t.modes.set(.wraparound, false);
|
||||||
|
|
||||||
|
// This puts our cursor at the end and there is NO SPACE for a
|
||||||
|
// wide character.
|
||||||
|
try t.printString("AAAA");
|
||||||
|
try t.print(0x2764); // Heart
|
||||||
|
try t.print(0xFE0F); // VS16 to make wide
|
||||||
|
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
|
||||||
|
try testing.expectEqual(@as(usize, 4), t.screen.cursor.x);
|
||||||
|
|
||||||
|
{
|
||||||
|
const str = try t.plainString(testing.allocator);
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
try testing.expectEqualStrings("AAAA❤", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expectEqual(@as(u21, '❤'), cell.codepoint);
|
||||||
|
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "Terminal: print right margin wrap" {
|
test "Terminal: print right margin wrap" {
|
||||||
var t = try init(testing.allocator, 10, 5);
|
var t = try init(testing.allocator, 10, 5);
|
||||||
defer t.deinit(testing.allocator);
|
defer t.deinit(testing.allocator);
|
||||||
|
Reference in New Issue
Block a user