Skip to content

Commit 9b54f44

Browse files
committed
👍 Add type-safe batch.collect() function
1 parent 31fc598 commit 9b54f44

File tree

4 files changed

+265
-0
lines changed

4 files changed

+265
-0
lines changed

denops_std/batch/collect.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import type {
2+
Context,
3+
Denops,
4+
Dispatcher,
5+
Meta,
6+
} from "https://deno.land/x/denops_core@v4.0.0/mod.ts";
7+
8+
type VimVoid<T> = T extends void ? 0 : T;
9+
10+
type Collect<T extends readonly unknown[] | []> = {
11+
-readonly [P in keyof T]: VimVoid<Awaited<T[P]>>;
12+
};
13+
14+
class CollectHelper implements Denops {
15+
#denops: Denops;
16+
#calls: [string, ...unknown[]][];
17+
#closed: boolean;
18+
19+
constructor(denops: Denops) {
20+
this.#denops = denops;
21+
this.#calls = [];
22+
this.#closed = false;
23+
}
24+
25+
static getCalls(helper: CollectHelper): [string, ...unknown[]][] {
26+
return helper.#calls;
27+
}
28+
29+
static close(helper: CollectHelper): void {
30+
helper.#closed = true;
31+
}
32+
33+
get name(): string {
34+
return this.#denops.name;
35+
}
36+
37+
get meta(): Meta {
38+
return this.#denops.meta;
39+
}
40+
41+
get context(): Record<string | number | symbol, unknown> {
42+
return this.#denops.context;
43+
}
44+
45+
get dispatcher(): Dispatcher {
46+
return this.#denops.dispatcher;
47+
}
48+
49+
set dispatcher(dispatcher: Dispatcher) {
50+
this.#denops.dispatcher = dispatcher;
51+
}
52+
53+
redraw(_force?: boolean): Promise<void> {
54+
throw new Error("The 'redraw' method is not available on CollectHelper.");
55+
}
56+
57+
call(fn: string, ...args: unknown[]): Promise<unknown> {
58+
if (this.#closed) {
59+
throw new Error(
60+
"CollectHelper instance is not available outside of 'collect' block",
61+
);
62+
}
63+
this.#calls.push([fn, ...args]);
64+
return Promise.resolve();
65+
}
66+
67+
batch(..._calls: [string, ...unknown[]][]): Promise<unknown[]> {
68+
throw new Error("The 'batch' method is not available on CollectHelper.");
69+
}
70+
71+
cmd(_cmd: string, _ctx: Context = {}): Promise<void> {
72+
throw new Error("The 'cmd' method is not available on CollectHelper.");
73+
}
74+
75+
eval(expr: string, ctx: Context = {}): Promise<unknown> {
76+
if (this.#closed) {
77+
throw new Error(
78+
"CollectHelper instance is not available outside of 'collect' block",
79+
);
80+
}
81+
this.call("denops#api#eval", expr, ctx);
82+
return Promise.resolve();
83+
}
84+
85+
dispatch(name: string, fn: string, ...args: unknown[]): Promise<unknown> {
86+
return this.#denops.dispatch(name, fn, ...args);
87+
}
88+
}
89+
90+
/**
91+
* Call multiple denops functions sequentially without RPC overhead and return values
92+
*
93+
* ```typescript
94+
* import { Denops } from "../mod.ts";
95+
* import { collect } from "./collect.ts";
96+
*
97+
* export async function main(denops: Denops): Promise<void> {
98+
* const results = await collect(denops, (denops) => [
99+
* denops.eval("&modifiable"),
100+
* denops.eval("&modified"),
101+
* denops.eval("&filetype"),
102+
* ]);
103+
* // results contains the value of modifiable, modified, and filetype
104+
* }
105+
* ```
106+
*
107+
* Not like `batch`, the function can NOT be nested.
108+
*
109+
* Note that `denops.call()` or `denops.eval()` always return falsy value in
110+
* `collect()`, indicating that you **cannot** write code like below:
111+
*
112+
* ```typescript
113+
* import { Denops } from "../mod.ts";
114+
* import { collect } from "./collect.ts";
115+
*
116+
* export async function main(denops: Denops): Promise<void> {
117+
* const results = await collect(denops, (denops) => {
118+
* // !!! DON'T DO THIS !!!
119+
* (async () => {
120+
* if (await denops.call("has", "nvim")) {
121+
* // deno-lint-ignore no-explicit-any
122+
* await (denops.call("api_info") as any).version;
123+
* } else {
124+
* await denops.eval("v:version");
125+
* }
126+
* })();
127+
* return [];
128+
* });
129+
* }
130+
* ```
131+
*
132+
* The `denops` instance passed to the `collect` block is NOT available outside of
133+
* the block. An error is thrown when `denops.call()`, `denops.cmd()`, or
134+
* `denops.eval()` is called.
135+
*
136+
* Note that `denops.redraw()` and `denops.cmd()` cannot be called within `collect()`.
137+
* If it is called, an error is raised.
138+
*/
139+
export async function collect<T extends readonly unknown[] | []>(
140+
denops: Denops,
141+
executor: (helper: CollectHelper) => T,
142+
): Promise<Collect<T>> {
143+
const helper = new CollectHelper(denops);
144+
try {
145+
await Promise.all(executor(helper));
146+
} finally {
147+
CollectHelper.close(helper);
148+
}
149+
const calls = CollectHelper.getCalls(helper);
150+
const results = await denops.batch(...calls);
151+
return results as Collect<T>;
152+
}

