search: scroll to selected search match

pull/9702/head
Mitchell Hashimoto 2025-11-25 11:00:32 -08:00
parent ba7b816af0
commit d0334b7ab6
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
2 changed files with 45 additions and 30 deletions

View File

@ -257,7 +257,16 @@ fn select(self: *Thread, sel: ScreenSearch.Select) !void {
// The selection will trigger a selection change notification // The selection will trigger a selection change notification
// if it did change. // if it did change.
try screen_search.select(sel); if (try screen_search.select(sel)) scroll: {
if (screen_search.selected) |m| {
// Selection changed, let's scroll the viewport to see it
// since we have the lock anyways.
const screen = self.opts.terminal.screens.get(
s.last_screen.key,
) orelse break :scroll;
screen.scroll(.{ .pin = m.highlight.start.* });
}
}
} }
/// Change the search term to the given value. /// Change the search term to the given value.

View File

@ -398,8 +398,10 @@ pub const ScreenSearch = struct {
self.selected = null; self.selected = null;
break :select_prev true; break :select_prev true;
}; };
defer if (select_prev) self.select(.prev) catch |err| { defer if (select_prev) {
log.info("reload failed to reset search selection err={}", .{err}); _ = self.select(.prev) catch |err| {
log.info("reload failed to reset search selection err={}", .{err});
};
}; };
const alloc = self.allocator(); const alloc = self.allocator();
@ -526,7 +528,7 @@ pub const ScreenSearch = struct {
if (self.selected) |*m| { if (self.selected) |*m| {
m.deinit(self.screen); m.deinit(self.screen);
self.selected = null; self.selected = null;
self.select(.next) catch |err| { _ = self.select(.next) catch |err| {
log.info("reload failed to reset search selection err={}", .{err}); log.info("reload failed to reset search selection err={}", .{err});
}; };
} }
@ -578,7 +580,7 @@ pub const ScreenSearch = struct {
// No match, just go back to the first match. // No match, just go back to the first match.
m.deinit(self.screen); m.deinit(self.screen);
self.selected = null; self.selected = null;
self.select(.next) catch |err| { _ = self.select(.next) catch |err| {
log.info("reload failed to reset search selection err={}", .{err}); log.info("reload failed to reset search selection err={}", .{err});
}; };
} }
@ -615,20 +617,20 @@ pub const ScreenSearch = struct {
/// Select the next or previous search result. This requires read/write /// Select the next or previous search result. This requires read/write
/// access to the underlying screen, since we utilize tracked pins to /// access to the underlying screen, since we utilize tracked pins to
/// ensure our selection sticks with contents changing. /// ensure our selection sticks with contents changing.
pub fn select(self: *ScreenSearch, to: Select) Allocator.Error!void { pub fn select(self: *ScreenSearch, to: Select) Allocator.Error!bool {
// All selection requires valid pins so we prune history and // All selection requires valid pins so we prune history and
// reload our active area immediately. This ensures all search // reload our active area immediately. This ensures all search
// results point to valid nodes. // results point to valid nodes.
try self.reloadActive(); try self.reloadActive();
self.pruneHistory(); self.pruneHistory();
switch (to) { return switch (to) {
.next => try self.selectNext(), .next => try self.selectNext(),
.prev => try self.selectPrev(), .prev => try self.selectPrev(),
} };
} }
fn selectNext(self: *ScreenSearch) Allocator.Error!void { fn selectNext(self: *ScreenSearch) Allocator.Error!bool {
// Get our previous match so we can change it. If we have no // Get our previous match so we can change it. If we have no
// prior match, we have the easy task of getting the first. // prior match, we have the easy task of getting the first.
var prev = if (self.selected) |*m| m else { var prev = if (self.selected) |*m| m else {
@ -643,7 +645,7 @@ pub const ScreenSearch = struct {
break :hl self.history_results.items[0]; break :hl self.history_results.items[0];
} else { } else {
// No matches at all. Can't select anything. // No matches at all. Can't select anything.
return; return false;
} }
}; };
@ -657,7 +659,7 @@ pub const ScreenSearch = struct {
.idx = 0, .idx = 0,
.highlight = tracked, .highlight = tracked,
}; };
return; return true;
}; };
const next_idx = prev.idx + 1; const next_idx = prev.idx + 1;
@ -665,7 +667,7 @@ pub const ScreenSearch = struct {
const history_len = self.history_results.items.len; const history_len = self.history_results.items.len;
if (next_idx >= active_len + history_len) { if (next_idx >= active_len + history_len) {
// No more matches. We don't wrap or reset the match currently. // No more matches. We don't wrap or reset the match currently.
return; return false;
} }
const hl: FlattenedHighlight = if (next_idx < active_len) const hl: FlattenedHighlight = if (next_idx < active_len)
self.active_results.items[active_len - 1 - next_idx] self.active_results.items[active_len - 1 - next_idx]
@ -682,9 +684,11 @@ pub const ScreenSearch = struct {
.idx = next_idx, .idx = next_idx,
.highlight = tracked, .highlight = tracked,
}; };
return true;
} }
fn selectPrev(self: *ScreenSearch) Allocator.Error!void { fn selectPrev(self: *ScreenSearch) Allocator.Error!bool {
// Get our previous match so we can change it. If we have no // Get our previous match so we can change it. If we have no
// prior match, we have the easy task of getting the last. // prior match, we have the easy task of getting the last.
var prev = if (self.selected) |*m| m else { var prev = if (self.selected) |*m| m else {
@ -699,7 +703,7 @@ pub const ScreenSearch = struct {
break :hl self.active_results.items[0]; break :hl self.active_results.items[0];
} else { } else {
// No matches at all. Can't select anything. // No matches at all. Can't select anything.
return; return false;
} }
}; };
@ -715,13 +719,13 @@ pub const ScreenSearch = struct {
.idx = active_len + history_len - 1, .idx = active_len + history_len - 1,
.highlight = tracked, .highlight = tracked,
}; };
return; return true;
}; };
// Can't go below zero // Can't go below zero
if (prev.idx == 0) { if (prev.idx == 0) {
// No more matches. We don't wrap or reset the match currently. // No more matches. We don't wrap or reset the match currently.
return; return false;
} }
const next_idx = prev.idx - 1; const next_idx = prev.idx - 1;
@ -741,6 +745,8 @@ pub const ScreenSearch = struct {
.idx = next_idx, .idx = next_idx,
.highlight = tracked, .highlight = tracked,
}; };
return true;
} }
}; };
@ -972,7 +978,7 @@ test "select next" {
// Select our next match (first) // Select our next match (first)
try search.searchAll(); try search.searchAll();
try search.select(.next); _ = try search.select(.next);
{ {
const sel = search.selectedMatch().?.untracked(); const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
@ -986,7 +992,7 @@ test "select next" {
} }
// Next match // Next match
try search.select(.next); _ = try search.select(.next);
{ {
const sel = search.selectedMatch().?.untracked(); const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
@ -1000,7 +1006,7 @@ test "select next" {
} }
// Next match (no wrap) // Next match (no wrap)
try search.select(.next); _ = try search.select(.next);
{ {
const sel = search.selectedMatch().?.untracked(); const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
@ -1026,8 +1032,8 @@ test "select in active changes contents completely" {
var search: ScreenSearch = try .init(alloc, t.screens.active, "Fizz"); var search: ScreenSearch = try .init(alloc, t.screens.active, "Fizz");
defer search.deinit(); defer search.deinit();
try search.searchAll(); try search.searchAll();
try search.select(.next); _ = try search.select(.next);
try search.select(.next); _ = try search.select(.next);
{ {
// Initial selection is the first fizz // Initial selection is the first fizz
const sel = search.selectedMatch().?.untracked(); const sel = search.selectedMatch().?.untracked();
@ -1101,7 +1107,7 @@ test "select into history" {
try search.searchAll(); try search.searchAll();
// Get all matches // Get all matches
try search.select(.next); _ = try search.select(.next);
{ {
const sel = search.selectedMatch().?.untracked(); const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
@ -1167,7 +1173,7 @@ test "select prev" {
// Select prev (oldest first) // Select prev (oldest first)
try search.searchAll(); try search.searchAll();
try search.select(.prev); _ = try search.select(.prev);
{ {
const sel = search.selectedMatch().?.untracked(); const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
@ -1181,7 +1187,7 @@ test "select prev" {
} }
// Prev match (towards newest) // Prev match (towards newest)
try search.select(.prev); _ = try search.select(.prev);
{ {
const sel = search.selectedMatch().?.untracked(); const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
@ -1195,7 +1201,7 @@ test "select prev" {
} }
// Prev match (no wrap, stays at newest) // Prev match (no wrap, stays at newest)
try search.select(.prev); _ = try search.select(.prev);
{ {
const sel = search.selectedMatch().?.untracked(); const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
@ -1223,7 +1229,7 @@ test "select prev then next" {
try search.searchAll(); try search.searchAll();
// Select next (newest first) // Select next (newest first)
try search.select(.next); _ = try search.select(.next);
{ {
const sel = search.selectedMatch().?.untracked(); const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
@ -1233,7 +1239,7 @@ test "select prev then next" {
} }
// Select next (older) // Select next (older)
try search.select(.next); _ = try search.select(.next);
{ {
const sel = search.selectedMatch().?.untracked(); const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
@ -1243,7 +1249,7 @@ test "select prev then next" {
} }
// Select prev (back to newer) // Select prev (back to newer)
try search.select(.prev); _ = try search.select(.prev);
{ {
const sel = search.selectedMatch().?.untracked(); const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
@ -1276,7 +1282,7 @@ test "select prev with history" {
try search.searchAll(); try search.searchAll();
// Select prev (oldest first, should be in history) // Select prev (oldest first, should be in history)
try search.select(.prev); _ = try search.select(.prev);
{ {
const sel = search.selectedMatch().?.untracked(); const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
@ -1290,7 +1296,7 @@ test "select prev with history" {
} }
// Select prev (towards newer, should move to active area) // Select prev (towards newer, should move to active area)
try search.select(.prev); _ = try search.select(.prev);
{ {
const sel = search.selectedMatch().?.untracked(); const sel = search.selectedMatch().?.untracked();
try testing.expectEqual(point.Point{ .active = .{ try testing.expectEqual(point.Point{ .active = .{