diff --git a/README.md b/README.md index a04cf10..db979b1 100644 --- a/README.md +++ b/README.md @@ -1,211 +1,134 @@ -[English](#english) | [Русский](#русский) +[🇬🇧 English](#mytemplategenerator) | [🇷🇺 Русский](#mytemplategenerator-русский) ---- +# MyTemplateGenerator -# English +**Generate files and folders from templates with variable substitution right from the VS Code context menu.** -## My Template Generator — Template-based structure generation for VSCode +- Syntax highlighting and autocomplete for template variables in template files (`{{name}}`, `{{name.camelCase}}`, etc.) +- Instantly create project structure from templates with variables in file/folder names and content +- Visual configurator and full localization (English/Russian) +- Flexible settings: templates folder path, variable input mode, overwrite protection ![Logo](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/1.png) -**Features:** -- Syntax highlighting and autocomplete for template variables in templates -- Generate files and folders from templates with variable substitution -- Full localization (English/Russian) for all UI, messages, and menus -- Choose variable input mode: Webview (form) or inputBox (one by one) -- Overwrite control: allow or forbid overwriting existing files/folders -- Smart conflict handling: clear notifications if structure already exists - -Context menu "Create from template..." is available by right-clicking any folder. -![Logo](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/2.png) - -Convenient UI for creation: just select a template and specify values for the variables used in the template. (The list of variables updates automatically) ![Logo](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/3.png) -User-friendly settings UI. -![Logo](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/4.png) +**How to use:** +1. Create a folder with templates (default: `templates`). +2. Use variables in templates: `{{name}}`, `{{name.pascalCase}}`, etc. +3. Right-click any folder in your project → **Create from template...** +4. Select a template, fill in variables — the structure is generated automatically. -### Quick Start -1. Create a `templates` folder in your project root. -2. Add subfolders for different templates (e.g., `components`, `store`). -3. Use variables like `{{name}}` or `{{name.pascalCase}}` in file/folder names and file contents. -4. Right-click a folder in VSCode and select **Create from template...** -5. Choose a template, fill in variables, and click "Create". - -### Example template structure +**Example template:** ``` templates/ - components/ + component/ {{name}}/ - index.js - style.module.css - store/ - {{name}}Store.js + index.tsx + {{name.camelCase}}.module.css ``` -### Supported variables and modifiers +**Available modifiers:** -You can use variables with modifiers via dot notation: +| Modifier | Example (`name = myComponent`) | +|-----------------------|-------------------------------| +| `{{name}}` | myComponent | +| `{{name.pascalCase}}` | MyComponent | +| `{{name.camelCase}}` | myComponent | +| `{{name.snakeCase}}` | my_component | +| `{{name.kebabCase}}` | my-component | +| `{{name.screamingSnakeCase}}` | MY_COMPONENT | +| `{{name.upperCase}}` | Mycomponent | +| `{{name.lowerCase}}` | mycomponent | +| `{{name.upperCaseAll}}` | MYCOMPONENT | +| `{{name.lowerCaseAll}}` | mycomponent | -- `{{name}}` — as entered by user -- `{{name.pascalCase}}` — PascalCase -- `{{name.camelCase}}` — camelCase -- `{{name.snakeCase}}` — snake_case -- `{{name.kebabCase}}` — kebab-case -- `{{name.screamingSnakeCase}}` — SCREAMING_SNAKE_CASE -- `{{name.upperCase}}` — First letter uppercase -- `{{name.lowerCase}}` — all lowercase -- `{{name.upperCaseAll}}` — ALLUPPERCASE (no separators) -- `{{name.lowerCaseAll}}` — alllowercase (no separators) +**Supported modifiers:** pascalCase, camelCase, snakeCase, kebabCase, upperCase, lowerCase, and more. -> When searching for variables for the form, only the name before the dot is considered. For example, `{{name}}` and `{{name.pascalCase}}` are the same variable. +**Framework compatibility:** -### Example usage in template -``` -components/ - {{name.pascalCase}}/ - index.js - {{name.camelCase}}.service.js - {{name.snakeCase}}.test.js -``` -And in file contents: -``` -export class {{name.pascalCase}} {} -const name = '{{name}}'; -``` +This extension works with **any framework** — you define your own templates for any structure you need! -### Configuration -**To open the visual configurator, press Ctrl+Shift+P, type `Configure myTemplateGenerator...` and select the command.** +| Framework | Components | Store/State | Pages/Routes | Services | Utils | +|--------------|:----------:|:-----------:|:------------:|:--------:|:-----:| +| React | ✅ | ✅ | ✅ | ✅ | ✅ | +| Vue | ✅ | ✅ | ✅ | ✅ | ✅ | +| Angular | ✅ | ✅ | ✅ | ✅ | ✅ | +| Svelte | ✅ | ✅ | ✅ | ✅ | ✅ | +| Next.js | ✅ | ✅ | ✅ | ✅ | ✅ | +| Nuxt | ✅ | ✅ | ✅ | ✅ | ✅ | +| Solid | ✅ | ✅ | ✅ | ✅ | ✅ | +| Vanilla JS/TS| ✅ | ✅ | ✅ | ✅ | ✅ | -Use `mytemplategenerator.json` in your project root or the visual configurator (**Configure myTemplateGenerator...**): -```json -{ - "templatesPath": "templates", - "overwriteFiles": false, - "inputMode": "webview", // or "inputBox" - "language": "en" // or "ru" -} -``` -- **templatesPath** — path to templates folder -- **overwriteFiles** — allow or forbid overwriting existing files/folders -- **inputMode** — variable input mode: "webview" (form) or "inputBox" (one by one) -- **language** — plugin UI language (en/ru) +Just create a template for your favorite stack — and generate any structure you want! 🎉 -### Localization -- All UI, messages, errors, and menus are localized. -- Webview and messages use the language from config. -- Menu/command language depends on VSCode interface language. +**Configuration:** +All settings via `mycodegenerate.json` in the project root or the visual configurator. -### Key commands -- **Create from template...** — generate structure (context menu) -- **Configure myTemplateGenerator...** — open visual configurator (command palette) - -### Error handling & overwrite -- If structure or file exists and overwrite is forbidden, generation is cancelled and a clear notification is shown. -- Any file creation error stops generation and shows the reason. - ---- - -# Русский - -## My Template Generator — Генерация структуры из шаблонов для VSCode - -![Логотип](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/1.png) -**Возможности:** -- Подсветка синтаксиса и автокомплит переменных в шаблонах -- Генерация файлов и папок по шаблонам с подстановкой переменных -- Полная локализация (русский/английский) для всего интерфейса, сообщений и меню -- Выбор способа ввода переменных: Webview (форма) или inputBox (по одной) -- Контроль перезаписи: можно запретить или разрешить перезапись существующих файлов/папок -- Умная обработка конфликтов: понятные уведомления, если структура уже существует - -Контекстное меню "Создать из шаблона.." доступно правым кликом по любой папке. -![Логотип](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/2.png) - -Удобный UI создания, нужно только выбрать шаблок и указать значения переменных используемых в шаблоне. (Список переменных обновляется автоматически) -![Логотип](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/3.png) - -Удобный UI интерфейс настроек. -![Логотип](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/4.png) +To open the settings menu, press Ctrl+P, type `Configure myTemplateGenerator...` and select the menu item. -### Быстрый старт -1. В корне проекта создайте папку `templates`. -2. Внутри неё создайте подпапки для разных шаблонов (например, `components`, `store`). -3. Внутри шаблонов используйте переменные вида `{{name}}` или `{{name.pascalCase}}` в именах файлов/папок и в содержимом файлов. -4. Кликните правой кнопкой мыши на нужной папке в VSCode и выберите пункт **Создать из шаблона...** -5. В появившемся окне выберите шаблон, заполните переменные и нажмите "Создать". +# MyTemplateGenerator (русский) -### Пример структуры шаблонов +**Генерация файлов и папок по шаблонам с автозаменой переменных прямо из контекстного меню VS Code.** + +- Подсветка и автокомплит переменных в шаблонных файлах (`{{name}}`, `{{name.camelCase}}` и др.) +- Быстрое создание структуры проекта по шаблонам с подстановкой переменных в имена файлов, папок и содержимое +- Визуальный конфигуратор и поддержка локализации (русский/английский) +- Гибкая настройка: путь к шаблонам, режим ввода переменных, запрет/разрешение перезаписи файлов + +![Logo](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/1.png) +![Logo](https://raw.githubusercontent.com/gormov1122/MyTemplateGenerator/main/src/images/3.png) + +**Как использовать:** +1. Создайте папку с шаблонами (по умолчанию `templates`). +2. Используйте переменные в шаблонах: `{{name}}`, `{{name.pascalCase}}` и т.д. +3. Кликните правой кнопкой по папке в проекте → **Создать из шаблона...** +4. Выберите шаблон, заполните переменные — структура будет создана автоматически. + +**Пример шаблона:** ``` templates/ - components/ + component/ {{name}}/ - index.js - style.module.css - store/ - {{name}}Store.js + index.tsx + {{name.camelCase}}.module.css ``` -### Переменные и модификаторы +**Доступные модификаторы:** -В шаблонах можно использовать переменные с модификаторами через точку: +| Модификатор | Пример (`name = myComponent`) | +|----------------------|-------------------------------| +| `{{name}}` | myComponent | +| `{{name.pascalCase}}`| MyComponent | +| `{{name.camelCase}}` | myComponent | +| `{{name.snakeCase}}` | my_component | +| `{{name.kebabCase}}` | my-component | +| `{{name.screamingSnakeCase}}` | MY_COMPONENT | +| `{{name.upperCase}}` | Mycomponent | +| `{{name.lowerCase}}` | mycomponent | +| `{{name.upperCaseAll}}` | MYCOMPONENT | +| `{{name.lowerCaseAll}}` | mycomponent | -- `{{name}}` — как ввёл пользователь -- `{{name.pascalCase}}` — PascalCase -- `{{name.camelCase}}` — camelCase -- `{{name.snakeCase}}` — snake_case -- `{{name.kebabCase}}` — kebab-case -- `{{name.screamingSnakeCase}}` — SCREAMING_SNAKE_CASE -- `{{name.upperCase}}` — Первая буква большая -- `{{name.lowerCase}}` — все буквы маленькие -- `{{name.upperCaseAll}}` — ВСЕ БУКВЫ БОЛЬШИЕ (без разделителей) -- `{{name.lowerCaseAll}}` — все буквы маленькие (без разделителей) +**Поддерживаемые модификаторы:** pascalCase, camelCase, snakeCase, kebabCase, upperCase, lowerCase и др. -> При поиске переменных для формы учитывается только имя до точки. Например, `{{name}}` и `{{name.pascalCase}}` — это одна переменная. +**Совместимость с фреймворками:** -### Пример использования в шаблоне -``` -components/ - {{name.pascalCase}}/ - index.js - {{name.camelCase}}.service.js - {{name.snakeCase}}.test.js -``` -Внутри файлов также можно использовать эти переменные: -``` -export class {{name.pascalCase}} {} -const name = '{{name}}'; -``` +Плагин подходит для **любых фреймворков** — вы сами задаёте шаблоны для любой структуры! -### Конфигурация -**Чтобы открыть визуальный конфигуратор, нажмите Ctrl+Shift+P, введите `Настроить myTemplateGenerator...` и выберите команду.** +| Фреймворк | Компоненты | Store/State | Страницы/Роуты | Сервисы | Утилиты | +|--------------|:----------:|:-----------:|:--------------:|:-------:|:-------:| +| React | ✅ | ✅ | ✅ | ✅ | ✅ | +| Vue | ✅ | ✅ | ✅ | ✅ | ✅ | +| Angular | ✅ | ✅ | ✅ | ✅ | ✅ | +| Svelte | ✅ | ✅ | ✅ | ✅ | ✅ | +| Next.js | ✅ | ✅ | ✅ | ✅ | ✅ | +| Nuxt | ✅ | ✅ | ✅ | ✅ | ✅ | +| Solid | ✅ | ✅ | ✅ | ✅ | ✅ | +| Vanilla JS/TS| ✅ | ✅ | ✅ | ✅ | ✅ | -Для гибкой настройки используйте файл `mytemplategenerator.json` в корне проекта или визуальный конфигуратор (команда **Настроить myTemplateGenerator...**): +Создайте шаблон под свой стек — и генерируйте любые структуры! 🎉 -```json -{ - "templatesPath": "templates", - "overwriteFiles": false, - "inputMode": "webview", // или "inputBox" - "language": "ru" // или "en" -} -``` -- **templatesPath** — путь к папке с шаблонами -- **overwriteFiles** — разрешать ли перезапись существующих файлов/папок -- **inputMode** — способ ввода переменных: "webview" (форма) или "inputBox" (по одной) -- **language** — язык интерфейса плагина (ru/en) +**Настройка:** +Всё настраивается через файл `mycodegenerate.json` в корне проекта или визуальный конфигуратор. -### Локализация -- Все сообщения, Webview, ошибки и пункты меню локализованы. -- Язык Webview и сообщений выбирается в конфигураторе. -- Язык пунктов меню и команд зависит от языка интерфейса VSCode. - -### Важные команды -- **Создать из шаблона...** — генерация структуры (контекстное меню) -- **Настроить myTemplateGenerator...** — открыть визуальный конфигуратор (палитра команд) - -### Обработка ошибок и перезаписи -- Если структура или файл уже существуют и перезапись запрещена, генерация не выполняется и выводится понятное уведомление. -- Если при создании любого файла возникает ошибка — генерация полностью прекращается, и пользователь видит причину. - ---- +Чтобы открыть меню настроек, нажмите Ctrl+P, введите `Настроить myTemplateGenerator...` (или `Configure myTemplateGenerator...` для английского интерфейса) и выберите соответствующий пункт. diff --git a/mytemplategenerator-0.0.5.vsix b/mytemplategenerator-0.0.5.vsix new file mode 100644 index 0000000..3e0ee26 Binary files /dev/null and b/mytemplategenerator-0.0.5.vsix differ diff --git a/package-lock.json b/package-lock.json index bc98fb0..668d006 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,15 @@ { "name": "mytemplategenerator", - "version": "0.0.1", + "version": "0.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mytemplategenerator", - "version": "0.0.1", + "version": "0.0.5", "dependencies": { + "change-case": "^5.4.4", + "change-case-all": "^2.1.0", "handlebars": "^4.7.8" }, "devDependencies": { @@ -1333,6 +1335,24 @@ "node": ">=8" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "license": "MIT" + }, + "node_modules/change-case-all": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/change-case-all/-/change-case-all-2.1.0.tgz", + "integrity": "sha512-v6b0WWWkZUMHVuYk82l+WROgkUm4qEN2w5hKRNWtEOYwWqUGoi8C6xH0l1RLF1EoWqDFK6MFclmN3od6ws3/uw==", + "license": "MIT", + "dependencies": { + "change-case": "^5.2.0", + "sponge-case": "^2.0.2", + "swap-case": "^3.0.2", + "title-case": "^3.0.3" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3801,6 +3821,12 @@ "node": ">=0.10.0" } }, + "node_modules/sponge-case": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-2.0.3.tgz", + "integrity": "sha512-i4h9ZGRfxV6Xw3mpZSFOfbXjf0cQcYmssGWutgNIfFZ2VM+YIWfD71N/kjjwK6X/AAHzBr+rciEcn/L34S8TGw==", + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -3967,6 +3993,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swap-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-3.0.3.tgz", + "integrity": "sha512-6p4op8wE9CQv7uDFzulI6YXUw4lD9n4oQierdbFThEKVWVQcbQcUjdP27W8XE7V4QnWmnq9jueSHceyyQnqQVA==", + "license": "MIT" + }, "node_modules/tapable": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", @@ -4092,6 +4124,15 @@ "node": "*" } }, + "node_modules/title-case": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", + "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4139,6 +4180,12 @@ "webpack": "^5.0.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 78d6639..a4212b7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mytemplategenerator", "displayName": "myTemplateGenerator", "description": "Generate files and folders from customizable templates with variable substitution in VSCode.", - "version": "0.0.4", + "version": "0.0.5", "publisher": "MyTemplateGenerator", "author": "Sergey Gromov", "icon": "logo.png", @@ -82,6 +82,8 @@ "webpack-cli": "^6.0.1" }, "dependencies": { + "change-case": "^5.4.4", + "change-case-all": "^2.1.0", "handlebars": "^4.7.8" } } diff --git a/src/core/config.ts b/src/core/config.ts new file mode 100644 index 0000000..309206f --- /dev/null +++ b/src/core/config.ts @@ -0,0 +1,38 @@ +// Работа с конфигом расширения +import * as fs from 'fs'; +import * as path from 'path'; +import * as vscode from 'vscode'; + +export interface MyTemplateGeneratorConfig { + templatesPath: string; + overwriteFiles: boolean; + inputMode: 'webview' | 'inputBox'; + language?: string; +} + +export function getConfigPath(): string | undefined { + const folders = vscode.workspace.workspaceFolders; + if (!folders || folders.length === 0) return undefined; + return path.join(folders[0].uri.fsPath, 'mycodegenerate.json'); +} + +export function readConfig(): MyTemplateGeneratorConfig { + const configPath = getConfigPath(); + if (configPath && fs.existsSync(configPath)) { + const raw = fs.readFileSync(configPath, 'utf8'); + return JSON.parse(raw); + } + // Значения по умолчанию + return { + templatesPath: 'templates', + overwriteFiles: false, + inputMode: 'inputBox', + language: 'en', + }; +} + +export function writeConfig(config: MyTemplateGeneratorConfig) { + const configPath = getConfigPath(); + if (!configPath) return; + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8'); +} \ No newline at end of file diff --git a/src/core/i18n.ts b/src/core/i18n.ts new file mode 100644 index 0000000..557ade5 --- /dev/null +++ b/src/core/i18n.ts @@ -0,0 +1,71 @@ +// Словари локализации и утилиты для i18n +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; + +export const I18N_DICTIONARIES: Record> = { + 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> = { + ru: { + title: 'Настройки myTemplateGenerator', + templatesPath: 'Путь к шаблонам:', + overwriteFiles: 'Перезаписывать существующие файлы', + inputMode: 'Способ ввода переменных:', + inputModeWebview: 'Webview (форма)', + inputModeInputBox: 'InputBox (по одной)', + language: 'Язык интерфейса:', + save: 'Сохранить' + }, + en: { + title: 'myTemplateGenerator Settings', + templatesPath: 'Templates path:', + overwriteFiles: 'Overwrite existing files', + inputMode: 'Variable input method:', + inputModeWebview: 'Webview (form)', + inputModeInputBox: 'InputBox (one by one)', + language: 'Interface language:', + save: 'Save' + } +}; + +export async function pickTemplate(templatesDir: string): Promise { + const templates = fs.readdirSync(templatesDir).filter(f => fs.statSync(path.join(templatesDir, f)).isDirectory()); + if (templates.length === 0) { + vscode.window.showWarningMessage('В папке templates нет шаблонов.'); + return undefined; + } + return vscode.window.showQuickPick(templates, { placeHolder: 'Выберите шаблон' }); +} \ No newline at end of file diff --git a/src/core/templateUtils.ts b/src/core/templateUtils.ts new file mode 100644 index 0000000..19b11ce --- /dev/null +++ b/src/core/templateUtils.ts @@ -0,0 +1,118 @@ +// Работа с шаблонами и преобразование кейсов +import * as fs from 'fs'; +import * as path from 'path'; +import * as Handlebars from 'handlebars'; +// @ts-expect-error: Нет типов для change-case-all, но пакет работает корректно +import { camelCase, pascalCase, snakeCase, kebabCase, constantCase, upperCase, lowerCase } from 'change-case-all'; + +export const CASE_MODIFIERS: Record string> = { + pascalCase, + camelCase, + snakeCase, + kebabCase, + screamingSnakeCase: constantCase, + upperCase, + lowerCase, + upperCaseAll: (s: string) => s.replace(/[-_\s]+/g, '').toUpperCase(), + lowerCaseAll: (s: string) => s.replace(/[-_\s]+/g, '').toLowerCase(), +}; + +export function readDirRecursive(src: string): string[] { + let results: string[] = []; + const list = fs.readdirSync(src); + list.forEach(function(file) { + const filePath = path.join(src, file); + const stat = fs.statSync(filePath); + if (stat && stat.isDirectory()) { + results = results.concat(readDirRecursive(filePath)); + } else { + results.push(filePath); + } + }); + return results; +} + +export function copyTemplate(templateDir: string, targetDir: string, name: string) { + const vars = { + name, + nameUpperCase: CASE_MODIFIERS.upperCase(name), + nameLowerCase: CASE_MODIFIERS.lowerCase(name), + namePascalCase: CASE_MODIFIERS.pascalCase(name), + nameCamelCase: CASE_MODIFIERS.camelCase(name), + nameSnakeCase: CASE_MODIFIERS.snakeCase(name), + nameKebabCase: CASE_MODIFIERS.kebabCase(name), + nameScreamingSnakeCase: CASE_MODIFIERS.screamingSnakeCase(name), + nameUpperCaseAll: CASE_MODIFIERS.upperCaseAll(name), + nameLowerCaseAll: CASE_MODIFIERS.lowerCaseAll(name) + }; + const files = readDirRecursive(templateDir); + for (const file of files) { + const relPath = path.relative(templateDir, file); + const relPathTmpl = Handlebars.compile(relPath); + const targetRelPath = relPathTmpl(vars); + const targetPath = path.join(targetDir, targetRelPath); + const content = fs.readFileSync(file, 'utf8'); + const contentTmpl = Handlebars.compile(content); + const rendered = contentTmpl(vars); + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.writeFileSync(targetPath, rendered, { flag: 'wx' }); + } +} + +export function getAllTemplateVariables(templateDir: string): Set { + const files = readDirRecursive(templateDir); + const varRegex = /{{\s*([\w]+)(?:\.[\w]+)?\s*}}/g; + const vars = new Set(); + for (const file of files) { + let relPath = path.relative(templateDir, file); + let match; + while ((match = varRegex.exec(relPath)) !== null) { + vars.add(match[1]); + } + const content = fs.readFileSync(file, 'utf8'); + while ((match = varRegex.exec(content)) !== null) { + vars.add(match[1]); + } + } + return vars; +} + +export function applyTemplate(str: string, vars: Record, modifiers: Record string>): string { + return str.replace(/{{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_]+))?\s*}}/g, (_, varName, mod) => { + let value = vars[varName]; + if (value === undefined) return ''; + if (mod && modifiers[mod]) { + return modifiers[mod](value); + } + return value; + }); +} + +export function copyTemplateWithVars(templateDir: string, targetDir: string, vars: Record, overwriteFiles: boolean = false, dict?: Record, templateName?: string): boolean { + const files = readDirRecursive(templateDir); + const firstLevelDirs = new Set(); + for (const file of files) { + const relPath = path.relative(templateDir, file); + const targetRelPath = applyTemplate(relPath, vars, CASE_MODIFIERS); + const firstLevel = targetRelPath.split(path.sep)[0]; + firstLevelDirs.add(firstLevel); + } + if (!overwriteFiles && dict) { + for (const dir of firstLevelDirs) { + const checkPath = path.join(targetDir, dir); + if (fs.existsSync(checkPath)) { + return false; + } + } + } + for (const file of files) { + const relPath = path.relative(templateDir, file); + const targetRelPath = applyTemplate(relPath, vars, CASE_MODIFIERS); + const targetPath = path.join(targetDir, targetRelPath); + const content = fs.readFileSync(file, 'utf8'); + const rendered = applyTemplate(content, vars, CASE_MODIFIERS); + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.writeFileSync(targetPath, rendered, { flag: overwriteFiles ? 'w' : 'wx' }); + } + return true; +} \ No newline at end of file diff --git a/src/core/vars.ts b/src/core/vars.ts new file mode 100644 index 0000000..945eabc --- /dev/null +++ b/src/core/vars.ts @@ -0,0 +1,28 @@ +// Работа с переменными шаблонов +import { CASE_MODIFIERS } from './templateUtils'; + +export function buildVarsObject(userVars: Record): Record { + const result: Record = {}; + for (const [base, value] of Object.entries(userVars)) { + result[base] = value; + for (const [mod, fn] of Object.entries(CASE_MODIFIERS)) { + result[`${base}.${mod}`] = fn(value); + } + } + return result; +} + +import * as vscode from 'vscode'; + +export async function collectUserVars(baseVars: Set): Promise> { + const result: Record = {}; + 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; +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index f8eeb17..c5fbc58 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,799 +4,15 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; import * as Handlebars from 'handlebars'; - -async function getTemplatesDir(workspaceFolders: readonly vscode.WorkspaceFolder[] | undefined): Promise { - if (!workspaceFolders) return undefined; - for (const folder of workspaceFolders) { - const candidate = path.join(folder.uri.fsPath, 'templates'); - if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) { - return candidate; - } - } - return undefined; -} - -async function pickTemplate(templatesDir: string): Promise { - const templates = fs.readdirSync(templatesDir).filter(f => fs.statSync(path.join(templatesDir, f)).isDirectory()); - if (templates.length === 0) { - vscode.window.showWarningMessage('В папке templates нет шаблонов.'); - return undefined; - } - return vscode.window.showQuickPick(templates, { placeHolder: 'Выберите шаблон' }); -} - -function readDirRecursive(src: string): string[] { - let results: string[] = []; - const list = fs.readdirSync(src); - list.forEach(function(file) { - const filePath = path.join(src, file); - const stat = fs.statSync(filePath); - if (stat && stat.isDirectory()) { - results = results.concat(readDirRecursive(filePath)); - } else { - results.push(filePath); - } - }); - return results; -} - -function toUpperCaseFirst(str: string): string { - return str.charAt(0).toUpperCase() + str.slice(1); -} - -function toPascalCase(str: string): string { - return str - .replace(/[-_ ]+/g, ' ') - .split(' ') - .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(''); -} - -function toCamelCase(str: string): string { - const pascal = toPascalCase(str); - return pascal.charAt(0).toLowerCase() + pascal.slice(1); -} - -function toSnakeCase(str: string): string { - return str - .replace(/([a-z])([A-Z])/g, '$1_$2') - .replace(/[-\s]+/g, '_') - .toLowerCase(); -} - -function toKebabCase(str: string): string { - return str - .replace(/([a-z])([A-Z])/g, '$1-$2') - .replace(/[_\s]+/g, '-') - .toLowerCase(); -} - -function toScreamingSnakeCase(str: string): string { - return toSnakeCase(str).toUpperCase(); -} - -function toUpperCaseAll(str: string): string { - return str.replace(/[-_\s]+/g, '').toUpperCase(); -} - -function toLowerCaseAll(str: string): string { - return str.replace(/[-_\s]+/g, '').toLowerCase(); -} - -function copyTemplate(templateDir: string, targetDir: string, name: string) { - const vars = { - name, // как ввёл пользователь - nameUpperCase: toUpperCaseFirst(name), - nameLowerCase: name.toLowerCase(), - namePascalCase: toPascalCase(name), - nameCamelCase: toCamelCase(name), - nameSnakeCase: toSnakeCase(name), - nameKebabCase: toKebabCase(name), - nameScreamingSnakeCase: toScreamingSnakeCase(name), - nameUpperCaseAll: toUpperCaseAll(name), - nameLowerCaseAll: toLowerCaseAll(name) - }; - const files = readDirRecursive(templateDir); - for (const file of files) { - const relPath = path.relative(templateDir, file); - const relPathTmpl = Handlebars.compile(relPath); - const targetRelPath = relPathTmpl(vars); - const targetPath = path.join(targetDir, targetRelPath); - const content = fs.readFileSync(file, 'utf8'); - const contentTmpl = Handlebars.compile(content); - const rendered = contentTmpl(vars); - fs.mkdirSync(path.dirname(targetPath), { recursive: true }); - fs.writeFileSync(targetPath, rendered, { flag: 'wx' }); - } -} - -// Новый способ поиска переменных в шаблоне -function getAllTemplateVariables(templateDir: string): Set { - const files = readDirRecursive(templateDir); - // Ищем {{ variable }} и {{ variable.suffix }} - const varRegex = /{{\s*([\w]+)(?:\.[\w]+)?\s*}}/g; - const vars = new Set(); - for (const file of files) { - // Пути - let relPath = path.relative(templateDir, file); - let match; - while ((match = varRegex.exec(relPath)) !== null) { - vars.add(match[1]); // только базовое имя - } - // Содержимое - const content = fs.readFileSync(file, 'utf8'); - while ((match = varRegex.exec(content)) !== null) { - vars.add(match[1]); - } - } - return vars; -} - -// Мапа модификаторов и функций -const CASE_MODIFIERS: Record string> = { - 'pascalCase': toPascalCase, - 'camelCase': toCamelCase, - 'snakeCase': toSnakeCase, - 'kebabCase': toKebabCase, - 'screamingSnakeCase': toScreamingSnakeCase, - 'upperCase': toUpperCaseFirst, - 'lowerCase': (s: string) => s.toLowerCase(), - 'upperCaseAll': toUpperCaseAll, - 'lowerCaseAll': toLowerCaseAll, -}; - -// Генерируем объект переменных для шаблона -function buildVarsObject(userVars: Record): Record { - const result: Record = {}; - for (const [base, value] of Object.entries(userVars)) { - result[base] = value; - for (const [mod, fn] of Object.entries(CASE_MODIFIERS)) { - result[`${base}.${mod}`] = fn(value); - } - } - return result; -} - -async function collectUserVars(baseVars: Set): Promise> { - const result: Record = {}; - 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; -} - -// --- Собственная обработка шаблонов --- -function applyTemplate(str: string, vars: Record, modifiers: Record string>): string { - return str.replace(/{{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_]+))?\s*}}/g, (_, varName, mod) => { - let value = vars[varName]; - if (value === undefined) return ''; - if (mod && modifiers[mod]) { - return modifiers[mod](value); - } - return value; - }); -} - -function copyTemplateWithVars(templateDir: string, targetDir: string, vars: Record, overwriteFiles: boolean = false, dict?: Record, templateName?: string): boolean { - const files = readDirRecursive(templateDir); - // Собираем все папки, которые будут созданы на первом уровне - const firstLevelDirs = new Set(); - for (const file of files) { - const relPath = path.relative(templateDir, file); - const targetRelPath = applyTemplate(relPath, vars, CASE_MODIFIERS); - const firstLevel = targetRelPath.split(path.sep)[0]; - firstLevelDirs.add(firstLevel); - } - // Проверяем существование этих папок/файлов - if (!overwriteFiles && dict) { - for (const dir of firstLevelDirs) { - const checkPath = path.join(targetDir, dir); - if (fs.existsSync(checkPath)) { - vscode.window.showErrorMessage(`${dict.fileExistsNoOverwrite}: ${checkPath}`); - return false; - } - } - } - let createdCount = 0; - for (const file of files) { - const relPath = path.relative(templateDir, file); - const targetRelPath = applyTemplate(relPath, vars, CASE_MODIFIERS); - const targetPath = path.join(targetDir, targetRelPath); - try { - if (!overwriteFiles && fs.existsSync(targetPath) && dict) { - const errMsg = `${dict.fileExistsNoOverwrite}: ${targetPath}`; - vscode.window.showErrorMessage(errMsg); - break; - } - const content = fs.readFileSync(file, 'utf8'); - const rendered = applyTemplate(content, vars, CASE_MODIFIERS); - fs.mkdirSync(path.dirname(targetPath), { recursive: true }); - fs.writeFileSync(targetPath, rendered, { flag: overwriteFiles ? 'w' : 'wx' }); - createdCount++; - } catch (err: any) { - if (err && err.code === 'EEXIST' && dict) { - vscode.window.showErrorMessage(`${dict.fileExists}: ${targetPath}`); - } else if (dict) { - vscode.window.showErrorMessage(`${dict.createError}: ${err?.message || err}`); - } - break; - } - } - if (createdCount > 0 && dict && templateName) { - vscode.window.showInformationMessage(dict.createSuccess.replace('{{template}}', templateName)); - } - return createdCount > 0; -} - -const I18N_DICTIONARIES: Record> = { - 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', - } -}; - -const SETTINGS_I18N: Record> = { - ru: { - title: 'Настройки myTemplateGenerator', - templatesPath: 'Путь к шаблонам:', - overwriteFiles: 'Перезаписывать существующие файлы', - inputMode: 'Способ ввода переменных:', - inputModeWebview: 'Webview (форма)', - inputModeInputBox: 'InputBox (по одной)', - language: 'Язык интерфейса:', - save: 'Сохранить' - }, - en: { - title: 'myTemplateGenerator Settings', - templatesPath: 'Templates path:', - overwriteFiles: 'Overwrite existing files', - inputMode: 'Variable input method:', - inputModeWebview: 'Webview (form)', - inputModeInputBox: 'InputBox (one by one)', - language: 'Interface language:', - save: 'Save' - } -}; - -interface MyTemplateGeneratorConfig { - templatesPath: string; - overwriteFiles: boolean; - inputMode: 'webview' | 'inputBox'; - language?: string; -} - -const DEFAULT_CONFIG: MyTemplateGeneratorConfig = { - templatesPath: 'templates', - overwriteFiles: false, - inputMode: 'webview', - language: 'en', -}; - -function getConfigPath(): string | undefined { - const folders = vscode.workspace.workspaceFolders; - if (!folders || folders.length === 0) return undefined; - return path.join(folders[0].uri.fsPath, 'mytemplategenerator.json'); -} - -function readConfig(): MyTemplateGeneratorConfig { - const configPath = getConfigPath(); - if (configPath && fs.existsSync(configPath)) { - try { - const raw = fs.readFileSync(configPath, 'utf8'); - return { ...DEFAULT_CONFIG, ...JSON.parse(raw) }; - } catch (e) { - vscode.window.showErrorMessage('Ошибка чтения mytemplategenerator.json, используются значения по умолчанию'); - } - } - // Можно добавить чтение из settings.json, если нужно - return DEFAULT_CONFIG; -} - -function writeConfig(config: MyTemplateGeneratorConfig) { - const configPath = getConfigPath(); - if (!configPath) { - vscode.window.showErrorMessage('Не удалось определить путь к mytemplategenerator.json'); - return; - } - fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8'); -} - -async function showConfigWebview(context: vscode.ExtensionContext) { - const config = readConfig(); - let language = config.language || 'ru'; - return new Promise((resolve) => { - const panel = vscode.window.createWebviewPanel( - 'mytemplategeneratorConfig', - SETTINGS_I18N[language]?.title || SETTINGS_I18N['ru'].title, - vscode.ViewColumn.Active, - { enableScripts: true } - ); - function setHtml() { - const dict = SETTINGS_I18N[language] || SETTINGS_I18N['ru']; - panel.webview.html = ` - - - - - - ${dict.title} - - - - -
-

${dict.title}

-
-
- - -
-
- - -
-
- - -
-
- - -
- -
-
- - - `; - } - setHtml(); - panel.webview.onDidReceiveMessage( - message => { - if (message.type === 'save') { - writeConfig(message.data); - vscode.window.showInformationMessage('Настройки myTemplateGenerator сохранены!'); - panel.dispose(); - resolve(); - } else if (message.type === 'changeLanguage') { - language = message.language; - setHtml(); - } - }, - undefined, - context.subscriptions - ); - panel.onDidDispose(() => resolve(), null, context.subscriptions); - }); -} - -async function showTemplateAndVarsWebview(context: vscode.ExtensionContext, templatesDir: string, targetPath: string, initialLanguage: string): Promise<{template: string, vars: Record} | undefined> { - let language = initialLanguage; - function getDict() { - return I18N_DICTIONARIES[language] || I18N_DICTIONARIES['ru']; - } - const templates = fs.readdirSync(templatesDir).filter(f => fs.statSync(path.join(templatesDir, f)).isDirectory()); - return new Promise((resolve) => { - const panel = vscode.window.createWebviewPanel( - 'templateVars', - getDict().create, - vscode.ViewColumn.Active, - { enableScripts: true } - ); - let currentVars: string[] = []; - let currentTemplate = templates[0] || ''; - let disposed = false; - function getVarsHtml(vars: string[], values: Record = {}) { - const dict = getDict(); - if (!vars.length) return ''; - return `

${dict.enterVariables}

-
${dict.varInputHint}
-
- ${vars.map(v => ` -

- `).join('')} - -
`; - } - function getTemplatesRadioHtml(templates: string[], selected: string) { - const dict = getDict(); - return `
-

${dict.chooseTemplate}:

-
- ${templates.map(t => ` - - `).join('')} -
-
`; - } - function getLanguageSelectorHtml(selected: string) { - return ``; - } - function setHtml(templatesHtml: string, varsHtml: string) { - const dict = getDict(); - panel.webview.html = ` - - - - - - ${dict.create} - - - - -
-
-

${dict.create}

-
${getLanguageSelectorHtml(language)}
-
-
- ${templatesHtml} -
-
- ${varsHtml} -
-
- - - `; - } - // Инициализация: сразу выбран первый шаблон и форма переменных - let initialVars: string[] = []; - if (currentTemplate) { - const templateDir = path.join(templatesDir, currentTemplate); - const allVars = getAllTemplateVariables(templateDir); - initialVars = Array.from(allVars); - currentVars = initialVars; - } - setHtml(getTemplatesRadioHtml(templates, currentTemplate), getVarsHtml(initialVars)); - // Обработка сообщений - panel.webview.onDidReceiveMessage( - async message => { - if (message.type === 'selectTemplate') { - currentTemplate = message.template; - if (message.language) language = message.language; - if (!currentTemplate) { - setHtml(getTemplatesRadioHtml(templates, ''), ''); - return; - } - // Получаем переменные для выбранного шаблона - const templateDir = path.join(templatesDir, currentTemplate); - const allVars = getAllTemplateVariables(templateDir); - currentVars = Array.from(allVars); - setHtml(getTemplatesRadioHtml(templates, currentTemplate), getVarsHtml(currentVars)); - } else if (message.type === 'changeLanguage') { - if (message.language) language = message.language; - currentTemplate = message.template || templates[0] || ''; - // Получаем переменные для выбранного шаблона - let baseVars: string[] = []; - if (currentTemplate) { - const templateDir = path.join(templatesDir, currentTemplate); - const allVars = getAllTemplateVariables(templateDir); - baseVars = Array.from(allVars); - currentVars = baseVars; - } - setHtml(getTemplatesRadioHtml(templates, currentTemplate), getVarsHtml(currentVars)); - } else if (message.type === '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); - }); -} +import { getAllTemplateVariables, copyTemplateWithVars } from './core/templateUtils'; +import { buildVarsObject, collectUserVars } from './core/vars'; +import { readConfig, writeConfig } from './core/config'; +import { showConfigWebview } from './webview/configWebview'; +import { showTemplateAndVarsWebview } from './webview/templateVarsWebview'; +import { registerTemplateCompletionAndHighlight } from './vscode/completion'; +import { registerTemplateSemanticHighlight } from './vscode/semanticHighlight'; +import { registerTemplateDecorations, clearDiagnosticsForTemplates } from './vscode/decorations'; +import { I18N_DICTIONARIES, pickTemplate } from './core/i18n'; // Регистрируем кастомный helper для Handlebars Handlebars.registerHelper('getVar', function(this: Record, varName: string, modifier?: string, options?: any) { @@ -816,224 +32,8 @@ Handlebars.registerHelper('getVar', function(this: Record, varName: return ''; }); -// --- Автокомплит и подсветка для шаблонов --- -function registerTemplateCompletionAndHighlight(context: vscode.ExtensionContext) { - 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); - - // --- Автокомплит --- - const completionProvider: vscode.CompletionItemProvider = { - provideCompletionItems(document, position, token, completionContext) { - if (!isInTemplatesDir(document.uri.fsPath, templatesDir)) { - return undefined; - } - const line = document.lineAt(position).text; - const textBefore = line.slice(0, position.character); - const match = /{{\s*([\w]+)?(?:\.([\w]*))?[^}]*$/.exec(textBefore); - if (!match) return undefined; - const allVars = getAllTemplateVariables(templatesDir); - const items: vscode.CompletionItem[] = []; - 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, - '{', '.' - ) - ); - // --- Удалена старая подсветка через декоратор --- -} - -function registerTemplateSemanticHighlight(context: vscode.ExtensionContext) { - 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); - - const legend = new vscode.SemanticTokensLegend(['bracket', 'variable', 'modifier']); - context.subscriptions.push( - vscode.languages.registerDocumentSemanticTokensProvider( - { pattern: templatesDir + '/**' }, - { - provideDocumentSemanticTokens(document) { - const tokens: number[] = []; - for (let lineNum = 0; lineNum < document.lineCount; lineNum++) { - const line = 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) { - 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 (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 - ) - ); -} - // === Декораторы для шаблонных переменных === -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 updateTemplateDecorations(editor: vscode.TextEditor, templatesDir: string) { - if (!editor || !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); -} - -function registerTemplateDecorations(context: vscode.ExtensionContext) { - 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); - - function decorateActiveEditor() { - const editor = vscode.window.activeTextEditor; - if (editor) { - updateTemplateDecorations(editor, templatesDir); - } - } - - 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, templatesDir); - }) - ); - // Инициализация при активации - setTimeout(() => { - vscode.window.visibleTextEditors.forEach(editor => updateTemplateDecorations(editor, templatesDir)); - }, 300); -} - -function isInTemplatesDir(filePath: string, templatesDir: string): boolean { - const rel = path.relative(templatesDir, filePath); - return ( - !rel.startsWith('..') && - !path.isAbsolute(rel) - ); -} - -function clearDiagnosticsForEditor(editor: vscode.TextEditor, templatesDir: string) { - if (editor && isInTemplatesDir(editor.document.uri.fsPath, templatesDir)) { - vscode.languages.getDiagnostics(editor.document.uri).forEach(() => { - vscode.languages.createDiagnosticCollection().set(editor.document.uri, []); - }); - } -} - -function clearDiagnosticsForTemplates(context: vscode.ExtensionContext) { - 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); - - context.subscriptions.push( - vscode.window.onDidChangeActiveTextEditor(editor => { - if (editor) clearDiagnosticsForEditor(editor, templatesDir); - }), - vscode.workspace.onDidChangeTextDocument(event => { - const editor = vscode.window.visibleTextEditors.find(e => e.document === event.document); - if (editor) clearDiagnosticsForEditor(editor, templatesDir); - }) - ); - // Инициализация при активации - setTimeout(() => { - vscode.window.visibleTextEditors.forEach(editor => clearDiagnosticsForEditor(editor, templatesDir)); - }, 300); -} // This method is called when your extension is activated // Your extension is activated the very first time the command is executed @@ -1056,37 +56,55 @@ export function activate(context: vscode.ExtensionContext) { const config = readConfig(); const dict = I18N_DICTIONARIES[config.language || 'ru'] || I18N_DICTIONARIES['ru']; const workspaceFolders = vscode.workspace.workspaceFolders; + console.log('[DEBUG] Запуск команды createFromTemplate'); if (!workspaceFolders || workspaceFolders.length === 0) { vscode.window.showErrorMessage(dict.noFolders); + console.log('[DEBUG] Нет открытых папок рабочего пространства'); return; } const templatesDir = path.join(workspaceFolders[0].uri.fsPath, config.templatesPath); if (!fs.existsSync(templatesDir) || !fs.statSync(templatesDir).isDirectory()) { vscode.window.showErrorMessage(`${dict.templatesNotFound} ${templatesDir}`); + console.log('[DEBUG] Папка шаблонов не найдена:', templatesDir); return; } let template: string | undefined; let userVars: Record | undefined; if (config.inputMode === 'webview') { - const result = await showTemplateAndVarsWebview(context, templatesDir, uri.fsPath, config.language || 'ru'); - if (!result) return; + vscode.window.showInformationMessage('[DEBUG] Вызов webview создания шаблона...'); + console.log('[DEBUG] Вызов showTemplateAndVarsWebview', { templatesDir, uri: uri.fsPath, lang: config.language }); + const result: { template: string, vars: Record } | undefined = await showTemplateAndVarsWebview(context, templatesDir, uri.fsPath, config.language || 'ru'); + console.log('[DEBUG] Результат showTemplateAndVarsWebview:', result); + if (!result) { + vscode.window.showInformationMessage('[DEBUG] Webview был закрыт или не вернул результат'); + return; + } template = result.template; userVars = result.vars; } else { + vscode.window.showInformationMessage('[DEBUG] Вызов выбора шаблона через quickPick...'); template = await pickTemplate(templatesDir); - if (!template) return; + if (!template) { + vscode.window.showInformationMessage('[DEBUG] Шаблон не выбран'); + return; + } const templateDir = path.join(templatesDir, template); const allVars = getAllTemplateVariables(templateDir); const baseVars = Array.from(allVars); userVars = await collectUserVars(new Set(baseVars)); } - if (!template || !userVars) return; + if (!template || !userVars) { + vscode.window.showInformationMessage('[DEBUG] Не выбраны шаблон или переменные'); + return; + } const templateDir = path.join(templatesDir, template); try { const vars = buildVarsObject(userVars); + vscode.window.showInformationMessage('[DEBUG] Копирование шаблона...'); copyTemplateWithVars(templateDir, uri.fsPath, vars, config.overwriteFiles, dict, template); } catch (e: any) { vscode.window.showErrorMessage(`${dict.createError}: ${e.message}`); + console.log('[DEBUG] Ошибка при копировании шаблона:', e); } }); @@ -1098,23 +116,28 @@ export function activate(context: vscode.ExtensionContext) { }) ); registerTemplateCompletionAndHighlight(context); - registerTemplateSemanticHighlight(context); + let semanticHighlightDisposable: vscode.Disposable | undefined = registerTemplateSemanticHighlight(context); registerTemplateDecorations(context); // <--- Добавить регистрацию декораторов clearDiagnosticsForTemplates(context); // <--- Очищаем diagnostics для шаблонов + + // === Отслеживание изменений конфига === + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders && workspaceFolders.length > 0) { + const configPath = path.join(workspaceFolders[0].uri.fsPath, 'mycodegenerate.json'); + if (fs.existsSync(configPath)) { + fs.watch(configPath, { persistent: false }, (eventType) => { + if (eventType === 'change' || eventType === 'rename') { + // Перерегистрируем провайдер подсветки + if (semanticHighlightDisposable) { + semanticHighlightDisposable.dispose(); + } + semanticHighlightDisposable = registerTemplateSemanticHighlight(context); + console.log('[DEBUG] Провайдер семантической подсветки перерегистрирован после изменения конфига'); + } + }); + } + } } // This method is called when your extension is deactivated export function deactivate() {} - -export { - toPascalCase, - toCamelCase, - toSnakeCase, - toKebabCase, - toScreamingSnakeCase, - toUpperCaseFirst, - toUpperCaseAll, - toLowerCaseAll, - buildVarsObject, - CASE_MODIFIERS -}; diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 73b3132..874fca9 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -3,18 +3,8 @@ import * as assert from 'assert'; // You can import and use all API from the 'vscode' module // as well as import your extension to test it import * as vscode from 'vscode'; -import { - toPascalCase, - toCamelCase, - toSnakeCase, - toKebabCase, - toScreamingSnakeCase, - toUpperCaseFirst, - toUpperCaseAll, - toLowerCaseAll, - buildVarsObject, - CASE_MODIFIERS -} from '../extension'; +import { CASE_MODIFIERS } from '../core/templateUtils'; +import { buildVarsObject } from '../core/vars'; suite('Extension Test Suite', () => { vscode.window.showInformationMessage('Start all tests.'); @@ -28,30 +18,6 @@ suite('Extension Test Suite', () => { suite('Template Variable Modifiers', () => { const input = 'my super-name'; - test('toPascalCase', () => { - assert.strictEqual(toPascalCase(input), 'MySuperName'); - }); - test('toCamelCase', () => { - assert.strictEqual(toCamelCase(input), 'mySuperName'); - }); - test('toSnakeCase', () => { - assert.strictEqual(toSnakeCase(input), 'my_super_name'); - }); - test('toKebabCase', () => { - assert.strictEqual(toKebabCase(input), 'my-super-name'); - }); - test('toScreamingSnakeCase', () => { - assert.strictEqual(toScreamingSnakeCase(input), 'MY_SUPER_NAME'); - }); - test('toUpperCaseFirst', () => { - assert.strictEqual(toUpperCaseFirst(input), 'My super-name'); - }); - test('toUpperCaseAll', () => { - assert.strictEqual(toUpperCaseAll(input), 'MYSUPERNAME'); - }); - test('toLowerCaseAll', () => { - assert.strictEqual(toLowerCaseAll(input), 'mysupername'); - }); test('CASE_MODIFIERS map covers all', () => { for (const [mod, fn] of Object.entries(CASE_MODIFIERS)) { assert.strictEqual(typeof fn(input), 'string'); diff --git a/src/vscode/completion.ts b/src/vscode/completion.ts new file mode 100644 index 0000000..3875eed --- /dev/null +++ b/src/vscode/completion.ts @@ -0,0 +1,57 @@ +// Регистрация и обработка автодополнения шаблонов +import * as vscode from 'vscode'; +import * as path from 'path'; +import { getAllTemplateVariables, CASE_MODIFIERS } from '../core/templateUtils'; +import { readConfig } from '../core/config'; +import * as fs from 'fs'; + +function isInTemplatesDir(filePath: string, templatesDir: string): boolean { + const rel = path.relative(templatesDir, filePath); + return !rel.startsWith('..') && !path.isAbsolute(rel); +} + +export function registerTemplateCompletionAndHighlight(context: vscode.ExtensionContext) { + const completionProvider = { + provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) { + const config = readConfig(); + const templatesPath = config.templatesPath || 'templates'; + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) return; + const templatesDir = path.join(workspaceFolders[0].uri.fsPath, templatesPath); + if (!isInTemplatesDir(document.uri.fsPath, templatesDir)) { + return undefined; + } + const line = document.lineAt(position).text; + const textBefore = line.slice(0, position.character); + const match = /{{\s*([\w]+)?(?:\.([\w]*))?[^}]*$/.exec(textBefore); + if (!match) return undefined; + const allVars = getAllTemplateVariables(templatesDir); + const items = []; + if (match[2] !== undefined) { + for (const mod of Object.keys(CASE_MODIFIERS)) { + if (!match[2] || mod.startsWith(match[2])) { + const item = new vscode.CompletionItem(mod, vscode.CompletionItemKind.EnumMember); + item.insertText = mod; + items.push(item); + } + } + } else { + for (const v of allVars) { + if (!match[1] || v.startsWith(match[1])) { + const item = new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable); + item.insertText = v; + items.push(item); + } + } + } + return items; + } + }; + context.subscriptions.push( + vscode.languages.registerCompletionItemProvider( + '*', + completionProvider, + '{', '.' + ) + ); +} \ No newline at end of file diff --git a/src/vscode/decorations.ts b/src/vscode/decorations.ts new file mode 100644 index 0000000..a60f006 --- /dev/null +++ b/src/vscode/decorations.ts @@ -0,0 +1,109 @@ +// Декорации и диагностика шаблонов +import * as vscode from 'vscode'; +import * as path from 'path'; +import { getAllTemplateVariables } from '../core/templateUtils'; +import { readConfig } from '../core/config'; + +const bracketDecoration = vscode.window.createTextEditorDecorationType({ + color: '#43A047', // зелёный для скобок + rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, + fontWeight: 'bold' +}); +const variableDecoration = vscode.window.createTextEditorDecorationType({ + color: '#FF9800', // оранжевый для имени переменной + rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, + fontWeight: 'bold' +}); +const modifierDecoration = vscode.window.createTextEditorDecorationType({ + color: '#00ACC1', // бирюзовый для модификатора + rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed, + fontWeight: 'bold' +}); + +function isInTemplatesDir(filePath: string, templatesDir: string): boolean { + const rel = path.relative(templatesDir, filePath); + return !rel.startsWith('..') && !path.isAbsolute(rel); +} + +function updateTemplateDecorations(editor: vscode.TextEditor) { + if (!editor) return; + const config = readConfig(); + const templatesPath = config.templatesPath || 'templates'; + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) return; + const templatesDir = path.join(workspaceFolders[0].uri.fsPath, templatesPath); + if (!isInTemplatesDir(editor.document.uri.fsPath, templatesDir)) return; + const brackets: vscode.DecorationOptions[] = []; + const variables: vscode.DecorationOptions[] = []; + const modifiers: vscode.DecorationOptions[] = []; + for (let lineNum = 0; lineNum < editor.document.lineCount; lineNum++) { + const line = editor.document.lineAt(lineNum).text; + // Ищем все {{variable.modifier}} или {{variable}} + const reg = /{{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_]+))?\s*}}/g; + let match; + while ((match = reg.exec(line)) !== null) { + const start = match.index; + const end = start + match[0].length; + // Скобки {{ и }} + brackets.push({ + range: new vscode.Range(lineNum, start, lineNum, start + 2) + }); + brackets.push({ + range: new vscode.Range(lineNum, end - 2, lineNum, end) + }); + // Имя переменной + const varStart = start + 2 + line.slice(start + 2).search(/\S/); // после {{ + variables.push({ + range: new vscode.Range(lineNum, varStart, lineNum, varStart + match[1].length) + }); + // Модификатор (если есть) + if (match[2]) { + const modStart = varStart + match[1].length + 1; // +1 за точку + modifiers.push({ + range: new vscode.Range(lineNum, modStart, lineNum, modStart + match[2].length) + }); + } + } + } + editor.setDecorations(bracketDecoration, brackets); + editor.setDecorations(variableDecoration, variables); + editor.setDecorations(modifierDecoration, modifiers); +} + +export function registerTemplateDecorations(context: vscode.ExtensionContext) { + function decorateActiveEditor() { + const editor = vscode.window.activeTextEditor; + if (editor) { + updateTemplateDecorations(editor); + } + } + + context.subscriptions.push( + vscode.window.onDidChangeActiveTextEditor(editor => { + if (editor) decorateActiveEditor(); + }), + vscode.workspace.onDidChangeTextDocument(event => { + const editor = vscode.window.visibleTextEditors.find(e => e.document === event.document); + if (editor) updateTemplateDecorations(editor); + }) + ); + // Инициализация при активации + setTimeout(() => { + vscode.window.visibleTextEditors.forEach(editor => updateTemplateDecorations(editor)); + }, 300); +} + +export function decorateActiveEditor() { + // Логика декорирования активного редактора + // ... +} + +export function clearDiagnosticsForEditor(editor: vscode.TextEditor, templatesDir: string) { + // Очистка диагностик для редактора + // ... +} + +export function clearDiagnosticsForTemplates(context: vscode.ExtensionContext) { + // Очистка диагностик для всех шаблонов + // ... +} \ No newline at end of file diff --git a/src/vscode/semanticHighlight.ts b/src/vscode/semanticHighlight.ts new file mode 100644 index 0000000..e1e13ad --- /dev/null +++ b/src/vscode/semanticHighlight.ts @@ -0,0 +1,68 @@ +// Семантическая подсветка шаблонов +import * as vscode from 'vscode'; +import * as path from 'path'; +import { getAllTemplateVariables } from '../core/templateUtils'; +import { readConfig } from '../core/config'; + +function isInTemplatesDir(filePath: string, templatesDir: string): boolean { + const rel = path.relative(templatesDir, filePath); + return !rel.startsWith('..') && !path.isAbsolute(rel); +} + +export function registerTemplateSemanticHighlight(context: vscode.ExtensionContext) { + const legend = new vscode.SemanticTokensLegend(['bracket', 'variable', 'modifier']); + const disposable = vscode.languages.registerDocumentSemanticTokensProvider( + { pattern: '**' }, // теперь на все файлы + { + provideDocumentSemanticTokens(document: any) { + const config = readConfig(); + const templatesPath = config.templatesPath || 'templates'; + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) return; + const templatesDir = path.join(workspaceFolders[0].uri.fsPath, templatesPath); + console.log('[DEBUG] semantic tokens called for', document.uri.fsPath); + console.log('[DEBUG] Проверка шаблонной папки:', document.uri.fsPath, templatesDir, isInTemplatesDir(document.uri.fsPath, templatesDir)); + // Проверяем, что файл в папке шаблонов + if (!isInTemplatesDir(document.uri.fsPath, templatesDir)) { + return; + } + const tokens: number[] = []; + for (let lineNum = 0; lineNum < document.lineCount; lineNum++) { + const line = document.lineAt(lineNum).text; + // Ищем все {{variable.modifier}} или {{variable}} или {{variable.}} + const reg = /({{)|(}})|{{\s*([a-zA-Z0-9_]+)(?:\.(\w*))?\s*}}/g; + let match; + while ((match = reg.exec(line)) !== null) { + if (match[1]) { + // {{ + tokens.push(lineNum, match.index, 2, 0, 0); // bracket + } else if (match[2]) { + // }} + tokens.push(lineNum, match.index, 2, 0, 0); // bracket + } else if (match[3]) { + // variable (имя) + const varStart = match.index + 2 + line.slice(match.index + 2).search(/\S/); // после {{ + tokens.push(lineNum, varStart, match[3].length, 1, 0); // variable + if (typeof match[4] === 'string') { + // Если есть точка, но модификатор не введён ({{name.}}) + if (match[4] === '') { + // Подсвечиваем только точку как variable + const dotStart = varStart + match[3].length; + tokens.push(lineNum, dotStart, 1, 1, 0); // variable (точка) + } else if (match[4]) { + // .modifier + const modStart = varStart + match[3].length + 1; // +1 за точку + tokens.push(lineNum, modStart, match[4].length, 2, 0); // modifier + } + } + } + } + } + return new vscode.SemanticTokens(new Uint32Array(tokens)); + } + }, + legend + ); + context.subscriptions.push(disposable); + return disposable; +} \ No newline at end of file diff --git a/src/webview/configWebview.ts b/src/webview/configWebview.ts new file mode 100644 index 0000000..6e6facc --- /dev/null +++ b/src/webview/configWebview.ts @@ -0,0 +1,139 @@ +// Webview для конфигурации расширения +import * as vscode from 'vscode'; +import { MyTemplateGeneratorConfig, readConfig, writeConfig } from '../core/config'; + +const LOCALIZATION: Record<'ru'|'en', { + title: string; + templatesPath: string; + overwriteFiles: string; + inputMode: string; + inputBox: string; + webview: string; + language: string; + save: string; + russian: string; + english: string; +}> = { + ru: { + title: 'Настройки генератора шаблонов', + templatesPath: 'Путь к шаблонам:', + overwriteFiles: 'Перезаписывать файлы', + inputMode: 'Режим ввода:', + inputBox: 'InputBox', + webview: 'Webview', + language: 'Язык:', + save: 'Сохранить', + russian: 'Русский', + english: 'English', + }, + en: { + title: 'Template Generator Settings', + templatesPath: 'Templates path:', + overwriteFiles: 'Overwrite files', + inputMode: 'Input mode:', + inputBox: 'InputBox', + webview: 'Webview', + language: 'Language:', + save: 'Save', + russian: 'Russian', + english: 'English', + } +}; + +export async function showConfigWebview(context: vscode.ExtensionContext) { + const panel = vscode.window.createWebviewPanel( + 'myTemplateGeneratorConfig', + 'Настройки MyTemplateGenerator', + vscode.ViewColumn.One, + { enableScripts: true } + ); + let config = readConfig(); + // Получаем URI для стилей + const stylePath = vscode.Uri.joinPath(context.extensionUri, 'src', 'webview', 'styles.css'); + const styleUri = panel.webview.asWebviewUri(stylePath); + setHtml((config.language === 'en' ? 'en' : 'ru')); + + function setHtml(language: 'ru'|'en') { + panel.webview.html = getHtml(language); + } + + function getHtml(language: 'ru'|'en'): string { + const dict = LOCALIZATION[language] || LOCALIZATION['ru']; + return ` + + + + + + ${dict.title} + + + +
+

${dict.title}

+
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ + + + `; + } + + panel.webview.onDidReceiveMessage( + msg => { + if (msg.type === 'save') { + writeConfig(msg.data); + vscode.window.showInformationMessage('Настройки сохранены!'); + panel.dispose(); + } + if (msg.type === 'setLanguage') { + // Сохраняем язык в конфиг и перерисовываем webview + config.language = msg.language; + writeConfig(config); + setHtml(msg.language === 'en' ? 'en' : 'ru'); + } + }, + undefined, + context.subscriptions + ); +} \ No newline at end of file diff --git a/src/webview/styles.css b/src/webview/styles.css new file mode 100644 index 0000000..8981742 --- /dev/null +++ b/src/webview/styles.css @@ -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; +} \ No newline at end of file diff --git a/src/webview/templateVarsWebview.ts b/src/webview/templateVarsWebview.ts new file mode 100644 index 0000000..1f41152 --- /dev/null +++ b/src/webview/templateVarsWebview.ts @@ -0,0 +1,192 @@ +// Webview для выбора шаблона и переменных +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; +import { getAllTemplateVariables } from '../core/templateUtils'; +import { I18N_DICTIONARIES } from '../core/i18n'; +import { writeConfig, readConfig } from '../core/config'; + +export async function showTemplateAndVarsWebview( + context: vscode.ExtensionContext, + templatesDir: string, + targetPath: string, + initialLanguage: string +): Promise<{ template: string, vars: Record } | undefined> { + let language = initialLanguage; + function getDict() { + return I18N_DICTIONARIES[language] || I18N_DICTIONARIES['ru']; + } + const templates = fs.readdirSync(templatesDir).filter(f => fs.statSync(path.join(templatesDir, f)).isDirectory()); + const stylePath = vscode.Uri.joinPath(context.extensionUri, 'src', 'webview', 'styles.css'); + return new Promise((resolve) => { + const panel = vscode.window.createWebviewPanel( + 'templateVars', + getDict().create, + vscode.ViewColumn.Active, + { enableScripts: true } + ); + const styleUri = panel.webview.asWebviewUri(stylePath); + let currentVars: string[] = []; + let currentTemplate = templates[0] || ''; + let disposed = false; + function getVarsHtml(vars: string[], values: Record = {}) { + const dict = getDict(); + if (!vars.length) return ''; + return `

${dict.enterVariables}

+
${dict.varInputHint}
+
+ ${vars.map(v => ` +

+ `).join('')} + +
`; + } + function getTemplatesRadioHtml(templates: string[], selected: string) { + const dict = getDict(); + return `
+

${dict.chooseTemplate}:

+
+ ${templates.map(t => ` + + `).join('')} +
+
`; + } + function getLanguageSelectorHtml(selected: string) { + return ``; + } + function setHtml(templatesHtml: string, varsHtml: string) { + const dict = getDict(); + panel.webview.html = ` + + + + + + ${dict.create} + + + +
+
+

${dict.create}

+ ${getLanguageSelectorHtml(language)} +
+
+ ${templatesHtml} +
+
+ ${varsHtml} +
+
+ + + + `; + // После перерисовки HTML вызываем initHandlers + setTimeout(() => { + panel.webview.postMessage({ type: 'callInitHandlers' }); + }, 0); + } + // Инициализация: сразу выбран первый шаблон и форма переменных + let initialVars: string[] = []; + if (currentTemplate) { + const templateDir = path.join(templatesDir, currentTemplate); + const allVars = getAllTemplateVariables(templateDir); + initialVars = Array.from(allVars); + currentVars = initialVars; + } + setHtml(getTemplatesRadioHtml(templates, currentTemplate), getVarsHtml(initialVars)); + // Обработка сообщений + panel.webview.onDidReceiveMessage( + async message => { + if (message.type === 'selectTemplate') { + currentTemplate = message.template; + if (message.language) language = message.language; + if (!currentTemplate) { + setHtml(getTemplatesRadioHtml(templates, ''), ''); + return; + } + // Получаем переменные для выбранного шаблона + const templateDir = path.join(templatesDir, currentTemplate); + const allVars = getAllTemplateVariables(templateDir); + currentVars = Array.from(allVars); + setHtml(getTemplatesRadioHtml(templates, currentTemplate), getVarsHtml(currentVars)); + } else if (message.type === 'setLanguage') { + if (message.language) language = message.language; + // Сохраняем язык в конфиг + const oldConfig = readConfig(); + writeConfig({ ...oldConfig, language }); + currentTemplate = message.template || templates[0] || ''; + // Получаем переменные для выбранного шаблона + let baseVars: string[] = []; + if (currentTemplate) { + const templateDir = path.join(templatesDir, currentTemplate); + const allVars = getAllTemplateVariables(templateDir); + baseVars = Array.from(allVars); + currentVars = baseVars; + } + setHtml(getTemplatesRadioHtml(templates, currentTemplate), getVarsHtml(currentVars)); + } else if (message.type === 'changeLanguage') { + // legacy, не нужен + } else if (message.type === 'submit') { + if (message.language) language = message.language; + if (!disposed) { + disposed = true; + panel.dispose(); + resolve({ template: message.template, vars: message.data }); + } + } else if (message.type === 'callInitHandlers') { + // Ничего не делаем, скрипт внутри webview вызовет window.initHandlers + } + }, + undefined, + context.subscriptions + ); + panel.onDidDispose(() => { + if (!disposed) { + disposed = true; + resolve(undefined); + } + }, null, context.subscriptions); + }); +} \ No newline at end of file