Update myTemplateGenerator to version 0.0.5. Introduced new configuration options, added support for additional case modifiers using the change-case-all package, and improved the webview for template selection and variable input. Updated package.json and package-lock.json accordingly. Added localization support for configuration settings and enhanced README with usage instructions.

This commit is contained in:
S.Gromov
2025-07-14 23:37:01 +03:00
parent 210ff6ca57
commit a98b1a0464
16 changed files with 1145 additions and 1240 deletions

271
README.md
View File

@@ -1,211 +1,134 @@
[English](#english) | [Русский](#русский)
[🇬🇧 English](#mytemplategenerator) | [🇷🇺 Русский](#mytemplategenerator-русский)
---
# MyTemplateGenerator
# English
**Generate files and folders from templates with variable substitution right from the VS Code context menu.**
## My Template Generator — Template-based structure generation for VSCode
- Syntax highlighting and autocomplete for template variables in template files (`{{name}}`, `{{name.camelCase}}`, etc.)
- Instantly create project structure from templates with variables in file/folder names and content
- Visual configurator and full localization (English/Russian)
- Flexible settings: templates folder path, variable input mode, overwrite protection
![Logo](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/1.png)
**Features:**
- Syntax highlighting and autocomplete for template variables in templates
- Generate files and folders from templates with variable substitution
- Full localization (English/Russian) for all UI, messages, and menus
- Choose variable input mode: Webview (form) or inputBox (one by one)
- Overwrite control: allow or forbid overwriting existing files/folders
- Smart conflict handling: clear notifications if structure already exists
Context menu "Create from template..." is available by right-clicking any folder.
![Logo](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/2.png)
Convenient UI for creation: just select a template and specify values for the variables used in the template. (The list of variables updates automatically)
![Logo](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/3.png)
User-friendly settings UI.
![Logo](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/4.png)
**How to use:**
1. Create a folder with templates (default: `templates`).
2. Use variables in templates: `{{name}}`, `{{name.pascalCase}}`, etc.
3. Right-click any folder in your project → **Create from template...**
4. Select a template, fill in variables — the structure is generated automatically.
### Quick Start
1. Create a `templates` folder in your project root.
2. Add subfolders for different templates (e.g., `components`, `store`).
3. Use variables like `{{name}}` or `{{name.pascalCase}}` in file/folder names and file contents.
4. Right-click a folder in VSCode and select **Create from template...**
5. Choose a template, fill in variables, and click "Create".
### Example template structure
**Example template:**
```
templates/
components/
component/
{{name}}/
index.js
style.module.css
store/
{{name}}Store.js
index.tsx
{{name.camelCase}}.module.css
```
### Supported variables and modifiers
**Available modifiers:**
You can use variables with modifiers via dot notation:
| Modifier | Example (`name = myComponent`) |
|-----------------------|-------------------------------|
| `{{name}}` | myComponent |
| `{{name.pascalCase}}` | MyComponent |
| `{{name.camelCase}}` | myComponent |
| `{{name.snakeCase}}` | my_component |
| `{{name.kebabCase}}` | my-component |
| `{{name.screamingSnakeCase}}` | MY_COMPONENT |
| `{{name.upperCase}}` | Mycomponent |
| `{{name.lowerCase}}` | mycomponent |
| `{{name.upperCaseAll}}` | MYCOMPONENT |
| `{{name.lowerCaseAll}}` | mycomponent |
- `{{name}}` — as entered by user
- `{{name.pascalCase}}` — PascalCase
- `{{name.camelCase}}` — camelCase
- `{{name.snakeCase}}` — snake_case
- `{{name.kebabCase}}` — kebab-case
- `{{name.screamingSnakeCase}}` — SCREAMING_SNAKE_CASE
- `{{name.upperCase}}` — First letter uppercase
- `{{name.lowerCase}}` — all lowercase
- `{{name.upperCaseAll}}` — ALLUPPERCASE (no separators)
- `{{name.lowerCaseAll}}` — alllowercase (no separators)
**Supported modifiers:** pascalCase, camelCase, snakeCase, kebabCase, upperCase, lowerCase, and more.
> When searching for variables for the form, only the name before the dot is considered. For example, `{{name}}` and `{{name.pascalCase}}` are the same variable.
**Framework compatibility:**
### Example usage in template
```
components/
{{name.pascalCase}}/
index.js
{{name.camelCase}}.service.js
{{name.snakeCase}}.test.js
```
And in file contents:
```
export class {{name.pascalCase}} {}
const name = '{{name}}';
```
This extension works with **any framework** — you define your own templates for any structure you need!
### Configuration
**To open the visual configurator, press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>, type `Configure myTemplateGenerator...` and select the command.**
| Framework | Components | Store/State | Pages/Routes | Services | Utils |
|--------------|:----------:|:-----------:|:------------:|:--------:|:-----:|
| React | ✅ | ✅ | ✅ | ✅ | ✅ |
| Vue | ✅ | ✅ | ✅ | ✅ | ✅ |
| Angular | ✅ | ✅ | ✅ | ✅ | ✅ |
| Svelte | ✅ | ✅ | ✅ | ✅ | ✅ |
| Next.js | ✅ | ✅ | ✅ | ✅ | ✅ |
| Nuxt | ✅ | ✅ | ✅ | ✅ | ✅ |
| Solid | ✅ | ✅ | ✅ | ✅ | ✅ |
| Vanilla JS/TS| ✅ | ✅ | ✅ | ✅ | ✅ |
Use `mytemplategenerator.json` in your project root or the visual configurator (**Configure myTemplateGenerator...**):
```json
{
"templatesPath": "templates",
"overwriteFiles": false,
"inputMode": "webview", // or "inputBox"
"language": "en" // or "ru"
}
```
- **templatesPath** — path to templates folder
- **overwriteFiles** — allow or forbid overwriting existing files/folders
- **inputMode** — variable input mode: "webview" (form) or "inputBox" (one by one)
- **language** — plugin UI language (en/ru)
Just create a template for your favorite stack — and generate any structure you want! 🎉
### Localization
- All UI, messages, errors, and menus are localized.
- Webview and messages use the language from config.
- Menu/command language depends on VSCode interface language.
**Configuration:**
All settings via `mycodegenerate.json` in the project root or the visual configurator.
### Key commands
- **Create from template...** — generate structure (context menu)
- **Configure myTemplateGenerator...** — open visual configurator (command palette)
### Error handling & overwrite
- If structure or file exists and overwrite is forbidden, generation is cancelled and a clear notification is shown.
- Any file creation error stops generation and shows the reason.
---
# Русский
## My Template Generator — Генерация структуры из шаблонов для VSCode
![Логотип](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/1.png)
**Возможности:**
- Подсветка синтаксиса и автокомплит переменных в шаблонах
- Генерация файлов и папок по шаблонам с подстановкой переменных
- Полная локализация (русский/английский) для всего интерфейса, сообщений и меню
- Выбор способа ввода переменных: Webview (форма) или inputBox (по одной)
- Контроль перезаписи: можно запретить или разрешить перезапись существующих файлов/папок
- Умная обработка конфликтов: понятные уведомления, если структура уже существует
Контекстное меню "Создать из шаблона.." доступно правым кликом по любой папке.
![Логотип](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/2.png)
Удобный UI создания, нужно только выбрать шаблок и указать значения переменных используемых в шаблоне. (Список переменных обновляется автоматически)
![Логотип](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/3.png)
Удобный UI интерфейс настроек.
![Логотип](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/4.png)
To open the settings menu, press <kbd>Ctrl</kbd>+<kbd>P</kbd>, type `Configure myTemplateGenerator...` and select the menu item.
### Быстрый старт
1. В корне проекта создайте папку `templates`.
2. Внутри неё создайте подпапки для разных шаблонов (например, `components`, `store`).
3. Внутри шаблонов используйте переменные вида `{{name}}` или `{{name.pascalCase}}` в именах файлов/папок и в содержимом файлов.
4. Кликните правой кнопкой мыши на нужной папке в VSCode и выберите пункт **Создать из шаблона...**
5. В появившемся окне выберите шаблон, заполните переменные и нажмите "Создать".
# MyTemplateGenerator (русский)
### Пример структуры шаблонов
**Генерация файлов и папок по шаблонам с автозаменой переменных прямо из контекстного меню VS Code.**
- Подсветка и автокомплит переменных в шаблонных файлах (`{{name}}`, `{{name.camelCase}}` и др.)
- Быстрое создание структуры проекта по шаблонам с подстановкой переменных в имена файлов, папок и содержимое
- Визуальный конфигуратор и поддержка локализации (русский/английский)
- Гибкая настройка: путь к шаблонам, режим ввода переменных, запрет/разрешение перезаписи файлов
![Logo](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/1.png)
![Logo](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/3.png)
**Как использовать:**
1. Создайте папку с шаблонами (по умолчанию `templates`).
2. Используйте переменные в шаблонах: `{{name}}`, `{{name.pascalCase}}` и т.д.
3. Кликните правой кнопкой по папке в проекте → **Создать из шаблона...**
4. Выберите шаблон, заполните переменные — структура будет создана автоматически.
**Пример шаблона:**
```
templates/
components/
component/
{{name}}/
index.js
style.module.css
store/
{{name}}Store.js
index.tsx
{{name.camelCase}}.module.css
```
### Переменные и модификаторы
**Доступные модификаторы:**
В шаблонах можно использовать переменные с модификаторами через точку:
| Модификатор | Пример (`name = myComponent`) |
|----------------------|-------------------------------|
| `{{name}}` | myComponent |
| `{{name.pascalCase}}`| MyComponent |
| `{{name.camelCase}}` | myComponent |
| `{{name.snakeCase}}` | my_component |
| `{{name.kebabCase}}` | my-component |
| `{{name.screamingSnakeCase}}` | MY_COMPONENT |
| `{{name.upperCase}}` | Mycomponent |
| `{{name.lowerCase}}` | mycomponent |
| `{{name.upperCaseAll}}` | MYCOMPONENT |
| `{{name.lowerCaseAll}}` | mycomponent |
- `{{name}}` — как ввёл пользователь
- `{{name.pascalCase}}` — PascalCase
- `{{name.camelCase}}` — camelCase
- `{{name.snakeCase}}` — snake_case
- `{{name.kebabCase}}` — kebab-case
- `{{name.screamingSnakeCase}}` — SCREAMING_SNAKE_CASE
- `{{name.upperCase}}` — Первая буква большая
- `{{name.lowerCase}}` — все буквы маленькие
- `{{name.upperCaseAll}}`ВСЕ БУКВЫ БОЛЬШИЕ (без разделителей)
- `{{name.lowerCaseAll}}` — все буквы маленькие (без разделителей)
**Поддерживаемые модификаторы:** pascalCase, camelCase, snakeCase, kebabCase, upperCase, lowerCase и др.
> При поиске переменных для формы учитывается только имя до точки. Например, `{{name}}` и `{{name.pascalCase}}` — это одна переменная.
**Совместимость с фреймворками:**
### Пример использования в шаблоне
```
components/
{{name.pascalCase}}/
index.js
{{name.camelCase}}.service.js
{{name.snakeCase}}.test.js
```
Внутри файлов также можно использовать эти переменные:
```
export class {{name.pascalCase}} {}
const name = '{{name}}';
```
Плагин подходит для **любых фреймворков** — вы сами задаёте шаблоны для любой структуры!
### Конфигурация
**Чтобы открыть визуальный конфигуратор, нажмите <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>, введите `Настроить myTemplateGenerator...` и выберите команду.**
| Фреймворк | Компоненты | Store/State | Страницы/Роуты | Сервисы | Утилиты |
|--------------|:----------:|:-----------:|:--------------:|:-------:|:-------:|
| React | ✅ | ✅ | ✅ | ✅ | ✅ |
| Vue | ✅ | ✅ | ✅ | ✅ | ✅ |
| Angular | ✅ | ✅ | ✅ | ✅ | ✅ |
| Svelte | ✅ | ✅ | ✅ | ✅ | ✅ |
| Next.js | ✅ | ✅ | ✅ | ✅ | ✅ |
| Nuxt | ✅ | ✅ | ✅ | ✅ | ✅ |
| Solid | ✅ | ✅ | ✅ | ✅ | ✅ |
| Vanilla JS/TS| ✅ | ✅ | ✅ | ✅ | ✅ |
Для гибкой настройки используйте файл `mytemplategenerator.json` в корне проекта или визуальный конфигуратор (команда **Настроить myTemplateGenerator...**):
Создайте шаблон под свой стек — и генерируйте любые структуры! 🎉
```json
{
"templatesPath": "templates",
"overwriteFiles": false,
"inputMode": "webview", // или "inputBox"
"language": "ru" // или "en"
}
```
- **templatesPath** — путь к папке с шаблонами
- **overwriteFiles** — разрешать ли перезапись существующих файлов/папок
- **inputMode** — способ ввода переменных: "webview" (форма) или "inputBox" (по одной)
- **language** — язык интерфейса плагина (ru/en)
**Настройка:**
Всё настраивается через файл `mycodegenerate.json` в корне проекта или визуальный конфигуратор.
### Локализация
- Все сообщения, Webview, ошибки и пункты меню локализованы.
- Язык Webview и сообщений выбирается в конфигураторе.
- Язык пунктов меню и команд зависит от языка интерфейса VSCode.
### Важные команды
- **Создать из шаблона...** — генерация структуры (контекстное меню)
- **Настроить myTemplateGenerator...** — открыть визуальный конфигуратор (палитра команд)
### Обработка ошибок и перезаписи
- Если структура или файл уже существуют и перезапись запрещена, генерация не выполняется и выводится понятное уведомление.
- Если при создании любого файла возникает ошибка — генерация полностью прекращается, и пользователь видит причину.
---
Чтобы открыть меню настроек, нажмите <kbd>Ctrl</kbd>+<kbd>P</kbd>, введите `Настроить myTemplateGenerator...` (или `Configure myTemplateGenerator...` для английского интерфейса) и выберите соответствующий пункт.

Binary file not shown.

51
package-lock.json generated
View File

@@ -1,13 +1,15 @@
{
"name": "mytemplategenerator",
"version": "0.0.1",
"version": "0.0.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mytemplategenerator",
"version": "0.0.1",
"version": "0.0.5",
"dependencies": {
"change-case": "^5.4.4",
"change-case-all": "^2.1.0",
"handlebars": "^4.7.8"
},
"devDependencies": {
@@ -1333,6 +1335,24 @@
"node": ">=8"
}
},
"node_modules/change-case": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
"license": "MIT"
},
"node_modules/change-case-all": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/change-case-all/-/change-case-all-2.1.0.tgz",
"integrity": "sha512-v6b0WWWkZUMHVuYk82l+WROgkUm4qEN2w5hKRNWtEOYwWqUGoi8C6xH0l1RLF1EoWqDFK6MFclmN3od6ws3/uw==",
"license": "MIT",
"dependencies": {
"change-case": "^5.2.0",
"sponge-case": "^2.0.2",
"swap-case": "^3.0.2",
"title-case": "^3.0.3"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -3801,6 +3821,12 @@
"node": ">=0.10.0"
}
},
"node_modules/sponge-case": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-2.0.3.tgz",
"integrity": "sha512-i4h9ZGRfxV6Xw3mpZSFOfbXjf0cQcYmssGWutgNIfFZ2VM+YIWfD71N/kjjwK6X/AAHzBr+rciEcn/L34S8TGw==",
"license": "MIT"
},
"node_modules/stdin-discarder": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
@@ -3967,6 +3993,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swap-case": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/swap-case/-/swap-case-3.0.3.tgz",
"integrity": "sha512-6p4op8wE9CQv7uDFzulI6YXUw4lD9n4oQierdbFThEKVWVQcbQcUjdP27W8XE7V4QnWmnq9jueSHceyyQnqQVA==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
@@ -4092,6 +4124,15 @@
"node": "*"
}
},
"node_modules/title-case": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz",
"integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.3"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -4139,6 +4180,12 @@
"webpack": "^5.0.0"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@@ -2,7 +2,7 @@
"name": "mytemplategenerator",
"displayName": "myTemplateGenerator",
"description": "Generate files and folders from customizable templates with variable substitution in VSCode.",
"version": "0.0.4",
"version": "0.0.5",
"publisher": "MyTemplateGenerator",
"author": "Sergey Gromov",
"icon": "logo.png",
@@ -82,6 +82,8 @@
"webpack-cli": "^6.0.1"
},
"dependencies": {
"change-case": "^5.4.4",
"change-case-all": "^2.1.0",
"handlebars": "^4.7.8"
}
}

