From bac1307c4b647aa4761f7fc30db8f99934a721f6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 4 Jul 2024 10:23:21 -0700 Subject: [PATCH] terminal: index hyperlink tests --- src/terminal/Terminal.zig | 131 ++++++++++++++++++++++++++++++++++++++ src/terminal/page.zig | 40 ++++++++++-- 2 files changed, 165 insertions(+), 6 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 7a7e4d5bc..5a74f8f66 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -5809,6 +5809,51 @@ test "Terminal: index from the bottom" { } } +test "Terminal: index scrolling with hyperlink" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 2, .rows = 5 }); + defer t.deinit(alloc); + + t.setCursorPos(5, 1); + try t.screen.startHyperlink("http://example.com", null); + try t.print('A'); + t.screen.endHyperlink(); + t.cursorLeft(1); // undo moving right from 'A' + try t.index(); + try t.print('B'); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n\n\nA\nB", str); + } + + { + const list_cell = t.screen.pages.getCell(.{ .viewport = .{ + .x = 0, + .y = 3, + } }).?; + const row = list_cell.row; + try testing.expect(row.hyperlink); + const cell = list_cell.cell; + try testing.expect(cell.hyperlink); + const id = list_cell.page.data.lookupHyperlink(cell).?; + try testing.expectEqual(@as(hyperlink.Id, 1), id); + } + { + const list_cell = t.screen.pages.getCell(.{ .viewport = .{ + .x = 0, + .y = 4, + } }).?; + const row = list_cell.row; + try testing.expect(!row.hyperlink); + const cell = list_cell.cell; + try testing.expect(!cell.hyperlink); + const id = list_cell.page.data.lookupHyperlink(cell); + try testing.expect(id == null); + } +} + test "Terminal: index outside of scrolling region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); @@ -5935,6 +5980,92 @@ test "Terminal: index inside scroll region" { } } +test "Terminal: index bottom of scroll region with hyperlinks" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 5 }); + defer t.deinit(alloc); + + t.setTopAndBottomMargin(1, 2); + try t.print('A'); + try t.index(); + t.carriageReturn(); + try t.screen.startHyperlink("http://example.com", null); + try t.print('B'); + t.screen.endHyperlink(); + try t.index(); + t.carriageReturn(); + try t.print('C'); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("B\nC", str); + } + + { + const list_cell = t.screen.pages.getCell(.{ .viewport = .{ + .x = 0, + .y = 0, + } }).?; + const row = list_cell.row; + try testing.expect(row.hyperlink); + const cell = list_cell.cell; + try testing.expect(cell.hyperlink); + const id = list_cell.page.data.lookupHyperlink(cell).?; + try testing.expectEqual(@as(hyperlink.Id, 1), id); + } + { + const list_cell = t.screen.pages.getCell(.{ .viewport = .{ + .x = 0, + .y = 1, + } }).?; + const row = list_cell.row; + try testing.expect(!row.hyperlink); + const cell = list_cell.cell; + try testing.expect(!cell.hyperlink); + const id = list_cell.page.data.lookupHyperlink(cell); + try testing.expect(id == null); + } +} + +test "Terminal: index bottom of scroll region clear hyperlinks" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 5 }); + defer t.deinit(alloc); + + t.setTopAndBottomMargin(1, 2); + try t.screen.startHyperlink("http://example.com", null); + try t.print('A'); + t.screen.endHyperlink(); + try t.index(); + t.carriageReturn(); + try t.print('B'); + try t.index(); + t.carriageReturn(); + try t.print('C'); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("B\nC", str); + } + + for (0..2) |y| { + const list_cell = t.screen.pages.getCell(.{ .viewport = .{ + .x = 0, + .y = @intCast(y), + } }).?; + const row = list_cell.row; + try testing.expect(!row.hyperlink); + const cell = list_cell.cell; + try testing.expect(!cell.hyperlink); + const id = list_cell.page.data.lookupHyperlink(cell); + try testing.expect(id == null); + const page = &list_cell.page.data; + try testing.expectEqual(0, page.hyperlink_set.count()); + } +} + test "Terminal: index bottom of scroll region with background SGR" { const alloc = testing.allocator; var t = try init(alloc, .{ .rows = 5, .cols = 5 }); diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 70b9dc582..c2c099013 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -615,9 +615,7 @@ pub const Page = struct { // If our destination has styles or graphemes then we need to // clear some state. - if (dst_row.grapheme or dst_row.styled) { - self.clearCells(dst_row, x_start, x_end); - } + if (dst_row.managedMemory()) self.clearCells(dst_row, x_start, x_end); // Copy all the row metadata but keep our cells offset dst_row.* = copy: { @@ -640,7 +638,7 @@ pub const Page = struct { // If we have no managed memory in the source, then we can just // copy it directly. - if (!src_row.grapheme and !src_row.styled) { + if (!src_row.managedMemory()) { fastmem.copy(Cell, cells, other_cells); } else { // We have managed memory, so we have to do a slower copy to @@ -655,6 +653,26 @@ pub const Page = struct { const cps = other.lookupGrapheme(src_cell).?; for (cps) |cp| try self.appendGrapheme(dst_row, dst_cell, cp); } + if (src_cell.hyperlink) hyperlink: { + dst_row.hyperlink = true; + + // Fast-path: same page we can move it directly + if (other == self) { + self.moveHyperlink(src_cell, dst_cell); + break :hyperlink; + } + + // Slow-path: get the hyperlink from the other page, + // add it, and migrate. + const id = other.lookupHyperlink(src_cell).?; + const other_link = other.hyperlink_set.get(other.memory, id); + const dst_id = try self.hyperlink_set.addContext( + self.memory, + other_link.*, + .{ .page = self }, + ); + try self.setHyperlink(dst_row, dst_cell, dst_id); + } if (src_cell.style_id != style.default_id) { dst_row.styled = true; @@ -668,8 +686,12 @@ pub const Page = struct { // Slow path: Get the style from the other // page and add it to this page's style set. - const other_style = other.styles.get(other.memory, src_cell.style_id).*; - if (try self.styles.addWithId(self.memory, other_style, src_cell.style_id)) |id| { + const other_style = other.styles.get(other.memory, src_cell.style_id); + if (try self.styles.addWithId( + self.memory, + other_style.*, + src_cell.style_id, + )) |id| { dst_cell.style_id = id; } } @@ -1365,6 +1387,12 @@ pub const Row = packed struct(u64) { return self == .prompt or self == .prompt_continuation or self == .input; } }; + + /// Returns true if this row has any managed memory outside of the + /// row structure (graphemes, styles, etc.) + fn managedMemory(self: Row) bool { + return self.grapheme or self.styled or self.hyperlink; + } }; /// A cell represents a single terminal grid cell.