From a98b1a0464848520258ec8d0690449dcfa946d4c Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Mon, 14 Jul 2025 23:37:01 +0300 Subject: [PATCH] Update myTemplateGenerator to version 0.0.5. Introduced new configuration options, added support for additional case modifiers using the change-case-all package, and improved the webview for template selection and variable input. Updated package.json and package-lock.json accordingly. Added localization support for configuration settings and enhanced README with usage instructions. --- README.md | 271 +++---- mytemplategenerator-0.0.5.vsix | Bin 0 -> 54623 bytes package-lock.json | 51 +- package.json | 4 +- src/core/config.ts | 38 + src/core/i18n.ts | 71 ++ src/core/templateUtils.ts | 118 +++ src/core/vars.ts | 28 + src/extension.ts | 1077 ++-------------------------- src/test/extension.test.ts | 38 +- src/vscode/completion.ts | 57 ++ src/vscode/decorations.ts | 109 +++ src/vscode/semanticHighlight.ts | 68 ++ src/webview/configWebview.ts | 139 ++++ src/webview/styles.css | 124 ++++ src/webview/templateVarsWebview.ts | 192 +++++ 16 files changed, 1145 insertions(+), 1240 deletions(-) create mode 100644 mytemplategenerator-0.0.5.vsix create mode 100644 src/core/config.ts create mode 100644 src/core/i18n.ts create mode 100644 src/core/templateUtils.ts create mode 100644 src/core/vars.ts create mode 100644 src/vscode/completion.ts create mode 100644 src/vscode/decorations.ts create mode 100644 src/vscode/semanticHighlight.ts create mode 100644 src/webview/configWebview.ts create mode 100644 src/webview/styles.css create mode 100644 src/webview/templateVarsWebview.ts 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 0000000000000000000000000000000000000000..3e0ee264848f51b5854f201b9650a760a10a3a09 GIT binary patch literal 54623 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 Lk-q(1Wq^MHDXL-- literal 0 HcmV?d00001 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