terminal: ActiveSearch for searching the active area
parent
43835d1468
commit
0ea350a8f2
|
|
@ -1,5 +1,6 @@
|
|||
//! Search functionality for the terminal.
|
||||
|
||||
pub const Active = @import("search/active.zig").ActiveSearch;
|
||||
pub const PageList = @import("search/pagelist.zig").PageListSearch;
|
||||
pub const Thread = @import("search/Thread.zig");
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const point = @import("../point.zig");
|
||||
const size = @import("../size.zig");
|
||||
const PageList = @import("../PageList.zig");
|
||||
const Selection = @import("../Selection.zig");
|
||||
const SlidingWindow = @import("sliding_window.zig").SlidingWindow;
|
||||
const Terminal = @import("../Terminal.zig");
|
||||
|
||||
/// Searches for a substring within the active area of a PageList.
|
||||
///
|
||||
/// The distinction for "active area" is important because it is the
|
||||
/// only part of a PageList that is mutable. Therefore, its the only part
|
||||
/// of the terminal that needs to be repeatedly searched as the contents
|
||||
/// change.
|
||||
///
|
||||
/// This struct specializes in searching only within that active area,
|
||||
/// and handling the active area moving as new lines are added to the bottom.
|
||||
pub const ActiveSearch = struct {
|
||||
window: SlidingWindow,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
needle: []const u8,
|
||||
) Allocator.Error!ActiveSearch {
|
||||
// We just do a forward search since the active area is usually
|
||||
// pretty small so search results are instant anyways. This avoids
|
||||
// a small amount of work to reverse things.
|
||||
var window: SlidingWindow = try .init(alloc, .forward, needle);
|
||||
errdefer window.deinit();
|
||||
return .{ .window = window };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ActiveSearch) void {
|
||||
self.window.deinit();
|
||||
}
|
||||
|
||||
/// Update the active area to reflect the current state of the PageList.
|
||||
///
|
||||
/// This doesn't do the search, it only copies the necessary data
|
||||
/// to perform the search later. This lets the caller hold the lock
|
||||
/// on the PageList for a minimal amount of time.
|
||||
///
|
||||
/// This returns the first page (in reverse order) NOT searched by
|
||||
/// this active area. This is useful for callers that want to follow up
|
||||
/// with populating the scrollback searcher. The scrollback searcher
|
||||
/// should start searching from the returned page backwards.
|
||||
///
|
||||
/// If the return value is null it means the active area covers the entire
|
||||
/// PageList, currently.
|
||||
pub fn update(
|
||||
self: *ActiveSearch,
|
||||
list: *const PageList,
|
||||
) Allocator.Error!?*PageList.List.Node {
|
||||
// Clear our previous sliding window
|
||||
self.window.clearAndRetainCapacity();
|
||||
|
||||
// First up, add enough pages to cover the active area.
|
||||
var rem: usize = list.rows;
|
||||
var node_ = list.pages.last;
|
||||
while (node_) |node| : (node_ = node.prev) {
|
||||
_ = try self.window.append(node);
|
||||
|
||||
// If we reached our target amount, then this is the last
|
||||
// page that contains the active area. We go to the previous
|
||||
// page once more since its the first page of our required
|
||||
// overlap.
|
||||
if (rem <= node.data.size.rows) {
|
||||
node_ = node.prev;
|
||||
break;
|
||||
}
|
||||
|
||||
rem -= node.data.size.rows;
|
||||
}
|
||||
|
||||
// Next, add enough overlap to cover needle.len - 1 bytes (if it
|
||||
// exists) so we can cover the overlap.
|
||||
rem = self.window.needle.len - 1;
|
||||
while (node_) |node| : (node_ = node.prev) {
|
||||
const added = try self.window.append(node);
|
||||
if (added >= rem) {
|
||||
node_ = node.prev;
|
||||
break;
|
||||
}
|
||||
rem -= added;
|
||||
}
|
||||
|
||||
// Return the first page NOT covered by the active area.
|
||||
return node_;
|
||||
}
|
||||
|
||||
/// Find the next match for the needle in the active area. This returns
|
||||
/// null when there are no more matches.
|
||||
pub fn next(self: *ActiveSearch) ?Selection {
|
||||
return self.window.next();
|
||||
}
|
||||
};
|
||||
|
||||
test "simple search" {
|
||||
const alloc = testing.allocator;
|
||||
var t: Terminal = try .init(alloc, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
var s = t.vtStream();
|
||||
defer s.deinit();
|
||||
try s.nextSlice("Fizz\r\nBuzz\r\nFizz\r\nBang");
|
||||
|
||||
var search: ActiveSearch = try .init(alloc, "Fizz");
|
||||
defer search.deinit();
|
||||
_ = try search.update(&t.screen.pages);
|
||||
|
||||
{
|
||||
const sel = search.next().?;
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
} }, t.screen.pages.pointFromPin(.active, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 3,
|
||||
.y = 0,
|
||||
} }, t.screen.pages.pointFromPin(.active, sel.end()).?);
|
||||
}
|
||||
{
|
||||
const sel = search.next().?;
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 0,
|
||||
.y = 2,
|
||||
} }, t.screen.pages.pointFromPin(.active, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 3,
|
||||
.y = 2,
|
||||
} }, t.screen.pages.pointFromPin(.active, sel.end()).?);
|
||||
}
|
||||
try testing.expect(search.next() == null);
|
||||
}
|
||||
|
||||
test "clear screen and search" {
|
||||
const alloc = testing.allocator;
|
||||
var t: Terminal = try .init(alloc, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
var s = t.vtStream();
|
||||
defer s.deinit();
|
||||
try s.nextSlice("Fizz\r\nBuzz\r\nFizz\r\nBang");
|
||||
|
||||
var search: ActiveSearch = try .init(alloc, "Fizz");
|
||||
defer search.deinit();
|
||||
_ = try search.update(&t.screen.pages);
|
||||
|
||||
try s.nextSlice("\x1b[2J"); // Clear screen
|
||||
try s.nextSlice("\x1b[H"); // Move cursor home
|
||||
try s.nextSlice("Buzz\r\nFizz\r\nBuzz");
|
||||
_ = try search.update(&t.screen.pages);
|
||||
|
||||
{
|
||||
const sel = search.next().?;
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 0,
|
||||
.y = 1,
|
||||
} }, t.screen.pages.pointFromPin(.active, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 3,
|
||||
.y = 1,
|
||||
} }, t.screen.pages.pointFromPin(.active, sel.end()).?);
|
||||
}
|
||||
try testing.expect(search.next() == null);
|
||||
}
|
||||
|
|
@ -142,6 +142,14 @@ pub const SlidingWindow = struct {
|
|||
/// the window moves, the window will prune itself while maintaining
|
||||
/// the invariant that the window is always big enough to contain
|
||||
/// the needle.
|
||||
///
|
||||
/// It may seem wasteful to return a full selection, since the needle
|
||||
/// length is known it seems like we can get away with just returning
|
||||
/// the start index. However, returning a full selection will give us
|
||||
/// more flexibility in the future (e.g. if we want to support regex
|
||||
/// searches or other more complex searches). It does cost us some memory,
|
||||
/// but searches are expected to be relatively rare compared to normal
|
||||
/// operations and can eat up some extra memory temporarily.
|
||||
pub fn next(self: *SlidingWindow) ?Selection {
|
||||
const slices = slices: {
|
||||
// If we have less data then the needle then we can't possibly match
|
||||
|
|
@ -368,10 +376,14 @@ pub const SlidingWindow = struct {
|
|||
/// Add a new node to the sliding window. This will always grow
|
||||
/// the sliding window; data isn't pruned until it is consumed
|
||||
/// via a search (via next()).
|
||||
///
|
||||
/// Returns the number of bytes of content added to the sliding window.
|
||||
/// The total bytes will be larger since this omits metadata, but it is
|
||||
/// an accurate measure of the text content size added.
|
||||
pub fn append(
|
||||
self: *SlidingWindow,
|
||||
node: *PageList.List.Node,
|
||||
) Allocator.Error!void {
|
||||
) Allocator.Error!usize {
|
||||
// Initialize our metadata for the node.
|
||||
var meta: Meta = .{
|
||||
.node = node,
|
||||
|
|
@ -422,6 +434,7 @@ pub const SlidingWindow = struct {
|
|||
try self.meta.append(meta);
|
||||
|
||||
self.assertIntegrity();
|
||||
return written.len;
|
||||
}
|
||||
|
||||
/// Only for tests!
|
||||
|
|
@ -474,7 +487,7 @@ test "SlidingWindow single append" {
|
|||
// We want to test single-page cases.
|
||||
try testing.expect(s.pages.pages.first == s.pages.pages.last);
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node);
|
||||
_ = try w.append(node);
|
||||
|
||||
// We should be able to find two matches.
|
||||
{
|
||||
|
|
@ -517,7 +530,7 @@ test "SlidingWindow single append no match" {
|
|||
// We want to test single-page cases.
|
||||
try testing.expect(s.pages.pages.first == s.pages.pages.last);
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node);
|
||||
_ = try w.append(node);
|
||||
|
||||
// No matches
|
||||
try testing.expect(w.next() == null);
|
||||
|
|
@ -550,8 +563,8 @@ test "SlidingWindow two pages" {
|
|||
|
||||
// Add both pages
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node);
|
||||
try w.append(node.next.?);
|
||||
_ = try w.append(node);
|
||||
_ = try w.append(node.next.?);
|
||||
|
||||
// Search should find two matches
|
||||
{
|
||||
|
|
@ -602,8 +615,8 @@ test "SlidingWindow two pages match across boundary" {
|
|||
|
||||
// Add both pages
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node);
|
||||
try w.append(node.next.?);
|
||||
_ = try w.append(node);
|
||||
_ = try w.append(node.next.?);
|
||||
|
||||
// Search should find a match
|
||||
{
|
||||
|
|
@ -647,8 +660,8 @@ test "SlidingWindow two pages no match prunes first page" {
|
|||
|
||||
// Add both pages
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node);
|
||||
try w.append(node.next.?);
|
||||
_ = try w.append(node);
|
||||
_ = try w.append(node.next.?);
|
||||
|
||||
// Search should find nothing
|
||||
try testing.expect(w.next() == null);
|
||||
|
|
@ -688,8 +701,8 @@ test "SlidingWindow two pages no match keeps both pages" {
|
|||
|
||||
// Add both pages
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node);
|
||||
try w.append(node.next.?);
|
||||
_ = try w.append(node);
|
||||
_ = try w.append(node.next.?);
|
||||
|
||||
// Search should find nothing
|
||||
try testing.expect(w.next() == null);
|
||||
|
|
@ -717,8 +730,8 @@ test "SlidingWindow single append across circular buffer boundary" {
|
|||
// our implementation changes our test will fail.
|
||||
try testing.expect(s.pages.pages.first == s.pages.pages.last);
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node);
|
||||
try w.append(node);
|
||||
_ = try w.append(node);
|
||||
_ = try w.append(node);
|
||||
{
|
||||
// No wrap around yet
|
||||
const slices = w.data.getPtrSlice(0, w.data.len());
|
||||
|
|
@ -734,7 +747,7 @@ test "SlidingWindow single append across circular buffer boundary" {
|
|||
w.testChangeNeedle("boo");
|
||||
|
||||
// Add new page, now wraps
|
||||
try w.append(node);
|
||||
_ = try w.append(node);
|
||||
{
|
||||
const slices = w.data.getPtrSlice(0, w.data.len());
|
||||
try testing.expect(slices[0].len > 0);
|
||||
|
|
@ -772,8 +785,8 @@ test "SlidingWindow single append match on boundary" {
|
|||
// our implementation changes our test will fail.
|
||||
try testing.expect(s.pages.pages.first == s.pages.pages.last);
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node);
|
||||
try w.append(node);
|
||||
_ = try w.append(node);
|
||||
_ = try w.append(node);
|
||||
{
|
||||
// No wrap around yet
|
||||
const slices = w.data.getPtrSlice(0, w.data.len());
|
||||
|
|
@ -789,7 +802,7 @@ test "SlidingWindow single append match on boundary" {
|
|||
w.testChangeNeedle("boo!");
|
||||
|
||||
// Add new page, now wraps
|
||||
try w.append(node);
|
||||
_ = try w.append(node);
|
||||
{
|
||||
const slices = w.data.getPtrSlice(0, w.data.len());
|
||||
try testing.expect(slices[0].len > 0);
|
||||
|
|
@ -823,7 +836,7 @@ test "SlidingWindow single append reversed" {
|
|||
// We want to test single-page cases.
|
||||
try testing.expect(s.pages.pages.first == s.pages.pages.last);
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node);
|
||||
_ = try w.append(node);
|
||||
|
||||
// We should be able to find two matches.
|
||||
{
|
||||
|
|
@ -866,7 +879,7 @@ test "SlidingWindow single append no match reversed" {
|
|||
// We want to test single-page cases.
|
||||
try testing.expect(s.pages.pages.first == s.pages.pages.last);
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node);
|
||||
_ = try w.append(node);
|
||||
|
||||
// No matches
|
||||
try testing.expect(w.next() == null);
|
||||
|
|
@ -899,8 +912,8 @@ test "SlidingWindow two pages reversed" {
|
|||
|
||||
// Add both pages in reverse order
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node.next.?);
|
||||
try w.append(node);
|
||||
_ = try w.append(node.next.?);
|
||||
_ = try w.append(node);
|
||||
|
||||
// Search should find two matches (in reverse order)
|
||||
{
|
||||
|
|
@ -951,8 +964,8 @@ test "SlidingWindow two pages match across boundary reversed" {
|
|||
|
||||
// Add both pages in reverse order
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node.next.?);
|
||||
try w.append(node);
|
||||
_ = try w.append(node.next.?);
|
||||
_ = try w.append(node);
|
||||
|
||||
// Search should find a match
|
||||
{
|
||||
|
|
@ -997,8 +1010,8 @@ test "SlidingWindow two pages no match prunes first page reversed" {
|
|||
|
||||
// Add both pages in reverse order
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node.next.?);
|
||||
try w.append(node);
|
||||
_ = try w.append(node.next.?);
|
||||
_ = try w.append(node);
|
||||
|
||||
// Search should find nothing
|
||||
try testing.expect(w.next() == null);
|
||||
|
|
@ -1038,8 +1051,8 @@ test "SlidingWindow two pages no match keeps both pages reversed" {
|
|||
|
||||
// Add both pages in reverse order
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node.next.?);
|
||||
try w.append(node);
|
||||
_ = try w.append(node.next.?);
|
||||
_ = try w.append(node);
|
||||
|
||||
// Search should find nothing
|
||||
try testing.expect(w.next() == null);
|
||||
|
|
@ -1067,8 +1080,8 @@ test "SlidingWindow single append across circular buffer boundary reversed" {
|
|||
// our implementation changes our test will fail.
|
||||
try testing.expect(s.pages.pages.first == s.pages.pages.last);
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node);
|
||||
try w.append(node);
|
||||
_ = try w.append(node);
|
||||
_ = try w.append(node);
|
||||
{
|
||||
// No wrap around yet
|
||||
const slices = w.data.getPtrSlice(0, w.data.len());
|
||||
|
|
@ -1085,7 +1098,7 @@ test "SlidingWindow single append across circular buffer boundary reversed" {
|
|||
w.testChangeNeedle("oob");
|
||||
|
||||
// Add new page, now wraps
|
||||
try w.append(node);
|
||||
_ = try w.append(node);
|
||||
{
|
||||
const slices = w.data.getPtrSlice(0, w.data.len());
|
||||
try testing.expect(slices[0].len > 0);
|
||||
|
|
@ -1123,8 +1136,8 @@ test "SlidingWindow single append match on boundary reversed" {
|
|||
// our implementation changes our test will fail.
|
||||
try testing.expect(s.pages.pages.first == s.pages.pages.last);
|
||||
const node: *PageList.List.Node = s.pages.pages.first.?;
|
||||
try w.append(node);
|
||||
try w.append(node);
|
||||
_ = try w.append(node);
|
||||
_ = try w.append(node);
|
||||
{
|
||||
// No wrap around yet
|
||||
const slices = w.data.getPtrSlice(0, w.data.len());
|
||||
|
|
@ -1141,7 +1154,7 @@ test "SlidingWindow single append match on boundary reversed" {
|
|||
w.testChangeNeedle("!oob");
|
||||
|
||||
// Add new page, now wraps
|
||||
try w.append(node);
|
||||
_ = try w.append(node);
|
||||
{
|
||||
const slices = w.data.getPtrSlice(0, w.data.len());
|
||||
try testing.expect(slices[0].len > 0);
|
||||
|
|
|
|||
Loading…
Reference in New Issue