Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

👍 generate sub local scope option types #270

Merged
merged 2 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 52 additions & 22 deletions .scripts/gen-option/format.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { Option, OptionScope, OptionType } from "./types.ts";
import type { Option, OptionConstructor, OptionExportType } from "./types.ts";

type Context = {
types: Set<OptionExportType>;
constructors: Set<OptionConstructor>;
};

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

function formatOption(option: Option): string[] {
const { type, scope, docs } = option;
function formatOption(option: Option, context: Context): string[] {
const { docs, type } = option;
const name = translate[option.name] ?? option.name;
const exportType = getOptionExportType(option);
context.types.add(exportType);
const constructor = getOptionConstructor({ ...option, name });
context.constructors.add(constructor);
const lines = [
...formatDocs(docs),
`export const ${name}: ${getOptionTypeName(scope, type)} = ${
getOptionConstructor(name, type)
};`,
`export const ${name}: ${exportType}<${type}> = new ${constructor}("${name}");`,
"",
];
return lines;
}

function getOptionTypeName(scope: OptionScope[], type: OptionType): string {
if (scope.includes("global") && scope.includes("local")) {
return `GlobalOrLocalOption<${type}>`;
} else if (scope.includes("global")) {
return `GlobalOption<${type}>`;
function getOptionExportType({ scope, localScope }: Option): OptionExportType {
if (scope.includes("local")) {
if (scope.includes("global")) {
switch (localScope) {
case "tab":
return "GlobalOrTabPageLocalOption";
case "window":
return "GlobalOrWindowLocalOption";
case "buffer":
default:
return "GlobalOrBufferLocalOption";
}
} else {
switch (localScope) {
case "tab":
return "TabPageLocalOption";
case "window":
return "WindowLocalOption";
case "buffer":
default:
return "BufferLocalOption";
}
}
} else {
return `LocalOption<${type}>`;
return "GlobalOption";
}
}

function getOptionConstructor(name: string, type: OptionType): string {
function getOptionConstructor({ type }: Option): OptionConstructor {
switch (type) {
case "string":
return `new StringOption("${name}")`;
return "StringOption";
case "number":
return `new NumberOption("${name}")`;
return "NumberOption";
case "boolean":
return `new BooleanOption("${name}")`;
return "BooleanOption";
default: {
const unknownType: never = type;
throw new Error(`Unknown type ${unknownType}`);
Expand All @@ -52,14 +77,19 @@ function getOptionConstructor(name: string, type: OptionType): string {
}

export function format(options: Option[], root: string): string[] {
const types = `${root}/types.ts`;
const utils = `${root}/_utils.ts`;
const context: Context = {
types: new Set(),
constructors: new Set(),
};
const body = options.flatMap((option) => formatOption(option, context));
const types = [...context.types];
const constructors = [...context.constructors];
const lines = [
"// NOTE: This file is generated. Do NOT modify it manually.",
`import type { GlobalOption, GlobalOrLocalOption, LocalOption } from "${types}";`,
`import { BooleanOption, NumberOption, StringOption } from "${utils}";`,
`import type { ${types.join(",")} } from "${root}/types.ts";`,
`import { ${constructors.join(",")} } from "${root}/_utils.ts";`,
"",
...options.map(formatOption),
...body,
];
return lines.flat();
return lines;
}
58 changes: 40 additions & 18 deletions .scripts/gen-option/parse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { isOptionType, type Option, type OptionType } from "./types.ts";
import { isArrayOf, isUndefined, isUnionOf } from "@core/unknownutil/is";
import {
isOptionLocalScope,
isOptionScope,
isOptionType,
type Option,
type OptionType,
} from "./types.ts";
import { createMarkdownFromHelp } from "../markdown.ts";
import { regexIndexOf, trimLines } from "../utils.ts";

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

const options: Option[] = [];
const succeeds = new Set<number>();
const errors: Array<{ name: string; start: number; block: string }> = [];
const errors: {
name: string;
start: number;
block: string;
err: Error;
}[] = [];
let last = -1;
for (const match of content.matchAll(/\*'(\w+)'\*/g)) {
const name = match[1];
Expand All @@ -39,22 +51,22 @@ export function parse(content: string) {
continue;
}
const { block, start, end } = extractBlock(content, index);
const option = parseBlock(name, block);
if (option) {
try {
const option = parseBlock(name, block);
options.push(option);
succeeds.add(start);
last = end;
} else {
errors.push({ name, start, block });
} catch (err) {
errors.push({ name, start, block, err });
}
}

if (errors.length) {
for (const { name, start, block } of errors) {
for (const { name, start, block, err } of errors) {
if (!succeeds.has(start)) {
const line = content.substring(0, start + 1).split("\n").length;
console.error(
`Failed to parse option definition for '${name}' at line ${line}:`,
`Failed to parse option definition for '${name}' at line ${line}: ${err}`,
);
console.error("----- block start -----");
console.error(block);
Expand Down Expand Up @@ -90,6 +102,7 @@ function extractBlock(content: string, index: number): {
* - {name} : Required.
* - {type} : Required. But some have fallbacks.
* - {scope} : Optional. If not present, assume "global".
* - {localscope}: Required if {scope} is "local".
* - {defaults} : Optional. Appended to {document}.
* - {attention} : Optional. Appended to {document}.
* - {document} : Optional.
Expand All @@ -98,14 +111,14 @@ function extractBlock(content: string, index: number): {
* name type defaults
* ~~~~~ ~~~~~~ ~~~~~~~~~~~~~
* 'aleph' 'al' number (default 224) *E123*
* global <- scope
* global <- scope, localscope
* {only available when compiled ... <- attention
* feature} :
* The ASCII code for the first letter of the ... <- document
* routine that maps the keyboard in Hebrew mode ... :
* ```
*/
function parseBlock(name: string, body: string): Option | undefined {
function parseBlock(name: string, body: string): Option {
// Extract definition line
const reTags = /(?:[ \t]+\*[^*\s]+\*)+[ \t]*$/.source;
const reShortNames = /(?:[ \t]+'\w+')*/.source;
Expand All @@ -117,17 +130,26 @@ function parseBlock(name: string, body: string): Option | undefined {
const m1 = body.match(new RegExp(reDefinition, "dm"));
const type = m1?.groups?.type ?? fallbackTypes[name];
if (!m1 || !isOptionType(type)) {
// {name} not found, or {type} is invalid
return;
throw new TypeError("Failed to parse name or type");
}
const defaults = m1.groups!.defaults?.replaceAll(/^\s+/gm, " ").trim();
body = trimLines(body.substring(m1.indices![0][1])) + "\n";

// Extract {scope}
const m2 = body.match(/^\t{3,}(global or local|global|local)(?:[ \t].*)?\n/d);
const scope = (
m2?.[1].split(" or ") ?? ["global"]
) as Array<"global" | "local">;
// Extract {scope}, {localscope}
const m2 = body.match(
/^\t{3,}(?<scope>global or local|global|local)(?: to (?<localscope>buffer|tab|window))?(?:[ \t].*)?\n/d,
);
const scope = m2?.groups?.scope.split(" or ") ?? ["global"];
if (!isArrayOf(isOptionScope)(scope)) {
throw new TypeError("Failed to parse scope");
}
const localScope = m2?.groups?.localscope;
if (!isUnionOf([isOptionLocalScope, isUndefined])(localScope)) {
throw new TypeError("Failed to parse local scope");
}
if (scope.includes("local") && localScope === undefined) {
throw new TypeError("Invalid scope and local scope");
}
body = trimLines(body.substring(m2?.indices?.at(0)?.at(1) ?? 0)) + "\n";

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

const docs = createMarkdownFromHelp(body);

return { name, type, scope, docs };
return { name, type, scope, localScope, docs };
}
44 changes: 38 additions & 6 deletions .scripts/gen-option/types.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,56 @@
import type { Predicate } from "@core/unknownutil/type";
import { isLiteralOneOf } from "@core/unknownutil/is";

export type Option = {
name: string;
type: OptionType;
scope: OptionScope[];
localScope?: OptionLocalScope;
docs: string;
};

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

export type OptionType = typeof OPTION_TYPES[number];

export function isOptionType(x: unknown): x is OptionType {
return OPTION_TYPES.includes(x as OptionType);
}
export const isOptionType = isLiteralOneOf(
OPTION_TYPES,
) satisfies Predicate<OptionType>;

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

export type OptionScope = typeof OPTION_SCOPES[number];

export function isOptionScope(x: unknown): x is OptionScope {
return OPTION_SCOPES.includes(x as OptionScope);
}
export const isOptionScope = isLiteralOneOf(
OPTION_SCOPES,
) satisfies Predicate<OptionScope>;

export const OPTION_LOCAL_SCOPES = ["buffer", "tab", "window"] as const;

export type OptionLocalScope = typeof OPTION_LOCAL_SCOPES[number];

export const isOptionLocalScope = isLiteralOneOf(
OPTION_LOCAL_SCOPES,
) satisfies Predicate<OptionLocalScope>;

export type DocsType = "vim" | "nvim";

export const OPTION_EXPORT_TYPES = [
"BufferLocalOption",
"TabPageLocalOption",
"WindowLocalOption",
"GlobalOption",
"GlobalOrBufferLocalOption",
"GlobalOrTabPageLocalOption",
"GlobalOrWindowLocalOption",
] as const;

export type OptionExportType = typeof OPTION_EXPORT_TYPES[number];

export const OPTION_CONSTRUCTORS = [
"BooleanOption",
"NumberOption",
"StringOption",
] as const;

export type OptionConstructor = typeof OPTION_CONSTRUCTORS[number];
Loading