Skip to content

Commit 5f830ea

Browse files
committed
👍 Add function code generator and gen command
1 parent ab2730e commit 5f830ea

File tree

6 files changed

+306
-0
lines changed

6 files changed

+306
-0
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ type-check: FORCE ## Type check
2626
test: FORCE ## Test
2727
@deno test --unstable -A
2828

29+
gen: FORCE ## Generate codes
30+
@deno run -A ./scripts/gen-function/gen-function.ts
31+
@make fmt
32+
2933
dlink: FORCE ## Update dlink
3034
(cd denops_std; ${TOOLS}/bin/dlink)
3135
@make fmt

scripts/gen-function/format.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Definition, Variant } from "./types.ts";
2+
3+
const translate: Record<string, string> = {
4+
"default": "defaultValue",
5+
"delete": "delete_",
6+
"eval": "eval_",
7+
"function": "function_",
8+
};
9+
10+
function formatDocs(docs: string): string[] {
11+
const lines = docs.replaceAll(/\*\//g, "* /").split("\n").map((v) =>
12+
` * ${v}`
13+
);
14+
return ["/**", ...lines, " */"];
15+
}
16+
17+
function formatVariants(fn: string, vars: Variant[]): string[] {
18+
const lines = vars.map((variant) => {
19+
const args = variant.map(({ name, optional }) => {
20+
name = translate[name] ?? name;
21+
return `${name}${optional ? "?" : ""}: unknown`;
22+
});
23+
return `export function ${fn}(denops: Denops, ${
24+
args.join(", ")
25+
}): Promise<unknown>;`;
26+
});
27+
return lines;
28+
}
29+
30+
function formatDefinition({ fn, docs, vars }: Definition): string[] {
31+
fn = translate[fn] ?? fn;
32+
const lines = [
33+
...formatDocs(docs),
34+
...formatVariants(fn, vars),
35+
`export function ${fn}(denops: Denops, ...args: unknown[]): Promise<unknown> {`,
36+
` return denops.call('${fn}', ...args);`,
37+
"}",
38+
"",
39+
];
40+
return lines;
41+
}
42+
43+
export function format(definitions: Definition[], root: string): string[] {
44+
const denops = `${root}/vendor/https/deno.land/x/denops_core/mod.ts`;
45+
const lines = [
46+
"// NOTE: This file is generated. Do NOT modify it manually.",
47+
"// deno-lint-ignore-file camelcase",
48+
`import { Denops } from "${denops}";`,
49+
"",
50+
...definitions.map(formatDefinition),
51+
];
52+
return lines.flat();
53+
}

