Komplettes update für die funktionen und beschreibungen

This commit is contained in:
kuro 2026-10-08 22:02:08 +02:00
parent 5b150ce550
commit 684d760f4b
10 changed files with 2888 additions and 322 deletions

113
README.md
View File

@ -1,71 +1,68 @@
# bmc-tpl-code-all-around README # BMC TPL Code All-Around
This is the README for your extension "bmc-tpl-code-all-around". After writing up a brief description, we recommend including the following sections. **Linter, Auto-Formatter, IntelliSense und Hover-Dokumentationen für die BMC TPL (Template Language).**[cite: 3]
## Features Diese Visual Studio Code Erweiterung verwandelt deinen Editor in eine vollwertige Entwicklungsumgebung für BMC Discovery TPL-Skripte. Sie nimmt dir Tipparbeit ab, formatiert Code nach strengen Best Practices, verhindert Syntax-Fehler noch vor dem Deployment und versteht projektübergreifende Abhängigkeiten.
Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file.
For example if there is an image subfolder under your extension project workspace:
\!\[feature X\]\(images/feature-x.png\)
> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow.
## Requirements
If you have any requirements or dependencies, add a section describing those and how to install and configure them.
## Extension Settings
Include if your extension adds any VS Code settings through the `contributes.configuration` extension point.
For example:
This extension contributes the following settings:
* `myExtension.enable`: Enable/disable this extension.
* `myExtension.thing`: Set to `blah` to do something.
## Known Issues
Calling out known issues can help limit users opening duplicate issues against your extension.
## Release Notes
Users appreciate release notes as you update your extension.
### 1.0.0
Initial release of ...
### 1.0.1
Fixed issue #.
### 1.1.0
Added features X, Y, and Z.
--- ---
## Following extension guidelines ## ✨ Features
Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension. ### 🤖 Intelligenter Linter (Syntax-Scanner)
Der Linter analysiert den Code in Echtzeit und warnt vor typischen TPL-Fehlern:
* **Strikte Hierarchie-Prüfung:** Überwacht die korrekte Reihenfolge der Blöcke innerhalb von Patterns und Syncmappings (`metadata` -> `overview` -> `constants` -> `triggers` -> `mapping`/`removal` -> `body`).
* **Semicolon- & Zuweisungs-Check:** Findet fehlende Semikolons (ignoriert sie aber intelligent in `enumeration`-Blöcken) und warnt, wenn `=` anstelle von `:=` für Zuweisungen genutzt wird.
* **Block- & Klammer-Tracking:** Erkennt nicht geschlossene Blöcke (wie fehlende `end body;` oder `end pattern;`) und findet ungeschlossene runde `(` oder eckige `[` Klammern.
* **Pflichtfeld-Validierung:** Prüft, ob auf Modul-Ebene zwingend benötigte Metadaten (`origin`, `author`, `products`, `publishers`, `tree_path`) vorhanden sind und ob im `body` ein initiales `log.debug` Statement steht.
* **Toleranz-Modus:** Erkennt automatisch TPL-Versionen (z.B. > 1.23) und stuft in neueren Versionen Fehler zu reinen Warnungen herab, um zukünftige Kompatibilität zu gewährleisten.
* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines) ### 🎨 Auto-Formatter & Visual Alignment (`Shift + Alt + F`)
Schluss mit kaputten Einrückungen! Der Formatter bringt deinen Code in Bestform:
* **VS Code Standard:** Erzwingt strikt 4 Leerzeichen pro Einrückungs-Ebene.
* **Dynamische Block-Einrückung:** Erkennt automatisch startende und endende Blöcke (`if`, `for`, `pattern`, `table` etc.) und passt die Einrückungsebene an.
* **Klammer-Gedächtnis (Visual Alignment):** Die Königsdisziplin! Der Formatter merkt sich die spaltengenaue Position von geöffneten Arrays `[` oder Funktionsaufrufen `(` und richtet alle Folgezeilen makellos an dieser Klammer aus. Schließende Klammern werden auf Wunsch perfekt darunter platziert.
## Working with Markdown ### 💡 IntelliSense & Autovervollständigung (Cross-File)
Spart extrem viel Zeit beim Coden durch Kontext-Wissen:
* **Workspace-Scanning:** Importierst du ein Modul (z.B. `from company.helpers import my_helper`), durchsucht das Plugin im Hintergrund dein gesamtes Projekt. Tippst du `my_helper.`, werden dir automatisch alle verfügbaren `define`-Methoden aus der fremden Datei vorgeschlagen!
* **Lokale Namespaces:** Erkennt `definitions`-Blöcke in der gleichen Datei und schlägt deren Methoden vor.
* **Variablen-Tracking:** Lernt alle Variablen, die du mit `:=` definiert hast, und schlägt sie dir später vor.
* **Log-Filter:** Nach dem Tippen von `log.` zeigt VS Code strikt nur noch die validen Methoden (`debug`, `info`, `warn`, `error`) an.
You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: ### 🔍 Hover-Dokumentation (Tooltips)
Wenn du mit der Maus über einem Methodennamen, einem Pattern oder einer Tabelle stehen bleibst:
* **DocString Extraction:** Das Plugin liest Multi-Line-Kommentare (`"""` oder `'''`), die direkt unter einer `define` oder `pattern` Deklaration stehen, aus und rendert sie als saubere Markdown-Tooltips.
* **Projektübergreifend:** Funktioniert auch für Methoden aus importierten Dateien! Du musst die aktuelle Datei nicht mehr verlassen, um zu sehen, was eine Hilfsfunktion macht.
* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux). ### ⚡ Quick Fixes (Code Actions / Glühbirne)
* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux). Bietet sofortige Reparaturoptionen mit einem Klick:
* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets. * **"Fehlende Modul-Metadaten einfügen":** Generiert sofort alle 5 Pflichtfelder im Modul-Metadata-Block.
* **"Log-Statement einfügen":** Baut das fehlende `log.debug("Start pattern...");` direkt am Anfang des `body`-Blocks ein.
* **"Tabs zu Spaces":** Ersetzt sofort alle illegalen Tabs (`\t`) in einer Zeile durch jeweils 4 korrekte Leerzeichen.
## For more information ---
* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) ## 🚀 Snippets (Code-Vorlagen)
* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/)
**Enjoy!** Einfach das Keyword tippen und `Enter` drücken, um komplette, perfekt eingerückte Blöcke zu generieren:
| Keyword | Beschreibung |
| :--- | :--- |
| `tpl_skeleton` | **God-Snippet**: Generiert eine komplett frische `.tpl` Datei inklusive Modul-Definition, Pflicht-Metadaten, Pattern, Triggern und Log-Statements. |
| `pattern` | Baut einen leeren Pattern-Block inklusive DocString und `end pattern;`. |
| `syncmapping` | Baut einen leeren Syncmapping-Block inklusive DocString. |
| `define` | Generiert eine Methodendefinition (`define name(params) ... end define;`). |
| `metadata`, `overview`, `body`... | Baut den jeweiligen Block auf und platziert den Cursor in der Mitte. |
| `table` | Erstellt eine Tabelle inklusive des `default -> "unknown";` Fallbacks. |
| `if`, `for` | Baut Standard-Schleifen und Abfragen. |
---
## 🛠️ Installation & Nutzung
1. Das Plugin aktivieren.
2. TPL-Dateien (Endung `.tpl`) öffnen.
3. Der Linter startet automatisch bei jedem Tastendruck.
4. **Formatieren:** Rechtsklick -> *Dokument formatieren* (oder `Shift + Alt + F`).
5. **Autovervollständigung:** Triggert automatisch beim Tippen, speziell nach einem Punkt (`.`) bei Modul-Aufrufen.
6. **Quick Fixes:** Auf die gelbe/rote Glühbirne am Zeilenanfang klicken oder `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac) drücken.

1
build_release.sh Executable file
View File

@ -0,0 +1 @@
vsce package

View File

@ -3,19 +3,31 @@
"displayName": "BMC TPL Code All around", "displayName": "BMC TPL Code All around",
"description": "Code linter, tracker and all around tool for coding in BMC Helix TPL", "description": "Code linter, tracker and all around tool for coding in BMC Helix TPL",
"version": "0.0.1", "version": "0.0.1",
"publisher": "kuro",
"engines": { "engines": {
"vscode": "^1.140.0" "vscode": "^1.140.0"
}, },
"categories": [ "categories": [
"Other" "Other"
], ],
"activationEvents": [], "activationEvents": [
"onLanguage:tpl"
],
"main": "./dist/extension.js", "main": "./dist/extension.js",
"contributes": { "contributes": {
"commands": [ "languages": [
{ {
"command": "bmc-tpl-code-all-around.helloWorld", "id": "tpl",
"title": "Hello World" "aliases": ["TPL", "tpl"],
"extensions": [".tpl"],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "tpl",
"scopeName": "source.tpl",
"path": "./syntaxes/tpl.tmLanguage.json"
} }
] ]
}, },
@ -44,9 +56,5 @@
"typescript": "^6.0.3", "typescript": "^6.0.3",
"@vscode/test-cli": "^0.0.15", "@vscode/test-cli": "^0.0.15",
"@vscode/test-electron": "^3.0.0" "@vscode/test-electron": "^3.0.0"
},
"overrides": {
"diff": "^8.0.4",
"serialize-javascript": "^7.0.6"
} }
} }

View File

@ -2,60 +2,66 @@ import * as vscode from 'vscode';
export class TplCodeActionProvider implements vscode.CodeActionProvider { export class TplCodeActionProvider implements vscode.CodeActionProvider {
provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { public provideCodeActions(
const actions: vscode.CodeAction[] = []; document: vscode.TextDocument,
range: vscode.Range | vscode.Selection,
context: vscode.CodeActionContext,
token: vscode.CancellationToken
): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> {
for (const diagnostic of context.diagnostics) { const actionList: vscode.CodeAction[] = [];
// Fix für fehlende Metadaten for (const diag of context.diagnostics) {
if (diagnostic.code === 'missing_metadata') {
const fix = new vscode.CodeAction('Fehlende Metadaten einfügen', vscode.CodeActionKind.QuickFix);
fix.edit = new vscode.WorkspaceEdit();
const missingParts = diagnostic.message.replace('Fehlende Pflicht-Metadaten: ', '').split(', '); // 1. Fix für fehlende Modul-Metadaten (Auf Modul-Ebene, 4 Spaces)
let insertText = ''; if (diag.code === 'missing_module_metadata') {
const action = new vscode.CodeAction('Fehlende Modul-Metadaten einfügen', vscode.CodeActionKind.QuickFix);
action.edit = new vscode.WorkspaceEdit();
// Wir nutzen hier auch konsequent die 3 Spaces für den Insert! const fehlendeFelder = diag.message.replace('Fehlende Pflicht-Metadaten (Modul-Ebene): ', '').split(', ');
if (missingParts.includes('author')) { insertText += ` author := 'Dein Name';\n`; } let neuerText = '';
if (missingParts.includes('publisher')) { insertText += ` publisher := 'Dein Unternehmen';\n`; }
if (missingParts.includes('origin')) { insertText += ` origin := 'Custom';\n`; }
if (missingParts.includes('tree_path')) { insertText += ` tree_path := 'Eigener Pfad', 'Kategorie', 'Pattern';\n`; }
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText); if (fehlendeFelder.includes('origin')) { neuerText += ` origin := 'Origin';\n`; }
fix.diagnostics = [diagnostic]; if (fehlendeFelder.includes('author')) { neuerText += ` author := 'Dein Name';\n`; }
fix.isPreferred = true; if (fehlendeFelder.includes('products')) { neuerText += ` products := 'Produktname';\n`; }
actions.push(fix); if (fehlendeFelder.includes('publishers')) { neuerText += ` publishers := 'Dein Unternehmen';\n`; }
if (fehlendeFelder.includes('tree_path')) { neuerText += ` tree_path := 'Company', 'Kategorie', 'Sub';\n`; }
action.edit.insert(document.uri, new vscode.Position(diag.range.start.line + 1, 0), neuerText);
action.diagnostics = [diag];
action.isPreferred = true;
actionList.push(action);
} }
// Fix für fehlenden Pattern-Start-Log // 2. Fix für fehlendes Log-Statement im Body (8 Spaces Einrückung)
if (diagnostic.code === 'missing_log_start') { if (diag.code === 'missing_log_start') {
const fix = new vscode.CodeAction('Log-Statement am Body-Anfang einfügen', vscode.CodeActionKind.QuickFix); const action = new vscode.CodeAction('Log-Statement am Body-Anfang einfügen', vscode.CodeActionKind.QuickFix);
fix.edit = new vscode.WorkspaceEdit(); action.edit = new vscode.WorkspaceEdit();
const expectedLog = diagnostic.source as string; const erwartetesLog = diag.source as string;
const insertText = ` ${expectedLog}\n`; // Auch hier 3 Spaces const neuerText = ` ${erwartetesLog}\n`;
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line, 0), insertText); action.edit.insert(document.uri, new vscode.Position(diag.range.start.line + 1, 0), neuerText);
fix.diagnostics = [diagnostic]; action.diagnostics = [diag];
fix.isPreferred = true; action.isPreferred = true;
actions.push(fix); actionList.push(action);
} }
// Fix für Tabs (Ersetzen durch 3 Spaces) // 3. Fix für Tabs (Ersetzt \t durch 4 Spaces in der ganzen Zeile)
if (diagnostic.code === 'invalid_tab') { if (diag.code === 'invalid_tab') {
const fix = new vscode.CodeAction('Tabs in dieser Zeile durch 3 Spaces ersetzen', vscode.CodeActionKind.QuickFix); const action = new vscode.CodeAction('Tabs in dieser Zeile durch 4 Spaces ersetzen', vscode.CodeActionKind.QuickFix);
fix.edit = new vscode.WorkspaceEdit(); action.edit = new vscode.WorkspaceEdit();
const line = document.lineAt(diagnostic.range.start.line); const zeile = document.lineAt(diag.range.start.line);
const fixedText = line.text.replace(/\t/g, ' '); const bereinigterText = zeile.text.replace(/\t/g, ' ');
fix.edit.replace(document.uri, line.range, fixedText); action.edit.replace(document.uri, zeile.range, bereinigterText);
fix.diagnostics = [diagnostic]; action.diagnostics = [diag];
fix.isPreferred = true; action.isPreferred = true;
actions.push(fix); actionList.push(action);
} }
} }
return actions; return actionList;
} }
} }

View File

@ -0,0 +1,211 @@
import * as vscode from 'vscode';
export class TplCompletionProvider implements vscode.CompletionItemProvider {
async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.CompletionItem[]> {
const linePrefix = document.lineAt(position).text.substring(0, position.character);
const items: vscode.CompletionItem[] = [];
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 importRegex = /from\s+([\w.]+)\s+import\s+(\w+)/g;
let m;
while ((m = importRegex.exec(text)) !== null) {
importMap.set(m[2], m[1]);
}
const localNamespaces = new Set<string>();
const defBlockRegex = /^\s*definitions\s+([a-zA-Z0-9_]+)/gm;
while ((m = defBlockRegex.exec(text)) !== null) {
localNamespaces.add(m[1]);
}
// 3. Wenn wir auf freier Fläche tippen (kein Punkt davor)
const matchDot = linePrefix.match(/([a-zA-Z0-9_]+)\.$/);
if (!matchDot) {
// A) Namespaces (Importiert und Lokal) vorschlagen
importMap.forEach((_, key) => {
const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Module);
item.detail = "Importiertes Modul";
items.push(item);
});
localNamespaces.forEach(ns => {
const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module);
item.detail = "Lokales Modul (definitions)";
items.push(item);
});
// B) Lokale Variablen vorschlagen (z.B. attribute_list :=)
const varRegex = /\b([a-zA-Z0-9_]+)\s*:=/g;
const variables = new Set<string>();
while ((m = varRegex.exec(text)) !== null) {
variables.add(m[1]);
}
variables.forEach(v => {
items.push(new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable));
});
// C) Das große Skeleton Snippet
const skeletonItem = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
skeletonItem.insertText = new vscode.SnippetString(
'tpl 1.23 module ${1:module_name};\n\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}\';\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";
items.push(skeletonItem);
// D) Block-Snippets
const blocks = ['metadata', 'overview', 'constants', 'triggers', 'body', 'mapping', 'removal', 'definitions', 'enumeration', 'configuration'];
for (const block of blocks) {
const item = new vscode.CompletionItem(block, vscode.CompletionItemKind.Snippet);
item.insertText = new vscode.SnippetString(`${block}\n $0\nend ${block};`);
item.detail = `Fügt ${block} Block mit 'end' ein`;
items.push(item);
}
const defineItem = new vscode.CompletionItem('define', vscode.CompletionItemKind.Snippet);
defineItem.insertText = new vscode.SnippetString('define ${1:name}(${2:params})\n $0\nend define;');
items.push(defineItem);
const patternItem = new vscode.CompletionItem('pattern', vscode.CompletionItemKind.Snippet);
patternItem.insertText = new vscode.SnippetString('pattern ${1:name}${2:1.0}\n """\n TODO: explain\n """\n\n $0\nend pattern;');
items.push(patternItem);
const syncmappingItem = new vscode.CompletionItem('syncmapping', vscode.CompletionItemKind.Snippet);
syncmappingItem.insertText = new vscode.SnippetString('syncmapping ${1:name}${2:1.0}\n """\n TODO: explain\n """\n\n $0\nend syncmapping;');
items.push(syncmappingItem);
const tableItem = new vscode.CompletionItem('table', vscode.CompletionItemKind.Snippet);
tableItem.insertText = new vscode.SnippetString('table ${1:name}${2:1.0}\n $0\n default -> "unknown";\nend table;');
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;
} else {
// 4. Wir haben nach einem Punkt getippt (z.B. helpers.)
const ns = matchDot[1];
const moduleMethods: vscode.CompletionItem[] = [];
// Regex sucht nach dem Wort direkt vor der Klammer: define [add] method_name(
const extractDefineRegex = /^\s*define\s+.*?\b([a-zA-Z0-9_]+)\s*\(/gm;
// Fall A: Lokaler Definitions-Block (gleiche Datei!)
if (localNamespaces.has(ns)) {
let defMatch;
while ((defMatch = extractDefineRegex.exec(text)) !== null) {
const methodName = defMatch[1];
const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method);
const docStr = this.extractDocString(text, defMatch.index);
if (docStr) {
item.documentation = new vscode.MarkdownString(docStr);
}
moduleMethods.push(item);
}
return moduleMethods;
}
// Fall B: Importiertes Modul (externe Datei)
if (importMap.has(ns)) {
const modPath = importMap.get(ns)!;
const uris = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**', 50);
for (const uri of uris) {
try {
const docBytes = await vscode.workspace.fs.readFile(uri);
const docText = Buffer.from(docBytes).toString('utf8');
const modRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${modPath.replace(/\./g, '\\\\.')}\\s*;`);
if (modRegex.test(docText)) {
let defMatch;
while ((defMatch = extractDefineRegex.exec(docText)) !== null) {
const methodName = defMatch[1];
const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method);
const docStr = this.extractDocString(docText, defMatch.index);
if (docStr) {
item.documentation = new vscode.MarkdownString(docStr);
}
moduleMethods.push(item);
}
break;
}
} catch(e) {}
}
if (moduleMethods.length > 0) {
return moduleMethods;
}
}
}
return items;
}
private extractDocString(text: string, startIndex: number): string | null {
const lines = text.substring(startIndex).split('\n');
let docString = "";
let inDocString = false;
let quoteType = "";
for (let j = 1; j < Math.min(15, lines.length); j++) {
const line = lines[j].trim();
if (line.length === 0) continue;
if (!inDocString) {
if (line.startsWith('"""') || line.startsWith("'''")) {
inDocString = true;
quoteType = line.substring(0, 3);
if (line.length > 3 && line.endsWith(quoteType)) {
docString = line.substring(3, line.length - 3).trim();
break;
}
const content = line.substring(3).trim();
if (content) docString += content + "\n";
} else {
break;
}
} else {
if (line.endsWith(quoteType)) {
const content = line.substring(0, line.length - 3).trim();
if (content) docString += content;
break;
} else {
docString += line + "\n";
}
}
}
return docString.trim() || null;
}
}

