mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-24 04:36:10 +03:00

Fixes #2500 Based on #2508 This separates out the concept of a "hyperlink" from a "hyperlink page entry." The difference is that the former has real Zig slices into things like strings and the latter has offsets into terminal page memory. From this separation, the Page structure now has an `insertHyperlink` function that takes a hyperlink and converts it to a page entry. This does a couple things: (1) it moves page memory management out of Screen and into Page which is historically more appropriate and (2) it let's us more easily test hyperlinks from the Page unit tests. Finally, this PR makes some error sets explicit.
224 lines
7.2 KiB
Zig
224 lines
7.2 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const assert = std.debug.assert;
|
|
const hash_map = @import("hash_map.zig");
|
|
const AutoOffsetHashMap = hash_map.AutoOffsetHashMap;
|
|
const pagepkg = @import("page.zig");
|
|
const size = @import("size.zig");
|
|
const Offset = size.Offset;
|
|
const Cell = pagepkg.Cell;
|
|
const Page = pagepkg.Page;
|
|
const RefCountedSet = @import("ref_counted_set.zig").RefCountedSet;
|
|
const Wyhash = std.hash.Wyhash;
|
|
const autoHash = std.hash.autoHash;
|
|
const autoHashStrat = std.hash.autoHashStrat;
|
|
|
|
/// The unique identifier for a hyperlink. This is at most the number of cells
|
|
/// that can fit in a single terminal page.
|
|
pub const Id = size.CellCountInt;
|
|
|
|
// The mapping of cell to hyperlink. We use an offset hash map to save space
|
|
// since its very unlikely a cell is a hyperlink, so its a waste to store
|
|
// the hyperlink ID in the cell itself.
|
|
pub const Map = AutoOffsetHashMap(Offset(Cell), Id);
|
|
|
|
/// A fully decoded hyperlink that may or may not have its
|
|
/// memory within a page. The memory location of this is dependent
|
|
/// on the context so users should check with the source of the
|
|
/// hyperlink.
|
|
pub const Hyperlink = struct {
|
|
id: Hyperlink.Id,
|
|
uri: []const u8,
|
|
|
|
/// See PageEntry.Id
|
|
pub const Id = union(enum) {
|
|
explicit: []const u8,
|
|
implicit: size.OffsetInt,
|
|
};
|
|
|
|
/// Deinit and deallocate all the pointers using the given
|
|
/// allocator.
|
|
///
|
|
/// WARNING: This should only be called if the hyperlink was
|
|
/// heap-allocated. This DOES NOT need to be unconditionally
|
|
/// called.
|
|
pub fn deinit(self: *const Hyperlink, alloc: Allocator) void {
|
|
alloc.free(self.uri);
|
|
switch (self.id) {
|
|
.implicit => {},
|
|
.explicit => |v| alloc.free(v),
|
|
}
|
|
}
|
|
|
|
/// Duplicate a hyperlink by allocating all values with the
|
|
/// given allocator. The returned hyperlink should have deinit
|
|
/// called.
|
|
pub fn dupe(
|
|
self: *const Hyperlink,
|
|
alloc: Allocator,
|
|
) Allocator.Error!Hyperlink {
|
|
const uri = try alloc.dupe(u8, self.uri);
|
|
errdefer alloc.free(uri);
|
|
|
|
const id: Hyperlink.Id = switch (self.id) {
|
|
.implicit => self.id,
|
|
.explicit => |v| .{ .explicit = try alloc.dupe(u8, v) },
|
|
};
|
|
errdefer switch (id) {
|
|
.implicit => {},
|
|
.explicit => |v| alloc.free(v),
|
|
};
|
|
|
|
return .{ .id = id, .uri = uri };
|
|
}
|
|
};
|
|
|
|
/// A hyperlink that has been committed to page memory. This
|
|
/// is a "page entry" because while it represents a hyperlink,
|
|
/// some decoding (pointer chasing) is still necessary to get the
|
|
/// fully realized ID, URI, etc.
|
|
pub const PageEntry = struct {
|
|
id: PageEntry.Id,
|
|
uri: Offset(u8).Slice,
|
|
|
|
pub const Id = union(enum) {
|
|
/// An explicitly provided ID via the OSC8 sequence.
|
|
explicit: Offset(u8).Slice,
|
|
|
|
/// No ID was provided so we auto-generate the ID based on an
|
|
/// incrementing counter attached to the screen.
|
|
implicit: size.OffsetInt,
|
|
};
|
|
|
|
/// Duplicate this hyperlink from one page to another.
|
|
pub fn dupe(
|
|
self: *const PageEntry,
|
|
self_page: *const Page,
|
|
dst_page: *Page,
|
|
) error{OutOfMemory}!PageEntry {
|
|
var copy = self.*;
|
|
|
|
// If the pages are the same then we can return a shallow copy.
|
|
if (self_page == dst_page) return copy;
|
|
|
|
// Copy the URI
|
|
{
|
|
const uri = self.uri.offset.ptr(self_page.memory)[0..self.uri.len];
|
|
const buf = try dst_page.string_alloc.alloc(u8, dst_page.memory, uri.len);
|
|
@memcpy(buf, uri);
|
|
copy.uri = .{
|
|
.offset = size.getOffset(u8, dst_page.memory, &buf[0]),
|
|
.len = uri.len,
|
|
};
|
|
}
|
|
errdefer dst_page.string_alloc.free(
|
|
dst_page.memory,
|
|
copy.uri.offset.ptr(dst_page.memory)[0..copy.uri.len],
|
|
);
|
|
|
|
// Copy the ID
|
|
switch (copy.id) {
|
|
.implicit => {}, // Shallow is fine
|
|
.explicit => |slice| {
|
|
const id = slice.offset.ptr(self_page.memory)[0..slice.len];
|
|
const buf = try dst_page.string_alloc.alloc(u8, dst_page.memory, id.len);
|
|
@memcpy(buf, id);
|
|
copy.id = .{ .explicit = .{
|
|
.offset = size.getOffset(u8, dst_page.memory, &buf[0]),
|
|
.len = id.len,
|
|
} };
|
|
},
|
|
}
|
|
errdefer switch (copy.id) {
|
|
.implicit => {},
|
|
.explicit => |v| dst_page.string_alloc.free(
|
|
dst_page.memory,
|
|
v.offset.ptr(dst_page.memory)[0..v.len],
|
|
),
|
|
};
|
|
|
|
return copy;
|
|
}
|
|
|
|
pub fn hash(self: *const PageEntry, base: anytype) u64 {
|
|
var hasher = Wyhash.init(0);
|
|
autoHash(&hasher, std.meta.activeTag(self.id));
|
|
switch (self.id) {
|
|
.implicit => |v| autoHash(&hasher, v),
|
|
.explicit => |slice| autoHashStrat(
|
|
&hasher,
|
|
slice.offset.ptr(base)[0..slice.len],
|
|
.Deep,
|
|
),
|
|
}
|
|
autoHashStrat(
|
|
&hasher,
|
|
self.uri.offset.ptr(base)[0..self.uri.len],
|
|
.Deep,
|
|
);
|
|
return hasher.final();
|
|
}
|
|
|
|
pub fn eql(
|
|
self: *const PageEntry,
|
|
self_base: anytype,
|
|
other: *const PageEntry,
|
|
other_base: anytype,
|
|
) bool {
|
|
if (std.meta.activeTag(self.id) != std.meta.activeTag(other.id)) return false;
|
|
switch (self.id) {
|
|
.implicit => if (self.id.implicit != other.id.implicit) return false,
|
|
.explicit => {
|
|
const self_ptr = self.id.explicit.offset.ptr(self_base);
|
|
const other_ptr = other.id.explicit.offset.ptr(other_base);
|
|
if (!std.mem.eql(
|
|
u8,
|
|
self_ptr[0..self.id.explicit.len],
|
|
other_ptr[0..other.id.explicit.len],
|
|
)) return false;
|
|
},
|
|
}
|
|
|
|
return std.mem.eql(
|
|
u8,
|
|
self.uri.offset.ptr(self_base)[0..self.uri.len],
|
|
other.uri.offset.ptr(other_base)[0..other.uri.len],
|
|
);
|
|
}
|
|
};
|
|
|
|
/// The set of hyperlinks. This is ref-counted so that a set of cells
|
|
/// can share the same hyperlink without duplicating the data.
|
|
pub const Set = RefCountedSet(
|
|
PageEntry,
|
|
Id,
|
|
size.CellCountInt,
|
|
struct {
|
|
page: ?*Page = null,
|
|
|
|
pub fn hash(self: *const @This(), link: PageEntry) u64 {
|
|
return link.hash(self.page.?.memory);
|
|
}
|
|
|
|
pub fn eql(self: *const @This(), a: PageEntry, b: PageEntry) bool {
|
|
return a.eql(self.page.?.memory, &b, self.page.?.memory);
|
|
}
|
|
|
|
pub fn deleted(self: *const @This(), link: PageEntry) void {
|
|
const page = self.page.?;
|
|
const alloc = &page.string_alloc;
|
|
switch (link.id) {
|
|
.implicit => {},
|
|
.explicit => |v| alloc.free(
|
|
page.memory,
|
|
v.offset.ptr(page.memory)[0..v.len],
|
|
),
|
|
}
|
|
alloc.free(
|
|
page.memory,
|
|
link.uri.offset.ptr(page.memory)[0..link.uri.len],
|
|
);
|
|
}
|
|
},
|
|
);
|