Merge pull request #1929 from qwerasd205/reflow-fix

Reflow rework, various fixes
This commit is contained in:
Mitchell Hashimoto
2024-07-09 09:08:45 -07:00
committed by GitHub
7 changed files with 890 additions and 535 deletions

View File

@ -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(); self.renderer_state.mutex.lock();

File diff suppressed because it is too large Load Diff

View File

@ -2337,6 +2337,7 @@ pub fn dumpString(
opts: DumpString, opts: DumpString,
) !void { ) !void {
var blank_rows: usize = 0; var blank_rows: usize = 0;
var blank_cells: usize = 0;
var iter = opts.tl.rowIterator(.right_down, opts.br); var iter = opts.tl.rowIterator(.right_down, opts.br);
while (iter.next()) |row_offset| { while (iter.next()) |row_offset| {
@ -2363,7 +2364,12 @@ pub fn dumpString(
blank_rows += 1; 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| { for (cells) |*cell| {
// Skip spacers // Skip spacers
switch (cell.wide) { switch (cell.wide) {
@ -2379,7 +2385,7 @@ pub fn dumpString(
continue; continue;
} }
if (blank_cells > 0) { if (blank_cells > 0) {
for (0..blank_cells) |_| try writer.writeByte(' '); try writer.writeByteNTimes(' ', blank_cells);
blank_cells = 0; blank_cells = 0;
} }

View File

@ -343,7 +343,7 @@ pub fn print(self: *Terminal, c: u21) !void {
if (self.screen.cursor.x == right_limit - 1) { if (self.screen.cursor.x == right_limit - 1) {
if (!self.modes.get(.wraparound)) return; if (!self.modes.get(.wraparound)) return;
self.printCell( self.printCell(
' ', 0,
if (right_limit == self.cols) .spacer_head else .narrow, if (right_limit == self.cols) .spacer_head else .narrow,
); );
try self.printWrap(); try self.printWrap();
@ -353,7 +353,7 @@ pub fn print(self: *Terminal, c: u21) !void {
// Write our spacer // Write our spacer
self.screen.cursorRight(1); self.screen.cursorRight(1);
self.printCell(' ', .spacer_tail); self.printCell(0, .spacer_tail);
// Move the cursor again so we're beyond our spacer // Move the cursor again so we're beyond our spacer
if (self.screen.cursor.x == right_limit - 1) { 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 // We only create a spacer head if we're at the real edge
// of the screen. Otherwise, we clear the space with a narrow. // of the screen. Otherwise, we clear the space with a narrow.
// This allows soft wrapping to work correctly. // 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(); try self.printWrap();
} }
self.screen.cursorMarkDirty(); self.screen.cursorMarkDirty();
self.printCell(c, .wide); self.printCell(c, .wide);
self.screen.cursorRight(1); self.screen.cursorRight(1);
self.printCell(' ', .spacer_tail); self.printCell(0, .spacer_tail);
} else { } else {
// This is pretty broken, terminals should never be only 1-wide. // This is pretty broken, terminals should never be only 1-wide.
// We sould prevent this downstream. // We sould prevent this downstream.
self.screen.cursorMarkDirty(); self.screen.cursorMarkDirty();
self.printCell(' ', .narrow); self.printCell(0, .narrow);
}, },
else => unreachable, 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 list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
const cell = list_cell.cell; 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); 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 list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell; 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.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null); 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 list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?;
const cell = list_cell.cell; 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.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null); 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 list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 5, .y = 0 } }).?;
const cell = list_cell.cell; 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.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null); 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 list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell; 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.expect(!cell.hasGrapheme());
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); 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 list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?;
const cell = list_cell.cell; 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); try testing.expectEqual(Cell.Wide.narrow, cell.wide);
const row = list_cell.row; const row = list_cell.row;
@ -7431,8 +7431,8 @@ test "Terminal: deleteLines wide character spacer head" {
defer testing.allocator.free(str); defer testing.allocator.free(str);
const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); const unwrapped_str = try t.plainStringUnwrapped(testing.allocator);
defer testing.allocator.free(unwrapped_str); defer testing.allocator.free(unwrapped_str);
try testing.expectEqualStrings("BBBB \n\u{1F600}CCC", 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", unwrapped_str);
} }
} }
@ -7472,7 +7472,7 @@ test "Terminal: deleteLines wide character spacer head left scroll margin" {
defer testing.allocator.free(str); defer testing.allocator.free(str);
const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); const unwrapped_str = try t.plainStringUnwrapped(testing.allocator);
defer testing.allocator.free(unwrapped_str); 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); 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); defer testing.allocator.free(str);
const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); const unwrapped_str = try t.plainStringUnwrapped(testing.allocator);
defer testing.allocator.free(unwrapped_str); 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); 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); defer testing.allocator.free(str);
const unwrapped_str = try t.plainStringUnwrapped(testing.allocator); const unwrapped_str = try t.plainStringUnwrapped(testing.allocator);
defer testing.allocator.free(unwrapped_str); 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); 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); const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str); 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 list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
const cell = list_cell.cell; 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); try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
} }
} }

