mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
terminal: working on a pagelist sliding window for search
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const CircBuf = @import("../datastruct/main.zig").CircBuf;
|
||||
const terminal = @import("main.zig");
|
||||
const point = terminal.point;
|
||||
const Page = terminal.Page;
|
||||
@ -8,6 +9,211 @@ const PageList = terminal.PageList;
|
||||
const Selection = terminal.Selection;
|
||||
const Screen = terminal.Screen;
|
||||
|
||||
pub const PageListSearch = struct {
|
||||
alloc: Allocator,
|
||||
|
||||
/// The list we're searching.
|
||||
list: *PageList,
|
||||
|
||||
/// The search term we're searching for.
|
||||
needle: []const u8,
|
||||
|
||||
/// The window is our sliding window of pages that we're searching so
|
||||
/// we can handle boundary cases where a needle is partially on the end
|
||||
/// of one page and the beginning of the next.
|
||||
///
|
||||
/// Note that we're not guaranteed to straddle exactly two pages. If
|
||||
/// the needle is large enough and/or the pages are small enough then
|
||||
/// the needle can straddle N pages. Additionally, pages aren't guaranteed
|
||||
/// to be equal size so we can't precompute the window size.
|
||||
window: SlidingWindow,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
list: *PageList,
|
||||
needle: []const u8,
|
||||
) !PageListSearch {
|
||||
var window = try CircBuf.init(alloc, 0);
|
||||
errdefer window.deinit();
|
||||
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.list = list,
|
||||
.current = list.pages.first,
|
||||
.needle = needle,
|
||||
.window = window,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *PageListSearch) void {
|
||||
_ = self;
|
||||
|
||||
// TODO: deinit window
|
||||
}
|
||||
};
|
||||
|
||||
/// The sliding window of the pages we're searching. The window is always
|
||||
/// big enough so that the needle can fit in it.
|
||||
const SlidingWindow = struct {
|
||||
/// The data buffer is a circular buffer of u8 that contains the
|
||||
/// encoded page text that we can use to search for the needle.
|
||||
data: DataBuf,
|
||||
|
||||
/// The meta buffer is a circular buffer that contains the metadata
|
||||
/// about the pages we're searching. This usually isn't that large
|
||||
/// so callers must iterate through it to find the offset to map
|
||||
/// data to meta.
|
||||
meta: MetaBuf,
|
||||
|
||||
const DataBuf = CircBuf(u8, 0);
|
||||
const MetaBuf = CircBuf(Meta, undefined);
|
||||
const Meta = struct {
|
||||
node: *PageList.List.Node,
|
||||
cell_map: Page.CellMap,
|
||||
|
||||
pub fn deinit(self: *Meta) void {
|
||||
self.cell_map.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn initEmpty(alloc: Allocator) Allocator.Error!SlidingWindow {
|
||||
var data = try DataBuf.init(alloc, 0);
|
||||
errdefer data.deinit(alloc);
|
||||
|
||||
var meta = try MetaBuf.init(alloc, 0);
|
||||
errdefer meta.deinit(alloc);
|
||||
|
||||
return .{
|
||||
.data = data,
|
||||
.meta = meta,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *SlidingWindow, alloc: Allocator) void {
|
||||
self.data.deinit(alloc);
|
||||
|
||||
var meta_it = self.meta.iterator(.forward);
|
||||
while (meta_it.next()) |meta| meta.deinit();
|
||||
self.meta.deinit(alloc);
|
||||
}
|
||||
|
||||
/// Add a new node to the sliding window.
|
||||
///
|
||||
/// The window will prune itself if it can while always maintaining
|
||||
/// the invariant that the `fixed_size` always fits within the window.
|
||||
///
|
||||
/// Note it is possible for the window to be smaller than `fixed_size`
|
||||
/// if not enough nodes have been added yet or the screen is just
|
||||
/// smaller than the needle.
|
||||
pub fn append(
|
||||
self: *SlidingWindow,
|
||||
alloc: Allocator,
|
||||
node: *PageList.List.Node,
|
||||
required_size: usize,
|
||||
) Allocator.Error!void {
|
||||
// Initialize our metadata for the node.
|
||||
var meta: Meta = .{
|
||||
.node = node,
|
||||
.cell_map = Page.CellMap.init(alloc),
|
||||
};
|
||||
errdefer meta.deinit();
|
||||
|
||||
// This is suboptimal but we need to encode the page once to
|
||||
// temporary memory, and then copy it into our circular buffer.
|
||||
// In the future, we should benchmark and see if we can encode
|
||||
// directly into the circular buffer.
|
||||
var encoded: std.ArrayListUnmanaged(u8) = .{};
|
||||
defer encoded.deinit(alloc);
|
||||
|
||||
// Encode the page into the buffer.
|
||||
const page: *const Page = &meta.node.data;
|
||||
_ = page.encodeUtf8(
|
||||
encoded.writer(alloc),
|
||||
.{ .cell_map = &meta.cell_map },
|
||||
) catch {
|
||||
// writer uses anyerror but the only realistic error on
|
||||
// an ArrayList is out of memory.
|
||||
return error.OutOfMemory;
|
||||
};
|
||||
assert(meta.cell_map.items.len == encoded.items.len);
|
||||
|
||||
// Now that we know our buffer length, we can consider if we can
|
||||
// prune our circular buffer or if we need to grow it.
|
||||
prune: {
|
||||
// Our buffer size after adding the new node.
|
||||
const before_size: usize = self.data.len() + encoded.items.len;
|
||||
|
||||
// Prune as long as removing the first (oldest) node retains
|
||||
// our required size invariant.
|
||||
var after_size: usize = before_size;
|
||||
while (self.meta.first()) |oldest_meta| {
|
||||
const new_size = after_size - oldest_meta.cell_map.items.len;
|
||||
if (new_size < required_size) break :prune;
|
||||
|
||||
// We can prune this node and retain our invariant.
|
||||
// Update our new size, deinitialize the memory, and
|
||||
// remove from the circular buffer.
|
||||
after_size = new_size;
|
||||
oldest_meta.deinit();
|
||||
self.meta.deleteOldest(1);
|
||||
}
|
||||
assert(after_size <= before_size);
|
||||
|
||||
// If we didn't prune anything then we're done.
|
||||
if (after_size == before_size) break :prune;
|
||||
|
||||
// We need to prune our data buffer as well.
|
||||
self.data.deleteOldest(before_size - after_size);
|
||||
}
|
||||
|
||||
// Ensure our buffers are big enough to store what we need.
|
||||
try self.data.ensureUnusedCapacity(alloc, encoded.items.len);
|
||||
try self.meta.ensureUnusedCapacity(alloc, 1);
|
||||
|
||||
// Append our new node to the circular buffer.
|
||||
try self.data.appendSlice(encoded.items);
|
||||
try self.meta.append(meta);
|
||||
|
||||
// Integrity check: verify our data matches our metadata exactly.
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
var meta_it = self.meta.iterator(.forward);
|
||||
var data_len: usize = 0;
|
||||
while (meta_it.next()) |m| data_len += m.cell_map.items.len;
|
||||
assert(data_len == self.data.len());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "SlidingWindow empty on init" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var w = try SlidingWindow.initEmpty(alloc);
|
||||
defer w.deinit(alloc);
|
||||
try testing.expectEqual(0, w.data.len());
|
||||
try testing.expectEqual(0, w.meta.len());
|
||||
}
|
||||
|
||||
test "SlidingWindow single append" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var w = try SlidingWindow.initEmpty(alloc);
|
||||
defer w.deinit(alloc);
|
||||
|
||||
var s = try Screen.init(alloc, 80, 24, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("hello. boo! hello. boo!");
|
||||
|
||||
// Imaginary needle for search
|
||||
const needle = "boo!";
|
||||
|
||||
// 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(alloc, node, needle.len);
|
||||
}
|
||||
|
||||
pub const PageSearch = struct {
|
||||
alloc: Allocator,
|
||||
node: *PageList.List.Node,
|
||||
|
Reference in New Issue
Block a user