Compare commits

...

10 Commits

Author SHA1 Message Date
cfbe03e06e feat: Template Forge 1.0.0
- генерация файлов и папок из шаблонов с подстановкой переменных
- каскадный поиск .templates вверх по дереву каталогов
- подсветка синтаксиса и автодополнение переменных в шаблонах
- webview и inputBox режимы ввода переменных
- локализация ru/en
- ядро генерации через @gromlab/create
- Gitea Actions CI для автопубликации
2026-04-02 19:12:35 +03:00
3da37cd591 chore: исключить handlebars из webpack сборки и добавить скрипт package-vsce 2025-10-25 17:19:30 +03:00
853b0f9a0b chore: обновить версию до 0.0.10 и изменить URL репозитория 2025-10-25 17:06:38 +03:00
S.Gromov
4ef5b4b27a refactor: github readme 2025-10-25 09:22:38 +03:00
S.Gromov
ea9c1181bb refactor: github readme 2025-10-25 09:20:32 +03:00
S.Gromov
dfcb1c5656 Update package version to 0.0.9, enhance extension description, and expand keywords for better discoverability. Revise README to reflect new features and improved localization support for multiple frameworks including React, Vue, Next.js, and Angular. 2025-07-15 13:05:52 +03:00
S.Gromov
0842268f28 Update package version to 0.0.8, add new configuration options for templates path, file overwrite behavior, input mode, and language selection. Remove old .vsix files and update README for improved usage instructions. 2025-07-15 09:22:48 +03:00
S.Gromov
bcb2c381fb Update package version to 0.0.7, add media/styles.css for styling, and adjust paths in webview files to reference new stylesheet location. Include .vscodeignore update to exclude media directory from packaging. 2025-07-15 00:15:58 +03:00
S.Gromov
784c1311f4 Update package.json to version 0.0.6, reflecting the latest changes and improvements in myTemplateGenerator. 2025-07-14 23:41:31 +03:00
S.Gromov
a98b1a0464 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. 2025-07-14 23:37:01 +03:00
26 changed files with 5249 additions and 1431 deletions

View File

@@ -0,0 +1,26 @@
name: Publish Extension
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- name: Publish to VS Code Marketplace
run: npx vsce publish -p ${{ secrets.VSCE_TOKEN }}
- name: Publish to Open VSX
run: npx ovsx publish -p ${{ secrets.OVSX_TOKEN }}

85
.github/README.md vendored Normal file
View File

@@ -0,0 +1,85 @@
# Template Forge
Расширение для VS Code. Генерирует файлы и папки из ваших шаблонов с подстановкой переменных.
Подсветка синтаксиса и автодополнение переменных прямо в файлах шаблонов.
## Как работает
1. Создайте папку `.templates` в корне проекта
2. Внутри -- каждая подпапка это шаблон
3. Используйте переменные в именах файлов и содержимом: `{{name}}` присутствует всегда, дополнительно можно использовать любое количество своих -- `{{author}}`, `{{module}}` и т.д.
4. ПКМ по папке в проводнике -- **Создать из шаблона...**
Расширение само найдёт все переменные в шаблоне и предложит заполнить каждую.
## Пример
Структура шаблона:
```
.templates/
component/
{{name.pascalCase}}/
{{name.pascalCase}}.tsx
types/
{{name.kebabCase}}.type.ts
styles/
{{name.kebabCase}}.module.css
```
Содержимое `{{name.pascalCase}}.tsx`:
```tsx
import cl from 'clsx'
import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.type'
import styles from './styles/{{name.kebabCase}}.module.css'
/**
* <Назначение компонента {{name.pascalCase}} в 1 строке>.
*
* Используется для:
* - <сценарий 1>
* - <сценарий 2>
*/
export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => {
const { children, className, ...htmlAttr } = props
return (
<div {...htmlAttr} className={cl(styles.root, className)}>
{children}
</div>
)
}
```
При вводе `user-profile` получите:
```
UserProfile/
UserProfile.tsx
types/
user-profile.type.ts
styles/
user-profile.module.css
```
## Модификаторы
| Модификатор | `user-profile` |
|---|---|
| `{{name}}` | user-profile |
| `{{name.pascalCase}}` | UserProfile |
| `{{name.camelCase}}` | userProfile |
| `{{name.snakeCase}}` | user_profile |
| `{{name.kebabCase}}` | user-profile |
| `{{name.screamingSnakeCase}}` | USER_PROFILE |
## Настройки
| Ключ | Описание | По умолчанию |
|---|---|---|
| `templateForge.templatesPath` | Путь к шаблонам | `.templates` |
| `templateForge.overwriteFiles` | Перезапись существующих файлов | `false` |
| `templateForge.inputMode` | Режим ввода: `webview` или `inputBox` | `webview` |
| `templateForge.language` | Язык интерфейса (`ru` / `en`) | `en` |

