Add modal input state and rails
This commit is contained in:
+16
-3
@@ -21,6 +21,8 @@ pub const Action = union(enum) {
|
||||
quit,
|
||||
open: []u8,
|
||||
symbol: symbol_mod.Symbol,
|
||||
repeat_rail,
|
||||
restore_last,
|
||||
not_built: Feature,
|
||||
|
||||
pub fn deinit(self: Action, allocator: std.mem.Allocator) void {
|
||||
@@ -66,7 +68,7 @@ pub const Leader = struct {
|
||||
if (self.message) |message| return message;
|
||||
return switch (self.mode) {
|
||||
.idle => "",
|
||||
.rail => "leader: s save q quit o open p symbols / search x close",
|
||||
.rail => "leader: s save q quit o open p symbols r repeat / search x close",
|
||||
.symbol_rail => symbol_mod.rail_status,
|
||||
.open_prompt => "open: type path, Enter opens, Esc cancels",
|
||||
};
|
||||
@@ -84,6 +86,10 @@ pub const Leader = struct {
|
||||
return self.open_prompt.items;
|
||||
}
|
||||
|
||||
pub fn isPromptActive(self: *const Leader) bool {
|
||||
return self.mode == .open_prompt;
|
||||
}
|
||||
|
||||
fn handleIdle(self: *Leader, event: input.Event) Action {
|
||||
self.message = null;
|
||||
switch (event) {
|
||||
@@ -117,6 +123,10 @@ pub const Leader = struct {
|
||||
self.mode = .symbol_rail;
|
||||
return .none;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "r")) {
|
||||
self.mode = .idle;
|
||||
return .repeat_rail;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "/")) {
|
||||
self.mode = .idle;
|
||||
self.message = "search is not built in this profile yet";
|
||||
@@ -137,7 +147,10 @@ pub const Leader = struct {
|
||||
self.message = "leader cancelled";
|
||||
return .none;
|
||||
},
|
||||
.space => return .none,
|
||||
.space => {
|
||||
self.mode = .idle;
|
||||
return .restore_last;
|
||||
},
|
||||
else => {
|
||||
self.mode = .idle;
|
||||
self.message = "unknown leader key";
|
||||
@@ -233,7 +246,7 @@ test "regular: space opens a visible leader rail and save dispatches" {
|
||||
defer leader.deinit();
|
||||
|
||||
try expectActionTag(.none, try leader.handleEvent(input.normalize(" ")));
|
||||
try std.testing.expectEqualStrings("leader: s save q quit o open p symbols / search x close", leader.status());
|
||||
try std.testing.expectEqualStrings("leader: s save q quit o open p symbols r repeat / search x close", leader.status());
|
||||
|
||||
try expectActionTag(.save, try leader.handleEvent(input.normalize("s")));
|
||||
try std.testing.expect(!leader.isActive());
|
||||
|
||||
@@ -491,6 +491,7 @@ test "regular: local editor client exposes save and dirty quit guards" {
|
||||
defer std.testing.allocator.free(initial);
|
||||
try std.testing.expectEqualStrings("", initial);
|
||||
|
||||
try client.handleInput("i");
|
||||
try client.handleInput("h");
|
||||
try client.handleInput("e");
|
||||
try client.handleInput("l");
|
||||
@@ -500,12 +501,19 @@ test "regular: local editor client exposes save and dirty quit guards" {
|
||||
defer std.testing.allocator.free(edited);
|
||||
try std.testing.expectEqualStrings("hello", edited);
|
||||
|
||||
try client.handleInput(" ");
|
||||
try std.testing.expectEqualStrings("insert_space", client.pendingRailName());
|
||||
try client.handleInput("n");
|
||||
try std.testing.expectEqualStrings("normal", client.modeName());
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("s");
|
||||
try std.testing.expectEqualStrings("hello", try client.saved());
|
||||
|
||||
try client.handleInput("i");
|
||||
try client.handleInput("!");
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("n");
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("q");
|
||||
try std.testing.expect(client.requestedQuit());
|
||||
client.clearQuit();
|
||||
|
||||
@@ -70,13 +70,19 @@ fn containsForbiddenToken(line: []const u8) bool {
|
||||
test "regular: mobile trace performs code edit with leader save and quit" {
|
||||
const trace =
|
||||
\\open
|
||||
\\type i
|
||||
\\type call
|
||||
\\key space
|
||||
\\type n
|
||||
\\key space
|
||||
\\key p
|
||||
\\key p
|
||||
\\type i
|
||||
\\type arg
|
||||
\\key right
|
||||
\\key space
|
||||
\\type n
|
||||
\\key space
|
||||
\\key p
|
||||
\\key s
|
||||
\\key space
|
||||
@@ -91,14 +97,20 @@ test "regular: mobile trace performs code edit with leader save and quit" {
|
||||
test "regular: mobile trace edits UTF-8 and hard-to-reach braces" {
|
||||
const trace =
|
||||
\\open safe
|
||||
\\type i
|
||||
\\type é
|
||||
\\key backspace
|
||||
\\key space
|
||||
\\type n
|
||||
\\key space
|
||||
\\key p
|
||||
\\key c
|
||||
\\type i
|
||||
\\type x
|
||||
\\key right
|
||||
\\key space
|
||||
\\type n
|
||||
\\key space
|
||||
\\key s
|
||||
\\key space
|
||||
\\key q
|
||||
|
||||
+399
-11
@@ -20,6 +20,22 @@ test {
|
||||
_ = Client;
|
||||
}
|
||||
|
||||
pub const EditorMode = enum {
|
||||
normal,
|
||||
insert,
|
||||
select,
|
||||
panel,
|
||||
prompt,
|
||||
};
|
||||
|
||||
const PrefixRail = enum {
|
||||
none,
|
||||
insert_space,
|
||||
match,
|
||||
go,
|
||||
repeat,
|
||||
};
|
||||
|
||||
pub const Error = error{
|
||||
ViewportTooSmall,
|
||||
InvalidResize,
|
||||
@@ -96,6 +112,10 @@ pub const Client = struct {
|
||||
saved_bytes: ?[]u8 = null,
|
||||
message: ?[]const u8 = null,
|
||||
quit: bool = false,
|
||||
mode: EditorMode = .normal,
|
||||
prefix: PrefixRail = .none,
|
||||
pending_count: usize = 0,
|
||||
last_rail: PrefixRail = .none,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, viewport: Viewport) !Client {
|
||||
return initWithIo(allocator, viewport, null);
|
||||
@@ -237,19 +257,22 @@ pub const Client = struct {
|
||||
}
|
||||
|
||||
const leader_status = self.leader.status();
|
||||
const prefix_status = self.prefixStatus();
|
||||
const status = if (self.message) |message|
|
||||
try std.fmt.allocPrint(allocator, "{s}", .{message})
|
||||
else if (leader_status.len != 0)
|
||||
try std.fmt.allocPrint(
|
||||
allocator,
|
||||
"{s}{s}{s}",
|
||||
.{ leader_status, if (self.leader.promptText().len > 0) ": " else "", self.leader.promptText() },
|
||||
"mode:{s} {s}{s}{s}",
|
||||
.{ @tagName(self.effectiveMode()), leader_status, if (self.leader.promptText().len > 0) ": " else "", self.leader.promptText() },
|
||||
)
|
||||
else if (prefix_status.len != 0)
|
||||
try std.fmt.allocPrint(allocator, "mode:{s} {s}", .{ @tagName(self.effectiveMode()), prefix_status })
|
||||
else
|
||||
try std.fmt.allocPrint(
|
||||
allocator,
|
||||
"mim row={d} col={d} bytes={d}{s}",
|
||||
.{ cursor_line + 1, cursor_col + 1, snap.bytes.len, if (self.quit) " quit" else "" },
|
||||
"mode:{s} row={d} col={d} bytes={d}{s}",
|
||||
.{ @tagName(self.effectiveMode()), cursor_line + 1, cursor_col + 1, snap.bytes.len, if (self.quit) " quit" else "" },
|
||||
);
|
||||
defer allocator.free(status);
|
||||
try appendVisibleCells(allocator, &out, status, self.viewport.width);
|
||||
@@ -312,15 +335,34 @@ pub const Client = struct {
|
||||
pub fn handleInput(self: *Client, raw: []const u8) !void {
|
||||
if (self.quit) return Error.ClientQuit;
|
||||
const event = input.normalize(raw);
|
||||
if (self.leader.capturesInput() or isLeaderTrigger(event)) {
|
||||
self.message = null;
|
||||
|
||||
const leader_trigger = isLeaderTrigger(event) and self.effectiveMode() != .insert;
|
||||
if (self.leader.capturesInput() or leader_trigger) {
|
||||
if (self.prefix == .insert_space and isLeaderTrigger(event)) {
|
||||
try self.insertText(" ");
|
||||
self.prefix = .none;
|
||||
return;
|
||||
}
|
||||
const action = try self.leader.handleEvent(event);
|
||||
defer action.deinit(self.allocator);
|
||||
try self.applyLeaderAction(action);
|
||||
return;
|
||||
}
|
||||
|
||||
_ = try self.leader.handleEvent(event);
|
||||
try self.applyNormalInput(event);
|
||||
try self.applyModeInput(event);
|
||||
}
|
||||
|
||||
pub fn modeName(self: *const Client) []const u8 {
|
||||
return @tagName(self.effectiveMode());
|
||||
}
|
||||
|
||||
pub fn pendingRailName(self: *const Client) []const u8 {
|
||||
return @tagName(self.prefix);
|
||||
}
|
||||
|
||||
pub fn pendingCount(self: *const Client) usize {
|
||||
return self.pending_count;
|
||||
}
|
||||
|
||||
pub fn saved(self: *const Client) ![]const u8 {
|
||||
@@ -344,6 +386,23 @@ pub const Client = struct {
|
||||
self.message = message;
|
||||
}
|
||||
|
||||
fn effectiveMode(self: *const Client) EditorMode {
|
||||
if (self.leader.isPromptActive()) return .prompt;
|
||||
const snap = self.session.snapshot() catch return self.mode;
|
||||
if (snap.active_panel_title != null) return .panel;
|
||||
return self.mode;
|
||||
}
|
||||
|
||||
fn prefixStatus(self: *const Client) []const u8 {
|
||||
return switch (self.prefix) {
|
||||
.none => if (self.pending_count != 0) "count pending" else "",
|
||||
.insert_space => "insert-space: n normal Space literal text commits space+text",
|
||||
.match => "match: m jump s inside a around ( { [ quotes",
|
||||
.go => "go: d definition r references e diagnostic a parameter",
|
||||
.repeat => "repeat: digits count . repeat-last",
|
||||
};
|
||||
}
|
||||
|
||||
fn addRepoFile(self: *Client, payload: []const u8) !void {
|
||||
const separator = std.mem.indexOfScalar(u8, payload, '=') orelse {
|
||||
self.message = "diagnostic:unsupported_file:invalid_repo_file_payload";
|
||||
@@ -629,18 +688,102 @@ pub const Client = struct {
|
||||
fn isLeaderTrigger(event: input.Event) bool {
|
||||
return switch (event) {
|
||||
.key => |key| key == .space,
|
||||
.text => |text| std.mem.eql(u8, text, " "),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn applyNormalInput(self: *Client, event: input.Event) !void {
|
||||
fn applyModeInput(self: *Client, event: input.Event) !void {
|
||||
if (self.prefix != .none) return self.applyPrefixInput(event);
|
||||
if (self.applyCountPrefix(event)) return;
|
||||
switch (self.effectiveMode()) {
|
||||
.normal => try self.applyNormalModeInput(event),
|
||||
.insert => try self.applyInsertModeInput(event),
|
||||
.select => try self.applySelectModeInput(event),
|
||||
.panel => try self.applyPanelModeInput(event),
|
||||
.prompt => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn applyCountPrefix(self: *Client, event: input.Event) bool {
|
||||
switch (event) {
|
||||
.text => |text| if (text.len == 1 and text[0] >= '0' and text[0] <= '9') {
|
||||
const digit = text[0] - '0';
|
||||
if (self.pending_count != 0 or digit != 0) {
|
||||
self.pending_count = self.pending_count * 10 + digit;
|
||||
self.last_rail = .repeat;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn takeRepeat(self: *Client) usize {
|
||||
const repeat = if (self.pending_count == 0) 1 else self.pending_count;
|
||||
self.pending_count = 0;
|
||||
return repeat;
|
||||
}
|
||||
|
||||
fn applyNormalModeInput(self: *Client, event: input.Event) !void {
|
||||
switch (event) {
|
||||
.text => |text| {
|
||||
const line = try std.fmt.allocPrint(self.allocator, "insert {s}", .{text});
|
||||
defer self.allocator.free(line);
|
||||
try self.applyProtocolCommand(line);
|
||||
if (std.mem.eql(u8, text, "i")) {
|
||||
self.mode = .insert;
|
||||
self.message = "insert";
|
||||
self.pending_count = 0;
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "s")) {
|
||||
self.mode = .select;
|
||||
self.message = "select";
|
||||
self.pending_count = 0;
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "m")) return self.openPrefix(.match);
|
||||
if (std.mem.eql(u8, text, "g")) return self.openPrefix(.go);
|
||||
if (std.mem.eql(u8, text, "j")) {
|
||||
_ = self.takeRepeat();
|
||||
self.message = "vertical movement is not built in this profile yet";
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "k")) {
|
||||
_ = self.takeRepeat();
|
||||
self.message = "vertical movement is not built in this profile yet";
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "h")) return self.repeatProtocol("command move_left", self.takeRepeat());
|
||||
if (std.mem.eql(u8, text, "l")) return self.repeatProtocol("command move_right", self.takeRepeat());
|
||||
self.unknownPrefixOrInput("normal");
|
||||
},
|
||||
.key => |key| switch (key) {
|
||||
.escape => {
|
||||
self.pending_count = 0;
|
||||
self.message = "normal";
|
||||
},
|
||||
.backspace => try self.applyProtocol("command delete_backward"),
|
||||
.arrow_left => try self.repeatProtocol("command move_left", self.takeRepeat()),
|
||||
.arrow_right => try self.repeatProtocol("command move_right", self.takeRepeat()),
|
||||
.arrow_up, .arrow_down => {
|
||||
_ = self.takeRepeat();
|
||||
self.message = "vertical movement is not built in this profile yet";
|
||||
},
|
||||
else => self.unknownPrefixOrInput("normal"),
|
||||
},
|
||||
.unknown => self.unknownPrefixOrInput("normal"),
|
||||
}
|
||||
}
|
||||
|
||||
fn applyInsertModeInput(self: *Client, event: input.Event) !void {
|
||||
switch (event) {
|
||||
.text => |text| if (std.mem.eql(u8, text, " ")) self.openPrefix(.insert_space) else try self.insertText(text),
|
||||
.key => |key| switch (key) {
|
||||
.space => self.openPrefix(.insert_space),
|
||||
.escape => {
|
||||
self.mode = .normal;
|
||||
self.message = "normal";
|
||||
},
|
||||
.backspace => try self.applyProtocol("command delete_backward"),
|
||||
.arrow_left => try self.applyProtocol("command move_left"),
|
||||
.arrow_right => try self.applyProtocol("command move_right"),
|
||||
@@ -650,6 +793,142 @@ pub const Client = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn applySelectModeInput(self: *Client, event: input.Event) !void {
|
||||
switch (event) {
|
||||
.text => |text| {
|
||||
if (std.mem.eql(u8, text, "n")) {
|
||||
self.mode = .normal;
|
||||
self.message = "normal";
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "m")) return self.openPrefix(.match);
|
||||
if (std.mem.eql(u8, text, "g")) return self.openPrefix(.go);
|
||||
self.unknownPrefixOrInput("select");
|
||||
},
|
||||
.key => |key| switch (key) {
|
||||
.escape => {
|
||||
self.mode = .normal;
|
||||
self.message = "normal";
|
||||
},
|
||||
.arrow_left => try self.applyProtocol("command move_left"),
|
||||
.arrow_right => try self.applyProtocol("command move_right"),
|
||||
else => {},
|
||||
},
|
||||
.unknown => self.unknownPrefixOrInput("select"),
|
||||
}
|
||||
}
|
||||
|
||||
fn applyPanelModeInput(self: *Client, event: input.Event) !void {
|
||||
switch (event) {
|
||||
.text => |text| {
|
||||
if (std.mem.eql(u8, text, "j")) return self.applyProtocol("command list_down");
|
||||
if (std.mem.eql(u8, text, "k")) return self.applyProtocol("command list_up");
|
||||
if (std.mem.eql(u8, text, "q")) {
|
||||
self.message = "panel close is not built in this profile yet";
|
||||
return;
|
||||
}
|
||||
self.unknownPrefixOrInput("panel");
|
||||
},
|
||||
.key => |key| switch (key) {
|
||||
.arrow_down => self.message = "panel down is not built in this profile yet",
|
||||
.arrow_up => self.message = "panel up is not built in this profile yet",
|
||||
.escape => self.message = "panel close is not built in this profile yet",
|
||||
else => {},
|
||||
},
|
||||
.unknown => self.unknownPrefixOrInput("panel"),
|
||||
}
|
||||
}
|
||||
|
||||
fn applyPrefixInput(self: *Client, event: input.Event) !void {
|
||||
const active = self.prefix;
|
||||
self.prefix = .none;
|
||||
switch (active) {
|
||||
.none => {},
|
||||
.insert_space => try self.applyInsertSpaceRail(event),
|
||||
.match => self.applyKnownRailOrMessage(event, "match rail ready"),
|
||||
.go => self.applyKnownRailOrMessage(event, "go rail ready"),
|
||||
.repeat => self.applyKnownRailOrMessage(event, "repeat rail ready"),
|
||||
}
|
||||
}
|
||||
|
||||
fn applyInsertSpaceRail(self: *Client, event: input.Event) !void {
|
||||
switch (event) {
|
||||
.text => |text| {
|
||||
if (std.mem.eql(u8, text, "n")) {
|
||||
self.mode = .normal;
|
||||
self.message = "normal";
|
||||
return;
|
||||
}
|
||||
try self.insertText(" ");
|
||||
try self.insertText(text);
|
||||
},
|
||||
.key => |key| switch (key) {
|
||||
.space => try self.insertText(" "),
|
||||
.escape => {
|
||||
self.mode = .normal;
|
||||
self.message = "normal";
|
||||
},
|
||||
else => try self.insertText(" "),
|
||||
},
|
||||
.unknown => try self.insertText(" "),
|
||||
}
|
||||
}
|
||||
|
||||
fn applyKnownRailOrMessage(self: *Client, event: input.Event, success: []const u8) void {
|
||||
switch (event) {
|
||||
.text => |text| if (text.len == 1 and std.mem.indexOfScalar(u8, "msadretpi(){}[]'\"`.", text[0]) != null) {
|
||||
self.message = success;
|
||||
self.pending_count = 0;
|
||||
return;
|
||||
},
|
||||
.key => |key| if (key == .escape or key == .backspace) {
|
||||
self.message = "rail cancelled";
|
||||
self.pending_count = 0;
|
||||
return;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
self.message = "unknown prefix key";
|
||||
self.last_rail = .none;
|
||||
self.pending_count = 0;
|
||||
}
|
||||
|
||||
fn openPrefix(self: *Client, prefix: PrefixRail) void {
|
||||
self.prefix = prefix;
|
||||
self.last_rail = prefix;
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn restoreLastRail(self: *Client) void {
|
||||
if (self.last_rail == .none) {
|
||||
self.message = "no previous rail";
|
||||
return;
|
||||
}
|
||||
self.prefix = self.last_rail;
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn insertText(self: *Client, text: []const u8) !void {
|
||||
const line = try std.fmt.allocPrint(self.allocator, "insert {s}", .{text});
|
||||
defer self.allocator.free(line);
|
||||
try self.applyProtocolCommand(line);
|
||||
}
|
||||
|
||||
fn repeatProtocol(self: *Client, line: []const u8, repeat: usize) !void {
|
||||
var i: usize = 0;
|
||||
while (i < repeat) : (i += 1) try self.applyProtocol(line);
|
||||
}
|
||||
|
||||
fn unknownPrefixOrInput(self: *Client, mode: []const u8) void {
|
||||
self.pending_count = 0;
|
||||
self.message = if (std.mem.eql(u8, mode, "normal"))
|
||||
"unknown normal key"
|
||||
else if (std.mem.eql(u8, mode, "select"))
|
||||
"unknown select key"
|
||||
else
|
||||
"unknown panel key";
|
||||
}
|
||||
|
||||
fn applyLeaderAction(self: *Client, action: leader_mod.Action) !void {
|
||||
switch (action) {
|
||||
.none => {},
|
||||
@@ -661,6 +940,8 @@ pub const Client = struct {
|
||||
try self.applyProtocol(line);
|
||||
},
|
||||
.symbol => |symbol| try self.applyProtocol(symbol_mod.protocolCommand(symbol)),
|
||||
.repeat_rail => self.openPrefix(.repeat),
|
||||
.restore_last => self.restoreLastRail(),
|
||||
.not_built => {},
|
||||
}
|
||||
}
|
||||
@@ -2060,3 +2341,110 @@ test "adversarial: missing pi command is visible and invalid command preserves b
|
||||
const snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("safe", snap.bytes);
|
||||
}
|
||||
|
||||
test "regular: modal input exposes normal insert select prompt and panel modes" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 8 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open abc");
|
||||
try std.testing.expectEqualStrings("normal", client.modeName());
|
||||
|
||||
try client.handleInput("i");
|
||||
try std.testing.expectEqualStrings("insert", client.modeName());
|
||||
try client.handleInput("x");
|
||||
const inserted = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(inserted);
|
||||
try std.testing.expectEqualStrings("xabc", inserted);
|
||||
|
||||
try client.handleInput(" ");
|
||||
try std.testing.expectEqualStrings("insert_space", client.pendingRailName());
|
||||
try client.handleInput("n");
|
||||
try std.testing.expectEqualStrings("normal", client.modeName());
|
||||
|
||||
try client.handleInput("s");
|
||||
try std.testing.expectEqualStrings("select", client.modeName());
|
||||
try client.handleInput("n");
|
||||
try std.testing.expectEqualStrings("normal", client.modeName());
|
||||
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("o");
|
||||
try std.testing.expectEqualStrings("prompt", client.modeName());
|
||||
|
||||
var panel_client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 8 });
|
||||
defer panel_client.deinit();
|
||||
try panel_client.handleTraceLine("panel_open files");
|
||||
try std.testing.expectEqualStrings("panel", panel_client.modeName());
|
||||
}
|
||||
|
||||
test "regular: modal rails expose match go repeat and unknown recovery" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 8 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open abc");
|
||||
|
||||
try client.handleInput("m");
|
||||
try std.testing.expectEqualStrings("match", client.pendingRailName());
|
||||
const match_frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(match_frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, match_frame, "match:") != null);
|
||||
try client.handleInput("z");
|
||||
try std.testing.expectEqualStrings("none", client.pendingRailName());
|
||||
const unknown_frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(unknown_frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, unknown_frame, "unknown prefix key") != null);
|
||||
|
||||
try client.handleInput("g");
|
||||
try std.testing.expectEqualStrings("go", client.pendingRailName());
|
||||
try client.handleInput("d");
|
||||
try std.testing.expectEqualStrings("none", client.pendingRailName());
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput(" ");
|
||||
try std.testing.expectEqualStrings("go", client.pendingRailName());
|
||||
try client.handleInput("d");
|
||||
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("r");
|
||||
try std.testing.expectEqualStrings("repeat", client.pendingRailName());
|
||||
try client.handleInput(".");
|
||||
try std.testing.expectEqualStrings("none", client.pendingRailName());
|
||||
}
|
||||
|
||||
test "regular: counts are pending visible and cleared by one movement" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 8 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open abcd");
|
||||
try client.handleTraceLine("right");
|
||||
try client.handleTraceLine("right");
|
||||
|
||||
try client.handleInput("2");
|
||||
try std.testing.expectEqual(@as(usize, 2), client.pendingCount());
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "count pending") != null);
|
||||
|
||||
try client.handleInput("h");
|
||||
try std.testing.expectEqual(@as(usize, 0), client.pendingCount());
|
||||
try client.handleInput("i");
|
||||
try client.handleInput("X");
|
||||
const bytes = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(bytes);
|
||||
try std.testing.expectEqualStrings("Xabcd", bytes);
|
||||
}
|
||||
|
||||
test "regular: insert pending space commits literal space or returns normal" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 8 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open ");
|
||||
try client.handleInput("i");
|
||||
try client.handleInput("a");
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("b");
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("c");
|
||||
const bytes = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(bytes);
|
||||
try std.testing.expectEqualStrings("a b c", bytes);
|
||||
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("n");
|
||||
try std.testing.expectEqualStrings("normal", client.modeName());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user