Skip to content

Commit 4cb6bde

Browse files
committedSep 6, 2024
👍 generate sub local scope option types
Fixes #267 Fixes some #268
1 parent afa88b7 commit 4cb6bde

File tree

5 files changed

+184
-50
lines changed

5 files changed

+184
-50
lines changed
 

‎.scripts/gen-option/format.ts

+52-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { Option, OptionScope, OptionType } from "./types.ts";
1+
import type { Option, OptionConstructor, OptionExportType } from "./types.ts";
2+
3+
type Context = {
4+
types: Set<OptionExportType>;
5+
constructors: Set<OptionConstructor>;
6+
};
27

38
const translate: Record<string, string> = {
49
"default": "defaultValue",
@@ -13,37 +18,57 @@ export function formatDocs(docs: string): string[] {
1318
return ["/**", ...normalizedLines, " */"];
1419
}
1520

16-
function formatOption(option: Option): string[] {
17-
const { type, scope, docs } = option;
21+
function formatOption(option: Option, context: Context): string[] {
22+
const { docs, type } = option;
1823
const name = translate[option.name] ?? option.name;
24+
const exportType = getOptionExportType(option);
25+
context.types.add(exportType);
26+
const constructor = getOptionConstructor({ ...option, name });
27+
context.constructors.add(constructor);
1928
const lines = [
2029
...formatDocs(docs),
21-
`export const ${name}: ${getOptionTypeName(scope, type)} = ${
22-
getOptionConstructor(name, type)
23-
};`,
30+
`export const ${name}: ${exportType}<${type}> = new ${constructor}("${name}");`,
2431
"",
2532
];
2633
return lines;
2734
}
2835

29-
function getOptionTypeName(scope: OptionScope[], type: OptionType): string {
30-
if (scope.includes("global") && scope.includes("local")) {
31-
return `GlobalOrLocalOption<${type}>`;
32-
} else if (scope.includes("global")) {
33-
return `GlobalOption<${type}>`;
36+
function getOptionExportType({ scope, localScope }: Option): OptionExportType {
37+
if (scope.includes("local")) {
38+
if (scope.includes("global")) {
39+
switch (localScope) {
40+
case "tab":
41+
return "GlobalOrTabPageLocalOption";
42+
case "window":
43+
return "GlobalOrWindowLocalOption";
44+
case "buffer":
45+
default:
46+
return "GlobalOrBufferLocalOption";
47+
}
48+
} else {
49+
switch (localScope) {
50+
case "tab":
51+
return "TabPageLocalOption";
52+
case "window":
53+
return "WindowLocalOption";
54+
case "buffer":
55+
default:
56+
return "BufferLocalOption";
57+
}
58+
}
3459
} else {
35-
return `LocalOption<${type}>`;
60+
return "GlobalOption";
3661
}
3762
}
3863

39-
function getOptionConstructor(name: string, type: OptionType): string {
64+
function getOptionConstructor({ type }: Option): OptionConstructor {
4065
switch (type) {
4166
case "string":
42-
return `new StringOption("${name}")`;
67+
return "StringOption";
4368
case "number":
44-
return `new NumberOption("${name}")`;
69+
return "NumberOption";
4570
case "boolean":
46-
return `new BooleanOption("${name}")`;
71+
return "BooleanOption";
4772
default: {
4873
const unknownType: never = type;
4974
throw new Error(`Unknown type ${unknownType}`);
@@ -52,14 +77,19 @@ function getOptionConstructor(name: string, type: OptionType): string {
5277
}
5378

5479
export function format(options: Option[], root: string): string[] {
55-
const types = `${root}/types.ts`;
56-
const utils = `${root}/_utils.ts`;
80+
const context: Context = {
81+
types: new Set(),
82+
constructors: new Set(),
83+
};
84+
const body = options.flatMap((option) => formatOption(option, context));
85+
const types = [...context.types];
86+
const constructors = [...context.constructors];
5787
const lines = [
5888
"// NOTE: This file is generated. Do NOT modify it manually.",
59-
`import type { GlobalOption, GlobalOrLocalOption, LocalOption } from "${types}";`,
60-
`import { BooleanOption, NumberOption, StringOption } from "${utils}";`,
89+
`import type { ${types.join(",")} } from "${root}/types.ts";`,
90+
`import { ${constructors.join(",")} } from "${root}/_utils.ts";`,
6191
"",
62-
...options.map(formatOption),
92+
...body,
6393
];
64-
return lines.flat();
94+
return lines;
6595
}

‎.scripts/gen-option/parse.ts

+40-18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { isOptionType, type Option, type OptionType } from "./types.ts";
1+
import { isArrayOf, isUndefined, isUnionOf } from "@core/unknownutil/is";
2+
import {
3+
isOptionLocalScope,
4+
isOptionScope,
5+
isOptionType,
6+
type Option,
7+
type OptionType,
8+
} from "./types.ts";
29
import { createMarkdownFromHelp } from "../markdown.ts";
310
import { regexIndexOf, trimLines } from "../utils.ts";
411

@@ -29,7 +36,12 @@ export function parse(content: string) {
2936

3037
const options: Option[] = [];
3138
const succeeds = new Set<number>();
32-
const errors: Array<{ name: string; start: number; block: string }> = [];
39+
const errors: {
40+
name: string;
41+
start: number;
42+
block: string;
43+
err: Error;
44+
}[] = [];
3345
let last = -1;
3446
for (const match of content.matchAll(/\*'(\w+)'\*/g)) {
3547
const name = match[1];
@@ -39,22 +51,22 @@ export function parse(content: string) {
3951
continue;
4052
}
4153
const { block, start, end } = extractBlock(content, index);
42-
const option = parseBlock(name, block);
43-
if (option) {
54+
try {
55+
const option = parseBlock(name, block);
4456
options.push(option);
4557
succeeds.add(start);
4658
last = end;
47-
} else {
48-
errors.push({ name, start, block });
59+
} catch (err) {
60+
errors.push({ name, start, block, err });
4961
}
5062
}
5163

5264
if (errors.length) {
53-
for (const { name, start, block } of errors) {
65+
for (const { name, start, block, err } of errors) {
5466
if (!succeeds.has(start)) {
5567
const line = content.substring(0, start + 1).split("\n").length;
5668
console.error(
57-
`Failed to parse option definition for '${name}' at line ${line}:`,
69+
`Failed to parse option definition for '${name}' at line ${line}: ${err}`,
5870
);
5971
console.error("----- block start -----");
6072
console.error(block);
@@ -90,6 +102,7 @@ function extractBlock(content: string, index: number): {
90102
* - {name} : Required.
91103
* - {type} : Required. But some have fallbacks.
92104
* - {scope} : Optional. If not present, assume "global".
105+
* - {localscope}: Required if {scope} is "local".
93106
* - {defaults} : Optional. Appended to {document}.
94107
* - {attention} : Optional. Appended to {document}.
95108
* - {document} : Optional.
@@ -98,14 +111,14 @@ function extractBlock(content: string, index: number): {
98111
* name type defaults
99112
* ~~~~~ ~~~~~~ ~~~~~~~~~~~~~
100113
* 'aleph' 'al' number (default 224) *E123*
101-
* global <- scope
114+
* global <- scope, localscope
102115
* {only available when compiled ... <- attention
103116
* feature} :
104117
* The ASCII code for the first letter of the ... <- document
105118
* routine that maps the keyboard in Hebrew mode ... :
106119
* ```
107120
*/
108-
function parseBlock(name: string, body: string): Option | undefined {
121+
function parseBlock(name: string, body: string): Option {
109122
// Extract definition line
110123
const reTags = /(?:[ \t]+\*[^*\s]+\*)+[ \t]*$/.source;
111124
const reShortNames = /(?:[ \t]+'\w+')*/.source;
@@ -117,17 +130,26 @@ function parseBlock(name: string, body: string): Option | undefined {
117130
const m1 = body.match(new RegExp(reDefinition, "dm"));
118131
const type = m1?.groups?.type ?? fallbackTypes[name];
119132
if (!m1 || !isOptionType(type)) {
120-
// {name} not found, or {type} is invalid
121-
return;
133+
throw new TypeError("Failed to parse name or type");
122134
}
123135
const defaults = m1.groups!.defaults?.replaceAll(/^\s+/gm, " ").trim();
124136
body = trimLines(body.substring(m1.indices![0][1])) + "\n";
125137

126-
// Extract {scope}
127-
const m2 = body.match(/^\t{3,}(global or local|global|local)(?:[ \t].*)?\n/d);
128-
const scope = (
129-
m2?.[1].split(" or ") ?? ["global"]
130-
) as Array<"global" | "local">;
138+
// Extract {scope}, {localscope}
139+
const m2 = body.match(
140+
/^\t{3,}(?<scope>global or local|global|local)(?: to (?<localscope>buffer|tab|window))?(?:[ \t].*)?\n/d,
141+
);
142+
const scope = m2?.groups?.scope.split(" or ") ?? ["global"];
143+
if (!isArrayOf(isOptionScope)(scope)) {
144+
throw new TypeError("Failed to parse scope");
145+
}
146+
const localScope = m2?.groups?.localscope;
147+
if (!isUnionOf([isOptionLocalScope, isUndefined])(localScope)) {
148+
throw new TypeError("Failed to parse local scope");
149+
}
150+
if (scope.includes("local") && localScope === undefined) {
151+
throw new TypeError("Invalid scope and local scope");
152+
}
131153
body = trimLines(body.substring(m2?.indices?.at(0)?.at(1) ?? 0)) + "\n";
132154

133155
// Extract {attention}
@@ -140,5 +162,5 @@ function parseBlock(name: string, body: string): Option | undefined {
140162

141163
const docs = createMarkdownFromHelp(body);
142164

143-
return { name, type, scope, docs };
165+
return { name, type, scope, localScope, docs };
144166
}

‎.scripts/gen-option/types.ts

+38-6
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,56 @@
1+
import type { Predicate } from "@core/unknownutil/type";
2+
import { isLiteralOneOf } from "@core/unknownutil/is";
3+
14
export type Option = {
25
name: string;
36
type: OptionType;
47
scope: OptionScope[];
8+
localScope?: OptionLocalScope;
59
docs: string;
610
};
711

812
export const OPTION_TYPES = ["string", "number", "boolean"] as const;
913

1014
export type OptionType = typeof OPTION_TYPES[number];
1115

12-
export function isOptionType(x: unknown): x is OptionType {
13-
return OPTION_TYPES.includes(x as OptionType);
14-
}
16+
export const isOptionType = isLiteralOneOf(
17+
OPTION_TYPES,
18+
) satisfies Predicate<OptionType>;
1519

1620
export const OPTION_SCOPES = ["global", "local"] as const;
1721

1822
export type OptionScope = typeof OPTION_SCOPES[number];
1923

20-
export function isOptionScope(x: unknown): x is OptionScope {
21-
return OPTION_SCOPES.includes(x as OptionScope);
22-
}
24+
export const isOptionScope = isLiteralOneOf(
25+
OPTION_SCOPES,
26+
) satisfies Predicate<OptionScope>;
27+
28+
export const OPTION_LOCAL_SCOPES = ["buffer", "tab", "window"] as const;
29+
30+
export type OptionLocalScope = typeof OPTION_LOCAL_SCOPES[number];
31+
32+
export const isOptionLocalScope = isLiteralOneOf(
33+
OPTION_LOCAL_SCOPES,
34+
) satisfies Predicate<OptionLocalScope>;
2335

2436
export type DocsType = "vim" | "nvim";
37+
38+
export const OPTION_EXPORT_TYPES = [
39+
"BufferLocalOption",
40+
"TabPageLocalOption",
41+
"WindowLocalOption",
42+
"GlobalOption",
43+
"GlobalOrBufferLocalOption",
44+
"GlobalOrTabPageLocalOption",
45+
"GlobalOrWindowLocalOption",
46+
] as const;
47+
48+
export type OptionExportType = typeof OPTION_EXPORT_TYPES[number];
49+
50+
export const OPTION_CONSTRUCTORS = [
51+
"BooleanOption",
52+
"NumberOption",
53+
"StringOption",
54+
] as const;
55+
56+
export type OptionConstructor = typeof OPTION_CONSTRUCTORS[number];

‎option/_utils.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
import type { Denops } from "@denops/core";
22
import { getbufvar, getwinvar, setbufvar, setwinvar } from "../function/mod.ts";
33
import { globalOptions, localOptions, options } from "../variable/option.ts";
4-
import type { GlobalOrLocalOption } from "./types.ts";
4+
import type {
5+
BufferLocalOption,
6+
GlobalOption,
7+
LocalOption,
8+
TabPageLocalOption,
9+
WindowLocalOption,
10+
} from "./types.ts";
511

612
type Coerce<T> = (value: unknown) => T;
713

8-
class OptionImpl<T> implements GlobalOrLocalOption<T> {
14+
class OptionImpl<T>
15+
implements
16+
GlobalOption<T>,
17+
LocalOption<T>,
18+
BufferLocalOption<T>,
19+
TabPageLocalOption<T>,
20+
WindowLocalOption<T> {
921
readonly #name: string;
1022
readonly #defaultValue: T;
1123
readonly #coerce: Coerce<T>;

‎option/types.ts

+40-2
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,13 @@ export interface LocalOption<T> extends Option<T> {
7373
* @returns A Promise that resolves when the option has been successfully reset.
7474
*/
7575
resetLocal(denops: Denops): Promise<void>;
76+
}
7677

78+
/**
79+
* A buffer local option that can be retrieved and modified.
80+
* @template T The type of the option value.
81+
*/
82+
export interface BufferLocalOption<T> extends WindowLocalOption<T> {
7783
/**
7884
* Gets the value of the option for the specified buffer.
7985
* @param bufnr The buffer number.
@@ -88,7 +94,21 @@ export interface LocalOption<T> extends Option<T> {
8894
* @returns A Promise that resolves when the option has been successfully set.
8995
*/
9096
setBuffer(denops: Denops, bufnr: number, value: T): Promise<void>;
97+
}
9198

99+
/**
100+
* A tab page local option that can be retrieved and modified.
101+
* @template T The type of the option value.
102+
*/
103+
export interface TabPageLocalOption<T> extends LocalOption<T> {
104+
// NOTE: `getTabPage()` or `setTabPage()` is not implemented. See https://github.com/vim-denops/deno-denops-std/issues/268#issuecomment-2316587922
105+
}
106+
107+
/**
108+
* A window local option that can be retrieved and modified.
109+
* @template T The type of the option value.
110+
*/
111+
export interface WindowLocalOption<T> extends LocalOption<T> {
92112
/**
93113
* Gets the value of the option for the specified window.
94114
* @param winnr The window number or `window-ID`.
@@ -106,7 +126,25 @@ export interface LocalOption<T> extends Option<T> {
106126
}
107127

108128
/**
109-
* A global or local option that can be retrieved and modified.
129+
* A global or buffer local option that can be retrieved and modified.
130+
* @template T The type of the option value.
131+
*/
132+
export type GlobalOrBufferLocalOption<T> =
133+
& GlobalOption<T>
134+
& BufferLocalOption<T>;
135+
136+
/**
137+
* A global or tab page local option that can be retrieved and modified.
138+
* @template T The type of the option value.
139+
*/
140+
export type GlobalOrTabPageLocalOption<T> =
141+
& GlobalOption<T>
142+
& TabPageLocalOption<T>;
143+
144+
/**
145+
* A global or window local option that can be retrieved and modified.
110146
* @template T The type of the option value.
111147
*/
112-
export type GlobalOrLocalOption<T> = GlobalOption<T> & LocalOption<T>;
148+
export type GlobalOrWindowLocalOption<T> =
149+
& GlobalOption<T>
150+
& WindowLocalOption<T>;

0 commit comments

Comments
 (0)
Please sign in to comment.