diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 167654d61..04b58b99b 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -6251,13 +6251,14 @@ pub const Pin = struct { if (n <= remaining_in_row) return self.left(n); const extra_after_remaining = n - remaining_in_row; - - const rows_off = 1 + extra_after_remaining / cols; + const rows_off = 1 + (extra_after_remaining - 1) / cols; switch (self.upOverflow(rows_off)) { .offset => |v| { var result = v; - result.x = @intCast(cols - extra_after_remaining % cols); + result.x = @intCast( + cols - 1 - (extra_after_remaining - 1) % cols, + ); return result; }, .overflow => return null, @@ -6278,13 +6279,12 @@ pub const Pin = struct { if (n <= remaining_in_row) return self.right(n); const extra_after_remaining = n - remaining_in_row; - - const rows_off = 1 + extra_after_remaining / cols; + const rows_off = 1 + (extra_after_remaining - 1) / cols; switch (self.downOverflow(rows_off)) { .offset => |v| { var result = v; - result.x = @intCast(extra_after_remaining % cols - 1); + result.x = @intCast((extra_after_remaining - 1) % cols); return result; }, .overflow => return null, @@ -6451,6 +6451,58 @@ fn growColdPagesForTest(self: *PageList, count: usize) !void { } } +test "PageList Pin rightWrap exact row multiple" { + const testing = std.testing; + + var s = try init(testing.allocator, 10, 3, null); + defer s.deinit(); + + const start = s.pin(.{ .active = .{ .x = 5, .y = 0 } }).?; + const wrapped = start.rightWrap(14).?; + _ = wrapped.rowAndCell(); + + try testing.expectEqual( + point.Point{ .active = .{ .x = 9, .y = 1 } }, + s.pointFromPin(.active, wrapped), + ); +} + +test "PageList Pin leftWrap exact row multiple" { + const testing = std.testing; + + var s = try init(testing.allocator, 10, 3, null); + defer s.deinit(); + + const start = s.pin(.{ .active = .{ .x = 5, .y = 2 } }).?; + const wrapped = start.leftWrap(15).?; + _ = wrapped.rowAndCell(); + + try testing.expectEqual( + point.Point{ .active = .{ .x = 0, .y = 1 } }, + s.pointFromPin(.active, wrapped), + ); +} + +test "PageList Pin rightWrap maximum distance" { + const testing = std.testing; + + var s = try init(testing.allocator, 1, 3, null); + defer s.deinit(); + + const start = s.pin(.{ .active = .{ .y = 0 } }).?; + try testing.expectEqual(null, start.rightWrap(std.math.maxInt(usize))); +} + +test "PageList Pin leftWrap maximum distance" { + const testing = std.testing; + + var s = try init(testing.allocator, 1, 3, null); + defer s.deinit(); + + const start = s.pin(.{ .active = .{ .y = 2 } }).?; + try testing.expectEqual(null, start.leftWrap(std.math.maxInt(usize))); +} + test "PageList incremental compression skips visible history" { const testing = std.testing; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index b9363684d..3bcfdf5d9 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -2120,8 +2120,8 @@ pub fn setCursorPos(self: *Terminal, row_req: usize, col_req: usize) void { // Calculate our new x/y const row = if (row_req == 0) 1 else row_req; const col = if (col_req == 0) 1 else col_req; - const x = @min(params.x_max, col + params.x_offset) -| 1; - const y = @min(params.y_max, row + params.y_offset) -| 1; + const x = @min(params.x_max, col +| params.x_offset) -| 1; + const y = @min(params.y_max, row +| params.y_offset) -| 1; // If the y is unchanged then this is fast pointer math if (y == self.screens.active.cursor.y) { @@ -3816,6 +3816,24 @@ fn clearDirty(t: *Terminal) void { t.screens.active.pages.clearDirty(); } +test "Terminal: setCursorPos saturates overflowing origin offsets" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 10, .rows = 10 }); + defer t.deinit(alloc); + + t.scrolling_region = .{ + .top = 2, + .bottom = 7, + .left = 3, + .right = 8, + }; + t.modes.set(.origin, true); + + t.setCursorPos(std.math.maxInt(usize), std.math.maxInt(usize)); + try testing.expectEqual(@as(size.CellCountInt, 8), t.screens.active.cursor.x); + try testing.expectEqual(@as(size.CellCountInt, 7), t.screens.active.cursor.y); +} + test "Terminal: input with no control characters" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 40, .rows = 40 }); diff --git a/src/terminal/search/active.zig b/src/terminal/search/active.zig index aee21b3e3..b99b23579 100644 --- a/src/terminal/search/active.zig +++ b/src/terminal/search/active.zig @@ -56,6 +56,10 @@ pub const ActiveSearch = struct { // Clear our previous sliding window self.window.clearAndRetainCapacity(); + // An empty needle represents an inactive search and has no overlap + // or history to load. + if (self.window.needle.len == 0) return null; + // First up, add enough pages to cover the active area. var rem: usize = list.rows; var node_ = list.pages.last; diff --git a/src/terminal/search/sliding_window.zig b/src/terminal/search/sliding_window.zig index ea039bf3a..5c29ce03b 100644 --- a/src/terminal/search/sliding_window.zig +++ b/src/terminal/search/sliding_window.zig @@ -176,6 +176,11 @@ pub const SlidingWindow = struct { /// or `append()`. If the caller wants to retain the flattened highlight /// then they should clone it. pub fn next(self: *SlidingWindow) ?FlattenedHighlight { + // An empty needle represents an inactive search. Searching for it + // would otherwise produce a zero-length match, which highlight() + // cannot represent because its end offset is inclusive. + if (self.needle.len == 0) return null; + const slices = slices: { // If we have less data then the needle then we can't possibly match const data_len = self.data.len(); @@ -704,6 +709,25 @@ test "SlidingWindow empty on init" { try testing.expectEqual(0, w.meta.len()); } +test "SlidingWindow empty needle has no matches" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .forward, ""); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ + .cols = 80, + .rows = 24, + .max_scrollback = 0, + }); + defer s.deinit(); + try s.testWriteString("hello"); + + _ = try w.append(s.pages.pages.first.?); + try testing.expectEqual(null, w.next()); +} + test "SlidingWindow single append" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/search/viewport.zig b/src/terminal/search/viewport.zig index a1458bbed..d8dbb9429 100644 --- a/src/terminal/search/viewport.zig +++ b/src/terminal/search/viewport.zig @@ -134,6 +134,7 @@ pub const ViewportSearch = struct { // Add enough overlap to cover needle.len - 1 bytes (if it // exists) so we can cover the overlap. We only do this for the // soft-wrapped prior pages. + const overlap_len = self.window.needle.len -| 1; var node_ = fingerprint.nodes[0].prev; var added: usize = 0; while (node_) |node| : (node_ = node.prev) { @@ -142,7 +143,7 @@ pub const ViewportSearch = struct { // wrap so this should be fine. const appended = try self.window.appendIfWrapped(node) orelse break; added += appended.content_len; - if (added >= self.window.needle.len - 1) break; + if (added >= overlap_len) break; } // We can use our fingerprint nodes to initialize our sliding @@ -162,7 +163,7 @@ pub const ViewportSearch = struct { while (node_) |node| : (node_ = node.next) { const appended = try self.window.append(node); added += appended.content_len; - if (added >= self.window.needle.len - 1) break; + if (added >= overlap_len) break; // If this row doesn't wrap, then we can quit if (!appended.last_row_wrapped) break;