38
src/core/config.ts Normal file
View File

@@ -0,0 +1,38 @@
// Работа с конфигом расширения
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
export interface MyTemplateGeneratorConfig {
templatesPath: string;
overwriteFiles: boolean;
inputMode: 'webview' | 'inputBox';
language?: string;
}
export function getConfigPath(): string | undefined {
const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) return undefined;
return path.join(folders[0].uri.fsPath, 'mycodegenerate.json');
}
export function readConfig(): MyTemplateGeneratorConfig {
const configPath = getConfigPath();
if (configPath && fs.existsSync(configPath)) {
const raw = fs.readFileSync(configPath, 'utf8');
return JSON.parse(raw);
}
// Значения по умолчанию
return {
templatesPath: 'templates',
overwriteFiles: false,
inputMode: 'inputBox',
language: 'en',
};
}
export function writeConfig(config: MyTemplateGeneratorConfig) {
const configPath = getConfigPath();
if (!configPath) return;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
}

71
src/core/i18n.ts Normal file
View File

@@ -0,0 +1,71 @@
// Словари локализации и утилиты для i18n
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
export const I18N_DICTIONARIES: Record<string, Record<string, string>> = {
ru: {
destinationPath: 'Путь назначения',
chooseTemplate: 'Выберите шаблон',
enterVariables: 'Введите значения переменных',
varInputHint: 'без скобок {{ }}',
create: 'Создать',
selectTemplate: 'Шаблон',
fileExistsNoOverwrite: 'Файл или папка уже существует и перезапись запрещена',
fileExists: 'Файл или папка уже существует',
createSuccess: 'Структура {{template}} успешно создана.',
createError: 'Ошибка при создании структуры',
noTemplates: 'В папке шаблонов нет шаблонов.',
templatesNotFound: 'Папка шаблонов не найдена:',
noFolders: 'Нет открытых папок рабочего пространства.',
inputName: 'Введите имя для шаблона',
},
en: {
destinationPath: 'Destination path',
chooseTemplate: 'Choose template',
enterVariables: 'Enter variables',
varInputHint: 'without curly braces {{ }}',
create: 'Create',
selectTemplate: 'Template',
fileExistsNoOverwrite: 'File or folder already exists and overwrite is disabled',
fileExists: 'File or folder already exists',
createSuccess: 'Structure {{template}} created successfully.',
createError: 'Error creating structure',
noTemplates: 'No templates found in templates folder.',
templatesNotFound: 'Templates folder not found:',
noFolders: 'No workspace folders open.',
inputName: 'Enter name for template',
}
};
export const SETTINGS_I18N: Record<string, Record<string, string>> = {
ru: {
title: 'Настройки myTemplateGenerator',
templatesPath: 'Путь к шаблонам:',
overwriteFiles: 'Перезаписывать существующие файлы',
inputMode: 'Способ ввода переменных:',
inputModeWebview: 'Webview (форма)',
inputModeInputBox: 'InputBox (по одной)',
language: 'Язык интерфейса:',
save: 'Сохранить'
},
en: {
title: 'myTemplateGenerator Settings',
templatesPath: 'Templates path:',
overwriteFiles: 'Overwrite existing files',
inputMode: 'Variable input method:',
inputModeWebview: 'Webview (form)',
inputModeInputBox: 'InputBox (one by one)',
language: 'Interface language:',
save: 'Save'
}
};
export async function pickTemplate(templatesDir: string): Promise<string | undefined> {
const templates = fs.readdirSync(templatesDir).filter(f => fs.statSync(path.join(templatesDir, f)).isDirectory());
if (templates.length === 0) {
vscode.window.showWarningMessage('В папке templates нет шаблонов.');
return undefined;
}
return vscode.window.showQuickPick(templates, { placeHolder: 'Выберите шаблон' });
}

