const std = @import("std"); const input = @import("input.zig"); const session_mod = @import("session.zig"); const symbol_mod = @import("symbol.zig"); // Space leader command grammar, separate from keyboard layout facts. // req: input/001, input/002, input/004, testing/001, testing/002, testing/003, testing/004 test { _ = Leader; } pub const Feature = enum { search, panel_close, lsp, diagnostics, jobs, }; pub const Action = union(enum) { none, save, quit, force_quit, open: []u8, symbol: symbol_mod.Symbol, file_picker, search_file, search_project, hover, signature, expand_hover, language_format, language_format_policy, language_organize_imports, language_code_actions, diagnostics_open, diagnostics_next, diagnostics_previous, diagnostics_filter, job_lint_file, job_lint_project, job_build, job_test, job_check, job_cancel, job_jump, job_yank, repeat_rail, restore_last, not_built: Feature, pub fn deinit(self: Action, allocator: std.mem.Allocator) void { switch (self) { .open => |path| allocator.free(path), else => {}, } } }; const Mode = enum { idle, rail, symbol_rail, search_rail, language_rail, diagnostic_rail, tool_rail, open_prompt, }; pub const Leader = struct { allocator: std.mem.Allocator, mode: Mode = .idle, open_prompt: std.ArrayList(u8) = .empty, message: ?[]const u8 = null, pub fn init(allocator: std.mem.Allocator) Leader { return .{ .allocator = allocator }; } pub fn deinit(self: *Leader) void { self.open_prompt.deinit(self.allocator); self.* = undefined; } pub fn handleEvent(self: *Leader, event: input.Event) !Action { switch (self.mode) { .idle => return self.handleIdle(event), .rail => return self.handleRail(event), .symbol_rail => return self.handleSymbolRail(event), .search_rail => return self.handleSearchRail(event), .language_rail => return self.handleLanguageRail(event), .diagnostic_rail => return self.handleDiagnosticRail(event), .tool_rail => return self.handleToolRail(event), .open_prompt => return self.handleOpenPrompt(event), } } pub fn status(self: *const Leader) []const u8 { if (self.message) |message| return message; return switch (self.mode) { .idle => "", .rail => "leader: w save q quit Q discard o open p symbols s search r repeat", .symbol_rail => symbol_mod.rail_status, .search_rail => "search: f current file p project s symbols", .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", }; } pub fn isActive(self: *const Leader) bool { return self.mode != .idle or self.message != null; } pub fn capturesInput(self: *const Leader) bool { return self.mode != .idle; } pub fn promptText(self: *const Leader) []const u8 { 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) { .key => |key| if (key == .space) { self.mode = .rail; return .none; }, else => {}, } return .none; } fn handleRail(self: *Leader, event: input.Event) !Action { self.message = null; switch (event) { .text => |text| { if (std.mem.eql(u8, text, "w")) { self.mode = .idle; return .save; } if (std.mem.eql(u8, text, "s")) { self.mode = .search_rail; return .none; } if (std.mem.eql(u8, text, "l")) { self.mode = .language_rail; return .none; } if (std.mem.eql(u8, text, "d")) { self.mode = .diagnostic_rail; return .diagnostics_open; } if (std.mem.eql(u8, text, "t")) { self.mode = .tool_rail; return .none; } if (std.mem.eql(u8, text, "q")) { self.mode = .idle; return .quit; } if (std.mem.eql(u8, text, "Q")) { self.mode = .idle; return .force_quit; } if (std.mem.eql(u8, text, "o")) { self.open_prompt.clearRetainingCapacity(); self.mode = .open_prompt; return .none; } if (std.mem.eql(u8, text, "f")) { self.mode = .idle; return .file_picker; } if (std.mem.eql(u8, text, "p")) { 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, "x")) { self.mode = .idle; self.message = "panel close is not built in this profile yet"; return .{ .not_built = .panel_close }; } self.mode = .idle; self.message = "unknown leader key"; return .none; }, .key => |key| switch (key) { .escape, .backspace => { self.mode = .idle; self.message = "leader cancelled"; return .none; }, .space => { self.mode = .idle; return .restore_last; }, else => { self.mode = .idle; self.message = "unknown leader key"; return .none; }, }, .unknown => { self.mode = .idle; self.message = "unknown leader key"; return .none; }, } } fn handleLanguageRail(self: *Leader, event: input.Event) Action { self.message = null; switch (event) { .text => |text| { self.mode = .idle; if (std.mem.eql(u8, text, "h")) return .hover; if (std.mem.eql(u8, text, "s")) return .signature; if (std.mem.eql(u8, text, "o")) return .language_organize_imports; if (std.mem.eql(u8, text, "f")) return .language_format; if (std.mem.eql(u8, text, "F")) return .language_format_policy; if (std.mem.eql(u8, text, "w")) return .language_format_policy; if (std.mem.eql(u8, text, "a")) return .language_code_actions; self.message = "language action is not built in this profile yet"; return .{ .not_built = .lsp }; }, .key => |key| switch (key) { .escape, .backspace => { self.mode = .idle; self.message = "language cancelled"; return .none; }, else => { self.mode = .idle; self.message = "unknown language key"; return .none; }, }, .unknown => { self.mode = .idle; self.message = "unknown language key"; return .none; }, } } fn handleDiagnosticRail(self: *Leader, event: input.Event) Action { self.message = null; switch (event) { .text => |text| { if (std.mem.eql(u8, text, "d")) { self.mode = .idle; return .diagnostics_open; } if (std.mem.eql(u8, text, "n")) { self.mode = .idle; return .diagnostics_next; } if (std.mem.eql(u8, text, "p")) { self.mode = .idle; return .diagnostics_previous; } if (std.mem.eql(u8, text, "f")) { self.mode = .idle; return .diagnostics_filter; } self.mode = .idle; self.message = "unknown diagnostics key"; return .none; }, .key => |key| switch (key) { .escape, .backspace => { self.mode = .idle; self.message = "diagnostics cancelled"; return .none; }, else => { self.mode = .idle; self.message = "unknown diagnostics key"; return .none; }, }, .unknown => { self.mode = .idle; self.message = "unknown diagnostics key"; return .none; }, } } fn handleToolRail(self: *Leader, event: input.Event) Action { self.message = null; switch (event) { .text => |text| { self.mode = .idle; if (std.mem.eql(u8, text, "l")) return .job_lint_file; if (std.mem.eql(u8, text, "L")) return .job_lint_project; if (std.mem.eql(u8, text, "b")) return .job_build; if (std.mem.eql(u8, text, "t")) return .job_test; if (std.mem.eql(u8, text, "c")) return .job_check; if (std.mem.eql(u8, text, "x")) return .job_cancel; if (std.mem.eql(u8, text, "j")) return .job_jump; if (std.mem.eql(u8, text, "y")) return .job_yank; self.message = "unknown tool key"; return .none; }, .key => |key| switch (key) { .escape, .backspace => { self.mode = .idle; self.message = "tools cancelled"; return .none; }, else => { self.mode = .idle; self.message = "unknown tool key"; return .none; }, }, .unknown => { self.mode = .idle; self.message = "unknown tool key"; return .none; }, } } fn handleSearchRail(self: *Leader, event: input.Event) Action { self.message = null; switch (event) { .text => |text| { self.mode = .idle; if (std.mem.eql(u8, text, "f")) return .search_file; if (std.mem.eql(u8, text, "p")) return .search_project; self.message = "search target is not built in this profile yet"; return .{ .not_built = .search }; }, .key => |key| switch (key) { .escape, .backspace => { self.mode = .idle; self.message = "search cancelled"; return .none; }, else => { self.mode = .idle; self.message = "unknown search key"; return .none; }, }, .unknown => { self.mode = .idle; self.message = "unknown search key"; return .none; }, } } fn handleSymbolRail(self: *Leader, event: input.Event) Action { self.message = null; switch (event) { .text => |text| { if (symbol_mod.lookup(text)) |entry| { self.mode = .idle; return .{ .symbol = entry.symbol }; } self.mode = .idle; self.message = "unknown symbol key"; return .none; }, .key => |key| switch (key) { .escape, .backspace => { self.mode = .idle; self.message = "symbol rail cancelled"; return .none; }, else => { self.mode = .idle; self.message = "unknown symbol key"; return .none; }, }, .unknown => { self.mode = .idle; self.message = "unknown symbol key"; return .none; }, } } fn handleOpenPrompt(self: *Leader, event: input.Event) !Action { self.message = null; switch (event) { .text => |text| { try self.open_prompt.appendSlice(self.allocator, text); return .none; }, .key => |key| switch (key) { .enter => { if (self.open_prompt.items.len == 0) { self.mode = .idle; self.message = "open cancelled: empty path"; return .none; } const path = try self.allocator.dupe(u8, self.open_prompt.items); self.open_prompt.clearRetainingCapacity(); self.mode = .idle; return .{ .open = path }; }, .backspace => { const previous = session_mod.previousBoundary(self.open_prompt.items, self.open_prompt.items.len); self.open_prompt.shrinkRetainingCapacity(previous); return .none; }, .escape => { self.open_prompt.clearRetainingCapacity(); self.mode = .idle; self.message = "open cancelled"; return .none; }, else => return .none, }, .unknown => { self.message = "open ignored unknown input"; return .none; }, } } }; fn expectActionTag(expected: std.meta.Tag(Action), action: Action) !void { try std.testing.expectEqual(expected, std.meta.activeTag(action)); } test "regular: space opens a visible leader rail and write dispatches" { var leader = Leader.init(std.testing.allocator); defer leader.deinit(); try expectActionTag(.none, try leader.handleEvent(input.normalize(" "))); try std.testing.expectEqualStrings("leader: w save q quit Q discard o open p symbols s search r repeat", leader.status()); try expectActionTag(.save, try leader.handleEvent(input.normalize("w"))); try std.testing.expect(!leader.isActive()); } test "regular: leader quit dispatches without Esc Ctrl Alt or function keys" { var leader = Leader.init(std.testing.allocator); defer leader.deinit(); try expectActionTag(.none, try leader.handleEvent(input.normalize(" "))); try expectActionTag(.quit, try leader.handleEvent(input.normalize("q"))); } test "regular: leader open prompt collects UTF-8 path and backspace respects codepoints" { var leader = Leader.init(std.testing.allocator); defer leader.deinit(); try expectActionTag(.none, try leader.handleEvent(input.normalize(" "))); try expectActionTag(.none, try leader.handleEvent(input.normalize("o"))); try std.testing.expectEqualStrings("open: type path, Enter opens, Esc cancels", leader.status()); try expectActionTag(.none, try leader.handleEvent(input.normalize("café"))); try std.testing.expectEqualStrings("café", leader.promptText()); try expectActionTag(.none, try leader.handleEvent(input.normalize("\x7f"))); try std.testing.expectEqualStrings("caf", leader.promptText()); try expectActionTag(.none, try leader.handleEvent(input.normalize(".zig"))); const action = try leader.handleEvent(input.normalize("\r")); defer action.deinit(std.testing.allocator); switch (action) { .open => |path| try std.testing.expectEqualStrings("caf.zig", path), else => return error.ExpectedOpenAction, } } test "regular: search rail opens current-file search and rejects other targets" { var leader = Leader.init(std.testing.allocator); defer leader.deinit(); try expectActionTag(.none, try leader.handleEvent(input.normalize(" "))); try expectActionTag(.none, try leader.handleEvent(input.normalize("s"))); try std.testing.expectEqualStrings("search: f current file p project s symbols", leader.status()); try expectActionTag(.search_file, try leader.handleEvent(input.normalize("f"))); try expectActionTag(.none, try leader.handleEvent(input.normalize(" "))); try expectActionTag(.none, try leader.handleEvent(input.normalize("s"))); try expectActionTag(.search_project, try leader.handleEvent(input.normalize("p"))); } test "adversarial: unknown leader key does not dispatch and recovers to idle" { var leader = Leader.init(std.testing.allocator); defer leader.deinit(); try expectActionTag(.none, try leader.handleEvent(input.normalize(" "))); try expectActionTag(.none, try leader.handleEvent(input.normalize("?"))); try std.testing.expectEqualStrings("unknown leader key", leader.status()); try expectActionTag(.none, try leader.handleEvent(input.normalize("a"))); } test "adversarial: empty open and escape cancel without dispatching" { var leader = Leader.init(std.testing.allocator); defer leader.deinit(); try expectActionTag(.none, try leader.handleEvent(input.normalize(" "))); try expectActionTag(.none, try leader.handleEvent(input.normalize("o"))); try expectActionTag(.none, try leader.handleEvent(input.normalize("\r"))); try std.testing.expectEqualStrings("open cancelled: empty path", leader.status()); try expectActionTag(.none, try leader.handleEvent(input.normalize(" "))); try expectActionTag(.none, try leader.handleEvent(input.normalize("o"))); try expectActionTag(.none, try leader.handleEvent(input.normalize("x"))); try expectActionTag(.none, try leader.handleEvent(input.normalize("\x1b"))); try std.testing.expectEqualStrings("open cancelled", leader.status()); } test "regular: leader symbol rail dispatches coding symbols by letters" { var leader = Leader.init(std.testing.allocator); defer leader.deinit(); try expectActionTag(.none, try leader.handleEvent(input.normalize(" "))); try expectActionTag(.none, try leader.handleEvent(input.normalize("p"))); try std.testing.expectEqualStrings(symbol_mod.rail_status, leader.status()); const action = try leader.handleEvent(input.normalize("c")); switch (action) { .symbol => |symbol| try std.testing.expectEqual(symbol_mod.Symbol.braces, symbol), else => return error.ExpectedSymbolAction, } } test "regular: leader symbol rail uses letters for hard-to-reach punctuation" { var leader = Leader.init(std.testing.allocator); defer leader.deinit(); inline for (.{ .{ "s", symbol_mod.Symbol.slash }, .{ "v", symbol_mod.Symbol.pipe }, .{ "q", symbol_mod.Symbol.double_quote }, .{ "e", symbol_mod.Symbol.single_quote }, .{ "t", symbol_mod.Symbol.backtick }, .{ "u", symbol_mod.Symbol.underscore }, }) |case| { try expectActionTag(.none, try leader.handleEvent(input.normalize(" "))); try expectActionTag(.none, try leader.handleEvent(input.normalize("p"))); const action = try leader.handleEvent(input.normalize(case[0])); switch (action) { .symbol => |actual| try std.testing.expectEqual(case[1], actual), else => return error.ExpectedSymbolAction, } } } test "adversarial: unknown and cancelled symbol rail inputs do not dispatch" { var leader = Leader.init(std.testing.allocator); defer leader.deinit(); try expectActionTag(.none, try leader.handleEvent(input.normalize(" "))); try expectActionTag(.none, try leader.handleEvent(input.normalize("p"))); try expectActionTag(.none, try leader.handleEvent(input.normalize("?"))); try std.testing.expectEqualStrings("unknown symbol key", leader.status()); try expectActionTag(.none, try leader.handleEvent(input.normalize(" "))); try expectActionTag(.none, try leader.handleEvent(input.normalize("p"))); try expectActionTag(.none, try leader.handleEvent(input.normalize("\x1b"))); try std.testing.expectEqualStrings("symbol rail cancelled", leader.status()); }