From eba674218e6bdbcefb3c7497ec700f00f6719428 Mon Sep 17 00:00:00 2001 From: kuro Date: Sun, 11 Oct 2026 06:11:11 +0200 Subject: [PATCH] Refactoring 3 - providers und formatters weiter unterteilt und angepasst und erste unit tests eingetragen --- src/core/TplLinter.ts | 2 +- src/core/rules/BlockRule.ts | 23 +- src/core/rules/LinterRule.ts | 5 +- src/providers/TplCompletionProvider.ts | 252 +++--------------- src/providers/TplDocumentFormatter.ts | 115 +++----- .../completions/BuiltInCompletion.ts | 35 +++ .../completions/CompletionStrategy.ts | 14 + .../completions/LocalContextCompletion.ts | 13 + .../completions/SnippetCompletion.ts | 99 +++++++ src/providers/formatters/AlignmentRules.ts | 41 +++ src/providers/formatters/FormatState.ts | 13 + src/providers/formatters/IndentationRules.ts | 30 +++ src/test/suite/formatters.test.ts | 53 ++++ src/test/suite/linter.test.ts | 45 ++++ 14 files changed, 428 insertions(+), 312 deletions(-) create mode 100644 src/providers/completions/BuiltInCompletion.ts create mode 100644 src/providers/completions/CompletionStrategy.ts create mode 100644 src/providers/completions/LocalContextCompletion.ts create mode 100644 src/providers/completions/SnippetCompletion.ts create mode 100644 src/providers/formatters/AlignmentRules.ts create mode 100644 src/providers/formatters/FormatState.ts create mode 100644 src/providers/formatters/IndentationRules.ts create mode 100644 src/test/suite/formatters.test.ts create mode 100644 src/test/suite/linter.test.ts diff --git a/src/core/TplLinter.ts b/src/core/TplLinter.ts index 946e3f4..0023c0a 100644 --- a/src/core/TplLinter.ts +++ b/src/core/TplLinter.ts @@ -54,7 +54,7 @@ export function validateTplDocument(document: vscode.TextDocument, collection: v // Regeln anwenden for (const rule of rules) { - rule.checkLine(line, rawLine, i, ctx); + rule.checkLine(line, rawLine, i, ctx, lines); // Hier 'lines' ergänzen! } } diff --git a/src/core/rules/BlockRule.ts b/src/core/rules/BlockRule.ts index f23047e..3799c7a 100644 --- a/src/core/rules/BlockRule.ts +++ b/src/core/rules/BlockRule.ts @@ -4,7 +4,8 @@ import { LinterContext, PatternPhase } from '../LinterContext'; export class BlockRule implements LinterRule { - public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext): void { + // HIER GEFIXT: 'lines: string[]' wurde zur Signatur hinzugefügt! + public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void { if (!ctx.hasModuleDef) { if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; } else { ctx.addDiag(lineIdx, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module ;' beginnen.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; } @@ -31,7 +32,8 @@ export class BlockRule implements LinterRule { this.handleBlockOpenings(line, rawLine, lineIdx, ctx); if (ctx.inBlock === 'pattern') { - this.checkPatternPhases(line, rawLine, lineIdx, ctx); + // HIER GEFIXT: 'lines' wird jetzt korrekt an die Methode weitergereicht + this.checkPatternPhases(line, rawLine, lineIdx, ctx, lines); } this.checkEndBlocks(line, rawLine, lineIdx, ctx); @@ -91,7 +93,7 @@ export class BlockRule implements LinterRule { } } - private checkPatternPhases(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { + private checkPatternPhases(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]) { const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => { if (line.startsWith(phaseName)) { if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); } @@ -114,6 +116,21 @@ export class BlockRule implements LinterRule { if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); } ctx.activePhaseName = "body"; ctx.activePhaseLine = lineIdx; + + // DER LOG-CHECK IST ZURÜCK! + let foundLog = false; + for (let j = lineIdx + 1; j < lines.length; j++) { + const aheadLine = lines[j].trim(); + if (aheadLine.startsWith('end pattern') || aheadLine.startsWith('end syncmapping')) { break; } + if (aheadLine.includes('log.debug') && aheadLine.includes(ctx.currentPatternName)) { foundLog = true; break; } + } + + if (!foundLog) { + const expectedLog = `log.debug("Start pattern ${ctx.currentPatternName}");`; + const diag = ctx.addDiag(lineIdx, 0, rawLine.length, `Im Body fehlt das Log-Statement mit dem Pattern-Namen. Bsp: ${expectedLog}`, vscode.DiagnosticSeverity.Error); + diag.code = 'missing_log_start'; + diag.source = expectedLog; + } } const closers = ['end metadata', 'end overview', 'end constants', 'end triggers', 'end mapping', 'end removal', 'end body']; diff --git a/src/core/rules/LinterRule.ts b/src/core/rules/LinterRule.ts index f04174f..f1ccda1 100644 --- a/src/core/rules/LinterRule.ts +++ b/src/core/rules/LinterRule.ts @@ -1,9 +1,6 @@ import { LinterContext } from '../LinterContext'; export interface LinterRule { - // Wird für jede Code-Zeile aufgerufen - checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext): void; - - // Wird ganz am Ende aufgerufen (z.B. um ungeschlossene Klammern zu prüfen) + checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void; checkEnd?(lines: string[], ctx: LinterContext): void; } \ No newline at end of file diff --git a/src/providers/TplCompletionProvider.ts b/src/providers/TplCompletionProvider.ts index 1022938..ad0cf90 100644 --- a/src/providers/TplCompletionProvider.ts +++ b/src/providers/TplCompletionProvider.ts @@ -1,232 +1,40 @@ import * as vscode from 'vscode'; -import { tplBuiltIns } from '../data/TplBuiltIns'; +import { CompletionStrategy } from './completions/CompletionStrategy'; +import { SnippetCompletion } from './completions/SnippetCompletion'; +import { BuiltInCompletion } from './completions/BuiltInCompletion'; +import { LocalContextCompletion } from './completions/LocalContextCompletion'; export class TplCompletionProvider implements vscode.CompletionItemProvider { - async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { - const linePrefix = document.lineAt(position).text.substring(0, position.character); - const items: vscode.CompletionItem[] = []; - const text = document.getText(); + private strategies: CompletionStrategy[]; - const importMap = new Map(); - const importRegex = /from\s+([\w.]+)\s+import\s+(\w+)/g; - let m; - while ((m = importRegex.exec(text)) !== null) { - importMap.set(m[2], m[1]); - } - - const localNamespaces = new Set(); - const defBlockRegex = /^\s*definitions\s+([a-zA-Z0-9_]+)/gm; - while ((m = defBlockRegex.exec(text)) !== null) { - localNamespaces.add(m[1]); - } - - const matchDot = linePrefix.match(/([a-zA-Z0-9_]+)\.$/); - - if (!matchDot) { - // A) Externe & Lokale Module - importMap.forEach((_, key) => { - const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Module); - item.detail = "Importiertes Modul"; - items.push(item); - }); - localNamespaces.forEach(ns => { - const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module); - item.detail = "Lokales Modul (definitions)"; - items.push(item); - }); - - // B) Standard BMC Namespaces - for (const key of Object.keys(tplBuiltIns)) { - if (key !== 'built_in_globals') { - const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Module); - item.detail = "BMC Built-In Namespace"; - items.push(item); - } else { - for (const [glob, def] of Object.entries(tplBuiltIns[key])) { - for (const snippet of def.snippets) { - // Snippet-Syntax entfernen für das Anzeige-Label (z.B. "${1:number}" -> "number") - const cleanSnippet = snippet.replace(/\$\{\d+:([^}]+)\}/g, '$1'); - - const item = new vscode.CompletionItem(`${glob}(${cleanSnippet})`, vscode.CompletionItemKind.Function); - // Setzt das echte Snippet mit Tab-Stops ein - item.insertText = new vscode.SnippetString(`${glob}(${snippet})`); - // VS Code darf nur nach dem Methodennamen filtern, nicht nach den Parametern! - item.filterText = glob; - item.documentation = new vscode.MarkdownString(def.description); - item.detail = "BMC Globale Funktion"; - items.push(item); - } - } - } - } - - // C) Lokale Variablen - const varRegex = /\b([a-zA-Z0-9_]+)\s*:=/g; - const variables = new Set(); - while ((m = varRegex.exec(text)) !== null) { - variables.add(m[1]); - } - variables.forEach(v => { - items.push(new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable)); - }); - - // D) Snippets - - // Konfiguration 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 treePathBase = config.get('metadata.treePathBase', 'Grafing'); - const tplVersion = config.get('version', 23); - - const skeletonItem = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet); - skeletonItem.insertText = new vscode.SnippetString( - `tpl 1.${tplVersion} module \${1:module_name};\n\n` + - `metadata\n origin := '${origin}';\n author := '${author}';\n products := '\${2:products}';\n publishers := '${publisher}';\n tree_path := '${treePathBase}', '\${3:category}', '\${4:subcategory}';\nend metadata;\n\n` + - `pattern \${5:pattern_name} 1.0\n """\n TODO: explain\n """\n\n triggers\n on process := DiscoveredProcess where cmd matches regex_escape("\${6:process}");\n end triggers;\n\n body\n log.debug("Start pattern \${5:pattern_name}");\n $0\n end body;\nend pattern;` - ); - skeletonItem.detail = "Generiert das komplette TPL Modul-Grundgerüst (mit Settings)"; - items.push(skeletonItem); - - const blocks = ['metadata', 'overview', 'constants', 'triggers', 'body', 'mapping', 'removal', 'definitions', 'enumeration', 'configuration']; - for (const block of blocks) { - const item = new vscode.CompletionItem(block, vscode.CompletionItemKind.Snippet); - item.insertText = new vscode.SnippetString(`${block}\n $0\nend ${block};`); - items.push(item); - } - - const defineItem = new vscode.CompletionItem('define', vscode.CompletionItemKind.Snippet); - defineItem.insertText = new vscode.SnippetString('define ${1:name}(${2:params})\n $0\nend define;'); - items.push(defineItem); - - const patternItem = new vscode.CompletionItem('pattern', vscode.CompletionItemKind.Snippet); - patternItem.insertText = new vscode.SnippetString('pattern ${1:name}${2:1.0}\n """\n TODO: explain\n """\n\n $0\nend pattern;'); - items.push(patternItem); - - const syncmappingItem = new vscode.CompletionItem('syncmapping', vscode.CompletionItemKind.Snippet); - syncmappingItem.insertText = new vscode.SnippetString('syncmapping ${1:name}${2:1.0}\n """\n TODO: explain\n """\n\n $0\nend syncmapping;'); - items.push(syncmappingItem); - - const tableItem = new vscode.CompletionItem('table', vscode.CompletionItemKind.Snippet); - tableItem.insertText = new vscode.SnippetString('table ${1:name}${2:1.0}\n $0\n default -> "unknown";\nend table;'); - items.push(tableItem); - - // --- Kontrollstrukturen (If, For, While) --- - - const ifItem = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet); - ifItem.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;'); - ifItem.detail = "If-Bedingung"; - items.push(ifItem); - - const elifItem = new vscode.CompletionItem('elif', vscode.CompletionItemKind.Snippet); - elifItem.insertText = new vscode.SnippetString('elif ${1:condition} then\n $0'); - elifItem.detail = "Else-If-Bedingung"; - items.push(elifItem); - - const elseItem = new vscode.CompletionItem('else', vscode.CompletionItemKind.Snippet); - elseItem.insertText = new vscode.SnippetString('else\n $0'); - items.push(elseItem); - - const forItem = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet); - forItem.insertText = new vscode.SnippetString('for ${1:item} in${2:list} do\n $0\nend for;'); - forItem.detail = "For-Schleife"; - items.push(forItem); - - const whileItem = new vscode.CompletionItem('while', vscode.CompletionItemKind.Snippet); - whileItem.insertText = new vscode.SnippetString('while ${1:condition} do\n $0\nend while;'); - whileItem.detail = "While-Schleife"; - items.push(whileItem); - - return items; - } else { - const ns = matchDot[1]; - - // NEU: BMC Built-Ins abfangen und Snippets bauen - if (tplBuiltIns[ns]) { - const bmcMethods: vscode.CompletionItem[] = []; - for (const [method, def] of Object.entries(tplBuiltIns[ns])) { - for (const snippet of def.snippets) { - const cleanSnippet = snippet.replace(/\$\{\d+:([^}]+)\}/g, '$1'); - - const item = new vscode.CompletionItem(`${method}(${cleanSnippet})`, vscode.CompletionItemKind.Method); - item.insertText = new vscode.SnippetString(`${method}(${snippet})`); - item.filterText = method; - item.documentation = new vscode.MarkdownString(def.description); - item.detail = `${ns}.${method}`; - - bmcMethods.push(item); - } - } - return bmcMethods; - } - - const moduleMethods: vscode.CompletionItem[] = []; - const extractDefineRegex = /^\s*define\s+.*?\b([a-zA-Z0-9_]+)\s*\(/gm; - - if (localNamespaces.has(ns)) { - let defMatch; - while ((defMatch = extractDefineRegex.exec(text)) !== null) { - const methodName = defMatch[1]; - const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method); - const docStr = this.extractDocString(text, defMatch.index); - if (docStr) item.documentation = new vscode.MarkdownString(docStr); - moduleMethods.push(item); - } - return moduleMethods; - } - - if (importMap.has(ns)) { - const modPath = importMap.get(ns)!; - const uris = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**', 50); - for (const uri of uris) { - try { - const docBytes = await vscode.workspace.fs.readFile(uri); - const docText = Buffer.from(docBytes).toString('utf8'); - const modRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${modPath.replace(/\./g, '\\\\.')}\\s*;`); - if (modRegex.test(docText)) { - let defMatch; - while ((defMatch = extractDefineRegex.exec(docText)) !== null) { - const methodName = defMatch[1]; - const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method); - const docStr = this.extractDocString(docText, defMatch.index); - if (docStr) item.documentation = new vscode.MarkdownString(docStr); - moduleMethods.push(item); - } - break; - } - } catch(e) {} - } - if (moduleMethods.length > 0) return moduleMethods; - } - } - return items; + constructor() { + // Hier registrieren wir alle Completion Engines + this.strategies = [ + new BuiltInCompletion(), + new SnippetCompletion(), + new LocalContextCompletion() + ]; } - private extractDocString(text: string, startIndex: number): string | null { - const lines = text.substring(startIndex).split('\n'); - let docString = ""; - let inDocString = false; - let quoteType = ""; - for (let j = 1; j < Math.min(15, lines.length); j++) { - const line = lines[j].trim(); - if (line.length === 0) continue; - if (!inDocString) { - if (line.startsWith('"""') || line.startsWith("'''")) { - inDocString = true; - quoteType = line.substring(0, 3); - if (line.length > 3 && line.endsWith(quoteType)) { docString = line.substring(3, line.length - 3).trim(); break; } - const content = line.substring(3).trim(); - if (content) docString += content + "\n"; - } else { break; } - } else { - if (line.endsWith(quoteType)) { - const content = line.substring(0, line.length - 3).trim(); - if (content) docString += content; - break; - } else { docString += line + "\n"; } + public provideCompletionItems( + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken, + context: vscode.CompletionContext + ): vscode.ProviderResult { + + // Den Text in der aktuellen Zeile bis zum Cursor holen + const linePrefix = document.lineAt(position).text.substring(0, position.character); + const results: vscode.CompletionItem[] = []; + + // Jede Engine um Vorschläge bitten + for (const strategy of this.strategies) { + const items = strategy.provideCompletions(document, position, linePrefix); + if (items && items.length > 0) { + results.push(...items); } } - return docString.trim() || null; + + return results; } } \ No newline at end of file diff --git a/src/providers/TplDocumentFormatter.ts b/src/providers/TplDocumentFormatter.ts index cdface0..e640220 100644 --- a/src/providers/TplDocumentFormatter.ts +++ b/src/providers/TplDocumentFormatter.ts @@ -1,97 +1,48 @@ import * as vscode from 'vscode'; +import { FormatState } from './formatters/FormatState'; +import { IndentationRules } from './formatters/IndentationRules'; +import { AlignmentRules } from './formatters/AlignmentRules'; export class TplDocumentFormatter implements vscode.DocumentFormattingEditProvider { - provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): vscode.ProviderResult { - const text = document.getText(); - const isCRLF = text.includes('\r\n'); - const lines = text.split(/\r?\n/); + private indentRules = new IndentationRules(); + private alignRules = new AlignmentRules(); + + public provideDocumentFormattingEdits( + document: vscode.TextDocument, + options: vscode.FormattingOptions, + token: vscode.CancellationToken + ): vscode.ProviderResult { - const formattedLines: string[] = []; const edits: vscode.TextEdit[] = []; + const state = new FormatState(); - let indentLevel = 0; - const indentString = ' '; - let inTripleDouble = false; - let inTripleSingle = false; - let consecutiveEmptyLines = 0; - - const alignmentStack: number[] = []; + // Nutze die VS Code Settings (meist 4 Spaces), andernfalls Fallback + state.tabSize = options.tabSize || 4; - for (let i = 0; i < lines.length; i++) { - const rawLine = lines[i]; - let line = rawLine.trim(); + for (let i = 0; i < document.lineCount; i++) { + const originalLine = document.lineAt(i); + const rawText = originalLine.text; + const trimmedText = rawText.trim(); - const tripleDoubleCount = (rawLine.match(/"""/g) || []).length; - const tripleSingleCount = (rawLine.match(/'''/g) || []).length; - - let isInternalStringLine = false; - - if (inTripleDouble) { - isInternalStringLine = true; - if (tripleDoubleCount % 2 !== 0) { inTripleDouble = false; isInternalStringLine = false; } - } else if (tripleDoubleCount % 2 !== 0) { inTripleDouble = true; } - - if (inTripleSingle) { - isInternalStringLine = true; - if (tripleSingleCount % 2 !== 0) { inTripleSingle = false; isInternalStringLine = false; } - } else if (tripleSingleCount % 2 !== 0) { inTripleSingle = true; } - - if (isInternalStringLine && (inTripleDouble || inTripleSingle)) { - formattedLines.push(rawLine); - consecutiveEmptyLines = 0; + // Leere Zeilen lassen wir leer + if (trimmedText.length === 0) { + if (rawText.length > 0) { + // Löscht unnötige Whitespaces in eigentlich leeren Zeilen + edits.push(vscode.TextEdit.replace(originalLine.range, '')); + } continue; } - if (line.length === 0) { - consecutiveEmptyLines++; - if (consecutiveEmptyLines <= 1) { formattedLines.push(""); } - continue; + // 1. Basis-Einrückung (if/else/pattern) berechnen + let formattedText = this.indentRules.apply(trimmedText, state); + + // 2. Visual Alignment (Klammern) anwenden & Stack updaten + formattedText = this.alignRules.apply(trimmedText, formattedText, state); + + // 3. Edit nur erstellen, wenn sich die Zeile wirklich ändert (spart Performance & Cursor-Sprünge) + if (rawText !== formattedText) { + edits.push(vscode.TextEdit.replace(originalLine.range, formattedText)); } - - consecutiveEmptyLines = 0; - - if (line.match(/^(end\s+(pattern|syncmapping|table|enumeration|configuration|definitions?|define|if|for|while|metadata|overview|constants|triggers|mapping|removal|body)|else|elif)\b/)) { - indentLevel = Math.max(0, indentLevel - 1); - } - - let newLineText = ""; - if (alignmentStack.length > 0) { - let spaces = alignmentStack[alignmentStack.length - 1]; - if (line.match(/^[)\]]/)) { spaces = Math.max(0, spaces - 1); } - newLineText = " ".repeat(spaces) + line; - } else { - newLineText = indentString.repeat(indentLevel) + line; - } - - formattedLines.push(newLineText); - - let cleanLineForParens = newLineText.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, match => " ".repeat(match.length)); - const commentIdx = cleanLineForParens.indexOf('//'); - if (commentIdx !== -1) { - cleanLineForParens = cleanLineForParens.substring(0, commentIdx) + " ".repeat(cleanLineForParens.length - commentIdx); - } - - for (let charIdx = 0; charIdx < cleanLineForParens.length; charIdx++) { - const char = cleanLineForParens[charIdx]; - if (char === '(' || char === '[') { - alignmentStack.push(charIdx + 1); - } else if (char === ')' || char === ']') { - if (alignmentStack.length > 0) { alignmentStack.pop(); } - } - } - - if (line.match(/^(pattern|syncmapping|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|if|for|while|else|elif)\b/)) { - if (!line.startsWith('end ')) { - indentLevel++; - } - } - } - - const newText = formattedLines.join(isCRLF ? '\r\n' : '\n'); - - if (newText !== text) { - const fullRange = new vscode.Range(document.positionAt(0), document.positionAt(text.length)); - edits.push(vscode.TextEdit.replace(fullRange, newText)); } return edits; diff --git a/src/providers/completions/BuiltInCompletion.ts b/src/providers/completions/BuiltInCompletion.ts new file mode 100644 index 0000000..7159b54 --- /dev/null +++ b/src/providers/completions/BuiltInCompletion.ts @@ -0,0 +1,35 @@ +import * as vscode from 'vscode'; +import { CompletionStrategy } from './CompletionStrategy'; + +export class BuiltInCompletion implements CompletionStrategy { + + public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined { + 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_]*)$/); + + if (match) { + const namespace = match[1]; + + if (namespace === 'log') { + const levels = ['debug', 'info', 'warn', 'error']; + for (const level of levels) { + const item = new vscode.CompletionItem(level, vscode.CompletionItemKind.Method); + item.insertText = new vscode.SnippetString(`${level}("\${1:message}");`); + item.documentation = new vscode.MarkdownString(`Schreibt eine Log-Meldung auf dem Level **${level}**.`); + items.push(item); + } + } + else if (namespace === 'discovery') { + const runCmd = new vscode.CompletionItem('runCommand', vscode.CompletionItemKind.Method); + runCmd.insertText = new vscode.SnippetString('runCommand(${1:host}, "${2:command}");'); + runCmd.detail = "discovery.runCommand(host, command)"; + items.push(runCmd); + } + return items; // Treffer gefunden, wir geben sie zurück! + } + + return undefined; + } +} \ No newline at end of file diff --git a/src/providers/completions/CompletionStrategy.ts b/src/providers/completions/CompletionStrategy.ts new file mode 100644 index 0000000..a1a1b8b --- /dev/null +++ b/src/providers/completions/CompletionStrategy.ts @@ -0,0 +1,14 @@ +import * as vscode from 'vscode'; + +export interface CompletionStrategy { + /** + * @param document Das aktuelle Dokument + * @param position Die Cursor-Position + * @param linePrefix Der Text der aktuellen Zeile *vor* dem Cursor + */ + provideCompletions( + document: vscode.TextDocument, + position: vscode.Position, + linePrefix: string + ): vscode.CompletionItem[] | undefined; +} \ No newline at end of file diff --git a/src/providers/completions/LocalContextCompletion.ts b/src/providers/completions/LocalContextCompletion.ts new file mode 100644 index 0000000..658abcd --- /dev/null +++ b/src/providers/completions/LocalContextCompletion.ts @@ -0,0 +1,13 @@ +import * as vscode from 'vscode'; +import { CompletionStrategy } from './CompletionStrategy'; + +export class LocalContextCompletion implements CompletionStrategy { + + public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined { + // TODO für Phase 2: + // 1. Regex Suche im Dokument nach ":=" (Variablen tracken) + // 2. Projektweite Suche nach "define" Methoden für Autocomplete + + return undefined; + } +} \ No newline at end of file diff --git a/src/providers/completions/SnippetCompletion.ts b/src/providers/completions/SnippetCompletion.ts new file mode 100644 index 0000000..57069cb --- /dev/null +++ b/src/providers/completions/SnippetCompletion.ts @@ -0,0 +1,99 @@ +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 { + // Wenn wir gerade NACH einem Punkt tippen (z.B. "log."), sind Snippets/Base-Module hier falsch! + if (linePrefix.match(/\.[a-zA-Z0-9_]*$/)) { + return undefined; + } + + const items: vscode.CompletionItem[] = []; + const config = vscode.workspace.getConfiguration('tpl'); + + // ========================================== + // 1. BASIS-NAMESPACES (discovery, log, etc.) + // ========================================== + const namespaces = ['discovery', 'log', 'model', 'search', 'text', 'regex', 'time', 'hash', 'number', 'inference']; + for (const ns of namespaces) { + const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module); + item.detail = `TPL Namespace: ${ns}`; + items.push(item); + } + + // ========================================== + // 2. DAS GOD-SNIPPET (Skeleton) + // ========================================== + const skel = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet); + const version = config.get('version', 23); + const author = config.get('metadata.author', 'Kuro'); + + skel.insertText = new vscode.SnippetString( + `tpl 1.${version} module \${1:Name};\n\n` + + `metadata\n` + + ` origin := '\${2:Custom}';\n` + + ` author := '${author}';\n` + + ` tree_path := 'Grafing', '\${3:Category}', '\${4:SubCategory}';\n` + + `end metadata;\n\n` + + `pattern \${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;\n` + + `end pattern;` + ); + skel.detail = "Generiert ein komplettes TPL Grundgerüst"; + items.push(skel); + + // ========================================== + // 3. TPL HAUPT-BLÖCKE (pattern, define, etc.) + // ========================================== + const mainBlocks = [ + { label: 'pattern', desc: 'pattern ... end pattern;' }, + { label: 'syncmapping', desc: 'syncmapping ... end syncmapping;' }, + { label: 'define', desc: 'define ... end define;' } + ]; + + for (const block of mainBlocks) { + const item = new vscode.CompletionItem(block.label, vscode.CompletionItemKind.Snippet); + item.insertText = new vscode.SnippetString(`${block.label} \${1:Name} \${2:1.0}\n """\n \${3:DocString}\n """\n $0\nend ${block.label};`); + if (block.label === 'define') { + item.insertText = new vscode.SnippetString(`define \${1:methodName}(\${2:params})\n """\n \${3:DocString}\n """\n $0\nend define;`); + } + item.detail = block.desc; + items.push(item); + } + + // ========================================== + // 4. SECTIONS (metadata, body, triggers, etc.) + // ========================================== + const sections = ['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body']; + for (const sec of sections) { + const item = new vscode.CompletionItem(sec, vscode.CompletionItemKind.Snippet); + item.insertText = new vscode.SnippetString(`${sec}\n $0\nend ${sec};`); + item.detail = `${sec} ... end ${sec};`; + items.push(item); + } + + // ========================================== + // 5. KONTROLLSTRUKTUREN & TABELLEN + // ========================================== + const ifSnippet = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet); + ifSnippet.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;'); + ifSnippet.detail = "if ... then ... end 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;'); + forSnippet.detail = "for ... in ... do ... end for;"; + items.push(forSnippet); + + const tableSnippet = new vscode.CompletionItem('table', vscode.CompletionItemKind.Snippet); + tableSnippet.insertText = new vscode.SnippetString('table ${1:TableName} 1.0\n ${2:"key"} -> "${3:value}";\n default -> "unknown";\nend table;'); + tableSnippet.detail = "table ... end table;"; + items.push(tableSnippet); + + return items; + } +} \ No newline at end of file diff --git a/src/providers/formatters/AlignmentRules.ts b/src/providers/formatters/AlignmentRules.ts new file mode 100644 index 0000000..e8d02bf --- /dev/null +++ b/src/providers/formatters/AlignmentRules.ts @@ -0,0 +1,41 @@ +import { FormatState } from './FormatState'; + +export class AlignmentRules { + + public apply(line: string, formattedLine: string, state: FormatState): string { + let finalLine = formattedLine; + + // Wenn wir innerhalb einer offenen Klammer sind, überschreibe die Block-Einrückung + if (state.parenAlignments.length > 0) { + const alignCol = state.parenAlignments[state.parenAlignments.length - 1]; + finalLine = ' '.repeat(alignCol) + line; + } else if (state.bracketAlignments.length > 0) { + const alignCol = state.bracketAlignments[state.bracketAlignments.length - 1]; + finalLine = ' '.repeat(alignCol) + line; + } + + // Aktualisiere Stacks basierend auf der FINAL formatierten Zeile (wichtig für die Spaltenzahl!) + this.updateAlignments(finalLine, state); + + return finalLine; + } + + private updateAlignments(line: string, state: FormatState) { + // Strings ignorieren, damit wir keine Klammern in Log-Texten zählen + let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""'); + + for (let i = 0; i < cleanLine.length; i++) { + const char = cleanLine[i]; + if (char === '(') { + // Einrückung ab dem Zeichen NACH der offenen Klammer + state.parenAlignments.push(i + 1); + } else if (char === ')') { + state.parenAlignments.pop(); + } else if (char === '[') { + state.bracketAlignments.push(i + 1); + } else if (char === ']') { + state.bracketAlignments.pop(); + } + } + } +} \ No newline at end of file diff --git a/src/providers/formatters/FormatState.ts b/src/providers/formatters/FormatState.ts new file mode 100644 index 0000000..83ecaa4 --- /dev/null +++ b/src/providers/formatters/FormatState.ts @@ -0,0 +1,13 @@ +export class FormatState { + public indentLevel: number = 0; + public tabSize: number = 4; + + // Speichert die Spalten-Position offener Klammern für Visual Alignment + public parenAlignments: number[] = []; + public bracketAlignments: number[] = []; + + // Gibt die aktuellen Basis-Leerzeichen zurück (z.B. Level 2 = 8 Leerzeichen) + public get currentIndentString(): string { + return ' '.repeat(this.indentLevel * this.tabSize); + } +} \ No newline at end of file diff --git a/src/providers/formatters/IndentationRules.ts b/src/providers/formatters/IndentationRules.ts new file mode 100644 index 0000000..99112be --- /dev/null +++ b/src/providers/formatters/IndentationRules.ts @@ -0,0 +1,30 @@ +import { FormatState } from './FormatState'; + +export class IndentationRules { + // Keywords, die eine neue Einrückungsebene öffnen (ohne static!) + private readonly blockOpens = /^(pattern|syncmapping|table|definitions?|enumeration|configuration|define|metadata|overview|constants|triggers|mapping|removal|body|if\s+|for\s+|while\s+)\b/; + + // Keywords, die eine Ebene schließen (ohne static!) + private readonly blockCloses = /^(end\s+|else\b|elif\b)/; + + public apply(line: string, state: FormatState): string { + // 1. Wenn die Zeile einen Block schließt, VORHER die Einrückung verringern + if (this.blockCloses.test(line) && state.indentLevel > 0) { + state.indentLevel--; + } + + // Aktuelle Einrückung für diese Zeile abholen + const currentIndent = state.currentIndentString; + + // 2. NACH der Zeile: Wenn sie einen Block öffnet, Level für die nächste Zeile erhöhen + if (/^(else\b|elif\b)/.test(line)) { + // else / elif springen nach dem Schließen direkt wieder auf + state.indentLevel++; + } + else if (this.blockOpens.test(line)) { + state.indentLevel++; + } + + return currentIndent + line; + } +} \ No newline at end of file diff --git a/src/test/suite/formatters.test.ts b/src/test/suite/formatters.test.ts new file mode 100644 index 0000000..4043b35 --- /dev/null +++ b/src/test/suite/formatters.test.ts @@ -0,0 +1,53 @@ +import * as assert from 'assert'; +import { FormatState } from '../../providers/formatters/FormatState'; +import { IndentationRules } from '../../providers/formatters/IndentationRules'; +import { AlignmentRules } from '../../providers/formatters/AlignmentRules'; + +suite('Formatter Test Suite', () => { + test('IndentationRules: if/then/end Block korrekt einrücken', () => { + const indentRules = new IndentationRules(); + const state = new FormatState(); + state.tabSize = 4; + + // Zeile 1: if condition then (erwartet: 0 Leerzeichen, aber Level springt auf 1 für die nächste Zeile) + let result = indentRules.apply('if x = 1 then', state); + assert.strictEqual(result, 'if x = 1 then'); + assert.strictEqual(state.indentLevel, 1); + + // Zeile 2: normaler Code im Block (erwartet: 4 Leerzeichen) + result = indentRules.apply('log.debug("test");', state); + assert.strictEqual(result, ' log.debug("test");'); + assert.strictEqual(state.indentLevel, 1); + + // Zeile 3: end if; (erwartet: 0 Leerzeichen, Level springt sofort vor der Ausgabe auf 0) + result = indentRules.apply('end if;', state); + assert.strictEqual(result, 'end if;'); + assert.strictEqual(state.indentLevel, 0); + }); + + test('IndentationRules: elif und else verhalten sich korrekt', () => { + const indentRules = new IndentationRules(); + const state = new FormatState(); + state.tabSize = 4; + state.indentLevel = 1; // Wir starten quasi IN einem offenen if-Block + + // elif muss zurückspringen (auf 0) und danach wieder auf 1 für den folgenden Block + let result = indentRules.apply('elif x = 2 then', state); + assert.strictEqual(result, 'elif x = 2 then'); + assert.strictEqual(state.indentLevel, 1); + }); + + test('AlignmentRules: Visual Alignment an runden Klammern', () => { + const alignRules = new AlignmentRules(); + const state = new FormatState(); + + // 1. Zeile mit öffnender Klammer + alignRules.apply('discovery.runCommand(', 'discovery.runCommand(', state); + assert.strictEqual(state.parenAlignments.length, 1); + assert.strictEqual(state.parenAlignments[0], 21); // 21 Zeichen bis nach der offenen Klammer + + // 2. Zeile (Parameter soll exakt bis Spalte 21 eingerückt werden) + const result = alignRules.apply('host,', 'host,', state); + assert.strictEqual(result, ' host,'); + }); +}); \ No newline at end of file diff --git a/src/test/suite/linter.test.ts b/src/test/suite/linter.test.ts new file mode 100644 index 0000000..d15224e --- /dev/null +++ b/src/test/suite/linter.test.ts @@ -0,0 +1,45 @@ +import * as assert from 'assert'; +import { LinterContext } from '../../core/LinterContext'; +import { SyntaxRule } from '../../core/rules/SyntaxRule'; + +suite('Linter SyntaxRule Test Suite', () => { + test('Zuweisung: Warnt bei "=" statt ":="', () => { + const rule = new SyntaxRule(); + const ctx = new LinterContext(); + + // Wir simulieren eine Code-Zeile + const rawLine = " my_var = 123;"; + rule.checkLine(rawLine.trim(), rawLine, 0, ctx); + + // Erwartung: Ein Fehler wegen falscher Zuweisung + assert.strictEqual(ctx.diagnostics.length, 1); + assert.ok(ctx.diagnostics[0].message.includes("In TPL wird ':=' für Zuweisungen genutzt")); + }); + + test('Zuweisung: Akzeptiert "=" als Vergleich in if-Bedingung', () => { + const rule = new SyntaxRule(); + const ctx = new LinterContext(); + + // Wir simulieren eine if-Zeile (setzt ctx.inConditionBlock intern auf true) + const rawLine = " if my_var = 123 then"; + rule.checkLine(rawLine.trim(), rawLine, 0, ctx); + + // Erwartung: Kein Fehler, da das = als Vergleich legitim ist + assert.strictEqual(ctx.diagnostics.length, 0); + }); + + test('Klammern: Findet ungeschlossene runde Klammern', () => { + const rule = new SyntaxRule(); + const ctx = new LinterContext(); + + // Wir öffnen eine Klammer, schließen sie aber nicht + const rawLine = " my_func(parameter;"; + rule.checkLine(rawLine.trim(), rawLine, 0, ctx); + + // checkEnd wird am Ende der Datei aufgerufen + rule.checkEnd(['', rawLine], ctx); + + assert.strictEqual(ctx.diagnostics.length, 1); + assert.ok(ctx.diagnostics[0].message.includes("Öffnende Klammer '(' wird nicht geschlossen!")); + }); +}); \ No newline at end of file