Add explicit job output panel
This commit is contained in:
+125
@@ -1,5 +1,6 @@
|
||||
const std = @import("std");
|
||||
const input = @import("input.zig");
|
||||
const job_mod = @import("job.zig");
|
||||
const leader_mod = @import("leader.zig");
|
||||
const protocol = @import("protocol.zig");
|
||||
const replay = @import("replay.zig");
|
||||
@@ -137,6 +138,10 @@ pub const Client = struct {
|
||||
if (std.mem.startsWith(u8, line, "git_status ")) return self.openGitStatus(line[11..]);
|
||||
if (std.mem.startsWith(u8, line, "git_diff_selected ")) return self.openSelectedGitDiff(line[18..]);
|
||||
if (std.mem.startsWith(u8, line, "git_open_changed_selected ")) return self.openSelectedGitFile(line[26..]);
|
||||
if (std.mem.startsWith(u8, line, "job_run ")) return self.openJobRun(line[8..]);
|
||||
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.startsWith(u8, line, "panel_open ")) return self.applyProtocolCommand(line);
|
||||
if (std.mem.startsWith(u8, line, "list_open ")) return self.applyProtocolCommand(line);
|
||||
if (std.mem.startsWith(u8, line, "list_filter ")) return self.applyProtocolCommand(line);
|
||||
@@ -367,6 +372,42 @@ pub const Client = struct {
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn openJobRun(self: *Client, cwd_and_command: []const u8) !void {
|
||||
const io = self.io orelse return Error.ProtocolRejected;
|
||||
const first_space = std.mem.indexOfScalar(u8, cwd_and_command, ' ') orelse return Error.ProtocolRejected;
|
||||
const cwd = cwd_and_command[0..first_space];
|
||||
const command_line = std.mem.trim(u8, cwd_and_command[first_space + 1 ..], " ");
|
||||
if (command_line.len == 0) return Error.ProtocolRejected;
|
||||
var argv = std.ArrayList([]const u8).empty;
|
||||
defer argv.deinit(self.allocator);
|
||||
var parts = std.mem.splitScalar(u8, command_line, ' ');
|
||||
while (parts.next()) |part| {
|
||||
if (part.len == 0) continue;
|
||||
try argv.append(self.allocator, part);
|
||||
}
|
||||
const rows = job_mod.runRowsAlloc(self.allocator, io, cwd, argv.items) catch return Error.ProtocolRejected;
|
||||
defer freeOwnedRows(self.allocator, rows);
|
||||
try self.session.openListPanel("job-output", rows);
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn openStaticJobRow(self: *Client, title: []const u8, row: []const u8) !void {
|
||||
try self.session.openListPanel(title, &.{row});
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
fn handleTraceKey(self: *Client, key_name: []const u8) !void {
|
||||
if (std.mem.eql(u8, key_name, "space")) return self.handleInput(" ");
|
||||
if (std.mem.eql(u8, key_name, "enter")) return self.handleInput("\r");
|
||||
@@ -1254,3 +1295,87 @@ test "adversarial: git status errors are visible in panel" {
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "git_error:") != null);
|
||||
}
|
||||
|
||||
fn makeTuiJobFixture(allocator: std.mem.Allocator) !struct { tmp: std.testing.TmpDir, cwd: []u8 } {
|
||||
var tmp = std.testing.tmpDir(.{});
|
||||
errdefer tmp.cleanup();
|
||||
var src = try tmp.dir.createDirPathOpen(std.testing.io, "src", .{});
|
||||
src.close(std.testing.io);
|
||||
try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "src/main.zig", .data = "one\nabcdTARGET\n" });
|
||||
try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "fail.sh", .data = "printf 'src/main.zig:2:5:error:bad\\n'\nexit 1\n" });
|
||||
const cwd = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}", .{&tmp.sub_path});
|
||||
errdefer allocator.free(cwd);
|
||||
return .{ .tmp = tmp, .cwd = cwd };
|
||||
}
|
||||
|
||||
test "regular: job panel captures failing command output and jumps to file line" {
|
||||
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 = 72, .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);
|
||||
{
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "job-output") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "job:status:exit_1") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "stdout:src/main.zig:2:5:error:bad") != null);
|
||||
}
|
||||
|
||||
try client.handleTraceLine("list_filter src/main.zig");
|
||||
const open = try std.fmt.allocPrint(std.testing.allocator, "job_open_selected {s}", .{fixture.cwd});
|
||||
defer std.testing.allocator.free(open);
|
||||
try client.handleTraceLine(open);
|
||||
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);
|
||||
}
|
||||
|
||||
test "regular: job status and cancel expose honest foreground state rows" {
|
||||
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 56, .height = 5 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("job_status");
|
||||
{
|
||||
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:idle") != null);
|
||||
}
|
||||
|
||||
try client.handleTraceLine("job_cancel");
|
||||
{
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "job_cancel:no_running_job") != null);
|
||||
}
|
||||
}
|
||||
|
||||
test "adversarial: bad job command is visible and failed job open does not corrupt buffer" {
|
||||
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 = 72, .height = 6 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("open safe");
|
||||
const run = try std.fmt.allocPrint(std.testing.allocator, "job_run {s} definitely-not-a-mim-command", .{fixture.cwd});
|
||||
defer std.testing.allocator.free(run);
|
||||
try client.handleTraceLine(run);
|
||||
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:spawn_error_") != null);
|
||||
|
||||
const open = try std.fmt.allocPrint(std.testing.allocator, "job_open_selected {s}", .{fixture.cwd});
|
||||
defer std.testing.allocator.free(open);
|
||||
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine(open));
|
||||
const snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("safe", snap.bytes);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user