feat: Template Forge 1.0.0
- генерация файлов и папок из шаблонов с подстановкой переменных - каскадный поиск .templates вверх по дереву каталогов - подсветка синтаксиса и автодополнение переменных в шаблонах - webview и inputBox режимы ввода переменных - локализация ru/en - ядро генерации через @gromlab/create - Gitea Actions CI для автопубликации
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: 'Выберите шаблон' });
|
||||
}
|
||||
|
||||
|
||||
66
src/core/templateResolver.ts
Normal file
66
src/core/templateResolver.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// Каскадный поиск .templates по дереву каталогов
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { listTemplateNames } from '@gromlab/create';
|
||||
|
||||
/**
|
||||
* Собирает все доступные шаблоны по каскаду вверх от startDir до rootDir.
|
||||
* Ближайшие шаблоны имеют приоритет (shadowing).
|
||||
* Возвращает Map: имя шаблона → абсолютный путь к его директории.
|
||||
*/
|
||||
export function discoverTemplates(
|
||||
startDir: string,
|
||||
rootDir: string,
|
||||
templatesFolder: string = '.templates'
|
||||
): Map<string, string> {
|
||||
const result = new Map<string, string>();
|
||||
const resolvedRoot = path.resolve(rootDir);
|
||||
let current = path.resolve(startDir);
|
||||
|
||||
while (true) {
|
||||
const templatesDir = path.join(current, templatesFolder);
|
||||
if (fs.existsSync(templatesDir) && fs.statSync(templatesDir).isDirectory()) {
|
||||
const names = listTemplateNames(templatesDir);
|
||||
for (const name of names) {
|
||||
if (!result.has(name)) {
|
||||
result.set(name, path.join(templatesDir, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (current === resolvedRoot) break;
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ищет конкретный шаблон по каскаду вверх от startDir до rootDir.
|
||||
* Возвращает абсолютный путь к первому .templates, содержащему шаблон, или undefined.
|
||||
*/
|
||||
export function resolveTemplate(
|
||||
templateName: string,
|
||||
startDir: string,
|
||||
rootDir: string,
|
||||
templatesFolder: string = '.templates'
|
||||
): string | undefined {
|
||||
const resolvedRoot = path.resolve(rootDir);
|
||||
let current = path.resolve(startDir);
|
||||
|
||||
while (true) {
|
||||
const candidate = path.join(current, templatesFolder, templateName);
|
||||
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
if (current === resolvedRoot) break;
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
136
src/extension.ts
136
src/extension.ts
@@ -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() {}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
'{', '.'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user