@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/lib/config.d.ts
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-merge helper for `contributes.extra.json`.
|
|
3
|
+
*
|
|
4
|
+
* This is the source of truth for the merge algorithm used by the scaffolded
|
|
5
|
+
* project's `scripts/gen.ts`. The template (`templates/react/scripts/gen.ts`)
|
|
6
|
+
* carries an inline copy because it must run standalone with no dependency on
|
|
7
|
+
* this package — keep the two in sync. This module exists so the algorithm is
|
|
8
|
+
* unit-testable in-process (no subprocess).
|
|
9
|
+
*/
|
|
10
|
+
/** Keys gen.ts owns — never overridden by contributes.extra.json. */
|
|
11
|
+
export declare const GEN_OWNED_KEYS: Set<string>;
|
|
12
|
+
export declare function isPlainObject(v: unknown): v is Record<string, any>;
|
|
13
|
+
export declare function deepMerge(base: any, override: any): any;
|
|
14
|
+
/**
|
|
15
|
+
* Merge `extra` into `contributes` in place. gen-owned keys present in `extra`
|
|
16
|
+
* are ignored so gen.ts stays authoritative for them.
|
|
17
|
+
*/
|
|
18
|
+
export declare function applyExtraContributes(contributes: Record<string, any>, extra: Record<string, any> | null | undefined): Record<string, any>;
|
package/dist/lib/helper/add.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type HelperKind = 'secrets' | 'config' | 'state' | 'notifications' | 'cache';
|
|
1
|
+
export type HelperKind = 'secrets' | 'config' | 'state' | 'notifications' | 'cache' | 'colorize';
|
|
2
2
|
export declare const HELPER_KINDS: HelperKind[];
|
|
3
3
|
export interface AddHelperOptions {
|
|
4
4
|
kind: HelperKind;
|
package/dist/lib/scaffold.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type ScaffoldPreset = 'minimal' | 'full';
|
|
2
|
+
export type ScaffoldType = 'ui' | 'language' | 'empty';
|
|
2
3
|
export interface ScaffoldOptions {
|
|
3
4
|
name: string;
|
|
4
5
|
displayName: string;
|
|
@@ -7,6 +8,9 @@ export interface ScaffoldOptions {
|
|
|
7
8
|
ui: 'react';
|
|
8
9
|
targetDir: string;
|
|
9
10
|
templatesRoot: string;
|
|
11
|
+
/** Extension shape. Default: 'ui'. */
|
|
12
|
+
type?: ScaffoldType;
|
|
13
|
+
/** UI preset — only used when type === 'ui'. */
|
|
10
14
|
preset?: ScaffoldPreset;
|
|
11
15
|
}
|
|
12
16
|
export declare function scaffold(opts: ScaffoldOptions): Promise<void>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const TEMPLATES_VERSION = "0.1.
|
|
1
|
+
export declare const TEMPLATES_VERSION = "0.1.10";
|
|
2
2
|
export declare const TEMPLATE_FILES: Record<string, string>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vsceasy/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Build VS Code extensions fast — React UI + typed RPC bridge between extension and webview + file-based routing for panels, commands, menus, tree views, and subpanels.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
## {{displayName}} language support
|
|
2
|
+
|
|
3
|
+
This extension was scaffolded with `vsceasy create --type language`. It provides
|
|
4
|
+
editor support for `.{{langId}}` files:
|
|
5
|
+
|
|
6
|
+
- **Syntax highlighting** — TextMate grammar in `syntaxes/{{langId}}.tmLanguage.json`
|
|
7
|
+
- **Language configuration** — comments, brackets, auto-closing pairs, folding in
|
|
8
|
+
`language-configuration.json`
|
|
9
|
+
- **Snippets** — `snippets/{{langId}}.json`
|
|
10
|
+
- **File icon** (opt-in) — `fileicons/{{langId}}-icon-theme.json`
|
|
11
|
+
|
|
12
|
+
### How contributions are wired
|
|
13
|
+
|
|
14
|
+
`vsceasy`'s `scripts/gen.ts` regenerates the generated parts of
|
|
15
|
+
`package.json#contributes` (commands, views…) on every build. Language
|
|
16
|
+
contributions are **not** generated — they live in **`contributes.extra.json`**
|
|
17
|
+
at the project root and are deep-merged into `package.json` by `gen.ts`. Edit
|
|
18
|
+
`contributes.extra.json` to change languages / grammars / snippets / iconThemes,
|
|
19
|
+
then run `bun run gen`.
|
|
20
|
+
|
|
21
|
+
### File icon is opt-in
|
|
22
|
+
|
|
23
|
+
VS Code file icons are provided by an **icon theme**, which is global: activating
|
|
24
|
+
it replaces *all* file icons in the workbench, not just `.{{langId}}`. This
|
|
25
|
+
extension ships a `{{displayName}} Icons` theme but does **not** force it. To use
|
|
26
|
+
it: `Preferences: File Icon Theme` → pick `{{displayName}} Icons`. If you don't
|
|
27
|
+
want to override every icon, leave it unselected — highlighting, config and
|
|
28
|
+
snippets work regardless.
|
|
29
|
+
|
|
30
|
+
### Develop
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
bun install
|
|
34
|
+
bun run gen # sync package.json#contributes
|
|
35
|
+
bun run package # build a .vsix
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Press `F5` (or run `bun run launch`) to open an Extension Development Host and
|
|
39
|
+
open a `.{{langId}}` file to see highlighting.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"languages": [
|
|
3
|
+
{
|
|
4
|
+
"id": "{{langId}}",
|
|
5
|
+
"aliases": ["{{displayName}}", "{{langId}}"],
|
|
6
|
+
"extensions": [".{{langId}}"],
|
|
7
|
+
"configuration": "./language-configuration.json"
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"grammars": [
|
|
11
|
+
{
|
|
12
|
+
"language": "{{langId}}",
|
|
13
|
+
"scopeName": "{{scopeName}}",
|
|
14
|
+
"path": "./syntaxes/{{langId}}.tmLanguage.json"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"snippets": [
|
|
18
|
+
{
|
|
19
|
+
"language": "{{langId}}",
|
|
20
|
+
"path": "./snippets/{{langId}}.json"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"iconThemes": [
|
|
24
|
+
{
|
|
25
|
+
"id": "{{langId}}-icons",
|
|
26
|
+
"label": "{{displayName}} Icons",
|
|
27
|
+
"path": "./fileicons/{{langId}}-icon-theme.json"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"configuration": {
|
|
31
|
+
"title": "{{displayName}}",
|
|
32
|
+
"properties": {
|
|
33
|
+
"{{commandPrefix}}.colorize": {
|
|
34
|
+
"type": "boolean",
|
|
35
|
+
"default": true,
|
|
36
|
+
"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."
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
2
|
+
<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"/>
|
|
3
|
+
<path fill="#5a5a5a" d="M20 3l6 6h-6z"/>
|
|
4
|
+
<text x="16" y="22" font-family="monospace" font-size="9" font-weight="bold" fill="#fff" text-anchor="middle">{{langExt}}</text>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"comments": {
|
|
3
|
+
"lineComment": "#"
|
|
4
|
+
},
|
|
5
|
+
"brackets": [
|
|
6
|
+
["{", "}"],
|
|
7
|
+
["[", "]"],
|
|
8
|
+
["(", ")"]
|
|
9
|
+
],
|
|
10
|
+
"autoClosingPairs": [
|
|
11
|
+
{ "open": "{", "close": "}" },
|
|
12
|
+
{ "open": "[", "close": "]" },
|
|
13
|
+
{ "open": "(", "close": ")" },
|
|
14
|
+
{ "open": "\"", "close": "\"", "notIn": ["string"] },
|
|
15
|
+
{ "open": "'", "close": "'", "notIn": ["string"] }
|
|
16
|
+
],
|
|
17
|
+
"surroundingPairs": [
|
|
18
|
+
["{", "}"],
|
|
19
|
+
["[", "]"],
|
|
20
|
+
["(", ")"],
|
|
21
|
+
["\"", "\""],
|
|
22
|
+
["'", "'"]
|
|
23
|
+
],
|
|
24
|
+
"folding": {
|
|
25
|
+
"markers": {
|
|
26
|
+
"start": "^\\s*#\\s*region\\b",
|
|
27
|
+
"end": "^\\s*#\\s*endregion\\b"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type * as vscode from 'vscode';
|
|
2
|
+
import { applyTokenColors, removeTokenColors, type TokenColorRule } from './helpers/colorize';
|
|
3
|
+
|
|
4
|
+
/** Root TextMate scope of this language — rules are applied only to these files. */
|
|
5
|
+
export const SCOPE = '{{scopeName}}';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default token colors for {{displayName}}, emphasizing each construct. Edit
|
|
9
|
+
* freely — they are applied to `[{{scopeName}}]` only, so other languages keep
|
|
10
|
+
* the user's theme. Scope names must match your grammar
|
|
11
|
+
* (syntaxes/{{langId}}.tmLanguage.json).
|
|
12
|
+
*/
|
|
13
|
+
export const RULES: TokenColorRule[] = [
|
|
14
|
+
{ scope: 'comment.line.number-sign.{{langId}}', settings: { foreground: '#6b7a6e', fontStyle: 'italic' } },
|
|
15
|
+
{ scope: 'string.quoted.double.basic.{{langId}}', settings: { foreground: '#98c379' } },
|
|
16
|
+
{ scope: 'string.quoted.single.literal.{{langId}}', settings: { foreground: '#98c379' } },
|
|
17
|
+
{ scope: 'constant.numeric.{{langId}}', settings: { foreground: '#d19a66' } },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export async function applyColors(vscodeNs: typeof vscode): Promise<void> {
|
|
21
|
+
await applyTokenColors(SCOPE, RULES);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function removeColors(vscodeNs: typeof vscode): Promise<void> {
|
|
25
|
+
await removeTokenColors(SCOPE);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** True when the user has opted in (default) to automatic coloring. */
|
|
29
|
+
export function colorizeEnabled(vscodeNs: typeof vscode): boolean {
|
|
30
|
+
return vscodeNs.workspace.getConfiguration('{{commandPrefix}}').get<boolean>('colorize', true);
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineCommand } from '../shared/vsceasy';
|
|
2
|
+
import { applyColors } from '../colorize';
|
|
3
|
+
|
|
4
|
+
export default defineCommand({
|
|
5
|
+
id: 'applyColors',
|
|
6
|
+
title: '{{displayName}}: Apply Colors',
|
|
7
|
+
run: async (vscode) => {
|
|
8
|
+
await applyColors(vscode);
|
|
9
|
+
vscode.window.showInformationMessage('{{displayName}} colors applied.');
|
|
10
|
+
},
|
|
11
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineCommand } from '../shared/vsceasy';
|
|
2
|
+
import { removeColors } from '../colorize';
|
|
3
|
+
|
|
4
|
+
export default defineCommand({
|
|
5
|
+
id: 'removeColors',
|
|
6
|
+
title: '{{displayName}}: Remove Colors',
|
|
7
|
+
run: async (vscode) => {
|
|
8
|
+
await removeColors(vscode);
|
|
9
|
+
vscode.window.showInformationMessage('{{displayName}} colors removed.');
|
|
10
|
+
},
|
|
11
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { bootstrap } from '../shared/vsceasy';
|
|
2
|
+
import { registry } from './_registry';
|
|
3
|
+
import { applyColors, removeColors, colorizeEnabled } from '../colorize';
|
|
4
|
+
|
|
5
|
+
export const activate = bootstrap(registry, {
|
|
6
|
+
onActivate: [
|
|
7
|
+
async (context, vscode) => {
|
|
8
|
+
// Auto-apply scoped token colors on activate when opted in (default).
|
|
9
|
+
// Scoped to {{scopeName}} only — other languages are untouched.
|
|
10
|
+
if (colorizeEnabled(vscode)) {
|
|
11
|
+
await applyColors(vscode);
|
|
12
|
+
}
|
|
13
|
+
// React to the user toggling `{{commandPrefix}}.colorize` at runtime.
|
|
14
|
+
context.subscriptions.push(
|
|
15
|
+
vscode.workspace.onDidChangeConfiguration(async (e) => {
|
|
16
|
+
if (!e.affectsConfiguration('{{commandPrefix}}.colorize')) return;
|
|
17
|
+
if (colorizeEnabled(vscode)) await applyColors(vscode);
|
|
18
|
+
else await removeColors(vscode);
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export function deactivate() {}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as vscode from 'vscode';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apply theme-independent token colors to a single TextMate scope (e.g. a
|
|
5
|
+
* language's root scope like `{{scopeName}}`), written to the user's
|
|
6
|
+
* `editor.tokenColorCustomizations`. Because the rules are keyed by
|
|
7
|
+
* `[<scope>]`, only files in that scope are recolored — every other language
|
|
8
|
+
* keeps the active theme's colors.
|
|
9
|
+
*
|
|
10
|
+
* Rules are tagged with a marker so {@link removeTokenColors} can strip exactly
|
|
11
|
+
* the ones this extension added, preserving any the user wrote by hand.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface TokenColorRule {
|
|
15
|
+
/** Comma-separated TextMate scopes, e.g. 'entity.name.section.foo, comment.line.foo'. */
|
|
16
|
+
scope: string;
|
|
17
|
+
settings: { foreground?: string; background?: string; fontStyle?: string };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type TaggedRule = TokenColorRule & { [MARK]?: true };
|
|
21
|
+
|
|
22
|
+
/** Marker key identifying rules this extension wrote (vs. the user's own). */
|
|
23
|
+
const MARK = '{{commandPrefix}}Colorize';
|
|
24
|
+
const SECTION = 'editor.tokenColorCustomizations';
|
|
25
|
+
|
|
26
|
+
const blockKey = (scope: string) => `[${scope}]`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Merge `rules` into `editor.tokenColorCustomizations["[<scope>]"].textMateRules`,
|
|
30
|
+
* preserving the user's own rules and other scope keys. Idempotent — re-applying
|
|
31
|
+
* replaces only previously-applied rules from this extension.
|
|
32
|
+
*/
|
|
33
|
+
export async function applyTokenColors(
|
|
34
|
+
scope: string,
|
|
35
|
+
rules: TokenColorRule[],
|
|
36
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
39
|
+
const current = (cfg.get<Record<string, any>>(SECTION) ?? {}) as Record<string, any>;
|
|
40
|
+
const key = blockKey(scope);
|
|
41
|
+
const block = (current[key] ?? {}) as { textMateRules?: TaggedRule[] };
|
|
42
|
+
const existing = Array.isArray(block.textMateRules) ? block.textMateRules : [];
|
|
43
|
+
const userRules = existing.filter((r) => !r[MARK]);
|
|
44
|
+
const ours: TaggedRule[] = rules.map((r) => ({ ...r, [MARK]: true }));
|
|
45
|
+
const next = { ...current, [key]: { ...block, textMateRules: [...userRules, ...ours] } };
|
|
46
|
+
await cfg.update(SECTION, next, target);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Remove only the rules this extension added for `scope`; leave the rest intact. */
|
|
50
|
+
export async function removeTokenColors(
|
|
51
|
+
scope: string,
|
|
52
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
55
|
+
const current = cfg.get<Record<string, any>>(SECTION);
|
|
56
|
+
const key = blockKey(scope);
|
|
57
|
+
if (!current || !current[key]) return;
|
|
58
|
+
const block = current[key] as { textMateRules?: TaggedRule[] };
|
|
59
|
+
const userRules = (block.textMateRules ?? []).filter((r) => !r[MARK]);
|
|
60
|
+
|
|
61
|
+
const nextBlock: Record<string, unknown> = { ...block };
|
|
62
|
+
if (userRules.length) nextBlock.textMateRules = userRules;
|
|
63
|
+
else delete nextBlock.textMateRules;
|
|
64
|
+
|
|
65
|
+
const next = { ...current };
|
|
66
|
+
if (Object.keys(nextBlock).length) next[key] = nextBlock;
|
|
67
|
+
else delete next[key];
|
|
68
|
+
|
|
69
|
+
await cfg.update(SECTION, Object.keys(next).length ? next : undefined, target);
|
|
70
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
|
3
|
+
"name": "{{displayName}}",
|
|
4
|
+
"scopeName": "{{scopeName}}",
|
|
5
|
+
"patterns": [
|
|
6
|
+
{ "include": "#comments" },
|
|
7
|
+
{ "include": "#strings" },
|
|
8
|
+
{ "include": "#numbers" }
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"comments": {
|
|
12
|
+
"patterns": [
|
|
13
|
+
{
|
|
14
|
+
"name": "comment.line.number-sign.{{langId}}",
|
|
15
|
+
"match": "#.*$"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"strings": {
|
|
20
|
+
"patterns": [
|
|
21
|
+
{
|
|
22
|
+
"name": "string.quoted.double.{{langId}}",
|
|
23
|
+
"begin": "\"",
|
|
24
|
+
"end": "\"",
|
|
25
|
+
"patterns": [
|
|
26
|
+
{ "name": "constant.character.escape.{{langId}}", "match": "\\\\." }
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "string.quoted.single.{{langId}}",
|
|
31
|
+
"begin": "'",
|
|
32
|
+
"end": "'"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"numbers": {
|
|
37
|
+
"patterns": [
|
|
38
|
+
{
|
|
39
|
+
"name": "constant.numeric.{{langId}}",
|
|
40
|
+
"match": "\\b[0-9]+(\\.[0-9]+)?\\b"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as vscode from 'vscode';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apply theme-independent token colors to a single TextMate scope (e.g. a
|
|
5
|
+
* language's root scope like `source.toml`), written to the user's
|
|
6
|
+
* `editor.tokenColorCustomizations`. Because the rules are keyed by
|
|
7
|
+
* `[<scope>]`, only files in that scope are recolored — every other language
|
|
8
|
+
* keeps the active theme's colors.
|
|
9
|
+
*
|
|
10
|
+
* Rules are tagged with a marker so {@link removeTokenColors} can strip exactly
|
|
11
|
+
* the ones this extension added, preserving any the user wrote by hand.
|
|
12
|
+
*
|
|
13
|
+
* Typical use — auto-apply on activate behind an opt-out setting:
|
|
14
|
+
*
|
|
15
|
+
* // extension.ts (onActivate hook)
|
|
16
|
+
* if (config.get<boolean>('colorize', true)) {
|
|
17
|
+
* await applyTokenColors('source.{{commandPrefix}}', MY_RULES);
|
|
18
|
+
* }
|
|
19
|
+
* vscode.workspace.onDidChangeConfiguration(async (e) => {
|
|
20
|
+
* if (!e.affectsConfiguration('{{commandPrefix}}.colorize')) return;
|
|
21
|
+
* if (config.get<boolean>('colorize', true)) await applyTokenColors('source.{{commandPrefix}}', MY_RULES);
|
|
22
|
+
* else await removeTokenColors('source.{{commandPrefix}}');
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* Declare the opt-out in package.json#contributes.configuration:
|
|
26
|
+
* "{{commandPrefix}}.colorize": { "type": "boolean", "default": true }
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export interface TokenColorRule {
|
|
30
|
+
/** Comma-separated TextMate scopes, e.g. 'entity.name.section.foo, comment.line.foo'. */
|
|
31
|
+
scope: string;
|
|
32
|
+
settings: { foreground?: string; background?: string; fontStyle?: string };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type TaggedRule = TokenColorRule & { [MARK]?: true };
|
|
36
|
+
|
|
37
|
+
/** Marker key identifying rules this extension wrote (vs. the user's own). */
|
|
38
|
+
const MARK = '{{commandPrefix}}Colorize';
|
|
39
|
+
const SECTION = 'editor.tokenColorCustomizations';
|
|
40
|
+
|
|
41
|
+
const blockKey = (scope: string) => `[${scope}]`;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Merge `rules` into `editor.tokenColorCustomizations["[<scope>]"].textMateRules`,
|
|
45
|
+
* preserving the user's own rules and other scope keys. Idempotent — re-applying
|
|
46
|
+
* replaces only previously-applied rules from this extension.
|
|
47
|
+
*/
|
|
48
|
+
export async function applyTokenColors(
|
|
49
|
+
scope: string,
|
|
50
|
+
rules: TokenColorRule[],
|
|
51
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
54
|
+
const current = (cfg.get<Record<string, any>>(SECTION) ?? {}) as Record<string, any>;
|
|
55
|
+
const key = blockKey(scope);
|
|
56
|
+
const block = (current[key] ?? {}) as { textMateRules?: TaggedRule[] };
|
|
57
|
+
const existing = Array.isArray(block.textMateRules) ? block.textMateRules : [];
|
|
58
|
+
const userRules = existing.filter((r) => !r[MARK]);
|
|
59
|
+
const ours: TaggedRule[] = rules.map((r) => ({ ...r, [MARK]: true }));
|
|
60
|
+
const next = { ...current, [key]: { ...block, textMateRules: [...userRules, ...ours] } };
|
|
61
|
+
await cfg.update(SECTION, next, target);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Remove only the rules this extension added for `scope`; leave the rest intact. */
|
|
65
|
+
export async function removeTokenColors(
|
|
66
|
+
scope: string,
|
|
67
|
+
target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const cfg = vscode.workspace.getConfiguration();
|
|
70
|
+
const current = cfg.get<Record<string, any>>(SECTION);
|
|
71
|
+
const key = blockKey(scope);
|
|
72
|
+
if (!current || !current[key]) return;
|
|
73
|
+
const block = current[key] as { textMateRules?: TaggedRule[] };
|
|
74
|
+
const userRules = (block.textMateRules ?? []).filter((r) => !r[MARK]);
|
|
75
|
+
|
|
76
|
+
const nextBlock: Record<string, unknown> = { ...block };
|
|
77
|
+
if (userRules.length) nextBlock.textMateRules = userRules;
|
|
78
|
+
else delete nextBlock.textMateRules;
|
|
79
|
+
|
|
80
|
+
const next = { ...current };
|
|
81
|
+
if (Object.keys(nextBlock).length) next[key] = nextBlock;
|
|
82
|
+
else delete next[key];
|
|
83
|
+
|
|
84
|
+
await cfg.update(SECTION, Object.keys(next).length ? next : undefined, target);
|
|
85
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// Scans src/panels, src/commands, and src/menus; writes src/extension/_registry.ts
|
|
3
3
|
// and syncs package.json#contributes (commands, viewsContainers, views).
|
|
4
|
+
//
|
|
5
|
+
// Non-generated contributes (e.g. languages, grammars, snippets, themes,
|
|
6
|
+
// iconThemes, walkthroughs) go in an optional `contributes.extra.json` at the
|
|
7
|
+
// project root. It is deep-merged into package.json#contributes on every run —
|
|
8
|
+
// the keys gen.ts owns (commands, keybindings, menus.commandPalette,
|
|
9
|
+
// viewsContainers, views) always win, everything else from extra is preserved.
|
|
4
10
|
|
|
5
11
|
import * as fs from 'fs';
|
|
6
12
|
import * as path from 'path';
|
|
@@ -16,6 +22,10 @@ const TREE_VIEWS_DIR = path.join(SRC, 'treeViews');
|
|
|
16
22
|
const JOBS_DIR = path.join(SRC, 'jobs');
|
|
17
23
|
const OUT = path.join(SRC, 'extension', '_registry.ts');
|
|
18
24
|
const PKG_PATH = path.join(ROOT, 'package.json');
|
|
25
|
+
const EXTRA_PATH = path.join(ROOT, 'contributes.extra.json');
|
|
26
|
+
|
|
27
|
+
/** Keys gen.ts owns — never overridden by contributes.extra.json. */
|
|
28
|
+
const GEN_OWNED_KEYS = new Set(['commands', 'keybindings', 'viewsContainers', 'views']);
|
|
19
29
|
|
|
20
30
|
interface Discovered {
|
|
21
31
|
id: string;
|
|
@@ -204,9 +214,54 @@ function syncPackageJson(
|
|
|
204
214
|
delete contributes.views;
|
|
205
215
|
}
|
|
206
216
|
|
|
217
|
+
mergeExtraContributes(contributes);
|
|
218
|
+
|
|
207
219
|
fs.writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\n');
|
|
208
220
|
}
|
|
209
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Deep-merge the optional `contributes.extra.json` (project root) into the
|
|
224
|
+
* package's `contributes`. Use for any contribution point gen.ts doesn't
|
|
225
|
+
* generate — languages, grammars, snippets, themes, iconThemes, walkthroughs…
|
|
226
|
+
*
|
|
227
|
+
* Rules:
|
|
228
|
+
* - gen-owned keys (commands, keybindings, viewsContainers, views) are ignored
|
|
229
|
+
* if present in extra — gen.ts stays authoritative for those.
|
|
230
|
+
* - plain objects merge recursively; arrays and primitives from extra replace.
|
|
231
|
+
*
|
|
232
|
+
* NOTE: this is an inline copy of src/lib/contributesMerge.ts in the vsceasy
|
|
233
|
+
* source (the script must run standalone). Keep the two in sync.
|
|
234
|
+
*/
|
|
235
|
+
function mergeExtraContributes(contributes: Record<string, any>) {
|
|
236
|
+
if (!fs.existsSync(EXTRA_PATH)) return;
|
|
237
|
+
let extra: Record<string, any>;
|
|
238
|
+
try {
|
|
239
|
+
extra = JSON.parse(fs.readFileSync(EXTRA_PATH, 'utf8'));
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.warn(`! Skipping contributes.extra.json — invalid JSON: ${(err as Error).message}`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (!extra || typeof extra !== 'object') return;
|
|
245
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
246
|
+
if (GEN_OWNED_KEYS.has(key)) continue;
|
|
247
|
+
contributes[key] = deepMerge(contributes[key], value);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isPlainObject(v: unknown): v is Record<string, any> {
|
|
252
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function deepMerge(base: any, override: any): any {
|
|
256
|
+
if (isPlainObject(base) && isPlainObject(override)) {
|
|
257
|
+
const out: Record<string, any> = { ...base };
|
|
258
|
+
for (const [k, v] of Object.entries(override)) out[k] = deepMerge(base[k], v);
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
// arrays and primitives: override wins
|
|
262
|
+
return override;
|
|
263
|
+
}
|
|
264
|
+
|
|
210
265
|
function loadDef(file: string): {
|
|
211
266
|
id?: string;
|
|
212
267
|
title?: string;
|