118
src/core/templateUtils.ts Normal file
View File

@@ -0,0 +1,118 @@
// Работа с шаблонами и преобразование кейсов
import * as fs from 'fs';
import * as path from 'path';
import * as Handlebars from 'handlebars';
// @ts-expect-error: Нет типов для change-case-all, но пакет работает корректно
import { camelCase, pascalCase, snakeCase, kebabCase, constantCase, upperCase, lowerCase } from 'change-case-all';
export const CASE_MODIFIERS: Record<string, (str: string) => string> = {
pascalCase,
camelCase,
snakeCase,
kebabCase,
screamingSnakeCase: constantCase,
upperCase,
lowerCase,
upperCaseAll: (s: string) => s.replace(/[-_\s]+/g, '').toUpperCase(),
lowerCaseAll: (s: string) => s.replace(/[-_\s]+/g, '').toLowerCase(),
};
export function readDirRecursive(src: string): string[] {
let results: string[] = [];
const list = fs.readdirSync(src);
list.forEach(function(file) {
const filePath = path.join(src, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
results = results.concat(readDirRecursive(filePath));
} else {
results.push(filePath);
}
});
return results;
}
export function copyTemplate(templateDir: string, targetDir: string, name: string) {
const vars = {
name,
nameUpperCase: CASE_MODIFIERS.upperCase(name),
nameLowerCase: CASE_MODIFIERS.lowerCase(name),
namePascalCase: CASE_MODIFIERS.pascalCase(name),
nameCamelCase: CASE_MODIFIERS.camelCase(name),
nameSnakeCase: CASE_MODIFIERS.snakeCase(name),
nameKebabCase: CASE_MODIFIERS.kebabCase(name),
nameScreamingSnakeCase: CASE_MODIFIERS.screamingSnakeCase(name),
nameUpperCaseAll: CASE_MODIFIERS.upperCaseAll(name),
nameLowerCaseAll: CASE_MODIFIERS.lowerCaseAll(name)
};
const files = readDirRecursive(templateDir);
for (const file of files) {
const relPath = path.relative(templateDir, file);
const relPathTmpl = Handlebars.compile(relPath);
const targetRelPath = relPathTmpl(vars);
const targetPath = path.join(targetDir, targetRelPath);
const content = fs.readFileSync(file, 'utf8');
const contentTmpl = Handlebars.compile(content);
const rendered = contentTmpl(vars);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, rendered, { flag: 'wx' });
}
}
export function getAllTemplateVariables(templateDir: string): Set<string> {
const files = readDirRecursive(templateDir);
const varRegex = /{{\s*([\w]+)(?:\.[\w]+)?\s*}}/g;
const vars = new Set<string>();
for (const file of files) {
let relPath = path.relative(templateDir, file);
let match;
while ((match = varRegex.exec(relPath)) !== null) {
vars.add(match[1]);
}
const content = fs.readFileSync(file, 'utf8');
while ((match = varRegex.exec(content)) !== null) {
vars.add(match[1]);
}
}
return vars;
}
export function applyTemplate(str: string, vars: Record<string, string>, modifiers: Record<string, (s: string) => string>): string {
return str.replace(/{{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_]+))?\s*}}/g, (_, varName, mod) => {
let value = vars[varName];
if (value === undefined) return '';
if (mod && modifiers[mod]) {
return modifiers[mod](value);
}
return value;
});
}
export function copyTemplateWithVars(templateDir: string, targetDir: string, vars: Record<string, string>, overwriteFiles: boolean = false, dict?: Record<string, string>, templateName?: string): boolean {
const files = readDirRecursive(templateDir);
const firstLevelDirs = new Set<string>();
for (const file of files) {
const relPath = path.relative(templateDir, file);
const targetRelPath = applyTemplate(relPath, vars, CASE_MODIFIERS);
const firstLevel = targetRelPath.split(path.sep)[0];
firstLevelDirs.add(firstLevel);
}
if (!overwriteFiles && dict) {
for (const dir of firstLevelDirs) {
const checkPath = path.join(targetDir, dir);
if (fs.existsSync(checkPath)) {
return false;
}
}
}
for (const file of files) {
const relPath = path.relative(templateDir, file);
const targetRelPath = applyTemplate(relPath, vars, CASE_MODIFIERS);
const targetPath = path.join(targetDir, targetRelPath);
const content = fs.readFileSync(file, 'utf8');
const rendered = applyTemplate(content, vars, CASE_MODIFIERS);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, rendered, { flag: overwriteFiles ? 'w' : 'wx' });
}
return true;
}

