mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
terminal/new: non-grapheme zwjs
This commit is contained in:
@ -2410,6 +2410,7 @@ test "Terminal: VS16 repeated with mode 2027" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X
|
||||||
test "Terminal: VS16 doesn't make character with 2027 disabled" {
|
test "Terminal: VS16 doesn't make character with 2027 disabled" {
|
||||||
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);
|
||||||
@ -2435,6 +2436,7 @@ test "Terminal: VS16 doesn't make character with 2027 disabled" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X
|
||||||
test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
|
test "Terminal: print multicodepoint grapheme, disabled 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);
|
||||||
@ -2526,6 +2528,7 @@ test "Terminal: print multicodepoint grapheme, mode 2027" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X
|
||||||
test "Terminal: print invalid VS16 non-grapheme" {
|
test "Terminal: print invalid VS16 non-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);
|
||||||
|
@ -87,10 +87,10 @@ pub fn cursorCellRight(self: *Screen) *pagepkg.Cell {
|
|||||||
return @ptrCast(cell + 1);
|
return @ptrCast(cell + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursorCellLeft(self: *Screen) *pagepkg.Cell {
|
pub fn cursorCellLeft(self: *Screen, n: size.CellCountInt) *pagepkg.Cell {
|
||||||
assert(self.cursor.x > 0);
|
assert(self.cursor.x >= n);
|
||||||
const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell);
|
const cell: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell);
|
||||||
return @ptrCast(cell - 1);
|
return @ptrCast(cell - n);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursorCellEndOfPrev(self: *Screen) *pagepkg.Cell {
|
pub fn cursorCellEndOfPrev(self: *Screen) *pagepkg.Cell {
|
||||||
@ -245,7 +245,7 @@ pub fn dumpString(
|
|||||||
blank_rows += 1;
|
blank_rows += 1;
|
||||||
|
|
||||||
var blank_cells: usize = 0;
|
var blank_cells: usize = 0;
|
||||||
for (cells) |cell| {
|
for (cells) |*cell| {
|
||||||
// Skip spacers
|
// Skip spacers
|
||||||
switch (cell.wide) {
|
switch (cell.wide) {
|
||||||
.narrow, .wide => {},
|
.narrow, .wide => {},
|
||||||
@ -265,6 +265,13 @@ pub fn dumpString(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try writer.print("{u}", .{cell.codepoint});
|
try writer.print("{u}", .{cell.codepoint});
|
||||||
|
|
||||||
|
if (cell.grapheme) {
|
||||||
|
const cps = row_offset.page.data.lookupGrapheme(cell).?;
|
||||||
|
for (cps) |cp| {
|
||||||
|
try writer.print("{u}", .{cp});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,7 +259,26 @@ pub fn print(self: *Terminal, c: u21) !void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@panic("TODO: zero-width characters");
|
// Find our previous cell
|
||||||
|
const prev = prev: {
|
||||||
|
const immediate = self.screen.cursorCellLeft(1);
|
||||||
|
if (immediate.wide != .spacer_tail) break :prev immediate;
|
||||||
|
break :prev self.screen.cursorCellLeft(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If this is a emoji variation selector, prev must be an emoji
|
||||||
|
if (c == 0xFE0F or c == 0xFE0E) {
|
||||||
|
const prev_props = unicode.getProperties(prev.codepoint);
|
||||||
|
const emoji = prev_props.grapheme_boundary_class == .extended_pictographic;
|
||||||
|
if (!emoji) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.screen.cursor.page_offset.page.data.appendGrapheme(
|
||||||
|
self.screen.cursor.page_row,
|
||||||
|
prev,
|
||||||
|
c,
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have a printable character, save it
|
// We have a printable character, save it
|
||||||
@ -359,7 +378,7 @@ fn printCell(
|
|||||||
.spacer_tail => {
|
.spacer_tail => {
|
||||||
assert(self.screen.cursor.x > 0);
|
assert(self.screen.cursor.x > 0);
|
||||||
|
|
||||||
const wide_cell = self.screen.cursorCellLeft();
|
const wide_cell = self.screen.cursorCellLeft(1);
|
||||||
wide_cell.* = .{ .style_id = self.screen.cursor.style_id };
|
wide_cell.* = .{ .style_id = self.screen.cursor.style_id };
|
||||||
if (self.screen.cursor.y > 0 and self.screen.cursor.x <= 1) {
|
if (self.screen.cursor.y > 0 and self.screen.cursor.x <= 1) {
|
||||||
const head_cell = self.screen.cursorCellEndOfPrev();
|
const head_cell = self.screen.cursorCellEndOfPrev();
|
||||||
@ -391,7 +410,7 @@ fn printCell(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn printWrap(self: *Terminal) !void {
|
fn printWrap(self: *Terminal) !void {
|
||||||
self.screen.cursor.page_row.flags.wrap = true;
|
self.screen.cursor.page_row.wrap = true;
|
||||||
|
|
||||||
// Get the old semantic prompt so we can extend it to the next
|
// Get the old semantic prompt so we can extend it to the next
|
||||||
// line. We need to do this before we index() because we may
|
// line. We need to do this before we index() because we may
|
||||||
@ -407,7 +426,7 @@ fn printWrap(self: *Terminal) !void {
|
|||||||
// New line must inherit semantic prompt of the old line
|
// New line must inherit semantic prompt of the old line
|
||||||
// const new_row = self.screen.getRow(.{ .active = self.screen.cursor.y });
|
// const new_row = self.screen.getRow(.{ .active = self.screen.cursor.y });
|
||||||
// new_row.setSemanticPrompt(old_prompt);
|
// new_row.setSemanticPrompt(old_prompt);
|
||||||
self.screen.cursor.page_row.flags.wrap_continuation = true;
|
self.screen.cursor.page_row.wrap_continuation = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Carriage return moves the cursor to the first column.
|
/// Carriage return moves the cursor to the first column.
|
||||||
@ -526,7 +545,7 @@ pub fn cursorLeft(self: *Terminal, count_req: usize) void {
|
|||||||
|
|
||||||
// If our previous line is not wrapped then we are done.
|
// If our previous line is not wrapped then we are done.
|
||||||
if (wrap_mode != .reverse_extended) {
|
if (wrap_mode != .reverse_extended) {
|
||||||
if (!self.screen.cursor.page_row.flags.wrap) break;
|
if (!self.screen.cursor.page_row.wrap) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.screen.cursorAbsolute(right_margin, self.screen.cursor.y - 1);
|
self.screen.cursorAbsolute(right_margin, self.screen.cursor.y - 1);
|
||||||
@ -901,6 +920,131 @@ test "Terminal: print over wide spacer tail" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
|
||||||
|
var t = try init(testing.allocator, 80, 80);
|
||||||
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
|
// 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 6 cells taken up
|
||||||
|
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
|
||||||
|
try testing.expectEqual(@as(usize, 6), 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, 1), 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);
|
||||||
|
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expectEqual(@as(u21, 0x1F469), 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 = 3, .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);
|
||||||
|
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expectEqual(@as(u21, 0x1F467), cell.codepoint);
|
||||||
|
try testing.expect(!cell.grapheme);
|
||||||
|
try testing.expectEqual(Cell.Wide.wide, cell.wide);
|
||||||
|
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 5, .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);
|
||||||
|
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: VS16 doesn't make character with 2027 disabled" {
|
||||||
|
var t = try init(testing.allocator, 5, 5);
|
||||||
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
|
// Disable grapheme clustering
|
||||||
|
t.modes.set(.grapheme_cluster, false);
|
||||||
|
|
||||||
|
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.narrow, cell.wide);
|
||||||
|
const cps = list_cell.page.data.lookupGrapheme(cell).?;
|
||||||
|
try testing.expectEqual(@as(usize, 1), cps.len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: print invalid VS16 non-grapheme" {
|
||||||
|
var t = try init(testing.allocator, 80, 80);
|
||||||
|
defer t.deinit(testing.allocator);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
@ -395,13 +395,9 @@ pub const Capacity = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const Row = packed struct(u64) {
|
pub const Row = packed struct(u64) {
|
||||||
_padding: u29 = 0,
|
|
||||||
|
|
||||||
/// The cells in the row offset from the page.
|
/// The cells in the row offset from the page.
|
||||||
cells: Offset(Cell),
|
cells: Offset(Cell),
|
||||||
|
|
||||||
/// Flags where we want to pack bits
|
|
||||||
flags: packed struct {
|
|
||||||
/// True if this row is soft-wrapped. The first cell of the next
|
/// True if this row is soft-wrapped. The first cell of the next
|
||||||
/// row is a continuation of this row.
|
/// row is a continuation of this row.
|
||||||
wrap: bool = false,
|
wrap: bool = false,
|
||||||
@ -415,7 +411,8 @@ pub const Row = packed struct(u64) {
|
|||||||
/// possible because erasing for example may need to clear existing
|
/// possible because erasing for example may need to clear existing
|
||||||
/// grapheme data.
|
/// grapheme data.
|
||||||
grapheme: bool = false,
|
grapheme: bool = false,
|
||||||
} = .{},
|
|
||||||
|
_padding: u29 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A cell represents a single terminal grid cell.
|
/// A cell represents a single terminal grid cell.
|
||||||
|
Reference in New Issue
Block a user