View File

@ -2,39 +2,98 @@ import * as vscode from 'vscode';
export class TplDocumentFormatter implements vscode.DocumentFormattingEditProvider { export class TplDocumentFormatter implements vscode.DocumentFormattingEditProvider {
provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): vscode.ProviderResult<vscode.TextEdit[]> { provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): vscode.ProviderResult<vscode.TextEdit[]> {
const text = document.getText();
const isCRLF = text.includes('\r\n');
const lines = text.split(/\r?\n/);
const formattedLines: string[] = [];
const edits: vscode.TextEdit[] = []; const edits: vscode.TextEdit[] = [];
const lines = document.getText().split('\n');
let indentLevel = 0; let indentLevel = 0;
// Zwingt den Formatter strikt zu 3 Spaces (ignoriert VS Code Settings) const indentString = ' ';
const indentString = ' '; let inTripleDouble = false;
let inTripleSingle = false;
let consecutiveEmptyLines = 0;
const alignmentStack: number[] = [];
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim(); const rawLine = lines[i];
if (line.length === 0) { continue; } let line = rawLine.trim();
// 1. Wenn die Zeile einen Block schließt, VORHER die Einrückung verringern const tripleDoubleCount = (rawLine.match(/"""/g) || []).length;
if (line.match(/^(end\s+(pattern|table|if|for|while)|else|elif)\b/)) { const tripleSingleCount = (rawLine.match(/'''/g) || []).length;
let isInternalStringLine = false;
if (inTripleDouble) {
isInternalStringLine = true;
if (tripleDoubleCount % 2 !== 0) { inTripleDouble = false; isInternalStringLine = false; }
} else if (tripleDoubleCount % 2 !== 0) { inTripleDouble = true; }
if (inTripleSingle) {
isInternalStringLine = true;
if (tripleSingleCount % 2 !== 0) { inTripleSingle = false; isInternalStringLine = false; }
} else if (tripleSingleCount % 2 !== 0) { inTripleSingle = true; }
if (isInternalStringLine && (inTripleDouble || inTripleSingle)) {
formattedLines.push(rawLine);
consecutiveEmptyLines = 0;
continue;
}
if (line.length === 0) {
consecutiveEmptyLines++;
if (consecutiveEmptyLines <= 1) { formattedLines.push(""); }
continue;
}
consecutiveEmptyLines = 0;
if (line.match(/^(end\s+(pattern|syncmapping|table|enumeration|configuration|definitions?|define|if|for|while|metadata|overview|constants|triggers|mapping|removal|body)|else|elif)\b/)) {
indentLevel = Math.max(0, indentLevel - 1); indentLevel = Math.max(0, indentLevel - 1);
} }
// 2. Die Zeile neu aufbauen (korrekte Einrückung + getrimmter Text) let newLineText = "";
const newLineText = indentString.repeat(indentLevel) + line; if (alignmentStack.length > 0) {
let spaces = alignmentStack[alignmentStack.length - 1];
// 3. Nur wenn sich was geändert hat, ein Edit erzeugen if (line.match(/^[)\]]/)) { spaces = Math.max(0, spaces - 1); }
if (lines[i] !== newLineText && lines[i] !== newLineText + '\r') { newLineText = " ".repeat(spaces) + line;
const range = new vscode.Range(i, 0, i, lines[i].length); } else {
edits.push(vscode.TextEdit.replace(range, newLineText)); newLineText = indentString.repeat(indentLevel) + line;
} }
// 4. Wenn die Zeile einen Block öffnet, NACHHER die Einrückung erhöhen formattedLines.push(newLineText);
if (line.match(/^(pattern|table|metadata|overview|constants|triggers|body|if|for|while|else|elif)\b/)) {
let cleanLineForParens = newLineText.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, match => " ".repeat(match.length));
const commentIdx = cleanLineForParens.indexOf('//');
if (commentIdx !== -1) {
cleanLineForParens = cleanLineForParens.substring(0, commentIdx) + " ".repeat(cleanLineForParens.length - commentIdx);
}
for (let charIdx = 0; charIdx < cleanLineForParens.length; charIdx++) {
const char = cleanLineForParens[charIdx];
if (char === '(' || char === '[') {
alignmentStack.push(charIdx + 1);
} else if (char === ')' || char === ']') {
if (alignmentStack.length > 0) { alignmentStack.pop(); }
}
}
if (line.match(/^(pattern|syncmapping|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|if|for|while|else|elif)\b/)) {
if (!line.startsWith('end ')) { if (!line.startsWith('end ')) {
indentLevel++; indentLevel++;
} }
} }
} }
const newText = formattedLines.join(isCRLF ? '\r\n' : '\n');
if (newText !== text) {
const fullRange = new vscode.Range(document.positionAt(0), document.positionAt(text.length));
edits.push(vscode.TextEdit.replace(fullRange, newText));
}
return edits; return edits;
} }
} }

123
src/TplHoverProvider.ts Normal file
View File

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

View File

@ -1,41 +1,29 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { TplCodeActionProvider } from './TplCodeActionProvider'; import { TplCodeActionProvider } from './TplCodeActionProvider';
import { TplDocumentFormatter } from './TplDocumentFormatter'; import { TplDocumentFormatter } from './TplDocumentFormatter';
import { TplCompletionProvider } from './TplCompletionProvider';
import { TplHoverProvider } from './TplHoverProvider';
// --- Konstanten --- const SUPPORTED_TPL_VERSION = 23;
const SUPPORTED_TPL_VERSION = 23; // TPL 1.23
const warnedDocuments = new Set<string>(); const warnedDocuments = new Set<string>();
// Erlaubte Phasen in einem Pattern (strikte Reihenfolge laut BMC TPL) enum PatternPhase { None = 0, Metadata = 1, Overview = 2, Constants = 3, Triggers = 4, Mapping = 5, Removal = 6, Body = 7 }
enum PatternPhase {
None = 0,
Metadata = 1,
Overview = 2,
Constants = 3,
Triggers = 4,
Body = 5
}
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
// 1. Diagnostic Collection (Der Scanner für rote/gelbe Wellenlinien)
const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner'); const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner');
context.subscriptions.push(diagnosticCollection); context.subscriptions.push(diagnosticCollection);
// 2. Events registrieren (Ausführen beim Öffnen und Ändern)
vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection)); vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection));
vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection)); vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection));
// 3. Quick-Fix Provider (Code Actions für Glühbirnen-Menü) registrieren context.subscriptions.push(vscode.languages.registerCodeActionsProvider('tpl', new TplCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }));
context.subscriptions.push( context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider('tpl', new TplDocumentFormatter()));
vscode.languages.registerCodeActionsProvider('tpl', new TplCodeActionProvider(), { context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.'));
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider()));
})
);
// 4. Format Document Provider (Auto-Aufräumen mit Shift+Alt+F) for (const doc of vscode.workspace.textDocuments) {
context.subscriptions.push( validateTplDocument(doc, diagnosticCollection);
vscode.languages.registerDocumentFormattingEditProvider('tpl', new TplDocumentFormatter()) }
);
} }
function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) { function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
@ -49,131 +37,201 @@ function validateTplDocument(document: vscode.TextDocument, collection: vscode.D
let toleranceMode = false; let toleranceMode = false;
let hasModuleDef = false; let hasModuleDef = false;
// --- State Machine Variablen --- let inBlock: 'none' | 'pattern' | 'table' | 'definitions' | 'enumeration' | 'configuration' = 'none';
let inBlock: 'none' | 'pattern' | 'table' | 'definitions' = 'none';
let currentPatternPhase: PatternPhase = PatternPhase.None; let currentPatternPhase: PatternPhase = PatternPhase.None;
let inMultiLineComment = false; let inMultiLineComment = false;
// State Tracking für Quick-Fixes let inTripleDouble = false;
let currentPatternName = ""; let inTripleSingle = false;
let foundMetadata = { author: false, publisher: false, origin: false, tree_path: false }; let stringNeedsSemicolon = false;
let metadataBlockLine = -1;
let bodyLineCount = 0; let activePhaseName = "";
let activePhaseLine = -1;
let currentPatternName = "";
// Modul-Ebene Metadaten Tracking
let inModuleMetadata = false;
let hasModuleMetadata = false;
let moduleMetadataBlockLine = -1;
let foundModuleMetadata = { origin: false, author: false, products: false, publishers: false, tree_path: false };
const parenStack: number[] = [];
const bracketStack: number[] = [];
const openDefines: number[] = [];
// --- 1. Versions-Check im Header ---
const versionMatch = text.match(/tpl\s+1\.(\d+)\s+module/); const versionMatch = text.match(/tpl\s+1\.(\d+)\s+module/);
if (versionMatch) { if (versionMatch) {
tplVersion = parseInt(versionMatch[1], 10); tplVersion = parseInt(versionMatch[1], 10);
if (tplVersion > SUPPORTED_TPL_VERSION) { if (tplVersion > SUPPORTED_TPL_VERSION) {
toleranceMode = true; toleranceMode = true;
if (!warnedDocuments.has(document.uri.toString())) { if (!warnedDocuments.has(document.uri.toString())) {
vscode.window.showInformationMessage(`TPL 1.${tplVersion} erkannt. Linter läuft im Toleranz-Modus (Fehler werden zu Warnungen).`); vscode.window.showInformationMessage(`TPL 1.${tplVersion} erkannt. Linter läuft im Toleranz-Modus.`);
warnedDocuments.add(document.uri.toString()); warnedDocuments.add(document.uri.toString());
} }
} }
} }
function addDiag(lineIdx: number, start: number, end: number, msg: string, severity: vscode.DiagnosticSeverity): vscode.Diagnostic { function addDiag(lineIdx: number, start: number, end: number, msg: string, severity: vscode.DiagnosticSeverity): vscode.Diagnostic {
if (toleranceMode && severity === vscode.DiagnosticSeverity.Error) { if (toleranceMode && severity === vscode.DiagnosticSeverity.Error) { severity = vscode.DiagnosticSeverity.Warning; }
severity = vscode.DiagnosticSeverity.Warning;
}
const diag = new vscode.Diagnostic(new vscode.Range(lineIdx, start, lineIdx, end), msg, severity); const diag = new vscode.Diagnostic(new vscode.Range(lineIdx, start, lineIdx, end), msg, severity);
diagnostics.push(diag); diagnostics.push(diag);
return diag; return diag;
} }
// --- 2. Der Haupt-Scanner (Zeile für Zeile) ---
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const rawLine = lines[i]; const rawLine = lines[i];
const line = rawLine.trim(); const line = rawLine.trim();
// --- Tab-Check (Strikte 3-Spaces-Regel) --- 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("'''"); }
const tabIndex = rawLine.indexOf('\t'); const tabIndex = rawLine.indexOf('\t');
if (tabIndex !== -1) { if (tabIndex !== -1) {
const diag = addDiag( const diag = addDiag(i, tabIndex, tabIndex + 1, "Bitte Tabs durch Spaces ersetzen", vscode.DiagnosticSeverity.Warning);
i,
tabIndex,
rawLine.length,
"Tabs sind hier nicht erlaubt! Bitte nutze exakt 3 Spaces für Einrückungen. (Tipp: Mit 'Alt + Shift' + Mausklick/Pfeiltasten kannst du mehrere Zeilen gleichzeitig markieren und korrigieren).",
vscode.DiagnosticSeverity.Warning
);
diag.code = 'invalid_tab'; diag.code = 'invalid_tab';
} }
// Kommentare überspringen (aber Regex für Semikolon-Check braucht MultiLine-Info) let cleanLineForParens = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '');
if (line.startsWith('//')) { continue; } const commentIdx = cleanLineForParens.indexOf('//');
if (line.startsWith('/*')) { inMultiLineComment = true; continue; } if (commentIdx !== -1) { cleanLineForParens = cleanLineForParens.substring(0, commentIdx); }
if (inMultiLineComment) {
if (line.includes('*/')) { inMultiLineComment = false; } for (let charIdx = 0; charIdx < cleanLineForParens.length; charIdx++) {
continue; 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); }
}
} }
if (line.startsWith('//') || line.startsWith('/*')) { if (line.startsWith('/*')) inMultiLineComment = true; continue; }
if (inMultiLineComment) { if (line.includes('*/')) inMultiLineComment = false; continue; }
if (line.length === 0) { continue; } if (line.length === 0) { continue; }
// --- Semikolon-Check --- if (isClosingString) {
const noSemicolonNeeded = /^(pattern|table|metadata|overview|constants|triggers|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|\/\/|\/\*|\*)/; if (stringNeedsSemicolon && !line.endsWith(';')) {
if (!noSemicolonNeeded.test(line)) { addDiag(i, rawLine.length - 1 < 0 ? 0 : rawLine.length - 1, rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error);
if (!line.endsWith(';') && !line.endsWith('{') && !line.endsWith(',')) { }
addDiag(i, rawLine.length - 1 < 0 ? 0 : rawLine.length - 1, rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error); } else {
const noSemicolonNeeded = /^(pattern|syncmapping|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|\/\/|\/\*|\*)/;
// Enumerations benötigen keine Semikolons
if (!noSemicolonNeeded.test(line) && !justOpenedString && parenStack.length === 0 && bracketStack.length === 0 && inBlock !== 'enumeration') {
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);
}
} }
} }
// Modul-Definition prüfen (Muss als Erstes stehen)
if (!hasModuleDef) { if (!hasModuleDef) {
if (line.startsWith('tpl ')) { if (line.startsWith('tpl ')) { hasModuleDef = true; }
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; // --- Modul-Metadaten Logik ---
if (line.startsWith('metadata') && inBlock === 'none') {
inModuleMetadata = true;
hasModuleMetadata = true;
moduleMetadataBlockLine = 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 && moduleMetadataBlockLine !== -1) {
const diag = addDiag(moduleMetadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten (Modul-Ebene): ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
diag.code = 'missing_module_metadata';
}
}
continue;
}
// Block checks
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);
} }
} }
// --- Block-Einstiege (Pattern & Table) --- if (line.startsWith('definitions ') || line.startsWith('definition ')) {
if (line.startsWith('pattern ')) { if (inBlock !== 'none') { addDiag(i, 0, line.length, "Ein Definitions-Block muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
if ((inBlock as string) !== 'none') { addDiag(i, 0, 7, "Ein Pattern kann nicht innerhalb eines anderen Blocks 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); }
inBlock = 'pattern'; inBlock = 'pattern';
currentPatternPhase = PatternPhase.None; currentPatternPhase = PatternPhase.None;
activePhaseName = "";
activePhaseLine = -1;
const nameMatch = line.match(/pattern\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/); const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/);
if (nameMatch) { if (nameMatch) { currentPatternName = nameMatch[2]; }
currentPatternName = nameMatch[1]; else { addDiag(i, 0, rawLine.length, "Ungültige Pattern-Deklaration. Erwartet: 'pattern <Name> <Version>'", vscode.DiagnosticSeverity.Error); }
} else {
addDiag(i, 0, rawLine.length, "Ungültige Pattern-Deklaration. Erwartet: 'pattern <Name> <Version>'", vscode.DiagnosticSeverity.Error);
}
foundMetadata = { author: false, publisher: false, origin: false, tree_path: false };
metadataBlockLine = -1;
bodyLineCount = 0;
continue; continue;
} }
if (line.startsWith('table ')) { if (line.startsWith('table ')) {
if ((inBlock as string) !== 'none') { addDiag(i, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); } if (inBlock !== 'none') { addDiag(i, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
inBlock = 'table'; inBlock = 'table'; continue;
if (!line.match(/table\s+[a-zA-Z0-9_]+\s+\d+\.\d+/)) {
addDiag(i, 0, rawLine.length, "Ungültige Tabellen-Deklaration. Erwartet: 'table <Name> <Version>'", vscode.DiagnosticSeverity.Error);
}
continue;
} }
// --- Innerhalb eines Patterns --- if (inBlock === 'pattern') {
if ((inBlock as string) === 'pattern') {
const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => { const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => {
if (line.startsWith(phaseName)) { if (line.startsWith(phaseName)) {
if ((currentPatternPhase as PatternPhase) >= phaseEnum) { if (activePhaseName !== "") { addDiag(activePhaseLine, 0, activePhaseName.length, `Block '${activePhaseName}' wurde nicht mit 'end ${activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
addDiag(i, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt. Die Reihenfolge ist: metadata -> overview -> constants -> triggers -> body.`, vscode.DiagnosticSeverity.Error); if (currentPatternPhase >= phaseEnum) { addDiag(i, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt.`, vscode.DiagnosticSeverity.Error); }
}
if ((currentPatternPhase as PatternPhase) === PatternPhase.Metadata && phaseEnum > PatternPhase.Metadata) {
const missing = Object.keys(foundMetadata).filter(k => !(foundMetadata as any)[k]);
if (missing.length > 0 && metadataBlockLine !== -1) {
const diag = addDiag(metadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten: ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
diag.code = 'missing_metadata';
}
}
currentPatternPhase = phaseEnum; currentPatternPhase = phaseEnum;
if (phaseEnum === PatternPhase.Metadata) { metadataBlockLine = i; } activePhaseName = phaseName;
activePhaseLine = i;
} }
}; };
@ -181,81 +239,66 @@ function validateTplDocument(document: vscode.TextDocument, collection: vscode.D
checkPhase('overview', PatternPhase.Overview); checkPhase('overview', PatternPhase.Overview);
checkPhase('constants', PatternPhase.Constants); checkPhase('constants', PatternPhase.Constants);
checkPhase('triggers', PatternPhase.Triggers); checkPhase('triggers', PatternPhase.Triggers);
checkPhase('body', PatternPhase.Body); checkPhase('mapping', PatternPhase.Mapping);
checkPhase('removal', PatternPhase.Removal);
if ((currentPatternPhase as PatternPhase) === PatternPhase.Metadata) { if (line.startsWith('body')) {
if (line.startsWith('author ')) { foundMetadata.author = true; } if (activePhaseName !== "") { addDiag(activePhaseLine, 0, activePhaseName.length, `Block '${activePhaseName}' wurde nicht mit 'end ${activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
if (line.startsWith('publisher ')) { foundMetadata.publisher = true; } activePhaseName = "body";
if (line.startsWith('origin ')) { foundMetadata.origin = true; } activePhaseLine = i;
if (line.startsWith('tree_path ')) { foundMetadata.tree_path = true; }
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; }
}
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;
}
} }
if ((currentPatternPhase as PatternPhase) === PatternPhase.Body && !line.startsWith('body')) { const closers = ['end metadata', 'end overview', 'end constants', 'end triggers', 'end mapping', 'end removal', 'end body'];
bodyLineCount++; for (const closer of closers) {
if (bodyLineCount === 1) { if (line.startsWith(closer)) {
const expectedLog = `log.debug("Pattern start ${currentPatternName}");`; const phase = closer.replace('end ', '').replace(';', '').trim();
if (!line.includes(`log.debug("Pattern start ${currentPatternName}`)) { if (activePhaseName !== phase) { addDiag(i, 0, line.length, `Unerwartetes '${closer}'. Erwartet wurde 'end ${activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
const diag = addDiag(i, 0, rawLine.length, `Body muss mit dem Log-Statement beginnen: ${expectedLog}`, vscode.DiagnosticSeverity.Error); else { activePhaseName = ""; }
diag.code = 'missing_log_start';
diag.source = expectedLog;
}
} }
} }
} }
// --- Innerhalb einer Tabelle --- const eqMatch = rawLine.match(/(?<![<>=!:])=(?![=])/);
if ((inBlock as string) === 'table') { if (eqMatch && eqMatch.index && !justOpenedString) {
if (!line.startsWith('end ') && !line.includes('table ')) {
if (!line.includes('->') && !line.includes(':=')) {
addDiag(i, 0, rawLine.length, "Ungültiger Tabelleneintrag. Nutze '[\"Key\"] -> \"Value\";' oder 'default := \"Value\";'", vscode.DiagnosticSeverity.Error);
}
}
}
// --- Allgemeine Syntax-Checks ---
const eqMatch = rawLine.match(/(?<![<>=!])=(?![=])/);
if (eqMatch && eqMatch.index) {
if (!line.includes('"') && !line.includes("'")) { if (!line.includes('"') && !line.includes("'")) {
addDiag(i, eqMatch.index, eqMatch.index + 1, "Zuweisung in TPL erfolgt mit ':=', nicht mit '='. '=' ist für Vergleiche.", vscode.DiagnosticSeverity.Error); addDiag(i, eqMatch.index, eqMatch.index + 1, "Zuweisung in TPL erfolgt mit ':=', nicht mit '='. '=' ist für Vergleiche.", vscode.DiagnosticSeverity.Error);
} }
} }
if (line.includes('?') && line.includes(':')) { if (line.startsWith('end pattern') || line.startsWith('end syncmapping')) {
if (tplVersion < 15) { if (activePhaseName !== "") { addDiag(activePhaseLine, 0, activePhaseName.length, `Block '${activePhaseName}' wurde nicht mit 'end ${activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
addDiag(i, 0, rawLine.length, `Ternärer Operator (? :) erfordert mindestens TPL 1.15 (Aktuell: 1.${tplVersion}).`, vscode.DiagnosticSeverity.Error); if (inBlock !== 'pattern') { addDiag(i, 0, 11, "'end pattern' gefunden, ohne offenen Pattern-Block.", vscode.DiagnosticSeverity.Error); }
}
}
const streamMatch = rawLine.match(/\bstream\b/);
if (streamMatch && streamMatch.index !== undefined) {
if (tplVersion < 20) {
addDiag(i, streamMatch.index, streamMatch.index + 6, `Das 'stream'-Keyword erfordert mindestens TPL 1.20.`, vscode.DiagnosticSeverity.Error);
}
}
const notInMatch = rawLine.match(/\bnot\s+in\b/);
if (notInMatch && notInMatch.index !== undefined) {
if (tplVersion < 18) {
addDiag(i, notInMatch.index, notInMatch.index + 6, `'not in' erfordert mindestens TPL 1.18.`, vscode.DiagnosticSeverity.Error);
}
}
// --- Block-Ausstiege ---
if (line.startsWith('end pattern')) {
if ((inBlock as string) !== 'pattern') {
addDiag(i, 0, 11, "'end pattern' gefunden, aber es wurde kein Pattern gestartet.", vscode.DiagnosticSeverity.Error);
} else if ((currentPatternPhase as PatternPhase) < PatternPhase.Body) {
addDiag(i, 0, 11, "Dem Pattern fehlt ein 'body'-Block.", vscode.DiagnosticSeverity.Error);
}
inBlock = 'none'; inBlock = 'none';
} }
else if (line.startsWith('end table')) { else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) {
if ((inBlock as string) !== 'table') {
addDiag(i, 0, 9, "'end table' gefunden, aber es wurde keine Tabelle gestartet.", vscode.DiagnosticSeverity.Error);
}
inBlock = 'none'; inBlock = 'none';
} }
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); }
inBlock = 'none';
}
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); }
}
} }
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); }
collection.set(document.uri, diagnostics); collection.set(document.uri, diagnostics);
} }

2166
syntaxes/tpl.tmLanguage.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +0,0 @@
# Welcome to your VS Code Extension
## What's in the folder
* This folder contains all of the files necessary for your extension.
* `package.json` - this is the manifest file in which you declare your extension and command.
* The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
* `src/extension.ts` - this is the main file where you will provide the implementation of your command.
* The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
## Setup
* install the recommended extensions (amodio.tsl-problem-matcher, ms-vscode.extension-test-runner, and dbaeumer.vscode-eslint)
## Get up and running straight away
* Press `F5` to open a new window with your extension loaded.
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
* Set breakpoints in your code inside `src/extension.ts` to debug your extension.
* Find output from your extension in the debug console.
## Make changes
* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
## Explore the API
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
## Run tests
* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner)
* Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered.
* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A`
* See the output of the test result in the Test Results view.
* Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder.
* The provided test runner will only consider files matching the name pattern `**.test.ts`.
* You can create folders inside the `test` folder to structure your tests any way you want.
## Go further
* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace.
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).