View File

@ -240,20 +240,28 @@ fn findFreeChunks(bitmaps: []u64, n: usize) ?usize {
assert(n <= @bitSizeOf(u64)); assert(n <= @bitSizeOf(u64));
for (bitmaps, 0..) |*bitmap, idx| { for (bitmaps, 0..) |*bitmap, idx| {
// Shift the bitmap to find `n` sequential free chunks. // 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.*; var shifted: u64 = bitmap.*;
for (1..n) |i| shifted &= bitmap.* >> @intCast(i); for (1..n) |i| shifted &= bitmap.* >> @intCast(i);
// If we have zero then we have no matches // If we have zero then we have no matches
if (shifted == 0) continue; 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); const bit = @ctz(shifted);
// Calculate the mask so we can mark it as used // Calculate the mask so we can mark it as used
for (0..n) |i| { const mask = (@as(u64, std.math.maxInt(u64)) >> @intCast(64 - n)) << @intCast(bit);
const mask = @as(u64, 1) << @intCast(bit + i); bitmap.* ^= mask;
bitmap.* ^= mask;
}
return (idx * 64) + bit; return (idx * 64) + bit;
} }

View File

@ -684,11 +684,12 @@ pub const Page = struct {
// add it, and migrate. // add it, and migrate.
const id = other.lookupHyperlink(src_cell).?; const id = other.lookupHyperlink(src_cell).?;
const other_link = other.hyperlink_set.get(other.memory, id); 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, self.memory,
try other_link.dupe(other, self), try other_link.dupe(other, self),
id,
.{ .page = self }, .{ .page = self },
); ) orelse id;
try self.setHyperlink(dst_row, dst_cell, dst_id); try self.setHyperlink(dst_row, dst_cell, dst_id);
} }
if (src_cell.style_id != style.default_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 // Slow path: Get the style from the other
// page and add it to this page's style set. // page and add it to this page's style set.
const other_style = other.styles.get(other.memory, src_cell.style_id); 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, self.memory,
other_style.*, other_style.*,
src_cell.style_id, src_cell.style_id,
)) |id| { ) orelse src_cell.style_id;
dst_cell.style_id = id;
}
} }
} }
} }
@ -942,7 +941,7 @@ pub const Page = struct {
map.removeByPtr(entry.key_ptr); map.removeByPtr(entry.key_ptr);
cell.hyperlink = false; 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. // to make sure its state is correct.
const cells = row.cells.ptr(self.memory)[0..self.size.cols]; const cells = row.cells.ptr(self.memory)[0..self.size.cols];
for (cells) |c| if (c.hyperlink) return; for (cells) |c| if (c.hyperlink) return;
@ -992,6 +991,45 @@ pub const Page = struct {
map.putAssumeCapacity(dst_offset, value); 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. /// Append a codepoint to the given cell as a grapheme.
pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) Allocator.Error!void { pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) Allocator.Error!void {
defer self.assertIntegrity(); defer self.assertIntegrity();
@ -1114,6 +1152,12 @@ pub const Page = struct {
return self.grapheme_map.map(self.memory).count(); 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. /// Returns the bitset for the dirty bits on this page.
/// ///
/// The returned value is a DynamicBitSetUnmanaged but it is NOT /// The returned value is a DynamicBitSetUnmanaged but it is NOT

View File

@ -232,6 +232,7 @@ pub fn RefCountedSet(
// we're reusing the existing value in the set. This allows // we're reusing the existing value in the set. This allows
// callers to clean up any resources associated with the value. // callers to clean up any resources associated with the value.
if (comptime @hasDecl(Context, "deleted")) ctx.deleted(value); if (comptime @hasDecl(Context, "deleted")) ctx.deleted(value);
items[id].meta.ref += 1; items[id].meta.ref += 1;
return id; return id;
} }
@ -279,6 +280,8 @@ pub fn RefCountedSet(
pub fn addWithIdContext(self: *Self, base: anytype, value: T, id: Id, ctx: Context) AddError!?Id { pub fn addWithIdContext(self: *Self, base: anytype, value: T, id: Id, ctx: Context) AddError!?Id {
const items = self.items.ptr(base); const items = self.items.ptr(base);
assert(id > 0);
if (id < self.next_id) { if (id < self.next_id) {
if (items[id].meta.ref == 0) { if (items[id].meta.ref == 0) {
self.deleteItem(base, id, ctx); self.deleteItem(base, id, ctx);
@ -291,6 +294,11 @@ pub fn RefCountedSet(
return if (added_id == id) null else added_id; return if (added_id == id) null else added_id;
} else if (ctx.eql(value, items[id].value)) { } 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; items[id].meta.ref += 1;
return null; return null;
@ -501,6 +509,7 @@ pub fn RefCountedSet(
// we're reusing the existing value in the set. This allows // we're reusing the existing value in the set. This allows
// callers to clean up any resources associated with the value. // callers to clean up any resources associated with the value.
if (comptime @hasDecl(Context, "deleted")) ctx.deleted(value); if (comptime @hasDecl(Context, "deleted")) ctx.deleted(value);
return id; return id;
} }