mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
terminal: handle moving/swapping/clearing cells with hyperlinks
This commit is contained in:
@ -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;
|
||||
|
@ -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 });
|
||||
|
@ -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();
|
||||
|
Reference in New Issue
Block a user