feat: Template Forge 1.0.0

- генерация файлов и папок из шаблонов с подстановкой переменных
- каскадный поиск .templates вверх по дереву каталогов
- подсветка синтаксиса и автодополнение переменных в шаблонах
- webview и inputBox режимы ввода переменных
- локализация ru/en
- ядро генерации через @gromlab/create
- Gitea Actions CI для автопубликации
This commit is contained in:
2026-04-02 19:12:35 +03:00
parent 3da37cd591
commit cfbe03e06e
22 changed files with 1019 additions and 823 deletions

View File

@@ -3,15 +3,15 @@ import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
export interface MyTemplateGeneratorConfig {
export interface TemplateForgeConfig {
templatesPath: string;
overwriteFiles: boolean;
inputMode: 'webview' | 'inputBox';
language?: string;
}
export function readConfig(): MyTemplateGeneratorConfig {
const config = vscode.workspace.getConfiguration('myTemplateGenerator');
export function readConfig(): TemplateForgeConfig {
const config = vscode.workspace.getConfiguration('templateForge');
return {
templatesPath: config.get<string>('templatesPath', '.templates'),
overwriteFiles: config.get<boolean>('overwriteFiles', false),
@@ -20,8 +20,8 @@ export function readConfig(): MyTemplateGeneratorConfig {
};
}
export async function writeConfig(newConfig: Partial<MyTemplateGeneratorConfig>) {
const config = vscode.workspace.getConfiguration('myTemplateGenerator');
export async function writeConfig(newConfig: Partial<TemplateForgeConfig>) {
const config = vscode.workspace.getConfiguration('templateForge');
if (newConfig.templatesPath !== undefined) {
await config.update('templatesPath', newConfig.templatesPath, vscode.ConfigurationTarget.Global);
}
@@ -34,4 +34,4 @@ export async function writeConfig(newConfig: Partial<MyTemplateGeneratorConfig>)
if (newConfig.language !== undefined) {
await config.update('language', newConfig.language, vscode.ConfigurationTarget.Global);
}
}
}

View File

@@ -1,7 +1,4 @@
// Словари локализации и утилиты для i18n
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
// Словари локализации
export const I18N_DICTIONARIES: Record<string, Record<string, string>> = {
ru: {
@@ -40,7 +37,7 @@ export const I18N_DICTIONARIES: Record<string, Record<string, string>> = {
export const SETTINGS_I18N: Record<string, Record<string, string>> = {
ru: {
title: 'Настройки myTemplateGenerator',
title: 'Настройки Template Forge',
templatesPath: 'Путь к шаблонам:',
overwriteFiles: 'Перезаписывать существующие файлы',
inputMode: 'Способ ввода переменных:',
@@ -50,7 +47,7 @@ export const SETTINGS_I18N: Record<string, Record<string, string>> = {
save: 'Сохранить'
},
en: {
title: 'myTemplateGenerator Settings',
title: 'Template Forge Settings',
templatesPath: 'Templates path:',
overwriteFiles: 'Overwrite existing files',
inputMode: 'Variable input method:',
@@ -61,11 +58,4 @@ export const SETTINGS_I18N: Record<string, Record<string, string>> = {
}
};
export async function pickTemplate(templatesDir: string): Promise<string | undefined> {
const templates = fs.readdirSync(templatesDir).filter(f => fs.statSync(path.join(templatesDir, f)).isDirectory());
if (templates.length === 0) {
vscode.window.showWarningMessage('В папке templates нет шаблонов.');
return undefined;
}
return vscode.window.showQuickPick(templates, { placeHolder: 'Выберите шаблон' });
}

View File

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

View File

@@ -1,118 +0,0 @@
// Работа с шаблонами и преобразование кейсов
import * as fs from 'fs';
import * as path from 'path';
import * as Handlebars from 'handlebars';
// @ts-expect-error: Нет типов для change-case-all, но пакет работает корректно
import { camelCase, pascalCase, snakeCase, kebabCase, constantCase, upperCase, lowerCase } from 'change-case-all';
export const CASE_MODIFIERS: Record<string, (str: string) => string> = {
pascalCase,
camelCase,
snakeCase,
kebabCase,
screamingSnakeCase: constantCase,
upperCase,
lowerCase,
upperCaseAll: (s: string) => s.replace(/[-_\s]+/g, '').toUpperCase(),
lowerCaseAll: (s: string) => s.replace(/[-_\s]+/g, '').toLowerCase(),
};
export function readDirRecursive(src: string): string[] {
let results: string[] = [];
const list = fs.readdirSync(src);
list.forEach(function(file) {
const filePath = path.join(src, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
results = results.concat(readDirRecursive(filePath));
} else {
results.push(filePath);
}
});
return results;
}
export function copyTemplate(templateDir: string, targetDir: string, name: string) {
const vars = {
name,
nameUpperCase: CASE_MODIFIERS.upperCase(name),
nameLowerCase: CASE_MODIFIERS.lowerCase(name),
namePascalCase: CASE_MODIFIERS.pascalCase(name),
nameCamelCase: CASE_MODIFIERS.camelCase(name),
nameSnakeCase: CASE_MODIFIERS.snakeCase(name),
nameKebabCase: CASE_MODIFIERS.kebabCase(name),
nameScreamingSnakeCase: CASE_MODIFIERS.screamingSnakeCase(name),
nameUpperCaseAll: CASE_MODIFIERS.upperCaseAll(name),
nameLowerCaseAll: CASE_MODIFIERS.lowerCaseAll(name)
};
const files = readDirRecursive(templateDir);
for (const file of files) {
const relPath = path.relative(templateDir, file);
const relPathTmpl = Handlebars.compile(relPath);
const targetRelPath = relPathTmpl(vars);
const targetPath = path.join(targetDir, targetRelPath);
const content = fs.readFileSync(file, 'utf8');
const contentTmpl = Handlebars.compile(content);
const rendered = contentTmpl(vars);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, rendered, { flag: 'wx' });
}
}
export function getAllTemplateVariables(templateDir: string): Set<string> {
const files = readDirRecursive(templateDir);
const varRegex = /{{\s*([\w]+)(?:\.[\w]+)?\s*}}/g;
const vars = new Set<string>();
for (const file of files) {
let relPath = path.relative(templateDir, file);
let match;
while ((match = varRegex.exec(relPath)) !== null) {
vars.add(match[1]);
}
const content = fs.readFileSync(file, 'utf8');
while ((match = varRegex.exec(content)) !== null) {
vars.add(match[1]);
}
}
return vars;
}
export function applyTemplate(str: string, vars: Record<string, string>, modifiers: Record<string, (s: string) => string>): string {
return str.replace(/{{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_]+))?\s*}}/g, (_, varName, mod) => {
let value = vars[varName];
if (value === undefined) return '';
if (mod && modifiers[mod]) {
return modifiers[mod](value);
}
return value;
});
}
export function copyTemplateWithVars(templateDir: string, targetDir: string, vars: Record<string, string>, overwriteFiles: boolean = false, dict?: Record<string, string>, templateName?: string): boolean {
const files = readDirRecursive(templateDir);
const firstLevelDirs = new Set<string>();
for (const file of files) {
const relPath = path.relative(templateDir, file);
const targetRelPath = applyTemplate(relPath, vars, CASE_MODIFIERS);
const firstLevel = targetRelPath.split(path.sep)[0];
firstLevelDirs.add(firstLevel);
}
if (!overwriteFiles && dict) {
for (const dir of firstLevelDirs) {
const checkPath = path.join(targetDir, dir);
if (fs.existsSync(checkPath)) {
return false;
}
}
}
for (const file of files) {
const relPath = path.relative(templateDir, file);
const targetRelPath = applyTemplate(relPath, vars, CASE_MODIFIERS);
const targetPath = path.join(targetDir, targetRelPath);
const content = fs.readFileSync(file, 'utf8');
const rendered = applyTemplate(content, vars, CASE_MODIFIERS);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, rendered, { flag: overwriteFiles ? 'w' : 'wx' });
}
return true;
}

View File

@@ -1,17 +1,4 @@
// Работа с переменными шаблонов
import { CASE_MODIFIERS } from './templateUtils';
export function buildVarsObject(userVars: Record<string, string>): Record<string, string> {
const result: Record<string, string> = {};
for (const [base, value] of Object.entries(userVars)) {
result[base] = value;
for (const [mod, fn] of Object.entries(CASE_MODIFIERS)) {
result[`${base}.${mod}`] = fn(value);
}
}
return result;
}
// Сбор переменных от пользователя через InputBox
import * as vscode from 'vscode';
export async function collectUserVars(baseVars: Set<string>): Promise<Record<string, string>> {
@@ -25,4 +12,4 @@ export async function collectUserVars(baseVars: Set<string>): Promise<Record<str
result[v] = input;
}
return result;
}
}

View File

@@ -1,57 +1,18 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import * as Handlebars from 'handlebars';
import { getAllTemplateVariables, copyTemplateWithVars } from './core/templateUtils';
import { buildVarsObject, collectUserVars } from './core/vars';
import { readConfig, writeConfig } from './core/config';
import { collectTemplateVariables, readDirRecursive, buildPlan, writePlan, getCollisions } from '@gromlab/create';
import { collectUserVars } from './core/vars';
import { readConfig } 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';
import { I18N_DICTIONARIES } from './core/i18n';
import { discoverTemplates, resolveTemplate } from './core/templateResolver';
// Регистрируем кастомный helper для Handlebars
Handlebars.registerHelper('getVar', function(this: Record<string, any>, varName: string, modifier?: string, options?: any) {
if (!varName) return '';
const vars = this;
if (modifier && typeof modifier === 'string') {
if (vars[`${varName}.${modifier}`]) return vars[`${varName}.${modifier}`];
if (vars[varName] && typeof vars[varName] === 'object' && vars[varName][modifier]) {
return vars[varName][modifier];
}
return '';
}
if (vars[varName]) {
if (typeof vars[varName] === 'object' && vars[varName].value) return vars[varName].value;
return vars[varName];
}
return '';
});
// === Декораторы для шаблонных переменных ===
// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
// The command has been defined in the package.json file
// Now provide the implementation of the command with registerCommand
// The commandId parameter must match the command field in package.json
const disposable = vscode.commands.registerCommand('mytemplategenerator.helloWorld', () => {
// The code you place here will be executed every time your command is executed
// Display a message box to the user
vscode.window.showInformationMessage('Hello World from myTemplateGenerator!');
});
const createFromTemplate = vscode.commands.registerCommand('mytemplategenerator.createFromTemplate', async (uri: vscode.Uri) => {
const createFromTemplate = vscode.commands.registerCommand('templateforge.createFromTemplate', async (uri: vscode.Uri) => {
const config = readConfig();
const dict = I18N_DICTIONARIES[config.language || 'ru'] || I18N_DICTIONARIES['ru'];
const workspaceFolders = vscode.workspace.workspaceFolders;
@@ -59,63 +20,76 @@ export function activate(context: vscode.ExtensionContext) {
vscode.window.showErrorMessage(dict.noFolders);
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}`);
const workspaceRoot = workspaceFolders[0].uri.fsPath;
const targetDir = uri.fsPath;
// Каскадный поиск шаблонов вверх от точки вызова до корня workspace
const templates = discoverTemplates(targetDir, workspaceRoot, config.templatesPath);
if (templates.size === 0) {
vscode.window.showErrorMessage(dict.templatesNotFound);
return;
}
let template: string | undefined;
let templateName: string | undefined;
let userVars: Record<string, string> | undefined;
if (config.inputMode === 'webview') {
vscode.window.showInformationMessage('[DEBUG] Вызов webview создания шаблона...');
const result: { template: string, vars: Record<string, string> } | undefined = await showTemplateAndVarsWebview(context, templatesDir, uri.fsPath, config.language || 'ru');
if (!result) {
vscode.window.showInformationMessage('[DEBUG] Webview был закрыт или не вернул результат');
return;
}
template = result.template;
const result = await showTemplateAndVarsWebview(context, templates, targetDir, config.language || 'ru');
if (!result) return;
templateName = result.template;
userVars = result.vars;
} else {
vscode.window.showInformationMessage('[DEBUG] Вызов выбора шаблона через quickPick...');
template = await pickTemplate(templatesDir);
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));
const templateNames = Array.from(templates.keys());
templateName = await vscode.window.showQuickPick(templateNames, { placeHolder: dict.chooseTemplate });
if (!templateName) return;
const templateDir = templates.get(templateName)!;
const allVars = collectTemplateVariables(templateDir);
userVars = await collectUserVars(allVars);
}
if (!template || !userVars) {
vscode.window.showInformationMessage('[DEBUG] Не выбраны шаблон или переменные');
if (!templateName || !userVars) return;
// Резолвим шаблон по каскаду (первый .templates содержащий его)
const templateDir = resolveTemplate(templateName, targetDir, workspaceRoot, config.templatesPath);
if (!templateDir) {
vscode.window.showErrorMessage(`${dict.createError}: "${templateName}"`);
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);
const files = readDirRecursive(templateDir);
const plan = buildPlan(templateDir, targetDir, userVars, files);
if (!config.overwriteFiles) {
const collisions = getCollisions(plan);
if (collisions.length > 0) {
vscode.window.showWarningMessage(dict.fileExistsNoOverwrite);
return;
}
}
writePlan(plan, userVars, config.overwriteFiles);
vscode.window.setStatusBarMessage(
dict.createSuccess.replace('{{template}}', templateName), 5000
);
} catch (e: any) {
vscode.window.showErrorMessage(`${dict.createError}: ${e.message}`);
}
});
context.subscriptions.push(
disposable,
createFromTemplate,
vscode.commands.registerCommand('mytemplategenerator.configure', async () => {
vscode.commands.registerCommand('templateforge.configure', async () => {
await showConfigWebview(context);
})
);
registerTemplateCompletionAndHighlight(context);
let semanticHighlightDisposable: vscode.Disposable | undefined = registerTemplateSemanticHighlight(context);
registerTemplateDecorations(context); // <--- Добавить регистрацию декораторов
clearDiagnosticsForTemplates(context); // <--- Очищаем diagnostics для шаблонов
// === Отслеживание изменений конфига ===
// (Удалено: теперь все настройки глобальные через VSCode settings)
registerTemplateCompletionAndHighlight(context);
registerTemplateSemanticHighlight(context);
registerTemplateDecorations(context);
clearDiagnosticsForTemplates(context);
}
// This method is called when your extension is deactivated
export function deactivate() {}

View File

@@ -1,10 +1,6 @@
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
import { CASE_MODIFIERS } from '../core/templateUtils';
import { buildVarsObject } from '../core/vars';
import { CASE_MODIFIERS } from '@gromlab/create';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
@@ -23,17 +19,4 @@ suite('Template Variable Modifiers', () => {
assert.strictEqual(typeof fn(input), 'string');
}
});
test('buildVarsObject generates all keys', () => {
const vars = buildVarsObject({ name: input });
assert.strictEqual(vars['name'], input);
assert.strictEqual(vars['name.pascalCase'], 'MySuperName');
assert.strictEqual(vars['name.camelCase'], 'mySuperName');
assert.strictEqual(vars['name.snakeCase'], 'my_super_name');
assert.strictEqual(vars['name.kebabCase'], 'my-super-name');
assert.strictEqual(vars['name.screamingSnakeCase'], 'MY_SUPER_NAME');
assert.strictEqual(vars['name.upperCase'], 'My super-name');
assert.strictEqual(vars['name.lowerCase'], 'my super-name');
assert.strictEqual(vars['name.upperCaseAll'], 'MYSUPERNAME');
assert.strictEqual(vars['name.lowerCaseAll'], 'mysupername');
});
});

View File

@@ -1,9 +1,8 @@
// Регистрация и обработка автодополнения шаблонов
import * as vscode from 'vscode';
import * as path from 'path';
import { getAllTemplateVariables, CASE_MODIFIERS } from '../core/templateUtils';
import { collectTemplateVariables, CASE_MODIFIERS } from '@gromlab/create';
import { readConfig } from '../core/config';
import * as fs from 'fs';
function isInTemplatesDir(filePath: string, templatesDir: string): boolean {
const rel = path.relative(templatesDir, filePath);
@@ -14,7 +13,7 @@ export function registerTemplateCompletionAndHighlight(context: vscode.Extension
const completionProvider = {
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
const config = readConfig();
const templatesPath = config.templatesPath || 'templates';
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);
@@ -25,7 +24,7 @@ export function registerTemplateCompletionAndHighlight(context: vscode.Extension
const textBefore = line.slice(0, position.character);
const match = /{{\s*([\w]+)?(?:\.([\w]*))?[^}]*$/.exec(textBefore);
if (!match) return undefined;
const allVars = getAllTemplateVariables(templatesDir);
const allVars = collectTemplateVariables(templatesDir);
const items = [];
if (match[2] !== undefined) {
for (const mod of Object.keys(CASE_MODIFIERS)) {
@@ -54,4 +53,4 @@ export function registerTemplateCompletionAndHighlight(context: vscode.Extension
'{', '.'
)
);
}
}

View File

@@ -1,7 +1,6 @@
// Декорации и диагностика шаблонов
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({

View File

@@ -1,7 +1,6 @@
// Семантическая подсветка шаблонов
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 {

View File

@@ -1,6 +1,6 @@
// Webview для конфигурации расширения
import * as vscode from 'vscode';
import { MyTemplateGeneratorConfig, readConfig, writeConfig } from '../core/config';
import { TemplateForgeConfig, readConfig, writeConfig } from '../core/config';
const LOCALIZATION: Record<'ru'|'en', {
title: string;
@@ -27,7 +27,7 @@ const LOCALIZATION: Record<'ru'|'en', {
english: 'English',
},
en: {
title: 'Template Generator Settings',
title: 'Template Forge Settings',
templatesPath: 'Templates path:',
overwriteFiles: 'Overwrite files',
inputMode: 'Input mode:',
@@ -42,8 +42,8 @@ const LOCALIZATION: Record<'ru'|'en', {
export async function showConfigWebview(context: vscode.ExtensionContext) {
const panel = vscode.window.createWebviewPanel(
'myTemplateGeneratorConfig',
'Настройки MyTemplateGenerator',
'templateForgeConfig',
'Template Forge Settings',
vscode.ViewColumn.One,
{ enableScripts: true }
);
@@ -123,7 +123,7 @@ export async function showConfigWebview(context: vscode.ExtensionContext) {
async msg => {
if (msg.type === 'save') {
await writeConfig(msg.data);
vscode.window.showInformationMessage('Настройки сохранены!');
vscode.window.setStatusBarMessage('Настройки сохранены!', 3000);
panel.dispose();
}
if (msg.type === 'setLanguage') {
@@ -136,4 +136,4 @@ export async function showConfigWebview(context: vscode.ExtensionContext) {
undefined,
context.subscriptions
);
}
}

View File

@@ -1,14 +1,12 @@
// Webview для выбора шаблона и переменных
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { getAllTemplateVariables } from '../core/templateUtils';
import { collectTemplateVariables } from '@gromlab/create';
import { I18N_DICTIONARIES } from '../core/i18n';
import { writeConfig, readConfig } from '../core/config';
export async function showTemplateAndVarsWebview(
context: vscode.ExtensionContext,
templatesDir: string,
templatesMap: Map<string, string>,
targetPath: string,
initialLanguage: string
): Promise<{ template: string, vars: Record<string, string> } | undefined> {
@@ -16,8 +14,7 @@ export async function showTemplateAndVarsWebview(
function getDict() {
return I18N_DICTIONARIES[language] || I18N_DICTIONARIES['ru'];
}
const templates = fs.readdirSync(templatesDir).filter(f => fs.statSync(path.join(templatesDir, f)).isDirectory());
// Стили теперь лежат в media/styles.css (папка для статики)
const templateNames = Array.from(templatesMap.keys());
const stylePath = vscode.Uri.joinPath(context.extensionUri, 'media', 'styles.css');
return new Promise((resolve) => {
const panel = vscode.window.createWebviewPanel(
@@ -28,8 +25,15 @@ export async function showTemplateAndVarsWebview(
);
const styleUri = panel.webview.asWebviewUri(stylePath);
let currentVars: string[] = [];
let currentTemplate = templates[0] || '';
let currentTemplate = templateNames[0] || '';
let disposed = false;
function getVarsForTemplate(name: string): string[] {
const dir = templatesMap.get(name);
if (!dir) return [];
return Array.from(collectTemplateVariables(dir));
}
function getVarsHtml(vars: string[], values: Record<string, string> = {}) {
const dict = getDict();
if (!vars.length) return '';
@@ -89,14 +93,12 @@ export async function showTemplateAndVarsWebview(
(function() {
const vscode = acquireVsCodeApi();
function initHandlers() {
// Template radio
const templateRadios = document.querySelectorAll('input[name="templateRadio"]');
templateRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
vscode.postMessage({ type: 'selectTemplate', template: e.target.value, language: document.getElementById('languageSelect').value });
});
});
// Vars form
const varsForm = document.getElementById('varsForm');
if (varsForm) {
varsForm.addEventListener('submit', (e) => {
@@ -108,7 +110,6 @@ export async function showTemplateAndVarsWebview(
vscode.postMessage({ type: 'submit', template: document.querySelector('input[name="templateRadio"]:checked')?.value || '', data, language: document.getElementById('languageSelect').value });
});
}
// Language select
const langSel = document.getElementById('languageSelect');
if (langSel) {
langSel.addEventListener('change', (e) => {
@@ -123,52 +124,33 @@ export async function showTemplateAndVarsWebview(
</body>
</html>
`;
// После перерисовки HTML вызываем initHandlers
setTimeout(() => {
panel.webview.postMessage({ type: 'callInitHandlers' });
}, 0);
}
// Инициализация: сразу выбран первый шаблон и форма переменных
let initialVars: string[] = [];
if (currentTemplate) {
const templateDir = path.join(templatesDir, currentTemplate);
const allVars = getAllTemplateVariables(templateDir);
initialVars = Array.from(allVars);
currentVars = initialVars;
}
setHtml(getTemplatesRadioHtml(templates, currentTemplate), getVarsHtml(initialVars));
// Обработка сообщений
// Инициализация
currentVars = getVarsForTemplate(currentTemplate);
setHtml(getTemplatesRadioHtml(templateNames, currentTemplate), getVarsHtml(currentVars));
panel.webview.onDidReceiveMessage(
async message => {
if (message.type === 'selectTemplate') {
currentTemplate = message.template;
if (message.language) language = message.language;
if (!currentTemplate) {
setHtml(getTemplatesRadioHtml(templates, ''), '');
setHtml(getTemplatesRadioHtml(templateNames, ''), '');
return;
}
// Получаем переменные для выбранного шаблона
const templateDir = path.join(templatesDir, currentTemplate);
const allVars = getAllTemplateVariables(templateDir);
currentVars = Array.from(allVars);
setHtml(getTemplatesRadioHtml(templates, currentTemplate), getVarsHtml(currentVars));
currentVars = getVarsForTemplate(currentTemplate);
setHtml(getTemplatesRadioHtml(templateNames, currentTemplate), getVarsHtml(currentVars));
} else if (message.type === 'setLanguage') {
if (message.language) language = message.language;
// Сохраняем язык в конфиг
const oldConfig = readConfig();
await writeConfig({ ...oldConfig, language });
currentTemplate = message.template || 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, не нужен
currentTemplate = message.template || templateNames[0] || '';
currentVars = getVarsForTemplate(currentTemplate);
setHtml(getTemplatesRadioHtml(templateNames, currentTemplate), getVarsHtml(currentVars));
} else if (message.type === 'submit') {
if (message.language) language = message.language;
if (!disposed) {
@@ -176,8 +158,6 @@ export async function showTemplateAndVarsWebview(
panel.dispose();
resolve({ template: message.template, vars: message.data });
}
} else if (message.type === 'callInitHandlers') {
// Ничего не делаем, скрипт внутри webview вызовет window.initHandlers
}
},
undefined,
@@ -190,4 +170,4 @@ export async function showTemplateAndVarsWebview(
}
}, null, context.subscriptions);
});
}
}