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

1087 lines
47 KiB
TypeScript
Raw Normal View History

// 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',
}
};
interface MyTemplateGeneratorConfig {
templatesPath: string;
overwriteFiles: boolean;
inputMode: 'webview' | 'inputBox';
language?: string;
}
const DEFAULT_CONFIG: MyTemplateGeneratorConfig = {
templatesPath: 'templates',
overwriteFiles: false,
inputMode: 'webview',
language: 'ru',
};
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();
return new Promise<void>((resolve) => {
const panel = vscode.window.createWebviewPanel(
'mytemplategeneratorConfig',
'Настройки myTemplateGenerator',
vscode.ViewColumn.Active,
{ enableScripts: true }
);
panel.webview.html = `
<!DOCTYPE html>
<html lang="ru">
<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>Настройки myTemplateGenerator</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 } });
});
});
</script>
</head>
<body>
<div class="config-container">
<h2>Настройки myTemplateGenerator</h2>
<form id="configForm">
<div class="form-group">
<label for="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">Перезаписывать существующие файлы</label>
</div>
<div class="form-group">
<label for="inputMode">Способ ввода переменных:</label>
<select id="inputMode">
<option value="webview" ${config.inputMode === 'webview' ? 'selected' : ''}>Webview (форма)</option>
<option value="inputBox" ${config.inputMode === 'inputBox' ? 'selected' : ''}>InputBox (по одной)</option>
</select>
</div>
<div class="form-group">
<label for="language">Язык интерфейса:</label>
<select id="language">
<option value="ru" ${config.language === 'ru' ? 'selected' : ''}>Русский</option>
<option value="en" ${config.language === 'en' ? 'selected' : ''}>English</option>
</select>
</div>
<button type="submit">Сохранить</button>
</form>
</div>
</body>
</html>
`;
panel.webview.onDidReceiveMessage(
message => {
if (message.type === 'save') {
writeConfig(message.data);
vscode.window.showInformationMessage('Настройки myTemplateGenerator сохранены!');
panel.dispose();
resolve();
}
},
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
};