Skip to content

Commit

Permalink
Merge pull request #270 from vim-denops/option-local-scope
Browse files Browse the repository at this point in the history
👍 generate sub local scope option types
  • Loading branch information
Milly committed Sep 26, 2024
2 parents afa88b7 + 75e7696 commit 402dedb
Show file tree
Hide file tree
Showing 8 changed files with 466 additions and 205 deletions.
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

0 comments on commit 402dedb

Please sign in to comment.