Add generic list panel primitive

This commit is contained in:
slhx agent
2026-06-21 02:45:16 +02:00
parent ffa51e17c7
commit c0a4d1c9bb
6 changed files with 498 additions and 23 deletions
+119 -11
View File
@@ -31,6 +31,12 @@ fn commandResponse(allocator: std.mem.Allocator, session: *session_mod.Session,
if (std.mem.eql(u8, command, "delete_backward")) return dispatchAndRespond(allocator, session, .delete_backward);
if (std.mem.startsWith(u8, command, "insert ")) return dispatchAndRespond(allocator, session, .{ .insert = command[7..] });
if (std.mem.startsWith(u8, command, "panel_open ")) return panelRespond(allocator, session, .{ .open = command[11..] });
if (std.mem.startsWith(u8, command, "list_open ")) return listOpenRespond(allocator, session, command[10..]);
if (std.mem.startsWith(u8, command, "list_filter ")) return listRespond(allocator, session, .{ .filter = command[12..] });
if (std.mem.eql(u8, command, "list_down")) return listRespond(allocator, session, .down);
if (std.mem.eql(u8, command, "list_up")) return listRespond(allocator, session, .up);
if (std.mem.eql(u8, command, "list_select")) return listRespond(allocator, session, .select);
if (std.mem.eql(u8, command, "list_cancel")) return listRespond(allocator, session, .cancel);
if (std.mem.eql(u8, command, "panel_close")) return panelRespond(allocator, session, .close);
if (std.mem.eql(u8, command, "panel_next")) return panelRespond(allocator, session, .next);
if (std.mem.eql(u8, command, "panel_prev")) return panelRespond(allocator, session, .previous);
@@ -89,13 +95,61 @@ fn panelRespond(allocator: std.mem.Allocator, session: *session_mod.Session, com
return stateResponse(allocator, session);
}
const ListCommand = union(enum) {
filter: []const u8,
down,
up,
select,
cancel,
};
fn listOpenRespond(allocator: std.mem.Allocator, session: *session_mod.Session, payload: []const u8) ![]u8 {
const split = std.mem.indexOfScalar(u8, payload, ' ') orelse return allocator.dupe(u8, "err invalid list open\n");
const title = payload[0..split];
const items_payload = payload[split + 1 ..];
var items = std.ArrayList([]const u8).empty;
defer items.deinit(allocator);
var iter = std.mem.splitScalar(u8, items_payload, '|');
while (iter.next()) |item| try items.append(allocator, item);
session.openListPanel(title, items.items) catch |err| switch (err) {
error.InvalidPanelTitle => return allocator.dupe(u8, "err invalid panel title\n"),
error.InvalidListItem => return allocator.dupe(u8, "err invalid list item\n"),
error.EmptyList => return allocator.dupe(u8, "err empty list\n"),
else => return err,
};
return stateResponse(allocator, session);
}
fn listRespond(allocator: std.mem.Allocator, session: *session_mod.Session, command: ListCommand) ![]u8 {
switch (command) {
.filter => |filter| session.filterListPanel(filter) catch |err| return listError(allocator, err),
.down => session.listPanelDown() catch |err| return listError(allocator, err),
.up => session.listPanelUp() catch |err| return listError(allocator, err),
.select => _ = session.selectListPanel() catch |err| return listError(allocator, err),
.cancel => session.cancelListPanel() catch |err| return listError(allocator, err),
}
return stateResponse(allocator, session);
}
fn listError(allocator: std.mem.Allocator, err: anyerror) ![]u8 {
return switch (err) {
error.NoPanelOpen => allocator.dupe(u8, "err no panel open\n"),
error.ActivePanelIsNotList => allocator.dupe(u8, "err active panel is not list\n"),
error.InvalidListFilter => allocator.dupe(u8, "err invalid list filter\n"),
error.EmptyList => allocator.dupe(u8, "err empty list\n"),
else => err,
};
}
fn stateResponse(allocator: std.mem.Allocator, session: *session_mod.Session) ![]u8 {
const snap = try session.snapshot();
const panel_path = try session.panelPathAlloc(allocator);
defer allocator.free(panel_path);
const panel_summary = try session.panelSummaryAlloc(allocator);
defer allocator.free(panel_summary);
return std.fmt.allocPrint(
allocator,
"ok state cursor_byte={d} cursor_cell={d} bytes_len={d} panel_depth={d} active_panel={s} panel_path={s}\n",
"ok state cursor_byte={d} cursor_cell={d} bytes_len={d} panel_depth={d} active_panel={s} panel_path={s} panel_summary={s}\n",
.{
snap.cursor_byte,
snap.cursor_cell,
@@ -103,6 +157,7 @@ fn stateResponse(allocator: std.mem.Allocator, session: *session_mod.Session) ![
snap.panel_depth,
if (snap.active_panel_title) |title| title else "-",
panel_path,
panel_summary,
},
);
}
@@ -113,15 +168,15 @@ test "regular: protocol opens bytes, reports state, and dispatches commands" {
const open = try handleLine(std.testing.allocator, &session, "open café\n");
defer std.testing.allocator.free(open);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=5 panel_depth=0 active_panel=- panel_path=-\n", open);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=5 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", open);
const moved = try handleLine(std.testing.allocator, &session, "command move_right\n");
defer std.testing.allocator.free(moved);
try std.testing.expectEqualStrings("ok state cursor_byte=1 cursor_cell=1 bytes_len=5 panel_depth=0 active_panel=- panel_path=-\n", moved);
try std.testing.expectEqualStrings("ok state cursor_byte=1 cursor_cell=1 bytes_len=5 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", moved);
const inserted = try handleLine(std.testing.allocator, &session, "command insert 🔥\n");
defer std.testing.allocator.free(inserted);
try std.testing.expectEqualStrings("ok state cursor_byte=5 cursor_cell=3 bytes_len=9 panel_depth=0 active_panel=- panel_path=-\n", inserted);
try std.testing.expectEqualStrings("ok state cursor_byte=5 cursor_cell=3 bytes_len=9 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", inserted);
}
test "regular: protocol delete command removes a whole UTF-8 codepoint" {
@@ -137,7 +192,7 @@ test "regular: protocol delete command removes a whole UTF-8 codepoint" {
response = try handleLine(std.testing.allocator, &session, "command delete_backward\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("ok state cursor_byte=1 cursor_cell=1 bytes_len=1 panel_depth=0 active_panel=- panel_path=-\n", response);
try std.testing.expectEqualStrings("ok state cursor_byte=1 cursor_cell=1 bytes_len=1 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response);
}
test "adversarial: protocol rejects unknown requests and commands" {
@@ -160,7 +215,7 @@ test "adversarial: protocol reports no buffer and invalid utf8 without corruptin
const no_buffer = try handleLine(std.testing.allocator, &session, "state\n");
defer std.testing.allocator.free(no_buffer);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=0 active_panel=- panel_path=-\n", no_buffer);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", no_buffer);
try session.openFixture("safe");
const before = try session.snapshot();
@@ -189,7 +244,7 @@ test "regular: protocol pair commands insert delimiters with cursor between them
response = try handleLine(std.testing.allocator, &session, "command pair parens\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("ok state cursor_byte=5 cursor_cell=5 bytes_len=6 panel_depth=0 active_panel=- panel_path=-\n", response);
try std.testing.expectEqualStrings("ok state cursor_byte=5 cursor_cell=5 bytes_len=6 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response);
try std.testing.expectEqualStrings("call()", (try session.snapshot()).bytes);
}
@@ -211,22 +266,22 @@ test "regular: protocol opens switches and closes generic panels" {
{
const response = try handleLine(std.testing.allocator, &session, "command panel_open files\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files]\n", response);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=empty\n", response);
}
{
const response = try handleLine(std.testing.allocator, &session, "command panel_open diagnostics\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=2 active_panel=diagnostics panel_path=files>[diagnostics]\n", response);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=2 active_panel=diagnostics panel_path=files>[diagnostics] panel_summary=empty\n", response);
}
{
const response = try handleLine(std.testing.allocator, &session, "command panel_prev\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=2 active_panel=files panel_path=[files]>diagnostics\n", response);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=2 active_panel=files panel_path=[files]>diagnostics panel_summary=empty\n", response);
}
{
const response = try handleLine(std.testing.allocator, &session, "command panel_close\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=diagnostics panel_path=[diagnostics]\n", response);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=diagnostics panel_path=[diagnostics] panel_summary=empty\n", response);
}
}
@@ -245,3 +300,56 @@ test "adversarial: protocol rejects invalid panel titles and empty panel actions
try std.testing.expectEqualStrings("err no panel open\n", response);
}
}
test "regular: protocol list panel filters moves selects and remains inspectable" {
var session = session_mod.Session.init(std.testing.allocator);
defer session.deinit();
{
const response = try handleLine(std.testing.allocator, &session, "command list_open files src/main.zig|src/panel.zig|README.md\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=-,cursor=0,visible=3,selected=-\n", response);
}
{
const response = try handleLine(std.testing.allocator, &session, "command list_filter src\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=src,cursor=0,visible=2,selected=-\n", response);
}
{
const response = try handleLine(std.testing.allocator, &session, "command list_down\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=src,cursor=1,visible=2,selected=-\n", response);
}
{
const response = try handleLine(std.testing.allocator, &session, "command list_select\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=src,cursor=1,visible=2,selected=src/panel.zig\n", response);
}
}
test "regular: protocol list cancel closes list panel" {
var session = session_mod.Session.init(std.testing.allocator);
defer session.deinit();
var response = try handleLine(std.testing.allocator, &session, "command list_open files a.zig|b.zig\n");
std.testing.allocator.free(response);
response = try handleLine(std.testing.allocator, &session, "command list_cancel\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response);
}
test "adversarial: protocol list errors are explicit" {
var session = session_mod.Session.init(std.testing.allocator);
defer session.deinit();
{
const response = try handleLine(std.testing.allocator, &session, "command list_open files bad item\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("err invalid list item\n", response);
}
{
const response = try handleLine(std.testing.allocator, &session, "command list_filter src\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("err no panel open\n", response);
}
}