2026-10-07 21:34:05 +00:00
import * as vscode from 'vscode' ;
import { TplCodeActionProvider } from './TplCodeActionProvider' ;
import { TplDocumentFormatter } from './TplDocumentFormatter' ;
2026-10-08 20:02:08 +00:00
import { TplCompletionProvider } from './TplCompletionProvider' ;
import { TplHoverProvider } from './TplHoverProvider' ;
2026-10-08 22:36:29 +00:00
import { tplBuiltIns } from './TplBuiltIns' ;
2026-10-07 21:34:05 +00:00
2026-10-08 20:02:08 +00:00
enum PatternPhase { None = 0 , Metadata = 1 , Overview = 2 , Constants = 3 , Triggers = 4 , Mapping = 5 , Removal = 6 , Body = 7 }
2026-10-07 21:34:05 +00:00
export function activate ( context : vscode.ExtensionContext ) {
const diagnosticCollection = vscode . languages . createDiagnosticCollection ( 'tpl-scanner' ) ;
context . subscriptions . push ( diagnosticCollection ) ;
vscode . workspace . onDidOpenTextDocument ( doc = > validateTplDocument ( doc , diagnosticCollection ) ) ;
vscode . workspace . onDidChangeTextDocument ( event = > validateTplDocument ( event . document , diagnosticCollection ) ) ;
2026-10-09 21:14:30 +00:00
// NEU: Sofort reagieren, wenn in den VS Code Einstellungen etwas geändert wird
vscode . workspace . onDidChangeConfiguration ( e = > {
if ( e . affectsConfiguration ( 'tpl' ) ) {
for ( const doc of vscode . workspace . textDocuments ) {
validateTplDocument ( doc , diagnosticCollection ) ;
}
}
} ) ;
2026-10-07 21:34:05 +00:00
2026-10-08 20:02:08 +00:00
context . subscriptions . push ( vscode . languages . registerCodeActionsProvider ( 'tpl' , new TplCodeActionProvider ( ) , { providedCodeActionKinds : [ vscode . CodeActionKind . QuickFix ] } ) ) ;
context . subscriptions . push ( vscode . languages . registerDocumentFormattingEditProvider ( 'tpl' , new TplDocumentFormatter ( ) ) ) ;
context . subscriptions . push ( vscode . languages . registerCompletionItemProvider ( 'tpl' , new TplCompletionProvider ( ) , '.' ) ) ;
context . subscriptions . push ( vscode . languages . registerHoverProvider ( 'tpl' , new TplHoverProvider ( ) ) ) ;
for ( const doc of vscode . workspace . textDocuments ) {
validateTplDocument ( doc , diagnosticCollection ) ;
}
2026-10-07 21:34:05 +00:00
}
function validateTplDocument ( document : vscode . TextDocument , collection : vscode.DiagnosticCollection ) {
if ( document . languageId !== 'tpl' ) { return ; }
2026-10-09 21:14:30 +00:00
const config = vscode . workspace . getConfiguration ( 'tpl' ) ;
const configuredTplVersion = config . get < number > ( 'version' , 23 ) ;
2026-10-07 21:34:05 +00:00
const diagnostics : vscode.Diagnostic [ ] = [ ] ;
const text = document . getText ( ) ;
const lines = text . split ( '\n' ) ;
let tplVersion = 10 ;
let hasModuleDef = false ;
2026-10-08 20:02:08 +00:00
let inBlock : 'none' | 'pattern' | 'table' | 'definitions' | 'enumeration' | 'configuration' = 'none' ;
2026-10-07 21:34:05 +00:00
let currentPatternPhase : PatternPhase = PatternPhase . None ;
let inMultiLineComment = false ;
2026-10-08 20:02:08 +00:00
let inTripleDouble = false ;
let inTripleSingle = false ;
let stringNeedsSemicolon = false ;
2026-10-07 21:34:05 +00:00
2026-10-08 20:02:08 +00:00
let activePhaseName = "" ;
let activePhaseLine = - 1 ;
2026-10-07 21:34:05 +00:00
let currentPatternName = "" ;
2026-10-08 20:02:08 +00:00
// Modul-Ebene Metadaten Tracking
let inModuleMetadata = false ;
let hasModuleMetadata = false ;
let module MetadataBlockLine = - 1 ;
let foundModuleMetadata = { origin : false , author : false , products : false , publishers : false , tree_path : false } ;
const parenStack : number [ ] = [ ] ;
const bracketStack : number [ ] = [ ] ;
const openDefines : number [ ] = [ ] ;
2026-10-09 21:14:30 +00:00
// State-Tracking für mehrzeilige Vergleiche / Zuweisungen
let inConditionBlock = false ;
2026-10-07 21:34:05 +00:00
function 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 ) ;
diagnostics . push ( diag ) ;
return diag ;
}
2026-10-09 21:14:30 +00:00
// Version prüfen
const versionMatch = text . match ( /tpl\s+1\.(\d+)\s+module/ ) ;
if ( versionMatch ) {
tplVersion = parseInt ( versionMatch [ 1 ] , 10 ) ;
// Warnung nur, wenn die Datei älter ist als die in den Settings eingestellte Version! Höher ist in Ordnung.
if ( tplVersion < configuredTplVersion ) {
const linesToSearch = text . substring ( 0 , versionMatch . index ! + 30 ) . split ( '\n' ) ;
const lineIdx = linesToSearch . length - 1 ;
addDiag ( lineIdx , 0 , versionMatch [ 0 ] . length , ` Das Modul nutzt eine alte TPL Version (1. ${ tplVersion } ). Aktuell in den Settings definiert ist 1. ${ configuredTplVersion } . ` , vscode . DiagnosticSeverity . Warning ) ;
}
}
2026-10-07 21:34:05 +00:00
for ( let i = 0 ; i < lines . length ; i ++ ) {
const rawLine = lines [ i ] ;
const line = rawLine . trim ( ) ;
2026-10-08 20:02:08 +00:00
const tripleDoubleCount = ( rawLine . match ( /"""/g ) || [ ] ) . length ;
const tripleSingleCount = ( rawLine . match ( /'''/g ) || [ ] ) . length ;
let isInternalStringLine = false ;
let justOpenedString = false ;
let isClosingString = false ;
if ( inTripleDouble ) {
isInternalStringLine = true ;
if ( tripleDoubleCount % 2 !== 0 ) { inTripleDouble = false ; isClosingString = true ; }
} else if ( tripleDoubleCount % 2 !== 0 ) {
inTripleDouble = true ; justOpenedString = true ;
}
if ( inTripleSingle ) {
isInternalStringLine = true ;
if ( tripleSingleCount % 2 !== 0 ) { inTripleSingle = false ; isClosingString = true ; }
} else if ( tripleSingleCount % 2 !== 0 && ! justOpenedString ) {
inTripleSingle = true ; justOpenedString = true ;
}
if ( isInternalStringLine && ( inTripleDouble || inTripleSingle ) ) { continue ; }
if ( justOpenedString ) { stringNeedsSemicolon = ! line . startsWith ( '"""' ) && ! line . startsWith ( "'''" ) ; }
2026-10-07 21:34:05 +00:00
const tabIndex = rawLine . indexOf ( '\t' ) ;
if ( tabIndex !== - 1 ) {
2026-10-08 20:02:08 +00:00
const diag = addDiag ( i , tabIndex , tabIndex + 1 , "Bitte Tabs durch Spaces ersetzen" , vscode . DiagnosticSeverity . Warning ) ;
2026-10-07 21:34:05 +00:00
diag . code = 'invalid_tab' ;
}
2026-10-08 20:02:08 +00:00
let cleanLineForParens = line . replace ( /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g , '' ) ;
const commentIdx = cleanLineForParens . indexOf ( '//' ) ;
if ( commentIdx !== - 1 ) { cleanLineForParens = cleanLineForParens . substring ( 0 , commentIdx ) ; }
for ( let charIdx = 0 ; charIdx < cleanLineForParens . length ; charIdx ++ ) {
const char = cleanLineForParens [ charIdx ] ;
if ( char === '(' ) { parenStack . push ( i ) ; }
else if ( char === ')' ) {
if ( parenStack . length > 0 ) { parenStack . pop ( ) ; }
else { addDiag ( i , 0 , rawLine . length , "Unerwartete schließende Klammer ')'. Es fehlt eine öffnende Klammer." , vscode . DiagnosticSeverity . Error ) ; }
}
else if ( char === '[' ) { bracketStack . push ( i ) ; }
else if ( char === ']' ) {
if ( bracketStack . length > 0 ) { bracketStack . pop ( ) ; }
else { addDiag ( i , 0 , rawLine . length , "Unerwartete schließende eckige Klammer ']'. Es fehlt eine öffnende Klammer." , vscode . DiagnosticSeverity . Error ) ; }
}
2026-10-07 21:34:05 +00:00
}
2026-10-08 20:02:08 +00:00
2026-10-09 21:14:30 +00:00
if ( line . startsWith ( '//' ) || line . startsWith ( '/*' ) ) { if ( line . startsWith ( '/*' ) ) { inMultiLineComment = true ; } continue ; }
if ( inMultiLineComment ) { if ( line . includes ( '*/' ) ) { inMultiLineComment = false ; } continue ; }
2026-10-07 21:34:05 +00:00
if ( line . length === 0 ) { continue ; }
2026-10-09 21:14:30 +00:00
const lineWithoutStrings = rawLine . replace ( /(["'])(?:(?=(\\?))\2.)*?\1/g , '""' ) ;
if ( /\b(if|elif|where)\b/ . test ( lineWithoutStrings ) ) {
inConditionBlock = true ;
}
if ( /\b(then|do)\b/ . test ( lineWithoutStrings ) || lineWithoutStrings . endsWith ( ';' ) ) {
inConditionBlock = false ;
}
2026-10-08 20:02:08 +00:00
if ( isClosingString ) {
if ( stringNeedsSemicolon && ! line . endsWith ( ';' ) ) {
addDiag ( i , rawLine . length - 1 < 0 ? 0 : rawLine.length - 1 , rawLine . length , "Fehlendes Semikolon am Ende der Zuweisung." , vscode . DiagnosticSeverity . Error ) ;
}
} else {
2026-10-09 21:14:30 +00:00
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 ) ;
2026-10-08 20:02:08 +00:00
2026-10-09 21:14:30 +00:00
if ( ! noSemicolonNeeded . test ( line ) && ! justOpenedString && parenStack . length === 0 && bracketStack . length === 0 && inBlock !== 'enumeration' && ! inConditionBlock && ! continuesOnNextLine ) {
2026-10-08 20:02:08 +00:00
const lineToCheck = line . split ( '//' ) [ 0 ] . trim ( ) ;
if ( ! lineToCheck . endsWith ( ';' ) && ! lineToCheck . endsWith ( '{' ) && ! lineToCheck . endsWith ( ',' ) ) {
addDiag ( i , rawLine . length - 1 < 0 ? 0 : rawLine.length - 1 , rawLine . length , "Fehlendes Semikolon am Ende der Anweisung." , vscode . DiagnosticSeverity . Error ) ;
}
2026-10-07 21:34:05 +00:00
}
}
if ( ! hasModuleDef ) {
2026-10-08 20:02:08 +00:00
if ( line . startsWith ( 'tpl ' ) ) { hasModuleDef = true ; }
else { addDiag ( i , 0 , rawLine . length , "Jede Datei muss mit 'tpl 1.x module <name>;' beginnen." , vscode . DiagnosticSeverity . Error ) ; hasModuleDef = true ; }
}
if ( line . startsWith ( 'metadata' ) && inBlock === 'none' ) {
inModuleMetadata = true ;
hasModuleMetadata = true ;
module MetadataBlockLine = i ;
continue ;
}
if ( inModuleMetadata ) {
if ( line . match ( /^origin\b/ ) ) { foundModuleMetadata . origin = true ; }
if ( line . match ( /^author\b/ ) ) { foundModuleMetadata . author = true ; }
if ( line . match ( /^products\b/ ) ) { foundModuleMetadata . products = true ; }
if ( line . match ( /^publishers\b/ ) ) { foundModuleMetadata . publishers = true ; }
if ( line . match ( /^tree_path\b/ ) ) { foundModuleMetadata . tree_path = true ; }
if ( line . startsWith ( 'end metadata' ) ) {
inModuleMetadata = false ;
const missing = Object . keys ( foundModuleMetadata ) . filter ( k = > ! ( foundModuleMetadata as any ) [ k ] ) ;
if ( missing . length > 0 && module MetadataBlockLine !== - 1 ) {
const diag = addDiag ( module MetadataBlockLine , 0 , 8 , ` Fehlende Pflicht-Metadaten (Modul-Ebene): ${ missing . join ( ', ' ) } ` , vscode . DiagnosticSeverity . Error ) ;
diag . code = 'missing_module_metadata' ;
}
2026-10-07 21:34:05 +00:00
}
2026-10-08 20:02:08 +00:00
continue ;
2026-10-07 21:34:05 +00:00
}
2026-10-08 20:02:08 +00:00
if ( line . match ( /^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/ ) ) {
if ( ! hasModuleMetadata && inBlock === 'none' ) {
addDiag ( i , 0 , line . length , "Ein 'metadata' Block auf Modul-Ebene ist Pflicht, bevor andere Blöcke definiert werden." , vscode . DiagnosticSeverity . Error ) ;
}
}
if ( line . startsWith ( 'definitions ' ) || line . startsWith ( 'definition ' ) ) {
if ( inBlock !== 'none' ) { addDiag ( i , 0 , line . length , "Ein Definitions-Block muss auf oberster Ebene deklariert werden." , vscode . DiagnosticSeverity . Error ) ; }
inBlock = 'definitions' ; continue ;
}
if ( line . startsWith ( 'define ' ) ) { openDefines . push ( i ) ; continue ; }
if ( line . startsWith ( 'enumeration ' ) ) {
if ( inBlock !== 'none' ) { addDiag ( i , 0 , line . length , "Eine Enumeration muss auf oberster Ebene deklariert werden." , vscode . DiagnosticSeverity . Error ) ; }
inBlock = 'enumeration' ; continue ;
}
if ( line . startsWith ( 'configuration ' ) ) {
if ( inBlock !== 'none' ) { addDiag ( i , 0 , line . length , "Eine Konfiguration muss auf oberster Ebene deklariert werden." , vscode . DiagnosticSeverity . Error ) ; }
inBlock = 'configuration' ; continue ;
}
if ( line . startsWith ( 'pattern ' ) || line . startsWith ( 'syncmapping ' ) ) {
if ( inBlock !== 'none' ) { addDiag ( i , 0 , 7 , "Ein Pattern/Syncmapping kann nicht innerhalb eines anderen Blocks deklariert werden." , vscode . DiagnosticSeverity . Error ) ; }
2026-10-07 21:34:05 +00:00
inBlock = 'pattern' ;
currentPatternPhase = PatternPhase . None ;
2026-10-08 20:02:08 +00:00
activePhaseName = "" ;
activePhaseLine = - 1 ;
2026-10-07 21:34:05 +00:00
2026-10-08 20:02:08 +00:00
const nameMatch = line . match ( /(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/ ) ;
if ( nameMatch ) { currentPatternName = nameMatch [ 2 ] ; }
else { addDiag ( i , 0 , rawLine . length , "Ungültige Pattern-Deklaration. Erwartet: 'pattern <Name> <Version>'" , vscode . DiagnosticSeverity . Error ) ; }
2026-10-07 21:34:05 +00:00
continue ;
}
if ( line . startsWith ( 'table ' ) ) {
2026-10-08 20:02:08 +00:00
if ( inBlock !== 'none' ) { addDiag ( i , 0 , 5 , "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden." , vscode . DiagnosticSeverity . Error ) ; }
inBlock = 'table' ; continue ;
2026-10-07 21:34:05 +00:00
}
2026-10-08 20:02:08 +00:00
if ( inBlock === 'pattern' ) {
2026-10-07 21:34:05 +00:00
const checkPhase = ( phaseName : string , phaseEnum : PatternPhase ) = > {
if ( line . startsWith ( phaseName ) ) {
2026-10-08 20:02:08 +00:00
if ( activePhaseName !== "" ) { addDiag ( activePhaseLine , 0 , activePhaseName . length , ` Block ' ${ activePhaseName } ' wurde nicht mit 'end ${ activePhaseName } ;' geschlossen. ` , vscode . DiagnosticSeverity . Error ) ; }
if ( currentPatternPhase >= phaseEnum ) { addDiag ( i , 0 , phaseName . length , ` ' ${ phaseName } ' Block ist hier nicht erlaubt. ` , vscode . DiagnosticSeverity . Error ) ; }
2026-10-07 21:34:05 +00:00
currentPatternPhase = phaseEnum ;
2026-10-08 20:02:08 +00:00
activePhaseName = phaseName ;
activePhaseLine = i ;
2026-10-07 21:34:05 +00:00
}
} ;
checkPhase ( 'metadata' , PatternPhase . Metadata ) ;
checkPhase ( 'overview' , PatternPhase . Overview ) ;
checkPhase ( 'constants' , PatternPhase . Constants ) ;
checkPhase ( 'triggers' , PatternPhase . Triggers ) ;
2026-10-08 20:02:08 +00:00
checkPhase ( 'mapping' , PatternPhase . Mapping ) ;
checkPhase ( 'removal' , PatternPhase . Removal ) ;
if ( line . startsWith ( 'body' ) ) {
if ( activePhaseName !== "" ) { addDiag ( activePhaseLine , 0 , activePhaseName . length , ` Block ' ${ activePhaseName } ' wurde nicht mit 'end ${ activePhaseName } ;' geschlossen. ` , vscode . DiagnosticSeverity . Error ) ; }
activePhaseName = "body" ;
activePhaseLine = i ;
let foundLog = false ;
for ( let j = i + 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 ( currentPatternName ) ) { foundLog = true ; break ; }
}
2026-10-07 21:34:05 +00:00
2026-10-08 20:02:08 +00:00
if ( ! foundLog ) {
const expectedLog = ` log.debug("Start pattern ${ currentPatternName } "); ` ;
const diag = addDiag ( i , 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 ;
2026-10-07 21:34:05 +00:00
}
}
2026-10-08 20:02:08 +00:00
const closers = [ 'end metadata' , 'end overview' , 'end constants' , 'end triggers' , 'end mapping' , 'end removal' , 'end body' ] ;
for ( const closer of closers ) {
if ( line . startsWith ( closer ) ) {
const phase = closer . replace ( 'end ' , '' ) . replace ( ';' , '' ) . trim ( ) ;
if ( activePhaseName !== phase ) { addDiag ( i , 0 , line . length , ` Unerwartetes ' ${ closer } '. Erwartet wurde 'end ${ activePhaseName } ;'. ` , vscode . DiagnosticSeverity . Error ) ; }
else { activePhaseName = "" ; }
2026-10-07 21:34:05 +00:00
}
}
}
2026-10-09 21:14:30 +00:00
if ( lineWithoutStrings . includes ( '=' ) && ! lineWithoutStrings . includes ( ':=' ) && ! lineWithoutStrings . includes ( '>=' ) && ! lineWithoutStrings . includes ( '<=' ) ) {
if ( ! inConditionBlock && parenStack . length === 0 && bracketStack . length === 0 && ! /\b(matches)\b/ . test ( lineWithoutStrings ) ) {
const idx = rawLine . indexOf ( '=' ) ;
if ( idx !== - 1 ) {
addDiag ( i , idx , idx + 1 , "Mögliche falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt. (Toleriert in Vergleichen/Bedingungen)" , vscode . DiagnosticSeverity . Warning ) ;
}
2026-10-07 21:34:05 +00:00
}
}
2026-10-08 20:02:08 +00:00
if ( line . startsWith ( 'end pattern' ) || line . startsWith ( 'end syncmapping' ) ) {
if ( activePhaseName !== "" ) { addDiag ( activePhaseLine , 0 , activePhaseName . length , ` Block ' ${ activePhaseName } ' wurde nicht mit 'end ${ activePhaseName } ;' geschlossen. ` , vscode . DiagnosticSeverity . Error ) ; }
if ( inBlock !== 'pattern' ) { addDiag ( i , 0 , 11 , "'end pattern' gefunden, ohne offenen Pattern-Block." , vscode . DiagnosticSeverity . Error ) ; }
inBlock = 'none' ;
2026-10-07 21:34:05 +00:00
}
2026-10-08 20:02:08 +00:00
else if ( line . startsWith ( 'end table' ) || line . startsWith ( 'end enumeration' ) || line . startsWith ( 'end configuration' ) ) {
2026-10-07 21:34:05 +00:00
inBlock = 'none' ;
}
2026-10-08 20:02:08 +00:00
else if ( line . startsWith ( 'end definitions' ) || line . startsWith ( 'end definition' ) ) {
if ( inBlock !== 'definitions' ) { addDiag ( i , 0 , line . length , "'end definitions' gefunden, ohne offenen Definitions-Block." , vscode . DiagnosticSeverity . Error ) ; }
2026-10-07 21:34:05 +00:00
inBlock = 'none' ;
}
2026-10-08 20:02:08 +00:00
else if ( line . startsWith ( 'end define' ) ) {
if ( openDefines . length > 0 ) { openDefines . pop ( ) ; }
else { addDiag ( i , 0 , line . length , "'end define' gefunden, ohne offenes 'define'." , vscode . DiagnosticSeverity . Error ) ; }
}
2026-10-07 21:34:05 +00:00
}
2026-10-08 20:02:08 +00:00
for ( const unclosedLine of parenStack ) { addDiag ( unclosedLine , 0 , lines [ unclosedLine ] . length , "Öffnende Klammer '(' wird nicht geschlossen!" , vscode . DiagnosticSeverity . Error ) ; }
for ( const unclosedLine of bracketStack ) { addDiag ( unclosedLine , 0 , lines [ unclosedLine ] . length , "Öffnende eckige Klammer '[' wird nicht geschlossen!" , vscode . DiagnosticSeverity . Error ) ; }
for ( const unclosedLine of openDefines ) { addDiag ( unclosedLine , 0 , lines [ unclosedLine ] . length , "Block 'define' wurde nicht mit 'end define;' geschlossen!" , vscode . DiagnosticSeverity . Error ) ; }
2026-10-07 21:34:05 +00:00
collection . set ( document . uri , diagnostics ) ;
}