diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/search/active.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/search/active.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/search/active.zig | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/search/active.zig b/vendor/libghostty-vt/src/terminal/search/active.zig new file mode 100644 index 00000000..692a10e1 --- /dev/null +++ b/vendor/libghostty-vt/src/terminal/search/active.zig @@ -0,0 +1,175 @@ +const std = @import("std"); +const testing = std.testing; +const Allocator = std.mem.Allocator; +const point = @import("../point.zig"); +const size = @import("../size.zig"); +const FlattenedHighlight = @import("../highlight.zig").Flattened; +const PageList = @import("../PageList.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) 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. + 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; + 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 + // 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. + 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 >= self.window.needle.len - 1) break; + } + + // 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 + /// null when there are no more matches. + pub fn next(self: *ActiveSearch) ?FlattenedHighlight { + 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(); + s.nextSlice("Fizz\r\nBuzz\r\nFizz\r\nBang"); + + var search: ActiveSearch = try .init(alloc, "Fizz"); + defer search.deinit(); + _ = try search.update(&t.screens.active.pages); + + { + const h = search.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 0, + .y = 0, + } }, t.screens.active.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 3, + .y = 0, + } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); + } + { + const h = search.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 0, + .y = 2, + } }, t.screens.active.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 3, + .y = 2, + } }, t.screens.active.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(); + s.nextSlice("Fizz\r\nBuzz\r\nFizz\r\nBang"); + + var search: ActiveSearch = try .init(alloc, "Fizz"); + defer search.deinit(); + _ = try search.update(&t.screens.active.pages); + + s.nextSlice("\x1b[2J"); // Clear screen + s.nextSlice("\x1b[H"); // Move cursor home + s.nextSlice("Buzz\r\nFizz\r\nBuzz"); + _ = try search.update(&t.screens.active.pages); + + { + const h = search.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 0, + .y = 1, + } }, t.screens.active.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 3, + .y = 1, + } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); + } + try testing.expect(search.next() == null); +} |