scripts/gen-function/gen-function.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {
2+
difference,
3+
intersection,
4+
} from "https://deno.land/x/set_operations@v1.0.0/mod.ts";
5+
import * as path from "https://deno.land/std@0.100.0/path/mod.ts";
6+
import * as commonManual from "../../denops_std/function/_manual.ts";
7+
import * as vimManual from "../../denops_std/function/vim/_manual.ts";
8+
import * as nvimManual from "../../denops_std/function/nvim/_manual.ts";
9+
import { parse } from "./parse.ts";
10+
import { format } from "./format.ts";
11+
import { downloadString } from "./utils.ts";
12+
13+
const VIM_VERSION = "8.1.2424";
14+
const NVIM_VERSION = "0.4.4";
15+
16+
const manualFnSet = new Set([
17+
...Object.keys(commonManual),
18+
...Object.keys(vimManual),
19+
...Object.keys(nvimManual),
20+
]);
21+
22+
const vimHelp = await downloadString(
23+
`https://raw.githubusercontent.com/vim/vim/v${VIM_VERSION}/runtime/doc/eval.txt`,
24+
);
25+
const vimDefs = parse(vimHelp);
26+
const vimFnSet = difference(new Set(vimDefs.map((def) => def.fn)), manualFnSet);
27+
28+
const nvimHelp = await downloadString(
29+
`https://raw.githubusercontent.com/neovim/neovim/v${NVIM_VERSION}/runtime/doc/eval.txt`,
30+
);
31+
const nvimDefs = parse(nvimHelp);
32+
const nvimFnSet = difference(
33+
new Set(nvimDefs.map((def) => def.fn)),
34+
manualFnSet,
35+
);
36+
37+
const commonFnSet = intersection(vimFnSet, nvimFnSet);
38+
const vimOnlyFnSet = difference(vimFnSet, nvimFnSet);
39+
const nvimOnlyFnSet = difference(nvimFnSet, vimFnSet);
40+
41+
const commonCode = format(
42+
vimDefs.filter((def) => commonFnSet.has(def.fn)),
43+
"..",
44+
);
45+
const vimOnlyCode = format(
46+
vimDefs.filter((def) => vimOnlyFnSet.has(def.fn)),
47+
"../..",
48+
);
49+
const nvimOnlyCode = format(
50+
nvimDefs.filter((def) => nvimOnlyFnSet.has(def.fn)),
51+
"../..",
52+
);
53+
54+
await Deno.writeTextFile(
55+
path.fromFileUrl(
56+
new URL("../../denops_std/function/_generated.ts", import.meta.url),
57+
),
58+
commonCode.join("\n"),
59+
);
60+
await Deno.writeTextFile(
61+
path.fromFileUrl(
62+
new URL("../../denops_std/function/vim/_generated.ts", import.meta.url),
63+
),
64+
vimOnlyCode.join("\n"),
65+
);
66+
await Deno.writeTextFile(
67+
path.fromFileUrl(
68+
new URL("../../denops_std/function/nvim/_generated.ts", import.meta.url),
69+
),
70+
nvimOnlyCode.join("\n"),
71+
);