28
src/core/vars.ts Normal file
View File

@@ -0,0 +1,28 @@
// Работа с переменными шаблонов
import { CASE_MODIFIERS } from './templateUtils';
export function buildVarsObject(userVars: Record<string, string>): Record<string, string> {
const result: Record<string, string> = {};
for (const [base, value] of Object.entries(userVars)) {
result[base] = value;
for (const [mod, fn] of Object.entries(CASE_MODIFIERS)) {
result[`${base}.${mod}`] = fn(value);
}
}
return result;
}
import * as vscode from 'vscode';
export async function collectUserVars(baseVars: Set<string>): Promise<Record<string, string>> {
const result: Record<string, string> = {};
for (const v of baseVars) {
const input = await vscode.window.showInputBox({
prompt: `Введите значение для ${v}`,
placeHolder: `{{${v}}}`
});
if (!input) throw new Error(`Значение для ${v} не введено`);
result[v] = input;
}
return result;
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,18 +3,8 @@ import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
import {
toPascalCase,
toCamelCase,
toSnakeCase,
toKebabCase,
toScreamingSnakeCase,
toUpperCaseFirst,
toUpperCaseAll,
toLowerCaseAll,
buildVarsObject,
CASE_MODIFIERS
} from '../extension';
import { CASE_MODIFIERS } from '../core/templateUtils';
import { buildVarsObject } from '../core/vars';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
@@ -28,30 +18,6 @@ suite('Extension Test Suite', () => {
suite('Template Variable Modifiers', () => {
const input = 'my super-name';
test('toPascalCase', () => {
assert.strictEqual(toPascalCase(input), 'MySuperName');
});
test('toCamelCase', () => {
assert.strictEqual(toCamelCase(input), 'mySuperName');
});
test('toSnakeCase', () => {
assert.strictEqual(toSnakeCase(input), 'my_super_name');
});
test('toKebabCase', () => {
assert.strictEqual(toKebabCase(input), 'my-super-name');
});
test('toScreamingSnakeCase', () => {
assert.strictEqual(toScreamingSnakeCase(input), 'MY_SUPER_NAME');
});
test('toUpperCaseFirst', () => {
assert.strictEqual(toUpperCaseFirst(input), 'My super-name');
});
test('toUpperCaseAll', () => {
assert.strictEqual(toUpperCaseAll(input), 'MYSUPERNAME');
});
test('toLowerCaseAll', () => {
assert.strictEqual(toLowerCaseAll(input), 'mysupername');
});
test('CASE_MODIFIERS map covers all', () => {
for (const [mod, fn] of Object.entries(CASE_MODIFIERS)) {
assert.strictEqual(typeof fn(input), 'string');

57
src/vscode/completion.ts Normal file
View File

@@ -0,0 +1,57 @@
// Регистрация и обработка автодополнения шаблонов
import * as vscode from 'vscode';
import * as path from 'path';
import { getAllTemplateVariables, CASE_MODIFIERS } from '../core/templateUtils';
import { readConfig } from '../core/config';
import * as fs from 'fs';
function isInTemplatesDir(filePath: string, templatesDir: string): boolean {
const rel = path.relative(templatesDir, filePath);
return !rel.startsWith('..') && !path.isAbsolute(rel);
}
export function registerTemplateCompletionAndHighlight(context: vscode.ExtensionContext) {
const completionProvider = {
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
const config = readConfig();
const templatesPath = config.templatesPath || 'templates';
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) return;
const templatesDir = path.join(workspaceFolders[0].uri.fsPath, templatesPath);
if (!isInTemplatesDir(document.uri.fsPath, templatesDir)) {
return undefined;
}
const line = document.lineAt(position).text;
const textBefore = line.slice(0, position.character);
const match = /{{\s*([\w]+)?(?:\.([\w]*))?[^}]*$/.exec(textBefore);
if (!match) return undefined;
const allVars = getAllTemplateVariables(templatesDir);
const items = [];
if (match[2] !== undefined) {
for (const mod of Object.keys(CASE_MODIFIERS)) {
if (!match[2] || mod.startsWith(match[2])) {
const item = new vscode.CompletionItem(mod, vscode.CompletionItemKind.EnumMember);
item.insertText = mod;
items.push(item);
}
}
} else {
for (const v of allVars) {
if (!match[1] || v.startsWith(match[1])) {
const item = new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable);
item.insertText = v;
items.push(item);
}
}
}
return items;
}
};
context.subscriptions.push(
vscode.languages.registerCompletionItemProvider(
'*',
completionProvider,
'{', '.'
)
);
}

109
src/vscode/decorations.ts Normal file
View File

@@ -0,0 +1,109 @@
// Декорации и диагностика шаблонов
import * as vscode from 'vscode';
import * as path from 'path';
import { getAllTemplateVariables } from '../core/templateUtils';
import { readConfig } from '../core/config';
const bracketDecoration = vscode.window.createTextEditorDecorationType({
color: '#43A047', // зелёный для скобок
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
fontWeight: 'bold'
});
const variableDecoration = vscode.window.createTextEditorDecorationType({
color: '#FF9800', // оранжевый для имени переменной
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
fontWeight: 'bold'
});
const modifierDecoration = vscode.window.createTextEditorDecorationType({
color: '#00ACC1', // бирюзовый для модификатора
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
fontWeight: 'bold'
});
function isInTemplatesDir(filePath: string, templatesDir: string): boolean {
const rel = path.relative(templatesDir, filePath);
return !rel.startsWith('..') && !path.isAbsolute(rel);
}
function updateTemplateDecorations(editor: vscode.TextEditor) {
if (!editor) return;
const config = readConfig();
const templatesPath = config.templatesPath || 'templates';
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) return;
const templatesDir = path.join(workspaceFolders[0].uri.fsPath, templatesPath);
if (!isInTemplatesDir(editor.document.uri.fsPath, templatesDir)) return;
const brackets: vscode.DecorationOptions[] = [];
const variables: vscode.DecorationOptions[] = [];
const modifiers: vscode.DecorationOptions[] = [];
for (let lineNum = 0; lineNum < editor.document.lineCount; lineNum++) {
const line = editor.document.lineAt(lineNum).text;
// Ищем все {{variable.modifier}} или {{variable}}
const reg = /{{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_]+))?\s*}}/g;
let match;
while ((match = reg.exec(line)) !== null) {
const start = match.index;
const end = start + match[0].length;
// Скобки {{ и }}
brackets.push({
range: new vscode.Range(lineNum, start, lineNum, start + 2)
});
brackets.push({
range: new vscode.Range(lineNum, end - 2, lineNum, end)
});
// Имя переменной
const varStart = start + 2 + line.slice(start + 2).search(/\S/); // после {{
variables.push({
range: new vscode.Range(lineNum, varStart, lineNum, varStart + match[1].length)
});
// Модификатор (если есть)
if (match[2]) {
const modStart = varStart + match[1].length + 1; // +1 за точку
modifiers.push({
range: new vscode.Range(lineNum, modStart, lineNum, modStart + match[2].length)
});
}
}
}
editor.setDecorations(bracketDecoration, brackets);
editor.setDecorations(variableDecoration, variables);
editor.setDecorations(modifierDecoration, modifiers);
}
export function registerTemplateDecorations(context: vscode.ExtensionContext) {
function decorateActiveEditor() {
const editor = vscode.window.activeTextEditor;
if (editor) {
updateTemplateDecorations(editor);
}
}
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(editor => {
if (editor) decorateActiveEditor();
}),
vscode.workspace.onDidChangeTextDocument(event => {
const editor = vscode.window.visibleTextEditors.find(e => e.document === event.document);
if (editor) updateTemplateDecorations(editor);
})
);
// Инициализация при активации
setTimeout(() => {
vscode.window.visibleTextEditors.forEach(editor => updateTemplateDecorations(editor));
}, 300);
}
export function decorateActiveEditor() {
// Логика декорирования активного редактора
// ...
}
export function clearDiagnosticsForEditor(editor: vscode.TextEditor, templatesDir: string) {
// Очистка диагностик для редактора
// ...
}
export function clearDiagnosticsForTemplates(context: vscode.ExtensionContext) {
// Очистка диагностик для всех шаблонов
// ...
}

View File

@@ -0,0 +1,68 @@
// Семантическая подсветка шаблонов
import * as vscode from 'vscode';
import * as path from 'path';
import { getAllTemplateVariables } from '../core/templateUtils';
import { readConfig } from '../core/config';
function isInTemplatesDir(filePath: string, templatesDir: string): boolean {
const rel = path.relative(templatesDir, filePath);
return !rel.startsWith('..') && !path.isAbsolute(rel);
}
export function registerTemplateSemanticHighlight(context: vscode.ExtensionContext) {
const legend = new vscode.SemanticTokensLegend(['bracket', 'variable', 'modifier']);
const disposable = vscode.languages.registerDocumentSemanticTokensProvider(
{ pattern: '**' }, // теперь на все файлы
{
provideDocumentSemanticTokens(document: any) {
const config = readConfig();
const templatesPath = config.templatesPath || 'templates';
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) return;
const templatesDir = path.join(workspaceFolders[0].uri.fsPath, templatesPath);
console.log('[DEBUG] semantic tokens called for', document.uri.fsPath);
console.log('[DEBUG] Проверка шаблонной папки:', document.uri.fsPath, templatesDir, isInTemplatesDir(document.uri.fsPath, templatesDir));
// Проверяем, что файл в папке шаблонов
if (!isInTemplatesDir(document.uri.fsPath, templatesDir)) {
return;
}
const tokens: number[] = [];
for (let lineNum = 0; lineNum < document.lineCount; lineNum++) {
const line = document.lineAt(lineNum).text;
// Ищем все {{variable.modifier}} или {{variable}} или {{variable.}}
const reg = /({{)|(}})|{{\s*([a-zA-Z0-9_]+)(?:\.(\w*))?\s*}}/g;
let match;
while ((match = reg.exec(line)) !== null) {
if (match[1]) {
// {{
tokens.push(lineNum, match.index, 2, 0, 0); // bracket
} else if (match[2]) {
// }}
tokens.push(lineNum, match.index, 2, 0, 0); // bracket
} else if (match[3]) {
// variable (имя)
const varStart = match.index + 2 + line.slice(match.index + 2).search(/\S/); // после {{
tokens.push(lineNum, varStart, match[3].length, 1, 0); // variable
if (typeof match[4] === 'string') {
// Если есть точка, но модификатор не введён ({{name.}})
if (match[4] === '') {
// Подсвечиваем только точку как variable
const dotStart = varStart + match[3].length;
tokens.push(lineNum, dotStart, 1, 1, 0); // variable (точка)
} else if (match[4]) {
// .modifier
const modStart = varStart + match[3].length + 1; // +1 за точку
tokens.push(lineNum, modStart, match[4].length, 2, 0); // modifier
}
}
}
}
}
return new vscode.SemanticTokens(new Uint32Array(tokens));
}
},
legend
);
context.subscriptions.push(disposable);
return disposable;
}

