summaryrefslogtreecommitdiff
path: root/src/output_pane.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/output_pane.zig')
-rw-r--r--src/output_pane.zig377
1 files changed, 209 insertions, 168 deletions
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 <name>` 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 <name>` 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());
+}