@vsceasy/cli 0.1.8 → 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.
Files changed (30) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +40 -0
  3. package/dist/bin/cli.js +858 -76
  4. package/dist/index.js +717 -19
  5. package/dist/lib/config.d.ts +2 -0
  6. package/dist/lib/contributesMerge.d.ts +18 -0
  7. package/dist/lib/crud/parseModel.d.ts +12 -0
  8. package/dist/lib/helper/add.d.ts +1 -1
  9. package/dist/lib/model/add.d.ts +18 -0
  10. package/dist/lib/model/parseFields.d.ts +9 -3
  11. package/dist/lib/scaffold.d.ts +4 -0
  12. package/dist/lib/templatesData.d.ts +1 -1
  13. package/package.json +1 -1
  14. package/templates/_assets/language/README.language.md +39 -0
  15. package/templates/_assets/language/contributes.extra.json +40 -0
  16. package/templates/_assets/language/fileicons/{{langId}}-icon-theme.json +13 -0
  17. package/templates/_assets/language/icons/{{langId}}.svg +5 -0
  18. package/templates/_assets/language/language-configuration.json +30 -0
  19. package/templates/_assets/language/snippets/{{langId}}.json +12 -0
  20. package/templates/_assets/language/src/colorize.ts +31 -0
  21. package/templates/_assets/language/src/commands/applyColors.ts +11 -0
  22. package/templates/_assets/language/src/commands/removeColors.ts +11 -0
  23. package/templates/_assets/language/src/extension/extension.ts +25 -0
  24. package/templates/_assets/language/src/helpers/colorize.ts +70 -0
  25. package/templates/_assets/language/syntaxes/{{langId}}.tmLanguage.json +45 -0
  26. package/templates/_generators/crud/formApp.tsx.tpl +2 -0
  27. package/templates/_generators/crud/formPanel.ts.tpl +2 -2
  28. package/templates/_generators/helper/colorize.ts.tpl +85 -0
  29. package/templates/_generators/model/model.ts.tpl +1 -1
  30. package/templates/react/scripts/gen.ts +55 -0
