Skip to content

Commit 3b34cd0

Browse files
committed
☕ Add gen-option script to generate options from Vim/Neovim help
1 parent ca75d2d commit 3b34cd0

File tree

6 files changed

+261
-0
lines changed

6 files changed

+261
-0
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ test: FORCE ## Test
2828

2929
gen: FORCE ## Generate codes
3030
@deno run --unstable -A ./scripts/gen-function/gen-function.ts
31+
@deno run --unstable -A ./scripts/gen-option/gen-option.ts
3132
@make fmt
3233

3334
update: FORCE ## Update dependencies

scripts/gen-option/format.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { Option } 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 defaultValue(type: string): string {
11+
switch (type) {
12+
case "string":
13+
return `""`;
14+
case "number":
15+
return `0`;
16+
case "boolean":
17+
return `false`;
18+
default:
19+
throw new Error(`Unknown type ${type}`);
20+
}
21+
}
22+
23+
function formatDocs(docs: string): string[] {
24+
const lines = docs.replaceAll(/\*\//g, "* /").split("\n").map((v) =>
25+
` * ${v}`
26+
);
27+
return ["/**", ...lines, " */"];
28+
}
29+
30+
function formatOption({ name, type, scope, docs }: Option): string[] {
31+
name = translate[name] ?? name;
32+
const lines = [
33+
...formatDocs(docs),
34+
`export const ${name} = {`,
35+
...formatOptionBody(name, type),
36+
...(scope.includes("global") ? formatGlobalOptionBody(name, type) : []),
37+
...(scope.includes("local") ? formatLocalOptionBody(name, type) : []),
38+
`};`,
39+
"",
40+
];
41+
return lines;
42+
}
43+
44+
function formatOptionBody(name: string, type: string): string[] {
45+
const lines = [
46+
` async get(denops: Denops): Promise<${type}> {`,
47+
` return await options.get(denops, "${name}") ?? ${defaultValue(type)};`,
48+
` },`,
49+
` set(denops: Denops, value: ${type}): Promise<void> {`,
50+
` return options.set(denops, "${name}", value);`,
51+
` },`,
52+
` reset(denops: Denops): Promise<void> {`,
53+
` return options.remove(denops, "${name}");`,
54+
` },`,
55+
];
56+
return lines;
57+
}
58+
59+
function formatGlobalOptionBody(name: string, type: string): string[] {
60+
const lines = [
61+
` async getGlobal(denops: Denops): Promise<${type}> {`,
62+
` return await globalOptions.get(denops, "${name}") ?? ${
63+
defaultValue(type)
64+
};`,
65+
` },`,
66+
` setGlobal(denops: Denops, value: ${type}): Promise<void> {`,
67+
` return globalOptions.set(denops, "${name}", value);`,
68+
` },`,
69+
` resetGlobal(denops: Denops): Promise<void> {`,
70+
` return globalOptions.remove(denops, "${name}");`,
71+
` },`,
72+
];
73+
return lines;
74+
}
75+
76+
function formatLocalOptionBody(name: string, type: string): string[] {
77+
const lines = [
78+
` async getLocal(denops: Denops): Promise<${type}> {`,
79+
` return await localOptions.get(denops, "${name}") ?? ${
80+
defaultValue(type)
81+
};`,
82+
` },`,
83+
` setLocal(denops: Denops, value: ${type}): Promise<void> {`,
84+
` return localOptions.set(denops, "${name}", value);`,
85+
` },`,
86+
` resetLocal(denops: Denops): Promise<void> {`,
87+
` return localOptions.remove(denops, "${name}");`,
88+
` },`,
89+
];
90+
return lines;
91+
}
92+
93+
export function format(options: Option[], root: string): string[] {
94+
const denops = `${root}/../deps.ts`;
95+
const variable = `${root}/../variable/mod.ts`;
96+
const lines = [
97+
"// NOTE: This file is generated. Do NOT modify it manually.",
98+
`import { Denops } from "${denops}";`,
99+
`import { globalOptions, localOptions, options } from "${variable}";`,
100+
"",
101+
...options.map(formatOption),
102+
];
103+
return lines.flat();
104+
}

