Add physical keyboard parity aliases
This commit is contained in:
@@ -378,8 +378,11 @@ Backed by current code/tests:
|
||||
- protocol/session/job/LSP/diagnostic skeletons;
|
||||
- `Space t` lint/build/test/check job rail backed by source-patched command profiles,
|
||||
captured output panels, missing-tool/cancel/timeout rows, and jump/yank actions;
|
||||
- physical-keyboard parity aliases for arrows, Home/End, PageUp/PageDown, Escape,
|
||||
direct digit counts, `%` match jumping, and panel page keys while preserving
|
||||
mobile Space paths;
|
||||
- basic trace commands for open, insert, save, panels, diagnostics, repo files,
|
||||
and local context.
|
||||
physical key aliases, and local context.
|
||||
|
||||
Design-only and needing future implementation slices:
|
||||
|
||||
@@ -395,9 +398,7 @@ Design-only and needing future implementation slices:
|
||||
- source-profile-backed format, organize-imports, code-action edit application;
|
||||
- format-on-save policy and one-shot save-without-format;
|
||||
- contextual rails for all prefixes listed in this document;
|
||||
- Insert pending-space rail, including `Space` pause then `n` to Normal;
|
||||
- counts/repetition grammar with direct digit and mobile repeat-rail paths;
|
||||
- physical-keyboard trace fixtures and alias table.
|
||||
- Insert pending-space rail, including `Space` pause then `n` to Normal.
|
||||
|
||||
Future slices should implement one vertical behavior at a time with replay or
|
||||
headless tests: for example, `m m` delimiter jump, `s i` indent selection,
|
||||
|
||||
@@ -18,6 +18,10 @@ pub const Key = enum {
|
||||
arrow_right,
|
||||
arrow_up,
|
||||
arrow_down,
|
||||
home,
|
||||
end,
|
||||
page_up,
|
||||
page_down,
|
||||
};
|
||||
|
||||
pub const Event = union(enum) {
|
||||
@@ -82,6 +86,10 @@ pub fn normalize(raw: []const u8) Event {
|
||||
if (std.mem.eql(u8, raw, "\x1b[C")) return .{ .key = .arrow_right };
|
||||
if (std.mem.eql(u8, raw, "\x1b[A")) return .{ .key = .arrow_up };
|
||||
if (std.mem.eql(u8, raw, "\x1b[B")) return .{ .key = .arrow_down };
|
||||
if (std.mem.eql(u8, raw, "\x1b[H") or std.mem.eql(u8, raw, "\x1b[1~") or std.mem.eql(u8, raw, "\x1b[7~")) return .{ .key = .home };
|
||||
if (std.mem.eql(u8, raw, "\x1b[F") or std.mem.eql(u8, raw, "\x1b[4~") or std.mem.eql(u8, raw, "\x1b[8~")) return .{ .key = .end };
|
||||
if (std.mem.eql(u8, raw, "\x1b[5~")) return .{ .key = .page_up };
|
||||
if (std.mem.eql(u8, raw, "\x1b[6~")) return .{ .key = .page_down };
|
||||
|
||||
if (std.unicode.utf8ValidateSlice(raw) and isPrintableText(raw)) return .{ .text = raw };
|
||||
return .{ .unknown = raw };
|
||||
@@ -199,6 +207,12 @@ test "regular: raw terminal bytes normalize to stable editor input events" {
|
||||
try expectEventLabel("\x1b[C", "key:arrow_right");
|
||||
try expectEventLabel("\x1b[A", "key:arrow_up");
|
||||
try expectEventLabel("\x1b[B", "key:arrow_down");
|
||||
try expectEventLabel("\x1b[H", "key:home");
|
||||
try expectEventLabel("\x1b[1~", "key:home");
|
||||
try expectEventLabel("\x1b[F", "key:end");
|
||||
try expectEventLabel("\x1b[4~", "key:end");
|
||||
try expectEventLabel("\x1b[5~", "key:page_up");
|
||||
try expectEventLabel("\x1b[6~", "key:page_down");
|
||||
}
|
||||
|
||||
test "regular: captured trace can be replayed" {
|
||||
|
||||
+1
-1
@@ -162,7 +162,7 @@ pub fn classifyEvent(profile: *const Profile, event: input.Event) Classified {
|
||||
const kind: Kind = switch (event) {
|
||||
.key => |key| switch (key) {
|
||||
.space, .tab => .whitespace,
|
||||
.enter, .backspace, .escape, .arrow_left, .arrow_right, .arrow_up, .arrow_down => .control,
|
||||
.enter, .backspace, .escape, .arrow_left, .arrow_right, .arrow_up, .arrow_down, .home, .end, .page_up, .page_down => .control,
|
||||
},
|
||||
.text => |text| lookupFact(profile, text) orelse .unknown,
|
||||
.unknown => .unknown,
|
||||
|
||||
+5
-5
@@ -101,12 +101,12 @@ pub const Leader = struct {
|
||||
if (self.message) |message| return message;
|
||||
return switch (self.mode) {
|
||||
.idle => "",
|
||||
.rail => "leader: w save q quit o open p symbols s search r repeat x close",
|
||||
.rail => "leader: w save q quit o open p symbols s search r repeat (digits/%/Esc ok)",
|
||||
.symbol_rail => symbol_mod.rail_status,
|
||||
.search_rail => "search: f current file p project s symbols",
|
||||
.language_rail => "language: h hover s signature o open hover",
|
||||
.diagnostic_rail => "diagnostics: d/open n next p previous f filter-source",
|
||||
.tool_rail => "tools: l lint-file L lint-project b build t test c check x cancel j jump y yank",
|
||||
.language_rail => "language: h hover s sig f fmt o imports a actions (Space path)",
|
||||
.diagnostic_rail => "diagnostics: d open n/p next/prev f source (arrows/Enter ok)",
|
||||
.tool_rail => "tools: l/L lint b build t test c check x cancel j jump y yank",
|
||||
.open_prompt => "open: type path, Enter opens, Esc cancels",
|
||||
};
|
||||
}
|
||||
@@ -443,7 +443,7 @@ test "regular: space opens a visible leader rail and write dispatches" {
|
||||
defer leader.deinit();
|
||||
|
||||
try expectActionTag(.none, try leader.handleEvent(input.normalize(" ")));
|
||||
try std.testing.expectEqualStrings("leader: w save q quit o open p symbols s search r repeat x close", leader.status());
|
||||
try std.testing.expectEqualStrings("leader: w save q quit o open p symbols s search r repeat (digits/%/Esc ok)", leader.status());
|
||||
|
||||
try expectActionTag(.save, try leader.handleEvent(input.normalize("w")));
|
||||
try std.testing.expect(!leader.isActive());
|
||||
|
||||
+131
@@ -928,6 +928,12 @@ pub const Client = struct {
|
||||
if (std.mem.eql(u8, key_name, "escape")) return self.handleInput("\x1b");
|
||||
if (std.mem.eql(u8, key_name, "left")) return self.handleInput("\x1b[D");
|
||||
if (std.mem.eql(u8, key_name, "right")) return self.handleInput("\x1b[C");
|
||||
if (std.mem.eql(u8, key_name, "up")) return self.handleInput("\x1b[A");
|
||||
if (std.mem.eql(u8, key_name, "down")) return self.handleInput("\x1b[B");
|
||||
if (std.mem.eql(u8, key_name, "home")) return self.handleInput("\x1b[H");
|
||||
if (std.mem.eql(u8, key_name, "end")) return self.handleInput("\x1b[F");
|
||||
if (std.mem.eql(u8, key_name, "page_up")) return self.handleInput("\x1b[5~");
|
||||
if (std.mem.eql(u8, key_name, "page_down")) return self.handleInput("\x1b[6~");
|
||||
if (key_name.len == 1) return self.handleInput(key_name);
|
||||
return Error.UnknownTraceEvent;
|
||||
}
|
||||
@@ -1021,6 +1027,7 @@ pub const Client = struct {
|
||||
if (std.mem.eql(u8, text, "U")) return self.redoEdit();
|
||||
if (std.mem.eql(u8, text, "0")) return self.applyProtocol("command move_line_start");
|
||||
if (std.mem.eql(u8, text, "$")) return self.applyProtocol("command move_line_end");
|
||||
if (std.mem.eql(u8, text, "%")) return self.jumpToMatch(null);
|
||||
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")) return self.repeatProtocol("command move_down", self.takeRepeat());
|
||||
@@ -1042,6 +1049,10 @@ pub const Client = struct {
|
||||
.arrow_right => try self.repeatProtocol("command move_right", self.takeRepeat()),
|
||||
.arrow_up => try self.repeatProtocol("command move_up", self.takeRepeat()),
|
||||
.arrow_down => try self.repeatProtocol("command move_down", self.takeRepeat()),
|
||||
.home => try self.applyProtocol("command move_line_start"),
|
||||
.end => try self.applyProtocol("command move_line_end"),
|
||||
.page_up => try self.repeatProtocol("command move_up", @max(@as(usize, 1), self.viewport.height - 2)),
|
||||
.page_down => try self.repeatProtocol("command move_down", @max(@as(usize, 1), self.viewport.height - 2)),
|
||||
else => self.unknownPrefixOrInput("normal"),
|
||||
},
|
||||
.unknown => self.unknownPrefixOrInput("normal"),
|
||||
@@ -1060,6 +1071,10 @@ pub const Client = struct {
|
||||
.backspace => try self.applyMutatingProtocol("command delete_backward"),
|
||||
.arrow_left => try self.applyProtocol("command move_left"),
|
||||
.arrow_right => try self.applyProtocol("command move_right"),
|
||||
.arrow_up => try self.applyProtocol("command move_up"),
|
||||
.arrow_down => try self.applyProtocol("command move_down"),
|
||||
.home => try self.applyProtocol("command move_line_start"),
|
||||
.end => try self.applyProtocol("command move_line_end"),
|
||||
else => {},
|
||||
},
|
||||
.unknown => {},
|
||||
@@ -1074,6 +1089,7 @@ pub const Client = struct {
|
||||
self.message = "normal";
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "%")) return self.jumpToMatch(null);
|
||||
if (std.mem.eql(u8, text, "m")) return self.openPrefix(.match);
|
||||
if (std.mem.eql(u8, text, "g")) return self.openPrefix(.go);
|
||||
if (text.len == 1) {
|
||||
@@ -1092,6 +1108,10 @@ pub const Client = struct {
|
||||
},
|
||||
.arrow_left => try self.applyProtocol("command move_left"),
|
||||
.arrow_right => try self.applyProtocol("command move_right"),
|
||||
.arrow_up => try self.applyProtocol("command move_up"),
|
||||
.arrow_down => try self.applyProtocol("command move_down"),
|
||||
.home => try self.applyProtocol("command move_line_start"),
|
||||
.end => try self.applyProtocol("command move_line_end"),
|
||||
else => {},
|
||||
},
|
||||
.unknown => self.unknownPrefixOrInput("select"),
|
||||
@@ -1110,6 +1130,8 @@ pub const Client = struct {
|
||||
.key => |key| switch (key) {
|
||||
.arrow_down => try self.applyProtocol("command list_down"),
|
||||
.arrow_up => try self.applyProtocol("command list_up"),
|
||||
.page_down => try self.repeatProtocol("command list_down", @max(@as(usize, 1), self.viewport.height - 2)),
|
||||
.page_up => try self.repeatProtocol("command list_up", @max(@as(usize, 1), self.viewport.height - 2)),
|
||||
.enter => try self.openActivePanelItem(),
|
||||
.escape => try self.closeActivePanel(),
|
||||
else => {},
|
||||
@@ -4581,3 +4603,112 @@ test "regular: provider edit rows parse source scope and replacement" {
|
||||
defer std.testing.allocator.free(action_row);
|
||||
try std.testing.expectEqualStrings("zls", try lsp_mod.providerFromActionPanelRow(action_row));
|
||||
}
|
||||
|
||||
test "regular: physical arrows home end page and escape match editor movement" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 5 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open one\ntwo\nthree\nfour\nfive");
|
||||
|
||||
var snap = try client.session.snapshot();
|
||||
const start = snap.cursor_byte;
|
||||
try client.handleTraceLine("key page_down");
|
||||
snap = try client.session.snapshot();
|
||||
try std.testing.expect(snap.cursor_byte > start);
|
||||
|
||||
try client.handleTraceLine("key home");
|
||||
snap = try client.session.snapshot();
|
||||
const line_start = snap.cursor_byte;
|
||||
try client.handleTraceLine("key end");
|
||||
snap = try client.session.snapshot();
|
||||
try std.testing.expect(snap.cursor_byte >= line_start);
|
||||
|
||||
try client.handleTraceLine("key page_up");
|
||||
snap = try client.session.snapshot();
|
||||
try std.testing.expect(snap.cursor_byte <= line_start);
|
||||
|
||||
try client.handleInput("i");
|
||||
try client.handleInput("x");
|
||||
try client.handleTraceLine("key escape");
|
||||
try client.handleInput("u");
|
||||
snap = try client.session.snapshot();
|
||||
try std.testing.expect(std.mem.indexOf(u8, snap.bytes, "onxe") == null);
|
||||
}
|
||||
|
||||
test "regular: physical digit count matches repeated arrow movement" {
|
||||
var physical = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
|
||||
defer physical.deinit();
|
||||
try physical.handleTraceLine("open a\nb\nc\nd");
|
||||
try physical.handleInput("3");
|
||||
try physical.handleTraceLine("key down");
|
||||
const physical_snap = try physical.session.snapshot();
|
||||
|
||||
var repeated = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
|
||||
defer repeated.deinit();
|
||||
try repeated.handleTraceLine("open a\nb\nc\nd");
|
||||
try repeated.handleTraceLine("key down");
|
||||
try repeated.handleTraceLine("key down");
|
||||
try repeated.handleTraceLine("key down");
|
||||
const repeated_snap = try repeated.session.snapshot();
|
||||
try std.testing.expectEqual(repeated_snap.cursor_byte, physical_snap.cursor_byte);
|
||||
}
|
||||
|
||||
test "regular: physical percent and mobile match rail jump to same pair" {
|
||||
var physical = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
|
||||
defer physical.deinit();
|
||||
try physical.handleTraceLine("open (abc)");
|
||||
try physical.handleInput("%");
|
||||
var snap = try physical.session.snapshot();
|
||||
try std.testing.expectEqual(@as(usize, 4), snap.cursor_byte);
|
||||
|
||||
var mobile = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
|
||||
defer mobile.deinit();
|
||||
try mobile.handleTraceLine("open (abc)");
|
||||
try mobile.handleInput("m");
|
||||
try mobile.handleInput("m");
|
||||
snap = try mobile.session.snapshot();
|
||||
try std.testing.expectEqual(@as(usize, 4), snap.cursor_byte);
|
||||
}
|
||||
|
||||
test "regular: insert/select modes accept arrows while Space n remains mobile escape" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 6 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open ab\ncd");
|
||||
try client.handleInput("i");
|
||||
try client.handleTraceLine("key end");
|
||||
try client.handleInput("X");
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("n");
|
||||
var snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("abX\ncd", snap.bytes);
|
||||
|
||||
try client.handleInput("s");
|
||||
try client.handleTraceLine("key up");
|
||||
try client.handleTraceLine("key home");
|
||||
try client.handleInput("n");
|
||||
snap = try client.session.snapshot();
|
||||
try std.testing.expect(snap.selection != null or snap.cursor_byte == 0);
|
||||
}
|
||||
|
||||
test "regular: panels accept physical page keys and enter without breaking arrows" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 64, .height = 4 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("list_open files a.zig|b.zig|c.zig|d.zig|e.zig");
|
||||
try client.handleTraceLine("key page_down");
|
||||
const after_down = try client.session.activeListItem();
|
||||
try std.testing.expect(!std.mem.eql(u8, after_down, "a.zig"));
|
||||
|
||||
try client.handleTraceLine("key up");
|
||||
const after_up = try client.session.activeListItem();
|
||||
try std.testing.expect(!std.mem.eql(u8, after_down, after_up));
|
||||
}
|
||||
|
||||
test "adversarial: unknown physical key names stay rejected and leader help stays narrow" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 72, .height = 5 });
|
||||
defer client.deinit();
|
||||
try std.testing.expectError(Error.UnknownTraceEvent, client.handleTraceLine("key f1"));
|
||||
try client.handleInput(" ");
|
||||
try std.testing.expect(std.mem.indexOf(u8, client.leader.status(), "digits/%/Esc ok") != null);
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try assertLinesFit(frame, 72);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user