Compare commits
10 Commits
210ff6ca57
...
cfbe03e06e
| Author | SHA1 | Date | |
|---|---|---|---|
| cfbe03e06e | |||
| 3da37cd591 | |||
| 853b0f9a0b | |||
|
|
4ef5b4b27a | ||
|
|
ea9c1181bb | ||
|
|
dfcb1c5656 | ||
|
|
0842268f28 | ||
|
|
bcb2c381fb | ||
|
|
784c1311f4 | ||
|
|
a98b1a0464 |
26
.gitea/workflows/publish.yml
Normal file
26
.gitea/workflows/publish.yml
Normal 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
85
.github/README.md
vendored
Normal 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
2
.gitignore
vendored
@@ -130,3 +130,5 @@ dist
|
|||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
*.vsix
|
||||||
15
.vscode/tasks.json
vendored
15
.vscode/tasks.json
vendored
@@ -6,7 +6,20 @@
|
|||||||
{
|
{
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"script": "watch",
|
"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,
|
"isBackground": true,
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "never",
|
"reveal": "never",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
out/**
|
out/**
|
||||||
node_modules/**
|
node_modules/**
|
||||||
src/**
|
src/**
|
||||||
|
!media/**
|
||||||
.gitignore
|
.gitignore
|
||||||
.yarnrc
|
.yarnrc
|
||||||
webpack.config.js
|
webpack.config.js
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Change Log
|
# 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.
|
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||||
|
|
||||||
|
|||||||
248
README.md
248
README.md
@@ -1,211 +1,85 @@
|
|||||||
[English](#english) | [Русский](#русский)
|
# Template Forge
|
||||||
|
|
||||||
---
|
Расширение для VS Code. Генерирует файлы и папки из ваших шаблонов с подстановкой переменных.
|
||||||
|
|
||||||
# English
|
Подсветка синтаксиса и автодополнение переменных прямо в файлах шаблонов.
|
||||||
|
|
||||||
## My Template Generator — Template-based structure generation for VSCode
|
## Как работает
|
||||||
|
|
||||||

|
1. Создайте папку `.templates` в корне проекта
|
||||||
**Features:**
|
2. Внутри -- каждая подпапка это шаблон
|
||||||
- Syntax highlighting and autocomplete for template variables in templates
|
3. Используйте переменные в именах файлов и содержимом: `{{name}}` присутствует всегда, дополнительно можно использовать любое количество своих -- `{{author}}`, `{{module}}` и т.д.
|
||||||
- Generate files and folders from templates with variable substitution
|
4. ПКМ по папке в проводнике -- **Создать из шаблона...**
|
||||||
- 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.
|
Структура шаблона:
|
||||||

|
|
||||||
|
|
||||||
### 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/
|
.templates/
|
||||||
components/
|
component/
|
||||||
{{name}}/
|
{{name.pascalCase}}/
|
||||||
index.js
|
{{name.pascalCase}}.tsx
|
||||||
style.module.css
|
types/
|
||||||
store/
|
{{name.kebabCase}}.type.ts
|
||||||
{{name}}Store.js
|
styles/
|
||||||
|
{{name.kebabCase}}.module.css
|
||||||
```
|
```
|
||||||
|
|
||||||
### Supported variables and modifiers
|
Содержимое `{{name.pascalCase}}.tsx`:
|
||||||
|
|
||||||
You can use variables with modifiers via dot notation:
|
```tsx
|
||||||
|
import cl from 'clsx'
|
||||||
|
import type { {{name.pascalCase}}Props } from './types/{{name.kebabCase}}.type'
|
||||||
|
import styles from './styles/{{name.kebabCase}}.module.css'
|
||||||
|
|
||||||
- `{{name}}` — as entered by user
|
/**
|
||||||
- `{{name.pascalCase}}` — PascalCase
|
* <Назначение компонента {{name.pascalCase}} в 1 строке>.
|
||||||
- `{{name.camelCase}}` — camelCase
|
*
|
||||||
- `{{name.snakeCase}}` — snake_case
|
* Используется для:
|
||||||
- `{{name.kebabCase}}` — kebab-case
|
* - <сценарий 1>
|
||||||
- `{{name.screamingSnakeCase}}` — SCREAMING_SNAKE_CASE
|
* - <сценарий 2>
|
||||||
- `{{name.upperCase}}` — First letter uppercase
|
*/
|
||||||
- `{{name.lowerCase}}` — all lowercase
|
export const {{name.pascalCase}} = (props: {{name.pascalCase}}Props) => {
|
||||||
- `{{name.upperCaseAll}}` — ALLUPPERCASE (no separators)
|
const { children, className, ...htmlAttr } = props
|
||||||
- `{{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.
|
return (
|
||||||
|
<div {...htmlAttr} className={cl(styles.root, className)}>
|
||||||
### Example usage in template
|
{children}
|
||||||
```
|
</div>
|
||||||
components/
|
)
|
||||||
{{name.pascalCase}}/
|
|
||||||
index.js
|
|
||||||
{{name.camelCase}}.service.js
|
|
||||||
{{name.snakeCase}}.test.js
|
|
||||||
```
|
|
||||||
And in file contents:
|
|
||||||
```
|
|
||||||
export class {{name.pascalCase}} {}
|
|
||||||
const name = '{{name}}';
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
**To open the visual configurator, press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>, type `Configure myTemplateGenerator...` and select the command.**
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
### Localization
|
При вводе `user-profile` получите:
|
||||||
- All UI, messages, errors, and menus are localized.
|
|
||||||
- Webview and messages use the language from config.
|
|
||||||
- Menu/command language depends on VSCode interface language.
|
|
||||||
|
|
||||||
### 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 интерфейс настроек.
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
### Быстрый старт
|
|
||||||
1. В корне проекта создайте папку `templates`.
|
|
||||||
2. Внутри неё создайте подпапки для разных шаблонов (например, `components`, `store`).
|
|
||||||
3. Внутри шаблонов используйте переменные вида `{{name}}` или `{{name.pascalCase}}` в именах файлов/папок и в содержимом файлов.
|
|
||||||
4. Кликните правой кнопкой мыши на нужной папке в VSCode и выберите пункт **Создать из шаблона...**
|
|
||||||
5. В появившемся окне выберите шаблон, заполните переменные и нажмите "Создать".
|
|
||||||
|
|
||||||
### Пример структуры шаблонов
|
|
||||||
```
|
```
|
||||||
templates/
|
UserProfile/
|
||||||
components/
|
UserProfile.tsx
|
||||||
{{name}}/
|
types/
|
||||||
index.js
|
user-profile.type.ts
|
||||||
style.module.css
|
styles/
|
||||||
store/
|
user-profile.module.css
|
||||||
{{name}}Store.js
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Переменные и модификаторы
|
## Модификаторы
|
||||||
|
|
||||||
В шаблонах можно использовать переменные с модификаторами через точку:
|
| Модификатор | `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}}` — это одна переменная.
|
| Ключ | Описание | По умолчанию |
|
||||||
|
|---|---|---|
|
||||||
### Пример использования в шаблоне
|
| `templateForge.templatesPath` | Путь к шаблонам | `.templates` |
|
||||||
```
|
| `templateForge.overwriteFiles` | Перезапись существующих файлов | `false` |
|
||||||
components/
|
| `templateForge.inputMode` | Режим ввода: `webview` или `inputBox` | `webview` |
|
||||||
{{name.pascalCase}}/
|
| `templateForge.language` | Язык интерфейса (`ru` / `en`) | `en` |
|
||||||
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...** — открыть визуальный конфигуратор (палитра команд)
|
|
||||||
|
|
||||||
### Обработка ошибок и перезаписи
|
|
||||||
- Если структура или файл уже существуют и перезапись запрещена, генерация не выполняется и выводится понятное уведомление.
|
|
||||||
- Если при создании любого файла возникает ошибка — генерация полностью прекращается, и пользователь видит причину.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|||||||
124
media/styles.css
Normal file
124
media/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;
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
3994
package-lock.json
generated
3994
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
121
package.json
121
package.json
@@ -1,46 +1,113 @@
|
|||||||
{
|
{
|
||||||
"name": "mytemplategenerator",
|
"name": "templateforge",
|
||||||
"displayName": "myTemplateGenerator",
|
"displayName": "Template Forge",
|
||||||
"description": "Generate files and folders from customizable templates with variable substitution in VSCode.",
|
"description": "Generate files and folders from templates with variable substitution and case modifiers. Syntax highlighting and autocomplete for template variables.",
|
||||||
"version": "0.0.4",
|
"version": "1.0.0",
|
||||||
"publisher": "MyTemplateGenerator",
|
"publisher": "TemplateForge",
|
||||||
"author": "Sergey Gromov",
|
"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",
|
"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": {
|
"engines": {
|
||||||
"vscode": "^1.60.0"
|
"vscode": "^1.60.0"
|
||||||
},
|
},
|
||||||
"categories": [
|
|
||||||
"Other"
|
|
||||||
],
|
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"onStartupFinished",
|
"onStartupFinished"
|
||||||
"onLanguage:javascript",
|
|
||||||
"onLanguage:typescript",
|
|
||||||
"onLanguage:css",
|
|
||||||
"onLanguage:plaintext",
|
|
||||||
"onFileSystem: file"
|
|
||||||
],
|
],
|
||||||
"main": "./dist/extension.js",
|
"main": "./dist/extension.js",
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"command": "mytemplategenerator.createFromTemplate",
|
"command": "templateforge.createFromTemplate",
|
||||||
"title": "%mytemplategenerator.createFromTemplate.title%"
|
"title": "%templateforge.createFromTemplate.title%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "mytemplategenerator.configure",
|
"command": "templateforge.configure",
|
||||||
"title": "%mytemplategenerator.configure.title%"
|
"title": "%templateforge.configure.title%"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
"explorer/context": [
|
"explorer/context": [
|
||||||
{
|
{
|
||||||
"command": "mytemplategenerator.createFromTemplate",
|
"command": "templateforge.createFromTemplate",
|
||||||
"group": "navigation@10"
|
"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": [
|
"semanticTokenColors": [
|
||||||
{
|
{
|
||||||
"token": "bracket",
|
"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": {
|
"devDependencies": {
|
||||||
"@types/mocha": "^10.0.10",
|
"@types/mocha": "^10.0.10",
|
||||||
"@types/node": "20.x",
|
"@types/node": "20.x",
|
||||||
@@ -75,6 +131,7 @@
|
|||||||
"@typescript-eslint/parser": "^8.31.1",
|
"@typescript-eslint/parser": "^8.31.1",
|
||||||
"@vscode/test-cli": "^0.0.11",
|
"@vscode/test-cli": "^0.0.11",
|
||||||
"@vscode/test-electron": "^2.5.2",
|
"@vscode/test-electron": "^2.5.2",
|
||||||
|
"@vscode/vsce": "^3.6.2",
|
||||||
"eslint": "^9.25.1",
|
"eslint": "^9.25.1",
|
||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
@@ -82,6 +139,6 @@
|
|||||||
"webpack-cli": "^6.0.1"
|
"webpack-cli": "^6.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"handlebars": "^4.7.8"
|
"@gromlab/create": "^0.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
"mytemplategenerator.createFromTemplate.title": "Create from template...",
|
"templateforge.createFromTemplate.title": "Create from template...",
|
||||||
"mytemplategenerator.configure.title": "Configure myTemplateGenerator..."
|
"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"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
"mytemplategenerator.createFromTemplate.title": "Создать из шаблона...",
|
"templateforge.createFromTemplate.title": "Создать из шаблона...",
|
||||||
"mytemplategenerator.configure.title": "Настроить myTemplateGenerator..."
|
"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
37
src/core/config.ts
Normal 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
61
src/core/i18n.ts
Normal 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'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
66
src/core/templateResolver.ts
Normal file
66
src/core/templateResolver.ts
Normal 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
15
src/core/vars.ts
Normal 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;
|
||||||
|
}
|
||||||
1145
src/extension.ts
1145
src/extension.ts
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,6 @@
|
|||||||
import * as assert from 'assert';
|
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 * as vscode from 'vscode';
|
||||||
import {
|
import { CASE_MODIFIERS } from '@gromlab/create';
|
||||||
toPascalCase,
|
|
||||||
toCamelCase,
|
|
||||||
toSnakeCase,
|
|
||||||
toKebabCase,
|
|
||||||
toScreamingSnakeCase,
|
|
||||||
toUpperCaseFirst,
|
|
||||||
toUpperCaseAll,
|
|
||||||
toLowerCaseAll,
|
|
||||||
buildVarsObject,
|
|
||||||
CASE_MODIFIERS
|
|
||||||
} from '../extension';
|
|
||||||
|
|
||||||
suite('Extension Test Suite', () => {
|
suite('Extension Test Suite', () => {
|
||||||
vscode.window.showInformationMessage('Start all tests.');
|
vscode.window.showInformationMessage('Start all tests.');
|
||||||
@@ -28,46 +14,9 @@ suite('Extension Test Suite', () => {
|
|||||||
suite('Template Variable Modifiers', () => {
|
suite('Template Variable Modifiers', () => {
|
||||||
const input = 'my super-name';
|
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', () => {
|
test('CASE_MODIFIERS map covers all', () => {
|
||||||
for (const [mod, fn] of Object.entries(CASE_MODIFIERS)) {
|
for (const [mod, fn] of Object.entries(CASE_MODIFIERS)) {
|
||||||
assert.strictEqual(typeof fn(input), 'string');
|
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
56
src/vscode/completion.ts
Normal 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
108
src/vscode/decorations.ts
Normal 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) {
|
||||||
|
// Очистка диагностик для всех шаблонов
|
||||||
|
// ...
|
||||||
|
}
|
||||||
65
src/vscode/semanticHighlight.ts
Normal file
65
src/vscode/semanticHighlight.ts
Normal 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;
|
||||||
|
}
|
||||||
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 { 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
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;
|
||||||
|
}
|
||||||
173
src/webview/templateVarsWebview.ts
Normal file
173
src/webview/templateVarsWebview.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user