Skip to content

Commit 04e8c64

Browse files
authored
Merge pull request #5 from mityu/feat-bind-source-curator-args
feat: add bindSourceArgs and bindCuratorArgs utility functions
2 parents 16197bc + 5cd98f9 commit 04e8c64

File tree

4 files changed

+359
-0
lines changed

4 files changed

+359
-0
lines changed

args_binder.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { Denops } from "@denops/std";
2+
import { type Derivable, derive } from "@vim-fall/custom/derivable";
3+
4+
import type { Detail } from "./item.ts";
5+
import { defineSource, type Source } from "./source.ts";
6+
import { type Curator, defineCurator } from "./curator.ts";
7+
8+
/**
9+
* A type that represents a list of strings or a function which gets a denops
10+
* instance and returns a list of strings.
11+
*/
12+
export type BoundArgsProvider =
13+
| string[]
14+
| ((denops: Denops) => string[] | Promise<string[]>);
15+
16+
/**
17+
* Get the value to be passed to the source as args with resolving it when it
18+
* is a function.
19+
*
20+
* @param denops - A denops instance.
21+
* @param args - A list of strings or a function that returns it.
22+
* @return The resolved value of `args`.
23+
*/
24+
async function deriveBoundArgs(
25+
denops: Denops,
26+
args: BoundArgsProvider,
27+
): Promise<string[]> {
28+
return args instanceof Function ? await args(denops) : args;
29+
}
30+
31+
/**
32+
* Creates a new source from an existing source with fixing some args.
33+
*
34+
* `args` is passed to the source as the head n number of arguments. The
35+
* command-line arguments follow them. `args` is used as is if it is a list of
36+
* strings. Otherwise, when it is a function, it is evaluated each time when
37+
* the source is called, and the resulting value is passed to the base source.
38+
*
39+
* @param baseSource - The source to fix args.
40+
* @param args - The args to pass to the source.
41+
* @return A single source which calls the given source with given args.
42+
*/
43+
export function bindSourceArgs<T extends Detail = Detail>(
44+
baseSource: Derivable<Source<T>>,
45+
args: BoundArgsProvider,
46+
): Source<T> {
47+
const source = derive(baseSource);
48+
49+
return defineSource(async function* (denops, params, options) {
50+
const boundArgs = await deriveBoundArgs(denops, args);
51+
const iter = source.collect(
52+
denops,
53+
{ ...params, args: [...boundArgs, ...params.args] },
54+
options,
55+
);
56+
for await (const item of iter) {
57+
yield item;
58+
}
59+
});
60+
}
61+
62+
/**
63+
* Creates a new curator from an existing curator with fixing some args.
64+
*
65+
* `args` is passed to the curator as the head n number of arguments. The
66+
* command-line arguments follow them. `args` is used as is if it is a list of
67+
* strings. Otherwise, when it is a function, it is evaluated each time when
68+
* the curator is called, and the resulting value is passed to the base
69+
* curator.
70+
*
71+
* @param baseSource - The curator to fix args.
72+
* @param args - The args to pass to the curator.
73+
* @return A single curator which calls the given curator with given args.
74+
*/
75+
export function bindCuratorArgs<T extends Detail = Detail>(
76+
baseCurator: Derivable<Curator<T>>,
77+
args: BoundArgsProvider,
78+
): Curator<T> {
79+
const curator = derive(baseCurator);
80+
81+
return defineCurator(async function* (denops, params, options) {
82+
const boundArgs = await deriveBoundArgs(denops, args);
83+
const iter = curator.curate(
84+
denops,
85+
{ ...params, args: [...boundArgs, ...params.args] },
86+
options,
87+
);
88+
for await (const item of iter) {
89+
yield item;
90+
}
91+
});
92+
}

