Skip to content

Commit 9ae3488

Browse files
authored
Merge pull request #242 from taminomara/gen-settings
Support generating config options from `.emmyrc` schema
2 parents 5126e3b + 1ffa533 commit 9ae3488

File tree

11 files changed

+1465
-137
lines changed

11 files changed

+1465
-137
lines changed

.prettierrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"tabWidth": 4,
3+
"useTabs": false
4+
}

build/settings.js

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
4+
function re(input) {
5+
// See https://keleshev.com/verbose-regular-expressions-in-javascript
6+
if (input.raw.length !== 1) {
7+
throw Error("verboseRegExp: interpolation is not supported");
8+
}
9+
10+
let source = input.raw[0];
11+
let regexp = /(?<!\\)\s|[/][/].*|[/][*][\s\S]*[*][/]/g;
12+
let result = source.replace(regexp, "");
13+
14+
return new RegExp(result, "g");
15+
}
16+
17+
const TO_TITLE_CASE_RE = re`
18+
// We will add a space (bear with me here):
19+
_ // 1. instead of underscore,
20+
| ( // 2. OR in the following case:
21+
(?<!^) // - not at the beginning of the string,
22+
( // - AND EITHER:
23+
(?<=[A-Z])(?=[A-Z][a-z]) // - before case gets lower (XMLTag -> XML-Tag),
24+
| (?<=[a-zA-Z])(?![a-zA-Z_]) // - between a letter and a non-letter (HTTP20 -> HTTP-20),
25+
| (?<![A-Z_])(?=[A-Z]) // - between non-uppercase and uppercase letter (TagXML -> Tag-XML),
26+
) // - AND ALSO:
27+
(?!$) // - not at the end of the string.
28+
)
29+
`;
30+
31+
function toTitleCase(s) {
32+
const t = s.replace(TO_TITLE_CASE_RE, " ");
33+
return t.charAt(0).toUpperCase() + t.substr(1);
34+
}
35+
36+
function main() {
37+
const schemaPath = path.join(__dirname, "..", "syntaxes", "schema.json");
38+
const schemaContent = fs.readFileSync(schemaPath, "utf-8");
39+
const schema = JSON.parse(schemaContent);
40+
const definitions = schema.$defs || {};
41+
const properties = schema.properties;
42+
const settings = extractSettings(definitions, properties);
43+
44+
const descriptions = {};
45+
const rendered = {};
46+
for (const [key, [path, value]] of Object.entries(settings)) {
47+
renderSetting(key, path, value, rendered, descriptions);
48+
}
49+
50+
dumpPackageJson(rendered);
51+
dumpLocalesEn(descriptions);
52+
dumpLocalesZhCn(descriptions);
53+
}
54+
55+
function extractSettings(
56+
definitions,
57+
obj,
58+
currentPath = ["emmylua"],
59+
result = {}
60+
) {
61+
if (typeof obj !== "object") {
62+
return result;
63+
}
64+
65+
for (const [key, value] of Object.entries(obj)) {
66+
if (typeof value !== "object") {
67+
continue;
68+
}
69+
70+
const nextPath = [...currentPath, key];
71+
72+
if (value["x-vscode-setting"]) {
73+
result[nextPath.join(".")] = [nextPath, value];
74+
} else if (value.$ref) {
75+
const ref = /^#\/\$defs\/([a-zA-Z0-9_]+)$/.exec(value.$ref);
76+
if (!ref) {
77+
continue;
78+
}
79+
80+
const definition = definitions[ref[1]];
81+
if (!definition || definition.type !== "object") {
82+
continue;
83+
}
84+
85+
extractSettings(
86+
definitions,
87+
definition.properties,
88+
nextPath,
89+
result
90+
);
91+
}
92+
}
93+
94+
return result;
95+
}
96+
97+
function renderSetting(key, path, setting, result, descriptions) {
98+
let rendered = { ...(setting["x-vscode-setting"] ?? {}) };
99+
if (typeof setting["x-vscode-setting"] === "boolean") {
100+
let type = setting.type;
101+
if (typeof type === "object" && type.length <= 2) {
102+
for (let i = 0; i < type.length; i++) {
103+
if (type[i] === "null") {
104+
continue;
105+
} else {
106+
type = type[i];
107+
break;
108+
}
109+
}
110+
}
111+
if (type === "number") {
112+
rendered.type = ["number", "null"];
113+
rendered.editPresentation = "singlelineText";
114+
} else if (type === "integer") {
115+
rendered.type = ["integer", "null"];
116+
rendered.editPresentation = "singlelineText";
117+
} else if (type === "string") {
118+
rendered.type = ["string", "null"];
119+
rendered.editPresentation = "singlelineText";
120+
} else if (type === "boolean") {
121+
rendered.type = ["boolean", "null"];
122+
rendered.enum = [null, true, false];
123+
rendered.enumItemLabels = ["Default", "On", "Off"];
124+
rendered.markdownEnumDescriptions = [
125+
"%config.common.enum.default.description%",
126+
"%config.common.enum.on.description%",
127+
"%config.common.enum.off.description%",
128+
];
129+
} else {
130+
console.error(
131+
`Failed to process option ${key}: unknown option type ${setting.type}`
132+
);
133+
return null;
134+
}
135+
rendered.default = null;
136+
} else if (typeof setting["x-vscode-setting"] === "object") {
137+
rendered = setting["x-vscode-setting"];
138+
} else {
139+
console.error(`Invalid x-vscode-setting value in option ${key}`);
140+
return null;
141+
}
142+
143+
if (!setting.description) {
144+
console.warn(`Found undocumented option: ${key}`);
145+
}
146+
147+
const translationKey = `config.${key}.description`;
148+
rendered.markdownDescription = `%${translationKey}%`;
149+
descriptions[translationKey] = setting.description ?? key;
150+
151+
const title = toTitleCase(path[1]);
152+
result[title] = result[title] ?? {};
153+
result[title][key] = rendered;
154+
}
155+
156+
function dumpLocalesEn(descriptions) {
157+
const localePath = path.join(__dirname, "..", "package.nls.json");
158+
const localeContent = fs.readFileSync(localePath, "utf-8");
159+
const locale = JSON.parse(localeContent);
160+
161+
for (const [key, value] of Object.entries(descriptions)) {
162+
if (locale[key] && locale[key] !== value) {
163+
console.warn(
164+
`Description for ${key} changed, translation update is required`
165+
);
166+
}
167+
locale[key] = value;
168+
}
169+
170+
const newLocaleSorted = {};
171+
Object.keys(locale)
172+
.sort()
173+
.forEach((key) => {
174+
newLocaleSorted[key] = locale[key];
175+
});
176+
177+
fs.writeFileSync(localePath, ensureNl(JSON.stringify(newLocaleSorted, null, 4)));
178+
}
179+
180+
function dumpLocalesZhCn(descriptions) {
181+
const localePath = path.join(__dirname, "..", "package.nls.zh-cn.json");
182+
const localeContent = fs.readFileSync(localePath, "utf-8");
183+
const locale = JSON.parse(localeContent);
184+
185+
for (const [key, value] of Object.entries(descriptions)) {
186+
if (!locale[key]) {
187+
console.warn(`Missing translation for ${key}`);
188+
locale[key] = value;
189+
}
190+
}
191+
192+
const newLocaleSorted = {};
193+
Object.keys(locale)
194+
.sort()
195+
.forEach((key) => {
196+
newLocaleSorted[key] = locale[key];
197+
});
198+
199+
fs.writeFileSync(localePath, ensureNl(JSON.stringify(newLocaleSorted, null, 4)));
200+
}
201+
202+
function dumpPackageJson(rendered) {
203+
const packagePath = path.join(__dirname, "..", "package.json");
204+
const packageContent = fs.readFileSync(packagePath, "utf-8");
205+
const package = JSON.parse(packageContent);
206+
207+
const configuration = package.contributes.configuration;
208+
209+
const configurationByTitle = {};
210+
const configurationByTitleAlwaysLast = {};
211+
212+
for (let i = 0; i < configuration.length; i++) {
213+
const title = configuration[i].title;
214+
// Ensure Misc, Language Server and Colors stay at the end of config.
215+
if (["Misc", "Language Server", "Colors"].includes(title)) {
216+
configurationByTitleAlwaysLast[title] = configuration[i].properties;
217+
} else {
218+
configurationByTitle[configuration[i].title] =
219+
configuration[i].properties;
220+
}
221+
}
222+
223+
for (const [title, items] of Object.entries(rendered)) {
224+
// Ensure Misc, Language Server and Colors stay at the end of config.
225+
if (["Misc", "Language Server", "Colors"].includes(title)) {
226+
configurationByTitleAlwaysLast[title] = {
227+
...(configurationByTitleAlwaysLast[title] ?? {}),
228+
...items,
229+
};
230+
} else {
231+
configurationByTitle[title] = {
232+
...(configurationByTitle[title] ?? {}),
233+
...items,
234+
};
235+
}
236+
}
237+
238+
const newConfiguration = [];
239+
let i = 0;
240+
for (const [title, items] of Object.entries({
241+
...configurationByTitle,
242+
...configurationByTitleAlwaysLast,
243+
})) {
244+
newConfiguration.push({
245+
title,
246+
order: i++,
247+
properties: items,
248+
});
249+
}
250+
251+
package.contributes.configuration = newConfiguration;
252+
253+
fs.writeFileSync(packagePath, ensureNl(JSON.stringify(package, null, 4)));
254+
}
255+
256+
function ensureNl(s) {
257+
if (!s.endsWith("\n")) {
258+
s += "\n";
259+
}
260+
return s;
261+
}
262+
263+
main();

0 commit comments

Comments
 (0)