diff --git a/src/core/TplLinter.ts b/src/core/TplLinter.ts index d8e920e..08addbe 100644 --- a/src/core/TplLinter.ts +++ b/src/core/TplLinter.ts @@ -3,9 +3,7 @@ import { LinterContext } from './LinterContext'; import { SyntaxRule } from './rules/SyntaxRule'; import { BlockRule } from './rules/BlockRule'; import { MetadataRule } from './rules/MetadataRule'; -import { IndentationRule } from './rules/IndentationRule'; // 1. Hier importieren - -// ... (deine eventuellen anderen Hilfs-Imports wie trackStrings) ... +import { IndentationRule } from './rules/IndentationRule'; export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) { if (document.languageId !== 'tpl') {return;} @@ -14,7 +12,6 @@ export function validateTplDocument(document: vscode.TextDocument, collection: v const lines = text.split('\n'); const ctx = new LinterContext(); - // 2. Hier nur die 3 existierenden Regeln reinlegen (MetadataRule entfernt!) const rules = [ new SyntaxRule(), new MetadataRule(), @@ -28,7 +25,12 @@ export function validateTplDocument(document: vscode.TextDocument, collection: v ctx.resetLineStates(); - // Basis-Lexer: Strings & Kommentare tracken (läuft vor den Regeln!) + // NEU: TODO-Check hier im Linter, BEVOR Kommentare ignoriert werden! + const todoMatch = rawLine.match(/\bTODO\b\s*:?\s*(.*)/i); + if (todoMatch) { + ctx.addDiag(i, todoMatch.index!, rawLine.length, `TODO: ${todoMatch[1].trim()}`, vscode.DiagnosticSeverity.Information); + } + trackStrings(rawLine, line, ctx); if (ctx.isInternalStringLine && (ctx.inTripleDouble || ctx.inTripleSingle)) { continue; } @@ -49,8 +51,11 @@ export function validateTplDocument(document: vscode.TextDocument, collection: v } } + // Sicherer Aufruf für alle Regeln for (const rule of rules) { + if (rule.checkEnd) { rule.checkEnd(lines, ctx); + } } collection.set(document.uri, ctx.diagnostics); diff --git a/src/core/WorkspaceIndex.ts b/src/core/WorkspaceIndex.ts new file mode 100644 index 0000000..bd78488 --- /dev/null +++ b/src/core/WorkspaceIndex.ts @@ -0,0 +1,119 @@ +import * as vscode from 'vscode'; + +export interface TplFunctionSignature { + name: string; // z.B. "modelling.doWork" + moduleName: string; // z.B. "Company.software_modelling" + params: string[]; // z.B. ["host", "ip_address"] + docString?: string; // Extrahierter Kommentar +} + +export class WorkspaceIndex { + // Map: Datei-Pfad -> Map von Funktionen (Funktionsname -> Signatur) + private fileIndex = new Map>(); + + // Map: Datei-Pfad -> Modulname + private fileModules = new Map(); + + // Map: Modulname -> Set von Definitions-Blöcken (für die Autovervollständigung beim Import) + private moduleDefinitions = new Map>(); + + public updateFile(uri: vscode.Uri, text: string) { + if (!uri.fsPath.endsWith('.tpl')) return; + + const fsPath = uri.fsPath; + const lines = text.split('\n'); + let currentModule = ""; + let currentDefBlock = ""; + const functions = new Map(); + const defBlocks = new Set(); + + // 1. Modulnamen parsen + const modMatch = text.match(/tpl\s+1\.\d+\s+module\s+([a-zA-Z0-9_.]+)\s*;/); + if (modMatch) { + currentModule = modMatch[1]; + } + this.fileModules.set(fsPath, currentModule); + + // 2. Definitions-Blöcke und Funktionen parsen + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Startet ein Definitions-Block? + const defBlockMatch = line.match(/^\s*definitions?\s+([a-zA-Z0-9_]+)/); + if (defBlockMatch) { + currentDefBlock = defBlockMatch[1]; + defBlocks.add(currentDefBlock); // Block merken! + continue; + } + if (line.match(/^\s*end\s+definitions?/)) { + currentDefBlock = ""; + continue; + } + + // Sucht nach 'define name(param1, param2)' + const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/); + if (defineMatch) { + const funcName = defineMatch[1]; + const paramString = defineMatch[2] || ""; + const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName; + + // Parameter säubern und zählen + const params = paramString.split(',').map(p => p.trim()).filter(p => p.length > 0); + + // DocString extrahieren + let docString = undefined; + const textAfterDefine = text.substring(text.indexOf(line) + line.length); + const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/); + if (docMatch) { docString = docMatch[1].trim(); } + + functions.set(fullName, { + name: fullName, + moduleName: currentModule, + params: params, + docString: docString + }); + } + } + + // In den RAM schreiben + this.fileIndex.set(fsPath, functions); + if (currentModule) { + this.moduleDefinitions.set(currentModule, defBlocks); + } + } + + public removeFile(uri: vscode.Uri) { + const moduleName = this.fileModules.get(uri.fsPath); + if (moduleName) { + this.moduleDefinitions.delete(moduleName); + } + this.fileIndex.delete(uri.fsPath); + this.fileModules.delete(uri.fsPath); + } + + // --- HILFSFUNKTIONEN FÜR PROVIDER UND LINTER --- + + // Liefert alle Module (z.B. für "from [Autocomplete] import ...") + public getAllModules(): string[] { + return Array.from(this.moduleDefinitions.keys()); + } + + // Liefert alle Definitions-Blöcke eines Moduls (z.B. für "from XYZ import [Autocomplete]") + public getDefinitionBlocksForModule(moduleName: string): string[] { + const blocks = this.moduleDefinitions.get(moduleName); + return blocks ? Array.from(blocks) : []; + } + + // Liefert alle Funktionen eines Moduls (für "modelling.[Autocomplete]") + public getFunctionsForModule(moduleName: string): TplFunctionSignature[] { + const result: TplFunctionSignature[] = []; + for (const [fsPath, map] of this.fileIndex.entries()) { + if (this.fileModules.get(fsPath) === moduleName) { + result.push(...map.values()); + } + } + return result; + } +} + +export const workspaceIndex = new WorkspaceIndex(); \ No newline at end of file diff --git a/src/core/rules/BlockRule.ts b/src/core/rules/BlockRule.ts index 8143410..164c239 100644 --- a/src/core/rules/BlockRule.ts +++ b/src/core/rules/BlockRule.ts @@ -7,7 +7,7 @@ export class BlockRule implements LinterRule { 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; } + else { ctx.addDiag(lineIdx, 0, rawLine.length, "Every file has to start with 'tpl 1.x module ;'", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; } } // Wenn wir im Metadata-Block sind, ignorieren wir den Rest. Die MetadataRule übernimmt! @@ -17,7 +17,7 @@ export class BlockRule implements LinterRule { if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) { if (!ctx.hasModuleMetadata && ctx.inBlock === 'none') { - ctx.addDiag(lineIdx, 0, line.length, "Ein 'metadata' Block auf Modul-Ebene ist Pflicht, bevor andere Blöcke definiert werden.", vscode.DiagnosticSeverity.Error); + ctx.addDiag(lineIdx, 0, line.length, "A 'metadata' block on modul-level is needed before any other blocks can be defined.", vscode.DiagnosticSeverity.Error); } } @@ -33,10 +33,10 @@ export class BlockRule implements LinterRule { public checkEnd(lines: string[], ctx: LinterContext): void { for (const unclosedLine of ctx.openDefines) { - ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' wurde nicht mit 'end define;' geschlossen!", vscode.DiagnosticSeverity.Error); + ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' was not ended with 'end define;'!", vscode.DiagnosticSeverity.Error); } 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); + ctx.addDiag(block.lineIdx, 0, lines[block.lineIdx].length, `Block '${block.type}' was not ended with 'end${block.type};'!`, vscode.DiagnosticSeverity.Error); } } @@ -52,33 +52,33 @@ export class BlockRule implements LinterRule { 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); + ctx.addDiag(lineIdx, 0, line.length, `'end ${expectedType}' found without open block.`, vscode.DiagnosticSeverity.Error); return; } 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); + ctx.addDiag(lineIdx, 0, line.length, `Wrong end. Expected: 'end ${lastBlock.type};', found: 'end${expectedType};'.`, vscode.DiagnosticSeverity.Error); ctx.openControlBlocks.push(lastBlock); } } private handleBlockOpenings(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { if (line.startsWith('definitions ') || line.startsWith('definition ')) { - if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Ein Definitions-Block muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); } + if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A defintions block has to be declared on the highest level.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'definitions'; } else if (line.startsWith('define ')) { ctx.openDefines.push(lineIdx); } else if (line.startsWith('enumeration ')) { - if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Eine Enumeration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); } + if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A enumeration has to be declared on the highest level.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'enumeration'; } else if (line.startsWith('configuration ')) { - if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Eine Konfiguration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); } + if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A configuration has to be declared on the highest level.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'configuration'; } else if (line.startsWith('pattern ') || line.startsWith('syncmapping ')) { - if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 7, "Ein Pattern/Syncmapping kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); } + if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 7, "A Pattern/Syncmapping can not be declared inside another block.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'pattern'; ctx.currentPatternPhase = PatternPhase.None; ctx.activePhaseName = ""; @@ -86,10 +86,10 @@ export class BlockRule implements LinterRule { const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/); if (nameMatch) { ctx.currentPatternName = nameMatch[2]; } - else { ctx.addDiag(lineIdx, 0, rawLine.length, "Ungültige Pattern-Deklaration. Erwartet: 'pattern '", vscode.DiagnosticSeverity.Error); } + else { ctx.addDiag(lineIdx, 0, rawLine.length, "Invalid Pattern declaration. Expected: 'pattern '", vscode.DiagnosticSeverity.Error); } } else if (line.startsWith('table ')) { - if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); } + if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 5, "A table can not be declared inside another block.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'table'; } } @@ -127,7 +127,7 @@ export class BlockRule implements LinterRule { 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); + const diag = ctx.addDiag(lineIdx, 0, rawLine.length, `Inside the Body the Log-Statement is missing, e.g. Pattern-Namen: ${expectedLog}`, vscode.DiagnosticSeverity.Error); diag.code = 'missing_log_start'; diag.source = expectedLog; } @@ -146,19 +146,19 @@ export class BlockRule implements LinterRule { private checkEndBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { if (line.startsWith('end pattern') || line.startsWith('end syncmapping')) { 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.inBlock !== 'pattern') { ctx.addDiag(lineIdx, 0, 11, "'end pattern' gefunden, ohne offenen Pattern-Block.", vscode.DiagnosticSeverity.Error); } + if (ctx.inBlock !== 'pattern') { ctx.addDiag(lineIdx, 0, 11, "'end pattern' found without open pattern block.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'none'; } else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) { ctx.inBlock = 'none'; } else if (line.startsWith('end definitions') || line.startsWith('end definition')) { - if (ctx.inBlock !== 'definitions') { ctx.addDiag(lineIdx, 0, line.length, "'end definitions' gefunden, ohne offenen Definitions-Block.", vscode.DiagnosticSeverity.Error); } + if (ctx.inBlock !== 'definitions') { ctx.addDiag(lineIdx, 0, line.length, "'end definitions' found without open definitions block.", vscode.DiagnosticSeverity.Error); } ctx.inBlock = 'none'; } else if (line.startsWith('end define')) { if (ctx.openDefines.length > 0) { ctx.openDefines.pop(); } - else { ctx.addDiag(lineIdx, 0, line.length, "'end define' gefunden, ohne offenes 'define'.", vscode.DiagnosticSeverity.Error); } + else { ctx.addDiag(lineIdx, 0, line.length, "'end define' found without open define.", vscode.DiagnosticSeverity.Error); } } } } \ No newline at end of file diff --git a/src/core/rules/SyntaxRule.ts b/src/core/rules/SyntaxRule.ts index 88cfcf1..594f1d1 100644 --- a/src/core/rules/SyntaxRule.ts +++ b/src/core/rules/SyntaxRule.ts @@ -7,11 +7,6 @@ export class SyntaxRule implements LinterRule { public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void { this.checkTabs(rawLine, lineIdx, ctx); - 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(); @@ -96,10 +91,8 @@ export class SyntaxRule implements LinterRule { ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error); } } else { - // "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); - // "in" (für for-Schleifen) als gültigen Zeilenumbruch hinzugefügt const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches|in)$/i.test(codePart); const endsWithBlockOpener = /\b(then|do)$/.test(codePart); diff --git a/src/extension.ts b/src/extension.ts index a6ec002..8c3495c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,16 +4,34 @@ import { TplCodeActionProvider } from './providers/TplCodeActionProvider'; import { TplDocumentFormatter } from './providers/TplDocumentFormatter'; import { TplCompletionProvider } from './providers/TplCompletionProvider'; import { TplHoverProvider } from './providers/TplHoverProvider'; +import { workspaceIndex } from './core/WorkspaceIndex'; // <-- Index importiert export function activate(context: vscode.ExtensionContext) { const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner'); context.subscriptions.push(diagnosticCollection); - // 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 INDEXING & LINTING + vscode.workspace.onDidOpenTextDocument(doc => { + if (doc.languageId === 'tpl') { + workspaceIndex.updateFile(doc.uri, doc.getText()); + validateTplDocument(doc, diagnosticCollection); + } + }); + + vscode.workspace.onDidChangeTextDocument(event => { + if (event.document.languageId === 'tpl') { + workspaceIndex.updateFile(event.document.uri, event.document.getText()); + validateTplDocument(event.document, diagnosticCollection); + } + }); + + // Aufräumen, wenn Dateien gelöscht werden + vscode.workspace.onDidDeleteFiles(event => { + for (const uri of event.files) { + workspaceIndex.removeFile(uri); + } + }); - // Live-Reload für Einstellungen (Scannt das gesamte Projekt neu) vscode.workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('tpl')) { scanWorkspace(diagnosticCollection); @@ -26,25 +44,26 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.')); context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider())); - // 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! + // Initialer Scan beim Start scanWorkspace(diagnosticCollection); } -// Die neue Engine für das projektweite Lesen +// Füttert RAM und Linter 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 + + // ZUERST in den RAM laden... + workspaceIndex.updateFile(doc.uri, doc.getText()); + + // ... DANN linten validateTplDocument(doc, collection); } } \ No newline at end of file