From 57c5522a6ba24555180aea1231cd2397a24d906c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 4 Jul 2024 10:00:12 -0700 Subject: [PATCH] terminal: handle moving/swapping/clearing cells with hyperlinks --- src/terminal/Screen.zig | 7 ++++ src/terminal/Terminal.zig | 79 +++++++++++++++++++++++++++++++++++++++ src/terminal/page.zig | 38 +++++++++++++++++++ 3 files changed, 124 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 5d138250c..2c4797841 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -949,6 +949,13 @@ pub fn clearCells( } } + // If we have hyperlinks, we need to clear those. + if (row.hyperlink) { + for (cells) |*cell| { + if (cell.hyperlink) page.clearHyperlink(row, cell); + } + } + if (row.styled) { for (cells) |*cell| { if (cell.style_id == style.default_id) continue; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 51d7a7fb1..2c39db76a 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -7643,6 +7643,85 @@ test "Terminal: insertBlanks split multi-cell character from tail" { } } +test "Terminal: insertBlanks shifts hyperlinks" { + // osc "8;;http://example.com" + // printf "link" + // printf "\r" + // csi "3@" + // echo + // + // link should be preserved, blanks should not be linked + + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 10, .rows = 2 }); + defer t.deinit(alloc); + + try t.screen.startHyperlink("http://example.com", null); + try t.printString("ABC"); + t.setCursorPos(1, 1); + t.insertBlanks(2); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" ABC", str); + } + + // Verify all our cells have a hyperlink + for (2..5) |x| { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ + .x = @intCast(x), + .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); + } + for (0..2) |x| { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ + .x = @intCast(x), + .y = 0, + } }).?; + 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: insertBlanks pushes hyperlink off end completely" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 3, .rows = 2 }); + defer t.deinit(alloc); + + try t.screen.startHyperlink("http://example.com", null); + try t.printString("ABC"); + t.setCursorPos(1, 1); + t.insertBlanks(3); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("", str); + } + + for (0..3) |x| { + const list_cell = t.screen.pages.getCell(.{ .screen = .{ + .x = @intCast(x), + .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.expect(id == null); + } +} + test "Terminal: insert mode with space" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 2 }); diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 2aa6f78e3..70b9dc582 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -816,6 +816,26 @@ pub const Page = struct { } } + // Hyperlinks are keyed by cell offset. + if (src.hyperlink or dst.hyperlink) { + if (src.hyperlink and !dst.hyperlink) { + self.moveHyperlink(src, dst); + } else if (!src.hyperlink and dst.hyperlink) { + self.moveHyperlink(dst, src); + } else { + // Both had hyperlinks, so we have to manually swap + const src_offset = getOffset(Cell, self.memory, src); + const dst_offset = getOffset(Cell, self.memory, dst); + var map = self.hyperlink_map.map(self.memory); + const src_entry = map.getEntry(src_offset).?; + const dst_entry = map.getEntry(dst_offset).?; + const src_value = src_entry.value_ptr.*; + const dst_value = dst_entry.value_ptr.*; + src_entry.value_ptr.* = dst_value; + dst_entry.value_ptr.* = src_value; + } + } + // Copy the metadata. Note that we do NOT have to worry about // styles because styles are keyed by ID and we're preserving the // exact ref count and row state here. @@ -920,6 +940,24 @@ pub const Page = struct { row.hyperlink = true; } + /// Move the hyperlink from one cell to another. This can't fail + /// because we avoid any allocations since we're just moving data. + /// Destination must NOT have a hyperlink. + fn moveHyperlink(self: *Page, src: *Cell, dst: *Cell) void { + if (comptime std.debug.runtime_safety) { + assert(src.hyperlink); + assert(!dst.hyperlink); + } + + const src_offset = getOffset(Cell, self.memory, src); + const dst_offset = getOffset(Cell, self.memory, dst); + var map = self.hyperlink_map.map(self.memory); + const entry = map.getEntry(src_offset).?; + const value = entry.value_ptr.*; + map.removeByPtr(entry.key_ptr); + map.putAssumeCapacity(dst_offset, value); + } + /// Append a codepoint to the given cell as a grapheme. pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) Allocator.Error!void { defer self.assertIntegrity();