mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
Merge pull request #2522 from ghostty-org/push-myqrrtsnwmum
terminal: refactor hyperlink memory management
This commit is contained in:
@ -125,7 +125,7 @@ pub const Cursor = struct {
|
||||
/// the cursor page pin changes. We can't get it from the old screen
|
||||
/// state because the page may be cleared. This is heap allocated
|
||||
/// because its most likely null.
|
||||
hyperlink: ?*Hyperlink = null,
|
||||
hyperlink: ?*hyperlink.Hyperlink = null,
|
||||
|
||||
/// The pointers into the page list where the cursor is currently
|
||||
/// located. This makes it faster to move the cursor.
|
||||
@ -134,7 +134,10 @@ pub const Cursor = struct {
|
||||
page_cell: *pagepkg.Cell,
|
||||
|
||||
pub fn deinit(self: *Cursor, alloc: Allocator) void {
|
||||
if (self.hyperlink) |link| link.destroy(alloc);
|
||||
if (self.hyperlink) |link| {
|
||||
link.deinit(alloc);
|
||||
alloc.destroy(link);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -182,31 +185,6 @@ pub const CharsetState = struct {
|
||||
const CharsetArray = std.EnumArray(charsets.Slots, charsets.Charset);
|
||||
};
|
||||
|
||||
pub const Hyperlink = struct {
|
||||
id: ?[]const u8,
|
||||
uri: []const u8,
|
||||
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
uri: []const u8,
|
||||
id: ?[]const u8,
|
||||
) !*Hyperlink {
|
||||
const self = try alloc.create(Hyperlink);
|
||||
errdefer alloc.destroy(self);
|
||||
self.id = if (id) |v| try alloc.dupe(u8, v) else null;
|
||||
errdefer if (self.id) |v| alloc.free(v);
|
||||
self.uri = try alloc.dupe(u8, uri);
|
||||
errdefer alloc.free(self.uri);
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Hyperlink, alloc: Allocator) void {
|
||||
if (self.id) |id| alloc.free(id);
|
||||
alloc.free(self.uri);
|
||||
alloc.destroy(self);
|
||||
}
|
||||
};
|
||||
|
||||
/// Initialize a new screen.
|
||||
///
|
||||
/// max_scrollback is the amount of scrollback to keep in bytes. This
|
||||
@ -471,10 +449,11 @@ pub fn adjustCapacity(
|
||||
self.cursor.hyperlink = null;
|
||||
|
||||
// Re-add
|
||||
self.startHyperlinkOnce(link.uri, link.id) catch unreachable;
|
||||
self.startHyperlinkOnce(link.*) catch unreachable;
|
||||
|
||||
// Remove our old link
|
||||
link.destroy(self.alloc);
|
||||
link.deinit(self.alloc);
|
||||
self.alloc.destroy(link);
|
||||
}
|
||||
|
||||
// Reload the cursor information because the pin changed.
|
||||
@ -1023,7 +1002,10 @@ fn cursorChangePin(self: *Screen, new: Pin) void {
|
||||
self.cursor.hyperlink = null;
|
||||
|
||||
// Re-add
|
||||
self.startHyperlink(link.uri, link.id) catch |err| {
|
||||
self.startHyperlink(link.uri, switch (link.id) {
|
||||
.explicit => |v| v,
|
||||
.implicit => null,
|
||||
}) catch |err| {
|
||||
// This shouldn't happen because startHyperlink should handle
|
||||
// resizing. This only happens if we're truly out of RAM. Degrade
|
||||
// to forgetting the hyperlink.
|
||||
@ -1031,7 +1013,8 @@ fn cursorChangePin(self: *Screen, new: Pin) void {
|
||||
};
|
||||
|
||||
// Remove our old link
|
||||
link.destroy(self.alloc);
|
||||
link.deinit(self.alloc);
|
||||
self.alloc.destroy(link);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1550,7 +1533,10 @@ fn resizeInternal(
|
||||
|
||||
// Fix up our hyperlink if we had one.
|
||||
if (hyperlink_) |link| {
|
||||
self.startHyperlink(link.uri, link.id) catch |err| {
|
||||
self.startHyperlink(link.uri, switch (link.id) {
|
||||
.explicit => |v| v,
|
||||
.implicit => null,
|
||||
}) catch |err| {
|
||||
// This shouldn't happen because startHyperlink should handle
|
||||
// resizing. This only happens if we're truly out of RAM. Degrade
|
||||
// to forgetting the hyperlink.
|
||||
@ -1558,7 +1544,8 @@ fn resizeInternal(
|
||||
};
|
||||
|
||||
// Remove our old link
|
||||
link.destroy(self.alloc);
|
||||
link.deinit(self.alloc);
|
||||
self.alloc.destroy(link);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1805,6 +1792,8 @@ pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void {
|
||||
};
|
||||
}
|
||||
|
||||
pub const StartHyperlinkError = Allocator.Error || PageList.AdjustCapacityError;
|
||||
|
||||
/// Start the hyperlink state. Future cells will be marked as hyperlinks with
|
||||
/// this state. Note that various terminal operations may clear the hyperlink
|
||||
/// state, such as switching screens (alt screen).
|
||||
@ -1812,14 +1801,29 @@ pub fn startHyperlink(
|
||||
self: *Screen,
|
||||
uri: []const u8,
|
||||
id_: ?[]const u8,
|
||||
) !void {
|
||||
) StartHyperlinkError!void {
|
||||
// Create our pending entry.
|
||||
const link: hyperlink.Hyperlink = .{
|
||||
.uri = uri,
|
||||
.id = if (id_) |id| .{
|
||||
.explicit = id,
|
||||
} else implicit: {
|
||||
defer self.cursor.hyperlink_implicit_id += 1;
|
||||
break :implicit .{ .implicit = self.cursor.hyperlink_implicit_id };
|
||||
},
|
||||
};
|
||||
errdefer switch (link.id) {
|
||||
.explicit => {},
|
||||
.implicit => self.cursor.hyperlink_implicit_id -= 1,
|
||||
};
|
||||
|
||||
// Loop until we have enough page memory to add the hyperlink
|
||||
while (true) {
|
||||
if (self.startHyperlinkOnce(uri, id_)) {
|
||||
if (self.startHyperlinkOnce(link)) {
|
||||
return;
|
||||
} else |err| switch (err) {
|
||||
// An actual self.alloc OOM is a fatal error.
|
||||
error.RealOutOfMemory => return error.OutOfMemory,
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
|
||||
// strings table is out of memory, adjust it up
|
||||
error.StringsOutOfMemory => _ = try self.adjustCapacity(
|
||||
@ -1849,74 +1853,21 @@ pub fn startHyperlink(
|
||||
/// all the previous state and try again.
|
||||
fn startHyperlinkOnce(
|
||||
self: *Screen,
|
||||
uri: []const u8,
|
||||
id_: ?[]const u8,
|
||||
) !void {
|
||||
source: hyperlink.Hyperlink,
|
||||
) (Allocator.Error || Page.InsertHyperlinkError)!void {
|
||||
// End any prior hyperlink
|
||||
self.endHyperlink();
|
||||
|
||||
// Create our hyperlink state.
|
||||
const link = Hyperlink.create(self.alloc, uri, id_) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.RealOutOfMemory,
|
||||
};
|
||||
errdefer link.destroy(self.alloc);
|
||||
// Allocate our new Hyperlink entry in non-page memory. This
|
||||
// lets us quickly get access to URI, ID.
|
||||
const link = try self.alloc.create(hyperlink.Hyperlink);
|
||||
errdefer self.alloc.destroy(link);
|
||||
link.* = try source.dupe(self.alloc);
|
||||
errdefer link.deinit(self.alloc);
|
||||
|
||||
// Copy our URI into the page memory.
|
||||
// Insert the hyperlink into page memory
|
||||
var page = &self.cursor.page_pin.page.data;
|
||||
const string_alloc = &page.string_alloc;
|
||||
const page_uri: Offset(u8).Slice = uri: {
|
||||
const buf = string_alloc.alloc(u8, page.memory, uri.len) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.StringsOutOfMemory,
|
||||
};
|
||||
errdefer string_alloc.free(page.memory, buf);
|
||||
@memcpy(buf, uri);
|
||||
|
||||
break :uri .{
|
||||
.offset = size.getOffset(u8, page.memory, &buf[0]),
|
||||
.len = uri.len,
|
||||
};
|
||||
};
|
||||
errdefer string_alloc.free(
|
||||
page.memory,
|
||||
page_uri.offset.ptr(page.memory)[0..page_uri.len],
|
||||
);
|
||||
|
||||
// Copy our ID into page memory or create an implicit ID via the counter
|
||||
const page_id: hyperlink.Hyperlink.Id = if (id_) |id| explicit: {
|
||||
const buf = string_alloc.alloc(u8, page.memory, id.len) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.StringsOutOfMemory,
|
||||
};
|
||||
errdefer string_alloc.free(page.memory, buf);
|
||||
@memcpy(buf, id);
|
||||
|
||||
break :explicit .{
|
||||
.explicit = .{
|
||||
.offset = size.getOffset(u8, page.memory, &buf[0]),
|
||||
.len = id.len,
|
||||
},
|
||||
};
|
||||
} else implicit: {
|
||||
defer self.cursor.hyperlink_implicit_id += 1;
|
||||
break :implicit .{ .implicit = self.cursor.hyperlink_implicit_id };
|
||||
};
|
||||
errdefer switch (page_id) {
|
||||
.implicit => self.cursor.hyperlink_implicit_id -= 1,
|
||||
.explicit => |slice| string_alloc.free(
|
||||
page.memory,
|
||||
slice.offset.ptr(page.memory)[0..slice.len],
|
||||
),
|
||||
};
|
||||
|
||||
// Put our hyperlink into the hyperlink set to get an ID
|
||||
const id = page.hyperlink_set.addContext(
|
||||
page.memory,
|
||||
.{ .id = page_id, .uri = page_uri },
|
||||
.{ .page = page },
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.SetOutOfMemory,
|
||||
error.NeedsRehash => return error.SetNeedsRehash,
|
||||
};
|
||||
errdefer page.hyperlink_set.release(page.memory, id);
|
||||
const id: hyperlink.Id = try page.insertHyperlink(link.*);
|
||||
|
||||
// Save it all
|
||||
self.cursor.hyperlink = link;
|
||||
@ -1944,7 +1895,8 @@ pub fn endHyperlink(self: *Screen) void {
|
||||
// will be called.
|
||||
var page = &self.cursor.page_pin.page.data;
|
||||
page.hyperlink_set.release(page.memory, self.cursor.hyperlink_id);
|
||||
self.cursor.hyperlink.?.destroy(self.alloc);
|
||||
self.cursor.hyperlink.?.deinit(self.alloc);
|
||||
self.alloc.destroy(self.cursor.hyperlink.?);
|
||||
self.cursor.hyperlink_id = 0;
|
||||
self.cursor.hyperlink = null;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
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;
|
||||
@ -21,9 +22,63 @@ pub const Id = size.CellCountInt;
|
||||
// the hyperlink ID in the cell itself.
|
||||
pub const Map = AutoOffsetHashMap(Offset(Cell), Id);
|
||||
|
||||
/// The main entry for hyperlinks.
|
||||
/// 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) {
|
||||
@ -37,10 +92,10 @@ pub const Hyperlink = struct {
|
||||
|
||||
/// Duplicate this hyperlink from one page to another.
|
||||
pub fn dupe(
|
||||
self: *const Hyperlink,
|
||||
self: *const PageEntry,
|
||||
self_page: *const Page,
|
||||
dst_page: *Page,
|
||||
) error{OutOfMemory}!Hyperlink {
|
||||
) error{OutOfMemory}!PageEntry {
|
||||
var copy = self.*;
|
||||
|
||||
// If the pages are the same then we can return a shallow copy.
|
||||
@ -85,7 +140,7 @@ pub const Hyperlink = struct {
|
||||
return copy;
|
||||
}
|
||||
|
||||
pub fn hash(self: *const Hyperlink, base: anytype) u64 {
|
||||
pub fn hash(self: *const PageEntry, base: anytype) u64 {
|
||||
var hasher = Wyhash.init(0);
|
||||
autoHash(&hasher, std.meta.activeTag(self.id));
|
||||
switch (self.id) {
|
||||
@ -105,9 +160,9 @@ pub const Hyperlink = struct {
|
||||
}
|
||||
|
||||
pub fn eql(
|
||||
self: *const Hyperlink,
|
||||
self: *const PageEntry,
|
||||
self_base: anytype,
|
||||
other: *const Hyperlink,
|
||||
other: *const PageEntry,
|
||||
other_base: anytype,
|
||||
) bool {
|
||||
if (std.meta.activeTag(self.id) != std.meta.activeTag(other.id)) return false;
|
||||
@ -135,21 +190,21 @@ pub const Hyperlink = struct {
|
||||
/// 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(
|
||||
Hyperlink,
|
||||
PageEntry,
|
||||
Id,
|
||||
size.CellCountInt,
|
||||
struct {
|
||||
page: ?*Page = null,
|
||||
|
||||
pub fn hash(self: *const @This(), link: Hyperlink) u64 {
|
||||
pub fn hash(self: *const @This(), link: PageEntry) u64 {
|
||||
return link.hash(self.page.?.memory);
|
||||
}
|
||||
|
||||
pub fn eql(self: *const @This(), a: Hyperlink, b: Hyperlink) bool {
|
||||
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: Hyperlink) void {
|
||||
pub fn deleted(self: *const @This(), link: PageEntry) void {
|
||||
const page = self.page.?;
|
||||
const alloc = &page.string_alloc;
|
||||
switch (link.id) {
|
||||
|
@ -808,7 +808,7 @@ pub const Page = struct {
|
||||
|
||||
// If our page can't support an additional cell with
|
||||
// a hyperlink then we have to return an error.
|
||||
if (self.hyperlinkCount() >= self.hyperlinkCapacity() - 1) {
|
||||
if (self.hyperlinkCount() >= self.hyperlinkCapacity()) {
|
||||
// The hyperlink map capacity needs to be increased.
|
||||
return error.HyperlinkMapOutOfMemory;
|
||||
}
|
||||
@ -1142,6 +1142,101 @@ pub const Page = struct {
|
||||
row.hyperlink = false;
|
||||
}
|
||||
|
||||
pub const InsertHyperlinkError = error{
|
||||
/// string_alloc errors
|
||||
StringsOutOfMemory,
|
||||
|
||||
/// hyperlink_set errors
|
||||
SetOutOfMemory,
|
||||
SetNeedsRehash,
|
||||
};
|
||||
|
||||
/// Convert a hyperlink into a page entry, returning the ID.
|
||||
///
|
||||
/// This does not de-dupe any strings, so if the URI, explicit ID,
|
||||
/// etc. is already in the strings table this will duplicate it.
|
||||
///
|
||||
/// To release the memory associated with the given hyperlink,
|
||||
/// release the ID from the `hyperlink_set`. If the refcount reaches
|
||||
/// zero and the slot is needed then the context will reap the
|
||||
/// memory.
|
||||
pub fn insertHyperlink(
|
||||
self: *Page,
|
||||
link: hyperlink.Hyperlink,
|
||||
) InsertHyperlinkError!hyperlink.Id {
|
||||
// Insert our URI into the page strings table.
|
||||
const page_uri: Offset(u8).Slice = uri: {
|
||||
const buf = self.string_alloc.alloc(
|
||||
u8,
|
||||
self.memory,
|
||||
link.uri.len,
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.StringsOutOfMemory,
|
||||
};
|
||||
errdefer self.string_alloc.free(self.memory, buf);
|
||||
@memcpy(buf, link.uri);
|
||||
|
||||
break :uri .{
|
||||
.offset = size.getOffset(u8, self.memory, &buf[0]),
|
||||
.len = link.uri.len,
|
||||
};
|
||||
};
|
||||
errdefer self.string_alloc.free(
|
||||
self.memory,
|
||||
page_uri.offset.ptr(self.memory)[0..page_uri.len],
|
||||
);
|
||||
|
||||
// Allocate an ID for our page memory if we have to.
|
||||
const page_id: hyperlink.PageEntry.Id = switch (link.id) {
|
||||
.explicit => |id| explicit: {
|
||||
const buf = self.string_alloc.alloc(
|
||||
u8,
|
||||
self.memory,
|
||||
id.len,
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.StringsOutOfMemory,
|
||||
};
|
||||
errdefer self.string_alloc.free(self.memory, buf);
|
||||
@memcpy(buf, id);
|
||||
|
||||
break :explicit .{
|
||||
.explicit = .{
|
||||
.offset = size.getOffset(u8, self.memory, &buf[0]),
|
||||
.len = id.len,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
.implicit => |id| .{ .implicit = id },
|
||||
};
|
||||
errdefer switch (page_id) {
|
||||
.implicit => {},
|
||||
.explicit => |slice| self.string_alloc.free(
|
||||
self.memory,
|
||||
slice.offset.ptr(self.memory)[0..slice.len],
|
||||
),
|
||||
};
|
||||
|
||||
// Build our entry
|
||||
const entry: hyperlink.PageEntry = .{
|
||||
.id = page_id,
|
||||
.uri = page_uri,
|
||||
};
|
||||
|
||||
// Put our hyperlink into the hyperlink set to get an ID
|
||||
const id = self.hyperlink_set.addContext(
|
||||
self.memory,
|
||||
entry,
|
||||
.{ .page = self },
|
||||
) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.SetOutOfMemory,
|
||||
error.NeedsRehash => return error.SetNeedsRehash,
|
||||
};
|
||||
errdefer self.hyperlink_set.release(self.memory, id);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Set the hyperlink for the given cell. If the cell already has a
|
||||
/// hyperlink, then this will handle memory management and refcount
|
||||
/// update for the prior hyperlink.
|
||||
@ -2237,6 +2332,50 @@ test "Page cloneFrom partial" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Page cloneFrom hyperlinks exact capacity" {
|
||||
var page = try Page.init(.{
|
||||
.cols = 50,
|
||||
.rows = 50,
|
||||
});
|
||||
defer page.deinit();
|
||||
|
||||
// Ensure our page can accommodate the capacity.
|
||||
const hyperlink_cap = page.hyperlinkCapacity();
|
||||
try testing.expect(hyperlink_cap <= page.size.cols * page.size.rows);
|
||||
|
||||
// Create a hyperlink.
|
||||
const hyperlink_id = try page.insertHyperlink(.{
|
||||
.id = .{ .implicit = 0 },
|
||||
.uri = "https://example.com",
|
||||
});
|
||||
|
||||
// Fill the exact cap with cells.
|
||||
fill: for (0..page.size.cols) |x| {
|
||||
for (0..page.size.rows) |y| {
|
||||
const rac = page.getRowAndCell(x, y);
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = 42 },
|
||||
};
|
||||
try page.setHyperlink(rac.row, rac.cell, hyperlink_id);
|
||||
page.hyperlink_set.use(page.memory, hyperlink_id);
|
||||
|
||||
if (page.hyperlinkCount() == hyperlink_cap) {
|
||||
break :fill;
|
||||
}
|
||||
}
|
||||
}
|
||||
try testing.expectEqual(page.hyperlinkCount(), page.hyperlinkCapacity());
|
||||
|
||||
// Clone the full page
|
||||
var page2 = try Page.init(page.capacity);
|
||||
defer page2.deinit();
|
||||
try page2.cloneFrom(&page, 0, page.size.rows);
|
||||
|
||||
// We should have the same number of hyperlinks
|
||||
try testing.expectEqual(page2.hyperlinkCount(), page.hyperlinkCount());
|
||||
}
|
||||
|
||||
test "Page cloneFrom graphemes" {
|
||||
var page = try Page.init(.{
|
||||
.cols = 10,
|
||||
|
Reference in New Issue
Block a user