588 lines · 21.8 KB · TypeScript
RawDownload
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.