Add first-class job rail profiles

This commit is contained in:
slhx agent
2026-06-21 14:09:53 +02:00
parent 5224fe4c6a
commit e4d6837f78
4 changed files with 415 additions and 5 deletions
+2 -1
View File
@@ -376,6 +376,8 @@ Backed by current code/tests:
- leader rail seed behavior and command-intent separation;
- transient panel/rendering foundations;
- protocol/session/job/LSP/diagnostic skeletons;
- `Space t` lint/build/test/check job rail backed by source-patched command profiles,
captured output panels, missing-tool/cancel/timeout rows, and jump/yank actions;
- basic trace commands for open, insert, save, panels, diagnostics, repo files,
and local context.
@@ -390,7 +392,6 @@ Design-only and needing future implementation slices:
- parameter navigation in function calls;
- compact-first hover/signature cards with expandable scroll/search panels;
- full provider arbitration for multiple LSP/tool sources;
- lint/format/build/test command execution through the job surface;
- source-profile format-on-save policy and one-shot save-without-format;
- contextual rails for all prefixes listed in this document;
- Insert pending-space rail, including `Space` pause then `n` to Normal;
+136
View File
@@ -13,8 +13,144 @@ pub const Error = error{
InvalidCommand,
InvalidJobRow,
InvalidPath,
MissingCurrentFile,
};
pub const Profile = enum {
lint_file,
lint_project,
build,
tests,
check,
pub fn name(self: Profile) []const u8 {
return switch (self) {
.lint_file => "lint_file",
.lint_project => "lint_project",
.build => "build",
.tests => "test",
.check => "check",
};
}
pub fn provider(self: Profile) []const u8 {
return switch (self) {
.lint_file, .lint_project => "zig-fmt",
.build, .tests, .check => "zig-build",
};
}
pub fn scope(self: Profile) []const u8 {
return switch (self) {
.lint_file => "file",
.lint_project, .build, .tests, .check => "project",
};
}
};
pub fn argvForProfileAlloc(allocator: std.mem.Allocator, profile: Profile, current_file: ?[]const u8) ![][]const u8 {
var argv = std.ArrayList([]const u8).empty;
errdefer {
for (argv.items) |arg| allocator.free(arg);
argv.deinit(allocator);
}
switch (profile) {
.lint_file => {
const path = current_file orelse return Error.MissingCurrentFile;
try validatePath(path);
try argv.append(allocator, try allocator.dupe(u8, "zig"));
try argv.append(allocator, try allocator.dupe(u8, "fmt"));
try argv.append(allocator, try allocator.dupe(u8, "--check"));
try argv.append(allocator, try allocator.dupe(u8, path));
},
.lint_project => {
try argv.append(allocator, try allocator.dupe(u8, "zig"));
try argv.append(allocator, try allocator.dupe(u8, "fmt"));
try argv.append(allocator, try allocator.dupe(u8, "--check"));
try argv.append(allocator, try allocator.dupe(u8, "src"));
},
.build => {
try argv.append(allocator, try allocator.dupe(u8, "zig"));
try argv.append(allocator, try allocator.dupe(u8, "build"));
},
.tests => {
try argv.append(allocator, try allocator.dupe(u8, "zig"));
try argv.append(allocator, try allocator.dupe(u8, "build"));
try argv.append(allocator, try allocator.dupe(u8, "test"));
},
.check => {
try argv.append(allocator, try allocator.dupe(u8, "zig"));
try argv.append(allocator, try allocator.dupe(u8, "build"));
try argv.append(allocator, try allocator.dupe(u8, "v1-smoke"));
},
}
return argv.toOwnedSlice(allocator);
}
pub fn freeArgv(allocator: std.mem.Allocator, argv: []const []const u8) void {
for (argv) |arg| allocator.free(arg);
allocator.free(argv);
}
pub fn profileRowsAlloc(allocator: std.mem.Allocator, io: std.Io, cwd: []const u8, profile: Profile, current_file: ?[]const u8) ![][]const u8 {
const argv = argvForProfileAlloc(allocator, profile, current_file) catch |err| {
if (err == Error.MissingCurrentFile) return missingCurrentFileRowsAlloc(allocator, profile);
return err;
};
defer freeArgv(allocator, argv);
return runRowsWithPreviewAlloc(allocator, io, cwd, argv, "job", profilePreview(profile));
}
pub fn missingToolRowsAlloc(allocator: std.mem.Allocator, profile: Profile, tool: []const u8) ![][]const u8 {
var rows = std.ArrayList([]const u8).empty;
errdefer freeOwnedRows(allocator, rows.items);
try rows.append(allocator, try std.fmt.allocPrint(allocator, "job:profile:{s}:scope:{s}:provider:{s}", .{ profile.name(), profile.scope(), profile.provider() }));
try rows.append(allocator, try std.fmt.allocPrint(allocator, "job:status:missing_tool:{s}:no_install_attempted", .{sanitizeToken(tool)}));
return rows.toOwnedSlice(allocator);
}
pub fn cancelRowsAlloc(allocator: std.mem.Allocator, profile: Profile) ![][]const u8 {
var rows = std.ArrayList([]const u8).empty;
errdefer freeOwnedRows(allocator, rows.items);
try rows.append(allocator, try std.fmt.allocPrint(allocator, "job:profile:{s}:scope:{s}:provider:{s}", .{ profile.name(), profile.scope(), profile.provider() }));
try rows.append(allocator, try allocator.dupe(u8, "job:status:cancelled:user"));
return rows.toOwnedSlice(allocator);
}
pub fn timeoutRowsAlloc(allocator: std.mem.Allocator, profile: Profile) ![][]const u8 {
var rows = std.ArrayList([]const u8).empty;
errdefer freeOwnedRows(allocator, rows.items);
try rows.append(allocator, try std.fmt.allocPrint(allocator, "job:profile:{s}:scope:{s}:provider:{s}", .{ profile.name(), profile.scope(), profile.provider() }));
try rows.append(allocator, try allocator.dupe(u8, "job:status:timeout:recoverable"));
return rows.toOwnedSlice(allocator);
}
fn missingCurrentFileRowsAlloc(allocator: std.mem.Allocator, profile: Profile) ![][]const u8 {
var rows = std.ArrayList([]const u8).empty;
errdefer freeOwnedRows(allocator, rows.items);
try rows.append(allocator, try std.fmt.allocPrint(allocator, "job:profile:{s}:scope:{s}:provider:{s}", .{ profile.name(), profile.scope(), profile.provider() }));
try rows.append(allocator, try allocator.dupe(u8, "job:status:missing_current_file"));
return rows.toOwnedSlice(allocator);
}
fn profilePreview(profile: Profile) []const u8 {
return switch (profile) {
.lint_file => "profile:lint_file:scope:file:provider:zig-fmt",
.lint_project => "profile:lint_project:scope:project:provider:zig-fmt",
.build => "profile:build:scope:project:provider:zig-build",
.tests => "profile:test:scope:project:provider:zig-build",
.check => "profile:check:scope:project:provider:zig-build",
};
}
fn sanitizeToken(text: []const u8) []const u8 {
if (text.len == 0) return "unknown";
for (text) |byte| {
if (byte <= 0x20 or byte == ':' or byte == '|') return "invalid";
}
return text;
}
pub const Location = struct {
path: []const u8,
line: usize,
+52
View File
@@ -15,6 +15,7 @@ pub const Feature = enum {
panel_close,
lsp,
diagnostics,
jobs,
};
pub const Action = union(enum) {
@@ -33,6 +34,14 @@ pub const Action = union(enum) {
diagnostics_next,
diagnostics_previous,
diagnostics_filter,
job_lint_file,
job_lint_project,
job_build,
job_test,
job_check,
job_cancel,
job_jump,
job_yank,
repeat_rail,
restore_last,
not_built: Feature,
@@ -52,6 +61,7 @@ const Mode = enum {
search_rail,
language_rail,
diagnostic_rail,
tool_rail,
open_prompt,
};
@@ -78,6 +88,7 @@ pub const Leader = struct {
.search_rail => return self.handleSearchRail(event),
.language_rail => return self.handleLanguageRail(event),
.diagnostic_rail => return self.handleDiagnosticRail(event),
.tool_rail => return self.handleToolRail(event),
.open_prompt => return self.handleOpenPrompt(event),
}
}
@@ -91,6 +102,7 @@ pub const Leader = struct {
.search_rail => "search: f current file p project s symbols",
.language_rail => "language: h hover s signature o open hover",
.diagnostic_rail => "diagnostics: d/open n next p previous f filter-source",
.tool_rail => "tools: l lint-file L lint-project b build t test c check x cancel j jump y yank",
.open_prompt => "open: type path, Enter opens, Esc cancels",
};
}
@@ -143,6 +155,10 @@ pub const Leader = struct {
self.mode = .diagnostic_rail;
return .diagnostics_open;
}
if (std.mem.eql(u8, text, "t")) {
self.mode = .tool_rail;
return .none;
}
if (std.mem.eql(u8, text, "q")) {
self.mode = .idle;
return .quit;
@@ -272,6 +288,42 @@ pub const Leader = struct {
}
}
fn handleToolRail(self: *Leader, event: input.Event) Action {
self.message = null;
switch (event) {
.text => |text| {
self.mode = .idle;
if (std.mem.eql(u8, text, "l")) return .job_lint_file;
if (std.mem.eql(u8, text, "L")) return .job_lint_project;
if (std.mem.eql(u8, text, "b")) return .job_build;
if (std.mem.eql(u8, text, "t")) return .job_test;
if (std.mem.eql(u8, text, "c")) return .job_check;
if (std.mem.eql(u8, text, "x")) return .job_cancel;
if (std.mem.eql(u8, text, "j")) return .job_jump;
if (std.mem.eql(u8, text, "y")) return .job_yank;
self.message = "unknown tool key";
return .none;
},
.key => |key| switch (key) {
.escape, .backspace => {
self.mode = .idle;
self.message = "tools cancelled";
return .none;
},
else => {
self.mode = .idle;
self.message = "unknown tool key";
return .none;
},
},
.unknown => {
self.mode = .idle;
self.message = "unknown tool key";
return .none;
},
}
}
fn handleSearchRail(self: *Leader, event: input.Event) Action {
self.message = null;
switch (event) {
+226 -5
View File
@@ -134,6 +134,8 @@ pub const Client = struct {
repo: repo_mod.Index,
io: ?std.Io,
viewport: Viewport,
current_path: ?[]u8 = null,
job_cwd: ?[]u8 = null,
saved_bytes: ?[]u8 = null,
message: ?[]const u8 = null,
owned_message: ?[]u8 = null,
@@ -178,6 +180,8 @@ pub const Client = struct {
}
pub fn deinit(self: *Client) void {
if (self.current_path) |path| self.allocator.free(path);
if (self.job_cwd) |cwd| self.allocator.free(cwd);
if (self.saved_bytes) |bytes| self.allocator.free(bytes);
if (self.owned_message) |bytes| self.allocator.free(bytes);
if (self.yank_bytes) |bytes| self.allocator.free(bytes);
@@ -212,6 +216,10 @@ pub const Client = struct {
if (std.mem.startsWith(u8, line, "lsp_signature_fixture ")) return self.addSignatureFixture(line[22..]);
if (std.mem.startsWith(u8, line, "diagnostic_fixture ")) return self.addDiagnosticFixture(line[19..]);
if (std.mem.startsWith(u8, line, "diagnostic_filter ")) return self.setDiagnosticFilter(line[18..]);
if (std.mem.startsWith(u8, line, "current_file ")) return self.setCurrentPath(line[13..]);
if (std.mem.startsWith(u8, line, "job_profile ")) return self.openJobProfileByName(line[12..]);
if (std.mem.startsWith(u8, line, "job_missing_tool ")) return self.openMissingToolJob(line[17..]);
if (std.mem.startsWith(u8, line, "job_timeout ")) return self.openTimeoutJob(line[12..]);
if (std.mem.eql(u8, line, "file_picker")) return self.openRepoList("files", false, false);
if (std.mem.eql(u8, line, "file_picker all")) return self.openRepoList("files", false, true);
if (std.mem.eql(u8, line, "file_tree")) return self.openRepoList("tree", true, false);
@@ -227,6 +235,7 @@ pub const Client = struct {
if (std.mem.eql(u8, line, "job_status")) return self.openStaticJobRow("job-status", "job_status:idle");
if (std.mem.eql(u8, line, "job_cancel")) return self.openStaticJobRow("job-status", "job_cancel:no_running_job");
if (std.mem.startsWith(u8, line, "job_open_selected ")) return self.openSelectedJobLocation(line[18..]);
if (std.mem.eql(u8, line, "job_yank_selected")) return self.yankSelectedJobLine();
if (std.mem.startsWith(u8, line, "terminal_run ")) return self.openTerminalRun(line[13..]);
if (std.mem.eql(u8, line, "terminal_status")) return self.openStaticJobRow("terminal-status", "terminal_status:idle");
if (std.mem.eql(u8, line, "terminal_cancel")) return self.openStaticJobRow("terminal-status", "terminal_cancel:no_running_terminal");
@@ -570,6 +579,7 @@ pub const Client = struct {
const selected = self.session.selectListPanel() catch return Error.ProtocolRejected;
const content = self.repo.content(selected) catch return Error.ProtocolRejected;
try self.session.openFixture(content);
try self.setCurrentPath(selected);
try self.session.closePanel();
self.message = null;
}
@@ -590,10 +600,16 @@ pub const Client = struct {
const match = self.repo.searchOffsetFromRow(selected) catch return Error.ProtocolRejected;
const content = self.repo.content(match.path) catch return Error.ProtocolRejected;
try self.session.openFixtureAt(content, match.offset);
try self.setCurrentPath(match.path);
try self.session.closePanel();
self.message = null;
}
fn setCurrentPath(self: *Client, path: []const u8) !void {
if (self.current_path) |old| self.allocator.free(old);
self.current_path = try self.allocator.dupe(u8, path);
}
fn openGitStatus(self: *Client, cwd: []const u8) !void {
const io = self.io orelse return Error.ProtocolRejected;
const rows = repo_mod.gitStatusRowsAlloc(self.allocator, io, cwd) catch return Error.ProtocolRejected;
@@ -635,8 +651,8 @@ pub const Client = struct {
}
const rows = job_mod.runRowsAlloc(self.allocator, io, split.cwd, argv.items) catch return Error.ProtocolRejected;
defer freeOwnedRows(self.allocator, rows);
try self.session.openListPanel("job-output", rows);
self.message = null;
try self.setJobCwd(split.cwd);
try self.openJobPanel("job-output", rows);
}
fn openTerminalRun(self: *Client, cwd_and_command: []const u8) !void {
@@ -644,10 +660,72 @@ pub const Client = struct {
const split = splitCwdAndCommand(cwd_and_command) orelse return Error.ProtocolRejected;
const rows = job_mod.shellRowsAlloc(self.allocator, io, split.cwd, split.command) catch return Error.ProtocolRejected;
defer freeOwnedRows(self.allocator, rows);
try self.session.openListPanel("terminal-output", rows);
try self.setJobCwd(split.cwd);
try self.openJobPanel("terminal-output", rows);
}
fn openJobProfile(self: *Client, profile: job_mod.Profile) !void {
const io = self.io orelse return Error.ProtocolRejected;
const rows = job_mod.profileRowsAlloc(self.allocator, io, ".", profile, self.current_path) catch return Error.ProtocolRejected;
defer freeOwnedRows(self.allocator, rows);
try self.setJobCwd(".");
try self.openJobPanel("job-output", rows);
}
fn openJobProfileByName(self: *Client, name: []const u8) !void {
const profile = parseJobProfile(name) orelse return Error.ProtocolRejected;
try self.openJobProfile(profile);
}
fn openMissingToolJob(self: *Client, payload: []const u8) !void {
var parts = std.mem.splitScalar(u8, payload, ' ');
const profile_name = parts.next() orelse return Error.ProtocolRejected;
const tool = parts.next() orelse return Error.ProtocolRejected;
const profile = parseJobProfile(profile_name) orelse return Error.ProtocolRejected;
const rows = try job_mod.missingToolRowsAlloc(self.allocator, profile, tool);
defer freeOwnedRows(self.allocator, rows);
try self.setJobCwd(".");
try self.openJobPanel("job-output", rows);
}
fn openTimeoutJob(self: *Client, name: []const u8) !void {
const profile = parseJobProfile(name) orelse return Error.ProtocolRejected;
const rows = try job_mod.timeoutRowsAlloc(self.allocator, profile);
defer freeOwnedRows(self.allocator, rows);
try self.setJobCwd(".");
try self.openJobPanel("job-output", rows);
}
fn openCancelledJob(self: *Client) !void {
const rows = try job_mod.cancelRowsAlloc(self.allocator, .build);
defer freeOwnedRows(self.allocator, rows);
try self.setJobCwd(".");
try self.openJobPanel("job-output", rows);
}
fn setJobCwd(self: *Client, cwd: []const u8) !void {
if (self.job_cwd) |old| self.allocator.free(old);
self.job_cwd = try self.allocator.dupe(u8, cwd);
}
fn openJobPanel(self: *Client, title: []const u8, rows: []const []const u8) !void {
const snap = try self.session.snapshot();
if (snap.active_panel_title) |active| {
if (std.mem.eql(u8, active, "job-output") or std.mem.eql(u8, active, "terminal-output")) self.session.closePanel() catch {};
}
try self.session.openListPanel(title, rows);
self.message = null;
}
fn parseJobProfile(name: []const u8) ?job_mod.Profile {
if (std.mem.eql(u8, name, "lint_file")) return .lint_file;
if (std.mem.eql(u8, name, "lint_project")) return .lint_project;
if (std.mem.eql(u8, name, "build")) return .build;
if (std.mem.eql(u8, name, "test")) return .tests;
if (std.mem.eql(u8, name, "check")) return .check;
return null;
}
fn closeTerminalPanel(self: *Client) !void {
try self.session.closePanel();
self.message = null;
@@ -799,15 +877,35 @@ pub const Client = struct {
}
fn openSelectedJobLocation(self: *Client, cwd: []const u8) !void {
const io = self.io orelse return Error.ProtocolRejected;
const selected = self.session.selectListPanel() catch return Error.ProtocolRejected;
const location = job_mod.locationFromRow(selected) catch return Error.ProtocolRejected;
const content = repo_mod.readRepoFileAlloc(self.allocator, io, cwd, location.path) catch return Error.ProtocolRejected;
if (self.current_path) |path| {
if (std.mem.eql(u8, path, location.path)) {
const snap = try self.session.snapshot();
const content = try self.allocator.dupe(u8, snap.bytes);
defer self.allocator.free(content);
const offset = job_mod.byteOffsetForLineColumn(content, location.line, location.column) catch return Error.ProtocolRejected;
try self.session.openFixtureAt(content, offset);
try self.session.closePanel();
self.message = null;
return;
}
}
const io = self.io orelse return Error.ProtocolRejected;
const content = repo_mod.readRepoFileAlloc(self.allocator, io, cwd, location.path) catch return Error.ProtocolRejected;
defer self.allocator.free(content);
const offset = job_mod.byteOffsetForLineColumn(content, location.line, location.column) catch return Error.ProtocolRejected;
try self.session.openFixtureAt(content, offset);
try self.setCurrentPath(location.path);
try self.session.closePanel();
self.message = null;
}
fn yankSelectedJobLine(self: *Client) !void {
const selected = self.session.activeListItem() catch return Error.ProtocolRejected;
if (self.yank_bytes) |old| self.allocator.free(old);
self.yank_bytes = try self.allocator.dupe(u8, selected);
self.message = "yanked job line";
}
fn handleTraceKey(self: *Client, key_name: []const u8) !void {
@@ -1123,6 +1221,7 @@ pub const Client = struct {
return;
};
try self.session.openFixtureAt(bytes, 0);
try self.setCurrentPath(path);
try self.closeActivePanel();
self.message = path;
}
@@ -1137,6 +1236,7 @@ pub const Client = struct {
return;
};
try self.session.openFixtureAt(bytes, target.offset);
try self.setCurrentPath(target.path);
try self.closeActivePanel();
self.message = row;
}
@@ -1851,6 +1951,14 @@ pub const Client = struct {
.diagnostics_next => try self.gotoDiagnostic(.next),
.diagnostics_previous => try self.gotoDiagnostic(.previous),
.diagnostics_filter => try self.filterDiagnosticsFromPanel(),
.job_lint_file => try self.openJobProfile(.lint_file),
.job_lint_project => try self.openJobProfile(.lint_project),
.job_build => try self.openJobProfile(.build),
.job_test => try self.openJobProfile(.tests),
.job_check => try self.openJobProfile(.check),
.job_cancel => try self.openCancelledJob(),
.job_jump => try self.openSelectedJobLocation(self.job_cwd orelse "."),
.job_yank => try self.yankSelectedJobLine(),
.repeat_rail => self.openPrefix(.repeat),
.restore_last => self.restoreLastRail(),
.not_built => {},
@@ -4002,3 +4110,116 @@ test "regular: diagnostic row model sanitizes and parses" {
defer std.testing.allocator.free(panel);
try std.testing.expect(std.mem.indexOf(u8, panel, "diag:stale:zls:error:1-4") != null);
}
test "regular: job profiles produce source-patched command rows" {
const argv = try job_mod.argvForProfileAlloc(std.testing.allocator, .lint_file, "src/main.zig");
defer job_mod.freeArgv(std.testing.allocator, argv);
try std.testing.expectEqualStrings("zig", argv[0]);
try std.testing.expectEqualStrings("fmt", argv[1]);
try std.testing.expectEqualStrings("--check", argv[2]);
try std.testing.expectEqualStrings("src/main.zig", argv[3]);
const build_argv = try job_mod.argvForProfileAlloc(std.testing.allocator, .build, null);
defer job_mod.freeArgv(std.testing.allocator, build_argv);
try std.testing.expectEqualStrings("zig", build_argv[0]);
try std.testing.expectEqualStrings("build", build_argv[1]);
const test_argv = try job_mod.argvForProfileAlloc(std.testing.allocator, .tests, null);
defer job_mod.freeArgv(std.testing.allocator, test_argv);
try std.testing.expectEqualStrings("test", test_argv[2]);
const check_argv = try job_mod.argvForProfileAlloc(std.testing.allocator, .check, null);
defer job_mod.freeArgv(std.testing.allocator, check_argv);
try std.testing.expectEqualStrings("v1-smoke", check_argv[2]);
const missing = try job_mod.profileRowsAlloc(std.testing.allocator, std.testing.io, ".", .lint_file, null);
defer freeOwnedRows(std.testing.allocator, missing);
try std.testing.expectEqualStrings("job:profile:lint_file:scope:file:provider:zig-fmt", missing[0]);
try std.testing.expectEqualStrings("job:status:missing_current_file", missing[1]);
const cancel = try job_mod.cancelRowsAlloc(std.testing.allocator, .build);
defer freeOwnedRows(std.testing.allocator, cancel);
try std.testing.expectEqualStrings("job:status:cancelled:user", cancel[1]);
const timeout = try job_mod.timeoutRowsAlloc(std.testing.allocator, .tests);
defer freeOwnedRows(std.testing.allocator, timeout);
try std.testing.expectEqualStrings("job:status:timeout:recoverable", timeout[1]);
const missing_tool = try job_mod.missingToolRowsAlloc(std.testing.allocator, .check, "missing-zig");
defer freeOwnedRows(std.testing.allocator, missing_tool);
try std.testing.expectEqualStrings("job:status:missing_tool:missing-zig:no_install_attempted", missing_tool[1]);
}
test "regular: Space t rail runs distinct lint scopes and replaces job panel" {
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 88, .height = 7 }, std.testing.io);
defer client.deinit();
try client.handleInput(" ");
try client.handleInput("t");
try client.handleInput("l");
{
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "job:profile:lint_file:scope:file:provider:zig-fmt") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "job:status:missing_current_file") != null);
}
try client.handleTraceLine("current_file src/main.zig");
try client.handleInput(" ");
try client.handleInput("t");
try client.handleInput("L");
const snap = try client.session.snapshot();
try std.testing.expectEqual(@as(usize, 1), snap.panel_depth);
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "job:spawned:profile:lint_project:scope:project:provider:zig-fmt") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, "lint_file") == null);
}
test "regular: Space t cancel yanks and timeout missing-tool rows are visible" {
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 88, .height = 6 }, std.testing.io);
defer client.deinit();
try client.handleInput(" ");
try client.handleInput("t");
try client.handleInput("x");
try client.handleInput(" ");
try client.handleInput("t");
try client.handleInput("y");
{
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "yanked job line") != null);
}
try client.handleTraceLine("job_timeout test");
{
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "job:status:timeout:recoverable") != null);
}
try client.handleTraceLine("job_missing_tool build missing-zig");
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "job:status:missing_tool:missing-zig:no_install_attempted") != null);
}
test "regular: Space t jump opens selected diagnostic row and does not mix output" {
var fixture = try makeTuiJobFixture(std.testing.allocator);
defer {
std.testing.allocator.free(fixture.cwd);
fixture.tmp.cleanup();
}
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 88, .height = 7 }, std.testing.io);
defer client.deinit();
const run = try std.fmt.allocPrint(std.testing.allocator, "job_run {s} sh fail.sh", .{fixture.cwd});
defer std.testing.allocator.free(run);
try client.handleTraceLine(run);
try client.handleTraceLine("list_filter src/main.zig");
try client.handleInput(" ");
try client.handleInput("t");
try client.handleInput("j");
const snap = try client.session.snapshot();
try std.testing.expectEqualStrings("one\nabcdTARGET\n", snap.bytes);
try std.testing.expectEqual(@as(usize, 8), snap.cursor_byte);
}