1import { assert, it } from "@effect/vitest";
2import * as Schema from "effect/Schema";
3import * as Effect from "effect/Effect";
4
5import {
6 KeybindingsConfig,
7 KeybindingRule,
8 ResolvedKeybindingRule,
9 ResolvedKeybindingsConfig,
10} from "./keybindings.ts";
11
12const decode = <S extends Schema.Top>(
13 schema: S,
14 input: unknown,
15): Effect.Effect<Schema.Schema.Type<S>, Schema.SchemaError, never> =>
16 Schema.decodeUnknownEffect(schema as never)(input) as Effect.Effect<
17 Schema.Schema.Type<S>,
18 Schema.SchemaError,
19 never
20 >;
21
22const decodeResolvedRule = Schema.decodeUnknownEffect(ResolvedKeybindingRule as never);
23const encodeResolvedKeybindings = Schema.encodeEffect(ResolvedKeybindingsConfig);
24
25it.effect("parses keybinding rules", () =>
26 Effect.gen(function* () {
27 const parsed = yield* decode(KeybindingRule, {
28 key: "mod+j",
29 command: "terminal.toggle",
30 });
31 assert.strictEqual(parsed.command, "terminal.toggle");
32
33 const parsedSidebarToggle = yield* decode(KeybindingRule, {
34 key: "mod+b",
35 command: "sidebar.toggle",
36 });
37 assert.strictEqual(parsedSidebarToggle.command, "sidebar.toggle");
38
39 const parsedRightPanelToggle = yield* decode(KeybindingRule, {
40 key: "mod+alt+b",
41 command: "rightPanel.toggle",
42 });
43 assert.strictEqual(parsedRightPanelToggle.command, "rightPanel.toggle");
44
45 const parsedThreadPanelToggle = yield* decode(KeybindingRule, {
46 key: "mod+shift+b",
47 command: "threadPanel.toggle",
48 });
49 assert.strictEqual(parsedThreadPanelToggle.command, "threadPanel.toggle");
50 const parsedRightPanelToggleMaximized = yield* decode(KeybindingRule, {
51 key: "mod+shift+m",
52 command: "rightPanel.toggleMaximized",
53 });
54 assert.strictEqual(parsedRightPanelToggleMaximized.command, "rightPanel.toggleMaximized");
55
56 const parsedClose = yield* decode(KeybindingRule, {
57 key: "mod+w",
58 command: "terminal.close",
59 });
60 assert.strictEqual(parsedClose.command, "terminal.close");
61
62 const parsedDiffToggle = yield* decode(KeybindingRule, {
63 key: "mod+d",
64 command: "diff.toggle",
65 });
66 assert.strictEqual(parsedDiffToggle.command, "diff.toggle");
67
68 const parsedCommandPalette = yield* decode(KeybindingRule, {
69 key: "mod+k",
70 command: "commandPalette.toggle",
71 });
72 assert.strictEqual(parsedCommandPalette.command, "commandPalette.toggle");
73
74 const parsedFilePicker = yield* decode(KeybindingRule, {
75 key: "mod+p",
76 command: "filePicker.toggle",
77 });
78 assert.strictEqual(parsedFilePicker.command, "filePicker.toggle");
79
80 const parsedProjectSearch = yield* decode(KeybindingRule, {
81 key: "mod+shift+f",
82 command: "projectSearch.toggle",
83 });
84 assert.strictEqual(parsedProjectSearch.command, "projectSearch.toggle");
85
86 const parsedUsageOpen = yield* decode(KeybindingRule, {
87 key: "mod+u",
88 command: "usage.open",
89 });
90 assert.strictEqual(parsedUsageOpen.command, "usage.open");
91
92 const parsedThemeEditor = yield* decode(KeybindingRule, {
93 key: "mod+alt+shift+t",
94 command: "themeEditor.toggle",
95 });
96 assert.strictEqual(parsedThemeEditor.command, "themeEditor.toggle");
97
98 const parsedLocal = yield* decode(KeybindingRule, {
99 key: "mod+shift+n",
100 command: "chat.newLocal",
101 });
102 assert.strictEqual(parsedLocal.command, "chat.newLocal");
103
104 const parsedFind = yield* decode(KeybindingRule, {
105 key: "mod+f",
106 command: "chat.find",
107 });
108 assert.strictEqual(parsedFind.command, "chat.find");
109
110 const parsedModelPickerToggle = yield* decode(KeybindingRule, {
111 key: "mod+shift+m",
112 command: "modelPicker.toggle",
113 });
114 assert.strictEqual(parsedModelPickerToggle.command, "modelPicker.toggle");
115
116 const parsedModelPickerJump = yield* decode(KeybindingRule, {
117 key: "mod+1",
118 command: "modelPicker.jump.1",
119 });
120 assert.strictEqual(parsedModelPickerJump.command, "modelPicker.jump.1");
121
122 const parsedThreadPrevious = yield* decode(KeybindingRule, {
123 key: "mod+shift+[",
124 command: "thread.previous",
125 });
126 assert.strictEqual(parsedThreadPrevious.command, "thread.previous");
127
128 const parsedThreadSettle = yield* decode(KeybindingRule, {
129 key: "mod+shift+s",
130 command: "thread.settle",
131 when: "!terminalFocus",
132 });
133 assert.strictEqual(parsedThreadSettle.command, "thread.settle");
134
135 const parsedThreadCopyReference = yield* decode(KeybindingRule, {
136 key: "mod+shift+c",
137 command: "thread.copyReference",
138 when: "!terminalFocus",
139 });
140 assert.strictEqual(parsedThreadCopyReference.command, "thread.copyReference");
141
142 const parsedPullRequestCopyNumber = yield* decode(KeybindingRule, {
143 key: "mod+shift+k",
144 command: "pullRequest.copyNumber",
145 when: "!terminalFocus",
146 });
147 assert.strictEqual(parsedPullRequestCopyNumber.command, "pullRequest.copyNumber");
148
149 const parsedThreadStop = yield* decode(KeybindingRule, {
150 key: "mod+escape",
151 command: "thread.stop",
152 });
153 assert.strictEqual(parsedThreadStop.command, "thread.stop");
154 }),
155);
156
157it.effect("rejects invalid command values", () =>
158 Effect.gen(function* () {
159 const result = yield* Effect.exit(
160 decode(KeybindingRule, {
161 key: "mod+j",
162 command: "script.Test.run",
163 }),
164 );
165 assert.strictEqual(result._tag, "Failure");
166 }),
167);
168
169it.effect("accepts dynamic script run commands", () =>
170 Effect.gen(function* () {
171 const parsed = yield* decode(KeybindingRule, {
172 key: "mod+r",
173 command: "script.setup.run",
174 });
175 assert.strictEqual(parsed.command, "script.setup.run");
176 }),
177);
178
179it.effect("parses keybindings array payload", () =>
180 Effect.gen(function* () {
181 const parsed = yield* decode(KeybindingsConfig, [
182 { key: "mod+j", command: "terminal.toggle" },
183 { key: "mod+d", command: "terminal.split", when: "terminalFocus" },
184 { key: "mod+shift+d", command: "terminal.splitVertical", when: "terminalFocus" },
185 ]);
186 assert.lengthOf(parsed, 3);
187 }),
188);
189
190it.effect("parses resolved keybinding rules", () =>
191 Effect.gen(function* () {
192 const parsed = yield* decode(ResolvedKeybindingRule, {
193 command: "terminal.split",
194 shortcut: {
195 key: "d",
196 metaKey: false,
197 ctrlKey: false,
198 shiftKey: false,
199 altKey: false,
200 modKey: true,
201 },
202 whenAst: {
203 type: "and",
204 left: { type: "identifier", name: "terminalOpen" },
205 right: {
206 type: "not",
207 node: { type: "identifier", name: "terminalFocus" },
208 },
209 },
210 });
211 assert.strictEqual(parsed.shortcut.key, "d");
212 }),
213);
214
215it.effect("parses resolved keybindings arrays", () =>
216 Effect.gen(function* () {
217 const parsed = yield* decode(ResolvedKeybindingsConfig, [
218 {
219 command: "terminal.toggle",
220 shortcut: {
221 key: "j",
222 metaKey: false,
223 ctrlKey: false,
224 shiftKey: false,
225 altKey: false,
226 modKey: true,
227 },
228 },
229 {
230 command: "thread.jump.3",
231 shortcut: {
232 key: "3",
233 metaKey: false,
234 ctrlKey: false,
235 shiftKey: false,
236 altKey: false,
237 modKey: true,
238 },
239 },
240 ]);
241 assert.lengthOf(parsed, 2);
242 }),
243);
244
245const shortcut = {
246 key: "p",
247 metaKey: false,
248 ctrlKey: false,
249 shiftKey: false,
250 altKey: false,
251 modKey: true,
252};
253
254it.effect("drops resolved rules with commands this build does not know", () =>
255 Effect.gen(function* () {
256 const parsed = yield* decode(ResolvedKeybindingsConfig, [
257 { command: "terminal.toggle", shortcut },
258 { command: "someFuture.toggle", shortcut },
259 { command: "filePicker.toggle", shortcut },
260 ]);
261 assert.deepEqual(
262 parsed.map((rule) => rule.command),
263 ["terminal.toggle", "filePicker.toggle"],
264 );
265 }),
266);
267
268it.effect("drops resolved rules with unknown when-node types", () =>
269 Effect.gen(function* () {
270 const parsed = yield* decode(ResolvedKeybindingsConfig, [
271 {
272 command: "terminal.toggle",
273 shortcut,
274 whenAst: { type: "xor", left: 1, right: 2 },
275 },
276 { command: "terminal.split", shortcut },
277 ]);
278 assert.deepEqual(
279 parsed.map((rule) => rule.command),
280 ["terminal.split"],
281 );
282 }),
283);
284
285it.effect("drops malformed resolved rule entries", () =>
286 Effect.gen(function* () {
287 const parsed = yield* decode(ResolvedKeybindingsConfig, [
288 "garbage",
289 { command: "terminal.toggle", shortcut },
290 null,
291 ]);
292 assert.deepEqual(
293 parsed.map((rule) => rule.command),
294 ["terminal.toggle"],
295 );
296 }),
297);
298
299it.effect("encodes resolved keybindings to the plain wire shape", () =>
300 Effect.gen(function* () {
301 const rules = [{ command: "terminal.toggle" as const, shortcut }];
302 const encoded = yield* encodeResolvedKeybindings(rules);
303 assert.deepEqual(encoded, rules);
304 const roundTripped = yield* decode(ResolvedKeybindingsConfig, encoded);
305 assert.deepEqual(roundTripped, rules);
306 }),
307);
308
309it.effect("drops unknown fields in resolved keybinding rules", () =>
310 decodeResolvedRule({
311 command: "terminal.toggle",
312 shortcut: {
313 key: "j",
314 metaKey: false,
315 ctrlKey: false,
316 shiftKey: false,
317 altKey: false,
318 modKey: true,
319 },
320 key: "mod+j",
321 }).pipe(
322 Effect.map((parsed) => {
323 const view = parsed as Record<string, unknown>;
324 assert.strictEqual("key" in view, false);
325 assert.strictEqual(view.command, "terminal.toggle");
326 }),
327 ),
328);