scripts/gen-option/gen-option.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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 manualOptionSet = 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/options.txt`,
24+
);
25+
const vimDefs = parse(vimHelp);
26+
const vimOptionSet = difference(
27+
new Set(vimDefs.map((def) => def.name)),
28+
manualOptionSet,
29+
);
30+
31+
const nvimHelp = await downloadString(
32+
`https://raw.githubusercontent.com/neovim/neovim/v${NVIM_VERSION}/runtime/doc/options.txt`,
33+
);
34+
const nvimDefs = parse(nvimHelp);
35+
const nvimOptionSet = difference(
36+
new Set(nvimDefs.map((def) => def.name)),
37+
manualOptionSet,
38+
);
39+
40+
const commonOptionSet = intersection(vimOptionSet, nvimOptionSet);
41+
const vimOnlyOptionSet = difference(vimOptionSet, nvimOptionSet);
42+
const nvimOnlyOptionSet = difference(nvimOptionSet, vimOptionSet);
43+
44+
const commonCode = format(
45+
vimDefs.filter((def) => commonOptionSet.has(def.name)),
46+
".",
47+
);
48+
const vimOnlyCode = format(
49+
vimDefs.filter((def) => vimOnlyOptionSet.has(def.name)),
50+
"..",
51+
);
52+
const nvimOnlyCode = format(
53+
nvimDefs.filter((def) => nvimOnlyOptionSet.has(def.name)),
54+
"..",
55+
);
56+
57+
await Deno.writeTextFile(
58+
path.fromFileUrl(
59+
new URL("../../denops_std/option/_generated.ts", import.meta.url),
60+
),
61+
commonCode.join("\n"),
62+
);
63+
await Deno.writeTextFile(
64+
path.fromFileUrl(
65+
new URL("../../denops_std/option/vim/_generated.ts", import.meta.url),
66+
),
67+
vimOnlyCode.join("\n"),
68+
);
69+
await Deno.writeTextFile(
70+
path.fromFileUrl(
71+
new URL("../../denops_std/option/nvim/_generated.ts", import.meta.url),
72+
),
73+
nvimOnlyCode.join("\n"),
74+
);

scripts/gen-option/parse.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Option } from "./types.ts";
2+
import { regexIndexOf } from "./utils.ts";
3+
4+
/**
5+
* Parse Vim/Neovim help.
6+
*
7+
* It extract a option definition block like below and return
8+
* a list of `Definition`.
9+
*
10+
* ```
11+
* *'aleph'* *'al'* *aleph* *Aleph*
12+
* 'aleph' 'al' number (default 224)
13+
* global
14+
* The ASCII code for the first letter of the Hebrew alphabet. The
15+
* routine that maps the keyboard in Hebrew mode, both in Insert mode
16+
* ...
17+
* ```
18+
*/
19+
export function parse(content: string) {
20+
const options: Option[] = [];
21+
const optionNames = [...content.matchAll(/\*'(\w+)'\*/g)].map((m) => m[1]);
22+
for (const name of optionNames) {
23+
const pattern = new RegExp(`\n'${name}' (?:'\\w+'\\s*)*\\s+(\\w+)`);
24+
const m1 = content.match(pattern);
25+
if (!m1) {
26+
continue;
27+
}
28+
const type = m1[1];
29+
const block = extractBlock(content, m1.index || 0);
30+
31+
const m2 = block.match(/\n\t\t\t(global or local|global|local)\s/);
32+
const scope = (m2 ? m2[1] : "global").split(" or ");
33+
34+
const docs = block
35+
.substring(block.indexOf("\n", (m2 ? m2.index || 0 : 0) + 1))
36+
.split("\n")
37+
.map((v) => v.replace(/^\t/, ""))
38+
.join("\n");
39+
options.push({
40+
name,
41+
type,
42+
scope,
43+
docs,
44+
});
45+
}
46+
return options;
47+
}
48+
49+
function extractBlock(content: string, index: number): string {
50+
const s = content.lastIndexOf("\n", index);
51+
const ms = regexIndexOf(content, /\n[<>\s]/, index);
52+
const me = regexIndexOf(content, /\n[^<>\t]/, ms);
53+
const e = content.lastIndexOf("\n", me);
54+
const block = content
55+
.substring(s, e)
56+
.replaceAll(/\*.+?\*/g, "") // Remove tags
57+
.replaceAll(/\s+\n/g, "\n") // Remove trailing '\s'
58+
.trim();
59+
return block;
60+
}

scripts/gen-option/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type Option = {
2+
name: string;
3+
type: string;
4+
scope: string[];
5+
docs: string;
6+
};

scripts/gen-option/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
}

0 commit comments

Comments
 (0)