started translating every info into english, started adding some functionality for better workspace awareness

This commit is contained in:
kuro 2026-10-18 04:38:07 +02:00
parent d6e97bea98
commit 0063862a24
5 changed files with 174 additions and 38 deletions

View File

@ -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,9 +51,12 @@ 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);
}

119
src/core/WorkspaceIndex.ts Normal file
View File

@ -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<string, Map<string, TplFunctionSignature>>();
// Map: Datei-Pfad -> Modulname
private fileModules = new Map<string, string>();
// Map: Modulname -> Set von Definitions-Blöcken (für die Autovervollständigung beim Import)
private moduleDefinitions = new Map<string, Set<string>>();
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<string, TplFunctionSignature>();
const defBlocks = new Set<string>();
// 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();

View File

@ -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 <name>;' beginnen.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; }
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Every file has to start with 'tpl 1.x module <name>;'", 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 <Name> <Version>'", vscode.DiagnosticSeverity.Error); }
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Invalid Pattern declaration. Expected: 'pattern <Name> <Version>'", 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); }
}
}
}

View File

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

View File

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