Add generic session panel stack

This commit is contained in:
slhx agent
2026-06-21 02:32:46 +02:00
parent ce00452a9d
commit 230f8f9129
6 changed files with 369 additions and 21 deletions
+83 -5
View File
@@ -1,4 +1,5 @@
const std = @import("std");
const panel_mod = @import("panel.zig");
// Headless editor state for the protocol-first core.
// req: coding/001, session/003
@@ -35,6 +36,9 @@ pub const Snapshot = struct {
cursor_byte: usize,
cursor_cell: usize,
selection: ?Selection,
panel_depth: usize,
active_panel_index: ?usize,
active_panel_title: ?[]const u8,
};
pub const Buffer = struct {
@@ -60,6 +64,9 @@ pub const Buffer = struct {
.cursor_byte = self.cursor.byte,
.cursor_cell = self.cursor.cell,
.selection = self.selection,
.panel_depth = 0,
.active_panel_index = null,
.active_panel_title = null,
};
}
@@ -118,13 +125,15 @@ pub const Buffer = struct {
pub const Session = struct {
allocator: std.mem.Allocator,
buffer: ?Buffer = null,
panels: panel_mod.Stack,
pub fn init(allocator: std.mem.Allocator) Session {
return .{ .allocator = allocator };
return .{ .allocator = allocator, .panels = panel_mod.Stack.init(allocator) };
}
pub fn deinit(self: *Session) void {
if (self.buffer) |*buffer| buffer.deinit();
self.panels.deinit();
self.* = undefined;
}
@@ -138,9 +147,44 @@ pub const Session = struct {
return error.NoBufferOpen;
}
pub fn openPanel(self: *Session, title: []const u8) !void {
try self.panels.open(title);
}
pub fn closePanel(self: *Session) !void {
try self.panels.closeActive();
}
pub fn nextPanel(self: *Session) !void {
try self.panels.next();
}
pub fn previousPanel(self: *Session) !void {
try self.panels.previous();
}
pub fn panelPathAlloc(self: *const Session, allocator: std.mem.Allocator) ![]u8 {
return self.panels.pathAlloc(allocator);
}
pub fn snapshot(self: *const Session) !Snapshot {
if (self.buffer) |*buffer| return buffer.snapshot();
return error.NoBufferOpen;
const panel_state = self.panels.state();
if (self.buffer) |*buffer| {
var snap = buffer.snapshot();
snap.panel_depth = panel_state.depth;
snap.active_panel_index = panel_state.active_index;
snap.active_panel_title = panel_state.active_title;
return snap;
}
return .{
.bytes = "",
.cursor_byte = 0,
.cursor_cell = 0,
.selection = null,
.panel_depth = panel_state.depth,
.active_panel_index = panel_state.active_index,
.active_panel_title = panel_state.active_title,
};
}
};
@@ -275,12 +319,14 @@ test "adversarial: invalid inserted UTF-8 is rejected and existing bytes are pre
try std.testing.expectEqual(@as(usize, 0), buffer.snapshot().cursor_byte);
}
test "adversarial: dispatch requires an open buffer" {
test "adversarial: dispatch requires an open buffer but state remains inspectable" {
var session = Session.init(std.testing.allocator);
defer session.deinit();
try std.testing.expectError(error.NoBufferOpen, session.dispatch(.move_right));
try std.testing.expectError(error.NoBufferOpen, session.snapshot());
const snap = try session.snapshot();
try std.testing.expectEqual(@as(usize, 0), snap.bytes.len);
try std.testing.expectEqual(@as(usize, 0), snap.panel_depth);
}
test "regular: pair insertion places cursor between delimiters" {
@@ -308,3 +354,35 @@ test "adversarial: invalid UTF-8 pair insertion is rejected and existing bytes a
try std.testing.expectEqualStrings("safe", buffer.snapshot().bytes);
try std.testing.expectEqual(@as(usize, 0), buffer.snapshot().cursor_byte);
}
test "regular: session opens closes and switches generic panel stack" {
var session = Session.init(std.testing.allocator);
defer session.deinit();
try session.openPanel("files");
try session.openPanel("diagnostics");
try session.previousPanel();
var snap = try session.snapshot();
try std.testing.expectEqual(@as(usize, 2), snap.panel_depth);
try std.testing.expectEqualStrings("files", snap.active_panel_title.?);
try session.closePanel();
snap = try session.snapshot();
try std.testing.expectEqual(@as(usize, 1), snap.panel_depth);
try std.testing.expectEqualStrings("diagnostics", snap.active_panel_title.?);
const path = try session.panelPathAlloc(std.testing.allocator);
defer std.testing.allocator.free(path);
try std.testing.expectEqualStrings("[diagnostics]", path);
}
test "adversarial: session panel operations fail clearly on empty or invalid state" {
var session = Session.init(std.testing.allocator);
defer session.deinit();
try std.testing.expectError(error.NoPanelOpen, session.closePanel());
try std.testing.expectError(error.NoPanelOpen, session.nextPanel());
try std.testing.expectError(error.InvalidPanelTitle, session.openPanel("bad title"));
const snap = try session.snapshot();
try std.testing.expectEqual(@as(usize, 0), snap.panel_depth);
}