From 1551e409c31992437cb2fa864f576d45c8433801 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 16 Aug 2026 15:49:12 -0300 Subject: big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh --- src/output_pane.zig | 377 +++++++++++++++++++++++++++++----------------------- 1 file changed, 209 insertions(+), 168 deletions(-) (limited to 'src/output_pane.zig') diff --git a/src/output_pane.zig b/src/output_pane.zig index f14d9a51..ecb6f957 100644 --- a/src/output_pane.zig +++ b/src/output_pane.zig @@ -18,16 +18,20 @@ const std = @import("std"); const pardes = @import("pardes.zig"); const Pardes = pardes.Pardes; const Pane = pardes.Pane; -const File = pardes.File; const file_pane = @import("file_pane.zig"); const modal = @import("modal.zig"); const builtins = @import("builtins.zig"); +const runtime_config = @import("runtime_config.zig"); +const effect_sources = @import("effect_sources.zig"); const Builtin = builtins.registry.Builtin(); const config = @import("config.zig"); +const dump = @import("dump.zig"); const lsp = @import("lsp/lsp.zig"); +const gui_shader_source_mode = effect_sources.guiShaderSourceMode(); + /// the installed fonts, for openFonts. GUI only, behind the same comptime /// branch builtins.zig imports it through — see the note there. -const fonts = if (pardes.font_picker) @import("fonts.zig") else struct {}; +const fonts = if (builtins.capabilities.font_picker) @import("fonts.zig") else struct {}; /// What opened this buffer — THE field, and the only input to `traits`. /// @@ -45,83 +49,35 @@ pub const Origin = union(enum) { search, }; -/// The cap on a recorded argument, matching the one lspRequest already puts on -/// an effect payload. ponytail: a longer pattern is TRUNCATED here, because -/// `arg` is a record of what was asked and never the text anything re-runs; if -/// something ever re-runs it, this becomes gpa-owned like `content`. -pub const max_arg = 128; +/// The exact command identity. A search prompt is bounded by the same one-line +/// cap as a tag, so retaining that whole bound keeps refill and dump/restore +/// identity exact without adding a per-output allocation. +pub const max_arg = dump.max_origin_arg; -/// An output buffer's own state, hung off `File.output`. +/// An output buffer's own state, hung off `file_pane.State.output`. pub const Output = struct { from: Origin, /// the command's ARGUMENT: the pattern a Grep matched, the new name a - /// Rename took, the SPC prefix a Help lists. Inline rather than allocated - /// — it is a fixed-size record, and an owned slice would buy a free in - /// deinitPane and an errdefer at every open site for nothing. + /// Rename took, the SPC prefix a Help lists. Inline rather than allocated: + /// tag input already enforces this exact cap. arg_buf: [max_arg]u8 = undefined, - arg_len: u8 = 0, + arg_len: u16 = 0, pub fn arg(o: *const Output) []const u8 { return o.arg_buf[0..o.arg_len]; } }; -pub fn setArg(o: *Output, text: []const u8) void { - o.arg_len = @intCast(@min(text.len, max_arg)); - @memcpy(o.arg_buf[0..o.arg_len], text[0..o.arg_len]); +pub fn setArg(o: *Output, text: []const u8) error{ArgumentTooLong}!void { + if (text.len > max_arg) return error.ArgumentTooLong; + o.arg_len = @intCast(text.len); + @memcpy(o.arg_buf[0..o.arg_len], text); } -/// Every way an output pane differs from a file pane. One struct so the -/// QUESTIONS are visible even where today every buffer answers alike: a column -/// that never varies still says "this is decided here", which is what makes -/// the next kind of buffer a row rather than a hunt. -pub const Traits = struct { - /// the buffer's name (`+Search`...). DERIVED from the command, never the - /// thing that identifies it — that inversion is what this file undoes. - name: []const u8, - /// n/N walk the rows: each is a `path:LINE:COL text` location the ordinary - /// look path resolves, so the buffer IS helix's picker. Prose (a hover - /// blurb, a formatting diff) has nowhere to step to. - /// - /// It is also what makes a step ROW-GRAINED (Grain below): one stop per - /// row, on the location at its head. A pane whose lines are free text — - /// a terminal, a file, a PDF, and the prose buffers here — steps every - /// look-able word instead, several to a line. - steps: bool = false, - /// ...and WHAT THE ROWS ARE. Off, each is a LOCATION with a look-able - /// `path:LINE:COL` word inside it. On, each is a COMMAND LINE — the whole - /// row, exactly as you would have typed it (ThemeSel's `Theme gruvbox`) — - /// with no path in it to pick out. - /// - /// TWO readers, one fact, which is why the column is named for the fact: - /// n/N (lookWalk) select the location at the head of a location row, - /// and THE WHOLE LINE of a command row, since the line - /// is the unit there. Either way they only select; Enter - /// looks what they left, Tab runs it. - /// searchStep Looks a location row's leading word and Execs a - /// command row whole — still how `]d`/`[d` and acme's - /// button-3 arrive somewhere in one gesture. - /// Both go through the ordinary builtins (config.look_cmd / exec_cmd), so - /// a row does exactly what the matching mouse button on it would. - /// - /// Nothing about this is output-pane specific, which is why it is a column - /// here and not a branch in either reader: `file_row` answers it too, so a - /// file pane whose lines happen to be commands is one word away from - /// behaving the same. - commands: bool = false, - /// an answer of exactly ONE row jumps straight there instead of opening - /// this buffer at all — helix: the gotos jump on a single location and - /// show a picker on several, a symbol list is always a picker. - jumps: bool = false, - /// a DOCUMENT for layout purposes: claims a column of its own, is a split - /// parent, pays for a split. A result list is not — it belongs to the pane - /// that asked for it, lands directly below it and takes its rows from - /// there, so opening or closing one never resizes a bystander. - doc: bool = false, - /// there is a file behind it to write. Also what its tag says: no Save to - /// offer means the plain pane tail rather than the file one. - saves: bool = false, -}; +/// Every way an output pane differs from a file pane. Manual builtins already +/// declare this exact row beside their implementation; use that schema here +/// too instead of copying it into a parallel struct. +pub const Traits = builtins.OutputTraits; /// A REAL file pane, as a row of the same table — so kind-agnostic code asks /// one question and gets one answer whichever it is holding. `name` is unused: @@ -135,16 +91,7 @@ pub fn traits(o: Origin) Traits { return switch (o) { // rows are `location text`, so n/N walk them .search => .{ .name = config.search_buffer, .steps = true }, - .cmd => |b| blk: { - const meta = builtins.registry.outputTraits(b) orelse - break :blk .{ .name = config.search_buffer, .steps = true }; - break :blk .{ - .name = meta.name, - .steps = meta.steps, - .jumps = meta.jumps, - .commands = meta.commands, - }; - }, + .cmd => |b| builtins.registry.outputTraits(b) orelse unreachable, .query => |k| switch (k) { .hover => .{ .name = config.hover_buffer }, // prose: an action list, a diff, a report about the backend @@ -218,7 +165,8 @@ pub fn word(o: Origin) []const u8 { pub fn fromWord(w: []const u8) ?Origin { if (w.len == 0) return null; if (std.mem.eql(u8, w, "/")) return .search; - if (std.meta.stringToEnum(Builtin, w)) |b| return .{ .cmd = b }; + if (std.meta.stringToEnum(Builtin, w)) |b| + if (builtins.registry.outputTraits(b) != null) return .{ .cmd = b }; if (std.meta.stringToEnum(lsp.Kind, w)) |k| return .{ .query = k }; return null; } @@ -243,11 +191,10 @@ pub fn open(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u std.mem.trimEnd(u8, dir, "/"), traits(from).name, }); errdefer p.gpa.free(path); - const pane = try p.newDocPane(id); var out: Output = .{ .from = from }; - setArg(&out, arg); + try setArg(&out, arg); + const pane = try p.newDocPane(id); pane.file = .{ .path = path, .content = content, .output = out }; - pane.kind = .file; pane.cur_pinned = true; return pane; } @@ -285,7 +232,7 @@ pub fn fillResults(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: [] if (!std.meta.eql(o.from, from)) continue; if (by_arg and !std.mem.eql(u8, o.arg(), arg)) continue; if (!std.mem.eql(u8, std.fs.path.dirname(rf.path) orelse "", dir)) continue; - setArg(o, arg); + try setArg(o, arg); // a refill that changes NOTHING keeps its place: a right click on an // already-armed word is an `n`, and throwing the list back to the top // only to scroll down to the stepped row is a jump with no information @@ -297,6 +244,7 @@ pub fn fillResults(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: [] if (traits(from).steps) { pane.search_pane = i; pane.search_row = anchor; + p.armLookWalk(i); } return; } @@ -310,14 +258,10 @@ pub fn fillResults(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: [] if (traits(from).steps) { pane.search_pane = free; pane.search_row = anchor; + p.armLookWalk(free); } } -fn putPrint(dst: []u8, at: *usize, comptime fmt: []const u8, args: anytype) void { - const text = std.fmt.bufPrint(dst[at.*..], fmt, args) catch unreachable; - at.* += text.len; -} - /// The Jumplist builtin: the focus history (Pardes.jumps) written out as text, /// one row per location, oldest first — the same `location text` shape every /// results buffer here has, which is what buys n/N stepping and Look-on-a-row @@ -336,35 +280,8 @@ fn putPrint(dst: []u8, at: *usize, comptime fmt: []const u8, args: anytype) void /// content line for anything holding text, else the pane's directory: enough /// to recognise the place without opening it. pub fn openJumps(p: *Pardes, id: usize) !void { - const arena = p.scratch.allocator(); - var len: usize = 0; - for (p.jumps[0..p.njumps]) |j| { - const jp = p.panes[j.pane] orelse continue; - var idbuf: [16]u8 = undefined; - const pdf_path: ?[]const u8 = if (comptime pardes.pdf_enabled) jp.pdfPath() else null; - const has_path = if (jp.file) |f| f.output == null else pdf_path != null; - const loc: []const u8 = if (has_path) - (if (jp.file) |f| f.path else pdf_path.?) - else - std.fmt.bufPrint(&idbuf, config.pane_addr ++ "{d}", .{j.pane}) catch unreachable; - const what: []const u8 = if (jp.file) |f| - std.mem.trim(u8, modal.lineSlice(f.content, j.line -| 1), " \t\r") - else if (jp.image) |iv| - iv.path - else if (pdf_path) |path| - path - else - jp.cwdSlice(); - var cut = @min(what.len, 120); - while (cut > 0 and cut < what.len and what[cut] & 0xc0 == 0x80) cut -= 1; - len += if (j.line == 0) - std.fmt.count("{s} {s}\n", .{ loc, what[0..cut] }) - else - std.fmt.count("{s}:{d}:{d} {s}\n", .{ loc, j.line, j.col, what[0..cut] }); - } - - const out = try arena.alloc(u8, len); - var at: usize = 0; + var out: std.Io.Writer.Allocating = .init(p.gpa); + errdefer out.deinit(); for (p.jumps[0..p.njumps]) |j| { const jp = p.panes[j.pane] orelse continue; var idbuf: [16]u8 = undefined; @@ -385,12 +302,12 @@ pub fn openJumps(p: *Pardes, id: usize) !void { var cut = @min(what.len, 120); while (cut > 0 and cut < what.len and what[cut] & 0xc0 == 0x80) cut -= 1; if (j.line == 0) - putPrint(out, &at, "{s} {s}\n", .{ loc, what[0..cut] }) + try out.writer.print("{s} {s}\n", .{ loc, what[0..cut] }) else - putPrint(out, &at, "{s}:{d}:{d} {s}\n", .{ loc, j.line, j.col, what[0..cut] }); + try out.writer.print("{s}:{d}:{d} {s}\n", .{ loc, j.line, j.col, what[0..cut] }); } - std.debug.assert(at == out.len); - try openStepped(p, id, .{ .cmd = .Jumplist }, out); + const content = try out.toOwnedSlice(); + try openStepped(p, id, .{ .cmd = .Jumplist }, content); } /// The ThemeSel builtin: the theme ring written out as one `Theme ` row @@ -400,59 +317,49 @@ pub fn openJumps(p: *Pardes, id: usize) !void { /// walking the list is trying the themes on and stopping on one is choosing /// it: no picker mode, no preview state, nothing to commit or cancel. /// -/// The command's own name comes from the builtin rather than a literal: the -/// word is derived from the struct in exactly one place (builtins.word), and a -/// rename there must not leave rows here that name something that is gone. +/// The command's own name comes from the runtime setting descriptor rather +/// than a second literal. The descriptor generates the builtin too, so a +/// rename cannot leave picker rows naming a command that is gone. pub fn openThemes(p: *Pardes, id: usize) !void { - const arena = p.scratch.allocator(); - var len: usize = 0; + var out: std.Io.Writer.Allocating = .init(p.gpa); + errdefer out.deinit(); for (pardes.themes) |t| - len += std.fmt.count(comptime builtins.word(builtins.Theme) ++ " {s}\n", .{t.name}); - const out = try arena.alloc(u8, len); - var at: usize = 0; - for (pardes.themes) |t| - putPrint(out, &at, comptime builtins.word(builtins.Theme) ++ " {s}\n", .{t.name}); - std.debug.assert(at == out.len); - try openStepped(p, id, .{ .cmd = .ThemeSel }, out); + try out.writer.print(comptime runtime_config.findAction(.theme).?.word ++ " {s}\n", .{t.name}); + const content = try out.toOwnedSlice(); + try openStepped(p, id, .{ .cmd = .ThemeSel }, content); } /// The FontSel builtin: openThemes over the fonts installed on the machine /// instead of the themes compiled into the binary — one `Font ` row /// each, in a buffer whose rows n/N RUN, so walking it wears the fonts and /// stopping picks one. Everything that makes that work is already above; this -/// is the same eight lines pointed at a different list. +/// is the same one-pass writer pointed at a different list. /// -/// Only where the shell draws its own text, and the body says so rather than -/// the signature: fonts.list and the FontSel origin both exist only there, and -/// a comptime-false `if` is what keeps the tty build from analysing either. -/// The dead parameters on that build are the honest shape of "this platform -/// cannot open one". +/// Only where the shell draws its own text. The same `font_picker` availability +/// bit that generates the Font/FontSel builtins keeps non-GUI builds from +/// analysing font discovery here. pub fn openFonts(p: *Pardes, id: usize) !void { - if (pardes.font_picker) { + if (builtins.capabilities.font_picker) { const arena = p.scratch.allocator(); const font_list = fonts.list(arena, null); - var len: usize = 0; - for (font_list) |f| - len += std.fmt.count(comptime builtins.word(builtins.Font) ++ " {s}\n", .{f.name}); - const out = try arena.alloc(u8, len); - var at: usize = 0; + var out: std.Io.Writer.Allocating = .init(p.gpa); + errdefer out.deinit(); for (font_list) |f| - putPrint(out, &at, comptime builtins.word(builtins.Font) ++ " {s}\n", .{f.name}); - std.debug.assert(at == out.len); - try openStepped(p, id, .{ .cmd = .FontSel }, out); + try out.writer.print(comptime runtime_config.findAction(.font).?.word ++ " {s}\n", .{f.name}); + const content = try out.toOwnedSlice(); + try openStepped(p, id, .{ .cmd = .FontSel }, content); } } /// Open a buffer n/N will walk, and arm them on it: the shared tail of every -/// builtin that answers with a list. `text` is borrowed (the callers build it -/// in the scratch arena) and copied into a gpa buffer the pane adopts. +/// builtin that answers with a list. `content` is gpa-owned and adopted by the +/// new pane, or freed here if opening it fails. /// /// Focus stays with the pane that ASKED, exactly as it does after a search: /// n/N are read there, and they step the buffer they just armed. -fn openStepped(p: *Pardes, id: usize, from: Origin, text: []const u8) !void { - const pane = p.panes[id] orelse return error.MissingPane; - const content = try p.gpa.dupe(u8, text); +fn openStepped(p: *Pardes, id: usize, from: Origin, content: []u8) !void { errdefer p.gpa.free(content); + const pane = p.panes[id] orelse return error.MissingPane; const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice(); const free = p.freeSlot() orelse return error.NoPaneSlots; const np = try open(p, free, dir, from, "", content); @@ -460,6 +367,7 @@ fn openStepped(p: *Pardes, id: usize, from: Origin, text: []const u8) !void { p.active = id; pane.search_pane = free; pane.search_row = null; + p.armLookWalk(free); } /// The Help builtin: THE INDEX of builtins — every one of them, and every way @@ -474,7 +382,7 @@ fn openStepped(p: *Pardes, id: usize, from: Origin, text: []const u8) !void { /// SPC cannot reach at all, so the reference page IS the filter's degenerate /// case. A second builtin would have been a second renderer over a superset of /// these rows, and the two would have drifted the first time a column moved. -pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) !void { +fn helpContent(gpa: std.mem.Allocator, prefix: []const u8) ![]u8 { const full_header = "pardes builtins, and how to run each:\nSPC and its keys, a chord, a button, the\ntopbar - or the name, executed anywhere.\n\n"; const group_header = "pardes builtins under SPC"; var len: usize = if (prefix.len == 0) @@ -487,7 +395,7 @@ pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) !void { if (!std.mem.startsWith(u8, row.path orelse "", prefix)) continue; len += row.line.len + 1; } - const content = try p.gpa.alloc(u8, len); + const content = try gpa.alloc(u8, len); var at: usize = 0; if (prefix.len == 0) { @memcpy(content[0..full_header.len], full_header); @@ -512,29 +420,88 @@ pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) !void { at += 1; } std.debug.assert(at == content.len); + return content; +} + +pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) !void { + const content = try helpContent(p.gpa, prefix); // content is handed off unfreed on purpose: openRead adopts it or frees // it, and nothing between the alloc above and this line can fail. return openRead(p, id, .{ .cmd = .Help }, prefix, content); } -/// The Config builtin: WHERE the startup config file is, as one line of text. -/// -/// The PATH and not the file. `Look` on the line opens it when it exists, and -/// when it does not the path is still the entire answer — "put your Theme and -/// Font lines HERE" is the question this is asked, and a builtin that opened -/// an empty buffer instead would have said nothing. The core never resolved -/// it: the launcher did, before init (Options.startup_config_path), so this -/// prints what was actually consulted rather than recomputing a guess that -/// could differ from it. +test "full Help renders every enabled builtin row" { + const content = try helpContent(std.testing.allocator, ""); + defer std.testing.allocator.free(content); + + const body = content[(std.mem.lastIndexOf(u8, content, "\n\n") orelse + return error.MissingHelpHeader) + 2 ..]; + var lines = std.mem.splitScalar(u8, body, '\n'); + for (pardes.builtin_rows) |row| + try std.testing.expectEqualStrings(row.line, lines.next() orelse + return error.MissingBuiltinHelpRow); + // The renderer terminates every row with a newline, so only split's empty + // trailing field may remain. Any extra non-empty field is an unregistered + // Help row and any missing row already failed in the loop above. + try std.testing.expectEqualStrings("", lines.next() orelse + return error.MissingHelpTerminator); + try std.testing.expect(lines.next() == null); +} + +/// The complete live Config report: the generated settings and the host facts +/// needed to interpret them. The startup path remains ordinary selectable text +/// in the report, so Look still opens the exact file the launcher consulted. pub fn openConfig(p: *Pardes, id: usize) !void { - const content = if (p.opts.startup_config_path) |path| - try std.fmt.allocPrint(p.gpa, "{s}\n", .{path}) - else - // the browser, and a native launch with no HOME to build one from - try p.gpa.dupe(u8, "no per-user config path\n"); + var out: std.Io.Writer.Allocating = .init(p.gpa); + errdefer out.deinit(); + try runtime_config.writeReport(&out.writer, .{ + .startup_config_path = p.opts.startup_config_path, + .platform = @tagName(pardes.platform), + .theme_name = p.theme().name, + .compiled_default_shell = config.default_shell, + .gui_shader_source_mode = if (gui_shader_source_mode) |mode| + mode.label() + else + null, + .hover_delay_frames = config.look_preview_delay_frames, + .native_images = p.native_images, + .capabilities = builtins.capabilities, + .state = &p.settings, + }); + const content = try out.toOwnedSlice(); return openRead(p, id, .{ .cmd = .Config }, "", content); } +/// Print the implementation that this build actually uses for one effect. +/// Sources are build inputs embedded as bytes, so this stays useful from an +/// installed binary with no repository beside it. +pub fn openEffectCode(p: *Pardes, id: usize, argument: []const u8) !void { + const name = std.mem.trim(u8, argument, " \t\r\n"); + const setting = runtime_config.find(name) orelse return error.UnknownEffect; + switch (setting.action) { + .transition, .scene => {}, + else => return error.NotAnEffect, + } + if (!setting.enabled(builtins.capabilities)) return error.EffectUnavailable; + const segments = effect_sources.forSetting(setting) orelse + return error.EffectUnavailable; + + var out: std.Io.Writer.Allocating = .init(p.gpa); + errdefer out.deinit(); + try out.writer.print("EffectCode {s} ({s})\n", .{ setting.word, @tagName(effect_sources.backend) }); + if (gui_shader_source_mode) |mode| + try out.writer.print("GUI shader source: {s}\n", .{mode.label()}); + try out.writer.writeByte('\n'); + for (segments) |segment| { + try out.writer.print("--- {s} ---\n", .{segment.path}); + try out.writer.writeAll(segment.source); + if (!std.mem.endsWith(u8, segment.source, "\n")) try out.writer.writeByte('\n'); + try out.writer.writeByte('\n'); + } + const content = try out.toOwnedSlice(); + return openRead(p, id, .{ .cmd = .EffectCode }, setting.word, content); +} + /// Open a buffer you READ, and go there: the shared tail of every builtin /// whose answer is a document rather than a list. Asking again REFRESHES the /// one already open instead of stacking a twin beside it — found by its @@ -555,8 +522,8 @@ fn openRead(p: *Pardes, id: usize, from: Origin, arg: []const u8, content: []u8) const hf = if (hp.file) |*f| f else continue; const ho = if (hf.output) |*o| o else continue; if (!std.meta.eql(ho.from, from)) continue; + try setArg(ho, arg); file_pane.setContent(p, hf, content); - setArg(ho, arg); hf.scroll = 0; hp.cur_row = 0; hp.msel.active = false; @@ -569,3 +536,77 @@ fn openRead(p: *Pardes, id: usize, from: Origin, arg: []const u8, content: []u8) p.placeDoc(id, free, np); p.active = free; } + +test "dump origins accept only builtins that actually own output panes" { + try std.testing.expectEqual(Origin{ .cmd = .Help }, fromWord("Help").?); + try std.testing.expect(fromWord("Kill") == null); + try std.testing.expect(fromWord("Theme") == null); +} + +test "result refill identity retains the full bounded argument" { + const p = try Pardes.init(std.testing.allocator, .{ + .tty_only = true, + .cols = 80, + .rows = 24, + }); + defer p.deinit(); + + var first: [max_arg]u8 = @splat('a'); + var second = first; + first[200] = 'x'; + second[200] = 'y'; + + try fillResults( + p, + 0, + "/tmp", + .search, + &first, + try p.gpa.dupe(u8, "first\n"), + null, + ); + const first_id = p.panes[0].?.search_pane orelse return error.MissingResults; + const next_slot = p.freeSlot(); + + try fillResults( + p, + 0, + "/tmp", + .search, + &first, + try p.gpa.dupe(u8, "refilled\n"), + null, + ); + try std.testing.expectEqual(first_id, p.panes[0].?.search_pane.?); + try std.testing.expectEqual(next_slot, p.freeSlot()); + try std.testing.expectEqualStrings("refilled\n", p.panes[first_id].?.file.?.content); + + try fillResults( + p, + 0, + "/tmp", + .search, + &second, + try p.gpa.dupe(u8, "second\n"), + null, + ); + try std.testing.expect(p.panes[0].?.search_pane.? != first_id); + + var output: Output = .{ .from = .search }; + var oversized: [max_arg + 1]u8 = @splat('z'); + try std.testing.expectError(error.ArgumentTooLong, setArg(&output, &oversized)); + const slot_before_error = p.freeSlot(); + try std.testing.expectError( + error.ArgumentTooLong, + fillResults( + p, + 0, + "/tmp", + .search, + &oversized, + try p.gpa.dupe(u8, "must be freed\n"), + null, + ), + ); + try std.testing.expectEqual(slot_before_error, p.freeSlot()); +} -- cgit v1.3