search: previous match

pull/9702/head
Mitchell Hashimoto 2025-11-25 09:39:04 -08:00
parent c38e098c4c
commit a2a771bb6f
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
1 changed files with 231 additions and 13 deletions

View File

@ -398,8 +398,7 @@ pub const ScreenSearch = struct {
self.selected = null;
break :select_prev true;
};
defer if (select_prev) self.select(.next) catch |err| {
// TODO: Change the above next to prev
defer if (select_prev) self.select(.prev) catch |err| {
log.info("reload failed to reset search selection err={}", .{err});
};
@ -585,11 +584,6 @@ pub const ScreenSearch = struct {
}
}
pub const Select = enum {
/// Next selection, in reverse order (newest to oldest)
next,
};
/// Return the selected match.
///
/// This does not require read/write access to the underlying screen.
@ -608,22 +602,33 @@ pub const ScreenSearch = struct {
return null;
}
pub const Select = enum {
/// Next selection, in reverse order (newest to oldest),
/// non-wrapping.
next,
/// Prev selection, in forward order (oldest to newest),
/// non-wrapping.
prev,
};
/// Select the next or previous search result. This requires read/write
/// access to the underlying screen, since we utilize tracked pins to
/// ensure our selection sticks with contents changing.
pub fn select(self: *ScreenSearch, to: Select) Allocator.Error!void {
switch (to) {
.next => try self.selectNext(),
}
}
fn selectNext(self: *ScreenSearch) Allocator.Error!void {
// All selection requires valid pins so we prune history and
// reload our active area immediately. This ensures all search
// results point to valid nodes.
try self.reloadActive();
self.pruneHistory();
switch (to) {
.next => try self.selectNext(),
.prev => try self.selectPrev(),
}
}
fn selectNext(self: *ScreenSearch) Allocator.Error!void {
// Get our previous match so we can change it. If we have no
// prior match, we have the easy task of getting the first.
var prev = if (self.selected) |*m| m else {
@ -678,6 +683,65 @@ pub const ScreenSearch = struct {
.highlight = tracked,
};
}
fn selectPrev(self: *ScreenSearch) Allocator.Error!void {
// Get our previous match so we can change it. If we have no
// prior match, we have the easy task of getting the last.
var prev = if (self.selected) |*m| m else {
// Get our highlight (oldest match)
const hl: FlattenedHighlight = hl: {
if (self.history_results.items.len > 0) {
// History is in reverse order, so last item is oldest
const len = self.history_results.items.len;
break :hl self.history_results.items[len - 1];
} else if (self.active_results.items.len > 0) {
// Active is in forward order, so first item is oldest
break :hl self.active_results.items[0];
} else {
// No matches at all. Can't select anything.
return;
}
};
// Pin it so we can track any movement
const tracked = try hl.untracked().track(self.screen);
errdefer tracked.deinit(self.screen);
// Our selection is the last index since we just started
// and we store our selection.
const active_len = self.active_results.items.len;
const history_len = self.history_results.items.len;
self.selected = .{
.idx = active_len + history_len - 1,
.highlight = tracked,
};
return;
};
// Can't go below zero
if (prev.idx == 0) {
// No more matches. We don't wrap or reset the match currently.
return;
}
const next_idx = prev.idx - 1;
const active_len = self.active_results.items.len;
const hl: FlattenedHighlight = if (next_idx < active_len)
self.active_results.items[active_len - 1 - next_idx]
else
self.history_results.items[next_idx - active_len];
// Pin it so we can track any movement
const tracked = try hl.untracked().track(self.screen);
errdefer tracked.deinit(self.screen);
// Free our previous match and setup our new selection
prev.deinit(self.screen);
self.selected = .{
.idx = next_idx,
.highlight = tracked,
};
}
};
test "simple search" {
@ -1085,3 +1149,157 @@ test "select into history" {
} }, t.screens.active.pages.pointFromPin(.screen, sel.end).?);
}
}
test "select prev" {
const alloc = testing.allocator;
var t: Terminal = try .init(alloc, .{ .cols = 10, .rows = 2 });
defer t.deinit(alloc);
var s = t.vtStream();
defer s.deinit();
try s.nextSlice("Fizz\r\nBuzz\r\nFizz\r\nBang");
var search: ScreenSearch = try .init(alloc, t.screens.active, "Fizz");
defer search.deinit();
// Initially no selection
try testing.expect(search.selectedMatch() == null);
// Select prev (oldest first)
try search.searchAll();
try search.select(.prev);
{
const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{
.x = 0,
.y = 0,
} }, t.screens.active.pages.pointFromPin(.screen, sel.start).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 3,
.y = 0,
} }, t.screens.active.pages.pointFromPin(.screen, sel.end).?);
}
// Prev match (towards newest)
try search.select(.prev);
{
const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{
.x = 0,
.y = 2,
} }, t.screens.active.pages.pointFromPin(.screen, sel.start).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 3,
.y = 2,
} }, t.screens.active.pages.pointFromPin(.screen, sel.end).?);
}
// Prev match (no wrap, stays at newest)
try search.select(.prev);
{
const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{
.x = 0,
.y = 2,
} }, t.screens.active.pages.pointFromPin(.screen, sel.start).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 3,
.y = 2,
} }, t.screens.active.pages.pointFromPin(.screen, sel.end).?);
}
}
test "select prev then next" {
const alloc = testing.allocator;
var t: Terminal = try .init(alloc, .{ .cols = 10, .rows = 2 });
defer t.deinit(alloc);
var s = t.vtStream();
defer s.deinit();
try s.nextSlice("Fizz\r\nBuzz\r\nFizz\r\nBang");
var search: ScreenSearch = try .init(alloc, t.screens.active, "Fizz");
defer search.deinit();
try search.searchAll();
// Select next (newest first)
try search.select(.next);
{
const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{
.x = 0,
.y = 2,
} }, t.screens.active.pages.pointFromPin(.screen, sel.start).?);
}
// Select next (older)
try search.select(.next);
{
const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{
.x = 0,
.y = 0,
} }, t.screens.active.pages.pointFromPin(.screen, sel.start).?);
}
// Select prev (back to newer)
try search.select(.prev);
{
const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{
.x = 0,
.y = 2,
} }, t.screens.active.pages.pointFromPin(.screen, sel.start).?);
}
}
test "select prev with history" {
const alloc = testing.allocator;
var t: Terminal = try .init(alloc, .{
.cols = 10,
.rows = 2,
.max_scrollback = std.math.maxInt(usize),
});
defer t.deinit(alloc);
const list: *PageList = &t.screens.active.pages;
var s = t.vtStream();
defer s.deinit();
try s.nextSlice("Fizz\r\n");
while (list.totalPages() < 3) try s.nextSlice("\r\n");
for (0..list.rows) |_| try s.nextSlice("\r\n");
try s.nextSlice("Fizz.");
var search: ScreenSearch = try .init(alloc, t.screens.active, "Fizz");
defer search.deinit();
try search.searchAll();
// Select prev (oldest first, should be in history)
try search.select(.prev);
{
const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{
.x = 0,
.y = 0,
} }, t.screens.active.pages.pointFromPin(.screen, sel.start).?);
try testing.expectEqual(point.Point{ .screen = .{
.x = 3,
.y = 0,
} }, t.screens.active.pages.pointFromPin(.screen, sel.end).?);
}
// Select prev (towards newer, should move to active area)
try search.select(.prev);
{
const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .active = .{
.x = 0,
.y = 1,
} }, t.screens.active.pages.pointFromPin(.active, sel.start).?);
try testing.expectEqual(point.Point{ .active = .{
.x = 3,
.y = 1,
} }, t.screens.active.pages.pointFromPin(.active, sel.end).?);
}
}