diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 1f4f2468b..74bbfe482 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -825,6 +825,8 @@ pub const PageFormatter = struct { /// byte written to the writer offset by the byte index. It is the /// caller's responsibility to free the map. /// + /// The x/y coordinate will be the coordinates within the page. + /// /// Warning: there is a significant performance hit to track this point_map: ?struct { alloc: Allocator, @@ -1450,6 +1452,76 @@ test "Page plain single line" { ); } +test "Page plain single line soft-wrapped unwrapped" { + const testing = std.testing; + const alloc = testing.allocator; + + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var t = try Terminal.init(alloc, .{ + .cols = 3, + .rows = 5, + }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + + try s.nextSlice("hello!"); + + // Verify we have only a single page + const pages = &t.screens.active.pages; + try testing.expect(pages.pages.first != null); + try testing.expect(pages.pages.first == pages.pages.last); + + // Create the formatter + const page = &pages.pages.last.?.data; + var formatter: PageFormatter = .init(page, .{ + .emit = .plain, + .unwrap = true, + }); + + // Test our point map. + var point_map: std.ArrayList(Coordinate) = .empty; + defer point_map.deinit(alloc); + formatter.point_map = .{ .alloc = alloc, .map = &point_map }; + + // Verify output + // Note: we don't test the trailing state, which may have bugs + // with unwrap... + _ = try formatter.formatWithState(&builder.writer); + const output = builder.writer.buffered(); + try testing.expectEqualStrings("hello!", output); + + // Verify our point map + try testing.expectEqual(output.len, point_map.items.len); + try testing.expectEqual( + Coordinate{ .x = 0, .y = 0 }, + point_map.items[0], + ); + try testing.expectEqual( + Coordinate{ .x = 1, .y = 0 }, + point_map.items[1], + ); + try testing.expectEqual( + Coordinate{ .x = 2, .y = 0 }, + point_map.items[2], + ); + try testing.expectEqual( + Coordinate{ .x = 0, .y = 1 }, + point_map.items[3], + ); + try testing.expectEqual( + Coordinate{ .x = 1, .y = 1 }, + point_map.items[4], + ); + try testing.expectEqual( + Coordinate{ .x = 2, .y = 1 }, + point_map.items[5], + ); +} + test "Page plain single wide char" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/render.zig b/src/terminal/render.zig index 296360381..83b4a7145 100644 --- a/src/terminal/render.zig +++ b/src/terminal/render.zig @@ -703,8 +703,16 @@ pub const RenderState = struct { .{ .tag = tag, .range = .{ - if (i == 0) hl.top_x else 0, - if (i == nodes.len - 1) hl.bot_x else self.cols - 1, + if (i == 0 and + row_pin.y == starts[0]) + hl.top_x + else + 0, + if (i == nodes.len - 1 and + row_pin.y == ends[nodes.len - 1] - 1) + hl.bot_x + else + self.cols - 1, }, }, ); diff --git a/src/terminal/search/sliding_window.zig b/src/terminal/search/sliding_window.zig index 0d853b3a0..3d64042ce 100644 --- a/src/terminal/search/sliding_window.zig +++ b/src/terminal/search/sliding_window.zig @@ -9,6 +9,7 @@ const PageList = terminal.PageList; const Pin = PageList.Pin; const Selection = terminal.Selection; const Screen = terminal.Screen; +const Terminal = terminal.Terminal; const PageFormatter = @import("../formatter.zig").PageFormatter; const FlattenedHighlight = terminal.highlight.Flattened; @@ -462,12 +463,13 @@ pub const SlidingWindow = struct { switch (self.direction) { .forward => {}, .reverse => { + const slice = self.chunk_buf.slice(); + const nodes = slice.items(.node); + const starts = slice.items(.start); + const ends = slice.items(.end); + if (self.chunk_buf.len > 1) { // Reverse all our chunks. This should be pretty obvious why. - const slice = self.chunk_buf.slice(); - const nodes = slice.items(.node); - const starts = slice.items(.start); - const ends = slice.items(.end); std.mem.reverse(*PageList.List.Node, nodes); std.mem.reverse(size.CellCountInt, starts); std.mem.reverse(size.CellCountInt, ends); @@ -484,10 +486,6 @@ pub const SlidingWindow = struct { // We DON'T need to do this for any middle pages because // they always use the full page. // - // We DON'T need to do this for chunks.len == 1 because - // the pages themselves aren't reversed and we don't have - // any prefix/suffix problems. - // // This is a fixup that makes our start/end match the // same logic as the loops above if they were in forward // order. @@ -496,6 +494,13 @@ pub const SlidingWindow = struct { ends[0] = nodes[0].data.size.rows; ends[nodes.len - 1] = starts[nodes.len - 1] + 1; starts[nodes.len - 1] = 0; + } else { + // For a single chunk, the y values are in reverse order + // (start is the screen-end, end is the screen-start). + // Swap them to get proper top-to-bottom order. + const start_y = starts[0]; + starts[0] = ends[0] - 1; + ends[0] = start_y + 1; } // X values also need to be reversed since the top/bottom @@ -539,7 +544,10 @@ pub const SlidingWindow = struct { // Encode the page into the buffer. const formatter: PageFormatter = formatter: { - var formatter: PageFormatter = .init(&meta.node.data, .plain); + var formatter: PageFormatter = .init(&meta.node.data, .{ + .emit = .plain, + .unwrap = true, + }); formatter.point_map = .{ .alloc = self.alloc, .map = &meta.cell_map, @@ -1555,3 +1563,77 @@ test "SlidingWindow single append match on boundary reversed" { } try testing.expect(w.next() == null); } + +test "SlidingWindow single append soft wrapped" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .forward, "boo!"); + defer w.deinit(); + + var t: Terminal = try .init(alloc, .{ .cols = 4, .rows = 5 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("A\r\nxxboo!\r\nC"); + + // We want to test single-page cases. + const screen = t.screens.active; + try testing.expect(screen.pages.pages.first == screen.pages.pages.last); + const node: *PageList.List.Node = screen.pages.pages.first.?; + _ = try w.append(node); + + // We should be able to find two matches. + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 2, + .y = 1, + } }, screen.pages.pointFromPin(.active, sel.start)); + try testing.expectEqual(point.Point{ .active = .{ + .x = 1, + .y = 2, + } }, screen.pages.pointFromPin(.active, sel.end)); + } + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); +} + +test "SlidingWindow single append reversed soft wrapped" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .reverse, "boo!"); + defer w.deinit(); + + var t: Terminal = try .init(alloc, .{ .cols = 4, .rows = 5 }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice("A\r\nxxboo!\r\nC"); + + // We want to test single-page cases. + const screen = t.screens.active; + try testing.expect(screen.pages.pages.first == screen.pages.pages.last); + const node: *PageList.List.Node = screen.pages.pages.first.?; + _ = try w.append(node); + + // We should be able to find two matches. + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 2, + .y = 1, + } }, screen.pages.pointFromPin(.active, sel.start)); + try testing.expectEqual(point.Point{ .active = .{ + .x = 1, + .y = 2, + } }, screen.pages.pointFromPin(.active, sel.end)); + } + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); +}