scripts/gen-function/parse.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { Definition, Variant } from "./types.ts";
2+
import { Counter, regexIndexOf } from "./utils.ts";
3+
4+
/**
5+
* Parse Vim/Neovim help.
6+
*
7+
* It extract a function definition block like below and return
8+
* a list of `Definition`.
9+
*
10+
* ```
11+
* cursor({lnum}, {col} [, {off}]) *cursor()*
12+
* cursor({list})
13+
* Positions the cursor at the column ...
14+
* line {lnum}. The first column is one...
15+
* ```
16+
*/
17+
export function parse(content: string): Definition[] {
18+
const definitions: Definition[] = [];
19+
for (const match of content.matchAll(/\*(\w+?)\(\)\*/g)) {
20+
const fn = match[1];
21+
const i = match.index || 0;
22+
const s = content.lastIndexOf("\n", i);
23+
const ms = regexIndexOf(content, /\n[<>\s]/, i);
24+
const me = regexIndexOf(content, /\n[^<>\s]/, ms);
25+
const e = content.lastIndexOf("\n", me);
26+
const block = content
27+
.substring(s, e)
28+
.replaceAll(/\*.+?\*/g, "") // Remove tags
29+
.replaceAll(/\s+\n/g, "\n") // Remove trailing '\s'
30+
.trim();
31+
definitions.push(parseBlock(fn, block));
32+
}
33+
return definitions;
34+
}
35+
36+
/**
37+
* Parse function definition block.
38+
*
39+
* A function definition block is constrcuted with following parts
40+
*
41+
* ```
42+
* cursor({lnum}, {col} [, {off}]) <- variant
43+
* cursor({list}) <- variant
44+
* Positions the cursor at the column ... <- document
45+
* line {lnum}. The first column is one... <- document
46+
* ```
47+
*
48+
* This function parse content like above and return `Definition`.
49+
*
50+
*/
51+
function parseBlock(fn: string, body: string): Definition {
52+
// Remove '\n' in {variant} to make {variant} single line (ex. `searchpairpos`)
53+
body = body.replaceAll(new RegExp(`^\(${fn}\\([^\)]*?\)\n\t*`, "g"), "$1");
54+
// Append ')' for an invalid {variant}. (ex. `win_id2tabwin` in Neovim)
55+
body = body.replaceAll(new RegExp(`^\(${fn}\\([^\)]*?\)\t+`, "g"), "$1)\t");
56+
// Insert '\n' between {variant} and {document} (ex. `argidx`)
57+
body = body.replaceAll(new RegExp(`^\(${fn}\\(.*?\\)\)\t`, "g"), "$1\n\t\t");
58+
59+
// Remove leading '>' or trailing '<' which is used to define code block in help
60+
body = body.replaceAll(/\n<|>\n/g, "\n");
61+
62+
// Split body into vars/docs
63+
const vars: Variant[] = [];
64+
const docs: string[] = [];
65+
body.split("\n").forEach((line) => {
66+
if (/^\s/.test(line)) {
67+
docs.push(line.replace(/^\t\t/, ""));
68+
} else {
69+
const variant = parseVariant(line);
70+
if (variant) {
71+
vars.push(variant);
72+
}
73+
}
74+
});
75+
return {
76+
fn,
77+
docs: docs.join("\n"),
78+
vars,
79+
};
80+
}
81+
82+
/**
83+
* Parse variant.
84+
*
85+
* A variant is constructed with following parts
86+
*
87+
* ```
88+
* fn args
89+
* ~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~
90+
* cursor({lnum}, {col} [, {off}])
91+
* | |
92+
* | +- optional marker
93+
* +- separator
94+
* ```
95+
*
96+
* This function parse content like above and return `Variant`.
97+
*
98+
*/
99+
function parseVariant(variant: string): Variant | undefined {
100+
// Extract {args} part from {variant}
101+
const m = variant.match(new RegExp(`^\\w+\\(\(.+?\)\\)`));
102+
if (!m) {
103+
// The {variant} does not have {args}, probabliy it's not variant (ex. `strstr`)
104+
return undefined;
105+
}
106+
let optional = false;
107+
const counter = new Counter();
108+
const args = m[1].split(",").map((t) => {
109+
const name = t.replaceAll(/[{}\[\]\s]/g, "");
110+
const spread = name.endsWith("...");
111+
const arg = {
112+
name: name.replace("...", "").replace(/^(\d+)$/, "v$1"),
113+
spread,
114+
optional,
115+
};
116+
if (t.endsWith("[")) {
117+
optional = true;
118+
}
119+
counter.count(name);
120+
return arg;
121+
}).filter((arg) => arg.name);
122+
const indexer = new Counter();
123+
const uniqueArgs = args.map(({ name, spread, optional }) => {
124+
if (counter.get(name) > 1) {
125+
const index = indexer.count(name);
126+
name = `${name}${index}`;
127+
}
128+
return { name, spread, optional };
129+
});
130+
return uniqueArgs;
131+
}

scripts/gen-function/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export type Arg = {
2+
name: string;
3+
spread: boolean;
4+
optional: boolean;
5+
};
6+
7+
export type Variant = Arg[];
8+
9+
export type Definition = {
10+
fn: string;
11+
docs: string;
12+
vars: Variant[];
13+
};

scripts/gen-function/utils.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as io from "https://deno.land/std@0.100.0/io/mod.ts";
2+
3+
export async function downloadString(url: string): Promise<string> {
4+
const textDecoder = new TextDecoder();
5+
const response = await fetch(url);
6+
if (!response.body) {
7+
throw new Error(`Failed to read ${url}`);
8+
}
9+
const reader = io.readerFromStreamReader(response.body.getReader());
10+
return textDecoder.decode(await io.readAll(reader));
11+
}
12+
13+
export function regexIndexOf(s: string, pattern: RegExp, offset = 0): number {
14+
const index = s.slice(offset).search(pattern);
15+
return index < 0 ? index : index + offset;
16+
}
17+
18+
export class Counter {
19+
#map: Map<string, number>;
20+
21+
constructor() {
22+
this.#map = new Map();
23+
}
24+
25+
count(name: string): number {
26+
const value = this.get(name) + 1;
27+
this.#map.set(name, value);
28+
return value;
29+
}
30+
31+
get(name: string): number {
32+
return this.#map.get(name) ?? 0;
33+
}
34+
}

0 commit comments

Comments
 (0)