mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-10 11:19:45 +00:00
terminal/search: ignore empty search needles
Low-level search state accepted an empty needle even though the search thread normally filters it out. SlidingWindow treated the empty string as a zero-length match and underflowed while calculating its inclusive end offset. Active and viewport overlap calculations could also underflow while loading adjacent pages. Treat an empty needle as an inactive search with no matches or history, and saturate the viewport overlap length.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user