View File

@@ -0,0 +1,139 @@
// Webview для конфигурации расширения
import * as vscode from 'vscode';
import { MyTemplateGeneratorConfig, readConfig, writeConfig } from '../core/config';
const LOCALIZATION: Record<'ru'|'en', {
title: string;
templatesPath: string;
overwriteFiles: string;
inputMode: string;
inputBox: string;
webview: string;
language: string;
save: string;
russian: string;
english: string;
}> = {
ru: {
title: 'Настройки генератора шаблонов',
templatesPath: 'Путь к шаблонам:',
overwriteFiles: 'Перезаписывать файлы',
inputMode: 'Режим ввода:',
inputBox: 'InputBox',
webview: 'Webview',
language: 'Язык:',
save: 'Сохранить',
russian: 'Русский',
english: 'English',
},
en: {
title: 'Template Generator Settings',
templatesPath: 'Templates path:',
overwriteFiles: 'Overwrite files',
inputMode: 'Input mode:',
inputBox: 'InputBox',
webview: 'Webview',
language: 'Language:',
save: 'Save',
russian: 'Russian',
english: 'English',
}
};
export async function showConfigWebview(context: vscode.ExtensionContext) {
const panel = vscode.window.createWebviewPanel(
'myTemplateGeneratorConfig',
'Настройки MyTemplateGenerator',
vscode.ViewColumn.One,
{ enableScripts: true }
);
let config = readConfig();
// Получаем URI для стилей
const stylePath = vscode.Uri.joinPath(context.extensionUri, 'src', 'webview', 'styles.css');
const styleUri = panel.webview.asWebviewUri(stylePath);
setHtml((config.language === 'en' ? 'en' : 'ru'));
function setHtml(language: 'ru'|'en') {
panel.webview.html = getHtml(language);
}
function getHtml(language: 'ru'|'en'): string {
const dict = LOCALIZATION[language] || LOCALIZATION['ru'];
return `
<!DOCTYPE html>
<html lang="${language}">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${panel.webview.cspSource}; script-src 'unsafe-inline';">
<title>${dict.title}</title>
<link rel="stylesheet" href="${styleUri}">
</head>
<body>
<div class="config-container">
<h2>${dict.title}</h2>
<form id="configForm">
<div class="form-group">
<label>${dict.templatesPath}
<input name="templatesPath" value="${config.templatesPath}" />
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="overwriteFiles" ${config.overwriteFiles ? 'checked' : ''}/>
${dict.overwriteFiles}
</label>
</div>
<div class="form-group">
<label>${dict.inputMode}
<select name="inputMode">
<option value="inputBox" ${config.inputMode === 'inputBox' ? 'selected' : ''}>${dict.inputBox}</option>
<option value="webview" ${config.inputMode === 'webview' ? 'selected' : ''}>${dict.webview}</option>
</select>
</label>
</div>
<div class="form-group">
<label>${dict.language}
<select name="language" id="languageSelect">
<option value="ru" ${language === 'ru' ? 'selected' : ''}>${dict.russian}</option>
<option value="en" ${language === 'en' ? 'selected' : ''}>${dict.english}</option>
</select>
</label>
</div>
<button type="submit" class="btn">${dict.save}</button>
</form>
</div>
<script>
const vscode = acquireVsCodeApi();
document.getElementById('configForm').onsubmit = function(e) {
e.preventDefault();
const data = Object.fromEntries(new FormData(this));
data.overwriteFiles = !!this.overwriteFiles.checked;
vscode.postMessage({ type: 'save', data });
};
document.getElementById('languageSelect').onchange = function(e) {
vscode.postMessage({ type: 'setLanguage', language: this.value });
};
</script>
</body>
</html>
`;
}
panel.webview.onDidReceiveMessage(
msg => {
if (msg.type === 'save') {
writeConfig(msg.data);
vscode.window.showInformationMessage('Настройки сохранены!');
panel.dispose();
}
if (msg.type === 'setLanguage') {
// Сохраняем язык в конфиг и перерисовываем webview
config.language = msg.language;
writeConfig(config);
setHtml(msg.language === 'en' ? 'en' : 'ru');
}
},
undefined,
context.subscriptions
);
}

