Files
vscode-template-file-generator/src/extension.ts

1121 lines
49 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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';
async function getTemplatesDir(workspaceFolders: readonly vscode.WorkspaceFolder[] | undefined): Promise<string | undefined> {
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<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: 'Выберите шаблон' });
}
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<string> {
const files = readDirRecursive(templateDir);
// Ищем {{ variable }} и {{ variable.suffix }}
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;
}
// Мапа модификаторов и функций
const CASE_MODIFIERS: Record<string, (str: string) => 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<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;
}
async function collectUserVars(baseVars: Set<string>): Promise<Record<string, string>> {
const result: Record<string, string> = {};
for (const v of baseVars) {
const input = await vscode.window.showInputBox({
prompt: `Введите значение для ${v}`,
placeHolder: `{{${v}}}`
});
if (!input) throw new Error(`Значение для ${v} не введено`);
result[v] = input;
}
return result;
}
// --- Собственная обработка шаблонов ---
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;
});
}
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)) {
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<string, Record<string, string>> = {
ru: {
destinationPath: 'Путь назначения',
chooseTemplate: 'Выберите шаблон',
enterVariables: 'Введите значения переменных',
varInputHint: 'без скобок {{ }}',
create: 'Создать',
selectTemplate: 'Шаблон',
fileExistsNoOverwrite: 'Файл или папка уже существует и перезапись запрещена',
fileExists: 'Файл или папка уже существует',
createSuccess: 'Структура {{template}} успешно создана.',
createError: 'Ошибка при создании структуры',
noTemplates: 'В папке шаблонов нет шаблонов.',
templatesNotFound: 'Папка шаблонов не найдена:',
noFolders: 'Нет открытых папок рабочего пространства.',
inputName: 'Введите имя для шаблона',
},
en: {
destinationPath: 'Destination path',
chooseTemplate: 'Choose template',
enterVariables: 'Enter variables',
varInputHint: 'without curly braces {{ }}',
create: 'Create',
selectTemplate: 'Template',
fileExistsNoOverwrite: 'File or folder already exists and overwrite is disabled',
fileExists: 'File or folder already exists',
createSuccess: 'Structure {{template}} created successfully.',
createError: 'Error creating structure',
noTemplates: 'No templates found in templates folder.',
templatesNotFound: 'Templates folder not found:',
noFolders: 'No workspace folders open.',
inputName: 'Enter name for template',
}
};
const SETTINGS_I18N: Record<string, Record<string, string>> = {
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<void>((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 = `
<!DOCTYPE html>
<html lang="${language}">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'unsafe-inline';">
<title>${dict.title}</title>
<style>
: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;
}
.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;
}
.config-container h2 {
margin: 0 0 18px 0;
font-size: 1.5em;
font-weight: 600;
letter-spacing: 0.01em;
}
.form-group {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 10px;
}
label {
color: var(--label);
font-size: 1em;
font-weight: 500;
margin-bottom: 2px;
}
input[type="text"], 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[type="text"]:focus, select:focus {
border-color: var(--input-focus);
box-shadow: 0 0 0 2px var(--input-focus)33;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
input[type="checkbox"] {
accent-color: var(--input-focus);
width: 18px;
height: 18px;
}
button {
margin-top: 10px;
background: var(--button-bg);
color: var(--button-text);
border: none;
border-radius: var(--border-radius);
padding: 10px 0;
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 {
background: var(--button-hover);
}
</style>
<script>
window.addEventListener('DOMContentLoaded', () => {
const vscode = acquireVsCodeApi();
document.getElementById('configForm').addEventListener('submit', (e) => {
e.preventDefault();
const templatesPath = document.getElementById('templatesPath').value;
const overwriteFiles = document.getElementById('overwriteFiles').checked;
const inputMode = document.getElementById('inputMode').value;
const language = document.getElementById('language').value;
vscode.postMessage({ type: 'save', data: { templatesPath, overwriteFiles, inputMode, language } });
});
document.getElementById('language').addEventListener('change', (e) => {
vscode.postMessage({ type: 'changeLanguage', language: e.target.value });
});
});
</script>
</head>
<body>
<div class="config-container">
<h2>${dict.title}</h2>
<form id="configForm">
<div class="form-group">
<label for="templatesPath">${dict.templatesPath}</label>
<input type="text" id="templatesPath" value="${config.templatesPath || ''}"/>
</div>
<div class="checkbox-group">
<input type="checkbox" id="overwriteFiles" ${config.overwriteFiles ? 'checked' : ''}/>
<label for="overwriteFiles">${dict.overwriteFiles}</label>
</div>
<div class="form-group">
<label for="inputMode">${dict.inputMode}</label>
<select id="inputMode">
<option value="webview" ${config.inputMode === 'webview' ? 'selected' : ''}>${dict.inputModeWebview}</option>
<option value="inputBox" ${config.inputMode === 'inputBox' ? 'selected' : ''}>${dict.inputModeInputBox}</option>
</select>
</div>
<div class="form-group">
<label for="language">${dict.language}</label>
<select id="language">
<option value="ru" ${language === 'ru' ? 'selected' : ''}>Русский</option>
<option value="en" ${language === 'en' ? 'selected' : ''}>English</option>
</select>
</div>
<button type="submit" style="padding: 10px 15px">${dict.save}</button>
</form>
</div>
</body>
</html>
`;
}
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<string, string>} | 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<string, string> = {}) {
const dict = getDict();
if (!vars.length) return '';
return `<h3>${dict.enterVariables}</h3>
<div style="color: #888; font-size: 13px; margin-bottom: 10px;">${dict.varInputHint}</div>
<form id="varsForm">
${vars.map(v => `
<label><input name="${v}" placeholder="{{${v}}}" value="${values[v] || ''}" required /></label><br/><br/>
`).join('')}
<button type="submit" style="padding: 10px 15px">${dict.create}</button>
</form>`;
}
function getTemplatesRadioHtml(templates: string[], selected: string) {
const dict = getDict();
return `<form id="templateForm">
<h3>${dict.chooseTemplate}:</h3>
<div style="display: flex; flex-direction: column; gap: 8px;">
${templates.map(t => `
<label><input type="radio" name="templateRadio" value="${t}" ${selected === t ? 'checked' : ''}/> ${t}</label>
`).join('')}
</div>
</form>`;
}
function getLanguageSelectorHtml(selected: string) {
return `<label style="margin-bottom: 12px; display: block;">
<select id="languageSelect">
<option value="ru" ${selected === 'ru' ? 'selected' : ''}>Русский</option>
<option value="en" ${selected === 'en' ? 'selected' : ''}>English</option>
</select>
</label>`;
}
function setHtml(templatesHtml: string, varsHtml: string) {
const dict = getDict();
panel.webview.html = `
<!DOCTYPE html>
<html lang="${language}">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'unsafe-inline';">
<title>${dict.create}</title>
<style>
: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 {
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 {
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 {
margin-top: 10px;
background: var(--button-bg);
color: var(--button-text);
border: none;
border-radius: var(--border-radius);
padding: 10px 0;
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 {
background: var(--button-hover);
}
.destination {
margin-bottom: 16px;
color: #888;
font-size: 13px;
}
.lang-select {
display: block;
}
</style>
<script>
window.addEventListener('DOMContentLoaded', () => {
const vscode = acquireVsCodeApi();
const templateRadios = document.querySelectorAll('input[name="templateRadio"]');
templateRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
vscode.postMessage({ type: 'selectTemplate', template: e.target.value, language: document.getElementById('languageSelect').value });
});
});
const varsForm = document.getElementById('varsForm');
if (varsForm) {
varsForm.addEventListener('submit', (e) => {
e.preventDefault();
const data = {};
${currentVars.map(v => `data['${v}'] = document.getElementsByName('${v}')[0].value;`).join('\n')}
vscode.postMessage({ type: 'submit', template: '${currentTemplate}', data, language: document.getElementById('languageSelect').value });
});
}
const langSel = document.getElementById('languageSelect');
if (langSel) {
langSel.addEventListener('change', (e) => {
vscode.postMessage({ type: 'changeLanguage', language: e.target.value, template: document.querySelector('input[name="templateRadio"]:checked')?.value || '' });
});
}
});
</script>
</head>
<body>
<div class="create-container">
<div class="head-wrap">
<h2>${dict.create}</h2>
<div class="lang-select">${getLanguageSelectorHtml(language)}</div>
</div>
<div id="templatesBlock">
${templatesHtml}
</div>
<div id="varsBlock">
${varsHtml}
</div>
</div>
</body>
</html>
`;
}
// Инициализация: сразу выбран первый шаблон и форма переменных
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);
});
}
// Регистрируем кастомный 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 '';
});
// --- Автокомплит и подсветка для шаблонов ---
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
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
console.log('Congratulations, your extension "mytemplategenerator" is now active!');
// 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 config = readConfig();
const dict = I18N_DICTIONARIES[config.language || 'ru'] || I18N_DICTIONARIES['ru'];
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
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}`);
return;
}
let template: string | undefined;
let userVars: Record<string, string> | undefined;
if (config.inputMode === 'webview') {
const result = await showTemplateAndVarsWebview(context, templatesDir, uri.fsPath, config.language || 'ru');
if (!result) return;
template = result.template;
userVars = result.vars;
} else {
template = await pickTemplate(templatesDir);
if (!template) 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;
const templateDir = path.join(templatesDir, template);
try {
const vars = buildVarsObject(userVars);
copyTemplateWithVars(templateDir, uri.fsPath, vars, config.overwriteFiles, dict, template);
} catch (e: any) {
vscode.window.showErrorMessage(`${dict.createError}: ${e.message}`);
}
});
context.subscriptions.push(
disposable,
createFromTemplate,
vscode.commands.registerCommand('mytemplategenerator.configure', async () => {
await showConfigWebview(context);
})
);
registerTemplateCompletionAndHighlight(context);
registerTemplateSemanticHighlight(context);
registerTemplateDecorations(context); // <--- Добавить регистрацию декораторов
clearDiagnosticsForTemplates(context); // <--- Очищаем diagnostics для шаблонов
}
// This method is called when your extension is deactivated
export function deactivate() {}
export {
toPascalCase,
toCamelCase,
toSnakeCase,
toKebabCase,
toScreamingSnakeCase,
toUpperCaseFirst,
toUpperCaseAll,
toLowerCaseAll,
buildVarsObject,
CASE_MODIFIERS
};