sql writer
This commit is contained in:
438
vscode-extension/relspec-template-editor/src/extension.ts
Normal file
438
vscode-extension/relspec-template-editor/src/extension.ts
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export function deactivate() {}
|
||||
Reference in New Issue
Block a user