124
src/webview/styles.css Normal file
View File

@@ -0,0 +1,124 @@
:root {
--bg: #f7f7fa;
--panel-bg: #fff;
--text: #222;
--label: #555;
--input-bg: #f0f0f3;
--input-border: #d0d0d7;
--input-focus: #1976d2;
--button-bg: #1976d2;
--button-text: #fff;
--button-hover: #1565c0;
--border-radius: 8px;
--shadow: 0 2px 12px rgba(0,0,0,0.07);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #181a1b;
--panel-bg: #23272e;
--text: #f3f3f3;
--label: #b0b0b0;
--input-bg: #23272e;
--input-border: #33363b;
--input-focus: #90caf9;
--button-bg: #1976d2;
--button-text: #fff;
--button-hover: #1565c0;
--border-radius: 8px;
--shadow: 0 2px 12px rgba(0,0,0,0.25);
}
}
body {
background: var(--bg);
color: var(--text);
font-family: 'Segoe UI', 'Roboto', Arial, sans-serif;
margin: 0;
min-height: 100vh;
}
.create-container, .config-container {
max-width: 420px;
margin: 48px auto;
background: var(--panel-bg);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 32px 36px 28px 36px;
display: flex;
flex-direction: column;
gap: 18px;
}
.head-wrap {
display: flex;
align-items: center;
justify-content: space-between;
}
.create-container h2, .config-container h2 {
margin: 0;
font-size: 1.5em;
font-weight: 600;
letter-spacing: 0.01em;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
label {
color: var(--label);
font-size: 1em;
font-weight: 500;
}
input, select {
background: var(--input-bg);
color: var(--text);
border: 1.5px solid var(--input-border);
border-radius: var(--border-radius);
padding: 8px 10px;
font-size: 1em;
transition: border 0.2s, box-shadow 0.2s;
outline: none;
}
input:focus, select:focus {
border-color: var(--input-focus);
box-shadow: 0 0 0 2px var(--input-focus)33;
}
button, .btn {
margin-top: 10px;
background: var(--button-bg);
color: var(--button-text);
border: none;
border-radius: var(--border-radius);
padding: 10px 15px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: background 0.2s, box-shadow 0.2s;
box-shadow: 0 1px 4px rgba(25, 118, 210, 0.08);
}
button:hover, button:focus, .btn:hover, .btn:focus {
background: var(--button-hover);
}
.destination {
margin-bottom: 16px;
color: #888;
font-size: 13px;
}
.lang-select {
display: block;
}
.var-hint {
color: #888;
font-size: 13px;
margin-bottom: 10px;
}
.template-list {
display: flex;
flex-direction: column;
gap: 8px;
}
#configForm {
display: flex;
flex-direction: column;
gap: 8px;
}

