@vsceasy/cli 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/bin/cli.js +652 -17
- package/dist/index.js +608 -6
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/contributesMerge.d.ts +18 -0
- package/dist/lib/helper/add.d.ts +1 -1
- package/dist/lib/scaffold.d.ts +4 -0
- package/dist/lib/templatesData.d.ts +1 -1
- package/package.json +1 -1
- package/templates/_assets/language/README.language.md +39 -0
- package/templates/_assets/language/contributes.extra.json +40 -0
- package/templates/_assets/language/fileicons/{{langId}}-icon-theme.json +13 -0
- package/templates/_assets/language/icons/{{langId}}.svg +5 -0
- package/templates/_assets/language/language-configuration.json +30 -0
- package/templates/_assets/language/snippets/{{langId}}.json +12 -0
- package/templates/_assets/language/src/colorize.ts +31 -0
- package/templates/_assets/language/src/commands/applyColors.ts +11 -0
- package/templates/_assets/language/src/commands/removeColors.ts +11 -0
- package/templates/_assets/language/src/extension/extension.ts +25 -0
- package/templates/_assets/language/src/helpers/colorize.ts +70 -0
- package/templates/_assets/language/syntaxes/{{langId}}.tmLanguage.json +45 -0
- package/templates/_generators/helper/colorize.ts.tpl +85 -0
- package/templates/react/scripts/gen.ts +55 -0
package/dist/index.js
CHANGED
|
@@ -134,15 +134,26 @@ async function scaffold(opts) {
|
|
|
134
134
|
throw new Error(`Target directory not empty: ${opts.targetDir}`);
|
|
135
135
|
}
|
|
136
136
|
fs2.mkdirSync(opts.targetDir, { recursive: true });
|
|
137
|
+
const type = opts.type ?? "ui";
|
|
137
138
|
const vars = buildVars(opts);
|
|
138
139
|
await copyTree(src, opts.targetDir, vars);
|
|
139
|
-
|
|
140
|
+
applyType(opts.targetDir, type, opts.preset ?? "full", opts.templatesRoot, vars);
|
|
140
141
|
writeConfig(opts.targetDir, {
|
|
141
142
|
publisher: opts.publisher,
|
|
142
143
|
commandPrefix: vars.commandPrefix,
|
|
143
|
-
|
|
144
|
+
type,
|
|
145
|
+
...type === "ui" ? { ui: opts.ui } : {}
|
|
144
146
|
});
|
|
145
147
|
}
|
|
148
|
+
function applyType(targetDir, type, preset, templatesRoot, vars) {
|
|
149
|
+
if (type === "ui") {
|
|
150
|
+
applyPreset(targetDir, preset);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
stripWebview(targetDir);
|
|
154
|
+
if (type === "language")
|
|
155
|
+
applyLanguage(targetDir, templatesRoot, vars);
|
|
156
|
+
}
|
|
146
157
|
function applyPreset(targetDir, preset) {
|
|
147
158
|
if (preset === "full")
|
|
148
159
|
return;
|
|
@@ -165,15 +176,104 @@ function applyPreset(targetDir, preset) {
|
|
|
165
176
|
`);
|
|
166
177
|
}
|
|
167
178
|
}
|
|
179
|
+
function stripWebview(targetDir) {
|
|
180
|
+
const removals = [
|
|
181
|
+
"src/panels",
|
|
182
|
+
"src/webview",
|
|
183
|
+
"src/commands/hello.ts",
|
|
184
|
+
"vite.config.ts"
|
|
185
|
+
];
|
|
186
|
+
for (const rel of removals) {
|
|
187
|
+
const abs = path2.join(targetDir, rel);
|
|
188
|
+
if (fs2.existsSync(abs))
|
|
189
|
+
fs2.rmSync(abs, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
const apiPath = path2.join(targetDir, "src", "shared", "api.ts");
|
|
192
|
+
if (fs2.existsSync(apiPath)) {
|
|
193
|
+
fs2.writeFileSync(apiPath, `// RPC contracts go here (add a panel with \`vsceasy panel add\`).
|
|
194
|
+
`);
|
|
195
|
+
}
|
|
196
|
+
trimReactFromPackageJson(targetDir);
|
|
197
|
+
}
|
|
198
|
+
function trimReactFromPackageJson(targetDir) {
|
|
199
|
+
const pkgPath = path2.join(targetDir, "package.json");
|
|
200
|
+
if (!fs2.existsSync(pkgPath))
|
|
201
|
+
return;
|
|
202
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
|
|
203
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
204
|
+
if (!pkg[section])
|
|
205
|
+
continue;
|
|
206
|
+
for (const dep of Object.keys(pkg[section])) {
|
|
207
|
+
if (REACT_DEPS.has(dep))
|
|
208
|
+
delete pkg[section][dep];
|
|
209
|
+
}
|
|
210
|
+
if (Object.keys(pkg[section]).length === 0)
|
|
211
|
+
delete pkg[section];
|
|
212
|
+
}
|
|
213
|
+
if (pkg.scripts) {
|
|
214
|
+
for (const key of Object.keys(pkg.scripts)) {
|
|
215
|
+
if (UI_SCRIPT_KEYS.has(key))
|
|
216
|
+
delete pkg.scripts[key];
|
|
217
|
+
}
|
|
218
|
+
if (pkg.scripts.dev) {
|
|
219
|
+
pkg.scripts.dev = "bun run gen:scan && bun run dev:ext";
|
|
220
|
+
}
|
|
221
|
+
for (const [k, v] of Object.entries(pkg.scripts)) {
|
|
222
|
+
pkg.scripts[k] = v.replace(/\s*&&\s*bun run build:ui/g, "");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
226
|
+
`);
|
|
227
|
+
}
|
|
228
|
+
function applyLanguage(targetDir, templatesRoot, vars) {
|
|
229
|
+
const assetsDir = path2.join(templatesRoot, "_assets", "language");
|
|
230
|
+
if (!fs2.existsSync(assetsDir)) {
|
|
231
|
+
throw new Error(`Language assets not found: ${assetsDir}`);
|
|
232
|
+
}
|
|
233
|
+
copyAssetsTree(assetsDir, targetDir, vars);
|
|
234
|
+
const langReadme = path2.join(targetDir, "README.language.md");
|
|
235
|
+
if (fs2.existsSync(langReadme)) {
|
|
236
|
+
fs2.renameSync(langReadme, path2.join(targetDir, "README.md"));
|
|
237
|
+
}
|
|
238
|
+
const pkgPath = path2.join(targetDir, "package.json");
|
|
239
|
+
if (fs2.existsSync(pkgPath)) {
|
|
240
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
|
|
241
|
+
pkg.activationEvents = [`onLanguage:${vars.langId}`];
|
|
242
|
+
fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
243
|
+
`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function copyAssetsTree(srcDir, destDir, vars) {
|
|
247
|
+
for (const entry of fs2.readdirSync(srcDir, { withFileTypes: true })) {
|
|
248
|
+
if (SKIP_NAMES.has(entry.name))
|
|
249
|
+
continue;
|
|
250
|
+
const srcPath = path2.join(srcDir, entry.name);
|
|
251
|
+
const destName = substitute(entry.name, vars);
|
|
252
|
+
const destPath = path2.join(destDir, destName);
|
|
253
|
+
if (entry.isDirectory()) {
|
|
254
|
+
fs2.mkdirSync(destPath, { recursive: true });
|
|
255
|
+
copyAssetsTree(srcPath, destPath, vars);
|
|
256
|
+
} else if (entry.isFile()) {
|
|
257
|
+
fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
|
|
258
|
+
fs2.writeFileSync(destPath, substitute(fs2.readFileSync(srcPath, "utf8"), vars));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
168
262
|
function buildVars(opts) {
|
|
169
263
|
const simpleName = opts.name.replace(/^@[^/]+\//, "");
|
|
170
264
|
const commandPrefix = simpleName.replace(/[^a-zA-Z0-9]+/g, "");
|
|
265
|
+
const langId = simpleName.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
266
|
+
const scopeName = `source.${langId}`;
|
|
267
|
+
const langExt = langId.slice(0, 4).toUpperCase();
|
|
171
268
|
return {
|
|
172
269
|
name: opts.name,
|
|
173
270
|
displayName: opts.displayName,
|
|
174
271
|
description: opts.description,
|
|
175
272
|
publisher: opts.publisher,
|
|
176
|
-
commandPrefix
|
|
273
|
+
commandPrefix,
|
|
274
|
+
langId,
|
|
275
|
+
scopeName,
|
|
276
|
+
langExt
|
|
177
277
|
};
|
|
178
278
|
}
|
|
179
279
|
async function copyTree(srcDir, destDir, vars) {
|
|
@@ -199,7 +299,7 @@ async function copyTree(srcDir, destDir, vars) {
|
|
|
199
299
|
function substitute(input, vars) {
|
|
200
300
|
return input.replace(/\{\{(\w+)\}\}/g, (_m, key) => vars[key] ?? `{{${key}}}`);
|
|
201
301
|
}
|
|
202
|
-
var fs2, path2, PLACEHOLDER_EXTS, SKIP_NAMES;
|
|
302
|
+
var fs2, path2, PLACEHOLDER_EXTS, SKIP_NAMES, REACT_DEPS, UI_SCRIPT_KEYS;
|
|
203
303
|
var init_scaffold = __esm(() => {
|
|
204
304
|
init_config();
|
|
205
305
|
fs2 = __toESM(require("fs"));
|
|
@@ -219,6 +319,15 @@ var init_scaffold = __esm(() => {
|
|
|
219
319
|
".yaml"
|
|
220
320
|
]);
|
|
221
321
|
SKIP_NAMES = new Set(["node_modules", "dist", ".DS_Store"]);
|
|
322
|
+
REACT_DEPS = new Set([
|
|
323
|
+
"react",
|
|
324
|
+
"react-dom",
|
|
325
|
+
"@types/react",
|
|
326
|
+
"@types/react-dom",
|
|
327
|
+
"@vitejs/plugin-react",
|
|
328
|
+
"vite"
|
|
329
|
+
]);
|
|
330
|
+
UI_SCRIPT_KEYS = new Set(["dev:ui", "build:ui"]);
|
|
222
331
|
});
|
|
223
332
|
|
|
224
333
|
// src/lib/validate.ts
|
|
@@ -1580,6 +1689,9 @@ function runDoctor(opts) {
|
|
|
1580
1689
|
results.push(...checkStatusBars(root));
|
|
1581
1690
|
results.push(...checkSubpanels(root));
|
|
1582
1691
|
results.push(checkContributesSync(root, pkg));
|
|
1692
|
+
const langCheck = checkLanguageAssets(root, pkg);
|
|
1693
|
+
if (langCheck)
|
|
1694
|
+
results.push(langCheck);
|
|
1583
1695
|
results.push(checkActivationEvents(pkg));
|
|
1584
1696
|
results.push(checkMarketplaceIcon(root, pkg));
|
|
1585
1697
|
results.push(checkGenScript(root));
|
|
@@ -1842,6 +1954,50 @@ function checkContributesSync(root, pkg) {
|
|
|
1842
1954
|
details: stale
|
|
1843
1955
|
};
|
|
1844
1956
|
}
|
|
1957
|
+
function checkLanguageAssets(root, pkg) {
|
|
1958
|
+
const sources = [pkg?.contributes];
|
|
1959
|
+
const extraPath = path12.join(root, "contributes.extra.json");
|
|
1960
|
+
if (fs12.existsSync(extraPath)) {
|
|
1961
|
+
try {
|
|
1962
|
+
sources.push(JSON.parse(fs12.readFileSync(extraPath, "utf8")));
|
|
1963
|
+
} catch {
|
|
1964
|
+
return {
|
|
1965
|
+
id: "language",
|
|
1966
|
+
level: "error",
|
|
1967
|
+
message: "contributes.extra.json is not valid JSON"
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
const refs = new Set;
|
|
1972
|
+
for (const c of sources) {
|
|
1973
|
+
if (!c)
|
|
1974
|
+
continue;
|
|
1975
|
+
for (const l of c.languages ?? [])
|
|
1976
|
+
if (l.configuration)
|
|
1977
|
+
refs.add(l.configuration);
|
|
1978
|
+
for (const g of c.grammars ?? [])
|
|
1979
|
+
if (g.path)
|
|
1980
|
+
refs.add(g.path);
|
|
1981
|
+
for (const s of c.snippets ?? [])
|
|
1982
|
+
if (s.path)
|
|
1983
|
+
refs.add(s.path);
|
|
1984
|
+
for (const t of c.iconThemes ?? [])
|
|
1985
|
+
if (t.path)
|
|
1986
|
+
refs.add(t.path);
|
|
1987
|
+
}
|
|
1988
|
+
if (refs.size === 0)
|
|
1989
|
+
return null;
|
|
1990
|
+
const missing = [...refs].filter((rel) => !fs12.existsSync(path12.join(root, rel)));
|
|
1991
|
+
if (missing.length === 0) {
|
|
1992
|
+
return { id: "language", level: "ok", message: `language ${refs.size} asset(s) present` };
|
|
1993
|
+
}
|
|
1994
|
+
return {
|
|
1995
|
+
id: "language",
|
|
1996
|
+
level: "error",
|
|
1997
|
+
message: `language: ${missing.length} referenced asset(s) missing`,
|
|
1998
|
+
details: missing
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
1845
2001
|
function checkGitignore(root) {
|
|
1846
2002
|
const file = path12.join(root, ".gitignore");
|
|
1847
2003
|
const required = ["dist", "node_modules"];
|
|
@@ -2238,9 +2394,314 @@ var init_upgrade = __esm(() => {
|
|
|
2238
2394
|
});
|
|
2239
2395
|
|
|
2240
2396
|
// src/lib/templatesData.ts
|
|
2241
|
-
var TEMPLATES_VERSION = "0.1.
|
|
2397
|
+
var TEMPLATES_VERSION = "0.1.10", TEMPLATE_FILES;
|
|
2242
2398
|
var init_templatesData = __esm(() => {
|
|
2243
2399
|
TEMPLATE_FILES = {
|
|
2400
|
+
"_assets/language/README.language.md": "## {{displayName}} language support\n\nThis extension was scaffolded with `vsceasy create --type language`. It provides\neditor support for `.{{langId}}` files:\n\n- **Syntax highlighting** — TextMate grammar in `syntaxes/{{langId}}.tmLanguage.json`\n- **Language configuration** — comments, brackets, auto-closing pairs, folding in\n `language-configuration.json`\n- **Snippets** — `snippets/{{langId}}.json`\n- **File icon** (opt-in) — `fileicons/{{langId}}-icon-theme.json`\n\n### How contributions are wired\n\n`vsceasy`'s `scripts/gen.ts` regenerates the generated parts of\n`package.json#contributes` (commands, views…) on every build. Language\ncontributions are **not** generated — they live in **`contributes.extra.json`**\nat the project root and are deep-merged into `package.json` by `gen.ts`. Edit\n`contributes.extra.json` to change languages / grammars / snippets / iconThemes,\nthen run `bun run gen`.\n\n### File icon is opt-in\n\nVS Code file icons are provided by an **icon theme**, which is global: activating\nit replaces *all* file icons in the workbench, not just `.{{langId}}`. This\nextension ships a `{{displayName}} Icons` theme but does **not** force it. To use\nit: `Preferences: File Icon Theme` → pick `{{displayName}} Icons`. If you don't\nwant to override every icon, leave it unselected — highlighting, config and\nsnippets work regardless.\n\n### Develop\n\n```sh\nbun install\nbun run gen # sync package.json#contributes\nbun run package # build a .vsix\n```\n\nPress `F5` (or run `bun run launch`) to open an Extension Development Host and\nopen a `.{{langId}}` file to see highlighting.\n",
|
|
2401
|
+
"_assets/language/contributes.extra.json": `{
|
|
2402
|
+
"languages": [
|
|
2403
|
+
{
|
|
2404
|
+
"id": "{{langId}}",
|
|
2405
|
+
"aliases": ["{{displayName}}", "{{langId}}"],
|
|
2406
|
+
"extensions": [".{{langId}}"],
|
|
2407
|
+
"configuration": "./language-configuration.json"
|
|
2408
|
+
}
|
|
2409
|
+
],
|
|
2410
|
+
"grammars": [
|
|
2411
|
+
{
|
|
2412
|
+
"language": "{{langId}}",
|
|
2413
|
+
"scopeName": "{{scopeName}}",
|
|
2414
|
+
"path": "./syntaxes/{{langId}}.tmLanguage.json"
|
|
2415
|
+
}
|
|
2416
|
+
],
|
|
2417
|
+
"snippets": [
|
|
2418
|
+
{
|
|
2419
|
+
"language": "{{langId}}",
|
|
2420
|
+
"path": "./snippets/{{langId}}.json"
|
|
2421
|
+
}
|
|
2422
|
+
],
|
|
2423
|
+
"iconThemes": [
|
|
2424
|
+
{
|
|
2425
|
+
"id": "{{langId}}-icons",
|
|
2426
|
+
"label": "{{displayName}} Icons",
|
|
2427
|
+
"path": "./fileicons/{{langId}}-icon-theme.json"
|
|
2428
|
+
}
|
|
2429
|
+
],
|
|
2430
|
+
"configuration": {
|
|
2431
|
+
"title": "{{displayName}}",
|
|
2432
|
+
"properties": {
|
|
2433
|
+
"{{commandPrefix}}.colorize": {
|
|
2434
|
+
"type": "boolean",
|
|
2435
|
+
"default": true,
|
|
2436
|
+
"markdownDescription": "Automatically apply distinct section colors to \`.{{langId}}\` files (scoped to \`{{scopeName}}\`, so other languages are unaffected). Turn off to keep your theme's default colors."
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
`,
|
|
2442
|
+
"_assets/language/fileicons/{{langId}}-icon-theme.json": `{
|
|
2443
|
+
"iconDefinitions": {
|
|
2444
|
+
"{{langId}}_file": {
|
|
2445
|
+
"iconPath": "../icons/{{langId}}.svg"
|
|
2446
|
+
}
|
|
2447
|
+
},
|
|
2448
|
+
"languageIds": {
|
|
2449
|
+
"{{langId}}": "{{langId}}_file"
|
|
2450
|
+
},
|
|
2451
|
+
"fileExtensions": {
|
|
2452
|
+
"{{langId}}": "{{langId}}_file"
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
`,
|
|
2456
|
+
"_assets/language/icons/{{langId}}.svg": `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
2457
|
+
<path fill="#8a8a8a" d="M6 3h14l6 6v20a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/>
|
|
2458
|
+
<path fill="#5a5a5a" d="M20 3l6 6h-6z"/>
|
|
2459
|
+
<text x="16" y="22" font-family="monospace" font-size="9" font-weight="bold" fill="#fff" text-anchor="middle">{{langExt}}</text>
|
|
2460
|
+
</svg>
|
|
2461
|
+
`,
|
|
2462
|
+
"_assets/language/language-configuration.json": `{
|
|
2463
|
+
"comments": {
|
|
2464
|
+
"lineComment": "#"
|
|
2465
|
+
},
|
|
2466
|
+
"brackets": [
|
|
2467
|
+
["{", "}"],
|
|
2468
|
+
["[", "]"],
|
|
2469
|
+
["(", ")"]
|
|
2470
|
+
],
|
|
2471
|
+
"autoClosingPairs": [
|
|
2472
|
+
{ "open": "{", "close": "}" },
|
|
2473
|
+
{ "open": "[", "close": "]" },
|
|
2474
|
+
{ "open": "(", "close": ")" },
|
|
2475
|
+
{ "open": "\\"", "close": "\\"", "notIn": ["string"] },
|
|
2476
|
+
{ "open": "'", "close": "'", "notIn": ["string"] }
|
|
2477
|
+
],
|
|
2478
|
+
"surroundingPairs": [
|
|
2479
|
+
["{", "}"],
|
|
2480
|
+
["[", "]"],
|
|
2481
|
+
["(", ")"],
|
|
2482
|
+
["\\"", "\\""],
|
|
2483
|
+
["'", "'"]
|
|
2484
|
+
],
|
|
2485
|
+
"folding": {
|
|
2486
|
+
"markers": {
|
|
2487
|
+
"start": "^\\\\s*#\\\\s*region\\\\b",
|
|
2488
|
+
"end": "^\\\\s*#\\\\s*endregion\\\\b"
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
`,
|
|
2493
|
+
"_assets/language/snippets/{{langId}}.json": `{
|
|
2494
|
+
"Section": {
|
|
2495
|
+
"prefix": "section",
|
|
2496
|
+
"body": ["[\${1:name}]", "$0"],
|
|
2497
|
+
"description": "A {{displayName}} section"
|
|
2498
|
+
},
|
|
2499
|
+
"Key/Value": {
|
|
2500
|
+
"prefix": "kv",
|
|
2501
|
+
"body": ["\${1:key} = \${2:value}"],
|
|
2502
|
+
"description": "A key/value pair"
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
`,
|
|
2506
|
+
"_assets/language/src/colorize.ts": `import type * as vscode from 'vscode';
|
|
2507
|
+
import { applyTokenColors, removeTokenColors, type TokenColorRule } from './helpers/colorize';
|
|
2508
|
+
|
|
2509
|
+
/** Root TextMate scope of this language — rules are applied only to these files. */
|
|
2510
|
+
export const SCOPE = '{{scopeName}}';
|
|
2511
|
+
|
|
2512
|
+
/**
|
|
2513
|
+
* Default token colors for {{displayName}}, emphasizing each construct. Edit
|
|
2514
|
+
* freely — they are applied to \`[{{scopeName}}]\` only, so other languages keep
|
|
2515
|
+
* the user's theme. Scope names must match your grammar
|
|
2516
|
+
* (syntaxes/{{langId}}.tmLanguage.json).
|
|
2517
|
+
*/
|
|
2518
|
+
export const RULES: TokenColorRule[] = [
|
|
2519
|
+
{ scope: 'comment.line.number-sign.{{langId}}', settings: { foreground: '#6b7a6e', fontStyle: 'italic' } },
|
|
2520
|
+
{ scope: 'string.quoted.double.basic.{{langId}}', settings: { foreground: '#98c379' } },
|
|
2521
|
+
{ scope: 'string.quoted.single.literal.{{langId}}', settings: { foreground: '#98c379' } },
|
|
2522
|
+
{ scope: 'constant.numeric.{{langId}}', settings: { foreground: '#d19a66' } },
|
|
2523
|
+
];
|
|
2524
|
+
|
|
2525
|
+
export async function applyColors(vscodeNs: typeof vscode): Promise<void> {
|
|
2526
|
+
await applyTokenColors(SCOPE, RULES);
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
export async function removeColors(vscodeNs: typeof vscode): Promise<void> {
|
|
2530
|
+
await removeTokenColors(SCOPE);
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
/** True when the user has opted in (default) to automatic coloring. */
|
|
2534
|
+
export function colorizeEnabled(vscodeNs: typeof vscode): boolean {
|
|
2535
|
+
return vscodeNs.workspace.getConfiguration('{{commandPrefix}}').get<boolean>('colorize', true);
|
|
2536
|
+
}
|
|
2537
|
+
`,
|
|
2538
|
+
"_assets/language/src/commands/applyColors.ts": `import { defineCommand } from '../shared/vsceasy';
|
|
2539
|
+
import { applyColors } from '../colorize';
|
|
2540
|
+
|
|
2541
|
+
export default defineCommand({
|
|
2542
|
+
id: 'applyColors',
|
|
2543
|
+
title: '{{displayName}}: Apply Colors',
|
|
2544
|
+
run: async (vscode) => {
|
|
2545
|
+
await applyColors(vscode);
|
|
2546
|
+
vscode.window.showInformationMessage('{{displayName}} colors applied.');
|
|
2547
|
+
},
|
|
2548
|
+
});
|
|
2549
|
+
`,
|
|
2550
|
+
"_assets/language/src/commands/removeColors.ts": `import { defineCommand } from '../shared/vsceasy';
|
|
2551
|
+
import { removeColors } from '../colorize';
|
|
2552
|
+
|
|
2553
|
+
export default defineCommand({
|
|
2554
|
+
id: 'removeColors',
|
|
2555
|
+
title: '{{displayName}}: Remove Colors',
|
|
2556
|
+
run: async (vscode) => {
|
|
2557
|
+
await removeColors(vscode);
|
|
2558
|
+
vscode.window.showInformationMessage('{{displayName}} colors removed.');
|
|
2559
|
+
},
|
|
2560
|
+
});
|
|
2561
|
+
`,
|
|
2562
|
+
"_assets/language/src/extension/extension.ts": `import { bootstrap } from '../shared/vsceasy';
|
|
2563
|
+
import { registry } from './_registry';
|
|
2564
|
+
import { applyColors, removeColors, colorizeEnabled } from '../colorize';
|
|
2565
|
+
|
|
2566
|
+
export const activate = bootstrap(registry, {
|
|
2567
|
+
onActivate: [
|
|
2568
|
+
async (context, vscode) => {
|
|
2569
|
+
// Auto-apply scoped token colors on activate when opted in (default).
|
|
2570
|
+
// Scoped to {{scopeName}} only — other languages are untouched.
|
|
2571
|
+
if (colorizeEnabled(vscode)) {
|
|
2572
|
+
await applyColors(vscode);
|
|
2573
|
+
}
|
|
2574
|
+
// React to the user toggling \`{{commandPrefix}}.colorize\` at runtime.
|
|
2575
|
+
context.subscriptions.push(
|
|
2576
|
+
vscode.workspace.onDidChangeConfiguration(async (e) => {
|
|
2577
|
+
if (!e.affectsConfiguration('{{commandPrefix}}.colorize')) return;
|
|
2578
|
+
if (colorizeEnabled(vscode)) await applyColors(vscode);
|
|
2579
|
+
else await removeColors(vscode);
|
|
2580
|
+
}),
|
|
2581
|
+
);
|
|
2582
|
+
},
|
|
2583
|
+
],
|
|
2584
|
+
});
|
|
2585
|
+
|
|
2586
|
+
export function deactivate() {}
|
|
2587
|
+
`,
|
|
2588
|
+
"_assets/language/src/helpers/colorize.ts": `import * as vscode from 'vscode';
|
|
2589
|
+
|
|
2590
|
+
/**
|
|
2591
|
+
* Apply theme-independent token colors to a single TextMate scope (e.g. a
|
|
2592
|
+
* language's root scope like \`{{scopeName}}\`), written to the user's
|
|
2593
|
+
* \`editor.tokenColorCustomizations\`. Because the rules are keyed by
|
|
2594
|
+
* \`[<scope>]\`, only files in that scope are recolored — every other language
|
|
2595
|
+
* keeps the active theme's colors.
|
|
2596
|
+
*
|
|
2597
|
+
* Rules are tagged with a marker so {@link removeTokenColors} can strip exactly
|
|
2598
|
+
* the ones this extension added, preserving any the user wrote by hand.
|
|
2599
|
+
*/
|
|
2600
|
+
|
|
2601
|
+
export interface TokenColorRule {
|
|
2602
|
+
/** Comma-separated TextMate scopes, e.g. 'entity.name.section.foo, comment.line.foo'. */
|
|
2603
|
+
scope: string;
|
|
2604
|
+
settings: { foreground?: string; background?: string; fontStyle?: string };
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
type TaggedRule = TokenColorRule & { [MARK]?: true };
|
|
2608
|
+
|
|
2609
|
+
/** Marker key identifying rules this extension wrote (vs. the user's own). */
|
|
2610
|
+
const MARK = '{{commandPrefix}}Colorize';
|
|
2611
|
+
const SECTION = 'editor.tokenColorCustomizations';
|
|
2612
|
+
|
|
2613
|
+
const blockKey = (scope: string) => \`[\${scope}]\`;
|
|
2614
|
+
|
|
2615
|
+
/**
|
|
2616
|
+
* Merge \`rules\` into \`editor.tokenColorCustomizations["[<scope>]"].textMateRules\`,
|
|
2617
|
+
* preserving the user's own rules and other scope keys. Idempotent — re-applying
|
|
2618
|
+
* replaces only previously-applied rules from this extension.
|
|
2619
|
+
*/
|
|
2620
|
+
export async function applyTokenColors(
|
|
2621
|
+
scope: string,
|
|
2622
|
+
rules: TokenColorRule[],
|
|
2623
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
2624
|
+
): Promise<void> {
|
|
2625
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
2626
|
+
const current = (cfg.get<Record<string, any>>(SECTION) ?? {}) as Record<string, any>;
|
|
2627
|
+
const key = blockKey(scope);
|
|
2628
|
+
const block = (current[key] ?? {}) as { textMateRules?: TaggedRule[] };
|
|
2629
|
+
const existing = Array.isArray(block.textMateRules) ? block.textMateRules : [];
|
|
2630
|
+
const userRules = existing.filter((r) => !r[MARK]);
|
|
2631
|
+
const ours: TaggedRule[] = rules.map((r) => ({ ...r, [MARK]: true }));
|
|
2632
|
+
const next = { ...current, [key]: { ...block, textMateRules: [...userRules, ...ours] } };
|
|
2633
|
+
await cfg.update(SECTION, next, target);
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
/** Remove only the rules this extension added for \`scope\`; leave the rest intact. */
|
|
2637
|
+
export async function removeTokenColors(
|
|
2638
|
+
scope: string,
|
|
2639
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
2640
|
+
): Promise<void> {
|
|
2641
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
2642
|
+
const current = cfg.get<Record<string, any>>(SECTION);
|
|
2643
|
+
const key = blockKey(scope);
|
|
2644
|
+
if (!current || !current[key]) return;
|
|
2645
|
+
const block = current[key] as { textMateRules?: TaggedRule[] };
|
|
2646
|
+
const userRules = (block.textMateRules ?? []).filter((r) => !r[MARK]);
|
|
2647
|
+
|
|
2648
|
+
const nextBlock: Record<string, unknown> = { ...block };
|
|
2649
|
+
if (userRules.length) nextBlock.textMateRules = userRules;
|
|
2650
|
+
else delete nextBlock.textMateRules;
|
|
2651
|
+
|
|
2652
|
+
const next = { ...current };
|
|
2653
|
+
if (Object.keys(nextBlock).length) next[key] = nextBlock;
|
|
2654
|
+
else delete next[key];
|
|
2655
|
+
|
|
2656
|
+
await cfg.update(SECTION, Object.keys(next).length ? next : undefined, target);
|
|
2657
|
+
}
|
|
2658
|
+
`,
|
|
2659
|
+
"_assets/language/syntaxes/{{langId}}.tmLanguage.json": `{
|
|
2660
|
+
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
|
2661
|
+
"name": "{{displayName}}",
|
|
2662
|
+
"scopeName": "{{scopeName}}",
|
|
2663
|
+
"patterns": [
|
|
2664
|
+
{ "include": "#comments" },
|
|
2665
|
+
{ "include": "#strings" },
|
|
2666
|
+
{ "include": "#numbers" }
|
|
2667
|
+
],
|
|
2668
|
+
"repository": {
|
|
2669
|
+
"comments": {
|
|
2670
|
+
"patterns": [
|
|
2671
|
+
{
|
|
2672
|
+
"name": "comment.line.number-sign.{{langId}}",
|
|
2673
|
+
"match": "#.*$"
|
|
2674
|
+
}
|
|
2675
|
+
]
|
|
2676
|
+
},
|
|
2677
|
+
"strings": {
|
|
2678
|
+
"patterns": [
|
|
2679
|
+
{
|
|
2680
|
+
"name": "string.quoted.double.{{langId}}",
|
|
2681
|
+
"begin": "\\"",
|
|
2682
|
+
"end": "\\"",
|
|
2683
|
+
"patterns": [
|
|
2684
|
+
{ "name": "constant.character.escape.{{langId}}", "match": "\\\\\\\\." }
|
|
2685
|
+
]
|
|
2686
|
+
},
|
|
2687
|
+
{
|
|
2688
|
+
"name": "string.quoted.single.{{langId}}",
|
|
2689
|
+
"begin": "'",
|
|
2690
|
+
"end": "'"
|
|
2691
|
+
}
|
|
2692
|
+
]
|
|
2693
|
+
},
|
|
2694
|
+
"numbers": {
|
|
2695
|
+
"patterns": [
|
|
2696
|
+
{
|
|
2697
|
+
"name": "constant.numeric.{{langId}}",
|
|
2698
|
+
"match": "\\\\b[0-9]+(\\\\.[0-9]+)?\\\\b"
|
|
2699
|
+
}
|
|
2700
|
+
]
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
`,
|
|
2244
2705
|
"_generators/command/command.ts.tpl": `import { defineCommand } from '../shared/vsceasy';
|
|
2245
2706
|
|
|
2246
2707
|
export default defineCommand({
|
|
@@ -2848,6 +3309,92 @@ export function createCache<V = unknown>(opts: CacheOptions = {}): Cache<V> {
|
|
|
2848
3309
|
|
|
2849
3310
|
return cache;
|
|
2850
3311
|
}
|
|
3312
|
+
`,
|
|
3313
|
+
"_generators/helper/colorize.ts.tpl": `import * as vscode from 'vscode';
|
|
3314
|
+
|
|
3315
|
+
/**
|
|
3316
|
+
* Apply theme-independent token colors to a single TextMate scope (e.g. a
|
|
3317
|
+
* language's root scope like \`source.toml\`), written to the user's
|
|
3318
|
+
* \`editor.tokenColorCustomizations\`. Because the rules are keyed by
|
|
3319
|
+
* \`[<scope>]\`, only files in that scope are recolored — every other language
|
|
3320
|
+
* keeps the active theme's colors.
|
|
3321
|
+
*
|
|
3322
|
+
* Rules are tagged with a marker so {@link removeTokenColors} can strip exactly
|
|
3323
|
+
* the ones this extension added, preserving any the user wrote by hand.
|
|
3324
|
+
*
|
|
3325
|
+
* Typical use — auto-apply on activate behind an opt-out setting:
|
|
3326
|
+
*
|
|
3327
|
+
* // extension.ts (onActivate hook)
|
|
3328
|
+
* if (config.get<boolean>('colorize', true)) {
|
|
3329
|
+
* await applyTokenColors('source.{{commandPrefix}}', MY_RULES);
|
|
3330
|
+
* }
|
|
3331
|
+
* vscode.workspace.onDidChangeConfiguration(async (e) => {
|
|
3332
|
+
* if (!e.affectsConfiguration('{{commandPrefix}}.colorize')) return;
|
|
3333
|
+
* if (config.get<boolean>('colorize', true)) await applyTokenColors('source.{{commandPrefix}}', MY_RULES);
|
|
3334
|
+
* else await removeTokenColors('source.{{commandPrefix}}');
|
|
3335
|
+
* });
|
|
3336
|
+
*
|
|
3337
|
+
* Declare the opt-out in package.json#contributes.configuration:
|
|
3338
|
+
* "{{commandPrefix}}.colorize": { "type": "boolean", "default": true }
|
|
3339
|
+
*/
|
|
3340
|
+
|
|
3341
|
+
export interface TokenColorRule {
|
|
3342
|
+
/** Comma-separated TextMate scopes, e.g. 'entity.name.section.foo, comment.line.foo'. */
|
|
3343
|
+
scope: string;
|
|
3344
|
+
settings: { foreground?: string; background?: string; fontStyle?: string };
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
type TaggedRule = TokenColorRule & { [MARK]?: true };
|
|
3348
|
+
|
|
3349
|
+
/** Marker key identifying rules this extension wrote (vs. the user's own). */
|
|
3350
|
+
const MARK = '{{commandPrefix}}Colorize';
|
|
3351
|
+
const SECTION = 'editor.tokenColorCustomizations';
|
|
3352
|
+
|
|
3353
|
+
const blockKey = (scope: string) => \`[\${scope}]\`;
|
|
3354
|
+
|
|
3355
|
+
/**
|
|
3356
|
+
* Merge \`rules\` into \`editor.tokenColorCustomizations["[<scope>]"].textMateRules\`,
|
|
3357
|
+
* preserving the user's own rules and other scope keys. Idempotent — re-applying
|
|
3358
|
+
* replaces only previously-applied rules from this extension.
|
|
3359
|
+
*/
|
|
3360
|
+
export async function applyTokenColors(
|
|
3361
|
+
scope: string,
|
|
3362
|
+
rules: TokenColorRule[],
|
|
3363
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
3364
|
+
): Promise<void> {
|
|
3365
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
3366
|
+
const current = (cfg.get<Record<string, any>>(SECTION) ?? {}) as Record<string, any>;
|
|
3367
|
+
const key = blockKey(scope);
|
|
3368
|
+
const block = (current[key] ?? {}) as { textMateRules?: TaggedRule[] };
|
|
3369
|
+
const existing = Array.isArray(block.textMateRules) ? block.textMateRules : [];
|
|
3370
|
+
const userRules = existing.filter((r) => !r[MARK]);
|
|
3371
|
+
const ours: TaggedRule[] = rules.map((r) => ({ ...r, [MARK]: true }));
|
|
3372
|
+
const next = { ...current, [key]: { ...block, textMateRules: [...userRules, ...ours] } };
|
|
3373
|
+
await cfg.update(SECTION, next, target);
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
/** Remove only the rules this extension added for \`scope\`; leave the rest intact. */
|
|
3377
|
+
export async function removeTokenColors(
|
|
3378
|
+
scope: string,
|
|
3379
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
3380
|
+
): Promise<void> {
|
|
3381
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
3382
|
+
const current = cfg.get<Record<string, any>>(SECTION);
|
|
3383
|
+
const key = blockKey(scope);
|
|
3384
|
+
if (!current || !current[key]) return;
|
|
3385
|
+
const block = current[key] as { textMateRules?: TaggedRule[] };
|
|
3386
|
+
const userRules = (block.textMateRules ?? []).filter((r) => !r[MARK]);
|
|
3387
|
+
|
|
3388
|
+
const nextBlock: Record<string, unknown> = { ...block };
|
|
3389
|
+
if (userRules.length) nextBlock.textMateRules = userRules;
|
|
3390
|
+
else delete nextBlock.textMateRules;
|
|
3391
|
+
|
|
3392
|
+
const next = { ...current };
|
|
3393
|
+
if (Object.keys(nextBlock).length) next[key] = nextBlock;
|
|
3394
|
+
else delete next[key];
|
|
3395
|
+
|
|
3396
|
+
await cfg.update(SECTION, Object.keys(next).length ? next : undefined, target);
|
|
3397
|
+
}
|
|
2851
3398
|
`,
|
|
2852
3399
|
"_generators/helper/config.ts.tpl": `import * as vscode from 'vscode';
|
|
2853
3400
|
|
|
@@ -4166,6 +4713,12 @@ bun run package # → {{name}}-0.0.1.vsix
|
|
|
4166
4713
|
"react/scripts/gen.ts": `#!/usr/bin/env bun
|
|
4167
4714
|
// Scans src/panels, src/commands, and src/menus; writes src/extension/_registry.ts
|
|
4168
4715
|
// and syncs package.json#contributes (commands, viewsContainers, views).
|
|
4716
|
+
//
|
|
4717
|
+
// Non-generated contributes (e.g. languages, grammars, snippets, themes,
|
|
4718
|
+
// iconThemes, walkthroughs) go in an optional \`contributes.extra.json\` at the
|
|
4719
|
+
// project root. It is deep-merged into package.json#contributes on every run —
|
|
4720
|
+
// the keys gen.ts owns (commands, keybindings, menus.commandPalette,
|
|
4721
|
+
// viewsContainers, views) always win, everything else from extra is preserved.
|
|
4169
4722
|
|
|
4170
4723
|
import * as fs from 'fs';
|
|
4171
4724
|
import * as path from 'path';
|
|
@@ -4181,6 +4734,10 @@ const TREE_VIEWS_DIR = path.join(SRC, 'treeViews');
|
|
|
4181
4734
|
const JOBS_DIR = path.join(SRC, 'jobs');
|
|
4182
4735
|
const OUT = path.join(SRC, 'extension', '_registry.ts');
|
|
4183
4736
|
const PKG_PATH = path.join(ROOT, 'package.json');
|
|
4737
|
+
const EXTRA_PATH = path.join(ROOT, 'contributes.extra.json');
|
|
4738
|
+
|
|
4739
|
+
/** Keys gen.ts owns — never overridden by contributes.extra.json. */
|
|
4740
|
+
const GEN_OWNED_KEYS = new Set(['commands', 'keybindings', 'viewsContainers', 'views']);
|
|
4184
4741
|
|
|
4185
4742
|
interface Discovered {
|
|
4186
4743
|
id: string;
|
|
@@ -4369,9 +4926,54 @@ function syncPackageJson(
|
|
|
4369
4926
|
delete contributes.views;
|
|
4370
4927
|
}
|
|
4371
4928
|
|
|
4929
|
+
mergeExtraContributes(contributes);
|
|
4930
|
+
|
|
4372
4931
|
fs.writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\\n');
|
|
4373
4932
|
}
|
|
4374
4933
|
|
|
4934
|
+
/**
|
|
4935
|
+
* Deep-merge the optional \`contributes.extra.json\` (project root) into the
|
|
4936
|
+
* package's \`contributes\`. Use for any contribution point gen.ts doesn't
|
|
4937
|
+
* generate — languages, grammars, snippets, themes, iconThemes, walkthroughs…
|
|
4938
|
+
*
|
|
4939
|
+
* Rules:
|
|
4940
|
+
* - gen-owned keys (commands, keybindings, viewsContainers, views) are ignored
|
|
4941
|
+
* if present in extra — gen.ts stays authoritative for those.
|
|
4942
|
+
* - plain objects merge recursively; arrays and primitives from extra replace.
|
|
4943
|
+
*
|
|
4944
|
+
* NOTE: this is an inline copy of src/lib/contributesMerge.ts in the vsceasy
|
|
4945
|
+
* source (the script must run standalone). Keep the two in sync.
|
|
4946
|
+
*/
|
|
4947
|
+
function mergeExtraContributes(contributes: Record<string, any>) {
|
|
4948
|
+
if (!fs.existsSync(EXTRA_PATH)) return;
|
|
4949
|
+
let extra: Record<string, any>;
|
|
4950
|
+
try {
|
|
4951
|
+
extra = JSON.parse(fs.readFileSync(EXTRA_PATH, 'utf8'));
|
|
4952
|
+
} catch (err) {
|
|
4953
|
+
console.warn(\`! Skipping contributes.extra.json — invalid JSON: \${(err as Error).message}\`);
|
|
4954
|
+
return;
|
|
4955
|
+
}
|
|
4956
|
+
if (!extra || typeof extra !== 'object') return;
|
|
4957
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
4958
|
+
if (GEN_OWNED_KEYS.has(key)) continue;
|
|
4959
|
+
contributes[key] = deepMerge(contributes[key], value);
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
|
|
4963
|
+
function isPlainObject(v: unknown): v is Record<string, any> {
|
|
4964
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
function deepMerge(base: any, override: any): any {
|
|
4968
|
+
if (isPlainObject(base) && isPlainObject(override)) {
|
|
4969
|
+
const out: Record<string, any> = { ...base };
|
|
4970
|
+
for (const [k, v] of Object.entries(override)) out[k] = deepMerge(base[k], v);
|
|
4971
|
+
return out;
|
|
4972
|
+
}
|
|
4973
|
+
// arrays and primitives: override wins
|
|
4974
|
+
return override;
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4375
4977
|
function loadDef(file: string): {
|
|
4376
4978
|
id?: string;
|
|
4377
4979
|
title?: string;
|
|
@@ -6537,7 +7139,7 @@ var init_add9 = __esm(() => {
|
|
|
6537
7139
|
init_config();
|
|
6538
7140
|
fs18 = __toESM(require("fs"));
|
|
6539
7141
|
path18 = __toESM(require("path"));
|
|
6540
|
-
HELPER_KINDS = ["secrets", "config", "state", "notifications", "cache"];
|
|
7142
|
+
HELPER_KINDS = ["secrets", "config", "state", "notifications", "cache", "colorize"];
|
|
6541
7143
|
});
|
|
6542
7144
|
|
|
6543
7145
|
// src/lib/job/add.ts
|