From f98e831edd38197071d0838eee7b15906ef484c2 Mon Sep 17 00:00:00 2001 From: mityu Date: Sat, 22 Feb 2025 16:50:24 +0900 Subject: [PATCH 1/3] feat: add bindSourceArgs and bindCuratorArgs utility functions Add utility functions to create a new source/curator which calls another source/curator with some fixed args. --- args_binder.ts | 55 +++++++++ args_binder_test.ts | 265 ++++++++++++++++++++++++++++++++++++++++++++ deno.jsonc | 1 + mod.ts | 1 + 4 files changed, 322 insertions(+) create mode 100644 args_binder.ts create mode 100644 args_binder_test.ts diff --git a/args_binder.ts b/args_binder.ts new file mode 100644 index 0000000..0159f66 --- /dev/null +++ b/args_binder.ts @@ -0,0 +1,55 @@ +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"; + +type BoundArgsProvider = + | string[] + | ((denops: Denops) => string[] | Promise); + +async function deriveBoundArgs( + denops: Denops, + args: BoundArgsProvider, +): Promise { + return args instanceof Function ? await args(denops) : 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; + } + }); +} + +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"; From b7efd7ff90294507b3138de3556b232c689ff8b2 Mon Sep 17 00:00:00 2001 From: mityu Date: Sun, 2 Mar 2025 18:24:37 +0900 Subject: [PATCH 2/3] chore: Add documents --- args_binder.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/args_binder.ts b/args_binder.ts index 0159f66..270d1f5 100644 --- a/args_binder.ts +++ b/args_binder.ts @@ -5,10 +5,22 @@ 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. + */ 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, @@ -16,6 +28,18 @@ async function deriveBoundArgs( 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, @@ -35,6 +59,19 @@ export function bindSourceArgs( }); } +/** + * 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, From 5cd98f956991544c76a9b4e85fd99b0b565dfed2 Mon Sep 17 00:00:00 2001 From: mityu Date: Sun, 2 Mar 2025 18:25:23 +0900 Subject: [PATCH 3/3] fix: Export the `BoundArgsProvider` type --- args_binder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/args_binder.ts b/args_binder.ts index 270d1f5..249c8bc 100644 --- a/args_binder.ts +++ b/args_binder.ts @@ -9,7 +9,7 @@ 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. */ -type BoundArgsProvider = +export type BoundArgsProvider = | string[] | ((denops: Denops) => string[] | Promise);