| 1 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as Schema from "effect/Schema"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | |
| 5 | import { |
| 6 | KeybindingsConfig, |
| 7 | KeybindingRule, |
| 8 | ResolvedKeybindingRule, |
| 9 | ResolvedKeybindingsConfig, |
| 10 | } from "./keybindings.ts"; |
| 11 | |
| 12 | const 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 | |
| 22 | const decodeResolvedRule = Schema.decodeUnknownEffect(ResolvedKeybindingRule as never); |
| 23 | const encodeResolvedKeybindings = Schema.encodeEffect(ResolvedKeybindingsConfig); |
| 24 | |
| 25 | it.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 | |
| 157 | it.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 | |
| 169 | it.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 | |
| 179 | it.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 | |
| 190 | it.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 | |
| 215 | it.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 | |
| 245 | const shortcut = { |
| 246 | key: "p", |
| 247 | metaKey: false, |
| 248 | ctrlKey: false, |
| 249 | shiftKey: false, |
| 250 | altKey: false, |
| 251 | modKey: true, |
| 252 | }; |
| 253 | |
| 254 | it.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 | |
| 268 | it.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 | |
| 285 | it.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 | |
| 299 | it.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 | |
| 309 | it.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 | ); |