Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions args_binder.ts
Original file line number Diff line number Diff line change
@@ -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<string[]>);

/**
* 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<string[]> {
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<T extends Detail = Detail>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add documentation comment of the function.

baseSource: Derivable<Source<T>>,
args: BoundArgsProvider,
): Source<T> {
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<T extends Detail = Detail>(
baseCurator: Derivable<Curator<T>>,
args: BoundArgsProvider,
): Curator<T> {
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;
}
});
}
265 changes: 265 additions & 0 deletions args_binder_test.ts
Original file line number Diff line number Diff line change
@@ -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<C>(
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<C>(baseSource, []);
assertType<IsExact<typeof baseSource, typeof implicitlyTyped>>(true);
assertType<IsExact<typeof baseSource, typeof explicitlyTyped>>(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<C>(
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<C>(baseSource, (_denops) => []);
assertType<IsExact<typeof baseSource, typeof implicitlyTyped>>(true);
assertType<IsExact<typeof baseSource, typeof explicitlyTyped>>(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<C>(
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<C>(baseCurator, []);
assertType<IsExact<typeof baseCurator, typeof implicitlyTyped>>(true);
assertType<IsExact<typeof baseCurator, typeof explicitlyTyped>>(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<C>(
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<C>(baseCurator, (_denops) => []);
assertType<IsExact<typeof baseCurator, typeof implicitlyTyped>>(true);
assertType<IsExact<typeof baseCurator, typeof explicitlyTyped>>(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);
},
);
});
});
1 change: 1 addition & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down