Add read-only git workbench panels
This commit is contained in:
+136
@@ -85,18 +85,24 @@ pub const Client = struct {
|
||||
session: session_mod.Session,
|
||||
leader: leader_mod.Leader,
|
||||
repo: repo_mod.Index,
|
||||
io: ?std.Io,
|
||||
viewport: Viewport,
|
||||
saved_bytes: ?[]u8 = null,
|
||||
message: ?[]const u8 = null,
|
||||
quit: bool = false,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, viewport: Viewport) !Client {
|
||||
return initWithIo(allocator, viewport, null);
|
||||
}
|
||||
|
||||
pub fn initWithIo(allocator: std.mem.Allocator, viewport: Viewport, io: ?std.Io) !Client {
|
||||
try viewport.validate();
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.session = session_mod.Session.init(allocator),
|
||||
.leader = leader_mod.Leader.init(allocator),
|
||||
.repo = repo_mod.Index.init(allocator),
|
||||
.io = io,
|
||||
.viewport = viewport,
|
||||
};
|
||||
}
|
||||
@@ -128,6 +134,9 @@ pub const Client = struct {
|
||||
if (std.mem.startsWith(u8, line, "search_text all ")) return self.openSearchList(line[16..], true);
|
||||
if (std.mem.startsWith(u8, line, "search_text ")) return self.openSearchList(line[12..], false);
|
||||
if (std.mem.eql(u8, line, "search_open_selected")) return self.openSelectedSearchResult();
|
||||
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, "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);
|
||||
@@ -329,6 +338,35 @@ pub const Client = struct {
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
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;
|
||||
defer freeOwnedRows(self.allocator, rows);
|
||||
try self.session.openListPanel("git-status", rows);
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn openSelectedGitDiff(self: *Client, cwd: []const u8) !void {
|
||||
const io = self.io orelse return Error.ProtocolRejected;
|
||||
const selected = self.session.selectListPanel() catch return Error.ProtocolRejected;
|
||||
const path = repo_mod.gitPathFromStatusRow(selected) catch return Error.ProtocolRejected;
|
||||
const rows = repo_mod.gitDiffRowsAlloc(self.allocator, io, cwd, path) catch return Error.ProtocolRejected;
|
||||
defer freeOwnedRows(self.allocator, rows);
|
||||
try self.session.openListPanel("git-diff", rows);
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn openSelectedGitFile(self: *Client, cwd: []const u8) !void {
|
||||
const io = self.io orelse return Error.ProtocolRejected;
|
||||
const selected = self.session.selectListPanel() catch return Error.ProtocolRejected;
|
||||
const path = repo_mod.gitPathFromStatusRow(selected) catch return Error.ProtocolRejected;
|
||||
const content = repo_mod.readRepoFileAlloc(self.allocator, io, cwd, path) catch return Error.ProtocolRejected;
|
||||
defer self.allocator.free(content);
|
||||
try self.session.openFixture(content);
|
||||
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");
|
||||
@@ -552,6 +590,11 @@ fn assertLinesFit(frame: []const u8, width: usize) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn freeOwnedRows(allocator: std.mem.Allocator, rows: []const []const u8) void {
|
||||
for (rows) |row| allocator.free(row);
|
||||
allocator.free(rows);
|
||||
}
|
||||
|
||||
test "regular: scripted narrow terminal trace edits saves exits and replays saved bytes" {
|
||||
const trace =
|
||||
\\open abc
|
||||
@@ -1118,3 +1161,96 @@ test "adversarial: project text search failures do not corrupt current buffer" {
|
||||
const snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("safe", snap.bytes);
|
||||
}
|
||||
|
||||
fn makeTuiGitFixture(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 = "const old = 1;\n" });
|
||||
const cwd = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}", .{&tmp.sub_path});
|
||||
errdefer allocator.free(cwd);
|
||||
try runGitTuiTest(allocator, cwd, &.{ "init", "--quiet" });
|
||||
try runGitTuiTest(allocator, cwd, &.{ "add", "src/main.zig" });
|
||||
try runGitTuiTest(allocator, cwd, &.{ "-c", "user.email=test@example.invalid", "-c", "user.name=Test", "commit", "--quiet", "-m", "init" });
|
||||
try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "src/main.zig", .data = "const new = 2;\n" });
|
||||
return .{ .tmp = tmp, .cwd = cwd };
|
||||
}
|
||||
|
||||
fn runGitTuiTest(allocator: std.mem.Allocator, cwd: []const u8, args: []const []const u8) !void {
|
||||
var argv = try allocator.alloc([]const u8, args.len + 3);
|
||||
defer allocator.free(argv);
|
||||
argv[0] = "git";
|
||||
argv[1] = "-C";
|
||||
argv[2] = cwd;
|
||||
@memcpy(argv[3..], args);
|
||||
const result = try std.process.run(allocator, std.testing.io, .{
|
||||
.argv = argv,
|
||||
.stdout_limit = .limited(64 * 1024),
|
||||
.stderr_limit = .limited(64 * 1024),
|
||||
});
|
||||
defer allocator.free(result.stdout);
|
||||
defer allocator.free(result.stderr);
|
||||
try std.testing.expect(switch (result.term) {
|
||||
.exited => |code| code == 0,
|
||||
else => false,
|
||||
});
|
||||
}
|
||||
|
||||
test "regular: git status panel opens changed file into editor" {
|
||||
var fixture = try makeTuiGitFixture(std.testing.allocator);
|
||||
defer {
|
||||
std.testing.allocator.free(fixture.cwd);
|
||||
fixture.tmp.cleanup();
|
||||
}
|
||||
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 64, .height = 6 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
const status = try std.fmt.allocPrint(std.testing.allocator, "git_status {s}", .{fixture.cwd});
|
||||
defer std.testing.allocator.free(status);
|
||||
try client.handleTraceLine(status);
|
||||
{
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "git-status") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "modified:src/main.zig") != null);
|
||||
}
|
||||
|
||||
const open_changed = try std.fmt.allocPrint(std.testing.allocator, "git_open_changed_selected {s}", .{fixture.cwd});
|
||||
defer std.testing.allocator.free(open_changed);
|
||||
try client.handleTraceLine(open_changed);
|
||||
const snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("const new = 2;\n", snap.bytes);
|
||||
}
|
||||
|
||||
test "regular: git diff panel renders hunks for selected changed file" {
|
||||
var fixture = try makeTuiGitFixture(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 status = try std.fmt.allocPrint(std.testing.allocator, "git_status {s}", .{fixture.cwd});
|
||||
defer std.testing.allocator.free(status);
|
||||
try client.handleTraceLine(status);
|
||||
const diff = try std.fmt.allocPrint(std.testing.allocator, "git_diff_selected {s}", .{fixture.cwd});
|
||||
defer std.testing.allocator.free(diff);
|
||||
try client.handleTraceLine(diff);
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "git-diff") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "hunk:@@") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "add:const_new_=_2;") != null);
|
||||
}
|
||||
|
||||
test "adversarial: git status errors are visible in panel" {
|
||||
var client = try Client.initWithIo(std.testing.allocator, .{ .width = 64, .height = 5 }, std.testing.io);
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("git_status /definitely/not/a/mim/repo");
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "git_error:") != null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user