import { KeybindingCommand, KeybindingRule, KeybindingsConfig } from "@t3tools/contracts";import * as NodeServices from "@effect/platform-node/NodeServices";import { assert, it } from "@effect/vitest";import { assertFailure } from "@effect/vitest/utils";import * as Cause from "effect/Cause";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Logger from "effect/Logger";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import * as ServerConfig from "./config.ts";import * as Keybindings from "./keybindings.ts";import { KeybindingsConfigError } from "@t3tools/contracts";import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson);const decodeKeybindingsConfigJson = Schema.decodeUnknownEffect(KeybindingsConfigJson);const encodeResolvedKeybindingFromConfig = Schema.encodeEffect( Keybindings.ResolvedKeybindingFromConfig,);const decodeResolvedKeybindingFromConfigExit = Schema.decodeUnknownExit( Keybindings.ResolvedKeybindingFromConfig,);const makeKeybindingsLayer = () => { return Keybindings.layer.pipe( Layer.provideMerge( Layer.fresh( ServerConfig.layerTest(process.cwd(), { prefix: "t3code-keybindings-test-", }), ), ), );}; const toDetailResult = <A, R>(effect: Effect.Effect<A, KeybindingsConfigError, R>) => effect.pipe( Effect.mapError((error) => error.detail), Effect.result, ); const writeKeybindingsConfig = (configPath: string, rules: readonly KeybindingRule[]) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const encoded = yield* encodeKeybindingsConfigJson(rules); yield* fileSystem.makeDirectory(path.dirname(configPath), { recursive: true }); yield* fileSystem.writeFileString(configPath, encoded); }); const readKeybindingsConfig = (configPath: string) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const rawConfig = yield* fileSystem.readFileString(configPath); return yield* decodeKeybindingsConfigJson(rawConfig); }); it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("parses shortcuts including plus key", () => Effect.sync(() => { assert.deepEqual(Keybindings.parseKeybindingShortcut("mod+j"), { key: "j", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }); assert.deepEqual(Keybindings.parseKeybindingShortcut("mod++"), { key: "+", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }); }), ); it.effect("compiles valid rule with parsed when AST", () => Effect.sync(() => { const compiled = Keybindings.compileResolvedKeybindingRule({ key: "mod+d", command: "terminal.split", when: "terminalOpen && !terminalFocus", }); assert.deepEqual(compiled, { command: "terminal.split", shortcut: { key: "d", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }, whenAst: { type: "and", left: { type: "identifier", name: "terminalOpen" }, right: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, }); }), ); it.effect("encodes resolved plus-key shortcuts", () => Effect.gen(function* () { const encoded = yield* encodeResolvedKeybindingFromConfig({ command: "terminal.toggle", shortcut: { key: "+", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }, }); assert.equal(encoded.key, "mod++"); assert.equal(encoded.command, "terminal.toggle"); }), ); it.effect("rejects invalid rules", () => Effect.sync(() => { assert.isNull( Keybindings.compileResolvedKeybindingRule({ key: "mod+shift+d+o", command: "terminal.new", }), ); assert.isNull( Keybindings.compileResolvedKeybindingRule({ key: "mod+d", command: "terminal.split", when: "terminalFocus && (", }), ); assert.isNull( Keybindings.compileResolvedKeybindingRule({ key: "mod+d", command: "terminal.split", when: `${"!".repeat(300)}terminalFocus`, }), ); }), ); it.effect("formats invalid resolved keybinding rules with the custom message", () => Effect.sync(() => { const result = decodeResolvedKeybindingFromConfigExit({ key: "mod+shift+d+o", command: "terminal.new", }); if (result._tag !== "Failure") { assert.fail("Expected invalid keybinding decode to fail"); } const detail = Cause.pretty(result.cause); assert.isTrue(detail.includes("Invalid keybinding rule")); assert.isFalse(detail.includes("Invalid data")); }), ); it.effect("bootstraps default keybindings when config file is missing", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; assert.isFalse(yield* fs.exists(keybindingsConfigPath)); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; yield* keybindings.syncDefaultKeybindingsOnStartup; }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); assert.deepEqual(persisted, Keybindings.DEFAULT_KEYBINDINGS); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("uses defaults in runtime when config is malformed without overriding file", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* fs.writeFileString(keybindingsConfigPath, "{ not-json"); const configState = yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.loadConfigState; }); assert.deepEqual( configState.keybindings, Keybindings.compileResolvedKeybindingsConfig(Keybindings.DEFAULT_KEYBINDINGS), ); assert.deepEqual(configState.issues, [ { kind: "keybindings.malformed-config", message: configState.issues[0]?.message ?? "", }, ]); assert.equal(yield* fs.readFileString(keybindingsConfigPath), "{ not-json"); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("ignores invalid entries in runtime and reports them as issues", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* fs.writeFileString( keybindingsConfigPath, // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify([ { key: "mod+j", command: "terminal.toggle" }, { key: "mod+shift+d+o", command: "terminal.new" }, { key: "mod+x", command: "invalid.command" }, ]), ); const configState = yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.loadConfigState; }); assert.isTrue(configState.keybindings.some((entry) => entry.command === "terminal.toggle")); assert.isFalse( configState.keybindings.some((entry) => String(entry.command) === "invalid.command"), ); assert.deepEqual(configState.issues, [ { kind: "keybindings.invalid-entry", index: 1, message: configState.issues[0]?.message ?? "", }, { kind: "keybindings.invalid-entry", index: 2, message: configState.issues[1]?.message ?? "", }, ]); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect( "upserts missing default keybindings on startup without overriding existing command rules", () => Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+shift+t", command: "terminal.toggle" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; yield* keybindings.syncDefaultKeybindingsOnStartup; }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); const byCommand = new Map(persisted.map((entry) => [entry.command, entry])); const persistedToggle = byCommand.get("terminal.toggle"); assert.isNotNull(persistedToggle); assert.equal(persistedToggle?.key, "mod+shift+t"); assert.isFalse( persisted.some((entry) => entry.command === "terminal.toggle" && entry.key === "mod+j"), ); for (const defaultRule of Keybindings.DEFAULT_KEYBINDINGS) { assert.isTrue(byCommand.has(defaultRule.command), `expected ${defaultRule.command}`); } assert.isTrue(byCommand.has("script.run-tests.run")); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("skips conflicting default keybindings on startup and logs a detailed warning", () => { const messages: string[] = []; const logger = Logger.make(({ message }) => { messages.push(String(message)); }); return Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+j", command: "script.custom-action.run" }, ]); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; yield* keybindings.syncDefaultKeybindingsOnStartup; }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); assert.isFalse(persisted.some((entry) => entry.command === "terminal.toggle")); assert.isTrue(persisted.some((entry) => entry.command === "script.custom-action.run")); assert.isTrue( messages.some((message) => message.includes("skipping default keybinding due to shortcut conflict"), ), ); }).pipe( Effect.provide( Layer.mergeAll( makeKeybindingsLayer(), Logger.layer([logger], { mergeWithExisting: false }), ), ), ); }); it.effect("upserts custom keybindings to configured path", () => Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+j", command: "terminal.toggle" }, ]); const resolved = yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", }); }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); const persistedView = persisted.map(({ key, command }) => ({ key, command })); assert.deepEqual(persistedView, [ { key: "mod+j", command: "terminal.toggle" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); assert.isTrue(resolved.some((entry) => entry.command === "script.run-tests.run")); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("appends additional custom keybindings for the same command", () => Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", }); }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); const persistedView = persisted.map(({ key, command }) => ({ key, command })); assert.deepEqual(persistedView, [ { key: "mod+r", command: "script.run-tests.run" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("replaces only the targeted custom keybinding", () => Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+r", command: "script.run-tests.run" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+alt+r", command: "script.run-tests.run", replace: { key: "mod+r", command: "script.run-tests.run" }, }); }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); const persistedView = persisted.map(({ key, command }) => ({ key, command })); assert.deepEqual(persistedView, [ { key: "mod+shift+r", command: "script.run-tests.run" }, { key: "mod+alt+r", command: "script.run-tests.run" }, ]); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("replacing with a rule that already exists elsewhere does not duplicate it", () => Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+r", command: "script.run-tests.run" }, { key: "mod+alt+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+alt+r", command: "script.run-tests.run", replace: { key: "mod+r", command: "script.run-tests.run" }, }); }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); const persistedView = persisted.map(({ key, command }) => ({ key, command })); assert.deepEqual(persistedView, [{ key: "mod+alt+r", command: "script.run-tests.run" }]); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("removes only the targeted custom keybinding", () => Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+r", command: "script.run-tests.run" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.removeKeybindingRule({ key: "mod+r", command: "script.run-tests.run", }); }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); const persistedView = persisted.map(({ key, command }) => ({ key, command })); assert.deepEqual(persistedView, [{ key: "mod+shift+r", command: "script.run-tests.run" }]); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("refuses to overwrite malformed keybindings config", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* fs.writeFileString(keybindingsConfigPath, "{ not-json"); const result = yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", }); }).pipe(toDetailResult); assertFailure(result, "expected JSON array"); const persistedRaw = yield* fs.readFileString(keybindingsConfigPath); assert.equal(persistedRaw, "{ not-json"); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("reports non-array config parse errors without duplicate prefix", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* fs.writeFileString( keybindingsConfigPath, '{"key":"mod+j","command":"terminal.toggle"}', ); const firstResult = yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", }); }).pipe(toDetailResult); assertFailure(firstResult, "expected JSON array"); const secondResult = yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", }); }).pipe(toDetailResult); assertFailure(secondResult, "expected JSON array"); }).pipe(Effect.provide(makeKeybindingsLayer())), ); // chmod cannot make a directory unwritable on Windows, so the write succeeds. it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( "fails when config directory is not writable", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; const { dirname } = yield* Path.Path; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+j", command: "terminal.toggle" }, ]); yield* fs.chmod(dirname(keybindingsConfigPath), 0o500); Lines 1–500 of 588. Load more
import { KeybindingCommand, KeybindingRule, KeybindingsConfig } from "@t3tools/contracts"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import { assertFailure } from "@effect/vitest/utils"; import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Logger from "effect/Logger"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as ServerConfig from "./config.ts"; import * as Keybindings from "./keybindings.ts"; import { KeybindingsConfigError } from "@t3tools/contracts"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig); const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson); const decodeKeybindingsConfigJson = Schema.decodeUnknownEffect(KeybindingsConfigJson); const encodeResolvedKeybindingFromConfig = Schema.encodeEffect( Keybindings.ResolvedKeybindingFromConfig, ); const decodeResolvedKeybindingFromConfigExit = Schema.decodeUnknownExit( Keybindings.ResolvedKeybindingFromConfig, ); const makeKeybindingsLayer = () => { return Keybindings.layer.pipe( Layer.provideMerge( Layer.fresh( ServerConfig.layerTest(process.cwd(), { prefix: "t3code-keybindings-test-", }), ), ), ); }; const toDetailResult = <A, R>(effect: Effect.Effect<A, KeybindingsConfigError, R>) => effect.pipe( Effect.mapError((error) => error.detail), Effect.result, ); const writeKeybindingsConfig = (configPath: string, rules: readonly KeybindingRule[]) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const encoded = yield* encodeKeybindingsConfigJson(rules); yield* fileSystem.makeDirectory(path.dirname(configPath), { recursive: true }); yield* fileSystem.writeFileString(configPath, encoded); }); const readKeybindingsConfig = (configPath: string) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const rawConfig = yield* fileSystem.readFileString(configPath); return yield* decodeKeybindingsConfigJson(rawConfig); }); it.layer(NodeServices.layer)("keybindings", (it) => { it.effect("parses shortcuts including plus key", () => Effect.sync(() => { assert.deepEqual(Keybindings.parseKeybindingShortcut("mod+j"), { key: "j", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }); assert.deepEqual(Keybindings.parseKeybindingShortcut("mod++"), { key: "+", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }); }), ); it.effect("compiles valid rule with parsed when AST", () => Effect.sync(() => { const compiled = Keybindings.compileResolvedKeybindingRule({ key: "mod+d", command: "terminal.split", when: "terminalOpen && !terminalFocus", }); assert.deepEqual(compiled, { command: "terminal.split", shortcut: { key: "d", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }, whenAst: { type: "and", left: { type: "identifier", name: "terminalOpen" }, right: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, }); }), ); it.effect("encodes resolved plus-key shortcuts", () => Effect.gen(function* () { const encoded = yield* encodeResolvedKeybindingFromConfig({ command: "terminal.toggle", shortcut: { key: "+", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }, }); assert.equal(encoded.key, "mod++"); assert.equal(encoded.command, "terminal.toggle"); }), ); it.effect("rejects invalid rules", () => Effect.sync(() => { assert.isNull( Keybindings.compileResolvedKeybindingRule({ key: "mod+shift+d+o", command: "terminal.new", }), ); assert.isNull( Keybindings.compileResolvedKeybindingRule({ key: "mod+d", command: "terminal.split", when: "terminalFocus && (", }), ); assert.isNull( Keybindings.compileResolvedKeybindingRule({ key: "mod+d", command: "terminal.split", when: `${"!".repeat(300)}terminalFocus`, }), ); }), ); it.effect("formats invalid resolved keybinding rules with the custom message", () => Effect.sync(() => { const result = decodeResolvedKeybindingFromConfigExit({ key: "mod+shift+d+o", command: "terminal.new", }); if (result._tag !== "Failure") { assert.fail("Expected invalid keybinding decode to fail"); } const detail = Cause.pretty(result.cause); assert.isTrue(detail.includes("Invalid keybinding rule")); assert.isFalse(detail.includes("Invalid data")); }), ); it.effect("bootstraps default keybindings when config file is missing", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; assert.isFalse(yield* fs.exists(keybindingsConfigPath)); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; yield* keybindings.syncDefaultKeybindingsOnStartup; }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); assert.deepEqual(persisted, Keybindings.DEFAULT_KEYBINDINGS); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("uses defaults in runtime when config is malformed without overriding file", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* fs.writeFileString(keybindingsConfigPath, "{ not-json"); const configState = yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.loadConfigState; }); assert.deepEqual( configState.keybindings, Keybindings.compileResolvedKeybindingsConfig(Keybindings.DEFAULT_KEYBINDINGS), ); assert.deepEqual(configState.issues, [ { kind: "keybindings.malformed-config", message: configState.issues[0]?.message ?? "", }, ]); assert.equal(yield* fs.readFileString(keybindingsConfigPath), "{ not-json"); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("ignores invalid entries in runtime and reports them as issues", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* fs.writeFileString( keybindingsConfigPath, // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify([ { key: "mod+j", command: "terminal.toggle" }, { key: "mod+shift+d+o", command: "terminal.new" }, { key: "mod+x", command: "invalid.command" }, ]), ); const configState = yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.loadConfigState; }); assert.isTrue(configState.keybindings.some((entry) => entry.command === "terminal.toggle")); assert.isFalse( configState.keybindings.some((entry) => String(entry.command) === "invalid.command"), ); assert.deepEqual(configState.issues, [ { kind: "keybindings.invalid-entry", index: 1, message: configState.issues[0]?.message ?? "", }, { kind: "keybindings.invalid-entry", index: 2, message: configState.issues[1]?.message ?? "", }, ]); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect( "upserts missing default keybindings on startup without overriding existing command rules", () => Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+shift+t", command: "terminal.toggle" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; yield* keybindings.syncDefaultKeybindingsOnStartup; }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); const byCommand = new Map(persisted.map((entry) => [entry.command, entry])); const persistedToggle = byCommand.get("terminal.toggle"); assert.isNotNull(persistedToggle); assert.equal(persistedToggle?.key, "mod+shift+t"); assert.isFalse( persisted.some((entry) => entry.command === "terminal.toggle" && entry.key === "mod+j"), ); for (const defaultRule of Keybindings.DEFAULT_KEYBINDINGS) { assert.isTrue(byCommand.has(defaultRule.command), `expected ${defaultRule.command}`); } assert.isTrue(byCommand.has("script.run-tests.run")); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("skips conflicting default keybindings on startup and logs a detailed warning", () => { const messages: string[] = []; const logger = Logger.make(({ message }) => { messages.push(String(message)); }); return Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+j", command: "script.custom-action.run" }, ]); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; yield* keybindings.syncDefaultKeybindingsOnStartup; }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); assert.isFalse(persisted.some((entry) => entry.command === "terminal.toggle")); assert.isTrue(persisted.some((entry) => entry.command === "script.custom-action.run")); assert.isTrue( messages.some((message) => message.includes("skipping default keybinding due to shortcut conflict"), ), ); }).pipe( Effect.provide( Layer.mergeAll( makeKeybindingsLayer(), Logger.layer([logger], { mergeWithExisting: false }), ), ), ); }); it.effect("upserts custom keybindings to configured path", () => Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+j", command: "terminal.toggle" }, ]); const resolved = yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", }); }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); const persistedView = persisted.map(({ key, command }) => ({ key, command })); assert.deepEqual(persistedView, [ { key: "mod+j", command: "terminal.toggle" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); assert.isTrue(resolved.some((entry) => entry.command === "script.run-tests.run")); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("appends additional custom keybindings for the same command", () => Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", }); }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); const persistedView = persisted.map(({ key, command }) => ({ key, command })); assert.deepEqual(persistedView, [ { key: "mod+r", command: "script.run-tests.run" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("replaces only the targeted custom keybinding", () => Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+r", command: "script.run-tests.run" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+alt+r", command: "script.run-tests.run", replace: { key: "mod+r", command: "script.run-tests.run" }, }); }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); const persistedView = persisted.map(({ key, command }) => ({ key, command })); assert.deepEqual(persistedView, [ { key: "mod+shift+r", command: "script.run-tests.run" }, { key: "mod+alt+r", command: "script.run-tests.run" }, ]); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("replacing with a rule that already exists elsewhere does not duplicate it", () => Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+r", command: "script.run-tests.run" }, { key: "mod+alt+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+alt+r", command: "script.run-tests.run", replace: { key: "mod+r", command: "script.run-tests.run" }, }); }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); const persistedView = persisted.map(({ key, command }) => ({ key, command })); assert.deepEqual(persistedView, [{ key: "mod+alt+r", command: "script.run-tests.run" }]); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("removes only the targeted custom keybinding", () => Effect.gen(function* () { const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+r", command: "script.run-tests.run" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.removeKeybindingRule({ key: "mod+r", command: "script.run-tests.run", }); }); const persisted = yield* readKeybindingsConfig(keybindingsConfigPath); const persistedView = persisted.map(({ key, command }) => ({ key, command })); assert.deepEqual(persistedView, [{ key: "mod+shift+r", command: "script.run-tests.run" }]); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("refuses to overwrite malformed keybindings config", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* fs.writeFileString(keybindingsConfigPath, "{ not-json"); const result = yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", }); }).pipe(toDetailResult); assertFailure(result, "expected JSON array"); const persistedRaw = yield* fs.readFileString(keybindingsConfigPath); assert.equal(persistedRaw, "{ not-json"); }).pipe(Effect.provide(makeKeybindingsLayer())), ); it.effect("reports non-array config parse errors without duplicate prefix", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; yield* fs.writeFileString( keybindingsConfigPath, '{"key":"mod+j","command":"terminal.toggle"}', ); const firstResult = yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", }); }).pipe(toDetailResult); assertFailure(firstResult, "expected JSON array"); const secondResult = yield* Effect.gen(function* () { const keybindings = yield* Keybindings.Keybindings; return yield* keybindings.upsertKeybindingRule({ key: "mod+shift+r", command: "script.run-tests.run", }); }).pipe(toDetailResult); assertFailure(secondResult, "expected JSON array"); }).pipe(Effect.provide(makeKeybindingsLayer())), ); // chmod cannot make a directory unwritable on Windows, so the write succeeds. it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")( "fails when config directory is not writable", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig; const { dirname } = yield* Path.Path; yield* writeKeybindingsConfig(keybindingsConfigPath, [ { key: "mod+j", command: "terminal.toggle" }, ]); yield* fs.chmod(dirname(keybindingsConfigPath), 0o500);