mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #1929 from qwerasd205/reflow-fix
Reflow rework, various fixes
This commit is contained in:
@ -1753,7 +1753,7 @@ pub fn scrollCallback(
|
||||
};
|
||||
};
|
||||
|
||||
log.info("scroll: delta_y={} delta_x={}", .{ y.delta, x.delta });
|
||||
// log.info("scroll: delta_y={} delta_x={}", .{ y.delta, x.delta });
|
||||
|
||||
{
|
||||
self.renderer_state.mutex.lock();
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2337,6 +2337,7 @@ pub fn dumpString(
|
||||
opts: DumpString,
|
||||
) !void {
|
||||
var blank_rows: usize = 0;
|
||||
var blank_cells: usize = 0;
|
||||
|
||||
var iter = opts.tl.rowIterator(.right_down, opts.br);
|
||||
while (iter.next()) |row_offset| {
|
||||
@ -2363,7 +2364,12 @@ pub fn dumpString(
|
||||
blank_rows += 1;
|
||||
}
|
||||
|
||||
var blank_cells: usize = 0;
|
||||
if (!row.wrap_continuation or !opts.unwrap) {
|
||||
// We should also reset blank cell counts at the start of each row
|
||||
// unless we're unwrapping and this row is a wrap continuation.
|
||||
blank_cells = 0;
|
||||
}
|
||||
|
||||
for (cells) |*cell| {
|
||||
// Skip spacers
|
||||
switch (cell.wide) {
|
||||
@ -2379,7 +2385,7 @@ pub fn dumpString(
|
||||
continue;
|
||||
}
|
||||
if (blank_cells > 0) {
|
||||
for (0..blank_cells) |_| try writer.writeByte(' ');
|
||||
try writer.writeByteNTimes(' ', blank_cells);
|
||||
blank_cells = 0;
|
||||
}
|
||||
|
||||
|
@ -343,7 +343,7 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
if (self.screen.cursor.x == right_limit - 1) {
|
||||
if (!self.modes.get(.wraparound)) return;
|
||||
self.printCell(
|
||||
' ',
|
||||
0,
|
||||
if (right_limit == self.cols) .spacer_head else .narrow,
|
||||
);
|
||||
try self.printWrap();
|
||||
@ -353,7 +353,7 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
|
||||
// Write our spacer
|
||||
self.screen.cursorRight(1);
|
||||
self.printCell(' ', .spacer_tail);
|
||||
self.printCell(0, .spacer_tail);
|
||||
|
||||
// Move the cursor again so we're beyond our spacer
|
||||
if (self.screen.cursor.x == right_limit - 1) {
|
||||
@ -478,19 +478,19 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
// We only create a spacer head if we're at the real edge
|
||||
// of the screen. Otherwise, we clear the space with a narrow.
|
||||
// This allows soft wrapping to work correctly.
|
||||
self.printCell(' ', if (right_limit == self.cols) .spacer_head else .narrow);
|
||||
self.printCell(0, if (right_limit == self.cols) .spacer_head else .narrow);
|
||||
try self.printWrap();
|
||||
}
|
||||
|
||||
self.screen.cursorMarkDirty();
|
||||
self.printCell(c, .wide);
|
||||
self.screen.cursorRight(1);
|
||||
self.printCell(' ', .spacer_tail);
|
||||
self.printCell(0, .spacer_tail);
|
||||
} else {
|
||||
// This is pretty broken, terminals should never be only 1-wide.
|
||||
// We sould prevent this downstream.
|
||||
self.screen.cursorMarkDirty();
|
||||
self.printCell(' ', .narrow);
|
||||
self.printCell(0, .narrow);
|
||||
},
|
||||
|
||||
else => unreachable,
|
||||
@ -2777,7 +2777,7 @@ test "Terminal: print wide char in single-width terminal" {
|
||||
{
|
||||
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, ' '), cell.content.codepoint);
|
||||
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
|
||||
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
|
||||
}
|
||||
|
||||
@ -2928,7 +2928,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
|
||||
{
|
||||
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, ' '), cell.content.codepoint);
|
||||
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
|
||||
try testing.expect(!cell.hasGrapheme());
|
||||
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
|
||||
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null);
|
||||
@ -2945,7 +2945,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
|
||||
{
|
||||
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, ' '), cell.content.codepoint);
|
||||
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
|
||||
try testing.expect(!cell.hasGrapheme());
|
||||
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
|
||||
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null);
|
||||
@ -2961,7 +2961,7 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" {
|
||||
{
|
||||
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 5, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, ' '), cell.content.codepoint);
|
||||
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
|
||||
try testing.expect(!cell.hasGrapheme());
|
||||
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
|
||||
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null);
|
||||
@ -3091,7 +3091,7 @@ test "Terminal: print multicodepoint grapheme, mode 2027" {
|
||||
{
|
||||
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, ' '), cell.content.codepoint);
|
||||
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
|
||||
try testing.expect(!cell.hasGrapheme());
|
||||
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
|
||||
}
|
||||
@ -3780,7 +3780,7 @@ test "Terminal: print wide char at right margin does not create spacer head" {
|
||||
{
|
||||
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, ' '), cell.content.codepoint);
|
||||
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
|
||||
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
|
||||
|
||||
const row = list_cell.row;
|
||||
@ -7431,8 +7431,8 @@ test "Terminal: deleteLines wide character spacer head" {
|
||||
defer testing.allocator.free(str);
|
||||
const unwrapped_str = try t.plainStringUnwrapped(testing.allocator);
|
||||
defer testing.allocator.free(unwrapped_str);
|
||||
try testing.expectEqualStrings("BBBB \n\u{1F600}CCC", str);
|
||||
try testing.expectEqualStrings("BBBB \n\u{1F600}CCC", unwrapped_str);
|
||||
try testing.expectEqualStrings("BBBB\n\u{1F600}CCC", str);
|
||||
try testing.expectEqualStrings("BBBB\n\u{1F600}CCC", unwrapped_str);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7472,7 +7472,7 @@ test "Terminal: deleteLines wide character spacer head left scroll margin" {
|
||||
defer testing.allocator.free(str);
|
||||
const unwrapped_str = try t.plainStringUnwrapped(testing.allocator);
|
||||
defer testing.allocator.free(unwrapped_str);
|
||||
try testing.expectEqualStrings("AABB \nBBCCC\n\u{1F600}", str);
|
||||
try testing.expectEqualStrings("AABB\nBBCCC\n\u{1F600}", str);
|
||||
try testing.expectEqualStrings("AABB BBCCC\u{1F600}", unwrapped_str);
|
||||
}
|
||||
}
|
||||
@ -7513,7 +7513,7 @@ test "Terminal: deleteLines wide character spacer head right scroll margin" {
|
||||
defer testing.allocator.free(str);
|
||||
const unwrapped_str = try t.plainStringUnwrapped(testing.allocator);
|
||||
defer testing.allocator.free(unwrapped_str);
|
||||
try testing.expectEqualStrings("BBBBA\n\u{1F600}CC \n C", str);
|
||||
try testing.expectEqualStrings("BBBBA\n\u{1F600}CC\n C", str);
|
||||
try testing.expectEqualStrings("BBBBA\u{1F600}CC C", unwrapped_str);
|
||||
}
|
||||
}
|
||||
@ -7597,7 +7597,7 @@ test "Terminal: deleteLines wide character spacer head left (< 2) and right scro
|
||||
defer testing.allocator.free(str);
|
||||
const unwrapped_str = try t.plainStringUnwrapped(testing.allocator);
|
||||
defer testing.allocator.free(unwrapped_str);
|
||||
try testing.expectEqualStrings("ABBBA\nB CC \n C", str);
|
||||
try testing.expectEqualStrings("ABBBA\nB CC\n C", str);
|
||||
try testing.expectEqualStrings("ABBBAB CC C", unwrapped_str);
|
||||
}
|
||||
}
|
||||
@ -7633,7 +7633,7 @@ test "Terminal: deleteLines wide characters split by left/right scroll region bo
|
||||
{
|
||||
const str = try t.plainString(testing.allocator);
|
||||
defer testing.allocator.free(str);
|
||||
try testing.expectEqualStrings("A B A\n ", str);
|
||||
try testing.expectEqualStrings("A B A", str);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8600,7 +8600,7 @@ test "Terminal: deleteChars split wide character from end" {
|
||||
{
|
||||
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, ' '), cell.content.codepoint);
|
||||
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
|
||||
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
|
||||
}
|
||||
}
|
||||
|
@ -240,20 +240,28 @@ fn findFreeChunks(bitmaps: []u64, n: usize) ?usize {
|
||||
assert(n <= @bitSizeOf(u64));
|
||||
for (bitmaps, 0..) |*bitmap, idx| {
|
||||
// Shift the bitmap to find `n` sequential free chunks.
|
||||
// EXAMPLE:
|
||||
// n = 4
|
||||
// shifted = 001111001011110010
|
||||
// & 000111100101111001
|
||||
// & 000011110010111100
|
||||
// & 000001111001011110
|
||||
// = 000001000000010000
|
||||
// ^ ^
|
||||
// In this example there are 2 places with at least 4 sequential 1s.
|
||||
var shifted: u64 = bitmap.*;
|
||||
for (1..n) |i| shifted &= bitmap.* >> @intCast(i);
|
||||
|
||||
// If we have zero then we have no matches
|
||||
if (shifted == 0) continue;
|
||||
|
||||
// Trailing zeroes gets us the bit 1-indexed
|
||||
// Trailing zeroes gets us the index of the first bit index with at
|
||||
// least `n` sequential 1s. In the example above, that would be `4`.
|
||||
const bit = @ctz(shifted);
|
||||
|
||||
// Calculate the mask so we can mark it as used
|
||||
for (0..n) |i| {
|
||||
const mask = @as(u64, 1) << @intCast(bit + i);
|
||||
const mask = (@as(u64, std.math.maxInt(u64)) >> @intCast(64 - n)) << @intCast(bit);
|
||||
bitmap.* ^= mask;
|
||||
}
|
||||
|
||||
return (idx * 64) + bit;
|
||||
}
|
||||
|
@ -684,11 +684,12 @@ pub const Page = struct {
|
||||
// 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(
|
||||
const dst_id = try self.hyperlink_set.addWithIdContext(
|
||||
self.memory,
|
||||
try other_link.dupe(other, self),
|
||||
id,
|
||||
.{ .page = self },
|
||||
);
|
||||
) orelse id;
|
||||
try self.setHyperlink(dst_row, dst_cell, dst_id);
|
||||
}
|
||||
if (src_cell.style_id != style.default_id) {
|
||||
@ -705,13 +706,11 @@ 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(
|
||||
dst_cell.style_id = try self.styles.addWithId(
|
||||
self.memory,
|
||||
other_style.*,
|
||||
src_cell.style_id,
|
||||
)) |id| {
|
||||
dst_cell.style_id = id;
|
||||
}
|
||||
) orelse src_cell.style_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -942,7 +941,7 @@ pub const Page = struct {
|
||||
map.removeByPtr(entry.key_ptr);
|
||||
cell.hyperlink = false;
|
||||
|
||||
// Mark that we no longer have graphemes, also search the row
|
||||
// Mark that we no longer have hyperlinks, also search the row
|
||||
// to make sure its state is correct.
|
||||
const cells = row.cells.ptr(self.memory)[0..self.size.cols];
|
||||
for (cells) |c| if (c.hyperlink) return;
|
||||
@ -992,6 +991,45 @@ pub const Page = struct {
|
||||
map.putAssumeCapacity(dst_offset, value);
|
||||
}
|
||||
|
||||
/// Returns the number of hyperlinks in the page. This isn't the byte
|
||||
/// size but the total number of unique cells that have hyperlink data.
|
||||
pub fn hyperlinkCount(self: *const Page) usize {
|
||||
return self.hyperlink_map.map(self.memory).count();
|
||||
}
|
||||
|
||||
/// Returns the hyperlink capacity for the page. This isn't the byte
|
||||
/// size but the number of unique cells that can have hyperlink data.
|
||||
pub fn hyperlinkCapacity(self: *const Page) usize {
|
||||
return self.hyperlink_map.map(self.memory).capacity();
|
||||
}
|
||||
|
||||
/// Set the graphemes for the given cell. This asserts that the cell
|
||||
/// has no graphemes set, and only contains a single codepoint.
|
||||
pub fn setGraphemes(self: *Page, row: *Row, cell: *Cell, cps: []u21) Allocator.Error!void {
|
||||
defer self.assertIntegrity();
|
||||
|
||||
assert(cell.hasText());
|
||||
assert(cell.content_tag == .codepoint);
|
||||
|
||||
const cell_offset = getOffset(Cell, self.memory, cell);
|
||||
var map = self.grapheme_map.map(self.memory);
|
||||
|
||||
const slice = try self.grapheme_alloc.alloc(u21, self.memory, cps.len);
|
||||
errdefer self.grapheme_alloc.free(self.memory, slice);
|
||||
@memcpy(slice, cps);
|
||||
|
||||
try map.putNoClobber(cell_offset, .{
|
||||
.offset = getOffset(u21, self.memory, @ptrCast(slice.ptr)),
|
||||
.len = slice.len,
|
||||
});
|
||||
errdefer map.remove(cell_offset);
|
||||
|
||||
cell.content_tag = .codepoint_grapheme;
|
||||
row.grapheme = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/// 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();
|
||||
@ -1114,6 +1152,12 @@ pub const Page = struct {
|
||||
return self.grapheme_map.map(self.memory).count();
|
||||
}
|
||||
|
||||
/// Returns the grapheme capacity for the page. This isn't the byte
|
||||
/// size but the number of unique cells that can have grapheme data.
|
||||
pub fn graphemeCapacity(self: *const Page) usize {
|
||||
return self.grapheme_map.map(self.memory).capacity();
|
||||
}
|
||||
|
||||
/// Returns the bitset for the dirty bits on this page.
|
||||
///
|
||||
/// The returned value is a DynamicBitSetUnmanaged but it is NOT
|
||||
|
@ -232,6 +232,7 @@ pub fn RefCountedSet(
|
||||
// we're reusing the existing value in the set. This allows
|
||||
// callers to clean up any resources associated with the value.
|
||||
if (comptime @hasDecl(Context, "deleted")) ctx.deleted(value);
|
||||
|
||||
items[id].meta.ref += 1;
|
||||
return id;
|
||||
}
|
||||
@ -279,6 +280,8 @@ pub fn RefCountedSet(
|
||||
pub fn addWithIdContext(self: *Self, base: anytype, value: T, id: Id, ctx: Context) AddError!?Id {
|
||||
const items = self.items.ptr(base);
|
||||
|
||||
assert(id > 0);
|
||||
|
||||
if (id < self.next_id) {
|
||||
if (items[id].meta.ref == 0) {
|
||||
self.deleteItem(base, id, ctx);
|
||||
@ -291,6 +294,11 @@ pub fn RefCountedSet(
|
||||
|
||||
return if (added_id == id) null else added_id;
|
||||
} else if (ctx.eql(value, items[id].value)) {
|
||||
// Notify the context that the value is "deleted" because
|
||||
// we're reusing the existing value in the set. This allows
|
||||
// callers to clean up any resources associated with the value.
|
||||
if (comptime @hasDecl(Context, "deleted")) ctx.deleted(value);
|
||||
|
||||
items[id].meta.ref += 1;
|
||||
|
||||
return null;
|
||||
@ -501,6 +509,7 @@ pub fn RefCountedSet(
|
||||
// we're reusing the existing value in the set. This allows
|
||||
// callers to clean up any resources associated with the value.
|
||||
if (comptime @hasDecl(Context, "deleted")) ctx.deleted(value);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user