@@ -1,6 +1,8 @@
1
1
  export interface VsceasyConfig {
2
2
  publisher?: string;
3
3
  commandPrefix?: string;
4
+ /** Extension shape. Omitted for legacy 'ui' projects. */
5
+ type?: 'ui' | 'language' | 'empty';
4
6
  ui?: 'react';
5
7
  defaultIcon?: string;
6
8
  defaultCategory?: string;
@@ -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>;
@@ -1,8 +1,18 @@
1
+ export interface ParsedRelation {
2
+ /** FK field on this model (e.g. `categoryId`). */
3
+ field: string;
4
+ /** Related model name (e.g. `Category`). */
5
+ model: string;
6
+ /** Field on the related model to show in the picker. */
7
+ label?: string;
8
+ }
1
9
  export interface ParsedField {
2
10
  name: string;
3
11
  /** Raw TS type as written in the interface (e.g. `string`, `number`, `'a' | 'b'`, `Date`). */
4
12
  type: string;
5
13
  optional: boolean;
14
+ /** Set when this field is a foreign key declared via `<Name>Relations`. */
15
+ relation?: ParsedRelation;
6
16
  }
7
17
  export interface ParsedModel {
8
18
  /** PascalCase interface name. */
@@ -19,6 +29,8 @@ export interface ParsedModel {
19
29
  indexes: string[];
20
30
  /** Ordered field list from the interface body. */
21
31
  fields: ParsedField[];
32
+ /** FK field → relation metadata, keyed by FK field name. */
33
+ relations: Record<string, ParsedRelation>;
22
34
  /** Absolute path the model was read from. */
23
35
  path: string;
24
36
  }
@@ -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;
@@ -1,3 +1,9 @@
1
+ export interface FieldRelation {
2
+ /** PascalCase name of the related model (e.g. `Category`). */
3
+ model: string;
4
+ /** Field on the related model to show in pickers. Default: first string field, else its pk. */
5
+ label?: string;
6
+ }
1
7
  export interface ModelField {
2
8
  name: string;
3
9
  /** Raw TS type. e.g. `string`, `number`, `string | null`, `Date`, `'a' | 'b'`. */
@@ -8,6 +14,12 @@ export interface ModelField {
8
14
  primaryKey?: boolean;
9
15
  /** Add to entity `indexes` (speeds up findOne by this field). */
10
16
  indexed?: boolean;
17
+ /**
18
+ * ManyToOne relation. When set, the field is emitted as a `<name>Id: string`
19
+ * foreign key and recorded in the model's relation metadata so `crud add`
20
+ * renders a populated dropdown. Authored as `name:ref(Model)` in the spec.
21
+ */
22
+ relation?: FieldRelation;
11
23
  }
12
24
  export interface AddModelOptions {
13
25
  name: string;
@@ -23,5 +35,11 @@ export interface AddModelResult {
23
35
  created: string[];
24
36
  primaryKey: string;
25
37
  indexes: string[];
38
+ /** Foreign-key fields and the models they point at. */
39
+ relations: Array<{
40
+ field: string;
41
+ model: string;
42
+ label?: string;
43
+ }>;
26
44
  }
27
45
  export declare function addModel(opts: AddModelOptions): AddModelResult;
@@ -1,4 +1,4 @@
1
- import type { ModelField } from './add';
1
+ import type { ModelField, FieldRelation } from './add';
2
2
  /**
3
3
  * Parse a compact model field spec into `ModelField[]`.
4
4
  *
@@ -7,8 +7,14 @@ import type { ModelField } from './add';
7
7
  * `!` after type → primaryKey
8
8
  * `@` after type → indexed
9
9
  *
10
- * Example: `id:string!,name:string,email?:string@,score:number`
10
+ * Relations use `name:ref(Model)` or `name:ref(Model, label=field)`:
11
+ * category:ref(Category) → FK categoryId, dropdown of Category rows
12
+ * category:ref(Category, label=name) → show Category.name in the dropdown
13
+ *
14
+ * Example: `id:string!,name:string,email?:string@,category:ref(Category)`
11
15
  */
12
16
  export declare function parseFieldsSpec(spec: string): ModelField[];
13
- /** Parse a single `name[?]:type[!][@]` line. Throws on malformed input. */
17
+ /** Parse a single `name[?]:type[!][@]` line (or `name:ref(Model)`). Throws on malformed input. */
14
18
  export declare function parseFieldLine(raw: string): ModelField;
19
+ /** Parse `ref(Model)` / `ref(Model, label=field)`. Returns null when not a ref. */
20
+ export declare function parseRef(type: string): FieldRelation | null;
@@ -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.8";
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.8",
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,13 @@
1
+ {
2
+ "iconDefinitions": {
3
+ "{{langId}}_file": {
4
+ "iconPath": "../icons/{{langId}}.svg"
5
+ }
6
+ },
7
+ "languageIds": {
8
+ "{{langId}}": "{{langId}}_file"
9
+ },
10
+ "fileExtensions": {
11
+ "{{langId}}": "{{langId}}_file"
12
+ }
13
+ }
@@ -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,12 @@
1
+ {
2
+ "Section": {
3
+ "prefix": "section",
4
+ "body": ["[${1:name}]", "$0"],
5
+ "description": "A {{displayName}} section"
6
+ },
7
+ "Key/Value": {
8
+ "prefix": "kv",
9
+ "body": ["${1:key} = ${2:value}"],
10
+ "description": "A key/value pair"
11
+ }
12
+ }
@@ -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
+ }
@@ -22,6 +22,7 @@ export function App() {
22
22
  const [editingId, setEditingId] = useState<{{Name}}['{{primaryKey}}'] | null>(null);
23
23
  const [error, setError] = useState<string | null>(null);
24
24
  const [saving, setSaving] = useState(false);
25
+ {{relationOptionsState}}
25
26
 
26
27
  const load = useCallback(async (initial: boolean) => {
27
28
  // The list stashes a row id before revealing this panel. Pull it (the host
@@ -59,6 +60,7 @@ export function App() {
59
60
  document.removeEventListener('visibilitychange', onVisible);
60
61
  };
61
62
  }, [load]);
63
+ {{relationOptionsLoad}}
62
64
 
63
65
  const onChange = <K extends keyof FormState>(k: K, v: FormState[K]) => {
64
66
  setForm((f) => ({ ...f, [k]: v }));
@@ -3,7 +3,7 @@ import { {{Name}}Service } from '../services/{{Name}}Service';
3
3
  import { takePending{{Name}}Id } from '../services/{{name}}FormNav';
4
4
  import type { {{Name}}FormApi } from '../shared/api';
5
5
  import type { {{Name}} } from '../models/{{Name}}';
6
-
6
+ {{relationImports}}
7
7
  export default definePanel<{{Name}}FormApi>({
8
8
  title: '{{title}}',
9
9
  column: 'beside',
@@ -25,7 +25,7 @@ export default definePanel<{{Name}}FormApi>({
25
25
  void vscode.commands.executeCommand('{{prefix}}.open{{Plural}}List');
26
26
  return saved;
27
27
  },
28
- async cancel() {
28
+ {{relationOptionsHandler}} async cancel() {
29
29
  // No-op — webview closes itself.
30
30
  },
31
31
  }),
@@ -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
+ }
@@ -14,4 +14,4 @@ export const {{Plural}} = defineEntity<{{Name}}>('{{collection}}', {
14
14
  * import { {{Plural}}Repo } from '../models/{{Name}}';
15
15
  * await {{Plural}}Repo().insert({ ... });
16
16
  */
17
- export const {{Plural}}Repo = () => db()({{Plural}});
17
+ export const {{Plural}}Repo = () => db()({{Plural}});{{relationsBlock}}