2
.gitignore vendored
View File

@@ -130,3 +130,5 @@ dist
.pnp.*
.DS_Store
*.vsix

15
.vscode/tasks.json vendored
View File

@@ -6,7 +6,20 @@
{
"type": "npm",
"script": "watch",
"problemMatcher": "$ts-webpack-watch",
"problemMatcher": {
"owner": "webpack",
"pattern": {
"regexp": "ERROR in (.+)\\((\\d+),(\\d+)\\)",
"file": 1,
"line": 2,
"column": 3
},
"background": {
"activeOnStart": true,
"beginsPattern": "webpack .+ compilat",
"endsPattern": "webpack .+ compiled"
}
},
"isBackground": true,
"presentation": {
"reveal": "never",

View File

@@ -3,6 +3,7 @@
out/**
node_modules/**
src/**
!media/**
.gitignore
.yarnrc
webpack.config.js

View File

@@ -1,6 +1,6 @@
# Change Log
All notable changes to the "mytemplategenerator" extension will be documented in this file.
All notable changes to the "templateforge" extension will be documented in this file.
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.

250
README.md
View File

@@ -1,211 +1,85 @@
[English](#english) | [Русский](#русский)
# Template Forge
---
Расширение для VS Code. Генерирует файлы и папки из ваших шаблонов с подстановкой переменных.
# English
Подсветка синтаксиса и автодополнение переменных прямо в файлах шаблонов.
## My Template Generator — Template-based structure generation for VSCode
## Как работает
![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
1. Создайте папку `.templates` в корне проекта
2. Внутри -- каждая подпапка это шаблон
3. Используйте переменные в именах файлов и содержимом: `{{name}}` присутствует всегда, дополнительно можно использовать любое количество своих -- `{{author}}`, `{{module}}` и т.д.
4. ПКМ по папке в проводнике -- **Создать из шаблона...**
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)
Структура шаблона:
### 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
```
templates/
components/
{{name}}/
index.js
style.module.css
store/
{{name}}Store.js
```
### Supported variables and modifiers
You can use variables with modifiers via dot notation:
- `{{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)
> 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.
### Example usage in template
```
components/
.templates/
component/
{{name.pascalCase}}/
index.js
{{name.camelCase}}.service.js
{{name.snakeCase}}.test.js
```
And in file contents:
```
export class {{name.pascalCase}} {}
const name = '{{name}}';
{{name.pascalCase}}.tsx
types/
{{name.kebabCase}}.type.ts
styles/
{{name.kebabCase}}.module.css
```
### Configuration
**To open the visual configurator, press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>, type `Configure myTemplateGenerator...` and select the command.**
Содержимое `{{name.pascalCase}}.tsx`:
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"
```tsx
import cl from 'clsx'
import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.type'
import styles from './styles/{{name.kebabCase}}.module.css'
/**
* <Назначение компонента {{name.pascalCase}} в 1 строке>.
*
* Используется для:
* - <сценарий 1>
* - <сценарий 2>
*/
export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => {
const { children, className, ...htmlAttr } = props
return (
<div {...htmlAttr} className={cl(styles.root, className)}>
{children}
</div>
)
}
```
- **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)
### 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.
При вводе `user-profile` получите:
### 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)
### Быстрый старт
1. В корне проекта создайте папку `templates`.
2. Внутри неё создайте подпапки для разных шаблонов (например, `components`, `store`).
3. Внутри шаблонов используйте переменные вида `{{name}}` или `{{name.pascalCase}}` в именах файлов/папок и в содержимом файлов.
4. Кликните правой кнопкой мыши на нужной папке в VSCode и выберите пункт **Создать из шаблона...**
5. В появившемся окне выберите шаблон, заполните переменные и нажмите "Создать".
### Пример структуры шаблонов
```
templates/
components/
{{name}}/
index.js
style.module.css
store/
{{name}}Store.js
UserProfile/
UserProfile.tsx
types/
user-profile.type.ts
styles/
user-profile.module.css
```
### Переменные и модификаторы
## Модификаторы
В шаблонах можно использовать переменные с модификаторами через точку:
| Модификатор | `user-profile` |
|---|---|
| `{{name}}` | user-profile |
| `{{name.pascalCase}}` | UserProfile |
| `{{name.camelCase}}` | userProfile |
| `{{name.snakeCase}}` | user_profile |
| `{{name.kebabCase}}` | user-profile |
| `{{name.screamingSnakeCase}}` | USER_PROFILE |
- `{{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}}` — все буквы маленькие (без разделителей)
## Настройки
> При поиске переменных для формы учитывается только имя до точки. Например, `{{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...` и выберите команду.**
Для гибкой настройки используйте файл `mytemplategenerator.json` в корне проекта или визуальный конфигуратор (команда **Настроить myTemplateGenerator...**):
```json
{
"templatesPath": "templates",
"overwriteFiles": false,
"inputMode": "webview", // или "inputBox"
"language": "ru" // или "en"
}
```
- **templatesPath** — путь к папке с шаблонами
- **overwriteFiles** — разрешать ли перезапись существующих файлов/папок
- **inputMode** — способ ввода переменных: "webview" (форма) или "inputBox" (по одной)
- **language** — язык интерфейса плагина (ru/en)
### Локализация
- Все сообщения, Webview, ошибки и пункты меню локализованы.
- Язык Webview и сообщений выбирается в конфигураторе.
- Язык пунктов меню и команд зависит от языка интерфейса VSCode.
### Важные команды
- **Создать из шаблона...** — генерация структуры (контекстное меню)
- **Настроить myTemplateGenerator...** — открыть визуальный конфигуратор (палитра команд)
### Обработка ошибок и перезаписи
- Если структура или файл уже существуют и перезапись запрещена, генерация не выполняется и выводится понятное уведомление.
- Если при создании любого файла возникает ошибка — генерация полностью прекращается, и пользователь видит причину.
---
| Ключ | Описание | По умолчанию |
|---|---|---|
| `templateForge.templatesPath` | Путь к шаблонам | `.templates` |
| `templateForge.overwriteFiles` | Перезапись существующих файлов | `false` |
| `templateForge.inputMode` | Режим ввода: `webview` или `inputBox` | `webview` |
| `templateForge.language` | Язык интерфейса (`ru` / `en`) | `en` |

124
media/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;
}

Binary file not shown.

Binary file not shown.

3994
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,113 @@
{
"name": "mytemplategenerator",
"displayName": "myTemplateGenerator",
"description": "Generate files and folders from customizable templates with variable substitution in VSCode.",
"version": "0.0.4",
"publisher": "MyTemplateGenerator",
"name": "templateforge",
"displayName": "Template Forge",
"description": "Generate files and folders from templates with variable substitution and case modifiers. Syntax highlighting and autocomplete for template variables.",
"version": "1.0.0",
"publisher": "TemplateForge",
"author": "Sergey Gromov",
"scripts": {
"build-app": "npm run build && npm run package-vsce",
"compile": "webpack",
"watch": "webpack --watch",
"build": "webpack --mode production --devtool hidden-source-map",
"package-vsce": "vsce package",
"compile-tests": "tsc -p . --outDir out",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"lint": "eslint src",
"test": "vscode-test"
},
"categories": [
"Snippets",
"Other"
],
"keywords": [
"template",
"scaffold",
"generator",
"boilerplate",
"component",
"file generator",
"project structure",
"code generation",
"snippets"
],
"icon": "logo.png",
"url": "https://github.com/gormov1122/MyTemplateGenerator",
"homepage": "https://gromlab.ru/gromov/templateforge",
"repository": {
"type": "git",
"url": "git@gromlab.ru:gromov/templateforge.git"
},
"bugs": {
"url": "https://gromlab.ru/gromov/templateforge/issues"
},
"engines": {
"vscode": "^1.60.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onStartupFinished",
"onLanguage:javascript",
"onLanguage:typescript",
"onLanguage:css",
"onLanguage:plaintext",
"onFileSystem: file"
"onStartupFinished"
],
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "mytemplategenerator.createFromTemplate",
"title": "%mytemplategenerator.createFromTemplate.title%"
"command": "templateforge.createFromTemplate",
"title": "%templateforge.createFromTemplate.title%"
},
{
"command": "mytemplategenerator.configure",
"title": "%mytemplategenerator.configure.title%"
"command": "templateforge.configure",
"title": "%templateforge.configure.title%"
}
],
"menus": {
"explorer/context": [
{
"command": "mytemplategenerator.createFromTemplate",
"command": "templateforge.createFromTemplate",
"group": "navigation@10"
}
]
},
"configuration": {
"title": "Template Forge",
"properties": {
"templateForge.templatesPath": {
"type": "string",
"default": ".templates",
"description": "%templateforge.config.templatesPath.description%",
"markdownDescription": "%templateforge.config.templatesPath.description%",
"scope": "application"
},
"templateForge.overwriteFiles": {
"type": "boolean",
"default": false,
"description": "%templateforge.config.overwriteFiles.description%",
"markdownDescription": "%templateforge.config.overwriteFiles.description%",
"scope": "application"
},
"templateForge.inputMode": {
"type": "string",
"enum": [
"webview",
"inputBox"
],
"default": "webview",
"description": "%templateforge.config.inputMode.description%",
"markdownDescription": "%templateforge.config.inputMode.description%",
"scope": "application"
},
"templateForge.language": {
"type": "string",
"enum": [
"ru",
"en"
],
"default": "en",
"description": "%templateforge.config.language.description%",
"markdownDescription": "%templateforge.config.language.description%",
"scope": "application"
}
}
},
"semanticTokenColors": [
{
"token": "bracket",
@@ -56,17 +123,6 @@
}
]
},
"scripts": {
"vscode:prepublish": "npm run package",
"compile": "webpack",
"watch": "webpack --watch",
"package": "webpack --mode production --devtool hidden-source-map",
"compile-tests": "tsc -p . --outDir out",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"lint": "eslint src",
"test": "vscode-test"
},
"devDependencies": {
"@types/mocha": "^10.0.10",
"@types/node": "20.x",
@@ -75,6 +131,7 @@
"@typescript-eslint/parser": "^8.31.1",
"@vscode/test-cli": "^0.0.11",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.6.2",
"eslint": "^9.25.1",
"ts-loader": "^9.5.2",
"typescript": "^5.8.3",
@@ -82,6 +139,6 @@
"webpack-cli": "^6.0.1"
},
"dependencies": {
"handlebars": "^4.7.8"
"@gromlab/create": "^0.2.0"
}
}

View File

@@ -1,4 +1,8 @@
{
"mytemplategenerator.createFromTemplate.title": "Create from template...",
"mytemplategenerator.configure.title": "Configure myTemplateGenerator..."
"templateforge.createFromTemplate.title": "Create from template...",
"templateforge.configure.title": "Configure Template Forge...",
"templateforge.config.templatesPath.description": "Path to the templates folder (relative to the project root)",
"templateforge.config.overwriteFiles.description": "Overwrite existing files when generating from template",
"templateforge.config.inputMode.description": "Variable input mode: webview or inputBox",
"templateforge.config.language.description": "Extension interface language"
}

View File

@@ -1,4 +1,8 @@
{
"mytemplategenerator.createFromTemplate.title": "Создать из шаблона...",
"mytemplategenerator.configure.title": "Настроить myTemplateGenerator..."
"templateforge.createFromTemplate.title": "Создать из шаблона...",
"templateforge.configure.title": "Настроить Template Forge...",
"templateforge.config.templatesPath.description": "Путь к папке с шаблонами (относительно корня проекта)",
"templateforge.config.overwriteFiles.description": "Перезаписывать ли существующие файлы при генерации",
"templateforge.config.inputMode.description": "Режим ввода переменных: webview или inputBox",
"templateforge.config.language.description": "Язык интерфейса расширения"
}

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

@@ -0,0 +1,37 @@
// Работа с конфигом расширения
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
export interface TemplateForgeConfig {
templatesPath: string;
overwriteFiles: boolean;
inputMode: 'webview' | 'inputBox';
language?: string;
}
export function readConfig(): TemplateForgeConfig {
const config = vscode.workspace.getConfiguration('templateForge');
return {
templatesPath: config.get<string>('templatesPath', '.templates'),
overwriteFiles: config.get<boolean>('overwriteFiles', false),
inputMode: config.get<'webview' | 'inputBox'>('inputMode', 'webview'),
language: config.get<string>('language', 'en'),
};
}
export async function writeConfig(newConfig: Partial<TemplateForgeConfig>) {
const config = vscode.workspace.getConfiguration('templateForge');
if (newConfig.templatesPath !== undefined) {
await config.update('templatesPath', newConfig.templatesPath, vscode.ConfigurationTarget.Global);
}
if (newConfig.overwriteFiles !== undefined) {
await config.update('overwriteFiles', newConfig.overwriteFiles, vscode.ConfigurationTarget.Global);
}
if (newConfig.inputMode !== undefined) {
await config.update('inputMode', newConfig.inputMode, vscode.ConfigurationTarget.Global);
}
if (newConfig.language !== undefined) {
await config.update('language', newConfig.language, vscode.ConfigurationTarget.Global);
}
}

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

@@ -0,0 +1,61 @@
// Словари локализации
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: 'Настройки Template Forge',
templatesPath: 'Путь к шаблонам:',
overwriteFiles: 'Перезаписывать существующие файлы',
inputMode: 'Способ ввода переменных:',
inputModeWebview: 'Webview (форма)',
inputModeInputBox: 'InputBox (по одной)',
language: 'Язык интерфейса:',
save: 'Сохранить'
},
en: {
title: 'Template Forge 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'
}
};

View File

@@ -0,0 +1,66 @@
// Каскадный поиск .templates по дереву каталогов
import * as fs from 'fs';
import * as path from 'path';
import { listTemplateNames } from '@gromlab/create';
/**
* Собирает все доступные шаблоны по каскаду вверх от startDir до rootDir.
* Ближайшие шаблоны имеют приоритет (shadowing).
* Возвращает Map: имя шаблона → абсолютный путь к его директории.
*/
export function discoverTemplates(
startDir: string,
rootDir: string,
templatesFolder: string = '.templates'
): Map<string, string> {
const result = new Map<string, string>();
const resolvedRoot = path.resolve(rootDir);
let current = path.resolve(startDir);
while (true) {
const templatesDir = path.join(current, templatesFolder);
if (fs.existsSync(templatesDir) && fs.statSync(templatesDir).isDirectory()) {
const names = listTemplateNames(templatesDir);
for (const name of names) {
if (!result.has(name)) {
result.set(name, path.join(templatesDir, name));
}
}
}
if (current === resolvedRoot) break;
const parent = path.dirname(current);
if (parent === current) break;
current = parent;
}
return result;
}
/**
* Ищет конкретный шаблон по каскаду вверх от startDir до rootDir.
* Возвращает абсолютный путь к первому .templates, содержащему шаблон, или undefined.
*/
export function resolveTemplate(
templateName: string,
startDir: string,
rootDir: string,
templatesFolder: string = '.templates'
): string | undefined {
const resolvedRoot = path.resolve(rootDir);
let current = path.resolve(startDir);
while (true) {
const candidate = path.join(current, templatesFolder, templateName);
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
return candidate;
}
if (current === resolvedRoot) break;
const parent = path.dirname(current);
if (parent === current) break;
current = parent;
}
return undefined;
}

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

@@ -0,0 +1,15 @@
// Сбор переменных от пользователя через InputBox
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

@@ -1,20 +1,6 @@
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 '@gromlab/create';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
@@ -28,46 +14,9 @@ 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');
}
});
test('buildVarsObject generates all keys', () => {
const vars = buildVarsObject({ name: input });
assert.strictEqual(vars['name'], input);
assert.strictEqual(vars['name.pascalCase'], 'MySuperName');
assert.strictEqual(vars['name.camelCase'], 'mySuperName');
assert.strictEqual(vars['name.snakeCase'], 'my_super_name');
assert.strictEqual(vars['name.kebabCase'], 'my-super-name');
assert.strictEqual(vars['name.screamingSnakeCase'], 'MY_SUPER_NAME');
assert.strictEqual(vars['name.upperCase'], 'My super-name');
assert.strictEqual(vars['name.lowerCase'], 'my super-name');
assert.strictEqual(vars['name.upperCaseAll'], 'MYSUPERNAME');
assert.strictEqual(vars['name.lowerCaseAll'], 'mysupername');
});
});

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

@@ -0,0 +1,56 @@
// Регистрация и обработка автодополнения шаблонов
import * as vscode from 'vscode';
import * as path from 'path';
import { collectTemplateVariables, CASE_MODIFIERS } from '@gromlab/create';
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 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 = collectTemplateVariables(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,
'{', '.'
)
);
}

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

@@ -0,0 +1,108 @@
// Декорации и диагностика шаблонов
import * as vscode from 'vscode';
import * as path from 'path';
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,65 @@
// Семантическая подсветка шаблонов
import * as vscode from 'vscode';
import * as path from 'path';
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);
// Проверяем, что файл в папке шаблонов
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 { TemplateForgeConfig, 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 Forge 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(
'templateForgeConfig',
'Template Forge Settings',
vscode.ViewColumn.One,
{ enableScripts: true }
);
let config = readConfig();
// Стили теперь лежат в media/styles.css (папка для статики)
const stylePath = vscode.Uri.joinPath(context.extensionUri, 'media', '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(
async msg => {
if (msg.type === 'save') {
await writeConfig(msg.data);
vscode.window.setStatusBarMessage('Настройки сохранены!', 3000);
panel.dispose();
}
if (msg.type === 'setLanguage') {
// Сохраняем язык в конфиг и перерисовываем webview
config.language = msg.language;
await 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,173 @@
// Webview для выбора шаблона и переменных
import * as vscode from 'vscode';
import { collectTemplateVariables } from '@gromlab/create';
import { I18N_DICTIONARIES } from '../core/i18n';
import { writeConfig, readConfig } from '../core/config';
export async function showTemplateAndVarsWebview(
context: vscode.ExtensionContext,
templatesMap: Map<string, 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 templateNames = Array.from(templatesMap.keys());
const stylePath = vscode.Uri.joinPath(context.extensionUri, 'media', '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 = templateNames[0] || '';
let disposed = false;
function getVarsForTemplate(name: string): string[] {
const dir = templatesMap.get(name);
if (!dir) return [];
return Array.from(collectTemplateVariables(dir));
}
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() {
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 });
});
});
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 });
});
}
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>
`;
setTimeout(() => {
panel.webview.postMessage({ type: 'callInitHandlers' });
}, 0);
}
// Инициализация
currentVars = getVarsForTemplate(currentTemplate);
setHtml(getTemplatesRadioHtml(templateNames, currentTemplate), getVarsHtml(currentVars));
panel.webview.onDidReceiveMessage(
async message => {
if (message.type === 'selectTemplate') {
currentTemplate = message.template;
if (message.language) language = message.language;
if (!currentTemplate) {
setHtml(getTemplatesRadioHtml(templateNames, ''), '');
return;
}
currentVars = getVarsForTemplate(currentTemplate);
setHtml(getTemplatesRadioHtml(templateNames, currentTemplate), getVarsHtml(currentVars));
} else if (message.type === 'setLanguage') {
if (message.language) language = message.language;
const oldConfig = readConfig();
await writeConfig({ ...oldConfig, language });
currentTemplate = message.template || templateNames[0] || '';
currentVars = getVarsForTemplate(currentTemplate);
setHtml(getTemplatesRadioHtml(templateNames, currentTemplate), getVarsHtml(currentVars));
} else if (message.type === 'submit') {
if (message.language) language = message.language;
if (!disposed) {
disposed = true;
panel.dispose();
resolve({ template: message.template, vars: message.data });
}
}
},
undefined,
context.subscriptions
);
panel.onDidDispose(() => {
if (!disposed) {
disposed = true;
resolve(undefined);
}
}, null, context.subscriptions);
});
}