Update myTemplateGenerator to version 0.0.5. Introduced new configuration options, added support for additional case modifiers using the change-case-all package, and improved the webview for template selection and variable input. Updated package.json and package-lock.json accordingly. Added localization support for configuration settings and enhanced README with usage instructions.
This commit is contained in:
38
src/core/config.ts
Normal file
38
src/core/config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// Работа с конфигом расширения
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export interface MyTemplateGeneratorConfig {
|
||||
templatesPath: string;
|
||||
overwriteFiles: boolean;
|
||||
inputMode: 'webview' | 'inputBox';
|
||||
language?: string;
|
||||
}
|
||||
|
||||
export function getConfigPath(): string | undefined {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (!folders || folders.length === 0) return undefined;
|
||||
return path.join(folders[0].uri.fsPath, 'mycodegenerate.json');
|
||||
}
|
||||
|
||||
export function readConfig(): MyTemplateGeneratorConfig {
|
||||
const configPath = getConfigPath();
|
||||
if (configPath && fs.existsSync(configPath)) {
|
||||
const raw = fs.readFileSync(configPath, 'utf8');
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
// Значения по умолчанию
|
||||
return {
|
||||
templatesPath: 'templates',
|
||||
overwriteFiles: false,
|
||||
inputMode: 'inputBox',
|
||||
language: 'en',
|
||||
};
|
||||
}
|
||||
|
||||
export function writeConfig(config: MyTemplateGeneratorConfig) {
|
||||
const configPath = getConfigPath();
|
||||
if (!configPath) return;
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
||||
}
|
||||
71
src/core/i18n.ts
Normal file
71
src/core/i18n.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
// Словари локализации и утилиты для i18n
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export const I18N_DICTIONARIES: Record<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',
|
||||
}
|
||||
};
|
||||
|
||||
export 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'
|
||||
}
|
||||
};
|
||||
|
||||
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: 'Выберите шаблон' });
|
||||
}
|
||||
118
src/core/templateUtils.ts
Normal file
118
src/core/templateUtils.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
// Работа с шаблонами и преобразование кейсов
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as Handlebars from 'handlebars';
|
||||
// @ts-expect-error: Нет типов для change-case-all, но пакет работает корректно
|
||||
import { camelCase, pascalCase, snakeCase, kebabCase, constantCase, upperCase, lowerCase } from 'change-case-all';
|
||||
|
||||
export const CASE_MODIFIERS: Record<string, (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;
|
||||
}
|
||||
28
src/core/vars.ts
Normal file
28
src/core/vars.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// Работа с переменными шаблонов
|
||||
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;
|
||||
}
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export 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;
|
||||
}
|
||||
1077
src/extension.ts
1077
src/extension.ts
File diff suppressed because it is too large
Load Diff
@@ -3,18 +3,8 @@ import * as assert from 'assert';
|
||||
// You can import and use all API from the 'vscode' module
|
||||
// as well as import your extension to test it
|
||||
import * as vscode from 'vscode';
|
||||
import {
|
||||
toPascalCase,
|
||||
toCamelCase,
|
||||
toSnakeCase,
|
||||
toKebabCase,
|
||||
toScreamingSnakeCase,
|
||||
toUpperCaseFirst,
|
||||
toUpperCaseAll,
|
||||
toLowerCaseAll,
|
||||
buildVarsObject,
|
||||
CASE_MODIFIERS
|
||||
} from '../extension';
|
||||
import { CASE_MODIFIERS } from '../core/templateUtils';
|
||||
import { buildVarsObject } from '../core/vars';
|
||||
|
||||
suite('Extension Test Suite', () => {
|
||||
vscode.window.showInformationMessage('Start all tests.');
|
||||
@@ -28,30 +18,6 @@ suite('Extension Test Suite', () => {
|
||||
suite('Template Variable Modifiers', () => {
|
||||
const input = 'my super-name';
|
||||
|
||||
test('toPascalCase', () => {
|
||||
assert.strictEqual(toPascalCase(input), 'MySuperName');
|
||||
});
|
||||
test('toCamelCase', () => {
|
||||
assert.strictEqual(toCamelCase(input), 'mySuperName');
|
||||
});
|
||||
test('toSnakeCase', () => {
|
||||
assert.strictEqual(toSnakeCase(input), 'my_super_name');
|
||||
});
|
||||
test('toKebabCase', () => {
|
||||
assert.strictEqual(toKebabCase(input), 'my-super-name');
|
||||
});
|
||||
test('toScreamingSnakeCase', () => {
|
||||
assert.strictEqual(toScreamingSnakeCase(input), 'MY_SUPER_NAME');
|
||||
});
|
||||
test('toUpperCaseFirst', () => {
|
||||
assert.strictEqual(toUpperCaseFirst(input), 'My super-name');
|
||||
});
|
||||
test('toUpperCaseAll', () => {
|
||||
assert.strictEqual(toUpperCaseAll(input), 'MYSUPERNAME');
|
||||
});
|
||||
test('toLowerCaseAll', () => {
|
||||
assert.strictEqual(toLowerCaseAll(input), 'mysupername');
|
||||
});
|
||||
test('CASE_MODIFIERS map covers all', () => {
|
||||
for (const [mod, fn] of Object.entries(CASE_MODIFIERS)) {
|
||||
assert.strictEqual(typeof fn(input), 'string');
|
||||
|
||||
57
src/vscode/completion.ts
Normal file
57
src/vscode/completion.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// Регистрация и обработка автодополнения шаблонов
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { getAllTemplateVariables, CASE_MODIFIERS } from '../core/templateUtils';
|
||||
import { readConfig } from '../core/config';
|
||||
import * as fs from 'fs';
|
||||
|
||||
function isInTemplatesDir(filePath: string, templatesDir: string): boolean {
|
||||
const rel = path.relative(templatesDir, filePath);
|
||||
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
||||
}
|
||||
|
||||
export function registerTemplateCompletionAndHighlight(context: vscode.ExtensionContext) {
|
||||
const completionProvider = {
|
||||
provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
|
||||
const config = readConfig();
|
||||
const templatesPath = config.templatesPath || 'templates';
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) return;
|
||||
const templatesDir = path.join(workspaceFolders[0].uri.fsPath, templatesPath);
|
||||
if (!isInTemplatesDir(document.uri.fsPath, templatesDir)) {
|
||||
return undefined;
|
||||
}
|
||||
const line = document.lineAt(position).text;
|
||||
const textBefore = line.slice(0, position.character);
|
||||
const match = /{{\s*([\w]+)?(?:\.([\w]*))?[^}]*$/.exec(textBefore);
|
||||
if (!match) return undefined;
|
||||
const allVars = getAllTemplateVariables(templatesDir);
|
||||
const items = [];
|
||||
if (match[2] !== undefined) {
|
||||
for (const mod of Object.keys(CASE_MODIFIERS)) {
|
||||
if (!match[2] || mod.startsWith(match[2])) {
|
||||
const item = new vscode.CompletionItem(mod, vscode.CompletionItemKind.EnumMember);
|
||||
item.insertText = mod;
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const v of allVars) {
|
||||
if (!match[1] || v.startsWith(match[1])) {
|
||||
const item = new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable);
|
||||
item.insertText = v;
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
};
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
'*',
|
||||
completionProvider,
|
||||
'{', '.'
|
||||
)
|
||||
);
|
||||
}
|
||||
109
src/vscode/decorations.ts
Normal file
109
src/vscode/decorations.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
// Декорации и диагностика шаблонов
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { getAllTemplateVariables } from '../core/templateUtils';
|
||||
import { readConfig } from '../core/config';
|
||||
|
||||
const bracketDecoration = vscode.window.createTextEditorDecorationType({
|
||||
color: '#43A047', // зелёный для скобок
|
||||
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
|
||||
fontWeight: 'bold'
|
||||
});
|
||||
const variableDecoration = vscode.window.createTextEditorDecorationType({
|
||||
color: '#FF9800', // оранжевый для имени переменной
|
||||
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
|
||||
fontWeight: 'bold'
|
||||
});
|
||||
const modifierDecoration = vscode.window.createTextEditorDecorationType({
|
||||
color: '#00ACC1', // бирюзовый для модификатора
|
||||
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
|
||||
fontWeight: 'bold'
|
||||
});
|
||||
|
||||
function isInTemplatesDir(filePath: string, templatesDir: string): boolean {
|
||||
const rel = path.relative(templatesDir, filePath);
|
||||
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
||||
}
|
||||
|
||||
function updateTemplateDecorations(editor: vscode.TextEditor) {
|
||||
if (!editor) return;
|
||||
const config = readConfig();
|
||||
const templatesPath = config.templatesPath || 'templates';
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) return;
|
||||
const templatesDir = path.join(workspaceFolders[0].uri.fsPath, templatesPath);
|
||||
if (!isInTemplatesDir(editor.document.uri.fsPath, templatesDir)) return;
|
||||
const brackets: vscode.DecorationOptions[] = [];
|
||||
const variables: vscode.DecorationOptions[] = [];
|
||||
const modifiers: vscode.DecorationOptions[] = [];
|
||||
for (let lineNum = 0; lineNum < editor.document.lineCount; lineNum++) {
|
||||
const line = editor.document.lineAt(lineNum).text;
|
||||
// Ищем все {{variable.modifier}} или {{variable}}
|
||||
const reg = /{{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_]+))?\s*}}/g;
|
||||
let match;
|
||||
while ((match = reg.exec(line)) !== null) {
|
||||
const start = match.index;
|
||||
const end = start + match[0].length;
|
||||
// Скобки {{ и }}
|
||||
brackets.push({
|
||||
range: new vscode.Range(lineNum, start, lineNum, start + 2)
|
||||
});
|
||||
brackets.push({
|
||||
range: new vscode.Range(lineNum, end - 2, lineNum, end)
|
||||
});
|
||||
// Имя переменной
|
||||
const varStart = start + 2 + line.slice(start + 2).search(/\S/); // после {{
|
||||
variables.push({
|
||||
range: new vscode.Range(lineNum, varStart, lineNum, varStart + match[1].length)
|
||||
});
|
||||
// Модификатор (если есть)
|
||||
if (match[2]) {
|
||||
const modStart = varStart + match[1].length + 1; // +1 за точку
|
||||
modifiers.push({
|
||||
range: new vscode.Range(lineNum, modStart, lineNum, modStart + match[2].length)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.setDecorations(bracketDecoration, brackets);
|
||||
editor.setDecorations(variableDecoration, variables);
|
||||
editor.setDecorations(modifierDecoration, modifiers);
|
||||
}
|
||||
|
||||
export function registerTemplateDecorations(context: vscode.ExtensionContext) {
|
||||
function decorateActiveEditor() {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor) {
|
||||
updateTemplateDecorations(editor);
|
||||
}
|
||||
}
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
if (editor) decorateActiveEditor();
|
||||
}),
|
||||
vscode.workspace.onDidChangeTextDocument(event => {
|
||||
const editor = vscode.window.visibleTextEditors.find(e => e.document === event.document);
|
||||
if (editor) updateTemplateDecorations(editor);
|
||||
})
|
||||
);
|
||||
// Инициализация при активации
|
||||
setTimeout(() => {
|
||||
vscode.window.visibleTextEditors.forEach(editor => updateTemplateDecorations(editor));
|
||||
}, 300);
|
||||
}
|
||||
|
||||
export function decorateActiveEditor() {
|
||||
// Логика декорирования активного редактора
|
||||
// ...
|
||||
}
|
||||
|
||||
export function clearDiagnosticsForEditor(editor: vscode.TextEditor, templatesDir: string) {
|
||||
// Очистка диагностик для редактора
|
||||
// ...
|
||||
}
|
||||
|
||||
export function clearDiagnosticsForTemplates(context: vscode.ExtensionContext) {
|
||||
// Очистка диагностик для всех шаблонов
|
||||
// ...
|
||||
}
|
||||
68
src/vscode/semanticHighlight.ts
Normal file
68
src/vscode/semanticHighlight.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// Семантическая подсветка шаблонов
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { getAllTemplateVariables } from '../core/templateUtils';
|
||||
import { readConfig } from '../core/config';
|
||||
|
||||
function isInTemplatesDir(filePath: string, templatesDir: string): boolean {
|
||||
const rel = path.relative(templatesDir, filePath);
|
||||
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
||||
}
|
||||
|
||||
export function registerTemplateSemanticHighlight(context: vscode.ExtensionContext) {
|
||||
const legend = new vscode.SemanticTokensLegend(['bracket', 'variable', 'modifier']);
|
||||
const disposable = vscode.languages.registerDocumentSemanticTokensProvider(
|
||||
{ pattern: '**' }, // теперь на все файлы
|
||||
{
|
||||
provideDocumentSemanticTokens(document: any) {
|
||||
const config = readConfig();
|
||||
const templatesPath = config.templatesPath || 'templates';
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) return;
|
||||
const templatesDir = path.join(workspaceFolders[0].uri.fsPath, templatesPath);
|
||||
console.log('[DEBUG] semantic tokens called for', document.uri.fsPath);
|
||||
console.log('[DEBUG] Проверка шаблонной папки:', document.uri.fsPath, templatesDir, isInTemplatesDir(document.uri.fsPath, templatesDir));
|
||||
// Проверяем, что файл в папке шаблонов
|
||||
if (!isInTemplatesDir(document.uri.fsPath, templatesDir)) {
|
||||
return;
|
||||
}
|
||||
const tokens: number[] = [];
|
||||
for (let lineNum = 0; lineNum < document.lineCount; lineNum++) {
|
||||
const line = document.lineAt(lineNum).text;
|
||||
// Ищем все {{variable.modifier}} или {{variable}} или {{variable.}}
|
||||
const reg = /({{)|(}})|{{\s*([a-zA-Z0-9_]+)(?:\.(\w*))?\s*}}/g;
|
||||
let match;
|
||||
while ((match = reg.exec(line)) !== null) {
|
||||
if (match[1]) {
|
||||
// {{
|
||||
tokens.push(lineNum, match.index, 2, 0, 0); // bracket
|
||||
} else if (match[2]) {
|
||||
// }}
|
||||
tokens.push(lineNum, match.index, 2, 0, 0); // bracket
|
||||
} else if (match[3]) {
|
||||
// variable (имя)
|
||||
const varStart = match.index + 2 + line.slice(match.index + 2).search(/\S/); // после {{
|
||||
tokens.push(lineNum, varStart, match[3].length, 1, 0); // variable
|
||||
if (typeof match[4] === 'string') {
|
||||
// Если есть точка, но модификатор не введён ({{name.}})
|
||||
if (match[4] === '') {
|
||||
// Подсвечиваем только точку как variable
|
||||
const dotStart = varStart + match[3].length;
|
||||
tokens.push(lineNum, dotStart, 1, 1, 0); // variable (точка)
|
||||
} else if (match[4]) {
|
||||
// .modifier
|
||||
const modStart = varStart + match[3].length + 1; // +1 за точку
|
||||
tokens.push(lineNum, modStart, match[4].length, 2, 0); // modifier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new vscode.SemanticTokens(new Uint32Array(tokens));
|
||||
}
|
||||
},
|
||||
legend
|
||||
);
|
||||
context.subscriptions.push(disposable);
|
||||
return disposable;
|
||||
}
|
||||
139
src/webview/configWebview.ts
Normal file
139
src/webview/configWebview.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
// Webview для конфигурации расширения
|
||||
import * as vscode from 'vscode';
|
||||
import { MyTemplateGeneratorConfig, readConfig, writeConfig } from '../core/config';
|
||||
|
||||
const LOCALIZATION: Record<'ru'|'en', {
|
||||
title: string;
|
||||
templatesPath: string;
|
||||
overwriteFiles: string;
|
||||
inputMode: string;
|
||||
inputBox: string;
|
||||
webview: string;
|
||||
language: string;
|
||||
save: string;
|
||||
russian: string;
|
||||
english: string;
|
||||
}> = {
|
||||
ru: {
|
||||
title: 'Настройки генератора шаблонов',
|
||||
templatesPath: 'Путь к шаблонам:',
|
||||
overwriteFiles: 'Перезаписывать файлы',
|
||||
inputMode: 'Режим ввода:',
|
||||
inputBox: 'InputBox',
|
||||
webview: 'Webview',
|
||||
language: 'Язык:',
|
||||
save: 'Сохранить',
|
||||
russian: 'Русский',
|
||||
english: 'English',
|
||||
},
|
||||
en: {
|
||||
title: 'Template Generator Settings',
|
||||
templatesPath: 'Templates path:',
|
||||
overwriteFiles: 'Overwrite files',
|
||||
inputMode: 'Input mode:',
|
||||
inputBox: 'InputBox',
|
||||
webview: 'Webview',
|
||||
language: 'Language:',
|
||||
save: 'Save',
|
||||
russian: 'Russian',
|
||||
english: 'English',
|
||||
}
|
||||
};
|
||||
|
||||
export async function showConfigWebview(context: vscode.ExtensionContext) {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'myTemplateGeneratorConfig',
|
||||
'Настройки MyTemplateGenerator',
|
||||
vscode.ViewColumn.One,
|
||||
{ enableScripts: true }
|
||||
);
|
||||
let config = readConfig();
|
||||
// Получаем URI для стилей
|
||||
const stylePath = vscode.Uri.joinPath(context.extensionUri, 'src', 'webview', 'styles.css');
|
||||
const styleUri = panel.webview.asWebviewUri(stylePath);
|
||||
setHtml((config.language === 'en' ? 'en' : 'ru'));
|
||||
|
||||
function setHtml(language: 'ru'|'en') {
|
||||
panel.webview.html = getHtml(language);
|
||||
}
|
||||
|
||||
function getHtml(language: 'ru'|'en'): string {
|
||||
const dict = LOCALIZATION[language] || LOCALIZATION['ru'];
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="${language}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${panel.webview.cspSource}; script-src 'unsafe-inline';">
|
||||
<title>${dict.title}</title>
|
||||
<link rel="stylesheet" href="${styleUri}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="config-container">
|
||||
<h2>${dict.title}</h2>
|
||||
<form id="configForm">
|
||||
<div class="form-group">
|
||||
<label>${dict.templatesPath}
|
||||
<input name="templatesPath" value="${config.templatesPath}" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" name="overwriteFiles" ${config.overwriteFiles ? 'checked' : ''}/>
|
||||
${dict.overwriteFiles}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${dict.inputMode}
|
||||
<select name="inputMode">
|
||||
<option value="inputBox" ${config.inputMode === 'inputBox' ? 'selected' : ''}>${dict.inputBox}</option>
|
||||
<option value="webview" ${config.inputMode === 'webview' ? 'selected' : ''}>${dict.webview}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${dict.language}
|
||||
<select name="language" id="languageSelect">
|
||||
<option value="ru" ${language === 'ru' ? 'selected' : ''}>${dict.russian}</option>
|
||||
<option value="en" ${language === 'en' ? 'selected' : ''}>${dict.english}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn">${dict.save}</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
document.getElementById('configForm').onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
const data = Object.fromEntries(new FormData(this));
|
||||
data.overwriteFiles = !!this.overwriteFiles.checked;
|
||||
vscode.postMessage({ type: 'save', data });
|
||||
};
|
||||
document.getElementById('languageSelect').onchange = function(e) {
|
||||
vscode.postMessage({ type: 'setLanguage', language: this.value });
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
panel.webview.onDidReceiveMessage(
|
||||
msg => {
|
||||
if (msg.type === 'save') {
|
||||
writeConfig(msg.data);
|
||||
vscode.window.showInformationMessage('Настройки сохранены!');
|
||||
panel.dispose();
|
||||
}
|
||||
if (msg.type === 'setLanguage') {
|
||||
// Сохраняем язык в конфиг и перерисовываем webview
|
||||
config.language = msg.language;
|
||||
writeConfig(config);
|
||||
setHtml(msg.language === 'en' ? 'en' : 'ru');
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
);
|
||||
}
|
||||
124
src/webview/styles.css
Normal file
124
src/webview/styles.css
Normal file
@@ -0,0 +1,124 @@
|
||||
:root {
|
||||
--bg: #f7f7fa;
|
||||
--panel-bg: #fff;
|
||||
--text: #222;
|
||||
--label: #555;
|
||||
--input-bg: #f0f0f3;
|
||||
--input-border: #d0d0d7;
|
||||
--input-focus: #1976d2;
|
||||
--button-bg: #1976d2;
|
||||
--button-text: #fff;
|
||||
--button-hover: #1565c0;
|
||||
--border-radius: 8px;
|
||||
--shadow: 0 2px 12px rgba(0,0,0,0.07);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #181a1b;
|
||||
--panel-bg: #23272e;
|
||||
--text: #f3f3f3;
|
||||
--label: #b0b0b0;
|
||||
--input-bg: #23272e;
|
||||
--input-border: #33363b;
|
||||
--input-focus: #90caf9;
|
||||
--button-bg: #1976d2;
|
||||
--button-text: #fff;
|
||||
--button-hover: #1565c0;
|
||||
--border-radius: 8px;
|
||||
--shadow: 0 2px 12px rgba(0,0,0,0.25);
|
||||
}
|
||||
}
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Segoe UI', 'Roboto', Arial, sans-serif;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.create-container, .config-container {
|
||||
max-width: 420px;
|
||||
margin: 48px auto;
|
||||
background: var(--panel-bg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 32px 36px 28px 36px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.head-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.create-container h2, .config-container h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
label {
|
||||
color: var(--label);
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
input, select {
|
||||
background: var(--input-bg);
|
||||
color: var(--text);
|
||||
border: 1.5px solid var(--input-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 8px 10px;
|
||||
font-size: 1em;
|
||||
transition: border 0.2s, box-shadow 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
input:focus, select:focus {
|
||||
border-color: var(--input-focus);
|
||||
box-shadow: 0 0 0 2px var(--input-focus)33;
|
||||
}
|
||||
button, .btn {
|
||||
margin-top: 10px;
|
||||
background: var(--button-bg);
|
||||
color: var(--button-text);
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 10px 15px;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, box-shadow 0.2s;
|
||||
box-shadow: 0 1px 4px rgba(25, 118, 210, 0.08);
|
||||
}
|
||||
button:hover, button:focus, .btn:hover, .btn:focus {
|
||||
background: var(--button-hover);
|
||||
}
|
||||
.destination {
|
||||
margin-bottom: 16px;
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
}
|
||||
.lang-select {
|
||||
display: block;
|
||||
}
|
||||
.var-hint {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.template-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#configForm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
192
src/webview/templateVarsWebview.ts
Normal file
192
src/webview/templateVarsWebview.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
// Webview для выбора шаблона и переменных
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { getAllTemplateVariables } from '../core/templateUtils';
|
||||
import { I18N_DICTIONARIES } from '../core/i18n';
|
||||
import { writeConfig, readConfig } from '../core/config';
|
||||
|
||||
export async function showTemplateAndVarsWebview(
|
||||
context: vscode.ExtensionContext,
|
||||
templatesDir: string,
|
||||
targetPath: string,
|
||||
initialLanguage: string
|
||||
): Promise<{ template: string, vars: Record<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());
|
||||
const stylePath = vscode.Uri.joinPath(context.extensionUri, 'src', 'webview', 'styles.css');
|
||||
return new Promise((resolve) => {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'templateVars',
|
||||
getDict().create,
|
||||
vscode.ViewColumn.Active,
|
||||
{ enableScripts: true }
|
||||
);
|
||||
const styleUri = panel.webview.asWebviewUri(stylePath);
|
||||
let currentVars: string[] = [];
|
||||
let currentTemplate = templates[0] || '';
|
||||
let disposed = false;
|
||||
function getVarsHtml(vars: string[], values: Record<string, string> = {}) {
|
||||
const dict = getDict();
|
||||
if (!vars.length) return '';
|
||||
return `<h3>${dict.enterVariables}</h3>
|
||||
<div class="var-hint">${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" class="btn">${dict.create}</button>
|
||||
</form>`;
|
||||
}
|
||||
function getTemplatesRadioHtml(templates: string[], selected: string) {
|
||||
const dict = getDict();
|
||||
return `<form id="templateForm">
|
||||
<h3>${dict.chooseTemplate}:</h3>
|
||||
<div class="template-list">
|
||||
${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 class="lang-select">
|
||||
<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'; style-src ${panel.webview.cspSource}; script-src 'unsafe-inline';">
|
||||
<title>${dict.create}</title>
|
||||
<link rel="stylesheet" href="${styleUri}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="create-container">
|
||||
<div class="head-wrap">
|
||||
<h2>${dict.create}</h2>
|
||||
${getLanguageSelectorHtml(language)}
|
||||
</div>
|
||||
<div id="templatesBlock">
|
||||
${templatesHtml}
|
||||
</div>
|
||||
<div id="varsBlock">
|
||||
${varsHtml}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(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) => {
|
||||
e.preventDefault();
|
||||
const data = {};
|
||||
Array.from(varsForm.elements).forEach(el => {
|
||||
if (el.name) data[el.name] = el.value;
|
||||
});
|
||||
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) => {
|
||||
vscode.postMessage({ type: 'setLanguage', language: e.target.value, template: document.querySelector('input[name="templateRadio"]:checked')?.value || '' });
|
||||
});
|
||||
}
|
||||
}
|
||||
window.initHandlers = initHandlers;
|
||||
document.addEventListener('DOMContentLoaded', initHandlers);
|
||||
})();
|
||||
</script>
|
||||
</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));
|
||||
// Обработка сообщений
|
||||
panel.webview.onDidReceiveMessage(
|
||||
async message => {
|
||||
if (message.type === 'selectTemplate') {
|
||||
currentTemplate = message.template;
|
||||
if (message.language) language = message.language;
|
||||
if (!currentTemplate) {
|
||||
setHtml(getTemplatesRadioHtml(templates, ''), '');
|
||||
return;
|
||||
}
|
||||
// Получаем переменные для выбранного шаблона
|
||||
const templateDir = path.join(templatesDir, currentTemplate);
|
||||
const allVars = getAllTemplateVariables(templateDir);
|
||||
currentVars = Array.from(allVars);
|
||||
setHtml(getTemplatesRadioHtml(templates, currentTemplate), getVarsHtml(currentVars));
|
||||
} else if (message.type === 'setLanguage') {
|
||||
if (message.language) language = message.language;
|
||||
// Сохраняем язык в конфиг
|
||||
const oldConfig = readConfig();
|
||||
writeConfig({ ...oldConfig, language });
|
||||
currentTemplate = message.template || templates[0] || '';
|
||||
// Получаем переменные для выбранного шаблона
|
||||
let baseVars: string[] = [];
|
||||
if (currentTemplate) {
|
||||
const templateDir = path.join(templatesDir, currentTemplate);
|
||||
const allVars = getAllTemplateVariables(templateDir);
|
||||
baseVars = Array.from(allVars);
|
||||
currentVars = baseVars;
|
||||
}
|
||||
setHtml(getTemplatesRadioHtml(templates, currentTemplate), getVarsHtml(currentVars));
|
||||
} else if (message.type === 'changeLanguage') {
|
||||
// legacy, не нужен
|
||||
} else if (message.type === 'submit') {
|
||||
if (message.language) language = message.language;
|
||||
if (!disposed) {
|
||||
disposed = true;
|
||||
panel.dispose();
|
||||
resolve({ template: message.template, vars: message.data });
|
||||
}
|
||||
} else if (message.type === 'callInitHandlers') {
|
||||
// Ничего не делаем, скрипт внутри webview вызовет window.initHandlers
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
);
|
||||
panel.onDidDispose(() => {
|
||||
if (!disposed) {
|
||||
disposed = true;
|
||||
resolve(undefined);
|
||||
}
|
||||
}, null, context.subscriptions);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user