Erweiterung die feste log regel fällt raus, dafür sind jetzt alle möglichen BMC TPL erweiterungen als auto complete drin

This commit is contained in:
kuro 2026-10-09 00:36:29 +02:00
parent 93f3179bcd
commit a5cd6a74ca
6 changed files with 180 additions and 115 deletions

3
.gitignore vendored
View File

@ -5,3 +5,6 @@ node_modules
*.vsix *.vsix
src/*.js* src/*.js*
src/test/*.js* src/test/*.js*
out/*
out/*/*
out/*/*/*

92
src/TplBuiltIns.ts Normal file
View File

@ -0,0 +1,92 @@
export const tplBuiltIns: Record<string, Record<string, string>> = {
"model": {
"NodeKind": "Erstellt oder aktualisiert einen Knoten basierend auf der Taxonomie (z.B. model.SoftwareInstance(key:=\"...\", name:=\"...\")).",
"destroy": "Zerstört den übergebenen Knoten dauerhaft und sofort aus der Datenbank.",
"withdraw": "Zieht die Behauptung des aktuellen Patterns für diesen Knoten zurück (leitet das Standard-Aging ein).",
"host": "Gibt den übergeordneten Host-Knoten zurück, der mit dem übergebenen Knoten (z.B. Prozess oder Software) verknüpft ist.",
"addDisplayAttribute": "Fügt dem Knoten ein Attribut hinzu, das in der Web-Oberfläche ganz oben hervorgehoben wird.",
"setRemovalGroup": "Ordnet den Knoten einer Gruppe zu. Knoten dieser Gruppe, die beim nächsten Scan nicht neu bestätigt werden, werden gelöscht.",
"findEq": "Führt eine exakte Suche nach Knoten in der Datenbank durch (z.B. Suche nach einer bestimmten ID)."
},
"text": {
"split": "Teilt einen String anhand eines Trennzeichens in eine Liste von Strings auf.",
"join": "Verbindet eine Liste von Strings zu einem einzigen String unter Verwendung eines Trennzeichens.",
"lower": "Konvertiert alle Zeichen eines Strings in Kleinbuchstaben.",
"upper": "Konvertiert alle Zeichen eines Strings in Großbuchstaben.",
"strip": "Entfernt führende und nachfolgende Leerzeichen (Whitespaces) aus einem String.",
"replace": "Ersetzt alle Vorkommen eines Suchstrings durch einen neuen String.",
"hasPrefix": "Prüft, ob ein String mit einem bestimmten Präfix beginnt (gibt true oder false zurück).",
"hasSuffix": "Prüft, ob ein String mit einem bestimmten Suffix endet (gibt true oder false zurück).",
"find": "Sucht einen Teilstring innerhalb eines Strings und gibt die numerische Position zurück (oder -1, wenn nicht gefunden).",
"hash": "Generiert einen numerischen Hash-Wert aus dem übergebenen String (nützlich für die Key-Generierung)."
},
"regex": {
"extract": "Extrahiert den ersten Treffer einer Capture-Group (in runden Klammern) aus einem String. Nimmt optional einen Fallback-Wert.",
"match": "Prüft, ob ein regulärer Ausdruck auf einen String zutrifft (gibt true oder false zurück).",
"findAll": "Findet alle Treffer eines regulären Ausdrucks in einem String und gibt sie als Liste zurück.",
"replace": "Ersetzt Muster in einem String, die auf den regulären Ausdruck zutreffen, durch einen neuen String."
},
"discovery": {
"runCommand": "Führt einen Kommandozeilenbefehl auf dem gescannten Zielsystem (Host) aus und gibt das Ergebnis zurück.",
"fileGet": "Ruft den vollständigen Inhalt einer Datei (z.B. Config-File) vom Zielsystem ab.",
"fileInfo": "Ruft nur die Metadaten (Größe, Rechte, Zeitstempel) einer Datei ab, ohne den Inhalt zu lesen.",
"listDirectory": "Listet den Inhalt (Dateien und Unterordner) eines Verzeichnisses auf dem Zielsystem auf.",
"registryKey": "Liest einen bestimmten Registry-Schlüssel auf einem Windows-Host aus.",
"wmiQuery": "Führt eine WQL-Abfrage per WMI auf einem Windows-Host aus.",
"snmpGet": "Führt einen SNMP GET-Request für eine spezifische OID aus (z.B. für Netzwerkgeräte).",
"snmpGetNext": "Führt einen SNMP GETNEXT-Request aus.",
"snmpGetTable": "Ruft eine komplette SNMP-Tabelle vom Zielgerät ab.",
"databaseQuery": "Führt eine SQL-Abfrage über eine in Discovery konfigurierte Datenbank-Integration aus.",
"restRequest": "Führt einen REST-API Request gegen ein konfiguriertes Ziel aus."
},
"log": {
"debug": "Schreibt eine Debug-Nachricht in das Discovery-Log (nur bei Loglevel DEBUG sichtbar, für tiefe Fehleranalyse).",
"info": "Schreibt eine Informationsnachricht in das Log (verfolgt den normalen Ablauf).",
"warn": "Schreibt eine Warnung in das Log (für unkritische Fehler, z.B. wenn eine Datei leer ist).",
"error": "Schreibt eine Fehlermeldung in das Log (für kritische Pattern-Fehler, die den Ablauf stören).",
"critical": "Schreibt einen kritischen Systemfehler in das Log."
},
"time": {
"current": "Gibt den aktuellen Zeitpunkt der Ausführung in TPL-Epochenzeit zurück.",
"format": "Formatiert einen TPL-Zeitstempel in einen für Menschen lesbaren String (z.B. \"%Y-%m-%d %H:%M:%S\").",
"parseUTC": "Parst einen Zeit-String als UTC-Zeit und konvertiert ihn in einen TPL-Zeitstempel.",
"parseLocal": "Parst einen Zeit-String als lokale Zeit des Hosts und konvertiert ihn in einen TPL-Zeitstempel.",
"delta": "Berechnet die Differenz zwischen zwei Zeitstempeln (gibt eine Dauer zurück)."
},
"json": {
"decode": "Parst einen JSON-String und konvertiert ihn in durchsuchbare TPL-Datenstrukturen (Listen und Dictionaries).",
"encode": "Konvertiert eine TPL-Datenstruktur (wie eine Liste oder ein Dictionary) zurück in einen JSON-formatierten String."
},
"xpath": {
"evaluate": "Führt eine XPath-Abfrage direkt auf einem XML-String aus und gibt die gefundenen Elemente oder Texte zurück.",
"open": "Öffnet und parst ein XML-Dokument für komplexere oder mehrfache Abfragen, was performanter als wiederholtes evaluate ist."
},
"hash": {
"md5": "Generiert einen MD5-Hash aus dem übergebenen String.",
"sha1": "Generiert einen SHA-1-Hash aus dem übergebenen String.",
"sha256": "Generiert einen SHA-256-Hash aus dem übergebenen String."
},
"math": {
"abs": "Gibt den absoluten (positiven) Wert einer Zahl zurück.",
"ceil": "Rundet eine Zahl auf die nächste Ganzzahl auf.",
"floor": "Rundet eine Zahl auf die nächste Ganzzahl ab.",
"round": "Rundet eine Zahl nach kaufmännischen Regeln.",
"min": "Gibt die kleinste Zahl aus einer übergebenen Liste von Zahlen zurück.",
"max": "Gibt die größte Zahl aus einer übergebenen Liste von Zahlen zurück."
},
"net": {
"ipFromString": "Konvertiert einen IP-String in ein internes TPL-IP-Objekt zur weiteren Verarbeitung.",
"ipToString": "Konvertiert ein TPL-IP-Objekt zurück in einen lesbaren String.",
"isIPv4": "Prüft, ob es sich bei dem String um eine gültige IPv4-Adresse handelt (true/false).",
"isIPv6": "Prüft, ob es sich bei dem String um eine gültige IPv6-Adresse handelt (true/false).",
"isMAC": "Prüft, ob der String das Format einer gültigen MAC-Adresse hat (true/false).",
"inSubnet": "Prüft, ob eine bestimmte IP-Adresse innerhalb eines angegebenen Subnetzes liegt."
},
"built_in_globals": {
"size": "Gibt die Länge oder Anzahl der Elemente einer Liste, eines Strings oder eines Dictionaries zurück.",
"toNumber": "Konvertiert einen String (z.B. \"123\") in einen Integer (Ganzzahl).",
"toString": "Konvertiert einen Wert (z.B. eine Zahl oder ein Objekt) in einen String.",
"type": "Gibt den Datentyp der übergebenen Variable als String zurück (z.B. \"list\", \"string\").",
"defined": "Prüft, ob eine Variable existiert oder ob ein Attribut an einem Knoten vorhanden ist, bevor darauf zugegriffen wird."
}
};

