This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(terminal): avoid integer overflow in selectPrev with no active matches (#12936)

Also found when test searching.

Run Ghostty debug on macOS and follow these steps:

1. Open Ghostty, `cat src/Surface.zig` and start search
`self.startClipboardRequest`.
2. Click up button(Press enter) 6 times and click down button (Press
shift+enter) 6 times.
3. You should see a panic crash.

### AI Disclosure
Claude implemented the fix and the unit test.

I reviewed it and tested it myself.

+67 -1
+67 -1
src/terminal/search/screen.zig
··· 798 798 799 799 const active_len = self.active_results.items.len; 800 800 const history_len = self.history_results.items.len; 801 - const next_idx = if (prev.idx != 0) prev.idx - 1 else active_len - 1 + history_len; 801 + const next_idx = if (prev.idx != 0) prev.idx - 1 else active_len + history_len - 1; 802 802 803 803 const hl: FlattenedHighlight = if (next_idx < active_len) 804 804 self.active_results.items[active_len - 1 - next_idx] ··· 1378 1378 .y = 1, 1379 1379 } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); 1380 1380 } 1381 + } 1382 + 1383 + test "select prev wraps when all matches are in history" { 1384 + // Regression test: when every match is in scrollback (the active area 1385 + // has none, so active_len == 0), selecting prev from index 0 must wrap 1386 + // to the last result without underflowing `active_len - 1`. 1387 + const alloc = testing.allocator; 1388 + var t: Terminal = try .init(alloc, .{ 1389 + .cols = 10, 1390 + .rows = 2, 1391 + .max_scrollback = std.math.maxInt(usize), 1392 + }); 1393 + defer t.deinit(alloc); 1394 + const list: *PageList = &t.screens.active.pages; 1395 + 1396 + var s = t.vtStream(); 1397 + defer s.deinit(); 1398 + 1399 + // Put the only match in scrollback, then scroll the active area to all 1400 + // blank lines so it contains no match (active_len == 0, history_len == 1). 1401 + s.nextSlice("Fizz\r\n"); 1402 + while (list.totalPages() < 3) s.nextSlice("\r\n"); 1403 + for (0..list.rows) |_| s.nextSlice("\r\n"); 1404 + 1405 + var search: ScreenSearch = try .init(alloc, t.screens.active, "Fizz"); 1406 + defer search.deinit(); 1407 + try search.searchAll(); 1408 + try testing.expectEqual(0, search.active_results.items.len); 1409 + 1410 + // Select the first match (idx 0), then wrap backwards. This must not 1411 + // panic and must keep a valid selection. 1412 + _ = try search.select(.next); 1413 + _ = try search.select(.prev); 1414 + try testing.expect(search.selectedMatch() != null); 1415 + } 1416 + 1417 + test "select after all matches disappear drops the selection" { 1418 + // The wrap arithmetic in selectPrev (active_len + history_len - 1) would 1419 + // underflow if a selection were ever live while both result lists are 1420 + // empty. This guards the invariant that makes that unreachable: when a 1421 + // reload/prune empties the results, the selection is dropped, so the next 1422 + // select() hits the "no matches" guard instead of the wrap arithmetic. 1423 + const alloc = testing.allocator; 1424 + var t: Terminal = try .init(alloc, .{ .cols = 10, .rows = 2 }); 1425 + defer t.deinit(alloc); 1426 + 1427 + var s = t.vtStream(); 1428 + defer s.deinit(); 1429 + s.nextSlice("Fizz"); 1430 + 1431 + var search: ScreenSearch = try .init(alloc, t.screens.active, "Fizz"); 1432 + defer search.deinit(); 1433 + try search.searchAll(); 1434 + try testing.expectEqual(1, search.active_results.items.len); 1435 + 1436 + // Take a selection, then overwrite the only match so a reload finds none 1437 + // (active and history both empty). 1438 + _ = try search.select(.next); 1439 + try testing.expect(search.selectedMatch() != null); 1440 + s.nextSlice("\x1b[1;1H "); 1441 + 1442 + // Must not underflow; the selection is dropped and nothing is selected. 1443 + _ = try search.select(.prev); 1444 + try testing.expect(search.selectedMatch() == null); 1445 + try testing.expectEqual(0, search.active_results.items.len); 1446 + try testing.expectEqual(0, search.history_results.items.len); 1381 1447 } 1382 1448 1383 1449 test "screen search no scrollback has no history" {