From 9e27dcdec977c04dfe1350a3d9fae83f51170595 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Aug 2023 15:14:35 -0700 Subject: [PATCH] font: shaper doesn't split run on selection if selection splits grapheme --- src/font/shaper/run.zig | 10 +++++++-- src/terminal/Screen.zig | 47 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 503a1c91e..c180cf12f 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -63,8 +63,14 @@ pub const RunIterator = struct { if (self.selection) |unordered_sel| { if (j > self.i) { const sel = unordered_sel.ordered(.forward); - if (sel.start.x > 0 and j == sel.start.x) break; - if (sel.end.x > 0 and j == sel.end.x + 1) break; + + if (sel.start.x > 0 and + j == sel.start.x and + self.row.graphemeBreak(sel.start.x)) break; + + if (sel.end.x > 0 and + j == sel.end.x + 1 and + self.row.graphemeBreak(sel.end.x)) break; } } diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 4ac304050..42394f7f7 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -513,6 +513,31 @@ pub const Row = struct { }; return .{ .data = data }; } + + /// Returns true if this cell is the end of a grapheme cluster. + /// + /// NOTE: If/when "real" grapheme cluster support is in then + /// this will be removed because every cell will represent exactly + /// one grapheme cluster. + pub fn graphemeBreak(self: Row, x: usize) bool { + const cell = &self.storage[x + 1].cell; + + // Right now, if we are a grapheme, we only store ZWJs on + // the grapheme data so that means we can't be a break. + if (cell.attrs.grapheme) return false; + + // If we are a tail then we check our prior cell. + if (cell.attrs.wide_spacer_tail and x > 0) { + return self.graphemeBreak(x - 1); + } + + // If we are a wide char, then we have to check our prior cell. + if (cell.attrs.wide and x > 0) { + return self.graphemeBreak(x - 1); + } + + return true; + } }; /// Used to iterate through the rows of a specific region. @@ -1927,7 +1952,7 @@ fn selectionSlices(self: *Screen, sel_raw: Selection) struct { const row = self.getRow(.{ .screen = sel.start.y }); const cell = row.getCell(sel.start.x); if (cell.attrs.wide_spacer_tail) { - sel.end.x -= 1; + sel.start.x -= 1; } } @@ -4362,7 +4387,7 @@ test "Screen: selectionString empty with soft wrap" { .end = .{ .x = 2, .y = 0 }, }, true); defer alloc.free(contents); - const expected = ""; + const expected = "👨"; try testing.expectEqualStrings(expected, contents); } } @@ -6190,3 +6215,21 @@ test "Screen: jump to prompt" { try testing.expect(!s.jump(.{ .prompt_delta = 1 })); try testing.expectEqual(@as(usize, 3), s.viewport); } + +test "Screen: row graphemeBreak" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 1, 10, 0); + defer s.deinit(); + try s.testWriteString("x"); + try s.testWriteString("👨‍A"); + + const row = s.getRow(.{ .screen = 0 }); + + // Normal char is a break + try testing.expect(row.graphemeBreak(0)); + + // Emoji with ZWJ is not + try testing.expect(!row.graphemeBreak(1)); +}