From 7ceff79ea94e9dba81c5268cc263364168b86c5c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 5 Sep 2022 09:47:35 -0700 Subject: [PATCH] various methods on Row are grapheme-aware and tested --- src/Window.zig | 10 +-- src/terminal/Screen.zig | 164 +++++++++++++++++++++++++++++++++++--- src/terminal/Terminal.zig | 39 ++++----- 3 files changed, 181 insertions(+), 32 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 03a7eff90..523909fea 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -1499,7 +1499,7 @@ pub fn eraseChars(self: *Window, count: usize) !void { } pub fn insertLines(self: *Window, count: usize) !void { - self.terminal.insertLines(count); + try self.terminal.insertLines(count); } pub fn insertBlanks(self: *Window, count: usize) !void { @@ -1507,7 +1507,7 @@ pub fn insertBlanks(self: *Window, count: usize) !void { } pub fn deleteLines(self: *Window, count: usize) !void { - self.terminal.deleteLines(count); + try self.terminal.deleteLines(count); } pub fn reverseIndex(self: *Window) !void { @@ -1663,7 +1663,7 @@ pub fn setCursorStyle( } pub fn decaln(self: *Window) !void { - self.terminal.decaln(); + try self.terminal.decaln(); } pub fn tabClear(self: *Window, cmd: terminal.TabClear) !void { @@ -1687,11 +1687,11 @@ pub fn enquiry(self: *Window) !void { } pub fn scrollDown(self: *Window, count: usize) !void { - self.terminal.scrollDown(count); + try self.terminal.scrollDown(count); } pub fn scrollUp(self: *Window, count: usize) !void { - self.terminal.scrollUp(count); + try self.terminal.scrollUp(count); } pub fn setActiveStatusDisplay( diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 8601b0f32..9f34b29ac 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -235,13 +235,32 @@ pub const Row = struct { /// Fill the entire row with a copy of a single cell. pub fn fill(self: Row, cell: Cell) void { - std.mem.set(StorageCell, self.storage[1..], .{ .cell = cell }); + self.fillSlice(cell, 0, self.storage.len - 1); } /// Fill a slice of a row. pub fn fillSlice(self: Row, cell: Cell, start: usize, len: usize) void { assert(len <= self.storage.len - 1); - std.mem.set(StorageCell, self.storage[start + 1 .. len + 1], .{ .cell = cell }); + assert(!cell.attrs.grapheme); // you can't fill with graphemes + + // If our row has no graphemes, then this is a fast copy + if (!self.storage[0].header.flags.grapheme) { + std.mem.set(StorageCell, self.storage[start + 1 .. len + 1], .{ .cell = cell }); + return; + } + + // We have graphemes, so we have to clear those first. + for (self.storage[start + 1 .. len + 1]) |*storage_cell, x| { + if (storage_cell.cell.attrs.grapheme) self.clearGraphemes(x); + storage_cell.* = .{ .cell = cell }; + } + + // We only reset the grapheme flag if we fill the whole row, for now. + // We can improve performance by more correctly setting this but I'm + // going to defer that until we can measure. + if (start == 0 and len == self.storage.len - 1) { + self.storage[0].header.flags.grapheme = false; + } } /// Get a single immutable cell. @@ -292,10 +311,42 @@ pub const Row = struct { try gop.value_ptr.append(self.screen.alloc, cp); } + /// Removes all graphemes associated with a cell. + pub fn clearGraphemes(self: Row, x: usize) void { + const cell = &self.storage[x + 1].cell; + const key = self.getId() + x + 1; + cell.attrs.grapheme = false; + _ = self.screen.graphemes.remove(key); + } + /// Copy the row src into this row. The row can be from another screen. - pub fn copyRow(self: Row, src: Row) void { + pub fn copyRow(self: Row, src: Row) !void { + // If we have graphemes, clear first to unset them. + if (self.storage[0].header.flags.grapheme) self.clear(.{}); + + // If the source has no graphemes (likely) then this is fast. const end = @minimum(src.storage.len, self.storage.len); - std.mem.copy(StorageCell, self.storage[1..], src.storage[1..end]); + if (!src.storage[0].header.flags.grapheme) { + std.mem.copy(StorageCell, self.storage[1..], src.storage[1..end]); + return; + } + + // Source has graphemes, this is slow. + for (src.storage[1..end]) |storage, x| { + self.storage[x + 1] = .{ .cell = storage.cell }; + + // Copy grapheme data if it exists + if (storage.cell.attrs.grapheme) { + const src_key = src.getId() + x + 1; + const src_data = src.screen.graphemes.get(src_key) orelse continue; + + const dst_key = self.getId() + x + 1; + const dst_gop = try self.screen.graphemes.getOrPut(self.screen.alloc, dst_key); + dst_gop.value_ptr.* = try src_data.copy(self.screen.alloc); + + self.storage[0].header.flags.grapheme = true; + } + } } /// Read-only iterator for the cells in the row. @@ -480,6 +531,14 @@ pub const GraphemeData = union(enum) { } } + pub fn copy(self: GraphemeData, alloc: Allocator) !GraphemeData { + // If we're not many we're not allocated so just copy on stack. + if (self != .many) return self; + + // Heap allocated + return GraphemeData{ .many = try alloc.dupe(u21, self.many) }; + } + test { log.warn("Grapheme={}", .{@sizeOf(GraphemeData)}); } @@ -650,12 +709,12 @@ pub fn getRow(self: *Screen, index: RowIndex) Row { } /// Copy the row at src to dst. -pub fn copyRow(self: *Screen, dst: RowIndex, src: RowIndex) void { +pub fn copyRow(self: *Screen, dst: RowIndex, src: RowIndex) !void { // One day we can make this more efficient but for now // we do the easy thing. const dst_row = self.getRow(dst); const src_row = self.getRow(src); - dst_row.copyRow(src_row); + try dst_row.copyRow(src_row); } /// Returns the offset into the storage buffer that the given row can @@ -1032,7 +1091,7 @@ pub fn resizeWithoutReflow(self: *Screen, rows: usize, cols: usize) !void { // Get this row const new_row = self.getRow(.{ .active = y }); - new_row.copyRow(old_row); + try new_row.copyRow(old_row); // Next row y += 1; @@ -1114,7 +1173,7 @@ pub fn resize(self: *Screen, rows: usize, cols: usize) !void { // Get this row var new_row = self.getRow(.{ .active = y }); - new_row.copyRow(old_row); + try new_row.copyRow(old_row); // We need to check if our cursor was on this line. If so, // we set the new cursor. @@ -1458,6 +1517,93 @@ pub fn testString(self: *Screen, alloc: Allocator, tag: RowIndexTag) ![]const u8 return try alloc.realloc(buf, str.len); } +test "Row: clear with graphemes" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 5, 5, 0); + defer s.deinit(); + + const row = s.getRow(.{ .active = 0 }); + try testing.expect(row.getId() > 0); + try testing.expectEqual(@as(usize, 5), row.lenCells()); + try testing.expect(!row.header().flags.grapheme); + + // Lets add a cell with a grapheme + { + const cell = row.getCellPtr(2); + cell.*.char = 'A'; + try row.attachGrapheme(2, 'B'); + try testing.expect(cell.attrs.grapheme); + try testing.expect(row.header().flags.grapheme); + try testing.expect(s.graphemes.count() == 1); + } + + // Clear the row + row.clear(.{}); + try testing.expect(!row.header().flags.grapheme); + try testing.expect(s.graphemes.count() == 0); +} + +test "Row: copy row with graphemes in destination" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 5, 5, 0); + defer s.deinit(); + + // Source row does NOT have graphemes + const row_src = s.getRow(.{ .active = 0 }); + { + const cell = row_src.getCellPtr(2); + cell.*.char = 'A'; + } + + // Destination has graphemes + const row = s.getRow(.{ .active = 1 }); + { + const cell = row.getCellPtr(1); + cell.*.char = 'B'; + try row.attachGrapheme(1, 'C'); + try testing.expect(cell.attrs.grapheme); + try testing.expect(row.header().flags.grapheme); + try testing.expect(s.graphemes.count() == 1); + } + + // Copy + try row.copyRow(row_src); + try testing.expect(!row.header().flags.grapheme); + try testing.expect(s.graphemes.count() == 0); +} + +test "Row: copy row with graphemes in source" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 5, 5, 0); + defer s.deinit(); + + // Source row does NOT have graphemes + const row_src = s.getRow(.{ .active = 0 }); + { + const cell = row_src.getCellPtr(2); + cell.*.char = 'A'; + try row_src.attachGrapheme(2, 'B'); + try testing.expect(cell.attrs.grapheme); + try testing.expect(row_src.header().flags.grapheme); + try testing.expect(s.graphemes.count() == 1); + } + + // Destination has no graphemes + const row = s.getRow(.{ .active = 1 }); + try row.copyRow(row_src); + try testing.expect(row.header().flags.grapheme); + try testing.expect(s.graphemes.count() == 2); + + row_src.clear(.{}); + try testing.expect(s.graphemes.count() == 1); +} + test "Screen" { const testing = std.testing; const alloc = testing.allocator; @@ -1758,7 +1904,7 @@ test "Screen: row copy" { // Copy try s.scroll(.{ .delta = 1 }); - s.copyRow(.{ .active = 2 }, .{ .active = 0 }); + try s.copyRow(.{ .active = 2 }, .{ .active = 0 }); // Test our contents var contents = try s.testString(alloc, .viewport); diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index aeea76937..c5e7b6803 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -608,6 +608,9 @@ fn printCell(self: *Terminal, unmapped_c: u21) *Screen.Cell { } } + // If the prior value had graphemes, clear those + if (cell.attrs.grapheme) row.clearGraphemes(self.screen.cursor.x); + // Write cell.* = self.screen.cursor.pen; cell.char = @intCast(u32, c); @@ -640,7 +643,7 @@ fn clearWideSpacerHead(self: *Terminal) void { /// Resets all margins and fills the whole screen with the character 'E' /// /// Sets the cursor to the top left corner. -pub fn decaln(self: *Terminal) void { +pub fn decaln(self: *Terminal) !void { const tracy = trace(@src()); defer tracy.end(); @@ -654,7 +657,7 @@ pub fn decaln(self: *Terminal) void { var row: usize = 1; while (row < self.rows) : (row += 1) { - self.screen.getRow(.{ .active = row }).copyRow(filled); + try self.screen.getRow(.{ .active = row }).copyRow(filled); } } @@ -697,7 +700,7 @@ pub fn index(self: *Terminal) !void { try self.screen.scroll(.{ .delta = 1 }); } else { // TODO: test - self.scrollUp(1); + try self.scrollUp(1); } return; @@ -726,7 +729,7 @@ pub fn reverseIndex(self: *Terminal) !void { // TODO: scrolling region if (self.screen.cursor.y == 0) { - self.scrollDown(1); + try self.scrollDown(1); } else { self.screen.cursor.y -|= 1; } @@ -1128,7 +1131,7 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { /// All cleared space is colored according to the current SGR state. /// /// Moves the cursor to the left margin. -pub fn insertLines(self: *Terminal, count: usize) void { +pub fn insertLines(self: *Terminal, count: usize) !void { const tracy = trace(@src()); defer tracy.end(); @@ -1149,7 +1152,7 @@ pub fn insertLines(self: *Terminal, count: usize) void { // Ensure we have the lines populated to the end while (y > top) : (y -= 1) { - self.screen.copyRow(.{ .active = y }, .{ .active = y - adjusted_count }); + try self.screen.copyRow(.{ .active = y }, .{ .active = y - adjusted_count }); } // Insert count blank lines @@ -1176,7 +1179,7 @@ pub fn insertLines(self: *Terminal, count: usize) void { /// cleared space is colored according to the current SGR state. /// /// Moves the cursor to the left margin. -pub fn deleteLines(self: *Terminal, count: usize) void { +pub fn deleteLines(self: *Terminal, count: usize) !void { const tracy = trace(@src()); defer tracy.end(); @@ -1194,7 +1197,7 @@ pub fn deleteLines(self: *Terminal, count: usize) void { // Scroll up the count amount. var y: usize = self.screen.cursor.y; while (y <= self.scrolling_region.bottom - adjusted_count) : (y += 1) { - self.screen.copyRow(.{ .active = y }, .{ .active = y + adjusted_count }); + try self.screen.copyRow(.{ .active = y }, .{ .active = y + adjusted_count }); } while (y <= self.scrolling_region.bottom) : (y += 1) { @@ -1205,7 +1208,7 @@ pub fn deleteLines(self: *Terminal, count: usize) void { /// Scroll the text down by one row. /// TODO: test -pub fn scrollDown(self: *Terminal, count: usize) void { +pub fn scrollDown(self: *Terminal, count: usize) !void { const tracy = trace(@src()); defer tracy.end(); @@ -1215,7 +1218,7 @@ pub fn scrollDown(self: *Terminal, count: usize) void { // Move to the top of the scroll region self.screen.cursor.y = self.scrolling_region.top; - self.insertLines(count); + try self.insertLines(count); } /// Removes amount lines from the top of the scroll region. The remaining lines @@ -1226,14 +1229,14 @@ pub fn scrollDown(self: *Terminal, count: usize) void { /// /// Does not change the (absolute) cursor position. // TODO: test -pub fn scrollUp(self: *Terminal, count: usize) void { +pub fn scrollUp(self: *Terminal, count: usize) !void { // Preserve the cursor const cursor = self.screen.cursor; defer self.screen.cursor = cursor; // Move to the top of the scroll region self.screen.cursor.y = self.scrolling_region.top; - self.deleteLines(count); + try self.deleteLines(count); } /// Options for scrolling the viewport of the terminal grid. @@ -1597,7 +1600,7 @@ test "Terminal: deleteLines" { try t.print('D'); t.cursorUp(2); - t.deleteLines(1); + try t.deleteLines(1); try t.print('E'); t.carriageReturn(); @@ -1633,7 +1636,7 @@ test "Terminal: deleteLines with scroll region" { t.setScrollingRegion(1, 3); t.setCursorPos(1, 1); - t.deleteLines(1); + try t.deleteLines(1); try t.print('E'); t.carriageReturn(); @@ -1674,7 +1677,7 @@ test "Terminal: insertLines" { t.setCursorPos(2, 1); // Insert two lines - t.insertLines(2); + try t.insertLines(2); { var str = try t.plainString(testing.allocator); @@ -1705,7 +1708,7 @@ test "Terminal: insertLines with scroll region" { t.setScrollingRegion(1, 2); t.setCursorPos(1, 1); - t.insertLines(1); + try t.insertLines(1); try t.print('X'); @@ -1740,7 +1743,7 @@ test "Terminal: insertLines more than remaining" { t.setCursorPos(2, 1); // Insert a bunch of lines - t.insertLines(20); + try t.insertLines(20); { var str = try t.plainString(testing.allocator); @@ -1881,7 +1884,7 @@ test "Terminal: DECALN" { t.carriageReturn(); try t.linefeed(); try t.print('B'); - t.decaln(); + try t.decaln(); try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); try testing.expectEqual(@as(usize, 0), t.screen.cursor.x);