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('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('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 { // 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 = { 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 ` Template Preview

Template

${escapeHtml(template)}

Sample Data

${escapeHtml(JSON.stringify(data, null, 2))}

Rendered SQL

${escapeHtml(preview)}
`; } function getErrorWebviewContent(error: string): string { return ` Error

Template Error

${escapeHtml(error)}
`; } function getFunctionsWebviewContent(functions: any[]): string { const functionsHtml = functions.map(f => `

${f.name}

${f.signature}

${f.description}

${f.example}
`).join(''); return ` Template Functions

Available Template Functions

${functionsHtml} `; } function escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } export function deactivate() {}