args_binder_test.ts

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import { assertEquals } from "@std/assert";
2+
import { assertType, type IsExact } from "@std/testing/types";
3+
import { DenopsStub } from "@denops/test/stub";
4+
5+
import { bindCuratorArgs, bindSourceArgs } from "./args_binder.ts";
6+
import { defineSource } from "./source.ts";
7+
import { defineCurator } from "./curator.ts";
8+
9+
Deno.test("bindSourceArgs", async (t) => {
10+
await t.step("with bear args", async (t) => {
11+
await t.step(
12+
"returns a source which calls another source with given fixed args",
13+
async () => {
14+
const baseSource = defineSource(
15+
async function* (_denops, params, _options) {
16+
yield* params.args.map((v, i) => ({ id: i, value: v, detail: {} }));
17+
},
18+
);
19+
const source = bindSourceArgs(
20+
baseSource,
21+
["foo", "bar", "baz"],
22+
);
23+
const denops = new DenopsStub();
24+
const params = { args: [] };
25+
const items = await Array.fromAsync(source.collect(denops, params, {}));
26+
assertEquals(items, [
27+
{ id: 0, value: "foo", detail: {} },
28+
{ id: 1, value: "bar", detail: {} },
29+
{ id: 2, value: "baz", detail: {} },
30+
]);
31+
},
32+
);
33+
34+
await t.step("check type constraint", () => {
35+
type C = { a: string };
36+
const baseSource = defineSource<C>(
37+
async function* (_denops, params, _options) {
38+
yield* params.args.map((v, i) => ({
39+
id: i,
40+
value: v,
41+
detail: { a: "" },
42+
}));
43+
},
44+
);
45+
bindSourceArgs<{ invalidTypeConstraint: number }>(
46+
// @ts-expect-error: The type of 'detail' does not match the above type constraint.
47+
baseSource,
48+
[],
49+
);
50+
const implicitlyTyped = bindSourceArgs(baseSource, []);
51+
const explicitlyTyped = bindSourceArgs<C>(baseSource, []);
52+
assertType<IsExact<typeof baseSource, typeof implicitlyTyped>>(true);
53+
assertType<IsExact<typeof baseSource, typeof explicitlyTyped>>(true);
54+
});
55+
});
56+
57+
await t.step("with derivable args", async (t) => {
58+
await t.step(
59+
"returns a source which calls another source with given fixed args",
60+
async () => {
61+
const baseSource = defineSource(
62+
async function* (_denops, params, _options) {
63+
yield* params.args.map((v, i) => ({ id: i, value: v, detail: {} }));
64+
},
65+
);
66+
const source = bindSourceArgs(
67+
baseSource,
68+
(_denops) => ["foo", "bar", "baz"],
69+
);
70+
const denops = new DenopsStub();
71+
const params = { args: [] };
72+
const items = await Array.fromAsync(source.collect(denops, params, {}));
73+
assertEquals(items, [
74+
{ id: 0, value: "foo", detail: {} },
75+
{ id: 1, value: "bar", detail: {} },
76+
{ id: 2, value: "baz", detail: {} },
77+
]);
78+
},
79+
);
80+
81+
await t.step("check type constraint", () => {
82+
type C = { a: string };
83+
const baseSource = defineSource<C>(
84+
async function* (_denops, params, _options) {
85+
yield* params.args.map((v, i) => ({
86+
id: i,
87+
value: v,
88+
detail: { a: "" },
89+
}));
90+
},
91+
);
92+
bindSourceArgs<{ invalidTypeConstraint: number }>(
93+
// @ts-expect-error: The type of 'detail' does not match the above type constraint.
94+
baseSource,
95+
(_denops) => [],
96+
);
97+
const implicitlyTyped = bindSourceArgs(baseSource, (_denops) => []);
98+
const explicitlyTyped = bindSourceArgs<C>(baseSource, (_denops) => []);
99+
assertType<IsExact<typeof baseSource, typeof implicitlyTyped>>(true);
100+
assertType<IsExact<typeof baseSource, typeof explicitlyTyped>>(true);
101+
});
102+
103+
await t.step(
104+
"args provider is evaluated each time when items are collected",
105+
async () => {
106+
const baseSource = defineSource(
107+
async function* (_denops, params, _options) {
108+
yield* params.args.map((v, i) => ({ id: i, value: v, detail: {} }));
109+
},
110+
);
111+
let called = 0;
112+
const source = bindSourceArgs(
113+
baseSource,
114+
(_denops) => {
115+
called++;
116+
return ["foo", "bar", "baz"];
117+
},
118+
);
119+
const denops = new DenopsStub();
120+
const params = { args: [] };
121+
const items = await Array.fromAsync(source.collect(denops, params, {}));
122+
assertEquals(items, [
123+
{ id: 0, value: "foo", detail: {} },
124+
{ id: 1, value: "bar", detail: {} },
125+
{ id: 2, value: "baz", detail: {} },
126+
]);
127+
assertEquals(called, 1);
128+
await Array.fromAsync(source.collect(denops, params, {}));
129+
assertEquals(called, 2);
130+
},
131+
);
132+
});
133+
});
134+
135+
Deno.test("bindCuratorArgs", async (t) => {
136+
await t.step("with bear args", async (t) => {
137+
await t.step(
138+
"returns a curator which calls another curator with given fixed args",
139+
async () => {
140+
const baseCurator = defineCurator(
141+
async function* (_denops, params, _options) {
142+
yield* params.args.map((v, i) => ({ id: i, value: v, detail: {} }));
143+
},
144+
);
145+
const curator = bindCuratorArgs(
146+
baseCurator,
147+
["foo", "bar", "baz"],
148+
);
149+
const denops = new DenopsStub();
150+
const params = { args: [], query: "" };
151+
const items = await Array.fromAsync(
152+
curator.curate(denops, params, {}),
153+
);
154+
assertEquals(items, [
155+
{ id: 0, value: "foo", detail: {} },
156+
{ id: 1, value: "bar", detail: {} },
157+
{ id: 2, value: "baz", detail: {} },
158+
]);
159+
},
160+
);
161+
162+
await t.step("check type constraint", () => {
163+
type C = { a: string };
164+
const baseCurator = defineCurator<C>(
165+
async function* (_denops, params, _options) {
166+
yield* params.args.map((v, i) => ({
167+
id: i,
168+
value: v,
169+
detail: { a: "" },
170+
}));
171+
},
172+
);
173+
bindCuratorArgs<{ invalidTypeConstraint: number }>(
174+
// @ts-expect-error: The type of 'detail' does not match the above type constraint.
175+
baseCurator,
176+
(_denops) => [],
177+
);
178+
const implicitlyTyped = bindCuratorArgs(baseCurator, []);
179+
const explicitlyTyped = bindCuratorArgs<C>(baseCurator, []);
180+
assertType<IsExact<typeof baseCurator, typeof implicitlyTyped>>(true);
181+
assertType<IsExact<typeof baseCurator, typeof explicitlyTyped>>(true);
182+
});
183+
});
184+
185+
await t.step("with derivable args", async (t) => {
186+
await t.step(
187+
"returns a curator which calls another curator with given fixed args",
188+
async () => {
189+
const baseCurator = defineCurator(
190+
async function* (_denops, params, _options) {
191+
yield* params.args.map((v, i) => ({ id: i, value: v, detail: {} }));
192+
},
193+
);
194+
const curator = bindCuratorArgs(
195+
baseCurator,
196+
(_denops) => ["foo", "bar", "baz"],
197+
);
198+
const denops = new DenopsStub();
199+
const params = { args: [], query: "" };
200+
const items = await Array.fromAsync(
201+
curator.curate(denops, params, {}),
202+
);
203+
assertEquals(items, [
204+
{ id: 0, value: "foo", detail: {} },
205+
{ id: 1, value: "bar", detail: {} },
206+
{ id: 2, value: "baz", detail: {} },
207+
]);
208+
},
209+
);
210+
211+
await t.step("check type constraint", () => {
212+
type C = { a: string };
213+
const baseCurator = defineCurator<C>(
214+
async function* (_denops, params, _options) {
215+
yield* params.args.map((v, i) => ({
216+
id: i,
217+
value: v,
218+
detail: { a: "" },
219+
}));
220+
},
221+
);
222+
bindCuratorArgs<{ invalidTypeConstraint: number }>(
223+
// @ts-expect-error: The type of 'detail' does not match the above type constraint.
224+
baseCurator,
225+
[],
226+
);
227+
const implicitlyTyped = bindCuratorArgs(baseCurator, (_denops) => []);
228+
const explicitlyTyped = bindCuratorArgs<C>(baseCurator, (_denops) => []);
229+
assertType<IsExact<typeof baseCurator, typeof implicitlyTyped>>(true);
230+
assertType<IsExact<typeof baseCurator, typeof explicitlyTyped>>(true);
231+
});
232+
233+
await t.step(
234+
"args provider is evaluated each time when items are collected",
235+
async () => {
236+
const baseCurator = defineCurator(
237+
async function* (_denops, params, _options) {
238+
yield* params.args.map((v, i) => ({ id: i, value: v, detail: {} }));
239+
},
240+
);
241+
let called = 0;
242+
const curator = bindCuratorArgs(
243+
baseCurator,
244+
(_denops) => {
245+
called++;
246+
return ["foo", "bar", "baz"];
247+
},
248+
);
249+
const denops = new DenopsStub();
250+
const params = { args: [], query: "" };
251+
const items = await Array.fromAsync(
252+
curator.curate(denops, params, {}),
253+
);
254+
assertEquals(items, [
255+
{ id: 0, value: "foo", detail: {} },
256+
{ id: 1, value: "bar", detail: {} },
257+
{ id: 2, value: "baz", detail: {} },
258+
]);
259+
assertEquals(called, 1);
260+
await Array.fromAsync(curator.curate(denops, params, {}));
261+
assertEquals(called, 2);
262+
},
263+
);
264+
});
265+
});

deno.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"exports": {
55
".": "./mod.ts",
66
"./action": "./action.ts",
7+
"./args-binder": "./args_binder.ts",
78
"./builtin": "./builtin/mod.ts",
89
"./builtin/action": "./builtin/action/mod.ts",
910
"./builtin/action/buffer": "./builtin/action/buffer.ts",

mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./action.ts";
2+
export * from "./args_binder.ts";
23
export * from "./coordinator.ts";
34
export * from "./curator.ts";
45
export * from "./item.ts";

0 commit comments

Comments
 (0)