Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
55 changes: 55 additions & 0 deletions args_binder.ts
Original file line number Diff line number Diff line change
@@ -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<string[]>);

async function deriveBoundArgs(
denops: Denops,
args: BoundArgsProvider,
): Promise<string[]> {
return args instanceof Function ? await args(denops) : 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;
}
});
}

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