View File

@@ -0,0 +1,192 @@
// Webview для выбора шаблона и переменных
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { getAllTemplateVariables } from '../core/templateUtils';
import { I18N_DICTIONARIES } from '../core/i18n';
import { writeConfig, readConfig } from '../core/config';
export async function showTemplateAndVarsWebview(
context: vscode.ExtensionContext,
templatesDir: string,
targetPath: string,
initialLanguage: string
): Promise<{ template: string, vars: Record<string, string> } | undefined> {
let language = initialLanguage;
function getDict() {
return I18N_DICTIONARIES[language] || I18N_DICTIONARIES['ru'];
}
const templates = fs.readdirSync(templatesDir).filter(f => fs.statSync(path.join(templatesDir, f)).isDirectory());
const stylePath = vscode.Uri.joinPath(context.extensionUri, 'src', 'webview', 'styles.css');
return new Promise((resolve) => {
const panel = vscode.window.createWebviewPanel(
'templateVars',
getDict().create,
vscode.ViewColumn.Active,
{ enableScripts: true }
);
const styleUri = panel.webview.asWebviewUri(stylePath);
let currentVars: string[] = [];
let currentTemplate = templates[0] || '';
let disposed = false;
function getVarsHtml(vars: string[], values: Record<string, string> = {}) {
const dict = getDict();
if (!vars.length) return '';
return `<h3>${dict.enterVariables}</h3>
<div class="var-hint">${dict.varInputHint}</div>
<form id="varsForm">
${vars.map(v => `
<label><input name="${v}" placeholder="{{${v}}}" value="${values[v] || ''}" required /></label><br/><br/>
`).join('')}
<button type="submit" class="btn">${dict.create}</button>
</form>`;
}
function getTemplatesRadioHtml(templates: string[], selected: string) {
const dict = getDict();
return `<form id="templateForm">
<h3>${dict.chooseTemplate}:</h3>
<div class="template-list">
${templates.map(t => `
<label><input type="radio" name="templateRadio" value="${t}" ${selected === t ? 'checked' : ''}/> ${t}</label>
`).join('')}
</div>
</form>`;
}
function getLanguageSelectorHtml(selected: string) {
return `<label class="lang-select">
<select id="languageSelect">
<option value="ru" ${selected === 'ru' ? 'selected' : ''}>Русский</option>
<option value="en" ${selected === 'en' ? 'selected' : ''}>English</option>
</select>
</label>`;
}
function setHtml(templatesHtml: string, varsHtml: string) {
const dict = getDict();
panel.webview.html = `
<!DOCTYPE html>
<html lang="${language}">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${panel.webview.cspSource}; script-src 'unsafe-inline';">
<title>${dict.create}</title>
<link rel="stylesheet" href="${styleUri}">
</head>
<body>
<div class="create-container">
<div class="head-wrap">
<h2>${dict.create}</h2>
${getLanguageSelectorHtml(language)}
</div>
<div id="templatesBlock">
${templatesHtml}
</div>
<div id="varsBlock">
${varsHtml}
</div>
</div>
<script>
(function() {
const vscode = acquireVsCodeApi();
function initHandlers() {
// Template radio
const templateRadios = document.querySelectorAll('input[name="templateRadio"]');
templateRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
vscode.postMessage({ type: 'selectTemplate', template: e.target.value, language: document.getElementById('languageSelect').value });
});
});
// Vars form
const varsForm = document.getElementById('varsForm');
if (varsForm) {
varsForm.addEventListener('submit', (e) => {
e.preventDefault();
const data = {};
Array.from(varsForm.elements).forEach(el => {
if (el.name) data[el.name] = el.value;
});
vscode.postMessage({ type: 'submit', template: document.querySelector('input[name="templateRadio"]:checked')?.value || '', data, language: document.getElementById('languageSelect').value });
});
}
// Language select
const langSel = document.getElementById('languageSelect');
if (langSel) {
langSel.addEventListener('change', (e) => {
vscode.postMessage({ type: 'setLanguage', language: e.target.value, template: document.querySelector('input[name="templateRadio"]:checked')?.value || '' });
});
}
}
window.initHandlers = initHandlers;
document.addEventListener('DOMContentLoaded', initHandlers);
})();
</script>
</body>
</html>
`;
// После перерисовки HTML вызываем initHandlers
setTimeout(() => {
panel.webview.postMessage({ type: 'callInitHandlers' });
}, 0);
}
// Инициализация: сразу выбран первый шаблон и форма переменных
let initialVars: string[] = [];
if (currentTemplate) {
const templateDir = path.join(templatesDir, currentTemplate);
const allVars = getAllTemplateVariables(templateDir);
initialVars = Array.from(allVars);
currentVars = initialVars;
}
setHtml(getTemplatesRadioHtml(templates, currentTemplate), getVarsHtml(initialVars));
// Обработка сообщений
panel.webview.onDidReceiveMessage(
async message => {
if (message.type === 'selectTemplate') {
currentTemplate = message.template;
if (message.language) language = message.language;
if (!currentTemplate) {
setHtml(getTemplatesRadioHtml(templates, ''), '');
return;
}
// Получаем переменные для выбранного шаблона
const templateDir = path.join(templatesDir, currentTemplate);
const allVars = getAllTemplateVariables(templateDir);
currentVars = Array.from(allVars);
setHtml(getTemplatesRadioHtml(templates, currentTemplate), getVarsHtml(currentVars));
} else if (message.type === 'setLanguage') {
if (message.language) language = message.language;
// Сохраняем язык в конфиг
const oldConfig = readConfig();
writeConfig({ ...oldConfig, language });
currentTemplate = message.template || templates[0] || '';
// Получаем переменные для выбранного шаблона
let baseVars: string[] = [];
if (currentTemplate) {
const templateDir = path.join(templatesDir, currentTemplate);
const allVars = getAllTemplateVariables(templateDir);
baseVars = Array.from(allVars);
currentVars = baseVars;
}
setHtml(getTemplatesRadioHtml(templates, currentTemplate), getVarsHtml(currentVars));
} else if (message.type === 'changeLanguage') {
// legacy, не нужен
} else if (message.type === 'submit') {
if (message.language) language = message.language;
if (!disposed) {
disposed = true;
panel.dispose();
resolve({ template: message.template, vars: message.data });
}
} else if (message.type === 'callInitHandlers') {
// Ничего не делаем, скрипт внутри webview вызовет window.initHandlers
}
},
undefined,
context.subscriptions
);
panel.onDidDispose(() => {
if (!disposed) {
disposed = true;
resolve(undefined);
}
}, null, context.subscriptions);
});
}