View File

@ -1,4 +1,5 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { tplBuiltIns } from './TplBuiltIns';
export class TplCompletionProvider implements vscode.CompletionItemProvider { export class TplCompletionProvider implements vscode.CompletionItemProvider {
async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.CompletionItem[]> { async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.CompletionItem[]> {
@ -6,15 +7,6 @@ export class TplCompletionProvider implements vscode.CompletionItemProvider {
const items: vscode.CompletionItem[] = []; const items: vscode.CompletionItem[] = [];
const text = document.getText(); const text = document.getText();
// 1. Die feste "log." Regel
if (linePrefix.endsWith('log.')) {
['debug', 'info', 'warn', 'error'].forEach(method => {
items.push(new vscode.CompletionItem(method, vscode.CompletionItemKind.Method));
});
return items; // Zwingt VS Code, nur diese 4 anzuzeigen!
}
// 2. Importe und lokale Definitions-Blöcke (Namespaces) sammeln
const importMap = new Map<string, string>(); const importMap = new Map<string, string>();
const importRegex = /from\s+([\w.]+)\s+import\s+(\w+)/g; const importRegex = /from\s+([\w.]+)\s+import\s+(\w+)/g;
let m; let m;
@ -28,11 +20,10 @@ export class TplCompletionProvider implements vscode.CompletionItemProvider {
localNamespaces.add(m[1]); localNamespaces.add(m[1]);
} }
// 3. Wenn wir auf freier Fläche tippen (kein Punkt davor)
const matchDot = linePrefix.match(/([a-zA-Z0-9_]+)\.$/); const matchDot = linePrefix.match(/([a-zA-Z0-9_]+)\.$/);
if (!matchDot) { if (!matchDot) {
// A) Namespaces (Importiert und Lokal) vorschlagen // A) Externe & Lokale Module
importMap.forEach((_, key) => { importMap.forEach((_, key) => {
const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Module); const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Module);
item.detail = "Importiertes Modul"; item.detail = "Importiertes Modul";
@ -44,7 +35,23 @@ export class TplCompletionProvider implements vscode.CompletionItemProvider {
items.push(item); items.push(item);
}); });
// B) Lokale Variablen vorschlagen (z.B. attribute_list :=) // B) Standard BMC Namespaces (text, regex, discovery etc.)
for (const key of Object.keys(tplBuiltIns)) {
if (key !== 'built_in_globals') {
const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Module);
item.detail = "BMC Built-In Namespace";
items.push(item);
} else {
for (const glob of Object.keys(tplBuiltIns[key])) {
const item = new vscode.CompletionItem(glob, vscode.CompletionItemKind.Function);
item.detail = "BMC Globale Funktion";
item.documentation = new vscode.MarkdownString(tplBuiltIns[key][glob]);
items.push(item);
}
}
}
// C) Lokale Variablen
const varRegex = /\b([a-zA-Z0-9_]+)\s*:=/g; const varRegex = /\b([a-zA-Z0-9_]+)\s*:=/g;
const variables = new Set<string>(); const variables = new Set<string>();
while ((m = varRegex.exec(text)) !== null) { while ((m = varRegex.exec(text)) !== null) {
@ -54,39 +61,20 @@ export class TplCompletionProvider implements vscode.CompletionItemProvider {
items.push(new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable)); items.push(new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable));
}); });
// C) Das große Skeleton Snippet // D) Snippets
const skeletonItem = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet); const skeletonItem = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
skeletonItem.insertText = new vscode.SnippetString( skeletonItem.insertText = new vscode.SnippetString(
'tpl 1.23 module ${1:module_name};\n\n' + 'tpl 1.23 module ${1:module_name};\n\n' +
'metadata\n' + 'metadata\n origin := \'${2:origin}\';\n author := \'${3:author}\';\n products := \'${4:products}\';\n publishers := \'${5:publishers}\';\n tree_path := \'${6:company}\', \'${7:category}\', \'${8:subcategory}\';\nend metadata;\n\n' +
' origin := \'${2:origin}\';\n' + 'pattern ${9:pattern_name} 1.0\n """\n TODO: explain\n """\n\n triggers\n on process := DiscoveredProcess where cmd matches regex_escape("${10:process}");\n end triggers;\n\n body\n log.debug("Start pattern ${9:pattern_name}");\n $0\n end body;\nend pattern;'
' author := \'${3:author}\';\n' +
' products := \'${4:products}\';\n' +
' publishers := \'${5:publishers}\';\n' +
' tree_path := \'${6:company}\', \'${7:category}\', \'${8:subcategory}\';\n' +
'end metadata;\n\n' +
'pattern ${9:pattern_name} 1.0\n' +
' """\n' +
' TODO: explain\n' +
' """\n\n' +
' triggers\n' +
' on process := DiscoveredProcess where cmd matches regex_escape("${10:process}");\n' +
' end triggers;\n\n' +
' body\n' +
' log.debug("Start pattern ${9:pattern_name}");\n' +
' $0\n' +
' end body;\n' +
'end pattern;'
); );
skeletonItem.detail = "Generiert das komplette TPL Modul-Grundgerüst"; skeletonItem.detail = "Generiert das komplette TPL Modul-Grundgerüst";
items.push(skeletonItem); items.push(skeletonItem);
// D) Block-Snippets
const blocks = ['metadata', 'overview', 'constants', 'triggers', 'body', 'mapping', 'removal', 'definitions', 'enumeration', 'configuration']; const blocks = ['metadata', 'overview', 'constants', 'triggers', 'body', 'mapping', 'removal', 'definitions', 'enumeration', 'configuration'];
for (const block of blocks) { for (const block of blocks) {
const item = new vscode.CompletionItem(block, vscode.CompletionItemKind.Snippet); const item = new vscode.CompletionItem(block, vscode.CompletionItemKind.Snippet);
item.insertText = new vscode.SnippetString(`${block}\n $0\nend ${block};`); item.insertText = new vscode.SnippetString(`${block}\n $0\nend ${block};`);
item.detail = `Fügt ${block} Block mit 'end' ein`;
items.push(item); items.push(item);
} }
@ -106,39 +94,36 @@ export class TplCompletionProvider implements vscode.CompletionItemProvider {
tableItem.insertText = new vscode.SnippetString('table ${1:name}${2:1.0}\n $0\n default -> "unknown";\nend table;'); tableItem.insertText = new vscode.SnippetString('table ${1:name}${2:1.0}\n $0\n default -> "unknown";\nend table;');
items.push(tableItem); items.push(tableItem);
const ifItem = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet);
ifItem.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;');
items.push(ifItem);
const forItem = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet);
forItem.insertText = new vscode.SnippetString('for ${1:item} in${2:list} do\n $0\nend for;');
items.push(forItem);
return items; return items;
} else { } else {
// 4. Wir haben nach einem Punkt getippt (z.B. helpers.)
const ns = matchDot[1]; const ns = matchDot[1];
const moduleMethods: vscode.CompletionItem[] = [];
// Regex sucht nach dem Wort direkt vor der Klammer: define [add] method_name( // NEU: Ist es ein BMC Built-In Namespace? (z.B. text. oder log.)
if (tplBuiltIns[ns]) {
const bmcMethods: vscode.CompletionItem[] = [];
for (const [method, desc] of Object.entries(tplBuiltIns[ns])) {
const item = new vscode.CompletionItem(method, vscode.CompletionItemKind.Method);
item.documentation = new vscode.MarkdownString(desc);
bmcMethods.push(item);
}
return bmcMethods;
}
const moduleMethods: vscode.CompletionItem[] = [];
const extractDefineRegex = /^\s*define\s+.*?\b([a-zA-Z0-9_]+)\s*\(/gm; const extractDefineRegex = /^\s*define\s+.*?\b([a-zA-Z0-9_]+)\s*\(/gm;
// Fall A: Lokaler Definitions-Block (gleiche Datei!)
if (localNamespaces.has(ns)) { if (localNamespaces.has(ns)) {
let defMatch; let defMatch;
while ((defMatch = extractDefineRegex.exec(text)) !== null) { while ((defMatch = extractDefineRegex.exec(text)) !== null) {
const methodName = defMatch[1]; const methodName = defMatch[1];
const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method); const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method);
const docStr = this.extractDocString(text, defMatch.index); const docStr = this.extractDocString(text, defMatch.index);
if (docStr) { if (docStr) item.documentation = new vscode.MarkdownString(docStr);
item.documentation = new vscode.MarkdownString(docStr);
}
moduleMethods.push(item); moduleMethods.push(item);
} }
return moduleMethods; return moduleMethods;
} }
// Fall B: Importiertes Modul (externe Datei)
if (importMap.has(ns)) { if (importMap.has(ns)) {
const modPath = importMap.get(ns)!; const modPath = importMap.get(ns)!;
const uris = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**', 50); const uris = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**', 50);
@ -146,28 +131,21 @@ export class TplCompletionProvider implements vscode.CompletionItemProvider {
try { try {
const docBytes = await vscode.workspace.fs.readFile(uri); const docBytes = await vscode.workspace.fs.readFile(uri);
const docText = Buffer.from(docBytes).toString('utf8'); const docText = Buffer.from(docBytes).toString('utf8');
const modRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${modPath.replace(/\./g, '\\\\.')}\\s*;`); const modRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${modPath.replace(/\./g, '\\\\.')}\\s*;`);
if (modRegex.test(docText)) { if (modRegex.test(docText)) {
let defMatch; let defMatch;
while ((defMatch = extractDefineRegex.exec(docText)) !== null) { while ((defMatch = extractDefineRegex.exec(docText)) !== null) {
const methodName = defMatch[1]; const methodName = defMatch[1];
const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method); const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method);
const docStr = this.extractDocString(docText, defMatch.index); const docStr = this.extractDocString(docText, defMatch.index);
if (docStr) { if (docStr) item.documentation = new vscode.MarkdownString(docStr);
item.documentation = new vscode.MarkdownString(docStr);
}
moduleMethods.push(item); moduleMethods.push(item);
} }
break; break;
} }
} catch(e) {} } catch(e) {}
} }
if (moduleMethods.length > 0) return moduleMethods;
if (moduleMethods.length > 0) {
return moduleMethods;
}
} }
} }
return items; return items;
@ -178,32 +156,23 @@ export class TplCompletionProvider implements vscode.CompletionItemProvider {
let docString = ""; let docString = "";
let inDocString = false; let inDocString = false;
let quoteType = ""; let quoteType = "";
for (let j = 1; j < Math.min(15, lines.length); j++) { for (let j = 1; j < Math.min(15, lines.length); j++) {
const line = lines[j].trim(); const line = lines[j].trim();
if (line.length === 0) continue; if (line.length === 0) continue;
if (!inDocString) { if (!inDocString) {
if (line.startsWith('"""') || line.startsWith("'''")) { if (line.startsWith('"""') || line.startsWith("'''")) {
inDocString = true; inDocString = true;
quoteType = line.substring(0, 3); quoteType = line.substring(0, 3);
if (line.length > 3 && line.endsWith(quoteType)) { if (line.length > 3 && line.endsWith(quoteType)) { docString = line.substring(3, line.length - 3).trim(); break; }
docString = line.substring(3, line.length - 3).trim();
break;
}
const content = line.substring(3).trim(); const content = line.substring(3).trim();
if (content) docString += content + "\n"; if (content) docString += content + "\n";
} else { } else { break; }
break;
}
} else { } else {
if (line.endsWith(quoteType)) { if (line.endsWith(quoteType)) {
const content = line.substring(0, line.length - 3).trim(); const content = line.substring(0, line.length - 3).trim();
if (content) docString += content; if (content) docString += content;
break; break;
} else { } else { docString += line + "\n"; }
docString += line + "\n";
}
} }
} }
return docString.trim() || null; return docString.trim() || null;

View File

@ -1,4 +1,5 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { tplBuiltIns } from './TplBuiltIns';
export class TplHoverProvider implements vscode.HoverProvider { export class TplHoverProvider implements vscode.HoverProvider {
async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | null> { async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | null> {
@ -17,32 +18,34 @@ export class TplHoverProvider implements vscode.HoverProvider {
methodOrPatternName = parts[1]; methodOrPatternName = parts[1];
} }
// FEHLERBEHEBUNG: [ \t] statt \s verwenden! // NEU: BMC Built-Ins abfangen und direkt ausgeben!
// \s frisst Zeilenumbrüche (\n), was dazu geführt hat, dass match.index if (namespace && tplBuiltIns[namespace] && tplBuiltIns[namespace][methodOrPatternName]) {
// auf leere Zeilen oberhalb der Methode gezeigt hat -> Leeres Hover-Fenster. const markdown = new vscode.MarkdownString();
const searchRegex = new RegExp(`^[ \\t]*(?:(?:define[ \\t]+(?:[a-zA-Z0-9_]+[ \\t]+)*${methodOrPatternName}[ \\t]*\\()|(?:(?:pattern|syncmapping|table|enumeration|configuration)[ \\t]+${methodOrPatternName}\\b))`, 'm'); markdown.appendCodeblock(`${namespace}.${methodOrPatternName}`, 'tpl');
markdown.appendMarkdown(`---\n*${tplBuiltIns[namespace][methodOrPatternName]}*`);
// 1. Suche im aktuellen Dokument (Ohne Namespace) return new vscode.Hover(markdown);
if (!namespace) { } else if (!namespace && tplBuiltIns['built_in_globals'][methodOrPatternName]) {
const match = searchRegex.exec(currentText); const markdown = new vscode.MarkdownString();
if (match) { markdown.appendCodeblock(`${methodOrPatternName}`, 'tpl');
return this.buildHover(currentText, match.index); markdown.appendMarkdown(`---\n*${tplBuiltIns['built_in_globals'][methodOrPatternName]}*`);
} return new vscode.Hover(markdown);
return null; // Bricht sauber ab, ohne das System zu blockieren
} }
// 2. Suche mit Namespace (z.B. helpers.method) const searchRegex = new RegExp(`^[ \\t]*(?:(?:define[ \\t]+(?:[a-zA-Z0-9_]+[ \\t]+)*${methodOrPatternName}[ \\t]*\\()|(?:(?:pattern|syncmapping|table|enumeration|configuration)[ \\t]+${methodOrPatternName}\\b))`, 'm');
// A) Ist der Namespace in derselben Datei? (lokaler definitions block)
const localDefRegex = new RegExp(`^[ \\t]*definitions[ \\t]+${namespace}\\b`, 'm'); if (!namespace) {
if (localDefRegex.test(currentText)) {
const match = searchRegex.exec(currentText); const match = searchRegex.exec(currentText);
if (match) { if (match) return this.buildHover(currentText, match.index);
return this.buildHover(currentText, match.index); return null;
} }
const localDefRegex = new RegExp(`^[ \\t]*definitions[ \\t]+${namespace}\\b`, 'm');
if (localDefRegex.test(currentText)) {
const match = searchRegex.exec(currentText);
if (match) return this.buildHover(currentText, match.index);
return null; return null;
} }
// B) Ist der Namespace importiert? (Workspace durchsuchen)
const importRegex = new RegExp(`from\\s+([\\w.]+)\\s+import\\s+${namespace}\\b`); const importRegex = new RegExp(`from\\s+([\\w.]+)\\s+import\\s+${namespace}\\b`);
const importMatch = importRegex.exec(currentText); const importMatch = importRegex.exec(currentText);
@ -53,35 +56,25 @@ export class TplHoverProvider implements vscode.HoverProvider {
try { try {
const docBytes = await vscode.workspace.fs.readFile(uri); const docBytes = await vscode.workspace.fs.readFile(uri);
const docText = Buffer.from(docBytes).toString('utf8'); const docText = Buffer.from(docBytes).toString('utf8');
const modRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${modPath.replace(/\./g, '\\\\.')}\\s*;`); const modRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${modPath.replace(/\./g, '\\\\.')}\\s*;`);
if (modRegex.test(docText)) { if (modRegex.test(docText)) {
const match = searchRegex.exec(docText); const match = searchRegex.exec(docText);
if (match) { if (match) return this.buildHover(docText, match.index);
return this.buildHover(docText, match.index);
}
break; break;
} }
} catch(e) {} } catch(e) {}
} }
} }
return null; return null;
} }
private buildHover(text: string, matchIndex: number): vscode.Hover { private buildHover(text: string, matchIndex: number): vscode.Hover {
const docStr = this.extractDocString(text, matchIndex); const docStr = this.extractDocString(text, matchIndex);
const markdown = new vscode.MarkdownString(); const markdown = new vscode.MarkdownString();
// Schneidet exakt die Code-Zeile aus (ohne vorausgehende Leerzeilen)
const declLine = text.substring(matchIndex).split('\n')[0].trim(); const declLine = text.substring(matchIndex).split('\n')[0].trim();
markdown.appendCodeblock(declLine, 'tpl'); markdown.appendCodeblock(declLine, 'tpl');
if (docStr) markdown.appendMarkdown(`---\n*${docStr}*`);
if (docStr) { else markdown.appendMarkdown(`---\n*(Keine Dokumentation gefunden)*`);
markdown.appendMarkdown(`---\n*${docStr}*`);
} else {
markdown.appendMarkdown(`---\n*(Keine Dokumentation gefunden)*`);
}
return new vscode.Hover(markdown); return new vscode.Hover(markdown);
} }
@ -90,32 +83,23 @@ export class TplHoverProvider implements vscode.HoverProvider {
let docString = ""; let docString = "";
let inDocString = false; let inDocString = false;
let quoteType = ""; let quoteType = "";
for (let j = 1; j < Math.min(15, lines.length); j++) { for (let j = 1; j < Math.min(15, lines.length); j++) {
const line = lines[j].trim(); const line = lines[j].trim();
if (line.length === 0) continue; if (line.length === 0) continue;
if (!inDocString) { if (!inDocString) {
if (line.startsWith('"""') || line.startsWith("'''")) { if (line.startsWith('"""') || line.startsWith("'''")) {
inDocString = true; inDocString = true;
quoteType = line.substring(0, 3); quoteType = line.substring(0, 3);
if (line.length > 3 && line.endsWith(quoteType)) { if (line.length > 3 && line.endsWith(quoteType)) { docString = line.substring(3, line.length - 3).trim(); break; }
docString = line.substring(3, line.length - 3).trim();
break;
}
const content = line.substring(3).trim(); const content = line.substring(3).trim();
if (content) docString += content + "\n"; if (content) docString += content + "\n";
} else { } else { break; }
break;
}
} else { } else {
if (line.endsWith(quoteType)) { if (line.endsWith(quoteType)) {
const content = line.substring(0, line.length - 3).trim(); const content = line.substring(0, line.length - 3).trim();
if (content) docString += content; if (content) docString += content;
break; break;
} else { } else { docString += line + "\n"; }
docString += line + "\n";
}
} }
} }
return docString.trim() || null; return docString.trim() || null;

View File

@ -3,6 +3,7 @@ import { TplCodeActionProvider } from './TplCodeActionProvider';
import { TplDocumentFormatter } from './TplDocumentFormatter'; import { TplDocumentFormatter } from './TplDocumentFormatter';
import { TplCompletionProvider } from './TplCompletionProvider'; import { TplCompletionProvider } from './TplCompletionProvider';
import { TplHoverProvider } from './TplHoverProvider'; import { TplHoverProvider } from './TplHoverProvider';
import { tplBuiltIns } from './TplBuiltIns';
const SUPPORTED_TPL_VERSION = 23; const SUPPORTED_TPL_VERSION = 23;
const warnedDocuments = new Set<string>(); const warnedDocuments = new Set<string>();
@ -150,6 +151,21 @@ function validateTplDocument(document: vscode.TextDocument, collection: vscode.D
} }
} }
// Linter-Check für falsche BMC Built-In Methoden
const callMatch = rawLine.match(/\b(text|regex|discovery|log|time|json|xpath|hash|math|net)\.([a-zA-Z0-9_]+)\b/g);
if (callMatch && !inMultiLineComment && !isInternalStringLine) {
for (const call of callMatch) {
const parts = call.split('.');
const ns = parts[0];
const method = parts[1];
if (tplBuiltIns[ns] && !tplBuiltIns[ns][method]) {
const idx = rawLine.indexOf(call);
addDiag(i, idx, idx + call.length, `Unbekannte Methode '${method}' im Namespace '${ns}'.`, vscode.DiagnosticSeverity.Error);
}
}
}
if (!hasModuleDef) { if (!hasModuleDef) {
if (line.startsWith('tpl ')) { hasModuleDef = true; } 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; } else { addDiag(i, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module <name>;' beginnen.", vscode.DiagnosticSeverity.Error); hasModuleDef = true; }

View File

@ -10,6 +10,7 @@
"mocha" "mocha"
], ],
"sourceMap": true, "sourceMap": true,
"outDir": "out",
"rootDir": "src", "rootDir": "src",
"strict": true, /* enable all strict type-checking options */ "strict": true, /* enable all strict type-checking options */
/* Additional Checks */ /* Additional Checks */