From d4c2376c2d450527040b7e740f3e806a98beb161 Mon Sep 17 00:00:00 2001 From: Pyry Takala <7336413+pyrytakala@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:34:07 +0000 Subject: [PATCH 01/22] Fix LangSet.hasLang() to compare against FcLangEqual instead of FcTrue FcLangSetHasLang returns FcLangResult enum values: - FcLangEqual (0): Exact match - FcLangDifferentTerritory (1): Same language, different territory - FcLangDifferentLang (2): Different language The previous comparison to FcTrue (1) caused: - Exact matches (0) to incorrectly return false - Partial matches (1) to incorrectly return true This fix changes the comparison to FcLangEqual (0) so hasLang() correctly returns true only for exact language matches. Fixes emoji font detection which relies on checking for 'und-zsye' language tag support. --- pkg/fontconfig/lang_set.zig | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/pkg/fontconfig/lang_set.zig b/pkg/fontconfig/lang_set.zig index aaf55bab6..abefcc3e6 100644 --- a/pkg/fontconfig/lang_set.zig +++ b/pkg/fontconfig/lang_set.zig @@ -11,8 +11,12 @@ pub const LangSet = opaque { c.FcLangSetDestroy(self.cval()); } + pub fn addLang(self: *LangSet, lang: [:0]const u8) bool { + return c.FcLangSetAdd(self.cval(), lang.ptr) == c.FcTrue; + } + pub fn hasLang(self: *const LangSet, lang: [:0]const u8) bool { - return c.FcLangSetHasLang(self.cvalConst(), lang.ptr) == c.FcTrue; + return c.FcLangSetHasLang(self.cvalConst(), lang.ptr) == c.FcLangEqual; } pub inline fn cval(self: *LangSet) *c.struct__FcLangSet { @@ -32,3 +36,26 @@ test "create" { try testing.expect(!fs.hasLang("und-zsye")); } + +test "hasLang exact match" { + const testing = std.testing; + + // Test exact match: langset with "en-US" should return true for "en-US" + var fs = LangSet.create(); + defer fs.destroy(); + try testing.expect(fs.addLang("en-US")); + try testing.expect(fs.hasLang("en-US")); + + // Test exact match: langset with "und-zsye" should return true for "und-zsye" + var fs_emoji = LangSet.create(); + defer fs_emoji.destroy(); + try testing.expect(fs_emoji.addLang("und-zsye")); + try testing.expect(fs_emoji.hasLang("und-zsye")); + + // Test mismatch: langset with "en-US" should return false for "fr" + try testing.expect(!fs.hasLang("fr")); + + // Test partial match: langset with "en-US" should return false for "en-GB" + // (different territory, but we only want exact matches) + try testing.expect(!fs.hasLang("en-GB")); +} From 5bfeba6603f2997a34a276eb33e49552e1926cd6 Mon Sep 17 00:00:00 2001 From: Pyry Takala <7336413+pyrytakala@users.noreply.github.com> Date: Mon, 24 Nov 2025 23:31:14 +0000 Subject: [PATCH 02/22] Fix LoadFlags struct bit alignment to match FreeType API The struct was missing padding at bit position 8, causing all subsequent flag fields (bits 9+) to be misaligned by one bit position. See: https://freetype.org/freetype2/docs/reference/ft2-glyph_retrieval.html#ft_load_xxx --- pkg/freetype/face.zig | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index f8714d4fe..b639a499b 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -263,24 +263,25 @@ pub const LoadFlags = packed struct { force_autohint: bool = false, crop_bitmap: bool = false, pedantic: bool = false, - ignore_global_advance_with: bool = false, + _padding1: u1 = 0, + ignore_global_advance_width: bool = false, no_recurse: bool = false, ignore_transform: bool = false, monochrome: bool = false, linear_design: bool = false, + sbits_only: bool = false, no_autohint: bool = false, - _padding1: u1 = 0, target_normal: bool = false, target_light: bool = false, target_mono: bool = false, target_lcd: bool = false, - target_lcd_v: bool = false, color: bool = false, + target_lcd_v: bool = false, compute_metrics: bool = false, bitmap_metrics_only: bool = false, _padding2: u1 = 0, no_svg: bool = false, - _padding3: u7 = 0, + _padding3: u6 = 0, test { // This must always be an i32 size so we can bitcast directly. @@ -290,12 +291,19 @@ pub const LoadFlags = packed struct { test "bitcast" { const testing = std.testing; + const cval: i32 = c.FT_LOAD_RENDER | c.FT_LOAD_PEDANTIC | c.FT_LOAD_COLOR; const flags = @as(LoadFlags, @bitCast(cval)); try testing.expect(!flags.no_hinting); try testing.expect(flags.render); try testing.expect(flags.pedantic); try testing.expect(flags.color); + + // Verify bit alignment (for bit 9) + const cval2: i32 = c.FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH; + const flags2 = @as(LoadFlags, @bitCast(cval2)); + try testing.expect(flags2.ignore_global_advance_width); + try testing.expect(!flags2.no_recurse); } }; From 6a9c869f9dbc4b728888ef11edec86cc1b05560a Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 24 Nov 2025 17:24:32 -0700 Subject: [PATCH 03/22] Partially revert 25856d6 since it broke pkg/freetype tests --- pkg/freetype/res/FiraCode-Regular.ttf | Bin 0 -> 289624 bytes pkg/freetype/test.zig | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100755 pkg/freetype/res/FiraCode-Regular.ttf diff --git a/pkg/freetype/res/FiraCode-Regular.ttf b/pkg/freetype/res/FiraCode-Regular.ttf new file mode 100755 index 0000000000000000000000000000000000000000..bd736851948d2d76483b434113e2d9ee35554446 GIT binary patch literal 289624 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 literal 0 HcmV?d00001 diff --git a/pkg/freetype/test.zig b/pkg/freetype/test.zig index 093061616..866c6f2a4 100644 --- a/pkg/freetype/test.zig +++ b/pkg/freetype/test.zig @@ -1 +1 @@ -pub const font_regular = @embedFile("res/JetBrainsMono-Regular.ttf"); +pub const font_regular = @embedFile("res/FiraCode-Regular.ttf"); From 3cd6939af63cccacf32d11377da474f12060d594 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 24 Nov 2025 17:35:53 -0700 Subject: [PATCH 04/22] pkg/freetype: add failing unit tests for LoadFlags --- pkg/freetype/face.zig | 70 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index b639a499b..e4c17cf92 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -254,7 +254,7 @@ pub const RenderMode = enum(c_uint) { /// A list of bit field constants for FT_Load_Glyph to indicate what kind of /// operations to perform during glyph loading. -pub const LoadFlags = packed struct { +pub const LoadFlags = packed struct(c_int) { no_scale: bool = false, no_hinting: bool = false, render: bool = false, @@ -283,28 +283,82 @@ pub const LoadFlags = packed struct { no_svg: bool = false, _padding3: u6 = 0, - test { - // This must always be an i32 size so we can bitcast directly. - const testing = std.testing; - try testing.expectEqual(@sizeOf(i32), @sizeOf(LoadFlags)); - } + pub const Target = enum(u4) { + normal = 0, + light = 1, + mono = 2, + lcd = 3, + lcd_v = 4, + }; test "bitcast" { const testing = std.testing; - + const cval: i32 = c.FT_LOAD_RENDER | c.FT_LOAD_PEDANTIC | c.FT_LOAD_COLOR; const flags = @as(LoadFlags, @bitCast(cval)); try testing.expect(!flags.no_hinting); try testing.expect(flags.render); try testing.expect(flags.pedantic); try testing.expect(flags.color); - + // Verify bit alignment (for bit 9) const cval2: i32 = c.FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH; const flags2 = @as(LoadFlags, @bitCast(cval2)); try testing.expect(flags2.ignore_global_advance_width); try testing.expect(!flags2.no_recurse); } + + test "all flags individually" { + const testing = std.testing; + + try testing.expectEqual( + c.FT_LOAD_DEFAULT, + @as(c_int, @bitCast(LoadFlags{})), + ); + + inline for ([_]struct { c_int, []const u8 }{ + .{ c.FT_LOAD_NO_SCALE, "no_scale" }, + .{ c.FT_LOAD_NO_HINTING, "no_hinting" }, + .{ c.FT_LOAD_RENDER, "render" }, + .{ c.FT_LOAD_NO_BITMAP, "no_bitmap" }, + .{ c.FT_LOAD_VERTICAL_LAYOUT, "vertical_layout" }, + .{ c.FT_LOAD_FORCE_AUTOHINT, "force_autohint" }, + .{ c.FT_LOAD_CROP_BITMAP, "crop_bitmap" }, + .{ c.FT_LOAD_PEDANTIC, "pedantic" }, + .{ c.FT_LOAD_ADVANCE_ONLY, "advance_only" }, + .{ c.FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH, "ignore_global_advance_width" }, + .{ c.FT_LOAD_NO_RECURSE, "no_recurse" }, + .{ c.FT_LOAD_IGNORE_TRANSFORM, "ignore_transform" }, + .{ c.FT_LOAD_MONOCHROME, "monochrome" }, + .{ c.FT_LOAD_LINEAR_DESIGN, "linear_design" }, + .{ c.FT_LOAD_SBITS_ONLY, "sbits_only" }, + .{ c.FT_LOAD_NO_AUTOHINT, "no_autohint" }, + .{ c.FT_LOAD_COLOR, "color" }, + .{ c.FT_LOAD_COMPUTE_METRICS, "compute_metrics" }, + .{ c.FT_LOAD_BITMAP_METRICS_ONLY, "bitmap_metrics_only" }, + .{ c.FT_LOAD_SVG_ONLY, "svg_only" }, + .{ c.FT_LOAD_NO_SVG, "no_svg" }, + }) |pair| { + var flags: LoadFlags = .{}; + @field(flags, pair[1]) = true; + try testing.expectEqual(pair[0], @as(c_int, @bitCast(flags))); + } + } + + test "all load targets" { + const testing = std.testing; + + inline for ([_]struct { c_int, Target }{ + .{ c.FT_LOAD_TARGET_NORMAL, .normal }, + .{ c.FT_LOAD_TARGET_LIGHT, .light }, + .{ c.FT_LOAD_TARGET_MONO, .mono }, + .{ c.FT_LOAD_TARGET_LCD, .lcd }, + .{ c.FT_LOAD_TARGET_LCD_V, .lcd_v }, + }) |pair| { + const flags: LoadFlags = .{ .target = pair[1] }; + try testing.expectEqual(pair[0], @as(c_int, @bitCast(flags))); + } + } }; test "loading memory font" { From 6d65abc489cc015fa4958567c86c10eb05cf09c3 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 24 Nov 2025 17:42:02 -0700 Subject: [PATCH 05/22] fix(pkg/freetype): fully correct load flags These now properly match the FreeType API- compared directly in the unit tests against the values provided by the FreeType header itself. This was ridiculously wrong before, like... wow. --- pkg/freetype/face.zig | 20 ++++++++++---------- src/font/face/freetype.zig | 14 +++++++++----- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index e4c17cf92..d4f74b7ee 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -252,8 +252,12 @@ pub const RenderMode = enum(c_uint) { sdf = c.FT_RENDER_MODE_SDF, }; -/// A list of bit field constants for FT_Load_Glyph to indicate what kind of -/// operations to perform during glyph loading. +/// A collection of flags for FT_Load_Glyph that indicate +/// what kind of operations to perform during glyph loading. +/// +/// Some of these flags are not included in the official FreeType +/// documentation, but are nevertheless present and named in the +/// header, so the names have been copied from there. pub const LoadFlags = packed struct(c_int) { no_scale: bool = false, no_hinting: bool = false, @@ -263,7 +267,7 @@ pub const LoadFlags = packed struct(c_int) { force_autohint: bool = false, crop_bitmap: bool = false, pedantic: bool = false, - _padding1: u1 = 0, + advance_only: bool = false, ignore_global_advance_width: bool = false, no_recurse: bool = false, ignore_transform: bool = false, @@ -271,17 +275,13 @@ pub const LoadFlags = packed struct(c_int) { linear_design: bool = false, sbits_only: bool = false, no_autohint: bool = false, - target_normal: bool = false, - target_light: bool = false, - target_mono: bool = false, - target_lcd: bool = false, + target: Target = .normal, color: bool = false, - target_lcd_v: bool = false, compute_metrics: bool = false, bitmap_metrics_only: bool = false, - _padding2: u1 = 0, + svg_only: bool = false, no_svg: bool = false, - _padding3: u6 = 0, + _padding: u7 = 0, pub const Target = enum(u4) { normal = 0, diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index ced313a94..fe3dcf707 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -376,11 +376,15 @@ pub const Face = struct { // If we're gonna be rendering this glyph in monochrome, // then we should use the monochrome hinter as well, or // else it won't look very good at all. - .target_mono = self.load_flags.monochrome, - - // Otherwise we select hinter based on the `light` flag. - .target_normal = !self.load_flags.light and !self.load_flags.monochrome, - .target_light = self.load_flags.light and !self.load_flags.monochrome, + // + // Otherwise if the user asked for light hinting we + // use that, otherwise we just use the normal target. + .target = if (self.load_flags.monochrome) + .mono + else if (self.load_flags.light) + .light + else + .normal, // NO_SVG set to true because we don't currently support rendering // SVG glyphs under FreeType, since that requires bundling another From 878ccd3f3406494266c109e47a1f2a6753dcb912 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 24 Nov 2025 19:52:14 -0800 Subject: [PATCH 06/22] renderer: use proper cell style for cursor-color/text Regression from render state work. --- src/renderer/generic.zig | 55 +++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 861625351..025578c81 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -2782,18 +2782,34 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Setup our cursor rendering information. cursor: { - // By default, we don't handle cursor inversion on the shader. + // Clear our cursor by default. self.cells.setCursor(null, null); self.uniforms.cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16), }; + // If the cursor isn't visible on the viewport, don't show + // a cursor. Otherwise, get our cursor cell, because we may + // need it for styling. + const cursor_vp = state.cursor.viewport orelse break :cursor; + const cursor_style: terminal.Style = cursor_style: { + const cells = state.row_data.items(.cells); + const cell = cells[cursor_vp.y].get(cursor_vp.x); + break :cursor_style if (cell.raw.hasStyling()) + cell.style + else + .{}; + }; + // If we have preedit text, we don't setup a cursor if (preedit != null) break :cursor; - // Prepare the cursor cell contents. + // If there isn't a cursor visual style requested then + // we don't render a cursor. const style = cursor_style_ orelse break :cursor; + + // Determine the cursor color. const cursor_color = cursor_color: { // If an explicit cursor color was set by OSC 12, use that. if (state.colors.cursor) |v| break :cursor_color v; @@ -2801,24 +2817,30 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Use our configured color if specified if (self.config.cursor_color) |v| switch (v) { .color => |color| break :cursor_color color.toTerminalRGB(), + inline .@"cell-foreground", .@"cell-background", => |_, tag| { - const sty: terminal.Style = state.cursor.style; - const fg_style = sty.fg(.{ + const fg_style = cursor_style.fg(.{ .default = state.colors.foreground, .palette = &state.colors.palette, .bold = self.config.bold_color, }); - const bg_style = sty.bg( + const bg_style = cursor_style.bg( &state.cursor.cell, &state.colors.palette, ) orelse state.colors.background; break :cursor_color switch (tag) { .color => unreachable, - .@"cell-foreground" => if (sty.flags.inverse) bg_style else fg_style, - .@"cell-background" => if (sty.flags.inverse) fg_style else bg_style, + .@"cell-foreground" => if (cursor_style.flags.inverse) + bg_style + else + fg_style, + .@"cell-background" => if (cursor_style.flags.inverse) + fg_style + else + bg_style, }; }, }; @@ -2833,9 +2855,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { ); // If the cursor is visible then we set our uniforms. - if (style == .block) cursor_uniforms: { - const cursor_vp = state.cursor.viewport orelse - break :cursor_uniforms; + if (style == .block) { const wide = state.cursor.cell.wide; self.uniforms.cursor_pos = .{ @@ -2862,21 +2882,26 @@ pub fn Renderer(comptime GraphicsAPI: type) type { break :blk txt.color.toTerminalRGB(); } - const sty = state.cursor.style; - const fg_style = sty.fg(.{ + const fg_style = cursor_style.fg(.{ .default = state.colors.foreground, .palette = &state.colors.palette, .bold = self.config.bold_color, }); - const bg_style = sty.bg( + const bg_style = cursor_style.bg( &state.cursor.cell, &state.colors.palette, ) orelse state.colors.background; break :blk switch (txt) { // If the cell is reversed, use the opposite cell color instead. - .@"cell-foreground" => if (sty.flags.inverse) bg_style else fg_style, - .@"cell-background" => if (sty.flags.inverse) fg_style else bg_style, + .@"cell-foreground" => if (cursor_style.flags.inverse) + bg_style + else + fg_style, + .@"cell-background" => if (cursor_style.flags.inverse) + fg_style + else + bg_style, else => unreachable, }; } else state.colors.background; From 56b69ff0fd966188331a841c93a97522af7a3891 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 22 Nov 2025 21:06:31 -0800 Subject: [PATCH 07/22] datastruct: make CircBuf use the assumeCapacity pattern --- src/datastruct/circ_buf.zig | 27 +++++++++++++++++--------- src/terminal/search/sliding_window.zig | 4 ++-- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/datastruct/circ_buf.zig b/src/datastruct/circ_buf.zig index baef6f9cf..0caa9e85d 100644 --- a/src/datastruct/circ_buf.zig +++ b/src/datastruct/circ_buf.zig @@ -91,15 +91,24 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { self.full = self.head == self.tail; } - /// Append a slice to the buffer. If the buffer cannot fit the - /// entire slice then an error will be returned. It is up to the - /// caller to rotate the circular buffer if they want to overwrite - /// the oldest data. - pub fn appendSlice( + /// Append a single value to the buffer, assuming there is capacity. + pub fn appendAssumeCapacity(self: *Self, v: T) void { + assert(!self.full); + self.storage[self.head] = v; + self.head += 1; + if (self.head >= self.storage.len) self.head = 0; + self.full = self.head == self.tail; + } + + /// Append a slice to the buffer. + pub fn appendSliceAssumeCapacity( self: *Self, slice: []const T, - ) Allocator.Error!void { - const storage = self.getPtrSlice(self.len(), slice.len); + ) void { + const storage = self.getPtrSlice( + self.len(), + slice.len, + ); fastmem.copy(T, storage[0], slice[0..storage[0].len]); fastmem.copy(T, storage[1], slice[storage[0].len..]); } @@ -456,7 +465,7 @@ test "CircBuf append slice" { var buf = try Buf.init(alloc, 5); defer buf.deinit(alloc); - try buf.appendSlice("hello"); + buf.appendSliceAssumeCapacity("hello"); { var it = buf.iterator(.forward); try testing.expect(it.next().?.* == 'h'); @@ -486,7 +495,7 @@ test "CircBuf append slice with wrap" { try testing.expect(!buf.full); try testing.expectEqual(@as(usize, 2), buf.len()); - try buf.appendSlice("AB"); + buf.appendSliceAssumeCapacity("AB"); { var it = buf.iterator(.forward); try testing.expect(it.next().?.* == 0); diff --git a/src/terminal/search/sliding_window.zig b/src/terminal/search/sliding_window.zig index 2d09c781a..b0df3c13b 100644 --- a/src/terminal/search/sliding_window.zig +++ b/src/terminal/search/sliding_window.zig @@ -444,8 +444,8 @@ pub const SlidingWindow = struct { try self.meta.ensureUnusedCapacity(self.alloc, 1); // Append our new node to the circular buffer. - try self.data.appendSlice(written); - try self.meta.append(meta); + self.data.appendSliceAssumeCapacity(written); + self.meta.appendAssumeCapacity(meta); self.assertIntegrity(); return written.len; From ec5bdf1a5a7ac3172e5103d6eb92b109c78980d5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 22 Nov 2025 21:06:31 -0800 Subject: [PATCH 08/22] terminal: highlights --- src/lib_vt.zig | 1 + src/terminal/PageList.zig | 4 + src/terminal/highlight.zig | 154 +++++++++++++++++++++++++++++++++++++ src/terminal/main.zig | 1 + 4 files changed, 160 insertions(+) create mode 100644 src/terminal/highlight.zig diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 95b308aab..03a883e20 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -26,6 +26,7 @@ pub const point = terminal.point; pub const color = terminal.color; pub const device_status = terminal.device_status; pub const formatter = terminal.formatter; +pub const highlight = terminal.highlight; pub const kitty = terminal.kitty; pub const modes = terminal.modes; pub const page = terminal.page; diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 0e793a254..53c0c346b 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -3729,7 +3729,11 @@ pub const PageIterator = struct { pub const Chunk = struct { node: *List.Node, + + /// Start y index (inclusive) of this chunk in the page. start: size.CellCountInt, + + /// End y index (exclusive) of this chunk in the page. end: size.CellCountInt, pub fn rows(self: Chunk) []Row { diff --git a/src/terminal/highlight.zig b/src/terminal/highlight.zig new file mode 100644 index 000000000..626d6e471 --- /dev/null +++ b/src/terminal/highlight.zig @@ -0,0 +1,154 @@ +//! Highlights are any contiguous sequences of cells that should +//! be called out in some way, most commonly for text selection but +//! also search results or any other purpose. +//! +//! Within the terminal package, a highlight is a generic concept +//! that represents a range of cells. + +// NOTE: The plan is for highlights to ultimately replace Selection +// completely. Selection is deeply tied to various parts of the Ghostty +// internals so this may take some time. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = @import("../quirks.zig").inlineAssert; +const size = @import("size.zig"); +const PageList = @import("PageList.zig"); +const PageChunk = PageList.PageIterator.Chunk; +const Pin = PageList.Pin; +const Screen = @import("Screen.zig"); + +/// An untracked highlight is a highlight that stores its highlighted +/// area as a top-left and bottom-right screen pin. Since it is untracked, +/// the pins are only valid for the current terminal state and may not +/// be safe to use after any terminal modifications. +/// +/// For rectangle highlights/selections, the downstream consumer of this +/// code is expected to interpret the pins in whatever shape they want. +/// For example, a rectangular selection would interpret the pins as +/// setting the x bounds for each row between start.y and end.y. +/// +/// To simplify all operations, start MUST be before or equal to end. +pub const Untracked = struct { + start: Pin, + end: Pin, +}; + +/// A tracked highlight is a highlight that stores its highlighted +/// area as tracked pins within a screen. +/// +/// A tracked highlight ensures that the pins remain valid even as +/// the terminal state changes. Because of this, tracked highlights +/// have more operations available to them. +/// +/// There is more overhead to creating and maintaining tracked highlights. +/// If you're manipulating highlights that are untracked and you're sure +/// that the terminal state won't change, you can use the `initAssume` +/// function. +pub const Tracked = struct { + start: *Pin, + end: *Pin, + + pub fn init( + screen: *Screen, + start: Pin, + end: Pin, + ) Allocator.Error!Tracked { + const start_tracked = try screen.pages.trackPin(start); + errdefer screen.pages.untrackPin(start_tracked); + const end_tracked = try screen.pages.trackPin(end); + errdefer screen.pages.untrackPin(end_tracked); + return .{ + .start = start_tracked, + .end = end_tracked, + }; + } + + /// Initializes a tracked highlight by assuming that the provided + /// pins are already tracked. This allows callers to perform tracked + /// operations without the overhead of tracking the pins, if the + /// caller can guarantee that the pins are already tracked or that + /// the terminal state will not change. + /// + /// Do not call deinit on highlights created with this function. + pub fn initAssume( + start: *Pin, + end: *Pin, + ) Tracked { + return .{ + .start = start, + .end = end, + }; + } + + pub fn deinit( + self: Tracked, + screen: *Screen, + ) void { + screen.pages.untrackPin(self.start); + screen.pages.untrackPin(self.end); + } +}; + +/// A flattened highlight is a highlight that stores its highlighted +/// area as a list of page chunks. This representation allows for +/// traversing the entire highlighted area without needing to read any +/// terminal state or dereference any page nodes (which may have been +/// pruned). +pub const Flattened = struct { + /// The page chunks that make up this highlight. This handles the + /// y bounds since chunks[0].start is the first highlighted row + /// and chunks[len - 1].end is the last highlighted row (exclsive). + chunks: std.MultiArrayList(PageChunk), + + /// The x bounds of the highlight. `bot_x` may be less than `top_x` + /// for typical left-to-right highlights: can start the selection right + /// of the end on a higher row. + top_x: size.CellCountInt, + bot_x: size.CellCountInt, + + /// Exposed for easier type references. + pub const Chunk = PageChunk; + + pub const empty: Flattened = .{ + .chunks = .empty, + .top_x = 0, + .bot_x = 0, + }; + + pub fn init( + alloc: Allocator, + start: Pin, + end: Pin, + ) Allocator.Error!Flattened { + var result: std.MultiArrayList(PageChunk) = .empty; + errdefer result.deinit(alloc); + var it = start.pageIterator(.right_down, end); + while (it.next()) |chunk| try result.append(alloc, chunk); + return .{ + .chunks = result, + .top_x = start.x, + .end_x = end.x, + }; + } + + /// Convert to an Untracked highlight. + pub fn untracked(self: Flattened) Untracked { + const slice = self.chunks.slice(); + const nodes = slice.items(.node); + const starts = slice.items(.start); + const ends = slice.items(.end); + return .{ + .start = .{ + .node = nodes[0], + .x = self.top_x, + .y = starts[0], + }, + .end = .{ + .node = nodes[nodes.len - 1], + .x = self.bot_x, + .y = ends[ends.len - 1] - 1, + }, + }; + } +}; diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 77a96bfee..fc7584c1a 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -15,6 +15,7 @@ pub const point = @import("point.zig"); pub const color = @import("color.zig"); pub const device_status = @import("device_status.zig"); pub const formatter = @import("formatter.zig"); +pub const highlight = @import("highlight.zig"); pub const kitty = @import("kitty.zig"); pub const modes = @import("modes.zig"); pub const page = @import("page.zig"); From 05d6315e822f6574c6586348540d8626cbbd1cb7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 22 Nov 2025 21:06:31 -0800 Subject: [PATCH 09/22] terminal: add a SlidingWindow2 that uses highlights --- src/terminal/search.zig | 1 + src/terminal/search/sliding_window2.zig | 1400 +++++++++++++++++++++++ 2 files changed, 1401 insertions(+) create mode 100644 src/terminal/search/sliding_window2.zig diff --git a/src/terminal/search.zig b/src/terminal/search.zig index e69603c25..1ac18515c 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -19,4 +19,5 @@ test { // Non-public APIs _ = @import("search/sliding_window.zig"); + _ = @import("search/sliding_window2.zig"); } diff --git a/src/terminal/search/sliding_window2.zig b/src/terminal/search/sliding_window2.zig new file mode 100644 index 000000000..6aad0bff9 --- /dev/null +++ b/src/terminal/search/sliding_window2.zig @@ -0,0 +1,1400 @@ +const std = @import("std"); +const assert = @import("../../quirks.zig").inlineAssert; +const Allocator = std.mem.Allocator; +const CircBuf = @import("../../datastruct/main.zig").CircBuf; +const terminal = @import("../main.zig"); +const point = terminal.point; +const size = terminal.size; +const PageList = terminal.PageList; +const Pin = PageList.Pin; +const Selection = terminal.Selection; +const Screen = terminal.Screen; +const PageFormatter = @import("../formatter.zig").PageFormatter; +const FlattenedHighlight = terminal.highlight.Flattened; + +/// Searches page nodes via a sliding window. The sliding window maintains +/// the invariant that data isn't pruned until (1) we've searched it and +/// (2) we've accounted for overlaps across pages to fit the needle. +/// +/// The sliding window is first initialized empty. Pages are then appended +/// in the order to search them. The sliding window supports both a forward +/// and reverse order specified via `init`. The pages should be appended +/// in the correct order matching the search direction. +/// +/// All appends grow the window. The window is only pruned when a search +/// is done (positive or negative match) via `next()`. +/// +/// To avoid unnecessary memory growth, the recommended usage is to +/// call `next()` until it returns null and then `append` the next page +/// and repeat the process. This will always maintain the minimum +/// required memory to search for the needle. +/// +/// The caller is responsible for providing the pages and ensuring they're +/// in the proper order. The SlidingWindow itself doesn't own the pages, but +/// it will contain pointers to them in order to return selections. If any +/// pages become invalid, the caller should clear the sliding window and +/// start over. +pub const SlidingWindow = struct { + /// The allocator to use for all the data within this window. We + /// store this rather than passing it around because its already + /// part of multiple elements (eg. Meta's CellMap) and we want to + /// ensure we always use a consistent allocator. Additionally, only + /// a small amount of sliding windows are expected to be in use + /// at any one time so the memory overhead isn't that large. + alloc: Allocator, + + /// 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, + + /// Buffer that can fit any amount of chunks necessary for next + /// to never fail allocation. + chunk_buf: std.MultiArrayList(FlattenedHighlight.Chunk), + + /// Offset into data for our current state. This handles the + /// situation where our search moved through meta[0] but didn't + /// do enough to prune it. + data_offset: usize = 0, + + /// The needle we're searching for. Does own the memory. + needle: []const u8, + + /// The search direction. If the direction is forward then pages should + /// be appended in forward linked list order from the PageList. If the + /// direction is reverse then pages should be appended in reverse order. + /// + /// This is important because in most cases, a reverse search is going + /// to be more desirable to search from the end of the active area + /// backwards so more recent data is found first. Supporting both is + /// trivial though and will let us do more complex optimizations in the + /// future (e.g. starting from the viewport and doing a forward/reverse + /// concurrently from that point). + direction: Direction, + + /// A buffer to store the overlap search data. This is used to search + /// overlaps between pages where the match starts on one page and + /// ends on another. The length is always `needle.len * 2`. + overlap_buf: []u8, + + const Direction = enum { forward, reverse }; + const DataBuf = CircBuf(u8, 0); + const MetaBuf = CircBuf(Meta, undefined); + const Meta = struct { + node: *PageList.List.Node, + cell_map: std.ArrayList(point.Coordinate), + + pub fn deinit(self: *Meta, alloc: Allocator) void { + self.cell_map.deinit(alloc); + } + }; + + pub fn init( + alloc: Allocator, + direction: Direction, + needle_unowned: []const u8, + ) 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); + + const needle = try alloc.dupe(u8, needle_unowned); + errdefer alloc.free(needle); + switch (direction) { + .forward => {}, + .reverse => std.mem.reverse(u8, needle), + } + + const overlap_buf = try alloc.alloc(u8, needle.len * 2); + errdefer alloc.free(overlap_buf); + + return .{ + .alloc = alloc, + .data = data, + .meta = meta, + .chunk_buf = .empty, + .needle = needle, + .direction = direction, + .overlap_buf = overlap_buf, + }; + } + + pub fn deinit(self: *SlidingWindow) void { + self.alloc.free(self.overlap_buf); + self.alloc.free(self.needle); + self.chunk_buf.deinit(self.alloc); + self.data.deinit(self.alloc); + + var meta_it = self.meta.iterator(.forward); + while (meta_it.next()) |meta| meta.deinit(self.alloc); + self.meta.deinit(self.alloc); + } + + /// Clear all data but retain allocated capacity. + pub fn clearAndRetainCapacity(self: *SlidingWindow) void { + var meta_it = self.meta.iterator(.forward); + while (meta_it.next()) |meta| meta.deinit(self.alloc); + self.meta.clear(); + self.data.clear(); + self.data_offset = 0; + } + + /// Search the window for the next occurrence of the needle. As + /// the window moves, the window will prune itself while maintaining + /// the invariant that the window is always big enough to contain + /// the needle. + /// + /// This returns a flattened highlight on a match. The + /// flattened highlight requires allocation and is therefore more expensive + /// than a normal selection, but it is more efficient to render since it + /// has all the information without having to dereference pointers into + /// the terminal state. + /// + /// The flattened highlight chunks reference internal memory for this + /// sliding window and are only valid until the next call to `next()` + /// or `append()`. If the caller wants to retain the flattened highlight + /// then they should clone it. + pub fn next(self: *SlidingWindow) ?FlattenedHighlight { + const slices = slices: { + // If we have less data then the needle then we can't possibly match + const data_len = self.data.len(); + if (data_len < self.needle.len) return null; + + break :slices self.data.getPtrSlice( + self.data_offset, + data_len - self.data_offset, + ); + }; + + // Search the first slice for the needle. + if (std.mem.indexOf(u8, slices[0], self.needle)) |idx| { + return self.highlight( + idx, + self.needle.len, + ); + } + + // Search the overlap buffer for the needle. + if (slices[0].len > 0 and slices[1].len > 0) overlap: { + // Get up to needle.len - 1 bytes from each side (as much as + // we can) and store it in the overlap buffer. + const prefix: []const u8 = prefix: { + const len = @min(slices[0].len, self.needle.len - 1); + const idx = slices[0].len - len; + break :prefix slices[0][idx..]; + }; + const suffix: []const u8 = suffix: { + const len = @min(slices[1].len, self.needle.len - 1); + break :suffix slices[1][0..len]; + }; + const overlap_len = prefix.len + suffix.len; + assert(overlap_len <= self.overlap_buf.len); + @memcpy(self.overlap_buf[0..prefix.len], prefix); + @memcpy(self.overlap_buf[prefix.len..overlap_len], suffix); + + // Search the overlap + const idx = std.mem.indexOf( + u8, + self.overlap_buf[0..overlap_len], + self.needle, + ) orelse break :overlap; + + // We found a match in the overlap buffer. We need to map the + // index back to the data buffer in order to get our selection. + return self.highlight( + slices[0].len - prefix.len + idx, + self.needle.len, + ); + } + + // Search the last slice for the needle. + if (std.mem.indexOf(u8, slices[1], self.needle)) |idx| { + return self.highlight( + slices[0].len + idx, + self.needle.len, + ); + } + + // No match. We keep `needle.len - 1` bytes available to + // handle the future overlap case. + var meta_it = self.meta.iterator(.reverse); + prune: { + var saved: usize = 0; + while (meta_it.next()) |meta| { + const needed = self.needle.len - 1 - saved; + if (meta.cell_map.items.len >= needed) { + // We save up to this meta. We set our data offset + // to exactly where it needs to be to continue + // searching. + self.data_offset = meta.cell_map.items.len - needed; + break; + } + + saved += meta.cell_map.items.len; + } else { + // If we exited the while loop naturally then we + // never got the amount we needed and so there is + // nothing to prune. + assert(saved < self.needle.len - 1); + break :prune; + } + + const prune_count = self.meta.len() - meta_it.idx; + if (prune_count == 0) { + // This can happen if we need to save up to the first + // meta value to retain our window. + break :prune; + } + + // We can now delete all the metas up to but NOT including + // the meta we found through meta_it. + meta_it = self.meta.iterator(.forward); + var prune_data_len: usize = 0; + for (0..prune_count) |_| { + const meta = meta_it.next().?; + prune_data_len += meta.cell_map.items.len; + meta.deinit(self.alloc); + } + self.meta.deleteOldest(prune_count); + self.data.deleteOldest(prune_data_len); + } + + // Our data offset now moves to needle.len - 1 from the end so + // that we can handle the overlap case. + self.data_offset = self.data.len() - self.needle.len + 1; + + self.assertIntegrity(); + return null; + } + + /// Return a flattened highlight for the given start and length. + /// + /// The flattened highlight can be used to render the highlight + /// in the most efficent way because it doesn't require a terminal + /// lock to access terminal data to compare whether some viewport + /// matches the highlight (because it doesn't need to traverse + /// the page nodes). + /// + /// The start index is assumed to be relative to the offset. i.e. + /// index zero is actually at `self.data[self.data_offset]`. The + /// selection will account for the offset. + fn highlight( + self: *SlidingWindow, + start_offset: usize, + len: usize, + ) terminal.highlight.Flattened { + const start = start_offset + self.data_offset; + const end = start + len - 1; + if (comptime std.debug.runtime_safety) { + assert(start < self.data.len()); + assert(start + len <= self.data.len()); + } + + // Clear our previous chunk buffer to store this result + self.chunk_buf.clearRetainingCapacity(); + var result: terminal.highlight.Flattened = .empty; + + // Go through the meta nodes to find our start. + const tl: struct { + /// If non-null, we need to continue searching for the bottom-right. + br: ?struct { + it: MetaBuf.Iterator, + consumed: usize, + }, + + /// Data to prune, both are lengths. + prune: struct { + meta: usize, + data: usize, + }, + } = tl: { + var meta_it = self.meta.iterator(.forward); + var meta_consumed: usize = 0; + while (meta_it.next()) |meta| { + // Always increment our consumed count so that our index + // is right for the end search if we do it. + const prior_meta_consumed = meta_consumed; + meta_consumed += meta.cell_map.items.len; + + // meta_i is the index we expect to find the match in the + // cell map within this meta if it contains it. + const meta_i = start - prior_meta_consumed; + + // This meta doesn't contain the match. This means we + // can also prune this set of data because we only look + // forward. + if (meta_i >= meta.cell_map.items.len) continue; + + // Now we look for the end. In MOST cases it is the same as + // our starting chunk because highlights are usually small and + // not on a boundary, so let's optimize for that. + const end_i = end - prior_meta_consumed; + if (end_i < meta.cell_map.items.len) { + @branchHint(.likely); + + // The entire highlight is within this meta. + const start_map = meta.cell_map.items[meta_i]; + const end_map = meta.cell_map.items[end_i]; + result.top_x = start_map.x; + result.bot_x = end_map.x; + self.chunk_buf.appendAssumeCapacity(.{ + .node = meta.node, + .start = @intCast(start_map.y), + .end = @intCast(end_map.y + 1), + }); + + break :tl .{ + .br = null, + .prune = .{ + .meta = meta_it.idx - 1, + .data = prior_meta_consumed, + }, + }; + } else { + // We found the meta that contains the start of the match + // only. Consume this entire node from our start offset. + const map = meta.cell_map.items[meta_i]; + result.top_x = map.x; + self.chunk_buf.appendAssumeCapacity(.{ + .node = meta.node, + .start = @intCast(map.y), + .end = meta.node.data.size.rows, + }); + + break :tl .{ + .br = .{ + .it = meta_it, + .consumed = meta_consumed, + }, + .prune = .{ + .meta = meta_it.idx - 1, + .data = prior_meta_consumed, + }, + }; + } + } else { + // Precondition that the start index is within the data buffer. + unreachable; + } + }; + + // Search for our end. + if (tl.br) |br| { + var meta_it = br.it; + var meta_consumed: usize = br.consumed; + while (meta_it.next()) |meta| { + // meta_i is the index we expect to find the match in the + // cell map within this meta if it contains it. + const meta_i = end - meta_consumed; + if (meta_i >= meta.cell_map.items.len) { + // This meta doesn't contain the match. We still add it + // to our results because we want the full flattened list. + self.chunk_buf.appendAssumeCapacity(.{ + .node = meta.node, + .start = 0, + .end = meta.node.data.size.rows, + }); + + meta_consumed += meta.cell_map.items.len; + continue; + } + + // We found it + const map = meta.cell_map.items[meta_i]; + result.bot_x = map.x; + self.chunk_buf.appendAssumeCapacity(.{ + .node = meta.node, + .start = 0, + .end = @intCast(map.y + 1), + }); + break; + } else { + // Precondition that the end index is within the data buffer. + unreachable; + } + } + + // Our offset into the current meta block is the start index + // minus the amount of data fully consumed. We then add one + // to move one past the match so we don't repeat it. + self.data_offset = start - tl.prune.data + 1; + + // If we went beyond our initial meta node we can prune. + if (tl.prune.meta > 0) { + // Deinit all our memory in the meta blocks prior to our + // match. + var meta_it = self.meta.iterator(.forward); + var meta_consumed: usize = 0; + for (0..tl.prune.meta) |_| { + const meta: *Meta = meta_it.next().?; + meta_consumed += meta.cell_map.items.len; + meta.deinit(self.alloc); + } + if (comptime std.debug.runtime_safety) { + assert(meta_it.idx == tl.prune.meta); + assert(meta_it.next().?.node == self.chunk_buf.items(.node)[0]); + } + self.meta.deleteOldest(tl.prune.meta); + + // Delete all the data up to our current index. + assert(tl.prune.data > 0); + self.data.deleteOldest(tl.prune.data); + } + + switch (self.direction) { + .forward => {}, + .reverse => { + if (self.chunk_buf.len > 1) { + // Reverse all our chunks. This should be pretty obvious why. + const slice = self.chunk_buf.slice(); + const nodes = slice.items(.node); + const starts = slice.items(.start); + const ends = slice.items(.end); + std.mem.reverse(*PageList.List.Node, nodes); + std.mem.reverse(size.CellCountInt, starts); + std.mem.reverse(size.CellCountInt, ends); + + // Now normally with forward traversal with multiple pages, + // the suffix of the first page and the prefix of the last + // page are used. + // + // For a reverse traversal, this is inverted (since the + // pages are in reverse order we get the suffix of the last + // page and the prefix of the first page). So we need to + // invert this. + // + // We DON'T need to do this for any middle pages because + // they always use the full page. + // + // We DON'T need to do this for chunks.len == 1 because + // the pages themselves aren't reversed and we don't have + // any prefix/suffix problems. + // + // This is a fixup that makes our start/end match the + // same logic as the loops above if they were in forward + // order. + assert(nodes.len >= 2); + starts[0] = ends[0] - 1; + ends[0] = nodes[0].data.size.rows; + ends[nodes.len - 1] = starts[nodes.len - 1] + 1; + starts[nodes.len - 1] = 0; + } + + // X values also need to be reversed since the top/bottom + // are swapped for the nodes. + const top_x = result.top_x; + result.top_x = result.bot_x; + result.bot_x = top_x; + }, + } + + // Copy over our MultiArrayList so it points to the proper memory. + result.chunks = self.chunk_buf; + return result; + } + + /// 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!usize { + // Initialize our metadata for the node. + var meta: Meta = .{ + .node = node, + .cell_map = .empty, + }; + errdefer meta.deinit(self.alloc); + + // 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.Io.Writer.Allocating = .init(self.alloc); + defer encoded.deinit(); + + // Encode the page into the buffer. + const formatter: PageFormatter = formatter: { + var formatter: PageFormatter = .init(&meta.node.data, .plain); + formatter.point_map = .{ + .alloc = self.alloc, + .map = &meta.cell_map, + }; + break :formatter formatter; + }; + formatter.format(&encoded.writer) 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.written().len); + + // If the node we're adding isn't soft-wrapped, we add the + // trailing newline. + const row = node.data.getRow(node.data.size.rows - 1); + if (!row.wrap) { + encoded.writer.writeByte('\n') catch return error.OutOfMemory; + try meta.cell_map.append( + self.alloc, + meta.cell_map.getLastOrNull() orelse .{ + .x = 0, + .y = 0, + }, + ); + } + + // Get our written data. If we're doing a reverse search then we + // need to reverse all our encodings. + const written = encoded.written(); + switch (self.direction) { + .forward => {}, + .reverse => { + std.mem.reverse(u8, written); + std.mem.reverse(point.Coordinate, meta.cell_map.items); + }, + } + + // Ensure our buffers are big enough to store what we need. + try self.data.ensureUnusedCapacity(self.alloc, written.len); + try self.meta.ensureUnusedCapacity(self.alloc, 1); + try self.chunk_buf.ensureTotalCapacity(self.alloc, self.meta.capacity()); + + // Append our new node to the circular buffer. + self.data.appendSliceAssumeCapacity(written); + self.meta.appendAssumeCapacity(meta); + + self.assertIntegrity(); + return written.len; + } + + /// Only for tests! + fn testChangeNeedle(self: *SlidingWindow, new: []const u8) void { + assert(new.len == self.needle.len); + self.alloc.free(self.needle); + self.needle = self.alloc.dupe(u8, new) catch unreachable; + } + + fn assertIntegrity(self: *const SlidingWindow) void { + if (comptime !std.debug.runtime_safety) return; + + // We don't run integrity checks on Valgrind because its soooooo slow, + // Valgrind is our integrity checker, and we run these during unit + // tests (non-Valgrind) anyways so we're verifying anyways. + if (std.valgrind.runningOnValgrind() > 0) return; + + // Integrity check: verify our data matches our metadata exactly. + 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()); + + // Integrity check: verify our data offset is within bounds. + assert(self.data_offset < self.data.len()); + } +}; + +test "SlidingWindow empty on init" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .forward, "boo!"); + defer w.deinit(); + 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: SlidingWindow = try .init(alloc, .forward, "boo!"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("hello. boo! hello. 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(node); + + // We should be able to find two matches. + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start)); + try testing.expectEqual(point.Point{ .active = .{ + .x = 10, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end)); + } + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 19, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start)); + try testing.expectEqual(point.Point{ .active = .{ + .x = 22, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end)); + } + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); +} + +test "SlidingWindow single append no match" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .forward, "nope!"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("hello. boo! hello. 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(node); + + // No matches + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); + + // Should still keep the page + try testing.expectEqual(1, w.meta.len()); +} + +test "SlidingWindow two pages" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .forward, "boo!"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("boo!"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("\n"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + try s.testWriteString("hello. boo!"); + + // Add both pages + const node: *PageList.List.Node = s.pages.pages.first.?; + _ = try w.append(node); + _ = try w.append(node.next.?); + + // Search should find two matches + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 76, + .y = 22, + } }, s.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 79, + .y = 22, + } }, s.pages.pointFromPin(.active, sel.end).?); + } + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 23, + } }, s.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 10, + .y = 23, + } }, s.pages.pointFromPin(.active, sel.end).?); + } + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); +} + +test "SlidingWindow two pages match across boundary" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .forward, "hello, world"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("hell"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("o, world!"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + + // Add both pages + const node: *PageList.List.Node = s.pages.pages.first.?; + _ = try w.append(node); + _ = try w.append(node.next.?); + + // Search should find a match + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 76, + .y = 22, + } }, s.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 23, + } }, s.pages.pointFromPin(.active, sel.end).?); + } + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); + + // We shouldn't prune because we don't have enough space + try testing.expectEqual(2, w.meta.len()); +} + +test "SlidingWindow two pages no match across boundary with newline" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .forward, "hello, world"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("hell"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("\no, world!"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + + // Add both pages + const node: *PageList.List.Node = s.pages.pages.first.?; + _ = try w.append(node); + _ = try w.append(node.next.?); + + // Search should NOT find a match + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); + + // We shouldn't prune because we don't have enough space + try testing.expectEqual(2, w.meta.len()); +} + +test "SlidingWindow two pages no match across boundary with newline reverse" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .reverse, "hello, world"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("hell"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("\no, world!"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + + // Add both pages in reverse order + const node: *PageList.List.Node = s.pages.pages.first.?; + _ = try w.append(node.next.?); + _ = try w.append(node); + + // Search should NOT find a match + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); +} + +test "SlidingWindow two pages no match prunes first page" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .forward, "nope!"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("boo!"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("\n"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + try s.testWriteString("hello. boo!"); + + // Add both pages + const node: *PageList.List.Node = s.pages.pages.first.?; + _ = try w.append(node); + _ = try w.append(node.next.?); + + // Search should find nothing + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); + + // We should've pruned our page because the second page + // has enough text to contain our needle. + try testing.expectEqual(1, w.meta.len()); +} + +test "SlidingWindow two pages no match keeps both pages" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("boo!"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("\n"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + try s.testWriteString("hello. boo!"); + + // Imaginary needle for search. Doesn't match! + var needle_list: std.ArrayList(u8) = .empty; + defer needle_list.deinit(alloc); + try needle_list.appendNTimes(alloc, 'x', first_page_rows * s.pages.cols); + const needle: []const u8 = needle_list.items; + + var w: SlidingWindow = try .init(alloc, .forward, needle); + defer w.deinit(); + + // Add both pages + const node: *PageList.List.Node = s.pages.pages.first.?; + _ = try w.append(node); + _ = try w.append(node.next.?); + + // Search should find nothing + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); + + // No pruning because both pages are needed to fit needle. + try testing.expectEqual(2, w.meta.len()); +} + +test "SlidingWindow single append across circular buffer boundary" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .forward, "abc"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("XXXXXXXXXXXXXXXXXXXboo!XXXXX"); + + // We are trying to break a circular buffer boundary so the way we + // do this is to duplicate the data then do a failing search. This + // will cause the first page to be pruned. The next time we append we'll + // put it in the middle of the circ buffer. We assert this so that if + // 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); + { + // No wrap around yet + const slices = w.data.getPtrSlice(0, w.data.len()); + try testing.expect(slices[0].len > 0); + try testing.expect(slices[1].len == 0); + } + + // Search non-match, prunes page + try testing.expect(w.next() == null); + try testing.expectEqual(1, w.meta.len()); + + // Change the needle, just needs to be the same length (not a real API) + w.testChangeNeedle("boo"); + + // Add new page, now wraps + _ = try w.append(node); + { + const slices = w.data.getPtrSlice(0, w.data.len()); + try testing.expect(slices[0].len > 0); + try testing.expect(slices[1].len > 0); + } + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 19, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 21, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end).?); + } + try testing.expect(w.next() == null); +} + +test "SlidingWindow single append match on boundary" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .forward, "abcd"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("o!XXXXXXXXXXXXXXXXXXXbo"); + + // We need to surgically modify the last row to be soft-wrapped + try testing.expect(s.pages.pages.first == s.pages.pages.last); + const node: *PageList.List.Node = s.pages.pages.first.?; + node.data.getRow(node.data.size.rows - 1).wrap = true; + + // We are trying to break a circular buffer boundary so the way we + // do this is to duplicate the data then do a failing search. This + // will cause the first page to be pruned. The next time we append we'll + // put it in the middle of the circ buffer. We assert this so that if + // our implementation changes our test will fail. + _ = try w.append(node); + _ = try w.append(node); + { + // No wrap around yet + const slices = w.data.getPtrSlice(0, w.data.len()); + try testing.expect(slices[0].len > 0); + try testing.expect(slices[1].len == 0); + } + + // Search non-match, prunes page + try testing.expect(w.next() == null); + try testing.expectEqual(1, w.meta.len()); + + // Change the needle, just needs to be the same length (not a real API) + w.testChangeNeedle("boo!"); + + // Add new page, now wraps + _ = try w.append(node); + { + const slices = w.data.getPtrSlice(0, w.data.len()); + try testing.expect(slices[0].len > 0); + try testing.expect(slices[1].len > 0); + } + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 21, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 1, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end).?); + } + try testing.expect(w.next() == null); +} + +test "SlidingWindow single append reversed" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .reverse, "boo!"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("hello. boo! hello. 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(node); + + // We should be able to find two matches. + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 19, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 22, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end).?); + } + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 10, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end).?); + } + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); +} + +test "SlidingWindow single append no match reversed" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .reverse, "nope!"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("hello. boo! hello. 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(node); + + // No matches + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); + + // Should still keep the page + try testing.expectEqual(1, w.meta.len()); +} + +test "SlidingWindow two pages reversed" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .reverse, "boo!"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("boo!"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("\n"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + try s.testWriteString("hello. boo!"); + + // Add both pages in reverse order + const node: *PageList.List.Node = s.pages.pages.first.?; + _ = try w.append(node.next.?); + _ = try w.append(node); + + // Search should find two matches (in reverse order) + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 23, + } }, s.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 10, + .y = 23, + } }, s.pages.pointFromPin(.active, sel.end).?); + } + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 76, + .y = 22, + } }, s.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 79, + .y = 22, + } }, s.pages.pointFromPin(.active, sel.end).?); + } + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); +} + +test "SlidingWindow two pages match across boundary reversed" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .reverse, "hello, world"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "hell" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("hell"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("o, world!"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + + // Add both pages in reverse order + const node: *PageList.List.Node = s.pages.pages.first.?; + _ = try w.append(node.next.?); + _ = try w.append(node); + + // Search should find a match + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 76, + .y = 22, + } }, s.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 23, + } }, s.pages.pointFromPin(.active, sel.end).?); + } + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); + + // In reverse mode, the last appended meta (first original page) is large + // enough to contain needle.len - 1 bytes, so pruning occurs + try testing.expectEqual(1, w.meta.len()); +} + +test "SlidingWindow two pages no match prunes first page reversed" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .reverse, "nope!"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("boo!"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("\n"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + try s.testWriteString("hello. boo!"); + + // Add both pages in reverse order + const node: *PageList.List.Node = s.pages.pages.first.?; + _ = try w.append(node.next.?); + _ = try w.append(node); + + // Search should find nothing + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); + + // We should've pruned our page because the second page + // has enough text to contain our needle. + try testing.expectEqual(1, w.meta.len()); +} + +test "SlidingWindow two pages no match keeps both pages reversed" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); + defer s.deinit(); + + // Fill up the first page. The final bytes in the first page + // are "boo!" + const first_page_rows = s.pages.pages.first.?.data.capacity.rows; + for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); + for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); + try s.testWriteString("boo!"); + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try s.testWriteString("\n"); + try testing.expect(s.pages.pages.first != s.pages.pages.last); + try s.testWriteString("hello. boo!"); + + // Imaginary needle for search. Doesn't match! + var needle_list: std.ArrayList(u8) = .empty; + defer needle_list.deinit(alloc); + try needle_list.appendNTimes(alloc, 'x', first_page_rows * s.pages.cols); + const needle: []const u8 = needle_list.items; + + var w: SlidingWindow = try .init(alloc, .reverse, needle); + defer w.deinit(); + + // Add both pages in reverse order + const node: *PageList.List.Node = s.pages.pages.first.?; + _ = try w.append(node.next.?); + _ = try w.append(node); + + // Search should find nothing + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); + + // No pruning because both pages are needed to fit needle. + try testing.expectEqual(2, w.meta.len()); +} + +test "SlidingWindow single append across circular buffer boundary reversed" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .reverse, "abc"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("XXXXXXXXXXXXXXXXXXXboo!XXXXX"); + + // We are trying to break a circular buffer boundary so the way we + // do this is to duplicate the data then do a failing search. This + // will cause the first page to be pruned. The next time we append we'll + // put it in the middle of the circ buffer. We assert this so that if + // 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); + { + // No wrap around yet + const slices = w.data.getPtrSlice(0, w.data.len()); + try testing.expect(slices[0].len > 0); + try testing.expect(slices[1].len == 0); + } + + // Search non-match, prunes page + try testing.expect(w.next() == null); + try testing.expectEqual(1, w.meta.len()); + + // Change the needle, just needs to be the same length (not a real API) + // testChangeNeedle doesn't reverse, so pass reversed needle for reverse mode + w.testChangeNeedle("oob"); + + // Add new page, now wraps + _ = try w.append(node); + { + const slices = w.data.getPtrSlice(0, w.data.len()); + try testing.expect(slices[0].len > 0); + try testing.expect(slices[1].len > 0); + } + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 19, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 21, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end).?); + } + try testing.expect(w.next() == null); +} + +test "SlidingWindow single append match on boundary reversed" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .reverse, "abcd"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("o!XXXXXXXXXXXXXXXXXXXbo"); + + // We need to surgically modify the last row to be soft-wrapped + try testing.expect(s.pages.pages.first == s.pages.pages.last); + const node: *PageList.List.Node = s.pages.pages.first.?; + node.data.getRow(node.data.size.rows - 1).wrap = true; + + // We are trying to break a circular buffer boundary so the way we + // do this is to duplicate the data then do a failing search. This + // will cause the first page to be pruned. The next time we append we'll + // put it in the middle of the circ buffer. We assert this so that if + // our implementation changes our test will fail. + _ = try w.append(node); + _ = try w.append(node); + { + // No wrap around yet + const slices = w.data.getPtrSlice(0, w.data.len()); + try testing.expect(slices[0].len > 0); + try testing.expect(slices[1].len == 0); + } + + // Search non-match, prunes page + try testing.expect(w.next() == null); + try testing.expectEqual(1, w.meta.len()); + + // Change the needle, just needs to be the same length (not a real API) + // testChangeNeedle doesn't reverse, so pass reversed needle for reverse mode + w.testChangeNeedle("!oob"); + + // Add new page, now wraps + _ = try w.append(node); + { + const slices = w.data.getPtrSlice(0, w.data.len()); + try testing.expect(slices[0].len > 0); + try testing.expect(slices[1].len > 0); + } + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 21, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 1, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end).?); + } + try testing.expect(w.next() == null); +} From 6623c20c2dafd4320048e49b6d5a2ee802be24f9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 24 Nov 2025 10:19:10 -0800 Subject: [PATCH 10/22] terminal: switch search to use flattened highlights --- src/terminal/highlight.zig | 12 + src/terminal/search.zig | 1 - src/terminal/search/Thread.zig | 41 +- src/terminal/search/active.zig | 24 +- src/terminal/search/pagelist.zig | 24 +- src/terminal/search/screen.zig | 91 +- src/terminal/search/sliding_window.zig | 410 ++++--- src/terminal/search/sliding_window2.zig | 1400 ----------------------- src/terminal/search/viewport.zig | 31 +- 9 files changed, 412 insertions(+), 1622 deletions(-) delete mode 100644 src/terminal/search/sliding_window2.zig diff --git a/src/terminal/highlight.zig b/src/terminal/highlight.zig index 626d6e471..772d4d54b 100644 --- a/src/terminal/highlight.zig +++ b/src/terminal/highlight.zig @@ -132,6 +132,18 @@ pub const Flattened = struct { }; } + pub fn deinit(self: *Flattened, alloc: Allocator) void { + self.chunks.deinit(alloc); + } + + pub fn clone(self: *const Flattened, alloc: Allocator) Allocator.Error!Flattened { + return .{ + .chunks = try self.chunks.clone(alloc), + .top_x = self.top_x, + .bot_x = self.bot_x, + }; + } + /// Convert to an Untracked highlight. pub fn untracked(self: Flattened) Untracked { const slice = self.chunks.slice(); diff --git a/src/terminal/search.zig b/src/terminal/search.zig index 1ac18515c..e69603c25 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -19,5 +19,4 @@ test { // Non-public APIs _ = @import("search/sliding_window.zig"); - _ = @import("search/sliding_window2.zig"); } diff --git a/src/terminal/search/Thread.zig b/src/terminal/search/Thread.zig index 776dfc84a..fdd5f81bc 100644 --- a/src/terminal/search/Thread.zig +++ b/src/terminal/search/Thread.zig @@ -12,11 +12,13 @@ const std = @import("std"); const builtin = @import("builtin"); const testing = std.testing; const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; const Mutex = std.Thread.Mutex; const xev = @import("../../global.zig").xev; const internal_os = @import("../../os/main.zig"); const BlockingQueue = @import("../../datastruct/main.zig").BlockingQueue; const point = @import("../point.zig"); +const FlattenedHighlight = @import("../highlight.zig").Flattened; const PageList = @import("../PageList.zig"); const Screen = @import("../Screen.zig"); const ScreenSet = @import("../ScreenSet.zig"); @@ -387,7 +389,7 @@ pub const Event = union(enum) { /// Matches in the viewport have changed. The memory is owned by the /// search thread and is only valid during the callback. - viewport_matches: []const Selection, + viewport_matches: []const FlattenedHighlight, }; /// Search state. @@ -603,10 +605,13 @@ const Search = struct { // process will make it stale again. self.stale_viewport_matches = false; - var results: std.ArrayList(Selection) = .empty; - defer results.deinit(alloc); - while (self.viewport.next()) |sel| { - results.append(alloc, sel) catch |err| switch (err) { + var arena: ArenaAllocator = .init(alloc); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + var results: std.ArrayList(FlattenedHighlight) = .empty; + while (self.viewport.next()) |hl| { + const hl_cloned = hl.clone(arena_alloc) catch continue; + results.append(arena_alloc, hl_cloned) catch |err| switch (err) { error.OutOfMemory => { log.warn( "error collecting viewport matches err={}", @@ -637,7 +642,12 @@ test { const Self = @This(); reset: std.Thread.ResetEvent = .{}, total: usize = 0, - viewport: []const Selection = &.{}, + viewport: []FlattenedHighlight = &.{}, + + fn deinit(self: *Self) void { + for (self.viewport) |*hl| hl.deinit(testing.allocator); + testing.allocator.free(self.viewport); + } fn callback(event: Event, userdata: ?*anyopaque) void { const ud: *Self = @ptrCast(@alignCast(userdata.?)); @@ -645,11 +655,16 @@ test { .complete => ud.reset.set(), .total_matches => |v| ud.total = v, .viewport_matches => |v| { + for (ud.viewport) |*hl| hl.deinit(testing.allocator); testing.allocator.free(ud.viewport); - ud.viewport = testing.allocator.dupe( - Selection, - v, + + ud.viewport = testing.allocator.alloc( + FlattenedHighlight, + v.len, ) catch unreachable; + for (ud.viewport, v) |*dst, src| { + dst.* = src.clone(testing.allocator) catch unreachable; + } }, } } @@ -665,7 +680,7 @@ test { try stream.nextSlice("Hello, world"); var ud: UserData = .{}; - defer alloc.free(ud.viewport); + defer ud.deinit(); var thread: Thread = try .init(alloc, .{ .mutex = &mutex, .terminal = &t, @@ -698,14 +713,14 @@ test { try testing.expectEqual(1, ud.total); try testing.expectEqual(1, ud.viewport.len); { - const sel = ud.viewport[0]; + const sel = ud.viewport[0].untracked(); try testing.expectEqual(point.Point{ .screen = .{ .x = 7, .y = 0, - } }, t.screens.active.pages.pointFromPin(.screen, sel.start()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.start).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 11, .y = 0, - } }, t.screens.active.pages.pointFromPin(.screen, sel.end()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.end).?); } } diff --git a/src/terminal/search/active.zig b/src/terminal/search/active.zig index 2ace939e7..2329c40b0 100644 --- a/src/terminal/search/active.zig +++ b/src/terminal/search/active.zig @@ -3,6 +3,7 @@ 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 Selection = @import("../Selection.zig"); const SlidingWindow = @import("sliding_window.zig").SlidingWindow; @@ -96,7 +97,7 @@ pub const ActiveSearch = struct { /// 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 { + pub fn next(self: *ActiveSearch) ?FlattenedHighlight { return self.window.next(); } }; @@ -115,26 +116,28 @@ test "simple search" { _ = try search.update(&t.screens.active.pages); { - const sel = search.next().?; + 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()).?); + } }, 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()).?); + } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } { - const sel = search.next().?; + 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()).?); + } }, 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()).?); + } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } try testing.expect(search.next() == null); } @@ -158,15 +161,16 @@ test "clear screen and search" { _ = try search.update(&t.screens.active.pages); { - const sel = search.next().?; + 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()).?); + } }, 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()).?); + } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } try testing.expect(search.next() == null); } diff --git a/src/terminal/search/pagelist.zig b/src/terminal/search/pagelist.zig index 8a01a61fb..bd1ce9ef7 100644 --- a/src/terminal/search/pagelist.zig +++ b/src/terminal/search/pagelist.zig @@ -5,6 +5,7 @@ const testing = std.testing; const CircBuf = @import("../../datastruct/main.zig").CircBuf; const terminal = @import("../main.zig"); const point = terminal.point; +const FlattenedHighlight = @import("../highlight.zig").Flattened; const Page = terminal.Page; const PageList = terminal.PageList; const Pin = PageList.Pin; @@ -97,7 +98,7 @@ pub const PageListSearch = struct { /// /// This does NOT access the PageList, so it can be called without /// a lock held. - pub fn next(self: *PageListSearch) ?Selection { + pub fn next(self: *PageListSearch) ?FlattenedHighlight { return self.window.next(); } @@ -149,26 +150,28 @@ test "simple search" { defer search.deinit(); { - const sel = search.next().?; + 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()).?); + } }, 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()).?); + } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } { - const sel = search.next().?; + 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()).?); + } }, 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()).?); + } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } try testing.expect(search.next() == null); @@ -335,12 +338,13 @@ test "feed with match spanning page boundary" { try testing.expect(try search.feed()); // Should find the spanning match - const sel = search.next().?; - try testing.expect(sel.start().node != sel.end().node); + const h = search.next().?; + const sel = h.untracked(); + try testing.expect(sel.start.node != sel.end.node); { const str = try t.screens.active.selectionString( alloc, - .{ .sel = sel }, + .{ .sel = .init(sel.start, sel.end, false) }, ); defer alloc.free(str); try testing.expectEqualStrings(str, "Test"); diff --git a/src/terminal/search/screen.zig b/src/terminal/search/screen.zig index d2d138442..071ccd090 100644 --- a/src/terminal/search/screen.zig +++ b/src/terminal/search/screen.zig @@ -3,6 +3,7 @@ const assert = @import("../../quirks.zig").inlineAssert; const testing = std.testing; const Allocator = std.mem.Allocator; const point = @import("../point.zig"); +const FlattenedHighlight = @import("../highlight.zig").Flattened; const PageList = @import("../PageList.zig"); const Pin = PageList.Pin; const Screen = @import("../Screen.zig"); @@ -44,8 +45,8 @@ pub const ScreenSearch = struct { /// 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_results: std.ArrayList(FlattenedHighlight), + active_results: std.ArrayList(FlattenedHighlight), /// History search state. const HistorySearch = struct { @@ -120,7 +121,9 @@ pub const ScreenSearch = struct { const alloc = self.allocator(); self.active.deinit(); if (self.history) |*h| h.deinit(self.screen); + for (self.active_results.items) |*hl| hl.deinit(alloc); self.active_results.deinit(alloc); + for (self.history_results.items) |*hl| hl.deinit(alloc); self.history_results.deinit(alloc); } @@ -145,11 +148,11 @@ pub const ScreenSearch = struct { pub fn matches( self: *ScreenSearch, alloc: Allocator, - ) Allocator.Error![]Selection { + ) Allocator.Error![]FlattenedHighlight { const active_results = self.active_results.items; const history_results = self.history_results.items; const results = try alloc.alloc( - Selection, + FlattenedHighlight, active_results.len + history_results.len, ); errdefer alloc.free(results); @@ -162,7 +165,7 @@ pub const ScreenSearch = struct { results[0..active_results.len], active_results, ); - std.mem.reverse(Selection, results[0..active_results.len]); + std.mem.reverse(FlattenedHighlight, results[0..active_results.len]); // History does a backward search, so we can just append them // after. @@ -247,13 +250,15 @@ pub const ScreenSearch = struct { // 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| { + while (self.active.next()) |hl| { // 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); + var hl_cloned = try hl.clone(alloc); + errdefer hl_cloned.deinit(alloc); + try self.active_results.append(alloc, hl_cloned); } // We've consumed the entire active area, move to history. @@ -270,13 +275,15 @@ pub const ScreenSearch = struct { // 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.searcher.next()) |sel| { + while (history.searcher.next()) |hl| { // Ignore selections that are found within the starting // node since those are covered by the active area search. - if (sel.start().node == history.start_pin.node) continue; + if (hl.chunks.items(.node)[0] == history.start_pin.node) continue; // Same note as tickActive for error handling. - try self.history_results.append(alloc, sel); + var hl_cloned = try hl.clone(alloc); + errdefer hl_cloned.deinit(alloc); + try self.history_results.append(alloc, hl_cloned); } // We need to be fed more data. @@ -291,6 +298,7 @@ pub const ScreenSearch = struct { /// /// The caller must hold the necessary locks to access the screen state. pub fn reloadActive(self: *ScreenSearch) Allocator.Error!void { + const alloc = self.allocator(); 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 @@ -305,6 +313,7 @@ pub const ScreenSearch = struct { if (h.start_pin.garbage) { h.deinit(self.screen); self.history = null; + for (self.history_results.items) |*hl| hl.deinit(alloc); self.history_results.clearRetainingCapacity(); break :state null; } @@ -317,7 +326,7 @@ pub const ScreenSearch = struct { // initialize. var search: PageListSearch = try .init( - self.allocator(), + alloc, self.needle(), list, history_node, @@ -346,7 +355,6 @@ pub const ScreenSearch = struct { // 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, @@ -361,17 +369,17 @@ pub const ScreenSearch = struct { } assert(history.start_pin.node == history_node); - var results: std.ArrayList(Selection) = try .initCapacity( + var results: std.ArrayList(FlattenedHighlight) = try .initCapacity( alloc, self.history_results.items.len, ); errdefer results.deinit(alloc); - while (window.next()) |sel| { - if (sel.start().node == history_node) continue; - try results.append( - alloc, - sel, - ); + while (window.next()) |hl| { + if (hl.chunks.items(.node)[0] == history_node) continue; + + var hl_cloned = try hl.clone(alloc); + errdefer hl_cloned.deinit(alloc); + try results.append(alloc, hl_cloned); } // If we have no matches then there is nothing to change @@ -380,13 +388,14 @@ pub const ScreenSearch = struct { // 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); + std.mem.reverse(FlattenedHighlight, results.items); try results.appendSlice(alloc, self.history_results.items); self.history_results.deinit(alloc); self.history_results = results; } // Reset our active search results and search again. + for (self.active_results.items) |*hl| hl.deinit(alloc); self.active_results.clearRetainingCapacity(); switch (self.state) { // If we're in the active state we run a normal tick so @@ -425,26 +434,26 @@ test "simple search" { try testing.expectEqual(2, matches.len); { - const sel = matches[0]; + const sel = matches[0].untracked(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 2, - } }, t.screens.active.pages.pointFromPin(.screen, sel.start()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.start).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, .y = 2, - } }, t.screens.active.pages.pointFromPin(.screen, sel.end()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.end).?); } { - const sel = matches[1]; + const sel = matches[1].untracked(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, - } }, t.screens.active.pages.pointFromPin(.screen, sel.start()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.start).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, .y = 0, - } }, t.screens.active.pages.pointFromPin(.screen, sel.end()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.end).?); } } @@ -477,15 +486,15 @@ test "simple search with history" { try testing.expectEqual(1, matches.len); { - const sel = matches[0]; + const sel = matches[0].untracked(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, - } }, t.screens.active.pages.pointFromPin(.screen, sel.start()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.start).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, .y = 0, - } }, t.screens.active.pages.pointFromPin(.screen, sel.end()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.end).?); } } @@ -528,26 +537,26 @@ test "reload active with history change" { defer alloc.free(matches); try testing.expectEqual(2, matches.len); { - const sel = matches[1]; + const sel = matches[1].untracked(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, - } }, t.screens.active.pages.pointFromPin(.screen, sel.start()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.start).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, .y = 0, - } }, t.screens.active.pages.pointFromPin(.screen, sel.end()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.end).?); } { - const sel = matches[0]; + const sel = matches[0].untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 1, .y = 1, - } }, t.screens.active.pages.pointFromPin(.active, sel.start()).?); + } }, t.screens.active.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 4, .y = 1, - } }, t.screens.active.pages.pointFromPin(.active, sel.end()).?); + } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } } @@ -562,15 +571,15 @@ test "reload active with history change" { defer alloc.free(matches); try testing.expectEqual(1, matches.len); { - const sel = matches[0]; + const sel = matches[0].untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 2, .y = 0, - } }, t.screens.active.pages.pointFromPin(.active, sel.start()).?); + } }, t.screens.active.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 5, .y = 0, - } }, t.screens.active.pages.pointFromPin(.active, sel.end()).?); + } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } } } @@ -603,14 +612,14 @@ test "active change contents" { try testing.expectEqual(1, matches.len); { - const sel = matches[0]; + const sel = matches[0].untracked(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 1, - } }, t.screens.active.pages.pointFromPin(.screen, sel.start()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.start).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, .y = 1, - } }, t.screens.active.pages.pointFromPin(.screen, sel.end()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.end).?); } } diff --git a/src/terminal/search/sliding_window.zig b/src/terminal/search/sliding_window.zig index b0df3c13b..c1428e35c 100644 --- a/src/terminal/search/sliding_window.zig +++ b/src/terminal/search/sliding_window.zig @@ -4,11 +4,13 @@ const Allocator = std.mem.Allocator; const CircBuf = @import("../../datastruct/main.zig").CircBuf; const terminal = @import("../main.zig"); const point = terminal.point; +const size = terminal.size; const PageList = terminal.PageList; const Pin = PageList.Pin; const Selection = terminal.Selection; const Screen = terminal.Screen; const PageFormatter = @import("../formatter.zig").PageFormatter; +const FlattenedHighlight = terminal.highlight.Flattened; /// Searches page nodes via a sliding window. The sliding window maintains /// the invariant that data isn't pruned until (1) we've searched it and @@ -51,6 +53,10 @@ pub const SlidingWindow = struct { /// data to meta. meta: MetaBuf, + /// Buffer that can fit any amount of chunks necessary for next + /// to never fail allocation. + chunk_buf: std.MultiArrayList(FlattenedHighlight.Chunk), + /// Offset into data for our current state. This handles the /// situation where our search moved through meta[0] but didn't /// do enough to prune it. @@ -113,6 +119,7 @@ pub const SlidingWindow = struct { .alloc = alloc, .data = data, .meta = meta, + .chunk_buf = .empty, .needle = needle, .direction = direction, .overlap_buf = overlap_buf, @@ -122,6 +129,7 @@ pub const SlidingWindow = struct { pub fn deinit(self: *SlidingWindow) void { self.alloc.free(self.overlap_buf); self.alloc.free(self.needle); + self.chunk_buf.deinit(self.alloc); self.data.deinit(self.alloc); var meta_it = self.meta.iterator(.forward); @@ -143,14 +151,17 @@ pub const SlidingWindow = struct { /// 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 { + /// This returns a flattened highlight on a match. The + /// flattened highlight requires allocation and is therefore more expensive + /// than a normal selection, but it is more efficient to render since it + /// has all the information without having to dereference pointers into + /// the terminal state. + /// + /// The flattened highlight chunks reference internal memory for this + /// sliding window and are only valid until the next call to `next()` + /// or `append()`. If the caller wants to retain the flattened highlight + /// then they should clone it. + pub fn next(self: *SlidingWindow) ?FlattenedHighlight { const slices = slices: { // If we have less data then the needle then we can't possibly match const data_len = self.data.len(); @@ -164,7 +175,7 @@ pub const SlidingWindow = struct { // Search the first slice for the needle. if (std.mem.indexOf(u8, slices[0], self.needle)) |idx| { - return self.selection( + return self.highlight( idx, self.needle.len, ); @@ -197,7 +208,7 @@ pub const SlidingWindow = struct { // We found a match in the overlap buffer. We need to map the // index back to the data buffer in order to get our selection. - return self.selection( + return self.highlight( slices[0].len - prefix.len + idx, self.needle.len, ); @@ -205,7 +216,7 @@ pub const SlidingWindow = struct { // Search the last slice for the needle. if (std.mem.indexOf(u8, slices[1], self.needle)) |idx| { - return self.selection( + return self.highlight( slices[0].len + idx, self.needle.len, ); @@ -263,114 +274,230 @@ pub const SlidingWindow = struct { return null; } - /// Return a selection for the given start and length into the data - /// buffer and also prune the data/meta buffers if possible up to - /// this start index. + /// Return a flattened highlight for the given start and length. + /// + /// The flattened highlight can be used to render the highlight + /// in the most efficient way because it doesn't require a terminal + /// lock to access terminal data to compare whether some viewport + /// matches the highlight (because it doesn't need to traverse + /// the page nodes). /// /// The start index is assumed to be relative to the offset. i.e. /// index zero is actually at `self.data[self.data_offset]`. The /// selection will account for the offset. - fn selection( + fn highlight( self: *SlidingWindow, start_offset: usize, len: usize, - ) Selection { + ) terminal.highlight.Flattened { const start = start_offset + self.data_offset; - assert(start < self.data.len()); - assert(start + len <= self.data.len()); + const end = start + len - 1; + if (comptime std.debug.runtime_safety) { + assert(start < self.data.len()); + assert(start + len <= self.data.len()); + } - // meta_consumed is the number of bytes we've consumed in the - // data buffer up to and NOT including the meta where we've - // found our pin. This is important because it tells us the - // amount of data we can safely deleted from self.data since - // we can't partially delete a meta block's data. (The partial - // amount is represented by self.data_offset). - var meta_it = self.meta.iterator(.forward); - var meta_consumed: usize = 0; - const tl: Pin = pin(&meta_it, &meta_consumed, start); + // Clear our previous chunk buffer to store this result + self.chunk_buf.clearRetainingCapacity(); + var result: terminal.highlight.Flattened = .empty; - // Store the information required to prune later. We store this - // now because we only want to prune up to our START so we can - // find overlapping matches. - const tl_meta_idx = meta_it.idx - 1; - const tl_meta_consumed = meta_consumed; + // Go through the meta nodes to find our start. + const tl: struct { + /// If non-null, we need to continue searching for the bottom-right. + br: ?struct { + it: MetaBuf.Iterator, + consumed: usize, + }, - // We have to seek back so that we reinspect our current - // iterator value again in case the start and end are in the - // same segment. - meta_it.seekBy(-1); - const br: Pin = pin(&meta_it, &meta_consumed, start + len - 1); - assert(meta_it.idx >= 1); + /// Data to prune, both are lengths. + prune: struct { + meta: usize, + data: usize, + }, + } = tl: { + var meta_it = self.meta.iterator(.forward); + var meta_consumed: usize = 0; + while (meta_it.next()) |meta| { + // Always increment our consumed count so that our index + // is right for the end search if we do it. + const prior_meta_consumed = meta_consumed; + meta_consumed += meta.cell_map.items.len; + + // meta_i is the index we expect to find the match in the + // cell map within this meta if it contains it. + const meta_i = start - prior_meta_consumed; + + // This meta doesn't contain the match. This means we + // can also prune this set of data because we only look + // forward. + if (meta_i >= meta.cell_map.items.len) continue; + + // Now we look for the end. In MOST cases it is the same as + // our starting chunk because highlights are usually small and + // not on a boundary, so let's optimize for that. + const end_i = end - prior_meta_consumed; + if (end_i < meta.cell_map.items.len) { + @branchHint(.likely); + + // The entire highlight is within this meta. + const start_map = meta.cell_map.items[meta_i]; + const end_map = meta.cell_map.items[end_i]; + result.top_x = start_map.x; + result.bot_x = end_map.x; + self.chunk_buf.appendAssumeCapacity(.{ + .node = meta.node, + .start = @intCast(start_map.y), + .end = @intCast(end_map.y + 1), + }); + + break :tl .{ + .br = null, + .prune = .{ + .meta = meta_it.idx - 1, + .data = prior_meta_consumed, + }, + }; + } else { + // We found the meta that contains the start of the match + // only. Consume this entire node from our start offset. + const map = meta.cell_map.items[meta_i]; + result.top_x = map.x; + self.chunk_buf.appendAssumeCapacity(.{ + .node = meta.node, + .start = @intCast(map.y), + .end = meta.node.data.size.rows, + }); + + break :tl .{ + .br = .{ + .it = meta_it, + .consumed = meta_consumed, + }, + .prune = .{ + .meta = meta_it.idx - 1, + .data = prior_meta_consumed, + }, + }; + } + } else { + // Precondition that the start index is within the data buffer. + unreachable; + } + }; + + // Search for our end. + if (tl.br) |br| { + var meta_it = br.it; + var meta_consumed: usize = br.consumed; + while (meta_it.next()) |meta| { + // meta_i is the index we expect to find the match in the + // cell map within this meta if it contains it. + const meta_i = end - meta_consumed; + if (meta_i >= meta.cell_map.items.len) { + // This meta doesn't contain the match. We still add it + // to our results because we want the full flattened list. + self.chunk_buf.appendAssumeCapacity(.{ + .node = meta.node, + .start = 0, + .end = meta.node.data.size.rows, + }); + + meta_consumed += meta.cell_map.items.len; + continue; + } + + // We found it + const map = meta.cell_map.items[meta_i]; + result.bot_x = map.x; + self.chunk_buf.appendAssumeCapacity(.{ + .node = meta.node, + .start = 0, + .end = @intCast(map.y + 1), + }); + break; + } else { + // Precondition that the end index is within the data buffer. + unreachable; + } + } // Our offset into the current meta block is the start index // minus the amount of data fully consumed. We then add one // to move one past the match so we don't repeat it. - self.data_offset = start - tl_meta_consumed + 1; + self.data_offset = start - tl.prune.data + 1; - // meta_it.idx is br's meta index plus one (because the iterator - // moves one past the end; we call next() one last time). So - // we compare against one to check that the meta that we matched - // in has prior meta blocks we can prune. - if (tl_meta_idx > 0) { + // If we went beyond our initial meta node we can prune. + if (tl.prune.meta > 0) { // Deinit all our memory in the meta blocks prior to our // match. - const meta_count = tl_meta_idx; - meta_it.reset(); - for (0..meta_count) |_| meta_it.next().?.deinit(self.alloc); - if (comptime std.debug.runtime_safety) { - assert(meta_it.idx == meta_count); - assert(meta_it.next().?.node == tl.node); + var meta_it = self.meta.iterator(.forward); + var meta_consumed: usize = 0; + for (0..tl.prune.meta) |_| { + const meta: *Meta = meta_it.next().?; + meta_consumed += meta.cell_map.items.len; + meta.deinit(self.alloc); } - self.meta.deleteOldest(meta_count); + if (comptime std.debug.runtime_safety) { + assert(meta_it.idx == tl.prune.meta); + assert(meta_it.next().?.node == self.chunk_buf.items(.node)[0]); + } + self.meta.deleteOldest(tl.prune.meta); // Delete all the data up to our current index. - assert(tl_meta_consumed > 0); - self.data.deleteOldest(tl_meta_consumed); + assert(tl.prune.data > 0); + self.data.deleteOldest(tl.prune.data); } - self.assertIntegrity(); - return switch (self.direction) { - .forward => .init(tl, br, false), - .reverse => .init(br, tl, false), - }; - } + switch (self.direction) { + .forward => {}, + .reverse => { + if (self.chunk_buf.len > 1) { + // Reverse all our chunks. This should be pretty obvious why. + const slice = self.chunk_buf.slice(); + const nodes = slice.items(.node); + const starts = slice.items(.start); + const ends = slice.items(.end); + std.mem.reverse(*PageList.List.Node, nodes); + std.mem.reverse(size.CellCountInt, starts); + std.mem.reverse(size.CellCountInt, ends); - /// Convert a data index into a pin. - /// - /// The iterator and offset are both expected to be passed by - /// pointer so that the pin can be efficiently called for multiple - /// indexes (in order). See selection() for an example. - /// - /// Precondition: the index must be within the data buffer. - fn pin( - it: *MetaBuf.Iterator, - offset: *usize, - idx: usize, - ) Pin { - while (it.next()) |meta| { - // meta_i is the index we expect to find the match in the - // cell map within this meta if it contains it. - const meta_i = idx - offset.*; - if (meta_i >= meta.cell_map.items.len) { - // This meta doesn't contain the match. This means we - // can also prune this set of data because we only look - // forward. - offset.* += meta.cell_map.items.len; - continue; - } + // Now normally with forward traversal with multiple pages, + // the suffix of the first page and the prefix of the last + // page are used. + // + // For a reverse traversal, this is inverted (since the + // pages are in reverse order we get the suffix of the last + // page and the prefix of the first page). So we need to + // invert this. + // + // We DON'T need to do this for any middle pages because + // they always use the full page. + // + // We DON'T need to do this for chunks.len == 1 because + // the pages themselves aren't reversed and we don't have + // any prefix/suffix problems. + // + // This is a fixup that makes our start/end match the + // same logic as the loops above if they were in forward + // order. + assert(nodes.len >= 2); + starts[0] = ends[0] - 1; + ends[0] = nodes[0].data.size.rows; + ends[nodes.len - 1] = starts[nodes.len - 1] + 1; + starts[nodes.len - 1] = 0; + } - // We found the meta that contains the start of the match. - const map = meta.cell_map.items[meta_i]; - return .{ - .node = meta.node, - .y = @intCast(map.y), - .x = map.x, - }; + // X values also need to be reversed since the top/bottom + // are swapped for the nodes. + const top_x = result.top_x; + result.top_x = result.bot_x; + result.bot_x = top_x; + }, } - // Unreachable because it is a precondition that the index is - // within the data buffer. - unreachable; + // Copy over our MultiArrayList so it points to the proper memory. + result.chunks = self.chunk_buf; + return result; } /// Add a new node to the sliding window. This will always grow @@ -442,6 +569,7 @@ pub const SlidingWindow = struct { // Ensure our buffers are big enough to store what we need. try self.data.ensureUnusedCapacity(self.alloc, written.len); try self.meta.ensureUnusedCapacity(self.alloc, 1); + try self.chunk_buf.ensureTotalCapacity(self.alloc, self.meta.capacity()); // Append our new node to the circular buffer. self.data.appendSliceAssumeCapacity(written); @@ -505,26 +633,28 @@ test "SlidingWindow single append" { // We should be able to find two matches. { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 7, .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start)); try testing.expectEqual(point.Point{ .active = .{ .x = 10, .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end)); } { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 19, .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start)); try testing.expectEqual(point.Point{ .active = .{ .x = 22, .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end)); } try testing.expect(w.next() == null); try testing.expect(w.next() == null); @@ -582,26 +712,28 @@ test "SlidingWindow two pages" { // Search should find two matches { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 76, .y = 22, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 79, .y = 22, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end).?); } { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 7, .y = 23, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 10, .y = 23, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end).?); } try testing.expect(w.next() == null); try testing.expect(w.next() == null); @@ -634,15 +766,16 @@ test "SlidingWindow two pages match across boundary" { // Search should find a match { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 76, .y = 22, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 7, .y = 23, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end).?); } try testing.expect(w.next() == null); try testing.expect(w.next() == null); @@ -831,15 +964,16 @@ test "SlidingWindow single append across circular buffer boundary" { try testing.expect(slices[1].len > 0); } { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 19, .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 21, .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end).?); } try testing.expect(w.next() == null); } @@ -889,15 +1023,16 @@ test "SlidingWindow single append match on boundary" { try testing.expect(slices[1].len > 0); } { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 21, .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 1, .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end).?); } try testing.expect(w.next() == null); } @@ -920,26 +1055,28 @@ test "SlidingWindow single append reversed" { // We should be able to find two matches. { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 19, .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 22, .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end).?); } { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 7, .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 10, .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end).?); } try testing.expect(w.next() == null); try testing.expect(w.next() == null); @@ -997,26 +1134,28 @@ test "SlidingWindow two pages reversed" { // Search should find two matches (in reverse order) { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 7, .y = 23, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 10, .y = 23, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end).?); } { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 76, .y = 22, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 79, .y = 22, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end).?); } try testing.expect(w.next() == null); try testing.expect(w.next() == null); @@ -1049,15 +1188,16 @@ test "SlidingWindow two pages match across boundary reversed" { // Search should find a match { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 76, .y = 22, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 7, .y = 23, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end).?); } try testing.expect(w.next() == null); try testing.expect(w.next() == null); @@ -1185,15 +1325,16 @@ test "SlidingWindow single append across circular buffer boundary reversed" { try testing.expect(slices[1].len > 0); } { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 19, .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 21, .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end).?); } try testing.expect(w.next() == null); } @@ -1244,15 +1385,16 @@ test "SlidingWindow single append match on boundary reversed" { try testing.expect(slices[1].len > 0); } { - const sel = w.next().?; + const h = w.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .active = .{ .x = 21, .y = 0, - } }, s.pages.pointFromPin(.active, sel.start()).?); + } }, s.pages.pointFromPin(.active, sel.start).?); try testing.expectEqual(point.Point{ .active = .{ .x = 1, .y = 0, - } }, s.pages.pointFromPin(.active, sel.end()).?); + } }, s.pages.pointFromPin(.active, sel.end).?); } try testing.expect(w.next() == null); } diff --git a/src/terminal/search/sliding_window2.zig b/src/terminal/search/sliding_window2.zig deleted file mode 100644 index 6aad0bff9..000000000 --- a/src/terminal/search/sliding_window2.zig +++ /dev/null @@ -1,1400 +0,0 @@ -const std = @import("std"); -const assert = @import("../../quirks.zig").inlineAssert; -const Allocator = std.mem.Allocator; -const CircBuf = @import("../../datastruct/main.zig").CircBuf; -const terminal = @import("../main.zig"); -const point = terminal.point; -const size = terminal.size; -const PageList = terminal.PageList; -const Pin = PageList.Pin; -const Selection = terminal.Selection; -const Screen = terminal.Screen; -const PageFormatter = @import("../formatter.zig").PageFormatter; -const FlattenedHighlight = terminal.highlight.Flattened; - -/// Searches page nodes via a sliding window. The sliding window maintains -/// the invariant that data isn't pruned until (1) we've searched it and -/// (2) we've accounted for overlaps across pages to fit the needle. -/// -/// The sliding window is first initialized empty. Pages are then appended -/// in the order to search them. The sliding window supports both a forward -/// and reverse order specified via `init`. The pages should be appended -/// in the correct order matching the search direction. -/// -/// All appends grow the window. The window is only pruned when a search -/// is done (positive or negative match) via `next()`. -/// -/// To avoid unnecessary memory growth, the recommended usage is to -/// call `next()` until it returns null and then `append` the next page -/// and repeat the process. This will always maintain the minimum -/// required memory to search for the needle. -/// -/// The caller is responsible for providing the pages and ensuring they're -/// in the proper order. The SlidingWindow itself doesn't own the pages, but -/// it will contain pointers to them in order to return selections. If any -/// pages become invalid, the caller should clear the sliding window and -/// start over. -pub const SlidingWindow = struct { - /// The allocator to use for all the data within this window. We - /// store this rather than passing it around because its already - /// part of multiple elements (eg. Meta's CellMap) and we want to - /// ensure we always use a consistent allocator. Additionally, only - /// a small amount of sliding windows are expected to be in use - /// at any one time so the memory overhead isn't that large. - alloc: Allocator, - - /// 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, - - /// Buffer that can fit any amount of chunks necessary for next - /// to never fail allocation. - chunk_buf: std.MultiArrayList(FlattenedHighlight.Chunk), - - /// Offset into data for our current state. This handles the - /// situation where our search moved through meta[0] but didn't - /// do enough to prune it. - data_offset: usize = 0, - - /// The needle we're searching for. Does own the memory. - needle: []const u8, - - /// The search direction. If the direction is forward then pages should - /// be appended in forward linked list order from the PageList. If the - /// direction is reverse then pages should be appended in reverse order. - /// - /// This is important because in most cases, a reverse search is going - /// to be more desirable to search from the end of the active area - /// backwards so more recent data is found first. Supporting both is - /// trivial though and will let us do more complex optimizations in the - /// future (e.g. starting from the viewport and doing a forward/reverse - /// concurrently from that point). - direction: Direction, - - /// A buffer to store the overlap search data. This is used to search - /// overlaps between pages where the match starts on one page and - /// ends on another. The length is always `needle.len * 2`. - overlap_buf: []u8, - - const Direction = enum { forward, reverse }; - const DataBuf = CircBuf(u8, 0); - const MetaBuf = CircBuf(Meta, undefined); - const Meta = struct { - node: *PageList.List.Node, - cell_map: std.ArrayList(point.Coordinate), - - pub fn deinit(self: *Meta, alloc: Allocator) void { - self.cell_map.deinit(alloc); - } - }; - - pub fn init( - alloc: Allocator, - direction: Direction, - needle_unowned: []const u8, - ) 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); - - const needle = try alloc.dupe(u8, needle_unowned); - errdefer alloc.free(needle); - switch (direction) { - .forward => {}, - .reverse => std.mem.reverse(u8, needle), - } - - const overlap_buf = try alloc.alloc(u8, needle.len * 2); - errdefer alloc.free(overlap_buf); - - return .{ - .alloc = alloc, - .data = data, - .meta = meta, - .chunk_buf = .empty, - .needle = needle, - .direction = direction, - .overlap_buf = overlap_buf, - }; - } - - pub fn deinit(self: *SlidingWindow) void { - self.alloc.free(self.overlap_buf); - self.alloc.free(self.needle); - self.chunk_buf.deinit(self.alloc); - self.data.deinit(self.alloc); - - var meta_it = self.meta.iterator(.forward); - while (meta_it.next()) |meta| meta.deinit(self.alloc); - self.meta.deinit(self.alloc); - } - - /// Clear all data but retain allocated capacity. - pub fn clearAndRetainCapacity(self: *SlidingWindow) void { - var meta_it = self.meta.iterator(.forward); - while (meta_it.next()) |meta| meta.deinit(self.alloc); - self.meta.clear(); - self.data.clear(); - self.data_offset = 0; - } - - /// Search the window for the next occurrence of the needle. As - /// the window moves, the window will prune itself while maintaining - /// the invariant that the window is always big enough to contain - /// the needle. - /// - /// This returns a flattened highlight on a match. The - /// flattened highlight requires allocation and is therefore more expensive - /// than a normal selection, but it is more efficient to render since it - /// has all the information without having to dereference pointers into - /// the terminal state. - /// - /// The flattened highlight chunks reference internal memory for this - /// sliding window and are only valid until the next call to `next()` - /// or `append()`. If the caller wants to retain the flattened highlight - /// then they should clone it. - pub fn next(self: *SlidingWindow) ?FlattenedHighlight { - const slices = slices: { - // If we have less data then the needle then we can't possibly match - const data_len = self.data.len(); - if (data_len < self.needle.len) return null; - - break :slices self.data.getPtrSlice( - self.data_offset, - data_len - self.data_offset, - ); - }; - - // Search the first slice for the needle. - if (std.mem.indexOf(u8, slices[0], self.needle)) |idx| { - return self.highlight( - idx, - self.needle.len, - ); - } - - // Search the overlap buffer for the needle. - if (slices[0].len > 0 and slices[1].len > 0) overlap: { - // Get up to needle.len - 1 bytes from each side (as much as - // we can) and store it in the overlap buffer. - const prefix: []const u8 = prefix: { - const len = @min(slices[0].len, self.needle.len - 1); - const idx = slices[0].len - len; - break :prefix slices[0][idx..]; - }; - const suffix: []const u8 = suffix: { - const len = @min(slices[1].len, self.needle.len - 1); - break :suffix slices[1][0..len]; - }; - const overlap_len = prefix.len + suffix.len; - assert(overlap_len <= self.overlap_buf.len); - @memcpy(self.overlap_buf[0..prefix.len], prefix); - @memcpy(self.overlap_buf[prefix.len..overlap_len], suffix); - - // Search the overlap - const idx = std.mem.indexOf( - u8, - self.overlap_buf[0..overlap_len], - self.needle, - ) orelse break :overlap; - - // We found a match in the overlap buffer. We need to map the - // index back to the data buffer in order to get our selection. - return self.highlight( - slices[0].len - prefix.len + idx, - self.needle.len, - ); - } - - // Search the last slice for the needle. - if (std.mem.indexOf(u8, slices[1], self.needle)) |idx| { - return self.highlight( - slices[0].len + idx, - self.needle.len, - ); - } - - // No match. We keep `needle.len - 1` bytes available to - // handle the future overlap case. - var meta_it = self.meta.iterator(.reverse); - prune: { - var saved: usize = 0; - while (meta_it.next()) |meta| { - const needed = self.needle.len - 1 - saved; - if (meta.cell_map.items.len >= needed) { - // We save up to this meta. We set our data offset - // to exactly where it needs to be to continue - // searching. - self.data_offset = meta.cell_map.items.len - needed; - break; - } - - saved += meta.cell_map.items.len; - } else { - // If we exited the while loop naturally then we - // never got the amount we needed and so there is - // nothing to prune. - assert(saved < self.needle.len - 1); - break :prune; - } - - const prune_count = self.meta.len() - meta_it.idx; - if (prune_count == 0) { - // This can happen if we need to save up to the first - // meta value to retain our window. - break :prune; - } - - // We can now delete all the metas up to but NOT including - // the meta we found through meta_it. - meta_it = self.meta.iterator(.forward); - var prune_data_len: usize = 0; - for (0..prune_count) |_| { - const meta = meta_it.next().?; - prune_data_len += meta.cell_map.items.len; - meta.deinit(self.alloc); - } - self.meta.deleteOldest(prune_count); - self.data.deleteOldest(prune_data_len); - } - - // Our data offset now moves to needle.len - 1 from the end so - // that we can handle the overlap case. - self.data_offset = self.data.len() - self.needle.len + 1; - - self.assertIntegrity(); - return null; - } - - /// Return a flattened highlight for the given start and length. - /// - /// The flattened highlight can be used to render the highlight - /// in the most efficent way because it doesn't require a terminal - /// lock to access terminal data to compare whether some viewport - /// matches the highlight (because it doesn't need to traverse - /// the page nodes). - /// - /// The start index is assumed to be relative to the offset. i.e. - /// index zero is actually at `self.data[self.data_offset]`. The - /// selection will account for the offset. - fn highlight( - self: *SlidingWindow, - start_offset: usize, - len: usize, - ) terminal.highlight.Flattened { - const start = start_offset + self.data_offset; - const end = start + len - 1; - if (comptime std.debug.runtime_safety) { - assert(start < self.data.len()); - assert(start + len <= self.data.len()); - } - - // Clear our previous chunk buffer to store this result - self.chunk_buf.clearRetainingCapacity(); - var result: terminal.highlight.Flattened = .empty; - - // Go through the meta nodes to find our start. - const tl: struct { - /// If non-null, we need to continue searching for the bottom-right. - br: ?struct { - it: MetaBuf.Iterator, - consumed: usize, - }, - - /// Data to prune, both are lengths. - prune: struct { - meta: usize, - data: usize, - }, - } = tl: { - var meta_it = self.meta.iterator(.forward); - var meta_consumed: usize = 0; - while (meta_it.next()) |meta| { - // Always increment our consumed count so that our index - // is right for the end search if we do it. - const prior_meta_consumed = meta_consumed; - meta_consumed += meta.cell_map.items.len; - - // meta_i is the index we expect to find the match in the - // cell map within this meta if it contains it. - const meta_i = start - prior_meta_consumed; - - // This meta doesn't contain the match. This means we - // can also prune this set of data because we only look - // forward. - if (meta_i >= meta.cell_map.items.len) continue; - - // Now we look for the end. In MOST cases it is the same as - // our starting chunk because highlights are usually small and - // not on a boundary, so let's optimize for that. - const end_i = end - prior_meta_consumed; - if (end_i < meta.cell_map.items.len) { - @branchHint(.likely); - - // The entire highlight is within this meta. - const start_map = meta.cell_map.items[meta_i]; - const end_map = meta.cell_map.items[end_i]; - result.top_x = start_map.x; - result.bot_x = end_map.x; - self.chunk_buf.appendAssumeCapacity(.{ - .node = meta.node, - .start = @intCast(start_map.y), - .end = @intCast(end_map.y + 1), - }); - - break :tl .{ - .br = null, - .prune = .{ - .meta = meta_it.idx - 1, - .data = prior_meta_consumed, - }, - }; - } else { - // We found the meta that contains the start of the match - // only. Consume this entire node from our start offset. - const map = meta.cell_map.items[meta_i]; - result.top_x = map.x; - self.chunk_buf.appendAssumeCapacity(.{ - .node = meta.node, - .start = @intCast(map.y), - .end = meta.node.data.size.rows, - }); - - break :tl .{ - .br = .{ - .it = meta_it, - .consumed = meta_consumed, - }, - .prune = .{ - .meta = meta_it.idx - 1, - .data = prior_meta_consumed, - }, - }; - } - } else { - // Precondition that the start index is within the data buffer. - unreachable; - } - }; - - // Search for our end. - if (tl.br) |br| { - var meta_it = br.it; - var meta_consumed: usize = br.consumed; - while (meta_it.next()) |meta| { - // meta_i is the index we expect to find the match in the - // cell map within this meta if it contains it. - const meta_i = end - meta_consumed; - if (meta_i >= meta.cell_map.items.len) { - // This meta doesn't contain the match. We still add it - // to our results because we want the full flattened list. - self.chunk_buf.appendAssumeCapacity(.{ - .node = meta.node, - .start = 0, - .end = meta.node.data.size.rows, - }); - - meta_consumed += meta.cell_map.items.len; - continue; - } - - // We found it - const map = meta.cell_map.items[meta_i]; - result.bot_x = map.x; - self.chunk_buf.appendAssumeCapacity(.{ - .node = meta.node, - .start = 0, - .end = @intCast(map.y + 1), - }); - break; - } else { - // Precondition that the end index is within the data buffer. - unreachable; - } - } - - // Our offset into the current meta block is the start index - // minus the amount of data fully consumed. We then add one - // to move one past the match so we don't repeat it. - self.data_offset = start - tl.prune.data + 1; - - // If we went beyond our initial meta node we can prune. - if (tl.prune.meta > 0) { - // Deinit all our memory in the meta blocks prior to our - // match. - var meta_it = self.meta.iterator(.forward); - var meta_consumed: usize = 0; - for (0..tl.prune.meta) |_| { - const meta: *Meta = meta_it.next().?; - meta_consumed += meta.cell_map.items.len; - meta.deinit(self.alloc); - } - if (comptime std.debug.runtime_safety) { - assert(meta_it.idx == tl.prune.meta); - assert(meta_it.next().?.node == self.chunk_buf.items(.node)[0]); - } - self.meta.deleteOldest(tl.prune.meta); - - // Delete all the data up to our current index. - assert(tl.prune.data > 0); - self.data.deleteOldest(tl.prune.data); - } - - switch (self.direction) { - .forward => {}, - .reverse => { - if (self.chunk_buf.len > 1) { - // Reverse all our chunks. This should be pretty obvious why. - const slice = self.chunk_buf.slice(); - const nodes = slice.items(.node); - const starts = slice.items(.start); - const ends = slice.items(.end); - std.mem.reverse(*PageList.List.Node, nodes); - std.mem.reverse(size.CellCountInt, starts); - std.mem.reverse(size.CellCountInt, ends); - - // Now normally with forward traversal with multiple pages, - // the suffix of the first page and the prefix of the last - // page are used. - // - // For a reverse traversal, this is inverted (since the - // pages are in reverse order we get the suffix of the last - // page and the prefix of the first page). So we need to - // invert this. - // - // We DON'T need to do this for any middle pages because - // they always use the full page. - // - // We DON'T need to do this for chunks.len == 1 because - // the pages themselves aren't reversed and we don't have - // any prefix/suffix problems. - // - // This is a fixup that makes our start/end match the - // same logic as the loops above if they were in forward - // order. - assert(nodes.len >= 2); - starts[0] = ends[0] - 1; - ends[0] = nodes[0].data.size.rows; - ends[nodes.len - 1] = starts[nodes.len - 1] + 1; - starts[nodes.len - 1] = 0; - } - - // X values also need to be reversed since the top/bottom - // are swapped for the nodes. - const top_x = result.top_x; - result.top_x = result.bot_x; - result.bot_x = top_x; - }, - } - - // Copy over our MultiArrayList so it points to the proper memory. - result.chunks = self.chunk_buf; - return result; - } - - /// 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!usize { - // Initialize our metadata for the node. - var meta: Meta = .{ - .node = node, - .cell_map = .empty, - }; - errdefer meta.deinit(self.alloc); - - // 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.Io.Writer.Allocating = .init(self.alloc); - defer encoded.deinit(); - - // Encode the page into the buffer. - const formatter: PageFormatter = formatter: { - var formatter: PageFormatter = .init(&meta.node.data, .plain); - formatter.point_map = .{ - .alloc = self.alloc, - .map = &meta.cell_map, - }; - break :formatter formatter; - }; - formatter.format(&encoded.writer) 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.written().len); - - // If the node we're adding isn't soft-wrapped, we add the - // trailing newline. - const row = node.data.getRow(node.data.size.rows - 1); - if (!row.wrap) { - encoded.writer.writeByte('\n') catch return error.OutOfMemory; - try meta.cell_map.append( - self.alloc, - meta.cell_map.getLastOrNull() orelse .{ - .x = 0, - .y = 0, - }, - ); - } - - // Get our written data. If we're doing a reverse search then we - // need to reverse all our encodings. - const written = encoded.written(); - switch (self.direction) { - .forward => {}, - .reverse => { - std.mem.reverse(u8, written); - std.mem.reverse(point.Coordinate, meta.cell_map.items); - }, - } - - // Ensure our buffers are big enough to store what we need. - try self.data.ensureUnusedCapacity(self.alloc, written.len); - try self.meta.ensureUnusedCapacity(self.alloc, 1); - try self.chunk_buf.ensureTotalCapacity(self.alloc, self.meta.capacity()); - - // Append our new node to the circular buffer. - self.data.appendSliceAssumeCapacity(written); - self.meta.appendAssumeCapacity(meta); - - self.assertIntegrity(); - return written.len; - } - - /// Only for tests! - fn testChangeNeedle(self: *SlidingWindow, new: []const u8) void { - assert(new.len == self.needle.len); - self.alloc.free(self.needle); - self.needle = self.alloc.dupe(u8, new) catch unreachable; - } - - fn assertIntegrity(self: *const SlidingWindow) void { - if (comptime !std.debug.runtime_safety) return; - - // We don't run integrity checks on Valgrind because its soooooo slow, - // Valgrind is our integrity checker, and we run these during unit - // tests (non-Valgrind) anyways so we're verifying anyways. - if (std.valgrind.runningOnValgrind() > 0) return; - - // Integrity check: verify our data matches our metadata exactly. - 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()); - - // Integrity check: verify our data offset is within bounds. - assert(self.data_offset < self.data.len()); - } -}; - -test "SlidingWindow empty on init" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .forward, "boo!"); - defer w.deinit(); - 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: SlidingWindow = try .init(alloc, .forward, "boo!"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); - defer s.deinit(); - try s.testWriteString("hello. boo! hello. 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(node); - - // We should be able to find two matches. - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 7, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start)); - try testing.expectEqual(point.Point{ .active = .{ - .x = 10, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end)); - } - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 19, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start)); - try testing.expectEqual(point.Point{ .active = .{ - .x = 22, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end)); - } - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); -} - -test "SlidingWindow single append no match" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .forward, "nope!"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); - defer s.deinit(); - try s.testWriteString("hello. boo! hello. 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(node); - - // No matches - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); - - // Should still keep the page - try testing.expectEqual(1, w.meta.len()); -} - -test "SlidingWindow two pages" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .forward, "boo!"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); - defer s.deinit(); - - // Fill up the first page. The final bytes in the first page - // are "boo!" - const first_page_rows = s.pages.pages.first.?.data.capacity.rows; - for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); - for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); - try s.testWriteString("boo!"); - try testing.expect(s.pages.pages.first == s.pages.pages.last); - try s.testWriteString("\n"); - try testing.expect(s.pages.pages.first != s.pages.pages.last); - try s.testWriteString("hello. boo!"); - - // Add both pages - const node: *PageList.List.Node = s.pages.pages.first.?; - _ = try w.append(node); - _ = try w.append(node.next.?); - - // Search should find two matches - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 76, - .y = 22, - } }, s.pages.pointFromPin(.active, sel.start).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 79, - .y = 22, - } }, s.pages.pointFromPin(.active, sel.end).?); - } - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 7, - .y = 23, - } }, s.pages.pointFromPin(.active, sel.start).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 10, - .y = 23, - } }, s.pages.pointFromPin(.active, sel.end).?); - } - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); -} - -test "SlidingWindow two pages match across boundary" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .forward, "hello, world"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); - defer s.deinit(); - - // Fill up the first page. The final bytes in the first page - // are "boo!" - const first_page_rows = s.pages.pages.first.?.data.capacity.rows; - for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); - for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); - try s.testWriteString("hell"); - try testing.expect(s.pages.pages.first == s.pages.pages.last); - try s.testWriteString("o, world!"); - try testing.expect(s.pages.pages.first != s.pages.pages.last); - - // Add both pages - const node: *PageList.List.Node = s.pages.pages.first.?; - _ = try w.append(node); - _ = try w.append(node.next.?); - - // Search should find a match - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 76, - .y = 22, - } }, s.pages.pointFromPin(.active, sel.start).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 7, - .y = 23, - } }, s.pages.pointFromPin(.active, sel.end).?); - } - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); - - // We shouldn't prune because we don't have enough space - try testing.expectEqual(2, w.meta.len()); -} - -test "SlidingWindow two pages no match across boundary with newline" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .forward, "hello, world"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); - defer s.deinit(); - - // Fill up the first page. The final bytes in the first page - // are "boo!" - const first_page_rows = s.pages.pages.first.?.data.capacity.rows; - for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); - for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); - try s.testWriteString("hell"); - try testing.expect(s.pages.pages.first == s.pages.pages.last); - try s.testWriteString("\no, world!"); - try testing.expect(s.pages.pages.first != s.pages.pages.last); - - // Add both pages - const node: *PageList.List.Node = s.pages.pages.first.?; - _ = try w.append(node); - _ = try w.append(node.next.?); - - // Search should NOT find a match - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); - - // We shouldn't prune because we don't have enough space - try testing.expectEqual(2, w.meta.len()); -} - -test "SlidingWindow two pages no match across boundary with newline reverse" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .reverse, "hello, world"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); - defer s.deinit(); - - // Fill up the first page. The final bytes in the first page - // are "boo!" - const first_page_rows = s.pages.pages.first.?.data.capacity.rows; - for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); - for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); - try s.testWriteString("hell"); - try testing.expect(s.pages.pages.first == s.pages.pages.last); - try s.testWriteString("\no, world!"); - try testing.expect(s.pages.pages.first != s.pages.pages.last); - - // Add both pages in reverse order - const node: *PageList.List.Node = s.pages.pages.first.?; - _ = try w.append(node.next.?); - _ = try w.append(node); - - // Search should NOT find a match - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); -} - -test "SlidingWindow two pages no match prunes first page" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .forward, "nope!"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); - defer s.deinit(); - - // Fill up the first page. The final bytes in the first page - // are "boo!" - const first_page_rows = s.pages.pages.first.?.data.capacity.rows; - for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); - for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); - try s.testWriteString("boo!"); - try testing.expect(s.pages.pages.first == s.pages.pages.last); - try s.testWriteString("\n"); - try testing.expect(s.pages.pages.first != s.pages.pages.last); - try s.testWriteString("hello. boo!"); - - // Add both pages - const node: *PageList.List.Node = s.pages.pages.first.?; - _ = try w.append(node); - _ = try w.append(node.next.?); - - // Search should find nothing - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); - - // We should've pruned our page because the second page - // has enough text to contain our needle. - try testing.expectEqual(1, w.meta.len()); -} - -test "SlidingWindow two pages no match keeps both pages" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); - defer s.deinit(); - - // Fill up the first page. The final bytes in the first page - // are "boo!" - const first_page_rows = s.pages.pages.first.?.data.capacity.rows; - for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); - for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); - try s.testWriteString("boo!"); - try testing.expect(s.pages.pages.first == s.pages.pages.last); - try s.testWriteString("\n"); - try testing.expect(s.pages.pages.first != s.pages.pages.last); - try s.testWriteString("hello. boo!"); - - // Imaginary needle for search. Doesn't match! - var needle_list: std.ArrayList(u8) = .empty; - defer needle_list.deinit(alloc); - try needle_list.appendNTimes(alloc, 'x', first_page_rows * s.pages.cols); - const needle: []const u8 = needle_list.items; - - var w: SlidingWindow = try .init(alloc, .forward, needle); - defer w.deinit(); - - // Add both pages - const node: *PageList.List.Node = s.pages.pages.first.?; - _ = try w.append(node); - _ = try w.append(node.next.?); - - // Search should find nothing - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); - - // No pruning because both pages are needed to fit needle. - try testing.expectEqual(2, w.meta.len()); -} - -test "SlidingWindow single append across circular buffer boundary" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .forward, "abc"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); - defer s.deinit(); - try s.testWriteString("XXXXXXXXXXXXXXXXXXXboo!XXXXX"); - - // We are trying to break a circular buffer boundary so the way we - // do this is to duplicate the data then do a failing search. This - // will cause the first page to be pruned. The next time we append we'll - // put it in the middle of the circ buffer. We assert this so that if - // 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); - { - // No wrap around yet - const slices = w.data.getPtrSlice(0, w.data.len()); - try testing.expect(slices[0].len > 0); - try testing.expect(slices[1].len == 0); - } - - // Search non-match, prunes page - try testing.expect(w.next() == null); - try testing.expectEqual(1, w.meta.len()); - - // Change the needle, just needs to be the same length (not a real API) - w.testChangeNeedle("boo"); - - // Add new page, now wraps - _ = try w.append(node); - { - const slices = w.data.getPtrSlice(0, w.data.len()); - try testing.expect(slices[0].len > 0); - try testing.expect(slices[1].len > 0); - } - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 19, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 21, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end).?); - } - try testing.expect(w.next() == null); -} - -test "SlidingWindow single append match on boundary" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .forward, "abcd"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); - defer s.deinit(); - try s.testWriteString("o!XXXXXXXXXXXXXXXXXXXbo"); - - // We need to surgically modify the last row to be soft-wrapped - try testing.expect(s.pages.pages.first == s.pages.pages.last); - const node: *PageList.List.Node = s.pages.pages.first.?; - node.data.getRow(node.data.size.rows - 1).wrap = true; - - // We are trying to break a circular buffer boundary so the way we - // do this is to duplicate the data then do a failing search. This - // will cause the first page to be pruned. The next time we append we'll - // put it in the middle of the circ buffer. We assert this so that if - // our implementation changes our test will fail. - _ = try w.append(node); - _ = try w.append(node); - { - // No wrap around yet - const slices = w.data.getPtrSlice(0, w.data.len()); - try testing.expect(slices[0].len > 0); - try testing.expect(slices[1].len == 0); - } - - // Search non-match, prunes page - try testing.expect(w.next() == null); - try testing.expectEqual(1, w.meta.len()); - - // Change the needle, just needs to be the same length (not a real API) - w.testChangeNeedle("boo!"); - - // Add new page, now wraps - _ = try w.append(node); - { - const slices = w.data.getPtrSlice(0, w.data.len()); - try testing.expect(slices[0].len > 0); - try testing.expect(slices[1].len > 0); - } - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 21, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 1, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end).?); - } - try testing.expect(w.next() == null); -} - -test "SlidingWindow single append reversed" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .reverse, "boo!"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); - defer s.deinit(); - try s.testWriteString("hello. boo! hello. 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(node); - - // We should be able to find two matches. - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 19, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 22, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end).?); - } - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 7, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 10, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end).?); - } - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); -} - -test "SlidingWindow single append no match reversed" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .reverse, "nope!"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); - defer s.deinit(); - try s.testWriteString("hello. boo! hello. 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(node); - - // No matches - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); - - // Should still keep the page - try testing.expectEqual(1, w.meta.len()); -} - -test "SlidingWindow two pages reversed" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .reverse, "boo!"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); - defer s.deinit(); - - // Fill up the first page. The final bytes in the first page - // are "boo!" - const first_page_rows = s.pages.pages.first.?.data.capacity.rows; - for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); - for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); - try s.testWriteString("boo!"); - try testing.expect(s.pages.pages.first == s.pages.pages.last); - try s.testWriteString("\n"); - try testing.expect(s.pages.pages.first != s.pages.pages.last); - try s.testWriteString("hello. boo!"); - - // Add both pages in reverse order - const node: *PageList.List.Node = s.pages.pages.first.?; - _ = try w.append(node.next.?); - _ = try w.append(node); - - // Search should find two matches (in reverse order) - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 7, - .y = 23, - } }, s.pages.pointFromPin(.active, sel.start).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 10, - .y = 23, - } }, s.pages.pointFromPin(.active, sel.end).?); - } - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 76, - .y = 22, - } }, s.pages.pointFromPin(.active, sel.start).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 79, - .y = 22, - } }, s.pages.pointFromPin(.active, sel.end).?); - } - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); -} - -test "SlidingWindow two pages match across boundary reversed" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .reverse, "hello, world"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); - defer s.deinit(); - - // Fill up the first page. The final bytes in the first page - // are "hell" - const first_page_rows = s.pages.pages.first.?.data.capacity.rows; - for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); - for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); - try s.testWriteString("hell"); - try testing.expect(s.pages.pages.first == s.pages.pages.last); - try s.testWriteString("o, world!"); - try testing.expect(s.pages.pages.first != s.pages.pages.last); - - // Add both pages in reverse order - const node: *PageList.List.Node = s.pages.pages.first.?; - _ = try w.append(node.next.?); - _ = try w.append(node); - - // Search should find a match - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 76, - .y = 22, - } }, s.pages.pointFromPin(.active, sel.start).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 7, - .y = 23, - } }, s.pages.pointFromPin(.active, sel.end).?); - } - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); - - // In reverse mode, the last appended meta (first original page) is large - // enough to contain needle.len - 1 bytes, so pruning occurs - try testing.expectEqual(1, w.meta.len()); -} - -test "SlidingWindow two pages no match prunes first page reversed" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .reverse, "nope!"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); - defer s.deinit(); - - // Fill up the first page. The final bytes in the first page - // are "boo!" - const first_page_rows = s.pages.pages.first.?.data.capacity.rows; - for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); - for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); - try s.testWriteString("boo!"); - try testing.expect(s.pages.pages.first == s.pages.pages.last); - try s.testWriteString("\n"); - try testing.expect(s.pages.pages.first != s.pages.pages.last); - try s.testWriteString("hello. boo!"); - - // Add both pages in reverse order - const node: *PageList.List.Node = s.pages.pages.first.?; - _ = try w.append(node.next.?); - _ = try w.append(node); - - // Search should find nothing - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); - - // We should've pruned our page because the second page - // has enough text to contain our needle. - try testing.expectEqual(1, w.meta.len()); -} - -test "SlidingWindow two pages no match keeps both pages reversed" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); - defer s.deinit(); - - // Fill up the first page. The final bytes in the first page - // are "boo!" - const first_page_rows = s.pages.pages.first.?.data.capacity.rows; - for (0..first_page_rows - 1) |_| try s.testWriteString("\n"); - for (0..s.pages.cols - 4) |_| try s.testWriteString("x"); - try s.testWriteString("boo!"); - try testing.expect(s.pages.pages.first == s.pages.pages.last); - try s.testWriteString("\n"); - try testing.expect(s.pages.pages.first != s.pages.pages.last); - try s.testWriteString("hello. boo!"); - - // Imaginary needle for search. Doesn't match! - var needle_list: std.ArrayList(u8) = .empty; - defer needle_list.deinit(alloc); - try needle_list.appendNTimes(alloc, 'x', first_page_rows * s.pages.cols); - const needle: []const u8 = needle_list.items; - - var w: SlidingWindow = try .init(alloc, .reverse, needle); - defer w.deinit(); - - // Add both pages in reverse order - const node: *PageList.List.Node = s.pages.pages.first.?; - _ = try w.append(node.next.?); - _ = try w.append(node); - - // Search should find nothing - try testing.expect(w.next() == null); - try testing.expect(w.next() == null); - - // No pruning because both pages are needed to fit needle. - try testing.expectEqual(2, w.meta.len()); -} - -test "SlidingWindow single append across circular buffer boundary reversed" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .reverse, "abc"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); - defer s.deinit(); - try s.testWriteString("XXXXXXXXXXXXXXXXXXXboo!XXXXX"); - - // We are trying to break a circular buffer boundary so the way we - // do this is to duplicate the data then do a failing search. This - // will cause the first page to be pruned. The next time we append we'll - // put it in the middle of the circ buffer. We assert this so that if - // 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); - { - // No wrap around yet - const slices = w.data.getPtrSlice(0, w.data.len()); - try testing.expect(slices[0].len > 0); - try testing.expect(slices[1].len == 0); - } - - // Search non-match, prunes page - try testing.expect(w.next() == null); - try testing.expectEqual(1, w.meta.len()); - - // Change the needle, just needs to be the same length (not a real API) - // testChangeNeedle doesn't reverse, so pass reversed needle for reverse mode - w.testChangeNeedle("oob"); - - // Add new page, now wraps - _ = try w.append(node); - { - const slices = w.data.getPtrSlice(0, w.data.len()); - try testing.expect(slices[0].len > 0); - try testing.expect(slices[1].len > 0); - } - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 19, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 21, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end).?); - } - try testing.expect(w.next() == null); -} - -test "SlidingWindow single append match on boundary reversed" { - const testing = std.testing; - const alloc = testing.allocator; - - var w: SlidingWindow = try .init(alloc, .reverse, "abcd"); - defer w.deinit(); - - var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); - defer s.deinit(); - try s.testWriteString("o!XXXXXXXXXXXXXXXXXXXbo"); - - // We need to surgically modify the last row to be soft-wrapped - try testing.expect(s.pages.pages.first == s.pages.pages.last); - const node: *PageList.List.Node = s.pages.pages.first.?; - node.data.getRow(node.data.size.rows - 1).wrap = true; - - // We are trying to break a circular buffer boundary so the way we - // do this is to duplicate the data then do a failing search. This - // will cause the first page to be pruned. The next time we append we'll - // put it in the middle of the circ buffer. We assert this so that if - // our implementation changes our test will fail. - _ = try w.append(node); - _ = try w.append(node); - { - // No wrap around yet - const slices = w.data.getPtrSlice(0, w.data.len()); - try testing.expect(slices[0].len > 0); - try testing.expect(slices[1].len == 0); - } - - // Search non-match, prunes page - try testing.expect(w.next() == null); - try testing.expectEqual(1, w.meta.len()); - - // Change the needle, just needs to be the same length (not a real API) - // testChangeNeedle doesn't reverse, so pass reversed needle for reverse mode - w.testChangeNeedle("!oob"); - - // Add new page, now wraps - _ = try w.append(node); - { - const slices = w.data.getPtrSlice(0, w.data.len()); - try testing.expect(slices[0].len > 0); - try testing.expect(slices[1].len > 0); - } - { - const h = w.next().?; - const sel = h.untracked(); - try testing.expectEqual(point.Point{ .active = .{ - .x = 21, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.start).?); - try testing.expectEqual(point.Point{ .active = .{ - .x = 1, - .y = 0, - } }, s.pages.pointFromPin(.active, sel.end).?); - } - try testing.expect(w.next() == null); -} diff --git a/src/terminal/search/viewport.zig b/src/terminal/search/viewport.zig index 70fc3088f..9d9cb754b 100644 --- a/src/terminal/search/viewport.zig +++ b/src/terminal/search/viewport.zig @@ -4,6 +4,7 @@ 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 Selection = @import("../Selection.zig"); const SlidingWindow = @import("sliding_window.zig").SlidingWindow; @@ -150,7 +151,7 @@ pub const ViewportSearch = struct { /// Find the next match for the needle in the active area. This returns /// null when there are no more matches. - pub fn next(self: *ViewportSearch) ?Selection { + pub fn next(self: *ViewportSearch) ?FlattenedHighlight { return self.window.next(); } @@ -207,26 +208,28 @@ test "simple search" { try testing.expect(try search.update(&t.screens.active.pages)); { - const sel = search.next().?; + 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()).?); + } }, 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()).?); + } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } { - const sel = search.next().?; + 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()).?); + } }, 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()).?); + } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } try testing.expect(search.next() == null); } @@ -250,15 +253,16 @@ test "clear screen and search" { try testing.expect(try search.update(&t.screens.active.pages)); { - const sel = search.next().?; + 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()).?); + } }, 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()).?); + } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } try testing.expect(search.next() == null); } @@ -289,15 +293,16 @@ test "history search, no active area" { try testing.expect(try search.update(&t.screens.active.pages)); { - const sel = search.next().?; + const h = search.next().?; + const sel = h.untracked(); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, .y = 0, - } }, t.screens.active.pages.pointFromPin(.screen, sel.start()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.start).?); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, .y = 0, - } }, t.screens.active.pages.pointFromPin(.screen, sel.end()).?); + } }, t.screens.active.pages.pointFromPin(.screen, sel.end).?); } try testing.expect(search.next() == null); From e49f4a6dbcc410331f5d0783e2981cfc7c4fab94 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 15 Nov 2025 20:02:35 -0800 Subject: [PATCH 11/22] `search` binding action starts a search thread on surface --- src/Surface.zig | 75 ++++++++++++++++++++++++++++++++++ src/input/Binding.zig | 5 +++ src/input/command.zig | 1 + src/terminal/search/Thread.zig | 3 ++ 4 files changed, 84 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 63af42680..6189aae8e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -155,6 +155,9 @@ selection_scroll_active: bool = false, /// the wall clock time that has elapsed between timestamps. command_timer: ?std.time.Instant = null, +/// Search state +search: ?Search = null, + /// The effect of an input event. This can be used by callers to take /// the appropriate action after an input event. For example, key /// input can be forwarded to the OS for further processing if it @@ -174,6 +177,26 @@ pub const InputEffect = enum { closed, }; +/// The search state for the surface. +const Search = struct { + state: terminal.search.Thread, + thread: std.Thread, + + pub fn deinit(self: *Search) void { + // Notify the thread to stop + self.state.stop.notify() catch |err| log.err( + "error notifying search thread to stop, may stall err={}", + .{err}, + ); + + // Wait for the OS thread to quit + self.thread.join(); + + // Now it is safe to deinit the state + self.state.deinit(); + } +}; + /// Mouse state for the surface. const Mouse = struct { /// The last tracked mouse button state by button. @@ -728,6 +751,9 @@ pub fn init( } pub fn deinit(self: *Surface) void { + // Stop search thread + if (self.search) |*s| s.deinit(); + // Stop rendering thread { self.renderer_thread.stop.notify() catch |err| @@ -1301,6 +1327,12 @@ fn reportColorScheme(self: *Surface, force: bool) void { self.io.queueMessage(.{ .write_stable = output }, .unlocked); } +fn searchCallback(event: terminal.search.Thread.Event, ud: ?*anyopaque) void { + const self: *Surface = @ptrCast(@alignCast(ud.?)); + _ = self; + _ = event; +} + /// Call this when modifiers change. This is safe to call even if modifiers /// match the previous state. /// @@ -4770,6 +4802,49 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool self.renderer_state.terminal.fullReset(); }, + .search => |text| search: { + const s: *Search = if (self.search) |*s| s else init: { + // If we're stopping the search and we had no prior search, + // then there is nothing to do. + if (text.len == 0) break :search; + + // We need to assign directly to self.search because we need + // a stable pointer back to the thread state. + self.search = .{ + .state = try .init(self.alloc, .{ + .mutex = self.renderer_state.mutex, + .terminal = self.renderer_state.terminal, + .event_cb = &searchCallback, + .event_userdata = self, + }), + .thread = undefined, + }; + const s: *Search = &self.search.?; + errdefer s.state.deinit(); + + s.thread = try .spawn( + .{}, + terminal.search.Thread.threadMain, + .{&s.state}, + ); + s.thread.setName("search") catch {}; + + break :init s; + }; + + // Zero-length text means stop searching. + if (text.len == 0) { + s.deinit(); + self.search = null; + break :search; + } + + _ = s.state.mailbox.push( + .{ .change_needle = text }, + .forever, + ); + }, + .copy_to_clipboard => |format| { // We can read from the renderer state without holding // the lock because only we will write to this field. diff --git a/src/input/Binding.zig b/src/input/Binding.zig index c9f3a7343..1b681e725 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -332,6 +332,10 @@ pub const Action = union(enum) { /// to 14.5 points. set_font_size: f32, + /// Start a search for the given text. If the text is empty, then + /// the search is canceled. If a previous search is active, it is replaced. + search: []const u8, + /// Clear the screen and all scrollback. clear_screen, @@ -1152,6 +1156,7 @@ pub const Action = union(enum) { .esc, .text, .cursor_key, + .search, .reset, .copy_to_clipboard, .copy_url_to_clipboard, diff --git a/src/input/command.zig b/src/input/command.zig index b6f75080d..11f65cea3 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -604,6 +604,7 @@ fn actionCommands(action: Action.Key) []const Command { .csi, .esc, .cursor_key, + .search, .set_font_size, .scroll_to_row, .scroll_page_fractional, diff --git a/src/terminal/search/Thread.zig b/src/terminal/search/Thread.zig index fdd5f81bc..a35d658b3 100644 --- a/src/terminal/search/Thread.zig +++ b/src/terminal/search/Thread.zig @@ -591,6 +591,7 @@ const Search = struct { // Check our total match data const total = screen_search.matchesLen(); if (total != self.last_total) { + log.debug("notifying total matches={}", .{total}); self.last_total = total; cb(.{ .total_matches = total }, ud); } @@ -626,11 +627,13 @@ const Search = struct { }; } + log.debug("notifying viewport matches len={}", .{results.items.len}); cb(.{ .viewport_matches = results.items }, ud); } // Send our complete notification if we just completed. if (!self.last_complete and self.isComplete()) { + log.debug("notifying search complete", .{}); self.last_complete = true; cb(.complete, ud); } From 72921741e8805415130fbe8cf664e2c16a20b5ec Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 15 Nov 2025 20:28:45 -0800 Subject: [PATCH 12/22] terminal: search.viewport supports dirty tracking for more efficient --- src/terminal/search/viewport.zig | 90 ++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/src/terminal/search/viewport.zig b/src/terminal/search/viewport.zig index 9d9cb754b..0f479b811 100644 --- a/src/terminal/search/viewport.zig +++ b/src/terminal/search/viewport.zig @@ -27,6 +27,12 @@ pub const ViewportSearch = struct { window: SlidingWindow, fingerprint: ?Fingerprint, + /// If this is null, then active dirty tracking is disabled and if the + /// viewport overlaps the active area we always re-search. If this is + /// non-null, then we only re-search if the active area is dirty. Dirty + /// marking is up to the caller. + active_dirty: ?bool, + pub fn init( alloc: Allocator, needle_unowned: []const u8, @@ -36,7 +42,11 @@ pub const ViewportSearch = struct { // a small amount of work to reverse things. var window: SlidingWindow = try .init(alloc, .forward, needle_unowned); errdefer window.deinit(); - return .{ .window = window, .fingerprint = null }; + return .{ + .window = window, + .fingerprint = null, + .active_dirty = null, + }; } pub fn deinit(self: *ViewportSearch) void { @@ -75,17 +85,29 @@ pub const ViewportSearch = struct { var fingerprint: Fingerprint = try .init(self.window.alloc, list); if (self.fingerprint) |*old| { if (old.eql(fingerprint)) match: { - // If our fingerprint contains the active area, then we always - // re-search since the active area is mutable. - const active_tl = list.getTopLeft(.active); - const active_br = list.getBottomRight(.active).?; + // Determine if we need to check if we overlap the active + // area. If we have dirty tracking on we also set it to + // false here. + const check_active: bool = active: { + const dirty = self.active_dirty orelse break :active true; + if (!dirty) break :active false; + self.active_dirty = false; + break :active true; + }; - // If our viewport contains the start or end of the active area, - // we are in the active area. We purposely do this first - // because our viewport is always larger than the active area. - for (old.nodes) |node| { - if (node == active_tl.node) break :match; - if (node == active_br.node) break :match; + if (check_active) { + // If our fingerprint contains the active area, then we always + // re-search since the active area is mutable. + const active_tl = list.getTopLeft(.active); + const active_br = list.getBottomRight(.active).?; + + // If our viewport contains the start or end of the active area, + // we are in the active area. We purposely do this first + // because our viewport is always larger than the active area. + for (old.nodes) |node| { + if (node == active_tl.node) break :match; + if (node == active_br.node) break :match; + } } // No change @@ -267,6 +289,52 @@ test "clear screen and search" { try testing.expect(search.next() == null); } +test "clear screen and search dirty tracking" { + 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: ViewportSearch = try .init(alloc, "Fizz"); + defer search.deinit(); + + // Turn on dirty tracking + search.active_dirty = false; + + // Should update since we've never searched before + try testing.expect(try search.update(&t.screens.active.pages)); + + // Should not update since nothing changed + try testing.expect(!try search.update(&t.screens.active.pages)); + + try s.nextSlice("\x1b[2J"); // Clear screen + try s.nextSlice("\x1b[H"); // Move cursor home + try s.nextSlice("Buzz\r\nFizz\r\nBuzz"); + + // Should still not update since active area isn't dirty + try testing.expect(!try search.update(&t.screens.active.pages)); + + // Mark + search.active_dirty = true; + try testing.expect(try search.update(&t.screens.active.pages)); + + { + const sel = search.next().?; + 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); +} + test "history search, no active area" { const alloc = testing.allocator; var t: Terminal = try .init(alloc, .{ .cols = 10, .rows = 2 }); From 061d157b503115eda4df9b1e3886de6fb8471f20 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 15 Nov 2025 20:47:42 -0800 Subject: [PATCH 13/22] terminal: search should use active area dirty tracking --- src/terminal/Terminal.zig | 10 ++++++++++ src/terminal/search/Thread.zig | 13 +++++++++++++ src/terminal/search/viewport.zig | 4 ++++ 3 files changed, 27 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index e75fd731a..68919107b 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -112,6 +112,16 @@ flags: packed struct { /// True if the terminal should perform selection scrolling. selection_scroll: bool = false, + /// Dirty flag used only by the search thread. The renderer is expected + /// to set this to true if the viewport was dirty as it was rendering. + /// This is used by the search thread to more efficiently re-search the + /// viewport and active area. + /// + /// Since the renderer is going to inspect the viewport/active area ANYWAYS, + /// this lets our search thread do less work and hold the lock less time, + /// resulting in more throughput for everything. + search_viewport_dirty: bool = false, + /// Dirty flags for the renderer. dirty: Dirty = .{}, } = .{}, diff --git a/src/terminal/search/Thread.zig b/src/terminal/search/Thread.zig index a35d658b3..7ca9df0b7 100644 --- a/src/terminal/search/Thread.zig +++ b/src/terminal/search/Thread.zig @@ -419,6 +419,10 @@ const Search = struct { var vp: ViewportSearch = try .init(alloc, needle); errdefer vp.deinit(); + // We use dirty tracking for active area changes. Start with it + // dirty so the first change is re-searched. + vp.active_dirty = true; + return .{ .viewport = vp, .screens = .init(.{}), @@ -553,6 +557,15 @@ const Search = struct { } } + // See the `search_viewport_dirty` flag on the terminal to know + // what exactly this is for. But, if this is set, we know the renderer + // found the viewport/active area dirty, so we should mark it as + // dirty in our viewport searcher so it forces a re-search. + if (t.flags.search_viewport_dirty) { + self.viewport.active_dirty = true; + t.flags.search_viewport_dirty = false; + } + // Check our viewport for changes. if (self.viewport.update(&t.screens.active.pages)) |updated| { if (updated) self.stale_viewport_matches = true; diff --git a/src/terminal/search/viewport.zig b/src/terminal/search/viewport.zig index 0f479b811..6a266f47a 100644 --- a/src/terminal/search/viewport.zig +++ b/src/terminal/search/viewport.zig @@ -125,6 +125,10 @@ pub const ViewportSearch = struct { self.fingerprint = null; } + // If our active area was set as dirty, we always unset it here + // because we're re-searching now. + if (self.active_dirty) |*v| v.* = false; + // Clear our previous sliding window self.window.clearAndRetainCapacity(); From 6c8ffb5fc1e741b427fa7acc854570e208af7c67 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 16 Nov 2025 07:05:32 -0800 Subject: [PATCH 14/22] renderer: receive message with viewport match selections Doesn't draw yet --- src/Surface.zig | 39 +++++++++++++++++++++++++++++++++++++-- src/renderer/Thread.zig | 8 ++++++++ src/renderer/generic.zig | 14 +++++++++++++- src/renderer/message.zig | 15 +++++++++++++-- 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 6189aae8e..cfc0b14aa 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1328,9 +1328,44 @@ fn reportColorScheme(self: *Surface, force: bool) void { } fn searchCallback(event: terminal.search.Thread.Event, ud: ?*anyopaque) void { + // IMPORTANT: This function is run on the SEARCH THREAD! It is NOT SAFE + // to access anything other than values that never change on the surface. + // The surface is guaranteed to be valid for the lifetime of the search + // thread. const self: *Surface = @ptrCast(@alignCast(ud.?)); - _ = self; - _ = event; + self.searchCallback_(event) catch |err| { + log.warn("error in search callback err={}", .{err}); + }; +} + +fn searchCallback_( + self: *Surface, + event: terminal.search.Thread.Event, +) !void { + switch (event) { + .viewport_matches => |matches_unowned| { + var arena: ArenaAllocator = .init(self.alloc); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + const matches = try alloc.dupe(terminal.highlight.Flattened, matches_unowned); + for (matches) |*m| m.* = try m.clone(alloc); + + _ = self.renderer_thread.mailbox.push( + .{ .search_viewport_matches = .{ + .arena = arena, + .matches = matches, + } }, + .forever, + ); + try self.renderer_thread.wakeup.notify(); + }, + + // Unhandled, so far. + .total_matches, + .complete, + => {}, + } } /// Call this when modifiers change. This is safe to call even if modifiers diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 004cfd5fa..738dce61c 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -451,6 +451,14 @@ fn drainMailbox(self: *Thread) !void { self.startDrawTimer(); }, + .search_viewport_matches => |v| { + // Note we don't free the new value because we expect our + // allocators to match. + if (self.renderer.search_matches) |*m| m.arena.deinit(); + self.renderer.search_matches = v; + self.renderer.search_matches_dirty = true; + }, + .inspector => |v| self.flags.has_inspector = v, .macos_display_id => |v| { diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 025578c81..1a816e751 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -122,6 +122,16 @@ pub fn Renderer(comptime GraphicsAPI: type) type { scrollbar: terminal.Scrollbar, scrollbar_dirty: bool, + /// The most recent viewport matches so that we can render search + /// matches in the visible frame. This is provided asynchronously + /// from the search thread so we have the dirty flag to also note + /// if we need to rebuild our cells to include search highlights. + /// + /// Note that the selections MAY BE INVALID (point to PageList nodes + /// that do not exist anymore). These must be validated prior to use. + search_matches: ?renderer.Message.SearchMatches, + search_matches_dirty: bool, + /// The current set of cells to render. This is rebuilt on every frame /// but we keep this around so that we don't reallocate. Each set of /// cells goes into a separate shader. @@ -672,6 +682,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .focused = true, .scrollbar = .zero, .scrollbar_dirty = false, + .search_matches = null, + .search_matches_dirty = false, // Render state .cells = .{}, @@ -744,7 +756,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { pub fn deinit(self: *Self) void { self.terminal_state.deinit(self.alloc); - + if (self.search_matches) |*m| m.arena.deinit(); self.swap_chain.deinit(); if (DisplayLink != void) { diff --git a/src/renderer/message.zig b/src/renderer/message.zig index b36a99d5c..8a319166b 100644 --- a/src/renderer/message.zig +++ b/src/renderer/message.zig @@ -1,6 +1,7 @@ const std = @import("std"); const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; const configpkg = @import("../config.zig"); const font = @import("../font/main.zig"); const renderer = @import("../renderer.zig"); @@ -10,7 +11,7 @@ const terminal = @import("../terminal/main.zig"); pub const Message = union(enum) { /// Purposely crash the renderer. This is used for testing and debugging. /// See the "crash" binding action. - crash: void, + crash, /// A change in state in the window focus that this renderer is /// rendering within. This is only sent when a change is detected so @@ -24,7 +25,7 @@ pub const Message = union(enum) { /// Reset the cursor blink by immediately showing the cursor then /// restarting the timer. - reset_cursor_blink: void, + reset_cursor_blink, /// Change the font grid. This can happen for any number of reasons /// including a font size change, family change, etc. @@ -52,12 +53,22 @@ pub const Message = union(enum) { impl: *renderer.Renderer.DerivedConfig, }, + /// Matches for the current viewport from the search thread. These happen + /// async so they may be off for a frame or two from the actually rendered + /// viewport. The renderer must handle this gracefully. + search_viewport_matches: SearchMatches, + /// Activate or deactivate the inspector. inspector: bool, /// The macOS display ID has changed for the window. macos_display_id: u32, + pub const SearchMatches = struct { + arena: ArenaAllocator, + matches: []const terminal.highlight.Flattened, + }; + /// Initialize a change_config message. pub fn initChangeConfig(alloc: Allocator, config: *const configpkg.Config) !Message { const thread_ptr = try alloc.create(renderer.Thread.DerivedConfig); From dd9ed531ad16a1fe9d06e088c1f26dbe922ed321 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 24 Nov 2025 12:26:59 -0800 Subject: [PATCH 15/22] render viewport matches --- src/renderer/generic.zig | 30 ++++++++++++++++++- src/terminal/render.zig | 63 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 1a816e751..691831e8a 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1191,6 +1191,23 @@ pub fn Renderer(comptime GraphicsAPI: type) type { log.warn("error searching for regex links err={}", .{err}); }; + // Clear our highlight state and update. + if (self.search_matches_dirty or self.terminal_state.dirty != .false) { + for (self.terminal_state.row_data.items(.highlights)) |*highlights| { + highlights.clearRetainingCapacity(); + } + + if (self.search_matches) |m| { + self.terminal_state.updateHighlightsFlattened( + self.alloc, + m.matches, + ) catch |err| { + // Not a critical error, we just won't show highlights. + log.warn("error updating search highlights err={}", .{err}); + }; + } + } + // Build our GPU cells try self.rebuildCells( critical.preedit, @@ -2366,6 +2383,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const row_cells = row_data.items(.cells); const row_dirty = row_data.items(.dirty); const row_selection = row_data.items(.selection); + const row_highlights = row_data.items(.highlights); // If our cell contents buffer is shorter than the screen viewport, // we render the rows that fit, starting from the bottom. If instead @@ -2381,7 +2399,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { row_cells[0..row_len], row_dirty[0..row_len], row_selection[0..row_len], - ) |y_usize, row, *cells, *dirty, selection| { + row_highlights[0..row_len], + ) |y_usize, row, *cells, *dirty, selection, highlights| { const y: terminal.size.CellCountInt = @intCast(y_usize); if (!rebuild) { @@ -2526,6 +2545,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // True if this cell is selected const selected: bool = selected: { + // If we're highlighted, then we're selected. In the + // future we want to use a different style for this + // but this to get started. + for (highlights.items) |hl| { + if (x >= hl[0] and x <= hl[1]) { + break :selected true; + } + } + const sel = selection orelse break :selected false; const x_compare = if (wide == .spacer_tail) x -| 1 diff --git a/src/terminal/render.zig b/src/terminal/render.zig index 86b299d72..49fc5af71 100644 --- a/src/terminal/render.zig +++ b/src/terminal/render.zig @@ -5,6 +5,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const fastmem = @import("../fastmem.zig"); const color = @import("color.zig"); const cursor = @import("cursor.zig"); +const highlight = @import("highlight.zig"); const point = @import("point.zig"); const size = @import("size.zig"); const page = @import("page.zig"); @@ -191,6 +192,10 @@ pub const RenderState = struct { /// The x range of the selection within this row. selection: ?[2]size.CellCountInt, + + /// The x ranges of highlights within this row. Highlights are + /// applied after the update by calling `updateHighlights`. + highlights: std.ArrayList([2]size.CellCountInt), }; pub const Cell = struct { @@ -348,6 +353,7 @@ pub const RenderState = struct { .cells = .empty, .dirty = true, .selection = null, + .highlights = .empty, }); } } else { @@ -630,6 +636,63 @@ pub const RenderState = struct { s.dirty = .{}; } + /// Update the highlights in the render state from the given flattened + /// highlights. Because this uses flattened highlights, it does not require + /// reading from the terminal state so it should be done outside of + /// any critical sections. + /// + /// This will not clear any previous highlights, so the caller must + /// manually clear them if desired. + pub fn updateHighlightsFlattened( + self: *RenderState, + alloc: Allocator, + hls: []const highlight.Flattened, + ) Allocator.Error!void { + // Fast path, we have no highlights! + if (hls.len == 0) return; + + // This is, admittedly, horrendous. This is some low hanging fruit + // to optimize. In my defense, screens are usually small, the number + // of highlights is usually small, and this only happens on the + // viewport outside of a locked area. Still, I'd love to see this + // improved someday. + const row_data = self.row_data.slice(); + const row_arenas = row_data.items(.arena); + const row_pins = row_data.items(.pin); + const row_highlights_slice = row_data.items(.highlights); + for ( + row_arenas, + row_pins, + row_highlights_slice, + ) |*row_arena, row_pin, *row_highlights| { + for (hls) |hl| { + const chunks_slice = hl.chunks.slice(); + const nodes = chunks_slice.items(.node); + const starts = chunks_slice.items(.start); + const ends = chunks_slice.items(.end); + for (0.., nodes) |i, node| { + // If this node doesn't match or we're not within + // the row range, skip it. + if (node != row_pin.node or + row_pin.y < starts[i] or + row_pin.y >= ends[i]) continue; + + // We're a match! + var arena = row_arena.promote(alloc); + defer row_arena.* = arena.state; + const arena_alloc = arena.allocator(); + try row_highlights.append( + arena_alloc, + .{ + if (i == 0) hl.top_x else 0, + if (i == nodes.len - 1) hl.bot_x else self.cols - 1, + }, + ); + } + } + } + } + pub const StringMap = std.ArrayListUnmanaged(point.Coordinate); /// Convert the current render state contents to a UTF-8 encoded From d0e3a79a74ac088be0a2db658abd8d634f043cb0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 24 Nov 2025 12:35:57 -0800 Subject: [PATCH 16/22] reset search on needle change or quit --- src/Surface.zig | 14 ++++++++++++++ src/terminal/search/Thread.zig | 25 ++++++++++++++++++++++++- src/terminal/search/viewport.zig | 7 ++++--- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index cfc0b14aa..989495309 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1342,6 +1342,8 @@ fn searchCallback_( self: *Surface, event: terminal.search.Thread.Event, ) !void { + // NOTE: This runs on the search thread. + switch (event) { .viewport_matches => |matches_unowned| { var arena: ArenaAllocator = .init(self.alloc); @@ -1361,6 +1363,18 @@ fn searchCallback_( try self.renderer_thread.wakeup.notify(); }, + // When we quit, tell our renderer to reset any search state. + .quit => { + _ = self.renderer_thread.mailbox.push( + .{ .search_viewport_matches = .{ + .arena = .init(self.alloc), + .matches = &.{}, + } }, + .forever, + ); + try self.renderer_thread.wakeup.notify(); + }, + // Unhandled, so far. .total_matches, .complete, diff --git a/src/terminal/search/Thread.zig b/src/terminal/search/Thread.zig index 7ca9df0b7..2c5607809 100644 --- a/src/terminal/search/Thread.zig +++ b/src/terminal/search/Thread.zig @@ -163,7 +163,14 @@ fn threadMain_(self: *Thread) !void { // Run log.debug("starting search thread", .{}); - defer log.debug("starting search thread shutdown", .{}); + defer { + log.debug("starting search thread shutdown", .{}); + + // Send the quit message + if (self.opts.event_cb) |cb| { + cb(.quit, self.opts.event_userdata); + } + } // Unlike some of our other threads, we interleave search work // with our xev loop so that we can try to make forward search progress @@ -247,6 +254,18 @@ fn changeNeedle(self: *Thread, needle: []const u8) !void { if (self.search) |*s| { s.deinit(); self.search = null; + + // When the search changes then we need to emit that it stopped. + if (self.opts.event_cb) |cb| { + cb( + .{ .total_matches = 0 }, + self.opts.event_userdata, + ); + cb( + .{ .viewport_matches = &.{} }, + self.opts.event_userdata, + ); + } } // No needle means stop the search. @@ -381,6 +400,9 @@ pub const Message = union(enum) { /// Events that can be emitted from the search thread. The caller /// chooses to handle these as they see fit. pub const Event = union(enum) { + /// Search is quitting. The search thread is exiting. + quit, + /// Search is complete for the given needle on all screens. complete, @@ -668,6 +690,7 @@ test { fn callback(event: Event, userdata: ?*anyopaque) void { const ud: *Self = @ptrCast(@alignCast(userdata.?)); switch (event) { + .quit => {}, .complete => ud.reset.set(), .total_matches => |v| ud.total = v, .viewport_matches => |v| { diff --git a/src/terminal/search/viewport.zig b/src/terminal/search/viewport.zig index 6a266f47a..55eedb724 100644 --- a/src/terminal/search/viewport.zig +++ b/src/terminal/search/viewport.zig @@ -326,15 +326,16 @@ test "clear screen and search dirty tracking" { try testing.expect(try search.update(&t.screens.active.pages)); { - const sel = search.next().?; + 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()).?); + } }, 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()).?); + } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } try testing.expect(search.next() == null); } From 06981175afec87c23c6aec569ccb0f2b9770343c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 24 Nov 2025 13:36:10 -0800 Subject: [PATCH 17/22] renderer: reset search dirty state after processing --- src/renderer/generic.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 691831e8a..b1a0151a4 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1193,6 +1193,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Clear our highlight state and update. if (self.search_matches_dirty or self.terminal_state.dirty != .false) { + self.search_matches_dirty = false; + for (self.terminal_state.row_data.items(.highlights)) |*highlights| { highlights.clearRetainingCapacity(); } From a4e40c75671400e11eaaeecb83a0c01bbeb818ed Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 24 Nov 2025 13:59:50 -0800 Subject: [PATCH 18/22] set proper dirty state to redo viewport search --- src/renderer/generic.zig | 6 ++++++ src/terminal/render.zig | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index b1a0151a4..fb82efd8d 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1126,6 +1126,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Update our terminal state try self.terminal_state.update(self.alloc, state.terminal); + // If our terminal state is dirty at all we need to redo + // the viewport search. + if (self.terminal_state.dirty != .false) { + state.terminal.flags.search_viewport_dirty = true; + } + // Get our scrollbar out of the terminal. We synchronize // the scrollbar read with frame data updates because this // naturally limits the number of calls to this method (it diff --git a/src/terminal/render.zig b/src/terminal/render.zig index 49fc5af71..8f4da12eb 100644 --- a/src/terminal/render.zig +++ b/src/terminal/render.zig @@ -656,15 +656,22 @@ pub const RenderState = struct { // of highlights is usually small, and this only happens on the // viewport outside of a locked area. Still, I'd love to see this // improved someday. + + // We need to track whether any row had a match so we can mark + // the dirty state. + var any_dirty: bool = false; + const row_data = self.row_data.slice(); const row_arenas = row_data.items(.arena); + const row_dirties = row_data.items(.dirty); const row_pins = row_data.items(.pin); const row_highlights_slice = row_data.items(.highlights); for ( row_arenas, row_pins, row_highlights_slice, - ) |*row_arena, row_pin, *row_highlights| { + row_dirties, + ) |*row_arena, row_pin, *row_highlights, *dirty| { for (hls) |hl| { const chunks_slice = hl.chunks.slice(); const nodes = chunks_slice.items(.node); @@ -688,9 +695,15 @@ pub const RenderState = struct { if (i == nodes.len - 1) hl.bot_x else self.cols - 1, }, ); + + dirty.* = true; + any_dirty = true; } } } + + // Mark our dirty state. + if (any_dirty and self.dirty == .false) self.dirty = .partial; } pub const StringMap = std.ArrayListUnmanaged(point.Coordinate); From de16e4a92b2bffb4c4cb277742552fe6b5044c75 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 24 Nov 2025 20:02:06 -0800 Subject: [PATCH 19/22] config: add selection-foreground/background --- src/config/Config.zig | 14 +++++++ src/renderer/generic.zig | 88 +++++++++++++++++++++++----------------- 2 files changed, 64 insertions(+), 38 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 6355b6c26..89254b93f 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -978,6 +978,20 @@ palette: Palette = .{}, /// Available since: 1.1.0 @"split-divider-color": ?Color = null, +/// The foreground and background color for search matches. This only applies +/// to non-focused search matches, also known as candidate matches. +/// +/// Valid values: +/// +/// - Hex (`#RRGGBB` or `RRGGBB`) +/// - Named X11 color +/// - "cell-foreground" to match the cell foreground color +/// - "cell-background" to match the cell background color +/// +/// The default value is +@"search-foreground": TerminalColor = .{ .color = .{ .r = 0, .g = 0, .b = 0 } }, +@"search-background": TerminalColor = .{ .color = .{ .r = 0xFF, .g = 0xE0, .b = 0x82 } }, + /// The command to run, usually a shell. If this is not an absolute path, it'll /// be looked up in the `PATH`. If this is not set, a default will be looked up /// from your system. The rules for the default lookup are: diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index fb82efd8d..7701a5418 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -537,6 +537,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { foreground: terminal.color.RGB, selection_background: ?configpkg.Config.TerminalColor, selection_foreground: ?configpkg.Config.TerminalColor, + search_background: configpkg.Config.TerminalColor, + search_foreground: configpkg.Config.TerminalColor, bold_color: ?configpkg.BoldColor, faint_opacity: u8, min_contrast: f32, @@ -608,6 +610,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .selection_background = config.@"selection-background", .selection_foreground = config.@"selection-foreground", + .search_background = config.@"search-background", + .search_foreground = config.@"search-foreground", .custom_shaders = custom_shaders, .bg_image = bg_image, @@ -2552,24 +2556,30 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .{}; // True if this cell is selected - const selected: bool = selected: { + const selected: enum { + false, + selection, + search, + } = selected: { // If we're highlighted, then we're selected. In the // future we want to use a different style for this // but this to get started. for (highlights.items) |hl| { if (x >= hl[0] and x <= hl[1]) { - break :selected true; + break :selected .search; } } - const sel = selection orelse break :selected false; + const sel = selection orelse break :selected .false; const x_compare = if (wide == .spacer_tail) x -| 1 else x; - break :selected x_compare >= sel[0] and - x_compare <= sel[1]; + if (x_compare >= sel[0] and + x_compare <= sel[1]) break :selected .selection; + + break :selected .false; }; // The `_style` suffixed values are the colors based on @@ -2586,25 +2596,26 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }); // The final background color for the cell. - const bg = bg: { - if (selected) { - // If we have an explicit selection background color - // specified int he config, use that - if (self.config.selection_background) |v| { - break :bg switch (v) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, - }; - } + const bg = switch (selected) { + // If we have an explicit selection background color + // specified in the config, use that. + // + // If no configuration, then our selection background + // is our foreground color. + .selection => if (self.config.selection_background) |v| switch (v) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, + } else state.colors.foreground, - // If no configuration, then our selection background - // is our foreground color. - break :bg state.colors.foreground; - } + .search => switch (self.config.search_background) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, + }, // Not selected - break :bg if (style.flags.inverse != isCovering(cell.codepoint())) + .false => if (style.flags.inverse != isCovering(cell.codepoint())) // Two cases cause us to invert (use the fg color as the bg) // - The "inverse" style flag. // - A "covering" glyph; we use fg for bg in that @@ -2616,7 +2627,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { fg_style else // Otherwise they cancel out. - bg_style; + bg_style, }; const fg = fg: { @@ -2628,23 +2639,24 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // - Cell is selected, inverted, and set to cell-foreground // - Cell is selected, not inverted, and set to cell-background // - Cell is inverted and not selected - if (selected) { - // Use the selection foreground if set - if (self.config.selection_foreground) |v| { - break :fg switch (v) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, - }; - } + break :fg switch (selected) { + .selection => if (self.config.selection_foreground) |v| switch (v) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, + } else state.colors.background, - break :fg state.colors.background; - } + .search => switch (self.config.search_foreground) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, + }, - break :fg if (style.flags.inverse) - final_bg - else - fg_style; + .false => if (style.flags.inverse) + final_bg + else + fg_style, + }; }; // Foreground alpha for this cell. @@ -2662,7 +2674,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const default: u8 = 255; // Cells that are selected should be fully opaque. - if (selected) break :bg_alpha default; + if (selected != .false) break :bg_alpha default; // Cells that are reversed should be fully opaque. if (style.flags.inverse) break :bg_alpha default; From bb21c3d6b3efc8543f1f6ca155e1c8506544f4c6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 24 Nov 2025 20:25:25 -0800 Subject: [PATCH 20/22] search: case-insesitive (ascii) search --- src/terminal/search/sliding_window.zig | 51 ++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/src/terminal/search/sliding_window.zig b/src/terminal/search/sliding_window.zig index c1428e35c..ff0fa0277 100644 --- a/src/terminal/search/sliding_window.zig +++ b/src/terminal/search/sliding_window.zig @@ -174,7 +174,7 @@ pub const SlidingWindow = struct { }; // Search the first slice for the needle. - if (std.mem.indexOf(u8, slices[0], self.needle)) |idx| { + if (std.ascii.indexOfIgnoreCase(slices[0], self.needle)) |idx| { return self.highlight( idx, self.needle.len, @@ -200,8 +200,7 @@ pub const SlidingWindow = struct { @memcpy(self.overlap_buf[prefix.len..overlap_len], suffix); // Search the overlap - const idx = std.mem.indexOf( - u8, + const idx = std.ascii.indexOfIgnoreCase( self.overlap_buf[0..overlap_len], self.needle, ) orelse break :overlap; @@ -215,7 +214,7 @@ pub const SlidingWindow = struct { } // Search the last slice for the needle. - if (std.mem.indexOf(u8, slices[1], self.needle)) |idx| { + if (std.ascii.indexOfIgnoreCase(slices[1], self.needle)) |idx| { return self.highlight( slices[0].len + idx, self.needle.len, @@ -660,6 +659,50 @@ test "SlidingWindow single append" { try testing.expect(w.next() == null); } +test "SlidingWindow single append case insensitive ASCII" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .forward, "Boo!"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("hello. boo! hello. 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(node); + + // We should be able to find two matches. + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 7, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start)); + try testing.expectEqual(point.Point{ .active = .{ + .x = 10, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end)); + } + { + const h = w.next().?; + const sel = h.untracked(); + try testing.expectEqual(point.Point{ .active = .{ + .x = 19, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.start)); + try testing.expectEqual(point.Point{ .active = .{ + .x = 22, + .y = 0, + } }, s.pages.pointFromPin(.active, sel.end)); + } + try testing.expect(w.next() == null); + try testing.expect(w.next() == null); +} test "SlidingWindow single append no match" { const testing = std.testing; const alloc = testing.allocator; From d31be89b169736d00dccc2f19ca6fde5c1aee7a1 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Mon, 24 Nov 2025 20:53:23 -0800 Subject: [PATCH 21/22] fix(renderer): load linearized fg color for cursor cell --- src/renderer/shaders/shaders.metal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/shaders/shaders.metal b/src/renderer/shaders/shaders.metal index 4797f89e4..4e02b6336 100644 --- a/src/renderer/shaders/shaders.metal +++ b/src/renderer/shaders/shaders.metal @@ -668,7 +668,7 @@ vertex CellTextVertexOut cell_text_vertex( out.color = load_color( uniforms.cursor_color, uniforms.use_display_p3, - false + true ); } From c92a00332527d8f318e0b8cd5c588a693a30a877 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 24 Nov 2025 21:18:48 -0800 Subject: [PATCH 22/22] pkg/{highway,simdutf}: disable ubsan This causes linker issues for some libghostty users. I don't know why we never saw these issues with Ghostty release builds, but generally speaking I think its fine to do this for 3rd party code unless we've witnessed an issue. And these deps have been stable for a long, long time. --- pkg/highway/build.zig | 4 ++++ pkg/simdutf/build.zig | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/highway/build.zig b/pkg/highway/build.zig index 4c75de49e..fd93675e6 100644 --- a/pkg/highway/build.zig +++ b/pkg/highway/build.zig @@ -67,6 +67,10 @@ pub fn build(b: *std.Build) !void { "-fno-cxx-exceptions", "-fno-slp-vectorize", "-fno-vectorize", + + // Fixes linker issues for release builds missing ubsanitizer symbols + "-fno-sanitize=undefined", + "-fno-sanitize-trap=undefined", }); if (target.result.os.tag != .windows) { try flags.appendSlice(b.allocator, &.{ diff --git a/pkg/simdutf/build.zig b/pkg/simdutf/build.zig index f2ddfeba4..0d827c1cc 100644 --- a/pkg/simdutf/build.zig +++ b/pkg/simdutf/build.zig @@ -24,7 +24,13 @@ pub fn build(b: *std.Build) !void { defer flags.deinit(b.allocator); // Zig 0.13 bug: https://github.com/ziglang/zig/issues/20414 // (See root Ghostty build.zig on why we do this) - try flags.appendSlice(b.allocator, &.{"-DSIMDUTF_IMPLEMENTATION_ICELAKE=0"}); + try flags.appendSlice(b.allocator, &.{ + "-DSIMDUTF_IMPLEMENTATION_ICELAKE=0", + + // Fixes linker issues for release builds missing ubsanitizer symbols + "-fno-sanitize=undefined", + "-fno-sanitize-trap=undefined", + }); lib.addCSourceFiles(.{ .flags = flags.items,