erkennung für fehlende end blocks, mehrzeilige if/for/while usw erkennung um keine semikolon fehler zu melden, besseres auslesen import defines, infos werden jetzt auch im Problems fenster gezeigt, bessere autovervollständigung, automatische fehlersuche über das projekt

This commit is contained in:
kuro 2026-10-12 21:43:46 +02:00
parent eba674218e
commit ba836ee30f
14 changed files with 481 additions and 194 deletions

View File

@ -62,7 +62,13 @@
"default": 23,
"description": "Aktuelle TPL-Nebenversion (z. B. 23 für TPL 1.23). Wird genutzt um veraltete Module zu erkennen und beim Generieren eingefügt."
}
},
"commands": [
{
"command": "tpl.scanWorkspace",
"title": "TPL: Gesamten Workspace prüfen"
}
]
},
"languages": [
{

View File

@ -35,6 +35,7 @@ export class LinterContext {
public parenStack: number[] = [];
public bracketStack: number[] = [];
public openDefines: number[] = [];
public openControlBlocks: { type: string, lineIdx: number }[] = [];
public addDiag(lineIdx: number, start: number, end: number, msg: string, severity: vscode.DiagnosticSeverity): vscode.Diagnostic {
const diag = new vscode.Diagnostic(new vscode.Range(lineIdx, start, lineIdx, end), msg, severity);

View File

@ -4,7 +4,6 @@ import { LinterContext, PatternPhase } from '../LinterContext';
export class BlockRule implements LinterRule {
// 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; }
@ -31,8 +30,10 @@ 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') {
// HIER GEFIXT: 'lines' wird jetzt korrekt an die Methode weitergereicht
this.checkPatternPhases(line, rawLine, lineIdx, ctx, lines);
}
@ -43,6 +44,38 @@ export class BlockRule implements LinterRule {
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 }); }
else if (/^for\b/.test(line)) { ctx.openControlBlocks.push({ type: 'for', lineIdx }); }
else if (/^while\b/.test(line)) { ctx.openControlBlocks.push({ type: 'while', lineIdx }); }
// Block wird geschlossen
if (line.startsWith('end if')) { this.closeControlBlock('if', line, lineIdx, ctx); }
else if (line.startsWith('end for')) { this.closeControlBlock('for', line, lineIdx, ctx); }
else if (line.startsWith('end while')) { this.closeControlBlock('while', line, lineIdx, ctx); }
}
private closeControlBlock(expectedType: string, line: string, lineIdx: number, ctx: LinterContext) {
if (ctx.openControlBlocks.length === 0) {
ctx.addDiag(lineIdx, 0, line.length, `'end ${expectedType}' gefunden, ohne offenen Block.`, vscode.DiagnosticSeverity.Error);
return;
}
// Wir nehmen den zuletzt geöffneten Block vom Stapel
const lastBlock = ctx.openControlBlocks.pop()!;
if (lastBlock.type !== expectedType) {
ctx.addDiag(lineIdx, 0, line.length, `Falsches Ende. Erwartet: 'end ${lastBlock.type};', gefunden: 'end ${expectedType};'.`, vscode.DiagnosticSeverity.Error);
// Damit Folgefehler vermieden werden, legen wir ihn zurück
ctx.openControlBlocks.push(lastBlock);
}
}
private trackModuleMetadata(line: string, lineIdx: number, ctx: LinterContext) {
@ -117,7 +150,6 @@ export class BlockRule implements LinterRule {
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();

View File

@ -4,11 +4,47 @@ import { LinterContext } from '../LinterContext';
export class SyntaxRule implements LinterRule {
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext): void {
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
this.checkTabs(rawLine, lineIdx, ctx);
this.checkBrackets(line, rawLine, lineIdx, ctx);
this.checkAssignments(rawLine, lineIdx, ctx);
this.checkSemicolons(line, 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);
}
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""');
const commentIdx = cleanLine.indexOf('//');
let codePart = commentIdx !== -1 ? cleanLine.substring(0, commentIdx).trim() : cleanLine.trim();
if (codePart.length === 0) return;
let isStructuralKeyword = /^(tpl|pattern|syncmapping|table|definitions?|define|enumeration|configuration|metadata|overview|constants|triggers|mapping|removal|body|end|on|tags|origin|author|products|publishers|tree_path|import|from)\b/.test(codePart);
if (codePart.startsWith('"') || codePart.startsWith("'")) {
isStructuralKeyword = true;
}
const isValidExecutionArea =
ctx.activePhaseName === 'body' ||
ctx.activePhaseName === 'triggers' ||
ctx.activePhaseName === 'constants' ||
ctx.activePhaseName === 'overview' ||
ctx.activePhaseName === 'mapping' ||
ctx.activePhaseName === 'metadata' ||
ctx.inModuleMetadata ||
ctx.inBlock === 'table' ||
ctx.inBlock === 'definitions' ||
ctx.openDefines.length > 0;
// 'then' und 'do' auf alleinstehenden Zeilen sind legitimer Code
if (!isStructuralKeyword && !isValidExecutionArea && !/^(then|do)\b/.test(codePart)) {
ctx.addDiag(lineIdx, 0, rawLine.length, "Ausführbarer Code ist hier nicht erlaubt. (Erlaubt in: 'body', 'triggers', 'constants', 'overview', 'mapping', 'metadata', 'definitions' oder 'define').", vscode.DiagnosticSeverity.Error);
}
this.checkBrackets(codePart, rawLine, lineIdx, ctx);
this.checkAssignments(codePart, rawLine, lineIdx, ctx);
this.checkSemicolons(codePart, rawLine, lineIdx, ctx, line);
}
public checkEnd(lines: string[], ctx: LinterContext): void {
@ -24,13 +60,9 @@ export class SyntaxRule implements LinterRule {
}
}
private checkBrackets(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '');
const commentIdx = cleanLine.indexOf('//');
if (commentIdx !== -1) { cleanLine = cleanLine.substring(0, commentIdx); }
for (let charIdx = 0; charIdx < cleanLine.length; charIdx++) {
const char = cleanLine[charIdx];
private checkBrackets(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
for (let charIdx = 0; charIdx < codePart.length; charIdx++) {
const char = codePart[charIdx];
if (char === '(') { ctx.parenStack.push(lineIdx); }
else if (char === ')') {
if (ctx.parenStack.length > 0) { ctx.parenStack.pop(); }
@ -44,13 +76,11 @@ export class SyntaxRule implements LinterRule {
}
}
private checkAssignments(rawLine: string, lineIdx: number, ctx: LinterContext) {
const lineWithoutStrings = rawLine.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, '""');
private checkAssignments(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
if (/\b(if|elif|where)\b/.test(codePart)) { ctx.inConditionBlock = true; }
if (/\b(if|elif|where)\b/.test(lineWithoutStrings)) { ctx.inConditionBlock = true; }
if (lineWithoutStrings.includes('=') && !lineWithoutStrings.includes(':=') && !lineWithoutStrings.includes('>=') && !lineWithoutStrings.includes('<=')) {
if (!ctx.inConditionBlock && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && !/\b(matches)\b/.test(lineWithoutStrings)) {
if (codePart.includes('=') && !codePart.includes(':=') && !codePart.includes('>=') && !codePart.includes('<=')) {
if (!ctx.inConditionBlock && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && !/\b(matches)\b/.test(codePart)) {
const idx = rawLine.indexOf('=');
if (idx !== -1) {
ctx.addDiag(lineIdx, idx, idx + 1, "Falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt.", vscode.DiagnosticSeverity.Error);
@ -58,21 +88,26 @@ export class SyntaxRule implements LinterRule {
}
}
if (/\b(then|do)\b/.test(lineWithoutStrings) || lineWithoutStrings.endsWith(';')) { ctx.inConditionBlock = false; }
if (/\b(then|do)\b/.test(codePart) || codePart.endsWith(';')) { ctx.inConditionBlock = false; }
}
private checkSemicolons(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
private checkSemicolons(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext, originalTrimmedLine: string) {
if (ctx.isClosingString) {
if (ctx.stringNeedsSemicolon && !line.endsWith(';')) {
if (ctx.stringNeedsSemicolon && !originalTrimmedLine.endsWith(';')) {
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error);
}
} else {
const noSemicolonNeeded = /^(pattern|syncmapping|traverse|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|\/\/|\/\*|\*)/;
const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches)$/i.test(line);
// "then" und "do" dürfen am Anfang einer Zeile stehen, ohne ein Semikolon zu erfordern
const noSemicolonNeeded = /^(pattern|syncmapping|traverse|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|then\b|do\b|\/\/|\/\*|\*)/.test(codePart);
if (!noSemicolonNeeded.test(line) && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine) {
const lineToCheck = line.split('//')[0].trim();
if (!lineToCheck.endsWith(';') && !lineToCheck.endsWith('{') && !lineToCheck.endsWith(',')) {
// "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);
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(',')) {
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error);
}
}

View File

@ -9,16 +9,14 @@ export function activate(context: vscode.ExtensionContext) {
const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner');
context.subscriptions.push(diagnosticCollection);
// Linter-Events binden
// Linter-Events für geöffnete/geänderte Dateien binden
vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection));
vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection));
// Live-Reload für Einstellungen
// Live-Reload für Einstellungen (Scannt das gesamte Projekt neu)
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('tpl')) {
for (const doc of vscode.workspace.textDocuments) {
validateTplDocument(doc, diagnosticCollection);
}
scanWorkspace(diagnosticCollection);
}
});
@ -28,8 +26,25 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.'));
context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider()));
// Bei Aktivierung offene Dokumente einmal scannen
for (const doc of vscode.workspace.textDocuments) {
validateTplDocument(doc, diagnosticCollection);
// NEU: Befehl registrieren, um manuell alle Dateien zu scannen
context.subscriptions.push(vscode.commands.registerCommand('tpl.scanWorkspace', () => {
scanWorkspace(diagnosticCollection);
vscode.window.showInformationMessage("TPL: Workspace-Scan gestartet.");
}));
// NEU: Bei Aktivierung der Extension direkt einmal alles prüfen!
scanWorkspace(diagnosticCollection);
}
// Die neue Engine für das projektweite Lesen
async function scanWorkspace(collection: vscode.DiagnosticCollection) {
// Sucht alle .tpl Dateien im gesamten geöffneten Projektordner
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
for (const file of files) {
// Öffnet die Datei "unsichtbar" im Hintergrund
const doc = await vscode.workspace.openTextDocument(file);
// Jagt sie durch unseren Linter
validateTplDocument(doc, collection);
}
}

View File

@ -8,7 +8,6 @@ export class TplCompletionProvider implements vscode.CompletionItemProvider {
private strategies: CompletionStrategy[];
constructor() {
// Hier registrieren wir alle Completion Engines
this.strategies = [
new BuiltInCompletion(),
new SnippetCompletion(),
@ -16,20 +15,18 @@ export class TplCompletionProvider implements vscode.CompletionItemProvider {
];
}
public provideCompletionItems(
public async provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken,
context: vscode.CompletionContext
): vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList> {
): Promise<vscode.CompletionItem[]> {
// 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);
const items = await strategy.provideCompletions(document, position, linePrefix);
if (items && items.length > 0) {
results.push(...items);
}

View File

@ -1,50 +1,76 @@
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<vscode.Hover | null> {
// Greift das Wort unter dem Mauszeiger ab, inklusive Punkte für Namespaces
public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | null> {
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
if (!range) {
return null;
}
if (!range) return null;
const word = document.getText(range); // z.B. "modelling.testDefine"
const text = document.getText();
const lines = text.split('\n');
const word = document.getText(range);
const parts = word.split('.');
const importBase = parts[0];
const funcName = parts.length > 1 ? parts[1] : word;
let namespace = '';
let methodOrPatternName = '';
// 1. Lokale Defines
if (parts.length === 1 || parts.length === 2) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const defMatch = line.match(new RegExp(`^\\s*define\\s+${funcName}\\b(?:\\s*\\((.*?)\\))?`));
if (defMatch) {
const params = defMatch[1] ? defMatch[1].trim() : '';
const md = new vscode.MarkdownString();
md.appendCodeblock(`define ${funcName}(${params})`, 'tpl');
if (parts.length === 2) {
namespace = parts[0];
methodOrPatternName = parts[1];
} else if (parts.length === 1) {
methodOrPatternName = parts[0];
} else {
return null;
const textAfterDefine = text.substring(text.indexOf(line) + line.length);
const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
if (docMatch) { md.appendMarkdown(`\n\n---\n\n${docMatch[1].trim()}`); }
return new vscode.Hover(md);
}
}
}
// BMC Namespace-Methoden (z.B. discovery.listDirectory)
if (namespace && tplBuiltIns[namespace] && tplBuiltIns[namespace][methodOrPatternName]) {
const def = tplBuiltIns[namespace][methodOrPatternName];
const markdown = new vscode.MarkdownString();
markdown.appendCodeblock(`${namespace}.${methodOrPatternName}`, 'tpl');
markdown.appendMarkdown(`---\n\n${def.description}`);
return new vscode.Hover(markdown);
// 2. 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) {
const doc = await vscode.workspace.openTextDocument(file);
const docText = doc.getText();
if (new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${moduleName.replace(/\./g, '\\.')}\\s*;`).test(docText)) {
const docLines = docText.split('\n');
for (let i = 0; i < docLines.length; i++) {
const line = docLines[i];
const defMatch = line.match(new RegExp(`^\\s*define\\s+${funcName}\\b(?:\\s*\\((.*?)\\))?`));
if (defMatch) {
const params = defMatch[1] ? defMatch[1].trim() : '';
const md = new vscode.MarkdownString();
md.appendCodeblock(`define ${funcName}(${params})`, 'tpl');
const textAfterDefine = docText.substring(docText.indexOf(line) + line.length);
const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
if (docMatch) { md.appendMarkdown(`\n\n---\n\n${docMatch[1].trim()}`); }
md.appendMarkdown(`\n\n*(Importiert aus \`${moduleName}\`)*`);
return new vscode.Hover(md);
}
// BMC Globale Funktionen (z.B. size, datatype)
else if (!namespace && tplBuiltIns['built_in_globals'] && tplBuiltIns['built_in_globals'][methodOrPatternName]) {
const def = tplBuiltIns['built_in_globals'][methodOrPatternName];
const markdown = new vscode.MarkdownString();
markdown.appendCodeblock(`${methodOrPatternName}`, 'tpl');
markdown.appendMarkdown(`---\n\n${def.description}`);
return new vscode.Hover(markdown);
}
}
}
}
// 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;
}

View File

@ -8,14 +8,12 @@ export class MetadataFix implements TplQuickFix {
const fix = new vscode.CodeAction("Fehlende Metadaten generieren", vscode.CodeActionKind.QuickFix);
fix.edit = new vscode.WorkspaceEdit();
// Einstellungen 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');
// Herausfinden, welche Felder fehlen (steht im Diagnostic-Message-String)
const missingFieldsStr = diagnostic.message.replace('Fehlende Pflicht-Metadaten (Modul-Ebene): ', '');
const missingFields = missingFieldsStr.split(', ');
@ -26,8 +24,8 @@ 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`;
// Direkt über der Zeile 'end metadata;' einfügen
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line, 0), insertText);
// 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];
return [fix];

View File

@ -1,14 +1,9 @@
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;
): vscode.ProviderResult<vscode.CompletionItem[] | undefined>;
}

View File

@ -2,12 +2,103 @@ import * as vscode from 'vscode';
import { CompletionStrategy } from './CompletionStrategy';
export class LocalContextCompletion implements CompletionStrategy {
public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise<vscode.CompletionItem[] | undefined> {
const text = document.getText();
const items: vscode.CompletionItem[] = [];
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
// 1. WENN DER USER NACH EINEM PUNKT TIPPT (Cross-File Lookup)
const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+)\.$/);
if (dotMatch) {
const prefix = dotMatch[1]; // z.B. "modelling"
if (prefix === 'log' || prefix === 'discovery') return undefined; // Built-ins ignorieren
return undefined;
// 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 externalItems = await this.resolveImportedMethods(moduleName, prefix);
return externalItems; // Nur externe Methoden zurückgeben
}
return undefined; // Unbekannter Namespace
}
// 2. LOKALE DEFINITIONS & IMPORTS PARSEN
const lines = text.split('\n');
let currentDefBlock = "";
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const defBlockMatch = line.match(/^\s*definitions?\s+([a-zA-Z0-9_]+)/);
if (defBlockMatch) { currentDefBlock = defBlockMatch[1]; continue; }
if (line.match(/^\s*end\s+definitions?/)) { currentDefBlock = ""; continue; }
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b/);
if (defineMatch) {
const funcName = defineMatch[1];
const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName;
const item = new vscode.CompletionItem(fullName, vscode.CompletionItemKind.Function);
item.detail = currentDefBlock ? `Local Function in '${currentDefBlock}'` : `Local Function`;
item.insertText = new vscode.SnippetString(`${fullName}(\${1:params});$0`);
items.push(item);
}
}
// 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) {
const item = new vscode.CompletionItem(matchVar[2], vscode.CompletionItemKind.Module);
item.detail = `Imported Definitions from ${matchVar[1]}`;
items.push(item);
}
const varRegex = /([a-zA-Z0-9_]+)\s*:=/g;
const foundVars = new Set<string>();
while ((matchVar = varRegex.exec(text)) !== null) {
if (matchVar[1] !== 'host') { foundVars.add(matchVar[1]); }
}
for (const v of foundVars) {
items.push(new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable));
}
return items;
}
// Hilfsmethode: Sucht im Workspace nach der anderen Datei!
private async resolveImportedMethods(moduleName: string, defBlockName: string): Promise<vscode.CompletionItem[]> {
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) {
if (line.match(new RegExp(`^\\s*definitions?\\s+${defBlockName}\\b`))) { inTargetBlock = true; continue; }
if (inTargetBlock && line.match(/^\s*end\s+definitions?/)) { inTargetBlock = false; break; }
if (inTargetBlock) {
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b/);
if (defineMatch) {
const funcName = defineMatch[1];
const item = new vscode.CompletionItem(funcName, vscode.CompletionItemKind.Function);
item.detail = `Method from ${moduleName}`;
item.insertText = new vscode.SnippetString(`${funcName}(\${1:params});$0`);
items.push(item);
}
}
}
break; // Datei gefunden und geparst, wir können abbrechen
}
}
return items;
}
}

View File

@ -4,95 +4,90 @@ 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;
}
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);
// 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 = ''; }
}
}
// ==========================================
// 2. DAS GOD-SNIPPET (Skeleton)
// 1. GLOBAL (Top-Level: Vor dem Pattern / Import-Bereich)
// ==========================================
if (inBlock === 'none') {
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";
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);
// ==========================================
// 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;
['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);
}
// ==========================================
// 4. SECTIONS (metadata, body, triggers, etc.)
// 2. INNERHALB PATTERN (aber VOR dem Body)
// ==========================================
const sections = ['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body'];
for (const sec of sections) {
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};`);
item.detail = `${sec} ... end ${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);
}
// ==========================================
// 5. KONTROLLSTRUKTUREN & TABELLEN
// 3. INNERHALB VON BODY ODER DEFINE (Ausführbarer Code)
// ==========================================
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;');
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,34 @@
import * as assert from 'assert';
import * as vscode from 'vscode';
import { TabFix } from '../../providers/actions/TabFix';
import { LogFix } from '../../providers/actions/LogFix';
suite('Code Actions Test Suite', () => {
test('TabFix: Erzeugt Replace-Edit für Tabulatoren', () => {
const fix = new TabFix();
const diag = new vscode.Diagnostic(new vscode.Range(0, 0, 0, 1), "Tab found", vscode.DiagnosticSeverity.Warning);
// Mock für TextDocument
const mockDoc = {
uri: vscode.Uri.file('test.tpl'),
lineAt: () => ({ text: '\t\tcode', range: new vscode.Range(0, 0, 0, 6) })
} as any;
const actions = fix.provideFix(mockDoc, diag);
assert.strictEqual(actions.length, 1);
assert.strictEqual(actions[0].title, "Tabs in 4 Spaces umwandeln");
assert.ok(actions[0].edit !== undefined);
});
test('LogFix: Erzeugt Insert-Edit mit Log-Statement', () => {
const fix = new LogFix();
const diag = new vscode.Diagnostic(new vscode.Range(10, 0, 10, 4), "Missing log");
diag.source = 'log.debug("Start pattern Test");'; // Linter-Source Simulation
const mockDoc = { uri: vscode.Uri.file('test.tpl') } as any;
const actions = fix.provideFix(mockDoc, diag);
assert.strictEqual(actions.length, 1);
assert.strictEqual(actions[0].title, "Fehlendes Log-Statement einfügen");
});
});

View File

@ -0,0 +1,38 @@
import * as assert from 'assert';
import { BuiltInCompletion } from '../../providers/completions/BuiltInCompletion';
import { SnippetCompletion } from '../../providers/completions/SnippetCompletion';
suite('Completion Provider Test Suite', () => {
test('BuiltInCompletion: Reagiert auf "log."', () => {
const strategy = new BuiltInCompletion();
// Wir faken Document und Position, da wir nur linePrefix brauchen
const result = strategy.provideCompletions(null as any, null as any, ' log.');
assert.ok(result !== undefined);
assert.strictEqual(result.length, 4); // debug, info, warn, error
assert.strictEqual(result[0].label, 'debug');
});
test('BuiltInCompletion: Ignoriert normalen Text', () => {
const strategy = new BuiltInCompletion();
const result = strategy.provideCompletions(null as any, null as any, ' my_var');
assert.strictEqual(result, undefined);
});
test('SnippetCompletion: Liefert Snippets für normalen Text', () => {
const strategy = new SnippetCompletion();
const result = strategy.provideCompletions(null as any, null as any, ' i');
assert.ok(result !== undefined);
const hasIf = result.some(item => item.label === 'if');
assert.ok(hasIf, "Muss 'if' Snippet vorschlagen");
});
test('SnippetCompletion: Blockiert Snippets nach Punkt', () => {
const strategy = new SnippetCompletion();
const result = strategy.provideCompletions(null as any, null as any, ' discovery.r');
assert.strictEqual(result, undefined, "Darf keine Block-Snippets in Namespaces anzeigen");
});
});

View File

@ -1,45 +1,69 @@
import * as assert from 'assert';
import * as vscode from 'vscode';
import { LinterContext } from '../../core/LinterContext';
import { SyntaxRule } from '../../core/rules/SyntaxRule';
import { BlockRule } from '../../core/rules/BlockRule';
suite('Linter SyntaxRule Test Suite', () => {
test('Zuweisung: Warnt bei "=" statt ":="', () => {
suite('Linter Test Suite', () => {
test('SyntaxRule: Warnt bei "=" Zuweisung außerhalb von Bedingungen', () => {
const rule = new SyntaxRule();
const ctx = new LinterContext();
ctx.activePhaseName = 'body'; // Kontext setzen, damit der strict-mode nicht meckert
// Wir simulieren eine Code-Zeile
const rawLine = " my_var = 123;";
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
// HIER GEFIXT: Wir übergeben ein Array als 5. Argument
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
// 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"));
const diag = ctx.diagnostics.find(d => d.message.includes("':=' für Zuweisungen"));
assert.ok(diag !== undefined, "Fehler für falsche Zuweisung erwartet");
});
test('Zuweisung: Akzeptiert "=" als Vergleich in if-Bedingung', () => {
test('SyntaxRule: Akzeptiert "=" als Vergleich in Bedingungen', () => {
const rule = new SyntaxRule();
const ctx = new LinterContext();
ctx.activePhaseName = 'body';
// 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);
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
// Erwartung: Kein Fehler, da das = als Vergleich legitim ist
// Darf keinen Fehler werfen, da inConditionBlock automatisch auf true geht
assert.strictEqual(ctx.diagnostics.length, 0);
});
test('Klammern: Findet ungeschlossene runde Klammern', () => {
test('SyntaxRule: Findet fehlendes Semikolon', () => {
const rule = new SyntaxRule();
const ctx = new LinterContext();
ctx.activePhaseName = 'body';
// Wir öffnen eine Klammer, schließen sie aber nicht
const rawLine = " my_func(parameter;";
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
const rawLine = " my_var := 123";
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
// checkEnd wird am Ende der Datei aufgerufen
rule.checkEnd(['', rawLine], ctx);
const diag = ctx.diagnostics.find(d => d.message.includes("Fehlendes Semikolon"));
assert.ok(diag !== undefined, "Fehler für fehlendes Semikolon erwartet");
});
test('BlockRule: Erzwingt Module-Header', () => {
const rule = new BlockRule();
const ctx = new LinterContext();
const rawLine = "pattern Test 1.0";
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
assert.strictEqual(ctx.diagnostics.length, 1);
assert.ok(ctx.diagnostics[0].message.includes("Öffnende Klammer '(' wird nicht geschlossen!"));
assert.ok(ctx.diagnostics[0].message.includes("tpl 1.x module"));
});
test('BlockRule: Findet fehlendes Pattern-Log im Body', () => {
const rule = new BlockRule();
const ctx = new LinterContext();
ctx.hasModuleDef = true;
ctx.inBlock = 'pattern';
ctx.currentPatternName = 'TestPattern';
const lines = ['body', ' x := 1;', 'end body;'];
rule.checkLine('body', 'body', 0, ctx, lines);
const logDiag = ctx.diagnostics.find(d => d.code === 'missing_log_start');
assert.ok(logDiag !== undefined, "Erwartet Fehler für fehlendes Log-Statement");
});
});