mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #2115 from ghostty-org/index
Index should create scrollback anytime top scroll region is top line
This commit is contained in:
@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const ansi = @import("ansi.zig");
|
const ansi = @import("ansi.zig");
|
||||||
const charsets = @import("charsets.zig");
|
const charsets = @import("charsets.zig");
|
||||||
|
const fastmem = @import("../fastmem.zig");
|
||||||
const kitty = @import("kitty.zig");
|
const kitty = @import("kitty.zig");
|
||||||
const sgr = @import("sgr.zig");
|
const sgr = @import("sgr.zig");
|
||||||
const unicode = @import("../unicode/main.zig");
|
const unicode = @import("../unicode/main.zig");
|
||||||
@ -741,6 +742,143 @@ pub fn cursorDownScroll(self: *Screen) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This scrolls the active area at and above the cursor. The lines below
|
||||||
|
/// the cursor are not scrolled.
|
||||||
|
pub fn cursorScrollAbove(self: *Screen) !void {
|
||||||
|
// If the cursor is on the bottom of the screen, its faster to use
|
||||||
|
// our specialized function for that case.
|
||||||
|
if (self.cursor.y == self.pages.rows - 1) {
|
||||||
|
return try self.cursorDownScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
defer self.assertIntegrity();
|
||||||
|
|
||||||
|
// Logic below assumes we always have at least one row that isn't moving
|
||||||
|
assert(self.cursor.y < self.pages.rows - 1);
|
||||||
|
|
||||||
|
const old_pin = self.cursor.page_pin.*;
|
||||||
|
if (try self.pages.grow()) |_| {
|
||||||
|
try self.cursorScrollAboveRotate();
|
||||||
|
} else {
|
||||||
|
// In this case, it means grow() didn't allocate a new page.
|
||||||
|
|
||||||
|
if (self.cursor.page_pin.page == self.pages.pages.last) {
|
||||||
|
// If we're on the last page we can do a very fast path because
|
||||||
|
// all the rows we need to move around are within a single page.
|
||||||
|
|
||||||
|
assert(old_pin.page == self.cursor.page_pin.page);
|
||||||
|
self.cursor.page_pin.* = self.cursor.page_pin.down(1).?;
|
||||||
|
|
||||||
|
const pin = self.cursor.page_pin;
|
||||||
|
const page = &self.cursor.page_pin.page.data;
|
||||||
|
|
||||||
|
// Rotate the rows so that the newly created empty row is at the
|
||||||
|
// beginning. e.g. [ 0 1 2 3 ] in to [ 3 0 1 2 ].
|
||||||
|
var rows = page.rows.ptr(page.memory.ptr);
|
||||||
|
fastmem.rotateOnceR(Row, rows[pin.y..page.size.rows]);
|
||||||
|
|
||||||
|
// Mark all our rotated rows as dirty.
|
||||||
|
var dirty = page.dirtyBitSet();
|
||||||
|
dirty.setRangeValue(.{ .start = pin.y, .end = page.size.rows }, true);
|
||||||
|
|
||||||
|
// Setup our cursor caches after the rotation so it points to the
|
||||||
|
// correct data
|
||||||
|
const page_rac = self.cursor.page_pin.rowAndCell();
|
||||||
|
self.cursor.page_row = page_rac.row;
|
||||||
|
self.cursor.page_cell = page_rac.cell;
|
||||||
|
|
||||||
|
// Note: we don't need to call cursorChangePin here because
|
||||||
|
// the pin page is the same so there is no accounting to do for
|
||||||
|
// styles or any of that.
|
||||||
|
} else {
|
||||||
|
// We didn't grow pages but our cursor isn't on the last page.
|
||||||
|
// In this case we need to do more work because we need to copy
|
||||||
|
// elements between pages.
|
||||||
|
//
|
||||||
|
// An example scenario of this is shown below:
|
||||||
|
//
|
||||||
|
// +----------+ = PAGE 0
|
||||||
|
// ... : :
|
||||||
|
// +-------------+ ACTIVE
|
||||||
|
// 4302 |1A00000000| | 0
|
||||||
|
// 4303 |2B00000000| | 1
|
||||||
|
// :^ : : = PIN 0
|
||||||
|
// 4304 |3C00000000| | 2
|
||||||
|
// +----------+ :
|
||||||
|
// +----------+ : = PAGE 1
|
||||||
|
// 0 |4D00000000| | 3
|
||||||
|
// 1 |5E00000000| | 4
|
||||||
|
// +----------+ :
|
||||||
|
// +-------------+
|
||||||
|
try self.cursorScrollAboveRotate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.cursor.style_id != style.default_id) {
|
||||||
|
// The newly created line needs to be styled according to
|
||||||
|
// the bg color if it is set.
|
||||||
|
if (self.cursor.style.bgCell()) |blank_cell| {
|
||||||
|
const cell_current: [*]pagepkg.Cell = @ptrCast(self.cursor.page_cell);
|
||||||
|
const cells = cell_current - self.cursor.x;
|
||||||
|
@memset(cells[0..self.pages.cols], blank_cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursorScrollAboveRotate(self: *Screen) !void {
|
||||||
|
self.cursor.page_pin.* = self.cursor.page_pin.down(1).?;
|
||||||
|
|
||||||
|
// Go through each of the pages following our pin, shift all rows
|
||||||
|
// down by one, and copy the last row of the previous page.
|
||||||
|
var current = self.pages.pages.last.?;
|
||||||
|
while (current != self.cursor.page_pin.page) : (current = current.prev.?) {
|
||||||
|
const prev = current.prev.?;
|
||||||
|
const prev_page = &prev.data;
|
||||||
|
const cur_page = ¤t.data;
|
||||||
|
const prev_rows = prev_page.rows.ptr(prev_page.memory.ptr);
|
||||||
|
const cur_rows = cur_page.rows.ptr(cur_page.memory.ptr);
|
||||||
|
|
||||||
|
// Rotate the pages down: [ 0 1 2 3 ] => [ 3 0 1 2 ]
|
||||||
|
fastmem.rotateOnceR(Row, cur_rows[0..cur_page.size.rows]);
|
||||||
|
|
||||||
|
// Copy the last row of the previous page to the top of current.
|
||||||
|
try cur_page.cloneRowFrom(
|
||||||
|
prev_page,
|
||||||
|
&cur_rows[0],
|
||||||
|
&prev_rows[prev_page.size.rows - 1],
|
||||||
|
);
|
||||||
|
|
||||||
|
// All rows we rotated are dirty
|
||||||
|
var dirty = cur_page.dirtyBitSet();
|
||||||
|
dirty.setRangeValue(.{ .start = 0, .end = cur_page.size.rows }, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our current is our cursor page, we need to rotate down from
|
||||||
|
// our cursor and clear our row.
|
||||||
|
assert(current == self.cursor.page_pin.page);
|
||||||
|
const cur_page = ¤t.data;
|
||||||
|
const cur_rows = cur_page.rows.ptr(cur_page.memory.ptr);
|
||||||
|
fastmem.rotateOnceR(Row, cur_rows[self.cursor.page_pin.y..cur_page.size.rows]);
|
||||||
|
self.clearCells(
|
||||||
|
cur_page,
|
||||||
|
&cur_rows[0],
|
||||||
|
cur_page.getCells(&cur_rows[0]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set all the rows we rotated and cleared dirty
|
||||||
|
var dirty = cur_page.dirtyBitSet();
|
||||||
|
dirty.setRangeValue(
|
||||||
|
.{ .start = self.cursor.page_pin.y, .end = cur_page.size.rows },
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Setup cursor cache data after all the rotations so our
|
||||||
|
// row is valid.
|
||||||
|
const page_rac = self.cursor.page_pin.rowAndCell();
|
||||||
|
self.cursor.page_row = page_rac.row;
|
||||||
|
self.cursor.page_cell = page_rac.cell;
|
||||||
|
}
|
||||||
|
|
||||||
/// Move the cursor down if we're not at the bottom of the screen. Otherwise
|
/// Move the cursor down if we're not at the bottom of the screen. Otherwise
|
||||||
/// scroll. Currently only used for testing.
|
/// scroll. Currently only used for testing.
|
||||||
fn cursorDownOrScroll(self: *Screen) !void {
|
fn cursorDownOrScroll(self: *Screen) !void {
|
||||||
@ -3817,6 +3955,255 @@ test "Screen: scroll and clear ignore blank lines" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Screen: scroll above same page" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 10, 3, 10);
|
||||||
|
defer s.deinit();
|
||||||
|
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
|
||||||
|
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||||
|
s.cursorAbsolute(0, 1);
|
||||||
|
s.pages.clearDirty();
|
||||||
|
try s.cursorScrollAbove();
|
||||||
|
|
||||||
|
{
|
||||||
|
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
|
defer alloc.free(contents);
|
||||||
|
try testing.expectEqualStrings("2EFGH\n\n3IJKL", contents);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expect(cell.content_tag == .bg_color_rgb);
|
||||||
|
try testing.expectEqual(Cell.RGB{
|
||||||
|
.r = 155,
|
||||||
|
.g = 0,
|
||||||
|
.b = 0,
|
||||||
|
}, cell.content.color_rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only y=1,2 are dirty because they are the ones that CHANGED contents
|
||||||
|
// (not just scroll).
|
||||||
|
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Screen: scroll above same page but cursor on previous page" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 10, 5, 10);
|
||||||
|
defer s.deinit();
|
||||||
|
|
||||||
|
// We need to get the cursor to a new page
|
||||||
|
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
|
||||||
|
for (0..first_page_size - 3) |_| try s.testWriteString("\n");
|
||||||
|
|
||||||
|
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
|
||||||
|
try s.testWriteString("1A\n2B\n3C\n4D\n5E");
|
||||||
|
s.cursorAbsolute(0, 1);
|
||||||
|
s.pages.clearDirty();
|
||||||
|
|
||||||
|
// Ensure we're still on the first page and have a second
|
||||||
|
try testing.expect(s.cursor.page_pin.page == s.pages.pages.first.?);
|
||||||
|
try testing.expect(s.pages.pages.first.?.next != null);
|
||||||
|
|
||||||
|
// At this point:
|
||||||
|
// +----------+ = PAGE 0
|
||||||
|
// ... : :
|
||||||
|
// +-------------+ ACTIVE
|
||||||
|
// 4303 |1A00000000| | 0
|
||||||
|
// 4304 |2B00000000| | 1
|
||||||
|
// :^ : : = PIN 0
|
||||||
|
// +----------+ :
|
||||||
|
// +----------+ : = PAGE 1
|
||||||
|
// 0 |3C00000000| | 2
|
||||||
|
// 1 |4D00000000| | 3
|
||||||
|
// 2 |5E00000000| | 4
|
||||||
|
// +----------+ :
|
||||||
|
// +-------------+
|
||||||
|
|
||||||
|
try s.cursorScrollAbove();
|
||||||
|
|
||||||
|
{
|
||||||
|
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
|
defer alloc.free(contents);
|
||||||
|
try testing.expectEqualStrings("2B\n\n3C\n4D\n5E", contents);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expect(cell.content_tag == .bg_color_rgb);
|
||||||
|
try testing.expectEqual(Cell.RGB{
|
||||||
|
.r = 155,
|
||||||
|
.g = 0,
|
||||||
|
.b = 0,
|
||||||
|
}, cell.content.color_rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Screen: scroll above same page but cursor on previous page last row" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 10, 5, 10);
|
||||||
|
defer s.deinit();
|
||||||
|
|
||||||
|
// We need to get the cursor to a new page
|
||||||
|
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
|
||||||
|
for (0..first_page_size - 2) |_| try s.testWriteString("\n");
|
||||||
|
|
||||||
|
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
|
||||||
|
try s.testWriteString("1A\n2B\n3C\n4D\n5E");
|
||||||
|
s.cursorAbsolute(0, 1);
|
||||||
|
s.pages.clearDirty();
|
||||||
|
|
||||||
|
// Ensure we're still on the first page and have a second
|
||||||
|
try testing.expect(s.cursor.page_pin.page == s.pages.pages.first.?);
|
||||||
|
try testing.expect(s.pages.pages.first.?.next != null);
|
||||||
|
|
||||||
|
// At this point:
|
||||||
|
// +----------+ = PAGE 0
|
||||||
|
// ... : :
|
||||||
|
// +-------------+ ACTIVE
|
||||||
|
// 4303 |1A00000000| | 0
|
||||||
|
// 4304 |2B00000000| | 1
|
||||||
|
// :^ : : = PIN 0
|
||||||
|
// +----------+ :
|
||||||
|
// +----------+ : = PAGE 1
|
||||||
|
// 0 |3C00000000| | 2
|
||||||
|
// 1 |4D00000000| | 3
|
||||||
|
// 2 |5E00000000| | 4
|
||||||
|
// +----------+ :
|
||||||
|
// +-------------+
|
||||||
|
|
||||||
|
try s.cursorScrollAbove();
|
||||||
|
|
||||||
|
// +----------+ = PAGE 0
|
||||||
|
// ... : :
|
||||||
|
// 4303 |1A00000000|
|
||||||
|
// +-------------+ ACTIVE
|
||||||
|
// 4304 |2B00000000| | 0
|
||||||
|
// +----------+ :
|
||||||
|
// +----------+ : = PAGE 1
|
||||||
|
// 0 | | | 1
|
||||||
|
// :^ : : = PIN 0
|
||||||
|
// 1 |3C00000000| | 2
|
||||||
|
// 2 |4D00000000| | 3
|
||||||
|
// 3 |5E00000000| | 4
|
||||||
|
// +----------+ :
|
||||||
|
// +-------------+
|
||||||
|
|
||||||
|
// try s.pages.diagram(std.io.getStdErr().writer());
|
||||||
|
|
||||||
|
{
|
||||||
|
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
|
defer alloc.free(contents);
|
||||||
|
try testing.expectEqualStrings("2B\n\n3C\n4D\n5E", contents);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expect(cell.content_tag == .bg_color_rgb);
|
||||||
|
try testing.expectEqual(Cell.RGB{
|
||||||
|
.r = 155,
|
||||||
|
.g = 0,
|
||||||
|
.b = 0,
|
||||||
|
}, cell.content.color_rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Screen: scroll above creates new page" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 10, 3, 10);
|
||||||
|
defer s.deinit();
|
||||||
|
|
||||||
|
// We need to get the cursor to a new page
|
||||||
|
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
|
||||||
|
for (0..first_page_size - 3) |_| try s.testWriteString("\n");
|
||||||
|
|
||||||
|
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
|
||||||
|
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||||
|
s.cursorAbsolute(0, 1);
|
||||||
|
s.pages.clearDirty();
|
||||||
|
|
||||||
|
// Ensure we're still on the first page
|
||||||
|
try testing.expect(s.cursor.page_pin.page == s.pages.pages.first.?);
|
||||||
|
try s.cursorScrollAbove();
|
||||||
|
|
||||||
|
{
|
||||||
|
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
|
defer alloc.free(contents);
|
||||||
|
try testing.expectEqualStrings("2EFGH\n\n3IJKL", contents);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expect(cell.content_tag == .bg_color_rgb);
|
||||||
|
try testing.expectEqual(Cell.RGB{
|
||||||
|
.r = 155,
|
||||||
|
.g = 0,
|
||||||
|
.b = 0,
|
||||||
|
}, cell.content.color_rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only y=1 is dirty because they are the ones that CHANGED contents
|
||||||
|
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Screen: scroll above no scrollback bottom of page" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 10, 3, 0);
|
||||||
|
defer s.deinit();
|
||||||
|
|
||||||
|
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
|
||||||
|
for (0..first_page_size - 3) |_| try s.testWriteString("\n");
|
||||||
|
|
||||||
|
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
|
||||||
|
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||||
|
s.cursorAbsolute(0, 1);
|
||||||
|
s.pages.clearDirty();
|
||||||
|
try s.cursorScrollAbove();
|
||||||
|
|
||||||
|
{
|
||||||
|
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
|
defer alloc.free(contents);
|
||||||
|
try testing.expectEqualStrings("2EFGH\n\n3IJKL", contents);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?;
|
||||||
|
const cell = list_cell.cell;
|
||||||
|
try testing.expect(cell.content_tag == .bg_color_rgb);
|
||||||
|
try testing.expectEqual(Cell.RGB{
|
||||||
|
.r = 155,
|
||||||
|
.g = 0,
|
||||||
|
.b = 0,
|
||||||
|
}, cell.content.color_rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only y=1,2 are dirty because they are the ones that CHANGED contents
|
||||||
|
// (not just scroll).
|
||||||
|
try testing.expect(!s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||||
|
try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||||
|
}
|
||||||
|
|
||||||
test "Screen: clone" {
|
test "Screen: clone" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
|
@ -1109,63 +1109,62 @@ pub fn index(self: *Terminal) !void {
|
|||||||
// Scrolling dirties the images because it updates their placements pins.
|
// Scrolling dirties the images because it updates their placements pins.
|
||||||
self.screen.kitty_images.dirty = true;
|
self.screen.kitty_images.dirty = true;
|
||||||
|
|
||||||
// If our scrolling region is the full screen, we create scrollback.
|
// If our scrolling region is at the top, we create scrollback.
|
||||||
// Otherwise, we simply scroll the region.
|
|
||||||
if (self.scrolling_region.top == 0 and
|
if (self.scrolling_region.top == 0 and
|
||||||
self.scrolling_region.bottom == self.rows - 1 and
|
|
||||||
self.scrolling_region.left == 0 and
|
self.scrolling_region.left == 0 and
|
||||||
self.scrolling_region.right == self.cols - 1)
|
self.scrolling_region.right == self.cols - 1)
|
||||||
{
|
{
|
||||||
try self.screen.cursorDownScroll();
|
try self.screen.cursorScrollAbove();
|
||||||
} else {
|
return;
|
||||||
// Slow path for left and right scrolling region margins.
|
|
||||||
if (self.scrolling_region.left != 0 or
|
|
||||||
self.scrolling_region.right != self.cols - 1 or
|
|
||||||
|
|
||||||
// PERF(mitchellh): If we have an SGR background set then
|
|
||||||
// we need to preserve that background in our erased rows.
|
|
||||||
// scrollUp does that but eraseRowBounded below does not.
|
|
||||||
// However, scrollUp is WAY slower. We should optimize this
|
|
||||||
// case to work in the eraseRowBounded codepath and remove
|
|
||||||
// this check.
|
|
||||||
!self.screen.blankCell().isZero())
|
|
||||||
{
|
|
||||||
self.scrollUp(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise use a fast path function from PageList to efficiently
|
|
||||||
// scroll the contents of the scrolling region.
|
|
||||||
|
|
||||||
// Preserve old cursor just for assertions
|
|
||||||
const old_cursor = self.screen.cursor;
|
|
||||||
|
|
||||||
try self.screen.pages.eraseRowBounded(
|
|
||||||
.{ .active = .{ .y = self.scrolling_region.top } },
|
|
||||||
self.scrolling_region.bottom - self.scrolling_region.top,
|
|
||||||
);
|
|
||||||
|
|
||||||
// eraseRow and eraseRowBounded will end up moving the cursor pin
|
|
||||||
// up by 1, so we need to move it back down. A `cursorReload`
|
|
||||||
// would be better option but this is more efficient and this is
|
|
||||||
// a super hot path so we do this instead.
|
|
||||||
if (comptime std.debug.runtime_safety) {
|
|
||||||
assert(self.screen.cursor.x == old_cursor.x);
|
|
||||||
assert(self.screen.cursor.y == old_cursor.y);
|
|
||||||
}
|
|
||||||
self.screen.cursor.y -= 1;
|
|
||||||
self.screen.cursorDown(1);
|
|
||||||
|
|
||||||
// The operations above can prune our cursor style so we need to
|
|
||||||
// update. This should never fail because the above can only FREE
|
|
||||||
// memory.
|
|
||||||
self.screen.manualStyleUpdate() catch |err| {
|
|
||||||
std.log.warn("deleteLines manualStyleUpdate err={}", .{err});
|
|
||||||
self.screen.cursor.style = .{};
|
|
||||||
self.screen.manualStyleUpdate() catch unreachable;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slow path for left and right scrolling region margins.
|
||||||
|
if (self.scrolling_region.left != 0 or
|
||||||
|
self.scrolling_region.right != self.cols - 1 or
|
||||||
|
|
||||||
|
// PERF(mitchellh): If we have an SGR background set then
|
||||||
|
// we need to preserve that background in our erased rows.
|
||||||
|
// scrollUp does that but eraseRowBounded below does not.
|
||||||
|
// However, scrollUp is WAY slower. We should optimize this
|
||||||
|
// case to work in the eraseRowBounded codepath and remove
|
||||||
|
// this check.
|
||||||
|
!self.screen.blankCell().isZero())
|
||||||
|
{
|
||||||
|
self.scrollUp(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise use a fast path function from PageList to efficiently
|
||||||
|
// scroll the contents of the scrolling region.
|
||||||
|
|
||||||
|
// Preserve old cursor just for assertions
|
||||||
|
const old_cursor = self.screen.cursor;
|
||||||
|
|
||||||
|
try self.screen.pages.eraseRowBounded(
|
||||||
|
.{ .active = .{ .y = self.scrolling_region.top } },
|
||||||
|
self.scrolling_region.bottom - self.scrolling_region.top,
|
||||||
|
);
|
||||||
|
|
||||||
|
// eraseRow and eraseRowBounded will end up moving the cursor pin
|
||||||
|
// up by 1, so we need to move it back down. A `cursorReload`
|
||||||
|
// would be better option but this is more efficient and this is
|
||||||
|
// a super hot path so we do this instead.
|
||||||
|
if (comptime std.debug.runtime_safety) {
|
||||||
|
assert(self.screen.cursor.x == old_cursor.x);
|
||||||
|
assert(self.screen.cursor.y == old_cursor.y);
|
||||||
|
}
|
||||||
|
self.screen.cursor.y -= 1;
|
||||||
|
self.screen.cursorDown(1);
|
||||||
|
|
||||||
|
// The operations above can prune our cursor style so we need to
|
||||||
|
// update. This should never fail because the above can only FREE
|
||||||
|
// memory.
|
||||||
|
self.screen.manualStyleUpdate() catch |err| {
|
||||||
|
std.log.warn("deleteLines manualStyleUpdate err={}", .{err});
|
||||||
|
self.screen.cursor.style = .{};
|
||||||
|
self.screen.manualStyleUpdate() catch unreachable;
|
||||||
|
};
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6438,10 +6437,11 @@ test "Terminal: index bottom of scroll region with hyperlinks" {
|
|||||||
|
|
||||||
test "Terminal: index bottom of scroll region clear hyperlinks" {
|
test "Terminal: index bottom of scroll region clear hyperlinks" {
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
|
var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 });
|
||||||
defer t.deinit(alloc);
|
defer t.deinit(alloc);
|
||||||
|
|
||||||
t.setTopAndBottomMargin(1, 2);
|
t.setTopAndBottomMargin(2, 3);
|
||||||
|
t.setCursorPos(2, 1);
|
||||||
try t.screen.startHyperlink("http://example.com", null);
|
try t.screen.startHyperlink("http://example.com", null);
|
||||||
try t.print('A');
|
try t.print('A');
|
||||||
t.screen.endHyperlink();
|
t.screen.endHyperlink();
|
||||||
@ -6455,10 +6455,10 @@ test "Terminal: index bottom of scroll region clear hyperlinks" {
|
|||||||
{
|
{
|
||||||
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("B\nC", str);
|
try testing.expectEqualStrings("\nB\nC", str);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (0..2) |y| {
|
for (1..3) |y| {
|
||||||
const list_cell = t.screen.pages.getCell(.{ .viewport = .{
|
const list_cell = t.screen.pages.getCell(.{ .viewport = .{
|
||||||
.x = 0,
|
.x = 0,
|
||||||
.y = @intCast(y),
|
.y = @intCast(y),
|
||||||
@ -6597,11 +6597,36 @@ test "Terminal: index inside left/right margin" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Terminal: index bottom of scroll region" {
|
test "Terminal: index bottom of scroll region creates scrollback" {
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
|
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
|
||||||
defer t.deinit(alloc);
|
defer t.deinit(alloc);
|
||||||
|
|
||||||
|
t.setTopAndBottomMargin(1, 3);
|
||||||
|
try t.printString("1\n2\n3");
|
||||||
|
t.setCursorPos(4, 1);
|
||||||
|
try t.print('X');
|
||||||
|
t.setCursorPos(3, 1);
|
||||||
|
try t.index();
|
||||||
|
try t.print('Y');
|
||||||
|
|
||||||
|
{
|
||||||
|
const str = try t.screen.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
try testing.expectEqualStrings("2\n3\nY\nX", str);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const str = try t.screen.dumpStringAlloc(alloc, .{ .screen = .{} });
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
try testing.expectEqualStrings("1\n2\n3\nY\nX", str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Terminal: index bottom of scroll region no scrollback" {
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 });
|
||||||
|
defer t.deinit(alloc);
|
||||||
|
|
||||||
t.setTopAndBottomMargin(1, 3);
|
t.setTopAndBottomMargin(1, 3);
|
||||||
t.setCursorPos(4, 1);
|
t.setCursorPos(4, 1);
|
||||||
try t.print('B');
|
try t.print('B');
|
||||||
@ -6611,11 +6636,6 @@ test "Terminal: index bottom of scroll region" {
|
|||||||
try t.index();
|
try t.index();
|
||||||
try t.print('X');
|
try t.print('X');
|
||||||
|
|
||||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
|
||||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
|
||||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
|
||||||
try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 3 } }));
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const str = try t.plainString(testing.allocator);
|
const str = try t.plainString(testing.allocator);
|
||||||
defer testing.allocator.free(str);
|
defer testing.allocator.free(str);
|
||||||
@ -6623,6 +6643,47 @@ test "Terminal: index bottom of scroll region" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Terminal: index bottom of scroll region blank line preserves SGR" {
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
|
||||||
|
defer t.deinit(alloc);
|
||||||
|
|
||||||
|
t.setTopAndBottomMargin(1, 3);
|
||||||
|
try t.printString("1\n2\n3");
|
||||||
|
t.setCursorPos(4, 1);
|
||||||
|
try t.print('X');
|
||||||
|
t.setCursorPos(3, 1);
|
||||||
|
try t.setAttribute(.{ .direct_color_bg = .{
|
||||||
|
.r = 0xFF,
|
||||||
|
.g = 0,
|
||||||
|
.b = 0,
|
||||||
|
} });
|
||||||
|
try t.index();
|
||||||
|
|
||||||
|
{
|
||||||
|
const str = try t.screen.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
try testing.expectEqualStrings("2\n3\n\nX", str);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const str = try t.screen.dumpStringAlloc(alloc, .{ .screen = .{} });
|
||||||
|
defer testing.allocator.free(str);
|
||||||
|
try testing.expectEqualStrings("1\n2\n3\n\nX", str);
|
||||||
|
}
|
||||||
|
for (0..t.cols) |x| {
|
||||||
|
const list_cell = t.screen.pages.getCell(.{ .active = .{
|
||||||
|
.x = @intCast(x),
|
||||||
|
.y = 2,
|
||||||
|
} }).?;
|
||||||
|
try testing.expect(list_cell.cell.content_tag == .bg_color_rgb);
|
||||||
|
try testing.expectEqual(Cell.RGB{
|
||||||
|
.r = 0xFF,
|
||||||
|
.g = 0,
|
||||||
|
.b = 0,
|
||||||
|
}, list_cell.cell.content.color_rgb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "Terminal: cursorUp basic" {
|
test "Terminal: cursorUp basic" {
|
||||||
const alloc = testing.allocator;
|
const alloc = testing.allocator;
|
||||||
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
|
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
|
||||||
|
Reference in New Issue
Block a user