terminal: ScreenSearch to search a single terminal screen
parent
7b26e6319e
commit
d349cc8932
|
|
@ -3858,8 +3858,9 @@ fn totalRows(self: *const PageList) usize {
|
|||
return rows;
|
||||
}
|
||||
|
||||
/// The total number of pages in this list.
|
||||
fn totalPages(self: *const PageList) usize {
|
||||
/// The total number of pages in this list. This should only be used
|
||||
/// for tests since it is O(N) over the list of pages.
|
||||
pub fn totalPages(self: *const PageList) usize {
|
||||
var pages: usize = 0;
|
||||
var node_ = self.pages.first;
|
||||
while (node_) |node| {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
pub const Active = @import("search/active.zig").ActiveSearch;
|
||||
pub const PageList = @import("search/pagelist.zig").PageListSearch;
|
||||
pub const Screen = @import("search/screen.zig").ScreenSearch;
|
||||
pub const Thread = @import("search/Thread.zig");
|
||||
|
||||
test {
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@ pub const ActiveSearch = struct {
|
|||
/// 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.
|
||||
/// This returns the first page (in reverse order) covered by this
|
||||
/// search. This allows the history search to overlap and search history.
|
||||
/// There CAN BE duplicates, and this page CAN BE mutable, so the history
|
||||
/// search results should prune anything that's in the active area.
|
||||
///
|
||||
/// If the return value is null it means the active area covers the entire
|
||||
/// PageList, currently.
|
||||
|
|
@ -59,8 +59,10 @@ pub const ActiveSearch = struct {
|
|||
// First up, add enough pages to cover the active area.
|
||||
var rem: usize = list.rows;
|
||||
var node_ = list.pages.last;
|
||||
var last_node: ?*PageList.List.Node = null;
|
||||
while (node_) |node| : (node_ = node.prev) {
|
||||
_ = try self.window.append(node);
|
||||
last_node = node;
|
||||
|
||||
// If we reached our target amount, then this is the last
|
||||
// page that contains the active area. We go to the previous
|
||||
|
|
@ -76,18 +78,20 @@ pub const ActiveSearch = struct {
|
|||
|
||||
// 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) {
|
||||
// If the last row of this node isn't wrapped we can't overlap.
|
||||
const row = node.data.getRow(node.data.size.rows - 1);
|
||||
if (!row.wrap) break;
|
||||
|
||||
// We could be more accurate here and count bytes since the
|
||||
// last wrap but its complicated and unlikely multiple pages
|
||||
// wrap so this should be fine.
|
||||
const added = try self.window.append(node);
|
||||
if (added >= rem) {
|
||||
node_ = node.prev;
|
||||
break;
|
||||
}
|
||||
rem -= added;
|
||||
if (added >= self.window.needle.len - 1) break;
|
||||
}
|
||||
|
||||
// Return the first page NOT covered by the active area.
|
||||
return node_;
|
||||
// Return the last node we added to our window.
|
||||
return last_node;
|
||||
}
|
||||
|
||||
/// Find the next match for the needle in the active area. This returns
|
||||
|
|
|
|||
|
|
@ -1,33 +1,614 @@
|
|||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const point = @import("../point.zig");
|
||||
const PageList = @import("../PageList.zig");
|
||||
const Pin = PageList.Pin;
|
||||
const Screen = @import("../Screen.zig");
|
||||
const Active = @import("active.zig").ActiveSearch;
|
||||
const Selection = @import("../Selection.zig");
|
||||
const Terminal = @import("../Terminal.zig");
|
||||
const ActiveSearch = @import("active.zig").ActiveSearch;
|
||||
const PageListSearch = @import("pagelist.zig").PageListSearch;
|
||||
const SlidingWindow = @import("sliding_window.zig").SlidingWindow;
|
||||
|
||||
/// Searches for a needle within a Screen, handling active area updates,
|
||||
/// pages being pruned from the screen (e.g. scrollback limits), and more.
|
||||
///
|
||||
/// Unlike our lower-level searchers (like ActiveSearch and PageListSearch),
|
||||
/// this will cache and store all search results so the caller can re-access
|
||||
/// them as needed. This structure does this because it is intended to help
|
||||
/// the caller handle the case where the Screen is changing while the user
|
||||
/// is searching.
|
||||
///
|
||||
/// An inactive screen can continue to be searched in the background, and when
|
||||
/// screen state changes, the renderer/caller can access the existing search
|
||||
/// results without needing to re-search everything. This prevents a particularly
|
||||
/// nasty UX where going to alt screen (e.g. neovim) and then back would
|
||||
/// restart the full scrollback search.
|
||||
pub const ScreenSearch = struct {
|
||||
/// The screen being searched.
|
||||
screen: *Screen,
|
||||
|
||||
/// The active area search state
|
||||
active: Active,
|
||||
active: ActiveSearch,
|
||||
|
||||
/// The history (scrollback) search state. May be null if there is
|
||||
/// no history yet.
|
||||
history: ?HistorySearch,
|
||||
|
||||
/// Current state of the search, a state machine.
|
||||
state: State,
|
||||
|
||||
/// The results found so far. These are stored separately because history
|
||||
/// is mostly immutable once found, while active area results may
|
||||
/// change. This lets us easily reset the active area results for a
|
||||
/// re-search scenario.
|
||||
history_results: std.ArrayList(Selection),
|
||||
active_results: std.ArrayList(Selection),
|
||||
|
||||
/// History search state.
|
||||
const HistorySearch = struct {
|
||||
/// The actual searcher state.
|
||||
searcher: PageListSearch,
|
||||
|
||||
/// The pin for the first node that this searcher is searching from.
|
||||
/// We use this when the active area changes to find the diff between
|
||||
/// the top of the new active area and the previous start point
|
||||
/// to determine if we need to search more history.
|
||||
start_pin: *Pin,
|
||||
|
||||
pub fn deinit(self: *HistorySearch, screen: *Screen) void {
|
||||
self.searcher.deinit();
|
||||
screen.pages.untrackPin(self.start_pin);
|
||||
}
|
||||
};
|
||||
|
||||
/// Search state machine
|
||||
const State = enum {
|
||||
/// Currently searching the active area
|
||||
active,
|
||||
|
||||
/// Currently searching the history area
|
||||
history,
|
||||
|
||||
/// History search is waiting for more data to be fed before
|
||||
/// it can progress.
|
||||
history_feed,
|
||||
|
||||
/// Search is complete given the current terminal state.
|
||||
complete,
|
||||
};
|
||||
|
||||
// Initialize a screen search for the given screen and needle.
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
screen: *const Screen,
|
||||
screen: *Screen,
|
||||
needle: []const u8,
|
||||
) Allocator.Error!ScreenSearch {
|
||||
_ = screen;
|
||||
var result: ScreenSearch = .{
|
||||
.screen = screen,
|
||||
.active = try .init(alloc, needle),
|
||||
.history = null,
|
||||
.state = .active,
|
||||
.active_results = .empty,
|
||||
.history_results = .empty,
|
||||
};
|
||||
errdefer result.deinit();
|
||||
|
||||
// Setup our active area search
|
||||
var active: Active = try .init(alloc, needle);
|
||||
errdefer active.deinit();
|
||||
// Update our initial active area state
|
||||
try result.reloadActive();
|
||||
|
||||
// Store our screen
|
||||
return result;
|
||||
}
|
||||
|
||||
return .{
|
||||
.active = active,
|
||||
pub fn deinit(self: *ScreenSearch) void {
|
||||
const alloc = self.allocator();
|
||||
self.active.deinit();
|
||||
if (self.history) |*h| h.deinit(self.screen);
|
||||
self.active_results.deinit(alloc);
|
||||
self.history_results.deinit(alloc);
|
||||
}
|
||||
|
||||
fn allocator(self: *ScreenSearch) Allocator {
|
||||
return self.active.window.alloc;
|
||||
}
|
||||
|
||||
pub const TickError = Allocator.Error || error{
|
||||
FeedRequired,
|
||||
SearchComplete,
|
||||
};
|
||||
|
||||
/// Returns all matches as an owned slice (caller must free).
|
||||
/// The matches are ordered from most recent to oldest (e.g. bottom
|
||||
/// of the screen to top of the screen).
|
||||
///
|
||||
/// This handles pruning overlapping results between active area
|
||||
/// and the history area so you should use this instead of accessing
|
||||
/// the result slices directly.
|
||||
pub fn matches(
|
||||
self: *ScreenSearch,
|
||||
alloc: Allocator,
|
||||
) Allocator.Error![]Selection {
|
||||
const active_results = self.active_results.items;
|
||||
const history_results: []const Selection = if (self.history) |*h| history_results: {
|
||||
// We prune all the history results that start in our first
|
||||
// history page because the active area will overlap and
|
||||
// get that.
|
||||
for (self.history_results.items, 0..) |sel, i| {
|
||||
if (sel.start().node != h.start_pin.node) {
|
||||
break :history_results self.history_results.items[i..];
|
||||
}
|
||||
}
|
||||
|
||||
break :history_results &.{};
|
||||
} else &.{};
|
||||
|
||||
const results = try alloc.alloc(
|
||||
Selection,
|
||||
active_results.len + history_results.len,
|
||||
);
|
||||
errdefer alloc.free(results);
|
||||
|
||||
// Active does a forward search, so we add the active results then
|
||||
// reverse them. There are usually not many active results so this
|
||||
// is fast enough compared to adding them in reverse order.
|
||||
assert(self.active.window.direction == .forward);
|
||||
@memcpy(
|
||||
results[0..active_results.len],
|
||||
active_results,
|
||||
);
|
||||
std.mem.reverse(Selection, results[0..active_results.len]);
|
||||
|
||||
// History does a backward search, so we can just append them
|
||||
// after.
|
||||
@memcpy(
|
||||
results[active_results.len..],
|
||||
history_results,
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// Search the full screen state. This will block until the search
|
||||
/// is complete. For performance, it is recommended to use `tick` and
|
||||
/// `feed` to incrementally make progress on the search instead.
|
||||
pub fn searchAll(self: *ScreenSearch) Allocator.Error!void {
|
||||
while (true) {
|
||||
self.tick() catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.FeedRequired => try self.feed(),
|
||||
error.SearchComplete => return,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Make incremental progress on the search without accessing any
|
||||
/// screen state (so no lock is required).
|
||||
///
|
||||
/// This will return error.FeedRequired if the search cannot make progress
|
||||
/// without being fed more data. In this case, the caller should call
|
||||
/// the `feed` function to provide more data to the searcher.
|
||||
///
|
||||
/// This will return error.SearchComplete if the search is fully complete.
|
||||
/// This is to signal to the caller that it can move to a more efficient
|
||||
/// sleep/wait state until there is more work to do (e.g. new data to feed).
|
||||
pub fn tick(self: *ScreenSearch) TickError!void {
|
||||
switch (self.state) {
|
||||
.active => try self.tickActive(),
|
||||
.history => try self.tickHistory(),
|
||||
.history_feed => return error.FeedRequired,
|
||||
.complete => return error.SearchComplete,
|
||||
}
|
||||
}
|
||||
|
||||
/// Feed more data to the searcher so it can continue searching. This
|
||||
/// accesses the screen state, so the caller must hold the necessary locks.
|
||||
pub fn feed(self: *ScreenSearch) Allocator.Error!void {
|
||||
const history: *PageListSearch = if (self.history) |*h| &h.searcher else {
|
||||
// No history to feed, search is complete.
|
||||
self.state = .complete;
|
||||
return;
|
||||
};
|
||||
|
||||
// Future: we may want to feed multiple pages at once here to
|
||||
// lower the frequency of lock acquisitions.
|
||||
if (!try history.feed()) {
|
||||
// No more data to feed, search is complete.
|
||||
self.state = .complete;
|
||||
return;
|
||||
}
|
||||
|
||||
// Depending on our state handle where feed goes
|
||||
switch (self.state) {
|
||||
// If we're searching active or history, then feeding doesn't
|
||||
// change the state.
|
||||
.active, .history => {},
|
||||
|
||||
// Feed goes back to searching history.
|
||||
.history_feed => self.state = .history,
|
||||
|
||||
// If we're complete then the feed call above should always
|
||||
// return false and we can't reach this.
|
||||
.complete => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
fn tickActive(self: *ScreenSearch) Allocator.Error!void {
|
||||
// For the active area, we consume the entire search in one go
|
||||
// because the active area is generally small.
|
||||
const alloc = self.allocator();
|
||||
while (self.active.next()) |sel| {
|
||||
// If this fails, then we miss a result since `active.next()`
|
||||
// moves forward and prunes data. In the future, we may want
|
||||
// to have some more robust error handling but the only
|
||||
// scenario this would fail is OOM and we're probably in
|
||||
// deeper trouble at that point anyways.
|
||||
try self.active_results.append(alloc, sel);
|
||||
}
|
||||
|
||||
// We've consumed the entire active area, move to history.
|
||||
self.state = .history;
|
||||
}
|
||||
|
||||
fn tickHistory(self: *ScreenSearch) Allocator.Error!void {
|
||||
const history: *PageListSearch = if (self.history) |*h| &h.searcher else {
|
||||
// No history to search, we're done.
|
||||
self.state = .complete;
|
||||
return;
|
||||
};
|
||||
|
||||
// Try to consume all the loaded matches in one go, because
|
||||
// the search is generally fast for loaded data.
|
||||
const alloc = self.allocator();
|
||||
while (history.next()) |sel| {
|
||||
// Same note as tickActive for error handling.
|
||||
try self.history_results.append(alloc, sel);
|
||||
}
|
||||
|
||||
// We need to be fed more data.
|
||||
self.state = .history_feed;
|
||||
}
|
||||
|
||||
/// Reload the active area because it has changed.
|
||||
///
|
||||
/// Since it is very fast, this will also do the full active area
|
||||
/// search again, too. This avoids any complexity around the search
|
||||
/// state machine.
|
||||
///
|
||||
/// The caller must hold the necessary locks to access the screen state.
|
||||
pub fn reloadActive(self: *ScreenSearch) Allocator.Error!void {
|
||||
const list: *PageList = &self.screen.pages;
|
||||
if (try self.active.update(list)) |history_node| history: {
|
||||
// We need to account for any active area growth that would
|
||||
// cause new pages to move into our history. If there are new
|
||||
// pages then we need to re-search the pages and add it to
|
||||
// our history results.
|
||||
|
||||
const history_: ?*HistorySearch = if (self.history) |*h| state: {
|
||||
// If our start pin became garbage, it means we pruned all
|
||||
// the way up through it, so we have no history anymore.
|
||||
// Reset our history state.
|
||||
if (h.start_pin.garbage) {
|
||||
h.deinit(self.screen);
|
||||
self.history = null;
|
||||
self.history_results.clearRetainingCapacity();
|
||||
break :state null;
|
||||
}
|
||||
|
||||
break :state h;
|
||||
} else null;
|
||||
|
||||
const history = history_ orelse {
|
||||
// No history search yet, but we now have history. So let's
|
||||
// initialize.
|
||||
|
||||
// Our usage of needle below depends on this
|
||||
assert(self.active.window.direction == .forward);
|
||||
|
||||
var search: PageListSearch = try .init(
|
||||
self.allocator(),
|
||||
self.active.window.needle,
|
||||
list,
|
||||
history_node,
|
||||
);
|
||||
errdefer search.deinit();
|
||||
|
||||
const pin = try list.trackPin(.{ .node = history_node });
|
||||
errdefer list.untrackPin(pin);
|
||||
|
||||
self.history = .{
|
||||
.searcher = search,
|
||||
.start_pin = pin,
|
||||
};
|
||||
|
||||
// We don't need to update any history since we had no history
|
||||
// before, so we can break out of the whole conditional.
|
||||
break :history;
|
||||
};
|
||||
|
||||
if (history.start_pin.node == history_node) {
|
||||
// No change in the starting node, we're done.
|
||||
break :history;
|
||||
}
|
||||
|
||||
// We had prior history with a valid pin and our current
|
||||
// starting history node doesn't match our previous. So there is
|
||||
// a small delta (usually small) that we need to search and update
|
||||
// our history results.
|
||||
const old_node = history.start_pin.node;
|
||||
|
||||
// Do a forward search from our prior node to this one. We
|
||||
// collect all the results into a new list. We ASSUME that
|
||||
// reloadActive is being called frequently enough that there isn't
|
||||
// a massive amount of history to search here.
|
||||
const alloc = self.allocator();
|
||||
var window: SlidingWindow = try .init(
|
||||
alloc,
|
||||
.forward,
|
||||
self.active.window.needle,
|
||||
);
|
||||
defer window.deinit();
|
||||
while (true) {
|
||||
_ = try window.append(history.start_pin.node);
|
||||
if (history.start_pin.node == history_node) break;
|
||||
const next = history.start_pin.node.next orelse break;
|
||||
history.start_pin.node = next;
|
||||
}
|
||||
assert(history.start_pin.node == history_node);
|
||||
|
||||
var results: std.ArrayList(Selection) = try .initCapacity(
|
||||
alloc,
|
||||
self.history_results.items.len,
|
||||
);
|
||||
errdefer results.deinit(alloc);
|
||||
while (window.next()) |sel| try results.append(
|
||||
alloc,
|
||||
sel,
|
||||
);
|
||||
|
||||
// If we have no matches then there is nothing to change
|
||||
// in our history (fast path)
|
||||
if (results.items.len == 0) break :history;
|
||||
|
||||
// Matches! Reverse our list then append all the remaining
|
||||
// history items that didn't start on our original node.
|
||||
std.mem.reverse(Selection, results.items);
|
||||
for (self.history_results.items, 0..) |sel, i| {
|
||||
if (sel.start().node != old_node) {
|
||||
try results.appendSlice(alloc, self.history_results.items[i..]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.history_results.deinit(alloc);
|
||||
self.history_results = results;
|
||||
}
|
||||
|
||||
// Reset our active search results and search again.
|
||||
self.active_results.clearRetainingCapacity();
|
||||
switch (self.state) {
|
||||
// If we're in the active state we run a normal tick so
|
||||
// we can move into a better state.
|
||||
.active => try self.tickActive(),
|
||||
|
||||
// Otherwise, just tick it and move back to whatever state
|
||||
// we were in.
|
||||
else => {
|
||||
const old_state = self.state;
|
||||
defer self.state = old_state;
|
||||
try self.tickActive();
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "simple search" {
|
||||
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.screen, "Fizz");
|
||||
defer search.deinit();
|
||||
try search.searchAll();
|
||||
try testing.expectEqual(2, search.active_results.items.len);
|
||||
// We don't test history results since there is overlap
|
||||
|
||||
// Get all matches
|
||||
const matches = try search.matches(alloc);
|
||||
defer alloc.free(matches);
|
||||
try testing.expectEqual(2, matches.len);
|
||||
|
||||
{
|
||||
const sel = matches[0];
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 0,
|
||||
.y = 2,
|
||||
} }, t.screen.pages.pointFromPin(.screen, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 3,
|
||||
.y = 2,
|
||||
} }, t.screen.pages.pointFromPin(.screen, sel.end()).?);
|
||||
}
|
||||
{
|
||||
const sel = matches[1];
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
} }, t.screen.pages.pointFromPin(.screen, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 3,
|
||||
.y = 0,
|
||||
} }, t.screen.pages.pointFromPin(.screen, sel.end()).?);
|
||||
}
|
||||
}
|
||||
|
||||
test "simple search 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.screen.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("hello.");
|
||||
|
||||
var search: ScreenSearch = try .init(alloc, &t.screen, "Fizz");
|
||||
defer search.deinit();
|
||||
try search.searchAll();
|
||||
try testing.expectEqual(0, search.active_results.items.len);
|
||||
|
||||
// Get all matches
|
||||
const matches = try search.matches(alloc);
|
||||
defer alloc.free(matches);
|
||||
try testing.expectEqual(1, matches.len);
|
||||
|
||||
{
|
||||
const sel = matches[0];
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
} }, t.screen.pages.pointFromPin(.screen, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 3,
|
||||
.y = 0,
|
||||
} }, t.screen.pages.pointFromPin(.screen, sel.end()).?);
|
||||
}
|
||||
}
|
||||
|
||||
test "reload active with history change" {
|
||||
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.screen.pages;
|
||||
|
||||
var s = t.vtStream();
|
||||
defer s.deinit();
|
||||
try s.nextSlice("Fizz\r\n");
|
||||
|
||||
// Start up our search which will populate our initial active area.
|
||||
var search: ScreenSearch = try .init(alloc, &t.screen, "Fizz");
|
||||
defer search.deinit();
|
||||
try search.searchAll();
|
||||
{
|
||||
const matches = try search.matches(alloc);
|
||||
defer alloc.free(matches);
|
||||
try testing.expectEqual(1, matches.len);
|
||||
}
|
||||
|
||||
// Grow into two pages so our history pin will move.
|
||||
while (list.totalPages() < 2) try s.nextSlice("\r\n");
|
||||
for (0..list.rows) |_| try s.nextSlice("\r\n");
|
||||
try s.nextSlice("2Fizz");
|
||||
|
||||
// Active area changed so reload
|
||||
try search.reloadActive();
|
||||
try search.searchAll();
|
||||
|
||||
// Get all matches
|
||||
{
|
||||
const matches = try search.matches(alloc);
|
||||
defer alloc.free(matches);
|
||||
try testing.expectEqual(2, matches.len);
|
||||
{
|
||||
const sel = matches[1];
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
} }, t.screen.pages.pointFromPin(.screen, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 3,
|
||||
.y = 0,
|
||||
} }, t.screen.pages.pointFromPin(.screen, sel.end()).?);
|
||||
}
|
||||
{
|
||||
const sel = matches[0];
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 1,
|
||||
.y = 1,
|
||||
} }, t.screen.pages.pointFromPin(.active, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 4,
|
||||
.y = 1,
|
||||
} }, t.screen.pages.pointFromPin(.active, sel.end()).?);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the screen which will make our pin garbage.
|
||||
t.fullReset();
|
||||
try s.nextSlice("WeFizzing");
|
||||
try search.reloadActive();
|
||||
try search.searchAll();
|
||||
|
||||
{
|
||||
const matches = try search.matches(alloc);
|
||||
defer alloc.free(matches);
|
||||
try testing.expectEqual(1, matches.len);
|
||||
{
|
||||
const sel = matches[0];
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 2,
|
||||
.y = 0,
|
||||
} }, t.screen.pages.pointFromPin(.active, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 5,
|
||||
.y = 0,
|
||||
} }, t.screen.pages.pointFromPin(.active, sel.end()).?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "active change contents" {
|
||||
const alloc = testing.allocator;
|
||||
var t: Terminal = try .init(alloc, .{ .cols = 10, .rows = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
var s = t.vtStream();
|
||||
defer s.deinit();
|
||||
try s.nextSlice("Fuzz\r\nBuzz\r\nFizz\r\nBang");
|
||||
|
||||
var search: ScreenSearch = try .init(alloc, &t.screen, "Fizz");
|
||||
defer search.deinit();
|
||||
try search.searchAll();
|
||||
try testing.expectEqual(1, search.active_results.items.len);
|
||||
|
||||
// Erase the screen, move our cursor to the top, and change contents.
|
||||
try s.nextSlice("\x1b[2J\x1b[H"); // Clear screen and move home
|
||||
try s.nextSlice("Bang\r\nFizz\r\nHello!");
|
||||
|
||||
try search.reloadActive();
|
||||
try search.searchAll();
|
||||
try testing.expectEqual(1, search.active_results.items.len);
|
||||
|
||||
// Get all matches
|
||||
const matches = try search.matches(alloc);
|
||||
defer alloc.free(matches);
|
||||
try testing.expectEqual(1, matches.len);
|
||||
|
||||
{
|
||||
const sel = matches[0];
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 0,
|
||||
.y = 1,
|
||||
} }, t.screen.pages.pointFromPin(.screen, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 3,
|
||||
.y = 1,
|
||||
} }, t.screen.pages.pointFromPin(.screen, sel.end()).?);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue