diff --git a/args_binder.ts b/args_binder.ts new file mode 100644 index 0000000..249c8bc --- /dev/null +++ b/args_binder.ts @@ -0,0 +1,92 @@ +import type { Denops } from "@denops/std"; +import { type Derivable, derive } from "@vim-fall/custom/derivable"; + +import type { Detail } from "./item.ts"; +import { defineSource, type Source } from "./source.ts"; +import { type Curator, defineCurator } from "./curator.ts"; + +/** + * A type that represents a list of strings or a function which gets a denops + * instance and returns a list of strings. + */ +export type BoundArgsProvider = + | string[] + | ((denops: Denops) => string[] | Promise); + +/** + * Get the value to be passed to the source as args with resolving it when it + * is a function. + * + * @param denops - A denops instance. + * @param args - A list of strings or a function that returns it. + * @return The resolved value of `args`. + */ +async function deriveBoundArgs( + denops: Denops, + args: BoundArgsProvider, +): Promise { + return args instanceof Function ? await args(denops) : args; +} + +/** + * Creates a new source from an existing source with fixing some args. + * + * `args` is passed to the source as the head n number of arguments. The + * command-line arguments follow them. `args` is used as is if it is a list of + * strings. Otherwise, when it is a function, it is evaluated each time when + * the source is called, and the resulting value is passed to the base source. + * + * @param baseSource - The source to fix args. + * @param args - The args to pass to the source. + * @return A single source which calls the given source with given args. + */ +export function bindSourceArgs( + baseSource: Derivable>, + args: BoundArgsProvider, +): Source { + const source = derive(baseSource); + + return defineSource(async function* (denops, params, options) { + const boundArgs = await deriveBoundArgs(denops, args); + const iter = source.collect( + denops, + { ...params, args: [...boundArgs, ...params.args] }, + options, + ); + for await (const item of iter) { + yield item; + } + }); +} + +/** + * Creates a new curator from an existing curator with fixing some args. + * + * `args` is passed to the curator as the head n number of arguments. The + * command-line arguments follow them. `args` is used as is if it is a list of + * strings. Otherwise, when it is a function, it is evaluated each time when + * the curator is called, and the resulting value is passed to the base + * curator. + * + * @param baseSource - The curator to fix args. + * @param args - The args to pass to the curator. + * @return A single curator which calls the given curator with given args. + */ +export function bindCuratorArgs( + baseCurator: Derivable>, + args: BoundArgsProvider, +): Curator { + const curator = derive(baseCurator); + + return defineCurator(async function* (denops, params, options) { + const boundArgs = await deriveBoundArgs(denops, args); + const iter = curator.curate( + denops, + { ...params, args: [...boundArgs, ...params.args] }, + options, + ); + for await (const item of iter) { + yield item; + } + }); +} diff --git a/args_binder_test.ts b/args_binder_test.ts new file mode 100644 index 0000000..6bbf552 --- /dev/null +++ b/args_binder_test.ts @@ -0,0 +1,265 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { DenopsStub } from "@denops/test/stub"; + +import { bindCuratorArgs, bindSourceArgs } from "./args_binder.ts"; +import { defineSource } from "./source.ts"; +import { defineCurator } from "./curator.ts"; + +Deno.test("bindSourceArgs", async (t) => { + await t.step("with bear args", async (t) => { + await t.step( + "returns a source which calls another source with given fixed args", + async () => { + const baseSource = defineSource( + async function* (_denops, params, _options) { + yield* params.args.map((v, i) => ({ id: i, value: v, detail: {} })); + }, + ); + const source = bindSourceArgs( + baseSource, + ["foo", "bar", "baz"], + ); + const denops = new DenopsStub(); + const params = { args: [] }; + const items = await Array.fromAsync(source.collect(denops, params, {})); + assertEquals(items, [ + { id: 0, value: "foo", detail: {} }, + { id: 1, value: "bar", detail: {} }, + { id: 2, value: "baz", detail: {} }, + ]); + }, + ); + + await t.step("check type constraint", () => { + type C = { a: string }; + const baseSource = defineSource( + async function* (_denops, params, _options) { + yield* params.args.map((v, i) => ({ + id: i, + value: v, + detail: { a: "" }, + })); + }, + ); + bindSourceArgs<{ invalidTypeConstraint: number }>( + // @ts-expect-error: The type of 'detail' does not match the above type constraint. + baseSource, + [], + ); + const implicitlyTyped = bindSourceArgs(baseSource, []); + const explicitlyTyped = bindSourceArgs(baseSource, []); + assertType>(true); + assertType>(true); + }); + }); + + await t.step("with derivable args", async (t) => { + await t.step( + "returns a source which calls another source with given fixed args", + async () => { + const baseSource = defineSource( + async function* (_denops, params, _options) { + yield* params.args.map((v, i) => ({ id: i, value: v, detail: {} })); + }, + ); + const source = bindSourceArgs( + baseSource, + (_denops) => ["foo", "bar", "baz"], + ); + const denops = new DenopsStub(); + const params = { args: [] }; + const items = await Array.fromAsync(source.collect(denops, params, {})); + assertEquals(items, [ + { id: 0, value: "foo", detail: {} }, + { id: 1, value: "bar", detail: {} }, + { id: 2, value: "baz", detail: {} }, + ]); + }, + ); + + await t.step("check type constraint", () => { + type C = { a: string }; + const baseSource = defineSource( + async function* (_denops, params, _options) { + yield* params.args.map((v, i) => ({ + id: i, + value: v, + detail: { a: "" }, + })); + }, + ); + bindSourceArgs<{ invalidTypeConstraint: number }>( + // @ts-expect-error: The type of 'detail' does not match the above type constraint. + baseSource, + (_denops) => [], + ); + const implicitlyTyped = bindSourceArgs(baseSource, (_denops) => []); + const explicitlyTyped = bindSourceArgs(baseSource, (_denops) => []); + assertType>(true); + assertType>(true); + }); + + await t.step( + "args provider is evaluated each time when items are collected", + async () => { + const baseSource = defineSource( + async function* (_denops, params, _options) { + yield* params.args.map((v, i) => ({ id: i, value: v, detail: {} })); + }, + ); + let called = 0; + const source = bindSourceArgs( + baseSource, + (_denops) => { + called++; + return ["foo", "bar", "baz"]; + }, + ); + const denops = new DenopsStub(); + const params = { args: [] }; + const items = await Array.fromAsync(source.collect(denops, params, {})); + assertEquals(items, [ + { id: 0, value: "foo", detail: {} }, + { id: 1, value: "bar", detail: {} }, + { id: 2, value: "baz", detail: {} }, + ]); + assertEquals(called, 1); + await Array.fromAsync(source.collect(denops, params, {})); + assertEquals(called, 2); + }, + ); + }); +}); + +Deno.test("bindCuratorArgs", async (t) => { + await t.step("with bear args", async (t) => { + await t.step( + "returns a curator which calls another curator with given fixed args", + async () => { + const baseCurator = defineCurator( + async function* (_denops, params, _options) { + yield* params.args.map((v, i) => ({ id: i, value: v, detail: {} })); + }, + ); + const curator = bindCuratorArgs( + baseCurator, + ["foo", "bar", "baz"], + ); + const denops = new DenopsStub(); + const params = { args: [], query: "" }; + const items = await Array.fromAsync( + curator.curate(denops, params, {}), + ); + assertEquals(items, [ + { id: 0, value: "foo", detail: {} }, + { id: 1, value: "bar", detail: {} }, + { id: 2, value: "baz", detail: {} }, + ]); + }, + ); + + await t.step("check type constraint", () => { + type C = { a: string }; + const baseCurator = defineCurator( + async function* (_denops, params, _options) { + yield* params.args.map((v, i) => ({ + id: i, + value: v, + detail: { a: "" }, + })); + }, + ); + bindCuratorArgs<{ invalidTypeConstraint: number }>( + // @ts-expect-error: The type of 'detail' does not match the above type constraint. + baseCurator, + (_denops) => [], + ); + const implicitlyTyped = bindCuratorArgs(baseCurator, []); + const explicitlyTyped = bindCuratorArgs(baseCurator, []); + assertType>(true); + assertType>(true); + }); + }); + + await t.step("with derivable args", async (t) => { + await t.step( + "returns a curator which calls another curator with given fixed args", + async () => { + const baseCurator = defineCurator( + async function* (_denops, params, _options) { + yield* params.args.map((v, i) => ({ id: i, value: v, detail: {} })); + }, + ); + const curator = bindCuratorArgs( + baseCurator, + (_denops) => ["foo", "bar", "baz"], + ); + const denops = new DenopsStub(); + const params = { args: [], query: "" }; + const items = await Array.fromAsync( + curator.curate(denops, params, {}), + ); + assertEquals(items, [ + { id: 0, value: "foo", detail: {} }, + { id: 1, value: "bar", detail: {} }, + { id: 2, value: "baz", detail: {} }, + ]); + }, + ); + + await t.step("check type constraint", () => { + type C = { a: string }; + const baseCurator = defineCurator( + async function* (_denops, params, _options) { + yield* params.args.map((v, i) => ({ + id: i, + value: v, + detail: { a: "" }, + })); + }, + ); + bindCuratorArgs<{ invalidTypeConstraint: number }>( + // @ts-expect-error: The type of 'detail' does not match the above type constraint. + baseCurator, + [], + ); + const implicitlyTyped = bindCuratorArgs(baseCurator, (_denops) => []); + const explicitlyTyped = bindCuratorArgs(baseCurator, (_denops) => []); + assertType>(true); + assertType>(true); + }); + + await t.step( + "args provider is evaluated each time when items are collected", + async () => { + const baseCurator = defineCurator( + async function* (_denops, params, _options) { + yield* params.args.map((v, i) => ({ id: i, value: v, detail: {} })); + }, + ); + let called = 0; + const curator = bindCuratorArgs( + baseCurator, + (_denops) => { + called++; + return ["foo", "bar", "baz"]; + }, + ); + const denops = new DenopsStub(); + const params = { args: [], query: "" }; + const items = await Array.fromAsync( + curator.curate(denops, params, {}), + ); + assertEquals(items, [ + { id: 0, value: "foo", detail: {} }, + { id: 1, value: "bar", detail: {} }, + { id: 2, value: "baz", detail: {} }, + ]); + assertEquals(called, 1); + await Array.fromAsync(curator.curate(denops, params, {})); + assertEquals(called, 2); + }, + ); + }); +}); diff --git a/deno.jsonc b/deno.jsonc index 3b15f4e..2eed7e3 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -4,6 +4,7 @@ "exports": { ".": "./mod.ts", "./action": "./action.ts", + "./args-binder": "./args_binder.ts", "./builtin": "./builtin/mod.ts", "./builtin/action": "./builtin/action/mod.ts", "./builtin/action/buffer": "./builtin/action/buffer.ts", diff --git a/mod.ts b/mod.ts index 4955d95..699505a 100644 --- a/mod.ts +++ b/mod.ts @@ -1,4 +1,5 @@ export * from "./action.ts"; +export * from "./args_binder.ts"; export * from "./coordinator.ts"; export * from "./curator.ts"; export * from "./item.ts";