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:
271
README.md
271
README.md
@@ -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
|
||||
|
||||

|
||||
**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.
|
||||

|
||||
|
||||
Convenient UI for creation: just select a template and specify values for the variables used in the template. (The list of variables updates automatically)
|
||||

|
||||
|
||||
User-friendly settings UI.
|
||||

|
||||
**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
|
||||
|
||||

|
||||
**Возможности:**
|
||||
- Подсветка синтаксиса и автокомплит переменных в шаблонах
|
||||
- Генерация файлов и папок по шаблонам с подстановкой переменных
|
||||
- Полная локализация (русский/английский) для всего интерфейса, сообщений и меню
|
||||
- Выбор способа ввода переменных: Webview (форма) или inputBox (по одной)
|
||||
- Контроль перезаписи: можно запретить или разрешить перезапись существующих файлов/папок
|
||||
- Умная обработка конфликтов: понятные уведомления, если структура уже существует
|
||||
|
||||
Контекстное меню "Создать из шаблона.." доступно правым кликом по любой папке.
|
||||

|
||||
|
||||
Удобный UI создания, нужно только выбрать шаблок и указать значения переменных используемых в шаблоне. (Список переменных обновляется автоматически)
|
||||

|
||||
|
||||
Удобный UI интерфейс настроек.
|
||||

|
||||
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}}` и др.)
|
||||
- Быстрое создание структуры проекта по шаблонам с подстановкой переменных в имена файлов, папок и содержимое
|
||||
- Визуальный конфигуратор и поддержка локализации (русский/английский)
|
||||
- Гибкая настройка: путь к шаблонам, режим ввода переменных, запрет/разрешение перезаписи файлов
|
||||
|
||||

|
||||

|
||||
|
||||
**Как использовать:**
|
||||
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...` для английского интерфейса) и выберите соответствующий пункт.
|
||||
|
||||
BIN
mytemplategenerator-0.0.5.vsix
Normal file
BIN
mytemplategenerator-0.0.5.vsix
Normal file
Binary file not shown.
51
package-lock.json
generated
51
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
38
src/core/config.ts
Normal 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
71
src/core/i18n.ts
Normal 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
118
src/core/templateUtils.ts
Normal 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
28
src/core/vars.ts
Normal 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;
|
||||
}
|
||||
1077
src/extension.ts
1077
src/extension.ts
File diff suppressed because it is too large
Load Diff
@@ -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
57
src/vscode/completion.ts
Normal 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
109
src/vscode/decorations.ts
Normal 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) {
|
||||
// Очистка диагностик для всех шаблонов
|
||||
// ...
|
||||
}
|
||||
68
src/vscode/semanticHighlight.ts
Normal file
68
src/vscode/semanticHighlight.ts
Normal 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;
|
||||
}
|
||||
139
src/webview/configWebview.ts
Normal file
139
src/webview/configWebview.ts
Normal 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
124
src/webview/styles.css
Normal 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;
|
||||
}
|
||||
192
src/webview/templateVarsWebview.ts
Normal file
192
src/webview/templateVarsWebview.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user