Add generic list panel primitive
This commit is contained in:
+273
-4
@@ -1,6 +1,6 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
// Generic transient panel stack state, independent of rendering and concrete panel types.
|
// Generic transient panel stack state, independent of concrete product panels.
|
||||||
// req: ui/001, ui/002, session/003, testing/001, testing/002, testing/003, testing/004
|
// req: ui/001, ui/002, session/003, testing/001, testing/002, testing/003, testing/004
|
||||||
|
|
||||||
test {
|
test {
|
||||||
@@ -9,17 +9,54 @@ test {
|
|||||||
|
|
||||||
pub const Error = error{
|
pub const Error = error{
|
||||||
InvalidPanelTitle,
|
InvalidPanelTitle,
|
||||||
|
InvalidListItem,
|
||||||
|
InvalidListFilter,
|
||||||
NoPanelOpen,
|
NoPanelOpen,
|
||||||
|
ActivePanelIsNotList,
|
||||||
|
EmptyList,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const PanelKind = enum {
|
||||||
|
empty,
|
||||||
|
list,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ListItem = struct {
|
||||||
|
text: []u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ListState = struct {
|
||||||
|
items: std.ArrayList(ListItem) = .empty,
|
||||||
|
filter: std.ArrayList(u8) = .empty,
|
||||||
|
cursor: usize = 0,
|
||||||
|
selected: ?[]u8 = null,
|
||||||
|
|
||||||
|
fn deinit(self: *ListState, allocator: std.mem.Allocator) void {
|
||||||
|
for (self.items.items) |item| allocator.free(item.text);
|
||||||
|
self.items.deinit(allocator);
|
||||||
|
self.filter.deinit(allocator);
|
||||||
|
if (self.selected) |selected| allocator.free(selected);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Panel = struct {
|
pub const Panel = struct {
|
||||||
title: []u8,
|
title: []u8,
|
||||||
|
kind: PanelKind = .empty,
|
||||||
|
list: ?ListState = null,
|
||||||
|
|
||||||
|
fn deinit(self: *Panel, allocator: std.mem.Allocator) void {
|
||||||
|
allocator.free(self.title);
|
||||||
|
if (self.list) |*list| list.deinit(allocator);
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const State = struct {
|
pub const State = struct {
|
||||||
depth: usize,
|
depth: usize,
|
||||||
active_index: ?usize,
|
active_index: ?usize,
|
||||||
active_title: ?[]const u8,
|
active_title: ?[]const u8,
|
||||||
|
active_kind: ?PanelKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Stack = struct {
|
pub const Stack = struct {
|
||||||
@@ -32,7 +69,7 @@ pub const Stack = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Stack) void {
|
pub fn deinit(self: *Stack) void {
|
||||||
for (self.panels.items) |panel| self.allocator.free(panel.title);
|
for (self.panels.items) |*panel| panel.deinit(self.allocator);
|
||||||
self.panels.deinit(self.allocator);
|
self.panels.deinit(self.allocator);
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
}
|
}
|
||||||
@@ -45,10 +82,30 @@ pub const Stack = struct {
|
|||||||
self.active_index = self.panels.items.len - 1;
|
self.active_index = self.panels.items.len - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn openList(self: *Stack, title: []const u8, items: []const []const u8) !void {
|
||||||
|
try validateTitle(title);
|
||||||
|
if (items.len == 0) return Error.EmptyList;
|
||||||
|
|
||||||
|
var list = ListState{};
|
||||||
|
errdefer list.deinit(self.allocator);
|
||||||
|
for (items) |item| {
|
||||||
|
try validateListItem(item);
|
||||||
|
const owned_item = try self.allocator.dupe(u8, item);
|
||||||
|
errdefer self.allocator.free(owned_item);
|
||||||
|
try list.items.append(self.allocator, .{ .text = owned_item });
|
||||||
|
}
|
||||||
|
|
||||||
|
const owned_title = try self.allocator.dupe(u8, title);
|
||||||
|
errdefer self.allocator.free(owned_title);
|
||||||
|
try self.panels.append(self.allocator, .{ .title = owned_title, .kind = .list, .list = list });
|
||||||
|
self.active_index = self.panels.items.len - 1;
|
||||||
|
self.clampListCursor();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn closeActive(self: *Stack) !void {
|
pub fn closeActive(self: *Stack) !void {
|
||||||
const index = self.active_index orelse return Error.NoPanelOpen;
|
const index = self.active_index orelse return Error.NoPanelOpen;
|
||||||
const removed = self.panels.orderedRemove(index);
|
var removed = self.panels.orderedRemove(index);
|
||||||
self.allocator.free(removed.title);
|
removed.deinit(self.allocator);
|
||||||
if (self.panels.items.len == 0) {
|
if (self.panels.items.len == 0) {
|
||||||
self.active_index = null;
|
self.active_index = null;
|
||||||
} else if (index >= self.panels.items.len) {
|
} else if (index >= self.panels.items.len) {
|
||||||
@@ -68,12 +125,48 @@ pub const Stack = struct {
|
|||||||
self.active_index = if (index == 0) self.panels.items.len - 1 else index - 1;
|
self.active_index = if (index == 0) self.panels.items.len - 1 else index - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn filterList(self: *Stack, filter: []const u8) !void {
|
||||||
|
try validateFilter(filter);
|
||||||
|
var list = try self.activeList();
|
||||||
|
list.filter.clearRetainingCapacity();
|
||||||
|
try list.filter.appendSlice(self.allocator, filter);
|
||||||
|
list.cursor = 0;
|
||||||
|
self.clampListCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listDown(self: *Stack) !void {
|
||||||
|
var list = try self.activeList();
|
||||||
|
const visible = self.visibleCount() catch 0;
|
||||||
|
if (visible == 0) return;
|
||||||
|
if (list.cursor + 1 < visible) list.cursor += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listUp(self: *Stack) !void {
|
||||||
|
var list = try self.activeList();
|
||||||
|
if (list.cursor > 0) list.cursor -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selectList(self: *Stack) ![]const u8 {
|
||||||
|
var list = try self.activeList();
|
||||||
|
const selected = self.visibleItemAt(list.cursor) orelse return Error.EmptyList;
|
||||||
|
const owned = try self.allocator.dupe(u8, selected);
|
||||||
|
if (list.selected) |old| self.allocator.free(old);
|
||||||
|
list.selected = owned;
|
||||||
|
return list.selected.?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancelList(self: *Stack) !void {
|
||||||
|
_ = try self.activeList();
|
||||||
|
try self.closeActive();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn state(self: *const Stack) State {
|
pub fn state(self: *const Stack) State {
|
||||||
const index = self.active_index;
|
const index = self.active_index;
|
||||||
return .{
|
return .{
|
||||||
.depth = self.panels.items.len,
|
.depth = self.panels.items.len,
|
||||||
.active_index = index,
|
.active_index = index,
|
||||||
.active_title = if (index) |i| self.panels.items[i].title else null,
|
.active_title = if (index) |i| self.panels.items[i].title else null,
|
||||||
|
.active_kind = if (index) |i| self.panels.items[i].kind else null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +186,100 @@ pub const Stack = struct {
|
|||||||
}
|
}
|
||||||
return out.toOwnedSlice(allocator);
|
return out.toOwnedSlice(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn summaryAlloc(self: *const Stack, allocator: std.mem.Allocator) ![]u8 {
|
||||||
|
const index = self.active_index orelse return allocator.dupe(u8, "-");
|
||||||
|
const panel = &self.panels.items[index];
|
||||||
|
switch (panel.kind) {
|
||||||
|
.empty => return allocator.dupe(u8, "empty"),
|
||||||
|
.list => {
|
||||||
|
const list = &panel.list.?;
|
||||||
|
const visible = try self.visibleCount();
|
||||||
|
return std.fmt.allocPrint(
|
||||||
|
allocator,
|
||||||
|
"list:filter={s},cursor={d},visible={d},selected={s}",
|
||||||
|
.{
|
||||||
|
if (list.filter.items.len == 0) "-" else list.filter.items,
|
||||||
|
list.cursor,
|
||||||
|
visible,
|
||||||
|
if (list.selected) |selected| selected else "-",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activeListRowsAlloc(self: *const Stack, allocator: std.mem.Allocator, max_rows: usize) ![][]u8 {
|
||||||
|
const index = self.active_index orelse return Error.NoPanelOpen;
|
||||||
|
const panel = &self.panels.items[index];
|
||||||
|
if (panel.kind != .list) return Error.ActivePanelIsNotList;
|
||||||
|
const list = &panel.list.?;
|
||||||
|
|
||||||
|
var rows = std.ArrayList([]u8).empty;
|
||||||
|
errdefer {
|
||||||
|
for (rows.items) |row| allocator.free(row);
|
||||||
|
rows.deinit(allocator);
|
||||||
|
}
|
||||||
|
var visible_index: usize = 0;
|
||||||
|
for (list.items.items) |item| {
|
||||||
|
if (!matchesFilter(item.text, list.filter.items)) continue;
|
||||||
|
if (rows.items.len >= max_rows) break;
|
||||||
|
const marker: []const u8 = if (visible_index == list.cursor) "> " else " ";
|
||||||
|
const row = try std.fmt.allocPrint(allocator, "{s}{s}", .{ marker, item.text });
|
||||||
|
try rows.append(allocator, row);
|
||||||
|
visible_index += 1;
|
||||||
|
}
|
||||||
|
if (rows.items.len == 0 and max_rows > 0) {
|
||||||
|
try rows.append(allocator, try allocator.dupe(u8, " no matches"));
|
||||||
|
}
|
||||||
|
return rows.toOwnedSlice(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn activeList(self: *Stack) !*ListState {
|
||||||
|
const index = self.active_index orelse return Error.NoPanelOpen;
|
||||||
|
const panel = &self.panels.items[index];
|
||||||
|
if (panel.kind != .list) return Error.ActivePanelIsNotList;
|
||||||
|
return &panel.list.?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visibleItemAt(self: *const Stack, wanted_index: usize) ?[]const u8 {
|
||||||
|
const index = self.active_index orelse return null;
|
||||||
|
const panel = &self.panels.items[index];
|
||||||
|
if (panel.kind != .list) return null;
|
||||||
|
const list = &panel.list.?;
|
||||||
|
var visible_index: usize = 0;
|
||||||
|
for (list.items.items) |item| {
|
||||||
|
if (!matchesFilter(item.text, list.filter.items)) continue;
|
||||||
|
if (visible_index == wanted_index) return item.text;
|
||||||
|
visible_index += 1;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visibleCount(self: *const Stack) !usize {
|
||||||
|
const index = self.active_index orelse return Error.NoPanelOpen;
|
||||||
|
const panel = &self.panels.items[index];
|
||||||
|
if (panel.kind != .list) return Error.ActivePanelIsNotList;
|
||||||
|
const list = &panel.list.?;
|
||||||
|
var count: usize = 0;
|
||||||
|
for (list.items.items) |item| {
|
||||||
|
if (matchesFilter(item.text, list.filter.items)) count += 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clampListCursor(self: *Stack) void {
|
||||||
|
const index = self.active_index orelse return;
|
||||||
|
const panel = &self.panels.items[index];
|
||||||
|
if (panel.kind != .list) return;
|
||||||
|
const list = &panel.list.?;
|
||||||
|
const visible = self.visibleCount() catch 0;
|
||||||
|
if (visible == 0) {
|
||||||
|
list.cursor = 0;
|
||||||
|
} else if (list.cursor >= visible) {
|
||||||
|
list.cursor = visible - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fn validateTitle(title: []const u8) !void {
|
fn validateTitle(title: []const u8) !void {
|
||||||
@@ -103,6 +290,25 @@ fn validateTitle(title: []const u8) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validateListItem(item: []const u8) !void {
|
||||||
|
if (item.len == 0) return Error.InvalidListItem;
|
||||||
|
if (!std.unicode.utf8ValidateSlice(item)) return Error.InvalidListItem;
|
||||||
|
for (item) |byte| {
|
||||||
|
if (byte <= 0x20 or byte == '|') return Error.InvalidListItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validateFilter(filter: []const u8) !void {
|
||||||
|
if (!std.unicode.utf8ValidateSlice(filter)) return Error.InvalidListFilter;
|
||||||
|
for (filter) |byte| {
|
||||||
|
if (byte <= 0x20 or byte == '|') return Error.InvalidListFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matchesFilter(item: []const u8, filter: []const u8) bool {
|
||||||
|
return filter.len == 0 or std.mem.indexOf(u8, item, filter) != null;
|
||||||
|
}
|
||||||
|
|
||||||
test "regular: opens nested panels and tracks active path" {
|
test "regular: opens nested panels and tracks active path" {
|
||||||
var stack = Stack.init(std.testing.allocator);
|
var stack = Stack.init(std.testing.allocator);
|
||||||
defer stack.deinit();
|
defer stack.deinit();
|
||||||
@@ -139,6 +345,55 @@ test "regular: next previous and close preserve stack order" {
|
|||||||
try std.testing.expectEqualStrings("files>[git]", path);
|
try std.testing.expectEqualStrings("files>[git]", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "regular: list panel filters moves and selects" {
|
||||||
|
var stack = Stack.init(std.testing.allocator);
|
||||||
|
defer stack.deinit();
|
||||||
|
|
||||||
|
try stack.openList("files", &.{ "src/main.zig", "src/panel.zig", "README.md" });
|
||||||
|
try stack.filterList("src");
|
||||||
|
try stack.listDown();
|
||||||
|
const selected = try stack.selectList();
|
||||||
|
try std.testing.expectEqualStrings("src/panel.zig", selected);
|
||||||
|
|
||||||
|
const summary = try stack.summaryAlloc(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(summary);
|
||||||
|
try std.testing.expectEqualStrings("list:filter=src,cursor=1,visible=2,selected=src/panel.zig", summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: active list rows expose cursor and empty match state" {
|
||||||
|
var stack = Stack.init(std.testing.allocator);
|
||||||
|
defer stack.deinit();
|
||||||
|
|
||||||
|
try stack.openList("files", &.{ "a.zig", "b.zig" });
|
||||||
|
try stack.listDown();
|
||||||
|
const rows = try stack.activeListRowsAlloc(std.testing.allocator, 4);
|
||||||
|
defer {
|
||||||
|
for (rows) |row| std.testing.allocator.free(row);
|
||||||
|
std.testing.allocator.free(rows);
|
||||||
|
}
|
||||||
|
try std.testing.expectEqualStrings(" a.zig", rows[0]);
|
||||||
|
try std.testing.expectEqualStrings("> b.zig", rows[1]);
|
||||||
|
|
||||||
|
try stack.filterList("none");
|
||||||
|
const no_match_rows = try stack.activeListRowsAlloc(std.testing.allocator, 4);
|
||||||
|
defer {
|
||||||
|
for (no_match_rows) |row| std.testing.allocator.free(row);
|
||||||
|
std.testing.allocator.free(no_match_rows);
|
||||||
|
}
|
||||||
|
try std.testing.expectEqualStrings(" no matches", no_match_rows[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: list cancel closes the active list panel" {
|
||||||
|
var stack = Stack.init(std.testing.allocator);
|
||||||
|
defer stack.deinit();
|
||||||
|
|
||||||
|
try stack.open("root");
|
||||||
|
try stack.openList("files", &.{"a.zig"});
|
||||||
|
try stack.cancelList();
|
||||||
|
try std.testing.expectEqual(@as(usize, 1), stack.state().depth);
|
||||||
|
try std.testing.expectEqualStrings("root", stack.state().active_title.?);
|
||||||
|
}
|
||||||
|
|
||||||
test "adversarial: empty stack operations fail clearly" {
|
test "adversarial: empty stack operations fail clearly" {
|
||||||
var stack = Stack.init(std.testing.allocator);
|
var stack = Stack.init(std.testing.allocator);
|
||||||
defer stack.deinit();
|
defer stack.deinit();
|
||||||
@@ -163,3 +418,17 @@ test "adversarial: invalid panel titles are rejected" {
|
|||||||
try std.testing.expectError(Error.InvalidPanelTitle, stack.open(&bad_utf8));
|
try std.testing.expectError(Error.InvalidPanelTitle, stack.open(&bad_utf8));
|
||||||
try std.testing.expectEqual(@as(usize, 0), stack.state().depth);
|
try std.testing.expectEqual(@as(usize, 0), stack.state().depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "adversarial: list rejects invalid items filters and non-list operations" {
|
||||||
|
var stack = Stack.init(std.testing.allocator);
|
||||||
|
defer stack.deinit();
|
||||||
|
|
||||||
|
try std.testing.expectError(Error.EmptyList, stack.openList("files", &.{}));
|
||||||
|
try std.testing.expectError(Error.InvalidListItem, stack.openList("files", &.{"bad item"}));
|
||||||
|
try stack.open("plain");
|
||||||
|
try std.testing.expectError(Error.ActivePanelIsNotList, stack.filterList("src"));
|
||||||
|
try std.testing.expectError(Error.ActivePanelIsNotList, stack.listDown());
|
||||||
|
try stack.closeActive();
|
||||||
|
try stack.openList("files", &.{"a.zig"});
|
||||||
|
try std.testing.expectError(Error.InvalidListFilter, stack.filterList("bad filter"));
|
||||||
|
}
|
||||||
|
|||||||
+119
-11
@@ -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.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, "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, "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_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_next")) return panelRespond(allocator, session, .next);
|
||||||
if (std.mem.eql(u8, command, "panel_prev")) return panelRespond(allocator, session, .previous);
|
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);
|
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 {
|
fn stateResponse(allocator: std.mem.Allocator, session: *session_mod.Session) ![]u8 {
|
||||||
const snap = try session.snapshot();
|
const snap = try session.snapshot();
|
||||||
const panel_path = try session.panelPathAlloc(allocator);
|
const panel_path = try session.panelPathAlloc(allocator);
|
||||||
defer allocator.free(panel_path);
|
defer allocator.free(panel_path);
|
||||||
|
const panel_summary = try session.panelSummaryAlloc(allocator);
|
||||||
|
defer allocator.free(panel_summary);
|
||||||
return std.fmt.allocPrint(
|
return std.fmt.allocPrint(
|
||||||
allocator,
|
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_byte,
|
||||||
snap.cursor_cell,
|
snap.cursor_cell,
|
||||||
@@ -103,6 +157,7 @@ fn stateResponse(allocator: std.mem.Allocator, session: *session_mod.Session) ![
|
|||||||
snap.panel_depth,
|
snap.panel_depth,
|
||||||
if (snap.active_panel_title) |title| title else "-",
|
if (snap.active_panel_title) |title| title else "-",
|
||||||
panel_path,
|
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");
|
const open = try handleLine(std.testing.allocator, &session, "open café\n");
|
||||||
defer std.testing.allocator.free(open);
|
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");
|
const moved = try handleLine(std.testing.allocator, &session, "command move_right\n");
|
||||||
defer std.testing.allocator.free(moved);
|
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");
|
const inserted = try handleLine(std.testing.allocator, &session, "command insert 🔥\n");
|
||||||
defer std.testing.allocator.free(inserted);
|
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" {
|
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");
|
response = try handleLine(std.testing.allocator, &session, "command delete_backward\n");
|
||||||
defer std.testing.allocator.free(response);
|
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" {
|
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");
|
const no_buffer = try handleLine(std.testing.allocator, &session, "state\n");
|
||||||
defer std.testing.allocator.free(no_buffer);
|
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");
|
try session.openFixture("safe");
|
||||||
const before = try session.snapshot();
|
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");
|
response = try handleLine(std.testing.allocator, &session, "command pair parens\n");
|
||||||
defer std.testing.allocator.free(response);
|
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);
|
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");
|
const response = try handleLine(std.testing.allocator, &session, "command panel_open files\n");
|
||||||
defer std.testing.allocator.free(response);
|
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");
|
const response = try handleLine(std.testing.allocator, &session, "command panel_open diagnostics\n");
|
||||||
defer std.testing.allocator.free(response);
|
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");
|
const response = try handleLine(std.testing.allocator, &session, "command panel_prev\n");
|
||||||
defer std.testing.allocator.free(response);
|
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");
|
const response = try handleLine(std.testing.allocator, &session, "command panel_close\n");
|
||||||
defer std.testing.allocator.free(response);
|
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);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+3
-3
@@ -183,12 +183,12 @@ test "regular: golden open move edit save recording replays deterministically" {
|
|||||||
defer recorder.deinit();
|
defer recorder.deinit();
|
||||||
|
|
||||||
try recorder.protocolLine("open let café = 1");
|
try recorder.protocolLine("open let café = 1");
|
||||||
try recorder.expectResponse("ok state cursor_byte=0 cursor_cell=0 bytes_len=13 panel_depth=0 active_panel=- panel_path=-");
|
try recorder.expectResponse("ok state cursor_byte=0 cursor_cell=0 bytes_len=13 panel_depth=0 active_panel=- panel_path=- panel_summary=-");
|
||||||
try recorder.protocolLine("command move_right");
|
try recorder.protocolLine("command move_right");
|
||||||
try recorder.protocolLine("command move_right");
|
try recorder.protocolLine("command move_right");
|
||||||
try recorder.protocolLine("command move_right");
|
try recorder.protocolLine("command move_right");
|
||||||
try recorder.inputInsert("🔥");
|
try recorder.inputInsert("🔥");
|
||||||
try recorder.expectResponse("ok state cursor_byte=7 cursor_cell=5 bytes_len=17 panel_depth=0 active_panel=- panel_path=-");
|
try recorder.expectResponse("ok state cursor_byte=7 cursor_cell=5 bytes_len=17 panel_depth=0 active_panel=- panel_path=- panel_summary=-");
|
||||||
try recorder.saveCheckpoint("let🔥 café = 1");
|
try recorder.saveCheckpoint("let🔥 café = 1");
|
||||||
|
|
||||||
const result = try replayText(std.testing.allocator, recorder.text());
|
const result = try replayText(std.testing.allocator, recorder.text());
|
||||||
@@ -201,7 +201,7 @@ test "regular: replay can be rerun with identical result" {
|
|||||||
\\protocol open abc
|
\\protocol open abc
|
||||||
\\protocol command move_right
|
\\protocol command move_right
|
||||||
\\input insert é
|
\\input insert é
|
||||||
\\expect-response ok state cursor_byte=3 cursor_cell=2 bytes_len=5 panel_depth=0 active_panel=- panel_path=-
|
\\expect-response ok state cursor_byte=3 cursor_cell=2 bytes_len=5 panel_depth=0 active_panel=- panel_path=- panel_summary=-
|
||||||
\\save aébc
|
\\save aébc
|
||||||
\\
|
\\
|
||||||
;
|
;
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ pub const Session = struct {
|
|||||||
try self.panels.open(title);
|
try self.panels.open(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn openListPanel(self: *Session, title: []const u8, items: []const []const u8) !void {
|
||||||
|
try self.panels.openList(title, items);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn closePanel(self: *Session) !void {
|
pub fn closePanel(self: *Session) !void {
|
||||||
try self.panels.closeActive();
|
try self.panels.closeActive();
|
||||||
}
|
}
|
||||||
@@ -163,10 +167,38 @@ pub const Session = struct {
|
|||||||
try self.panels.previous();
|
try self.panels.previous();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn filterListPanel(self: *Session, filter: []const u8) !void {
|
||||||
|
try self.panels.filterList(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listPanelDown(self: *Session) !void {
|
||||||
|
try self.panels.listDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listPanelUp(self: *Session) !void {
|
||||||
|
try self.panels.listUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selectListPanel(self: *Session) ![]const u8 {
|
||||||
|
return self.panels.selectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancelListPanel(self: *Session) !void {
|
||||||
|
try self.panels.cancelList();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn panelPathAlloc(self: *const Session, allocator: std.mem.Allocator) ![]u8 {
|
pub fn panelPathAlloc(self: *const Session, allocator: std.mem.Allocator) ![]u8 {
|
||||||
return self.panels.pathAlloc(allocator);
|
return self.panels.pathAlloc(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn panelSummaryAlloc(self: *const Session, allocator: std.mem.Allocator) ![]u8 {
|
||||||
|
return self.panels.summaryAlloc(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activeListRowsAlloc(self: *const Session, allocator: std.mem.Allocator, max_rows: usize) ![][]u8 {
|
||||||
|
return self.panels.activeListRowsAlloc(allocator, max_rows);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn snapshot(self: *const Session) !Snapshot {
|
pub fn snapshot(self: *const Session) !Snapshot {
|
||||||
const panel_state = self.panels.state();
|
const panel_state = self.panels.state();
|
||||||
if (self.buffer) |*buffer| {
|
if (self.buffer) |*buffer| {
|
||||||
|
|||||||
+4
-4
@@ -175,7 +175,7 @@ test "regular: local socket state request observes a running session" {
|
|||||||
defer std.testing.allocator.free(response);
|
defer std.testing.allocator.free(response);
|
||||||
thread.join();
|
thread.join();
|
||||||
|
|
||||||
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=3 panel_depth=0 active_panel=- panel_path=-\n", response);
|
try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=3 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "regular: local socket command mutates session state" {
|
test "regular: local socket command mutates session state" {
|
||||||
@@ -197,7 +197,7 @@ test "regular: local socket command mutates session state" {
|
|||||||
defer std.testing.allocator.free(response);
|
defer std.testing.allocator.free(response);
|
||||||
thread.join();
|
thread.join();
|
||||||
|
|
||||||
try std.testing.expectEqualStrings("ok state cursor_byte=3 cursor_cell=2 bytes_len=5 panel_depth=0 active_panel=- panel_path=-\n", response);
|
try std.testing.expectEqualStrings("ok state cursor_byte=3 cursor_cell=2 bytes_len=5 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "adversarial: socket transport returns explicit protocol errors" {
|
test "adversarial: socket transport returns explicit protocol errors" {
|
||||||
@@ -236,11 +236,11 @@ test "regular: local socket panel command exposes inspectable panel state" {
|
|||||||
const response = try request(std.testing.allocator, path, "command panel_open files");
|
const response = try request(std.testing.allocator, path, "command panel_open files");
|
||||||
defer std.testing.allocator.free(response);
|
defer std.testing.allocator.free(response);
|
||||||
thread.join();
|
thread.join();
|
||||||
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);
|
||||||
|
|
||||||
thread = try std.Thread.spawn(.{}, Server.acceptOnce, .{&server});
|
thread = try std.Thread.spawn(.{}, Server.acceptOnce, .{&server});
|
||||||
const state = try request(std.testing.allocator, path, "state");
|
const state = try request(std.testing.allocator, path, "state");
|
||||||
defer std.testing.allocator.free(state);
|
defer std.testing.allocator.free(state);
|
||||||
thread.join();
|
thread.join();
|
||||||
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", state);
|
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", state);
|
||||||
}
|
}
|
||||||
|
|||||||
+67
-1
@@ -94,6 +94,12 @@ pub const Client = struct {
|
|||||||
if (std.mem.startsWith(u8, line, "key ")) return self.handleTraceKey(line[4..]);
|
if (std.mem.startsWith(u8, line, "key ")) return self.handleTraceKey(line[4..]);
|
||||||
if (std.mem.startsWith(u8, line, "type ")) return self.handleInput(line[5..]);
|
if (std.mem.startsWith(u8, line, "type ")) return self.handleInput(line[5..]);
|
||||||
if (std.mem.startsWith(u8, line, "panel_open ")) return self.applyProtocolCommand(line);
|
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);
|
||||||
|
if (std.mem.eql(u8, line, "list_down")) return self.applyProtocol("command list_down");
|
||||||
|
if (std.mem.eql(u8, line, "list_up")) return self.applyProtocol("command list_up");
|
||||||
|
if (std.mem.eql(u8, line, "list_select")) return self.applyProtocol("command list_select");
|
||||||
|
if (std.mem.eql(u8, line, "list_cancel")) return self.applyProtocol("command list_cancel");
|
||||||
if (std.mem.eql(u8, line, "panel_close")) return self.applyProtocol("command panel_close");
|
if (std.mem.eql(u8, line, "panel_close")) return self.applyProtocol("command panel_close");
|
||||||
if (std.mem.eql(u8, line, "panel_next")) return self.applyProtocol("command panel_next");
|
if (std.mem.eql(u8, line, "panel_next")) return self.applyProtocol("command panel_next");
|
||||||
if (std.mem.eql(u8, line, "panel_prev")) return self.applyProtocol("command panel_prev");
|
if (std.mem.eql(u8, line, "panel_prev")) return self.applyProtocol("command panel_prev");
|
||||||
@@ -174,7 +180,20 @@ pub const Client = struct {
|
|||||||
try out.append(allocator, '\n');
|
try out.append(allocator, '\n');
|
||||||
body_lines_used += 1;
|
body_lines_used += 1;
|
||||||
|
|
||||||
if (body_lines_used < max_body_lines) {
|
const remaining_rows = max_body_lines - body_lines_used;
|
||||||
|
const rows = self.session.activeListRowsAlloc(allocator, remaining_rows) catch null;
|
||||||
|
if (rows) |list_rows| {
|
||||||
|
defer {
|
||||||
|
for (list_rows) |row| allocator.free(row);
|
||||||
|
allocator.free(list_rows);
|
||||||
|
}
|
||||||
|
for (list_rows) |row| {
|
||||||
|
if (body_lines_used >= max_body_lines) break;
|
||||||
|
try appendVisibleCells(allocator, &out, row, self.viewport.width);
|
||||||
|
try out.append(allocator, '\n');
|
||||||
|
body_lines_used += 1;
|
||||||
|
}
|
||||||
|
} else if (body_lines_used < max_body_lines) {
|
||||||
const title = snap.active_panel_title.?;
|
const title = snap.active_panel_title.?;
|
||||||
const detail = try std.fmt.allocPrint(allocator, "{s}: no content yet", .{title});
|
const detail = try std.fmt.allocPrint(allocator, "{s}: no content yet", .{title});
|
||||||
defer allocator.free(detail);
|
defer allocator.free(detail);
|
||||||
@@ -599,3 +618,50 @@ test "adversarial: invalid panel title and empty close recover without changing
|
|||||||
try std.testing.expect(std.mem.indexOf(u8, frame, "abc") != null);
|
try std.testing.expect(std.mem.indexOf(u8, frame, "abc") != null);
|
||||||
try assertLinesFit(frame, 16);
|
try assertLinesFit(frame, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "regular: list panel filters moves selects and renders rows" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 32, .height = 5 });
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
try client.handleTraceLine("list_open files src/main.zig|src/panel.zig|README.md");
|
||||||
|
try client.handleTraceLine("list_filter src");
|
||||||
|
try client.handleTraceLine("list_down");
|
||||||
|
const frame = try client.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "panel [files]") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "> src/panel.zig") != null);
|
||||||
|
try assertLinesFit(frame, 32);
|
||||||
|
|
||||||
|
try client.handleTraceLine("list_select");
|
||||||
|
const summary = try client.session.panelSummaryAlloc(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(summary);
|
||||||
|
try std.testing.expectEqualStrings("list:filter=src,cursor=1,visible=2,selected=src/panel.zig", summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: list cancel returns to previous editor surface" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 24, .height = 4 });
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
try client.handleTraceLine("open abc");
|
||||||
|
try client.handleTraceLine("list_open files a.zig|b.zig");
|
||||||
|
try client.handleTraceLine("list_cancel");
|
||||||
|
const frame = try client.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "abc") != null);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "panel") == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "adversarial: list no-match state and invalid actions recover" {
|
||||||
|
var client = try Client.init(std.testing.allocator, .{ .width = 24, .height = 4 });
|
||||||
|
defer client.deinit();
|
||||||
|
|
||||||
|
try client.handleTraceLine("list_open files a.zig|b.zig");
|
||||||
|
try client.handleTraceLine("list_filter none");
|
||||||
|
const frame = try client.render(std.testing.allocator);
|
||||||
|
defer std.testing.allocator.free(frame);
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, frame, "no matches") != null);
|
||||||
|
try assertLinesFit(frame, 24);
|
||||||
|
|
||||||
|
try client.handleTraceLine("list_cancel");
|
||||||
|
try std.testing.expectError(Error.ProtocolRejected, client.handleTraceLine("list_down"));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user