sql writer
Some checks are pending
CI / Test (1.23) (push) Waiting to run
CI / Test (1.24) (push) Waiting to run
CI / Test (1.25) (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Build (push) Waiting to run

This commit is contained in:
2025-12-17 20:44:02 +02:00
parent 40bc0be1cb
commit 5e1448dcdb
48 changed files with 4592 additions and 950 deletions

View File

@@ -0,0 +1,438 @@
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
// Extension activation
export function activate(context: vscode.ExtensionContext) {
console.log('RelSpec Template Editor activated');
// Register commands
context.subscriptions.push(
vscode.commands.registerCommand('relspec.previewTemplate', previewTemplate)
);
context.subscriptions.push(
vscode.commands.registerCommand('relspec.validateTemplate', validateTemplate)
);
context.subscriptions.push(
vscode.commands.registerCommand('relspec.newTemplate', newTemplate)
);
context.subscriptions.push(
vscode.commands.registerCommand('relspec.listFunctions', listFunctions)
);
// Register completion provider
const completionProvider = vscode.languages.registerCompletionItemProvider(
'gotmpl',
new TemplateCompletionProvider(),
'{{', '.'
);
context.subscriptions.push(completionProvider);
// Register hover provider
const hoverProvider = vscode.languages.registerHoverProvider(
'gotmpl',
new TemplateHoverProvider()
);
context.subscriptions.push(hoverProvider);
// Auto-validate on save
context.subscriptions.push(
vscode.workspace.onDidSaveTextDocument((document) => {
const config = vscode.workspace.getConfiguration('relspec');
if (config.get('autoValidate') && document.languageId === 'gotmpl') {
validateTemplate();
}
})
);
}
// Preview template with sample data
async function previewTemplate() {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.languageId !== 'gotmpl') {
vscode.window.showErrorMessage('Please open a .tmpl file');
return;
}
const panel = vscode.window.createWebviewPanel(
'templatePreview',
'Template Preview',
vscode.ViewColumn.Beside,
{
enableScripts: true
}
);
const templateContent = editor.document.getText();
const config = vscode.workspace.getConfiguration('relspec');
const sampleDataString = config.get<string>('previewSampleData', '{}');
try {
const sampleData = JSON.parse(sampleDataString);
const preview = await renderTemplate(templateContent, sampleData);
panel.webview.html = getWebviewContent(templateContent, preview, sampleData);
} catch (error) {
panel.webview.html = getErrorWebviewContent(String(error));
}
}
// Validate template syntax
async function validateTemplate() {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.languageId !== 'gotmpl') {
return;
}
const templateContent = editor.document.getText();
const diagnostics: vscode.Diagnostic[] = [];
// Check for common template errors
const errors = checkTemplateSyntax(templateContent);
for (const error of errors) {
const range = new vscode.Range(
error.line,
error.column,
error.line,
error.column + error.length
);
const diagnostic = new vscode.Diagnostic(
range,
error.message,
vscode.DiagnosticSeverity.Error
);
diagnostics.push(diagnostic);
}
// Update diagnostics
const collection = vscode.languages.createDiagnosticCollection('relspec');
collection.set(editor.document.uri, diagnostics);
if (diagnostics.length === 0) {
vscode.window.showInformationMessage('Template is valid');
}
}
// Create new template from scaffold
async function newTemplate() {
const templateType = await vscode.window.showQuickPick(
[
{ label: 'DDL Template', value: 'ddl' },
{ label: 'Constraint Template', value: 'constraint' },
{ label: 'Index Template', value: 'index' },
{ label: 'Audit Template', value: 'audit' },
{ label: 'Custom Fragment', value: 'fragment' }
],
{ placeHolder: 'Select template type' }
);
if (!templateType) {
return;
}
const templateName = await vscode.window.showInputBox({
prompt: 'Enter template name',
placeHolder: 'my_template'
});
if (!templateName) {
return;
}
const scaffold = getTemplateScaffold(templateType.value, templateName);
const config = vscode.workspace.getConfiguration('relspec');
const templatePath = config.get<string>('templatePath', 'pkg/writers/pgsql/templates');
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders) {
vscode.window.showErrorMessage('No workspace folder open');
return;
}
const filePath = path.join(workspaceFolders[0].uri.fsPath, templatePath, `${templateName}.tmpl`);
fs.writeFileSync(filePath, scaffold);
const document = await vscode.workspace.openTextDocument(filePath);
await vscode.window.showTextDocument(document);
}
// List available template functions
async function listFunctions() {
const functions = getTemplateFunctions();
const panel = vscode.window.createWebviewPanel(
'functionsList',
'RelSpec Template Functions',
vscode.ViewColumn.Beside,
{}
);
panel.webview.html = getFunctionsWebviewContent(functions);
}
// Completion provider for template functions and keywords
class TemplateCompletionProvider implements vscode.CompletionItemProvider {
provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position
): vscode.CompletionItem[] {
const linePrefix = document.lineAt(position).text.substr(0, position.character);
if (!linePrefix.endsWith('{{')) {
return [];
}
const functions = getTemplateFunctions();
const completionItems: vscode.CompletionItem[] = [];
// Add function completions
for (const func of functions) {
const item = new vscode.CompletionItem(func.name, vscode.CompletionItemKind.Function);
item.detail = func.signature;
item.documentation = new vscode.MarkdownString(func.description);
item.insertText = new vscode.SnippetString(`${func.name} \${1:arg}}}`)
;
completionItems.push(item);
}
// Add keyword completions
const keywords = ['if', 'else', 'end', 'range', 'with', 'define', 'template', 'block'];
for (const keyword of keywords) {
const item = new vscode.CompletionItem(keyword, vscode.CompletionItemKind.Keyword);
completionItems.push(item);
}
return completionItems;
}
}
// Hover provider for template functions
class TemplateHoverProvider implements vscode.HoverProvider {
provideHover(
document: vscode.TextDocument,
position: vscode.Position
): vscode.Hover | undefined {
const range = document.getWordRangeAtPosition(position);
if (!range) {
return undefined;
}
const word = document.getText(range);
const functions = getTemplateFunctions();
const func = functions.find(f => f.name === word);
if (func) {
const markdown = new vscode.MarkdownString();
markdown.appendCodeblock(func.signature, 'go');
markdown.appendMarkdown('\n\n' + func.description);
markdown.appendMarkdown('\n\n**Example:**\n');
markdown.appendCodeblock(func.example, 'gotmpl');
return new vscode.Hover(markdown);
}
return undefined;
}
}
// Helper functions
function renderTemplate(template: string, data: any): Promise<string> {
// In a real implementation, this would call the Go binary
// For now, return a placeholder
return Promise.resolve(`-- Rendered SQL would appear here\n-- Template: ${template.substring(0, 50)}...`);
}
function checkTemplateSyntax(template: string): Array<{ line: number; column: number; length: number; message: string }> {
const errors: Array<{ line: number; column: number; length: number; message: string }> = [];
// Basic syntax checking
const lines = template.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for unclosed {{
const openCount = (line.match(/\{\{/g) || []).length;
const closeCount = (line.match(/\}\}/g) || []).length;
if (openCount > closeCount) {
errors.push({
line: i,
column: line.indexOf('{{'),
length: 2,
message: 'Unclosed template tag'
});
}
}
return errors;
}
function getTemplateScaffold(type: string, name: string): string {
const scaffolds: Record<string, string> = {
ddl: `{{/* ${name} - DDL operation template */}}
{{- define "${name}" -}}
ALTER TABLE {{.SchemaName}}.{{.TableName}}
-- Add your DDL operation here
{{- end -}}`,
constraint: `{{/* ${name} - Constraint template */}}
{{- define "${name}" -}}
ALTER TABLE {{.SchemaName}}.{{.TableName}}
ADD CONSTRAINT {{.ConstraintName}}
-- Add constraint definition here
{{- end -}}`,
index: `{{/* ${name} - Index template */}}
{{- define "${name}" -}}
CREATE {{if .Unique}}UNIQUE {{end}}INDEX IF NOT EXISTS {{.IndexName}}
ON {{.SchemaName}}.{{.TableName}}
USING {{.IndexType}} ({{join .Columns ", "}});
{{- end -}}`,
audit: `{{/* ${name} - Audit template */}}
{{- define "${name}" -}}
-- Audit configuration for {{.TableName}}
{{- end -}}`,
fragment: `{{/* ${name} - Reusable fragment */}}
{{- define "${name}" -}}
-- Add your reusable SQL fragment here
{{- end -}}`
};
return scaffolds[type] || scaffolds.fragment;
}
function getTemplateFunctions() {
return [
{
name: 'upper',
signature: 'upper(s string) string',
description: 'Convert string to uppercase',
example: '{{upper "hello"}} // HELLO'
},
{
name: 'lower',
signature: 'lower(s string) string',
description: 'Convert string to lowercase',
example: '{{lower "HELLO"}} // hello'
},
{
name: 'snake_case',
signature: 'snake_case(s string) string',
description: 'Convert string to snake_case',
example: '{{snake_case "UserId"}} // user_id'
},
{
name: 'camelCase',
signature: 'camelCase(s string) string',
description: 'Convert string to camelCase',
example: '{{camelCase "user_id"}} // userId'
},
{
name: 'quote',
signature: 'quote(s string) string',
description: 'Quote string for SQL (escapes single quotes)',
example: '{{quote "O\'Brien"}} // \'O\'\'Brien\''
},
{
name: 'safe_identifier',
signature: 'safe_identifier(s string) string',
description: 'Make string safe for SQL identifier',
example: '{{safe_identifier "User-Id"}} // user_id'
},
{
name: 'join',
signature: 'join(slice []string, sep string) string',
description: 'Join string slice with separator',
example: '{{join .Columns ", "}}'
}
];
}
function getWebviewContent(template: string, preview: string, data: any): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Template Preview</title>
<style>
body { font-family: var(--vscode-font-family); padding: 20px; }
.section { margin-bottom: 30px; }
h2 { color: var(--vscode-foreground); border-bottom: 1px solid var(--vscode-panel-border); }
pre { background: var(--vscode-editor-background); padding: 15px; border-radius: 4px; overflow-x: auto; }
.data { color: var(--vscode-editor-foreground); }
.sql { color: var(--vscode-textPreformat-foreground); }
</style>
</head>
<body>
<div class="section">
<h2>Template</h2>
<pre class="data">${escapeHtml(template)}</pre>
</div>
<div class="section">
<h2>Sample Data</h2>
<pre class="data">${escapeHtml(JSON.stringify(data, null, 2))}</pre>
</div>
<div class="section">
<h2>Rendered SQL</h2>
<pre class="sql">${escapeHtml(preview)}</pre>
</div>
</body>
</html>`;
}
function getErrorWebviewContent(error: string): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Error</title>
<style>
body { font-family: var(--vscode-font-family); padding: 20px; }
.error { color: var(--vscode-errorForeground); }
</style>
</head>
<body>
<h2 class="error">Template Error</h2>
<pre>${escapeHtml(error)}</pre>
</body>
</html>`;
}
function getFunctionsWebviewContent(functions: any[]): string {
const functionsHtml = functions.map(f => `
<div class="function">
<h3>${f.name}</h3>
<code>${f.signature}</code>
<p>${f.description}</p>
<pre>${f.example}</pre>
</div>
`).join('');
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Template Functions</title>
<style>
body { font-family: var(--vscode-font-family); padding: 20px; }
.function { margin-bottom: 30px; border-bottom: 1px solid var(--vscode-panel-border); padding-bottom: 20px; }
h3 { color: var(--vscode-foreground); margin-bottom: 10px; }
code { background: var(--vscode-textCodeBlock-background); padding: 4px 8px; border-radius: 3px; }
pre { background: var(--vscode-editor-background); padding: 15px; border-radius: 4px; }
</style>
</head>
<body>
<h1>Available Template Functions</h1>
${functionsHtml}
</body>
</html>`;
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
export function deactivate() {}