diff --git a/package.json b/package.json index c16d3e2..5da9f23 100644 --- a/package.json +++ b/package.json @@ -35,27 +35,63 @@ "main": "./dist/extension.js", "contributes": { "configuration": { - "title": "BMC TPL Einstellungen", + "title": "BMC TPL Settings", "properties": { "tpl.metadata.author": { "type": "string", "default": "Autor", - "description": "Standard-Autor für neu generierte TPL-Module." + "description": "Standard-Author for new generated TPL-Modules." }, "tpl.metadata.origin": { "type": "string", "default": "Custom", - "description": "Standard-Origin für neu generierte TPL-Module." + "description": "Standard-Origin for new generated TPL-Modules." }, "tpl.metadata.publisher": { "type": "string", "default": "IT-Operations", - "description": "Standard-Publisher für neu generierte TPL-Module." + "description": "Standard-Publisher ffor new generated TPL-Modules." }, "tpl.metadata.treePathBase": { "type": "string", "default": "Company", - "description": "Erster Teil (Basis) des tree_paths in den Metadaten." + "description": "First Part (Basis) of the tree_paths in the Metadata." + }, + "tpl.metadata.require.origin": { + "type": "boolean", + "default": true, + "description": "Marks 'origin' as Required on module level." + }, + "tpl.metadata.require.author": { + "type": "boolean", + "default": true, + "description": "Marks 'author' as Required on module level." + }, + "tpl.metadata.require.products": { + "type": "boolean", + "default": true, + "description": "Marks 'products' aas Required on module level." + }, + "tpl.metadata.require.publishers": { + "type": "boolean", + "default": true, + "description": "Marks 'publishers' as Required on module level." + }, + "tpl.metadata.require.treePath": { + "type": "boolean", + "default": true, + "description": "Marks 'tree_path' as Required on module level." + }, + "tpl.metadata.treePath.minLevels": { + "type": "number", + "default": 3, + "description": "How many levels should the tree_path in Metadata at least have? (e.g. 3 for 'Base', 'Category', 'Subcategory')." + }, + "tpl.metadata.treePath.staticNodes": { + "type": "array", + "items": { "type": "string" }, + "default": ["default"], + "description": "Fixed definitions for the tree_path left to right (e.g. ['Company', 'Software']). The rest of the levels are free to choose." }, "tpl.version": { "type": "number", diff --git a/src/core/LinterContext.ts b/src/core/LinterContext.ts index e5008df..302ba48 100644 --- a/src/core/LinterContext.ts +++ b/src/core/LinterContext.ts @@ -29,8 +29,8 @@ export class LinterContext { public inModuleMetadata = false; public hasModuleMetadata = false; public moduleMetadataBlockLine = -1; - public foundModuleMetadata = { origin: false, author: false, products: false, publishers: false, tree_path: false }; - + public foundModuleMetadata = new Set(); + // Klammer-Tracking public parenStack: number[] = []; public bracketStack: number[] = []; diff --git a/src/core/TplLinter.ts b/src/core/TplLinter.ts index 0023c0a..90ec625 100644 --- a/src/core/TplLinter.ts +++ b/src/core/TplLinter.ts @@ -1,36 +1,27 @@ import * as vscode from 'vscode'; import { LinterContext } from './LinterContext'; -import { LinterRule } from './rules/LinterRule'; import { SyntaxRule } from './rules/SyntaxRule'; import { BlockRule } from './rules/BlockRule'; +import { MetadataRule } from './rules/MetadataRule'; +import { IndentationRule } from './rules/IndentationRule'; // 1. Hier importieren + +// ... (deine eventuellen anderen Hilfs-Imports wie trackStrings) ... export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) { - if (document.languageId !== 'tpl') { return; } + if (document.languageId !== 'tpl') return; - const ctx = new LinterContext(); - const config = vscode.workspace.getConfiguration('tpl'); - ctx.configuredTplVersion = config.get('version', 23); - const text = document.getText(); const lines = text.split('\n'); + const ctx = new LinterContext(); - // 1. Version prüfen - const versionMatch = text.match(/tpl\s+1\.(\d+)\s+module/); - if (versionMatch) { - ctx.tplVersion = parseInt(versionMatch[1], 10); - if (ctx.tplVersion < ctx.configuredTplVersion) { - const linesToSearch = text.substring(0, versionMatch.index! + 30).split('\n'); - ctx.addDiag(linesToSearch.length - 1, 0, versionMatch[0].length, `Das Modul nutzt eine alte TPL Version (1.${ctx.tplVersion}). Aktuell in den Settings definiert ist 1.${ctx.configuredTplVersion}.`, vscode.DiagnosticSeverity.Warning); - } - } - - // 2. Regeln laden - const rules: LinterRule[] = [ + // 2. Hier nur die 3 existierenden Regeln reinlegen (MetadataRule entfernt!) + const rules = [ new SyntaxRule(), - new BlockRule() + new MetadataRule(), + new BlockRule(), + new IndentationRule() ]; - // 3. Zeilenweise iterieren for (let i = 0; i < lines.length; i++) { const rawLine = lines[i]; const line = rawLine.trim(); @@ -54,18 +45,14 @@ export function validateTplDocument(document: vscode.TextDocument, collection: v // Regeln anwenden for (const rule of rules) { - rule.checkLine(line, rawLine, i, ctx, lines); // Hier 'lines' ergänzen! + rule.checkLine(line, rawLine, i, ctx, lines); } } - // 4. Abschluss-Prüfungen durchführen for (const rule of rules) { - if (rule.checkEnd) { rule.checkEnd(lines, ctx); - } } - // Diagnostics an VS Code übergeben collection.set(document.uri, ctx.diagnostics); } diff --git a/src/core/rules/BlockRule.ts b/src/core/rules/BlockRule.ts index b1041a2..f9d4be3 100644 --- a/src/core/rules/BlockRule.ts +++ b/src/core/rules/BlockRule.ts @@ -10,16 +10,9 @@ export class BlockRule implements LinterRule { else { ctx.addDiag(lineIdx, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module ;' beginnen.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; } } - if (line.startsWith('metadata') && ctx.inBlock === 'none') { - ctx.inModuleMetadata = true; - ctx.hasModuleMetadata = true; - ctx.moduleMetadataBlockLine = lineIdx; - return; - } - - if (ctx.inModuleMetadata) { - this.trackModuleMetadata(line, lineIdx, ctx); - return; + // NEU: Wenn wir im Metadata-Block sind, ignorieren wir den Rest. Die MetadataRule übernimmt! + if (ctx.inModuleMetadata || (line.startsWith('metadata') && ctx.inBlock === 'none')) { + return; } if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) { @@ -29,8 +22,6 @@ export class BlockRule implements LinterRule { } this.handleBlockOpenings(line, rawLine, lineIdx, ctx); - - // NEU: if, for und while tracken this.handleControlBlocks(line, rawLine, lineIdx, ctx); if (ctx.inBlock === 'pattern') { @@ -40,16 +31,6 @@ export class BlockRule implements LinterRule { this.checkEndBlocks(line, rawLine, lineIdx, ctx); } - public checkEnd(lines: string[], ctx: LinterContext): void { - for (const unclosedLine of ctx.openDefines) { - ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' wurde nicht mit 'end define;' geschlossen!", vscode.DiagnosticSeverity.Error); - } - // NEU: Offene if/for/while Blöcke am Dateiende ankreiden - for (const block of ctx.openControlBlocks) { - ctx.addDiag(block.lineIdx, 0, lines[block.lineIdx].length, `Block '${block.type}' wurde nicht mit 'end ${block.type};' geschlossen!`, vscode.DiagnosticSeverity.Error); - } - } - private handleControlBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { // Block wird geöffnet if (/^if\b/.test(line)) { ctx.openControlBlocks.push({ type: 'if', lineIdx }); } @@ -193,4 +174,8 @@ export class BlockRule implements LinterRule { else { ctx.addDiag(lineIdx, 0, line.length, "'end define' gefunden, ohne offenes 'define'.", vscode.DiagnosticSeverity.Error); } } } + + public checkEnd(lines: string[], ctx: LinterContext): void { + // empty + } } \ No newline at end of file diff --git a/src/core/rules/IndentationRule.ts b/src/core/rules/IndentationRule.ts new file mode 100644 index 0000000..41b3a1f --- /dev/null +++ b/src/core/rules/IndentationRule.ts @@ -0,0 +1,32 @@ +import * as vscode from 'vscode'; +import { LinterRule } from './LinterRule'; +import { LinterContext } from '../LinterContext'; + +export class IndentationRule implements LinterRule { + + public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void { + // Leere Zeilen oder Zeilen, die nur aus Leerzeichen/Tabs bestehen, ignorieren + if (rawLine.trim().length === 0) return; + + // Wenn Tabs drin sind, meckert die SyntaxRule bereits. Das ignorieren wir hier. + if (rawLine.includes('\t')) return; + + const leadingSpacesMatch = rawLine.match(/^ +/); + const spaceCount = leadingSpacesMatch ? leadingSpacesMatch[0].length : 0; + + // Ist die Anzahl der Leerzeichen kein Vielfaches von 4? + if (spaceCount > 0 && spaceCount % 4 !== 0) { + ctx.addDiag( + lineIdx, + 0, + spaceCount, // Markiert genau den Bereich der Leerzeichen + `Unsaubere Einrückung (${spaceCount} Leerzeichen). Nutze ein Vielfaches von 4. (Tipp: Shift+Alt+F zum Auto-Formatieren)`, + vscode.DiagnosticSeverity.Warning // Warning reicht hier völlig, Error wäre beim Tippen zu aggressiv + ); + } + } + + public checkEnd(lines: string[], ctx: LinterContext): void { + // Hier muss am Ende der Datei nichts geprüft werden + } +} \ No newline at end of file diff --git a/src/core/rules/MetadataRule.ts b/src/core/rules/MetadataRule.ts new file mode 100644 index 0000000..ae12df7 --- /dev/null +++ b/src/core/rules/MetadataRule.ts @@ -0,0 +1,80 @@ +import * as vscode from 'vscode'; +import { LinterRule } from './LinterRule'; +import { LinterContext } from '../LinterContext'; + +export class MetadataRule implements LinterRule { + public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void { + if (line.startsWith('metadata') && ctx.inBlock === 'none') { + ctx.inModuleMetadata = true; + ctx.hasModuleMetadata = true; + ctx.moduleMetadataBlockLine = lineIdx; + ctx.foundModuleMetadata.clear(); + return; + } + + if (ctx.inModuleMetadata) { + const config = vscode.workspace.getConfiguration('tpl'); + + // Felder erfassen + if (line.match(/^origin\b/)) ctx.foundModuleMetadata.add('origin'); + if (line.match(/^author\b/)) ctx.foundModuleMetadata.add('author'); + if (line.match(/^products\b/)) ctx.foundModuleMetadata.add('products'); + if (line.match(/^publishers\b/)) ctx.foundModuleMetadata.add('publishers'); + + // Tree-Path Sonderprüfung + if (line.match(/^tree_path\b/)) { + ctx.foundModuleMetadata.add('tree_path'); + if (config.get('metadata.require.treePath', true)) { + this.checkTreePath(line, rawLine, lineIdx, ctx, config); + } + } + + // Block-Ende: Pflichtfelder abgleichen + if (line.startsWith('end metadata')) { + ctx.inModuleMetadata = false; + this.checkRequiredFields(ctx, config); + } + } + } + + private checkTreePath(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, config: vscode.WorkspaceConfiguration) { + const minLevels = config.get('metadata.treePath.minLevels', 3); + const staticNodes = config.get('metadata.treePath.staticNodes', ['Grafing']); + + // Extrahiert die Strings aus tree_path := 'A', 'B'; + const match = line.match(/tree_path\s*:=\s*(.*);/); + if (match) { + const paths = match[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')); + + if (paths.length < minLevels) { + ctx.addDiag(lineIdx, 0, rawLine.length, `Der tree_path muss mindestens ${minLevels} Ebenen haben!`, vscode.DiagnosticSeverity.Error); + } + + for (let i = 0; i < staticNodes.length; i++) { + if (paths[i] !== staticNodes[i]) { + ctx.addDiag(lineIdx, 0, rawLine.length, `Die tree_path Ebene ${i + 1} muss zwingend '${staticNodes[i]}' lauten!`, vscode.DiagnosticSeverity.Error); + } + } + } + } + + private checkRequiredFields(ctx: LinterContext, config: vscode.WorkspaceConfiguration) { + const requiredFields: string[] = []; + if (config.get('metadata.require.origin', true)) requiredFields.push('origin'); + if (config.get('metadata.require.author', true)) requiredFields.push('author'); + if (config.get('metadata.require.products', true)) requiredFields.push('products'); + if (config.get('metadata.require.publishers', true)) requiredFields.push('publishers'); + if (config.get('metadata.require.treePath', true)) requiredFields.push('tree_path'); + + const missing = requiredFields.filter(f => !ctx.foundModuleMetadata.has(f)); + + if (missing.length > 0 && ctx.moduleMetadataBlockLine !== -1) { + const diag = ctx.addDiag(ctx.moduleMetadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten (Modul-Ebene): ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error); + diag.code = 'missing_module_metadata'; + } + } + + public checkEnd(lines: string[], ctx: LinterContext): void { + // empty + } +} \ No newline at end of file diff --git a/src/core/rules/SyntaxRule.ts b/src/core/rules/SyntaxRule.ts index 388c734..7e1b0da 100644 --- a/src/core/rules/SyntaxRule.ts +++ b/src/core/rules/SyntaxRule.ts @@ -7,7 +7,6 @@ export class SyntaxRule implements LinterRule { public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void { this.checkTabs(rawLine, lineIdx, ctx); - // TODOs finden (blauer Info-Hinweis) const todoMatch = rawLine.match(/\bTODO\s*:(.*)/i); if (todoMatch) { ctx.addDiag(lineIdx, todoMatch.index!, rawLine.length, `TODO: ${todoMatch[1].trim()}`, vscode.DiagnosticSeverity.Information); @@ -103,8 +102,7 @@ export class SyntaxRule implements LinterRule { // "in" (für for-Schleifen) als gültigen Zeilenumbruch hinzugefügt const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches|in)$/i.test(codePart); - // HIER GEFIXT: Wenn die Zeile auf 'then' oder 'do' endet, verlangt sie kein Semikolon! - const endsWithBlockOpener = /\b(then|do)$/.test(codePart); + ´const endsWithBlockOpener = /\b(then|do)$/.test(codePart); if (!noSemicolonNeeded && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine && !endsWithBlockOpener) { if (codePart.length > 0 && !codePart.endsWith(';') && !codePart.endsWith('{') && !codePart.endsWith(',')) { diff --git a/src/providers/TplHoverProvider.ts b/src/providers/TplHoverProvider.ts index 7f85f18..fa4a06b 100644 --- a/src/providers/TplHoverProvider.ts +++ b/src/providers/TplHoverProvider.ts @@ -1,11 +1,12 @@ import * as vscode from 'vscode'; +import { tplBuiltIns } from '../data/TplBuiltIns'; export class TplHoverProvider implements vscode.HoverProvider { public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/); if (!range) return null; - const word = document.getText(range); // z.B. "modelling.testDefine" + const word = document.getText(range); const text = document.getText(); const lines = text.split('\n'); @@ -13,7 +14,26 @@ export class TplHoverProvider implements vscode.HoverProvider { const importBase = parts[0]; const funcName = parts.length > 1 ? parts[1] : word; - // 1. Lokale Defines + // 1. BMC BUILT-INS DYNAMISCH AUSLESEN + const bmcNamespaces = Object.keys(tplBuiltIns); + + if (bmcNamespaces.includes(importBase)) { + // Wenn die Methode in der Datei existiert, zeige die echte Doku! + if (funcName !== importBase && tplBuiltIns[importBase][funcName]) { + const bmcDoc = tplBuiltIns[importBase][funcName]; + const md = new vscode.MarkdownString(); + + // Wir zeigen den echten Methodennamen als Code-Block + md.appendCodeblock(`${importBase}.${funcName}`, 'tpl'); + md.appendMarkdown(`\n\n---\n\n${bmcDoc.description}`); + + return new vscode.Hover(md); + } + // Fallback, wenn man nur über dem Namespace (z.B. "discovery") hovert + return new vscode.Hover(new vscode.MarkdownString(`**${word}**\n\n*(BMC Built-in Namespace)*`)); + } + + // 2. Lokale Defines if (parts.length === 1 || parts.length === 2) { for (let i = 0; i < lines.length; i++) { const line = lines[i]; @@ -32,13 +52,12 @@ export class TplHoverProvider implements vscode.HoverProvider { } } - // 2. Cross-File Hover für from ... import + // 3. Cross-File Hover für from ... import const fromImportRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${importBase}\\b`); const fromMatch = text.match(fromImportRegex); if (fromMatch) { const moduleName = fromMatch[1]; - // Wenn der User über die Methode fährt (modelling.XYZ) if (funcName !== importBase) { const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**'); for (const file of files) { @@ -64,14 +83,9 @@ export class TplHoverProvider implements vscode.HoverProvider { } } } - // Wenn er nur über "modelling" fährt return new vscode.Hover(new vscode.MarkdownString(`Importierte Definitions **${importBase}** aus Modul \`${moduleName}\`.`)); } - // 3. Hover für Built-Ins - if (word.startsWith('log.')) { return new vscode.Hover(new vscode.MarkdownString(`TPL Logging-Modul`)); } - if (word.startsWith('discovery.')) { return new vscode.Hover(new vscode.MarkdownString(`TPL Discovery-Modul`)); } - return null; } } \ No newline at end of file diff --git a/src/providers/actions/MetadataFix.ts b/src/providers/actions/MetadataFix.ts index 08b8046..3a50a02 100644 --- a/src/providers/actions/MetadataFix.ts +++ b/src/providers/actions/MetadataFix.ts @@ -9,10 +9,10 @@ export class MetadataFix implements TplQuickFix { fix.edit = new vscode.WorkspaceEdit(); const config = vscode.workspace.getConfiguration('tpl'); - const author = config.get('metadata.author', 'Kuro'); + const author = config.get('metadata.author', 'default'); const origin = config.get('metadata.origin', 'Custom'); const publisher = config.get('metadata.publisher', 'IT-Operations'); - const treePathBase = config.get('metadata.treePathBase', 'Grafing'); + const treePathBase = config.get('metadata.treePathBase', 'default'); const missingFieldsStr = diagnostic.message.replace('Fehlende Pflicht-Metadaten (Modul-Ebene): ', ''); const missingFields = missingFieldsStr.split(', '); @@ -24,7 +24,6 @@ export class MetadataFix implements TplQuickFix { if (missingFields.includes('publishers')) insertText += ` publishers := '${publisher}';\n`; if (missingFields.includes('tree_path')) insertText += ` tree_path := '${treePathBase}', 'TODO_Category', 'TODO_Subcategory';\n`; - // HIER GEFIXT: Einfügen auf Zeile + 1, damit es IN den Block rutscht fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText); fix.diagnostics = [diagnostic]; diff --git a/src/providers/completions/BuiltInCompletion.ts b/src/providers/completions/BuiltInCompletion.ts index 7159b54..947da75 100644 --- a/src/providers/completions/BuiltInCompletion.ts +++ b/src/providers/completions/BuiltInCompletion.ts @@ -7,7 +7,7 @@ export class BuiltInCompletion implements CompletionStrategy { const items: vscode.CompletionItem[] = []; // Prüft, ob wir im Namespace "log." oder "discovery." sind (auch wenn man danach weiter tippt!) - const match = linePrefix.match(/\b(log|discovery)\.([a-zA-Z0-9_]*)$/); + const match = linePrefix.match(/\b(discovery|log|model|text|regex|time|xpath|json|hash|inference)\.([a-zA-Z0-9_]*)$/); if (match) { const namespace = match[1]; diff --git a/src/providers/completions/LocalContextCompletion.ts b/src/providers/completions/LocalContextCompletion.ts index 157f435..215e8dc 100644 --- a/src/providers/completions/LocalContextCompletion.ts +++ b/src/providers/completions/LocalContextCompletion.ts @@ -1,29 +1,32 @@ import * as vscode from 'vscode'; import { CompletionStrategy } from './CompletionStrategy'; +import { tplBuiltIns } from '../../data/TplBuiltIns'; export class LocalContextCompletion implements CompletionStrategy { public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise { const text = document.getText(); const items: vscode.CompletionItem[] = []; - // 1. WENN DER USER NACH EINEM PUNKT TIPPT (Cross-File Lookup) - const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+)\.$/); + const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)$/); + if (dotMatch) { - const prefix = dotMatch[1]; // z.B. "modelling" - if (prefix === 'log' || prefix === 'discovery') return undefined; // Built-ins ignorieren + const prefix = dotMatch[1]; + + const bmcNamespaces = Object.keys(tplBuiltIns); + if (bmcNamespaces.includes(prefix)) { + return undefined; // Überlässt das Feld komplett der BuiltInCompletion! + } - // Prüfen, ob der Präfix importiert wurde const importRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${prefix}\\b`); const match = importRegex.exec(text); if (match) { - const moduleName = match[1]; // z.B. "Company.software_modelling" + const moduleName = match[1]; const externalItems = await this.resolveImportedMethods(moduleName, prefix); - return externalItems; // Nur externe Methoden zurückgeben + return externalItems; } - return undefined; // Unbekannter Namespace + return undefined; } - // 2. LOKALE DEFINITIONS & IMPORTS PARSEN const lines = text.split('\n'); let currentDefBlock = ""; @@ -44,7 +47,6 @@ export class LocalContextCompletion implements CompletionStrategy { } } - // Imports für die globale Worterkennung bereitstellen const fromImportRegex = /from\s+([a-zA-Z0-9_.]+)\s+import\s+([a-zA-Z0-9_]+)/g; let matchVar; while ((matchVar = fromImportRegex.exec(text)) !== null) { @@ -65,20 +67,16 @@ export class LocalContextCompletion implements CompletionStrategy { return items; } - // Hilfsmethode: Sucht im Workspace nach der anderen Datei! private async resolveImportedMethods(moduleName: string, defBlockName: string): Promise { const items: vscode.CompletionItem[] = []; - // Sucht alle .tpl Dateien im Projekt const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**'); for (const file of files) { const doc = await vscode.workspace.openTextDocument(file); const text = doc.getText(); - // Ist das das gesuchte Modul? (Punkte escapen) const moduleRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${moduleName.replace(/\./g, '\\.')}\\s*;`); if (moduleRegex.test(text)) { - // Modul gefunden! Jetzt die Defines im entsprechenden Block auslesen const lines = text.split('\n'); let inTargetBlock = false; for (const line of lines) { @@ -96,7 +94,7 @@ export class LocalContextCompletion implements CompletionStrategy { } } } - break; // Datei gefunden und geparst, wir können abbrechen + break; } } return items; diff --git a/src/providers/completions/SnippetCompletion.ts b/src/providers/completions/SnippetCompletion.ts index ee6297c..4c3e3c0 100644 --- a/src/providers/completions/SnippetCompletion.ts +++ b/src/providers/completions/SnippetCompletion.ts @@ -2,93 +2,145 @@ import * as vscode from 'vscode'; import { CompletionStrategy } from './CompletionStrategy'; export class SnippetCompletion implements CompletionStrategy { - - public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined { + public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise { + // Bei Modul-Aufrufen (Punkt) keine Snippets feuern if (linePrefix.match(/\.[a-zA-Z0-9_]*$/)) { return undefined; } const items: vscode.CompletionItem[] = []; - const config = vscode.workspace.getConfiguration('tpl'); - - // Kontext-Scanner: Wo befindet sich der Cursor gerade? - let inBlock: 'none' | 'pattern' | 'definitions' = 'none'; - let inPhase = ''; - - for (let i = 0; i <= position.line; i++) { - const text = document.lineAt(i).text.trim(); - if (text.match(/^(pattern|syncmapping)\b/)) { inBlock = 'pattern'; inPhase = ''; } - else if (text.match(/^definitions?\b/)) { inBlock = 'definitions'; inPhase = ''; } - else if (text.match(/^end (pattern|syncmapping|definitions?)\b/)) { inBlock = 'none'; inPhase = ''; } - - if (inBlock === 'pattern') { - ['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body'].forEach(p => { - if (text.startsWith(p)) { inPhase = p; } - else if (text.startsWith(`end ${p}`)) { inPhase = ''; } - }); - } - if (inBlock === 'definitions') { - if (text.startsWith('define ')) { inPhase = 'define'; } - else if (text.startsWith('end define')) { inPhase = ''; } - } - } - - // ========================================== - // 1. GLOBAL (Top-Level: Vor dem Pattern / Import-Bereich) - // ========================================== - if (inBlock === 'none') { - const skel = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet); - skel.insertText = new vscode.SnippetString(`tpl 1.23 module \${1:Name};\n\nmetadata\n origin := '\${2:Custom}';\n tree_path := 'Grafing', '\${3:Category}', '\${4:SubCategory}';\nend metadata;\n\npattern \${5:PatternName} 1.0\n """\n \${6:Beschreibung}\n """\n overview\n tags \${7:tag1};\n end overview;\n\n triggers\n on host := Host created, confirmed;\n end triggers;\n\n body\n log.debug("Start pattern \${5:PatternName}");\n $0\n end body;\nend pattern;`); - items.push(skel); - - ['pattern', 'syncmapping', 'definitions'].forEach(block => { - const item = new vscode.CompletionItem(block, vscode.CompletionItemKind.Snippet); - item.insertText = new vscode.SnippetString(`${block} \${1:Name} \${2:1.0}\n """\n \${3:DocString}\n """\n $0\nend ${block};`); - items.push(item); - }); - - const imp = new vscode.CompletionItem('import', vscode.CompletionItemKind.Snippet); - imp.insertText = new vscode.SnippetString('import ${1:module_name};'); - items.push(imp); - - const fromImp = new vscode.CompletionItem('from import', vscode.CompletionItemKind.Snippet); - fromImp.insertText = new vscode.SnippetString('from ${1:module_name} import${2:function_name};'); - items.push(fromImp); - } - - // ========================================== - // 2. INNERHALB PATTERN (aber VOR dem Body) - // ========================================== - if (inBlock === 'pattern' && inPhase === '') { - ['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body'].forEach(sec => { - const item = new vscode.CompletionItem(sec, vscode.CompletionItemKind.Snippet); - item.insertText = new vscode.SnippetString(`${sec}\n $0\nend ${sec};`); - items.push(item); - }); - } - if (inBlock === 'definitions' && inPhase === '') { - const item = new vscode.CompletionItem('define', vscode.CompletionItemKind.Snippet); - item.insertText = new vscode.SnippetString(`define \${1:methodName}(\${2:params})\n """\n \${3:DocString}\n """\n $0\nend define;`); - items.push(item); + // 1. Kontext herausfinden (Sind wir in einem Pattern oder Syncmapping?) + const textBeforeCursor = document.getText(new vscode.Range(new vscode.Position(0, 0), position)); + const inPattern = textBeforeCursor.lastIndexOf('pattern ') > textBeforeCursor.lastIndexOf('end pattern;'); + const inSyncmapping = textBeforeCursor.lastIndexOf('syncmapping ') > textBeforeCursor.lastIndexOf('end syncmapping;'); + const inBlock = inPattern || inSyncmapping; + + // 2. Settings laden + const config = vscode.workspace.getConfiguration('tpl'); + const author = config.get('metadata.author', 'Kuro'); + const origin = config.get('metadata.origin', 'Custom'); + const publisher = config.get('metadata.publisher', 'IT-Operations'); + const version = config.get('version', '23'); + + // Dynamischer Tree-Path + const minLevels = config.get('metadata.treePath.minLevels', 3); + const staticNodes = config.get('metadata.treePath.staticNodes', ['Grafing']); + + let treePathStr = ''; + let snippetTabIdx = 1; + + for (let i = 0; i < minLevels; i++) { + if (i < staticNodes.length) { + treePathStr += `'${staticNodes[i]}'`; + } else { + treePathStr += `'\${${snippetTabIdx}:Level_${i+1}}'`; + snippetTabIdx++; + } + if (i < minLevels - 1) treePathStr += ', '; + } + + // Dynamischer Metadata-String + let metadataBlock = 'metadata\n'; + if (config.get('metadata.require.origin', true)) metadataBlock += ` origin := '${origin}';\n`; + if (config.get('metadata.require.treePath', true)) metadataBlock += ` tree_path := ${treePathStr};\n`; + if (config.get('metadata.require.products', true)) metadataBlock += ` products := '\${${snippetTabIdx++}:Product}';\n`; + if (config.get('metadata.require.publishers', true)) metadataBlock += ` publishers := '${publisher}';\n`; + if (config.get('metadata.require.author', true)) metadataBlock += ` author := '${author}';\n`; + metadataBlock += 'end metadata;'; + + // ========================================== + // SNIPPETS FÜR DIE MODUL-EBENE (Außerhalb) + // ========================================== + if (!inBlock) { + const skeleton = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet); + skeleton.detail = "Generiert ein frisches TPL Modul"; + skeleton.insertText = new vscode.SnippetString( + `tpl 1.\${1:${version}} module \${2:MyModule};\n\n` + + `metadata\n` + + ` origin := '${origin}';\n` + + ` tree_path := '${treePathBase}', '\${3:Category}', '\${4:Subcategory}';\n` + + ` products := '\${5:Product}';\n` + + ` publishers := '${publisher}';\n` + + ` author := '${author}';\n` + + `end metadata;\n\n` + + `pattern \${6:MyPattern} 1.0\n` + + ` """\n \${7:Beschreibung des Patterns}\n """\n` + + ` metadata\n` + + ` publishers := '${publisher}';\n` + + ` tags := '\${8:Tags}';\n` + + ` end metadata;\n\n` + + ` overview\n` + + ` tags \${9:Tags};\n` + + ` end overview;\n\n` + + ` constants\n` + + ` \${10:const_name} := '\${11:value}';\n` + + ` end constants;\n\n` + + ` triggers\n` + + ` on process := SoftwareInstance created, confirmed where type = '\${12:Type}';\n` + + ` end triggers;\n\n` + + ` body\n` + + ` log.debug("Start pattern %pattern_name%");\n` + + ` $0\n` + + ` end body;\n` + + `end pattern;` + ); + items.push(skeleton); + + const modMeta = new vscode.CompletionItem('metadata', vscode.CompletionItemKind.Snippet); + modMeta.detail = "Modul-Metadaten Block (Pflichtfelder)"; + modMeta.insertText = new vscode.SnippetString( + `metadata\n` + + ` origin := '${origin}';\n` + + ` tree_path := '${treePathBase}', '\${1:Category}', '\${2:Subcategory}';\n` + + ` products := '\${3:Product}';\n` + + ` publishers := '${publisher}';\n` + + ` author := '${author}';\n` + + `end metadata;\n$0` + ); + items.push(modMeta); } // ========================================== - // 3. INNERHALB VON BODY ODER DEFINE (Ausführbarer Code) + // SNIPPETS FÜR DIE PATTERN-EBENE (Innerhalb) // ========================================== - if (inPhase === 'body' || inPhase === 'define') { - ['discovery', 'log', 'model', 'search', 'text', 'regex', 'time', 'hash', 'number', 'inference'].forEach(ns => { - const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module); - items.push(item); - }); - - const ifSnippet = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet); - ifSnippet.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;'); - items.push(ifSnippet); - - const forSnippet = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet); - forSnippet.insertText = new vscode.SnippetString('for ${1:item} in${2:list} do\n $0\nend for;'); - items.push(forSnippet); + if (inBlock) { + const patMeta = new vscode.CompletionItem('metadata', vscode.CompletionItemKind.Snippet); + patMeta.detail = "Pattern-Metadaten Block"; + patMeta.insertText = new vscode.SnippetString( + `metadata\n` + + ` publishers := '${publisher}';\n` + + ` tags := '\${1:Tags}';\n` + + `end metadata;\n$0` + ); + items.push(patMeta); } + // ========================================== + // ALLGEMEINE SNIPPETS (Überall gültig) + // ========================================== + const ifSnippet = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet); + ifSnippet.insertText = new vscode.SnippetString(`if \${1:condition} then\n $0\nend if;`); + items.push(ifSnippet); + + const elifSnippet = new vscode.CompletionItem('elif', vscode.CompletionItemKind.Snippet); + elifSnippet.insertText = new vscode.SnippetString(`elif \${1:condition} then\n $0`); + items.push(elifSnippet); + + const elseSnippet = new vscode.CompletionItem('else', vscode.CompletionItemKind.Snippet); + elseSnippet.insertText = new vscode.SnippetString(`else\n $0`); + items.push(elseSnippet); + + const forSnippet = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet); + forSnippet.insertText = new vscode.SnippetString(`for \${1:item} in \${2:list} do\n $0\nend for;`); + items.push(forSnippet); + + const whileSnippet = new vscode.CompletionItem('while', vscode.CompletionItemKind.Snippet); + whileSnippet.insertText = new vscode.SnippetString(`while \${1:condition} do\n $0\nend while;`); + items.push(whileSnippet); + + const defineSnippet = new vscode.CompletionItem('define', vscode.CompletionItemKind.Snippet); + defineSnippet.insertText = new vscode.SnippetString(`define \${1:name}(\${2:params})\n """\n \${3:DocString}\n """\n $0\nend define;`); + items.push(defineSnippet); + return items; } } \ No newline at end of file