denops_std/batch/collect_test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {
2+
assertEquals,
3+
assertRejects,
4+
} from "https://deno.land/std@0.186.0/testing/asserts.ts";
5+
import { test } from "https://deno.land/x/denops_test@v1.1.0/mod.ts";
6+
import type { Denops } from "https://deno.land/x/denops_core@v4.0.0/mod.ts";
7+
import { collect } from "./collect.ts";
8+
9+
test({
10+
mode: "all",
11+
name: "collect()",
12+
fn: async (denops, t) => {
13+
await t.step({
14+
name: "sequentially execute 'denops.call()'.",
15+
fn: async () => {
16+
const results = await collect(denops, (denops) => [
17+
denops.call("range", 0),
18+
denops.call("range", 1),
19+
denops.call("range", 2),
20+
]);
21+
assertEquals(results, [[], [0], [0, 1]]);
22+
},
23+
});
24+
await t.step({
25+
name: "throws an error when 'denops.cmd()' is called.",
26+
fn: async () => {
27+
await assertRejects(
28+
async () => {
29+
await collect(denops, (denops) => [
30+
denops.cmd("echo 'hello'"),
31+
]);
32+
},
33+
"method is not available",
34+
);
35+
},
36+
});
37+
await t.step({
38+
name: "sequentially execute 'denops.eval()'.",
39+
fn: async () => {
40+
await denops.cmd("let g:denops_collect_test = 10");
41+
const results = await collect(denops, (denops) => [
42+
denops.eval("g:denops_collect_test + 1"),
43+
denops.eval("g:denops_collect_test - 1"),
44+
denops.eval("g:denops_collect_test * 10"),
45+
]);
46+
assertEquals(results, [11, 9, 100]);
47+
},
48+
});
49+
await t.step({
50+
name: "throws an error when 'denops.batch()' is called.",
51+
fn: async () => {
52+
await assertRejects(
53+
async () => {
54+
await collect(denops, (denops) => [
55+
denops.batch(),
56+
]);
57+
},
58+
"method is not available",
59+
);
60+
},
61+
});
62+
await t.step({
63+
name:
64+
"The 'helper' instance passed in collect block is NOT available outside of the block",
65+
fn: async () => {
66+
await denops.cmd("let g:denops_collect_test = 0");
67+
await denops.cmd(
68+
"command! DenopsCollectTest let g:denops_collect_test += 1",
69+
);
70+
71+
let helper: Denops;
72+
await collect(denops, (denops) => {
73+
helper = denops;
74+
return [];
75+
});
76+
await assertRejects(
77+
async () => {
78+
await helper!.call("execute", "DenopsCollectTest");
79+
},
80+
"not available outside",
81+
);
82+
await assertRejects(
83+
async () => {
84+
await helper.cmd("DenopsCollectTest");
85+
},
86+
"not available outside",
87+
);
88+
await assertRejects(
89+
async () => {
90+
const _ = await helper.eval("v:version");
91+
},
92+
"not available outside",
93+
);
94+
},
95+
});
96+
await t.step({
97+
name: "throws an error when 'denops.redraw()' is called.",
98+
fn: async () => {
99+
await assertRejects(
100+
async () => {
101+
await collect(denops, (denops) => [
102+
denops.redraw(),
103+
]);
104+
},
105+
"method is not available",
106+
);
107+
},
108+
});
109+
},
110+
});

denops_std/batch/gather.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ class GatherHelper implements Denops {
132132
*
133133
* Note that `denops.redraw()` cannot be called within `gather()`. If it is called,
134134
* an error is raised.
135+
*
136+
* @deprecated Use `collect()` instead.
135137
*/
136138
export async function gather(
137139
denops: Denops,

denops_std/batch/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
* @module
55
*/
66
export * from "./batch.ts";
7+
export * from "./collect.ts";
78
export * from "./gather.ts";

0 commit comments

Comments
 (0)