Refactoring 3 - providers und formatters weiter unterteilt und angepasst und erste unit tests eingetragen

This commit is contained in:
kuro 2026-10-11 06:11:11 +02:00
parent 132cfacb70
commit eba674218e
14 changed files with 428 additions and 312 deletions

View File

@ -54,7 +54,7 @@ export function validateTplDocument(document: vscode.TextDocument, collection: v
// Regeln anwenden // Regeln anwenden
for (const rule of rules) { for (const rule of rules) {
rule.checkLine(line, rawLine, i, ctx); rule.checkLine(line, rawLine, i, ctx, lines); // Hier 'lines' ergänzen!
} }
} }

View File

@ -4,7 +4,8 @@ import { LinterContext, PatternPhase } from '../LinterContext';
export class BlockRule implements LinterRule { 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 (!ctx.hasModuleDef) {
if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; } if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; }
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module <name>;' beginnen.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; } else { ctx.addDiag(lineIdx, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module <name>;' beginnen.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; }
@ -31,7 +32,8 @@ export class BlockRule implements LinterRule {
this.handleBlockOpenings(line, rawLine, lineIdx, ctx); this.handleBlockOpenings(line, rawLine, lineIdx, ctx);
if (ctx.inBlock === 'pattern') { 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); 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) => { const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => {
if (line.startsWith(phaseName)) { 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); } 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); } 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.activePhaseName = "body";
ctx.activePhaseLine = lineIdx; 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']; const closers = ['end metadata', 'end overview', 'end constants', 'end triggers', 'end mapping', 'end removal', 'end body'];

View File

@ -1,9 +1,6 @@
import { LinterContext } from '../LinterContext'; import { LinterContext } from '../LinterContext';
export interface LinterRule { export interface LinterRule {
// Wird für jede Code-Zeile aufgerufen checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void;
checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext): void;
// Wird ganz am Ende aufgerufen (z.B. um ungeschlossene Klammern zu prüfen)
checkEnd?(lines: string[], ctx: LinterContext): void; checkEnd?(lines: string[], ctx: LinterContext): void;
} }

View File

@ -1,232 +1,40 @@
import * as vscode from 'vscode'; 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 { export class TplCompletionProvider implements vscode.CompletionItemProvider {
async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.CompletionItem[]> { private strategies: CompletionStrategy[];
const linePrefix = document.lineAt(position).text.substring(0, position.character);
const items: vscode.CompletionItem[] = [];
const text = document.getText();
const importMap = new Map<string, string>(); constructor() {
const importRegex = /from\s+([\w.]+)\s+import\s+(\w+)/g; // Hier registrieren wir alle Completion Engines
let m; this.strategies = [
while ((m = importRegex.exec(text)) !== null) { new BuiltInCompletion(),
importMap.set(m[2], m[1]); new SnippetCompletion(),
} new LocalContextCompletion()
];
const localNamespaces = new Set<string>();
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<string>();
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<string>('metadata.author', 'Kuro');
const origin = config.get<string>('metadata.origin', 'Custom');
const publisher = config.get<string>('metadata.publisher', 'IT-Operations');
const treePathBase = config.get<string>('metadata.treePathBase', 'Grafing');
const tplVersion = config.get<number>('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;
} }
private extractDocString(text: string, startIndex: number): string | null { public provideCompletionItems(
const lines = text.substring(startIndex).split('\n'); document: vscode.TextDocument,
let docString = ""; position: vscode.Position,
let inDocString = false; token: vscode.CancellationToken,
let quoteType = ""; context: vscode.CompletionContext
for (let j = 1; j < Math.min(15, lines.length); j++) { ): vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList> {
const line = lines[j].trim();
if (line.length === 0) continue; // Den Text in der aktuellen Zeile bis zum Cursor holen
if (!inDocString) { const linePrefix = document.lineAt(position).text.substring(0, position.character);
if (line.startsWith('"""') || line.startsWith("'''")) { const results: vscode.CompletionItem[] = [];
inDocString = true;
quoteType = line.substring(0, 3); // Jede Engine um Vorschläge bitten
if (line.length > 3 && line.endsWith(quoteType)) { docString = line.substring(3, line.length - 3).trim(); break; } for (const strategy of this.strategies) {
const content = line.substring(3).trim(); const items = strategy.provideCompletions(document, position, linePrefix);
if (content) docString += content + "\n"; if (items && items.length > 0) {
} else { break; } results.push(...items);
} else {
if (line.endsWith(quoteType)) {
const content = line.substring(0, line.length - 3).trim();
if (content) docString += content;
break;
} else { docString += line + "\n"; }
} }
} }
return docString.trim() || null;
return results;
} }
} }

View File

@ -1,97 +1,48 @@
import * as vscode from 'vscode'; 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 { export class TplDocumentFormatter implements vscode.DocumentFormattingEditProvider {
provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): vscode.ProviderResult<vscode.TextEdit[]> { private indentRules = new IndentationRules();
const text = document.getText(); private alignRules = new AlignmentRules();
const isCRLF = text.includes('\r\n');
const lines = text.split(/\r?\n/); public provideDocumentFormattingEdits(
document: vscode.TextDocument,
options: vscode.FormattingOptions,
token: vscode.CancellationToken
): vscode.ProviderResult<vscode.TextEdit[]> {
const formattedLines: string[] = [];
const edits: vscode.TextEdit[] = []; const edits: vscode.TextEdit[] = [];
const state = new FormatState();
let indentLevel = 0; // Nutze die VS Code Settings (meist 4 Spaces), andernfalls Fallback
const indentString = ' '; state.tabSize = options.tabSize || 4;
let inTripleDouble = false;
let inTripleSingle = false;
let consecutiveEmptyLines = 0;
const alignmentStack: number[] = []; for (let i = 0; i < document.lineCount; i++) {
const originalLine = document.lineAt(i);
const rawText = originalLine.text;
const trimmedText = rawText.trim();
for (let i = 0; i < lines.length; i++) { // Leere Zeilen lassen wir leer
const rawLine = lines[i]; if (trimmedText.length === 0) {
let line = rawLine.trim(); if (rawText.length > 0) {
// Löscht unnötige Whitespaces in eigentlich leeren Zeilen
const tripleDoubleCount = (rawLine.match(/"""/g) || []).length; edits.push(vscode.TextEdit.replace(originalLine.range, ''));
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;
continue; continue;
} }
if (line.length === 0) { // 1. Basis-Einrückung (if/else/pattern) berechnen
consecutiveEmptyLines++; let formattedText = this.indentRules.apply(trimmedText, state);
if (consecutiveEmptyLines <= 1) { formattedLines.push(""); }
continue; // 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; return edits;

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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<number>('version', 23);
const author = config.get<string>('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;
}
}

View File

@ -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();
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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,');
});
});

View File

@ -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!"));
});
});