Compare commits
No commits in common. "master" and "beta_3" have entirely different histories.
88
README.md
88
README.md
|
|
@ -1,84 +1,78 @@
|
||||||
# BMC TPL Code All-Around
|
# BMC TPL Code All-Around
|
||||||
|
|
||||||
**Linter, Auto-Formatter, IntelliSense und Hover-Dokumentationen für die BMC TPL (Template Language).**
|
**Linter, Auto-Formatter, IntelliSense und Hover-Dokumentationen für die BMC TPL (Template Language).**[cite: 5]
|
||||||
|
|
||||||
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.
|
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.[cite: 5]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
### 🤖 Intelligenter Linter (Syntax-Scanner)
|
### 🤖 Intelligenter Linter (Syntax-Scanner)
|
||||||
Der Linter analysiert den Code in Echtzeit, merkt sich Zustände über Zeilenumbrüche hinweg und warnt vor typischen TPL-Fehlern:
|
Der Linter analysiert den Code in Echtzeit und warnt vor typischen TPL-Fehlern:[cite: 5]
|
||||||
* **Strikte Hierarchie-Prüfung:** Überwacht die korrekte Reihenfolge der Blöcke innerhalb von Patterns und Syncmappings (`metadata` -> `overview` -> `constants` -> `triggers` -> `mapping`/`removal` -> `body`).
|
* **Strikte Hierarchie-Prüfung:** Überwacht die korrekte Reihenfolge der Blöcke innerhalb von Patterns und Syncmappings (`metadata` -> `overview` -> `constants` -> `triggers` -> `mapping`/`removal` -> `body`).[cite: 5]
|
||||||
* **Smarter Zuweisungs-Check:** Findet fehlende Semikolons (toleriert sie aber in `enumeration`-Blöcken oder bei umgebrochenen Code-Zeilen). Warnt streng mit einem Fehler, wenn `=` anstelle von `:=` für Variablenzuweisungen genutzt wird (außerhalb von legitimen `if`/`where` Vergleichen).
|
* **Semicolon- & Zuweisungs-Check:** Findet fehlende Semikolons (ignoriert sie aber intelligent in `enumeration`-Blöcken) und warnt, wenn `=` anstelle von `:=` für Zuweisungen genutzt wird.[cite: 5]
|
||||||
* **Block- & Klammer-Tracking:** Erkennt nicht geschlossene Blöcke (wie fehlende `end body;` oder `end pattern;`) und findet ungeschlossene runde `(` oder eckige `[` Klammern.
|
* **Block- & Klammer-Tracking:** Erkennt nicht geschlossene Blöcke (wie fehlende `end body;` oder `end pattern;`) und findet ungeschlossene runde `(` oder eckige `[` Klammern.[cite: 5]
|
||||||
* **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.
|
* **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.[cite: 5]
|
||||||
* **Versions-Prüfung:** Gleicht die im Modul deklarierte TPL-Version mit der in deinen Settings definierten Version ab und warnt vor veralteten Skripten.
|
* **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.[cite: 5]
|
||||||
|
|
||||||
### ⚙️ Konfigurierbare Plugin-Einstellungen (Settings)
|
|
||||||
Personalisiere das Plugin direkt über die VS Code Benutzeroberfläche (File > Preferences > Settings > Extensions > BMC TPL Einstellungen):
|
|
||||||
* **Standard Metadaten:** Hinterlege deine eigenen Werte für `Author`, `Publisher`, `Origin` und den `Tree Path`. Diese werden beim Generieren neuer Module automatisch eingesetzt.
|
|
||||||
* **TPL Zielversion:** Definiere die TPL-Version (z.B. `23` für 1.23), für die du standardmäßig entwickelst.
|
|
||||||
* **Live-Reload:** Änderungen an den Einstellungen greifen sofort, ganz ohne Neustart des Editors.
|
|
||||||
|
|
||||||
### 🎨 Auto-Formatter & Visual Alignment (`Shift + Alt + F`)
|
### 🎨 Auto-Formatter & Visual Alignment (`Shift + Alt + F`)
|
||||||
Schluss mit kaputten Einrückungen! Der Formatter bringt deinen Code in Bestform:
|
Schluss mit kaputten Einrückungen! Der Formatter bringt deinen Code in Bestform:[cite: 5]
|
||||||
* **VS Code Standard:** Erzwingt strikt 4 Leerzeichen pro Einrückungs-Ebene.
|
* **VS Code Standard:** Erzwingt strikt 4 Leerzeichen pro Einrückungs-Ebene.[cite: 5]
|
||||||
* **Dynamische Block-Einrückung:** Erkennt automatisch startende und endende Blöcke (`if`, `for`, `pattern`, `table` etc.) und passt die Einrückungsebene an.
|
* **Dynamische Block-Einrückung:** Erkennt automatisch startende und endende Blöcke (`if`, `for`, `pattern`, `table` etc.) und passt die Einrückungsebene an.[cite: 5]
|
||||||
* **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.
|
* **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.[cite: 5]
|
||||||
|
|
||||||
### 💡 IntelliSense & Autovervollständigung (Cross-File)
|
### 💡 IntelliSense & Autovervollständigung (Cross-File)
|
||||||
Spart extrem viel Zeit beim Coden durch massives Kontext-Wissen:
|
Spart extrem viel Zeit beim Coden durch Kontext-Wissen:[cite: 5]
|
||||||
* **Volle BMC Standard-Bibliothek:** Nahtlose Integration aller Built-In Funktionen (z.B. `discovery`, `number`, `text`, `model`). Unterstützt Parameter-Überladungen (z.B. `number.toText` wahlweise mit oder ohne Basis) per Tab-Navigation.
|
* **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![cite: 5]
|
||||||
* **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 vorgeschlagen.
|
* **Lokale Namespaces:** Erkennt `definitions`-Blöcke in der gleichen Datei und schlägt deren Methoden vor.[cite: 5]
|
||||||
* **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.[cite: 5]
|
||||||
* **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.[cite: 5]
|
||||||
|
|
||||||
### 🔍 Hover-Dokumentation (Tooltips)
|
### 🔍 Hover-Dokumentation (Tooltips)
|
||||||
Wenn du mit der Maus über einem Methodennamen, einem Pattern oder einer Tabelle stehen bleibst:
|
Wenn du mit der Maus über einem Methodennamen, einem Pattern oder einer Tabelle stehen bleibst:[cite: 5]
|
||||||
* **Offizielle BMC Doku:** Volle Integration der originalen, mehrzeiligen BMC Dokumentationen für alle eingebauten Funktionen (kein Browser mehr nötig!).
|
* **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.[cite: 5]
|
||||||
* **DocString Extraction:** Liest eigene Multi-Line-Kommentare (`"""` oder `'''`) aus, die unter `define` oder `pattern` Deklarationen stehen, und rendert sie als saubere Markdown-Tooltips (auch projektübergreifend!).
|
* **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.[cite: 5]
|
||||||
|
|
||||||
### ⚡ Quick Fixes (Code Actions / Glühbirne)
|
### ⚡ Quick Fixes (Code Actions / Glühbirne)
|
||||||
Bietet sofortige Reparaturoptionen mit einem Klick:
|
Bietet sofortige Reparaturoptionen mit einem Klick:[cite: 5]
|
||||||
* **"Fehlende Modul-Metadaten einfügen":** Generiert sofort alle 5 Pflichtfelder im Modul-Metadata-Block.
|
* **"Fehlende Modul-Metadaten einfügen":** Generiert sofort alle 5 Pflichtfelder im Modul-Metadata-Block.[cite: 5]
|
||||||
* **"Log-Statement einfügen":** Baut das fehlende `log.debug("Start pattern...");` direkt am Anfang des `body`-Blocks ein.
|
* **"Log-Statement einfügen":** Baut das fehlende `log.debug("Start pattern...");` direkt am Anfang des `body`-Blocks ein.[cite: 5]
|
||||||
* **"Tabs zu Spaces":** Ersetzt sofort alle illegalen Tabs (`\t`) in einer Zeile durch jeweils 4 korrekte Leerzeichen.
|
* **"Tabs zu Spaces":** Ersetzt sofort alle illegalen Tabs (`\t`) in einer Zeile durch jeweils 4 korrekte Leerzeichen.[cite: 5]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Snippets (Code-Vorlagen)
|
## 🚀 Snippets (Code-Vorlagen)
|
||||||
|
|
||||||
Einfach das Keyword tippen und `Enter` drücken, um komplette, perfekt eingerückte Blöcke zu generieren:
|
Einfach das Keyword tippen und `Enter` drücken, um komplette, perfekt eingerückte Blöcke zu generieren:[cite: 5]
|
||||||
|
|
||||||
| Keyword | Beschreibung |
|
| Keyword | Beschreibung |
|
||||||
| :--- | :--- |
|
| :--- | :--- |
|
||||||
| `tpl_skeleton` | **God-Snippet**: Generiert ein komplett frisches TPL Modul. Nutzt automatisch deine hinterlegten Settings für Metadaten und Version. |
|
| `tpl_skeleton` | **God-Snippet**: Generiert eine komplett frische `.tpl` Datei inklusive Modul-Definition, Pflicht-Metadaten, Pattern, Triggern und Log-Statements. |[cite: 5]
|
||||||
| `pattern` / `syncmapping` | Baut einen leeren Pattern/Syncmapping-Block inklusive DocString und End-Tag. |
|
| `pattern` | Baut einen leeren Pattern-Block inklusive DocString und `end pattern;`. |[cite: 5]
|
||||||
| `define` | Generiert eine Methodendefinition (`define name(params) ... end define;`). |
|
| `syncmapping` | Baut einen leeren Syncmapping-Block inklusive DocString. |[cite: 5]
|
||||||
| `metadata`, `overview`, `body`... | Baut den jeweiligen Block auf und platziert den Cursor in der Mitte. |
|
| `define` | Generiert eine Methodendefinition (`define name(params) ... end define;`). |[cite: 5]
|
||||||
| `table` | Erstellt eine Tabelle inklusive des `default -> "unknown";` Fallbacks. |
|
| `metadata`, `overview`, `body`... | Baut den jeweiligen Block auf und platziert den Cursor in der Mitte. |[cite: 5]
|
||||||
| `if`, `elif`, `else` | Baut bedingte Abfragen inkl. `then` und `end if;` und platziert den Cursor sauber bei der Bedingung. |
|
| `table` | Erstellt eine Tabelle inklusive des `default -> "unknown";` Fallbacks. |[cite: 5]
|
||||||
| `for`, `while` | Baut Standard-Schleifen inkl. `do` und End-Tag und lässt dich direkt iterieren. |
|
| `if`, `for` | Baut Standard-Schleifen und Abfragen. |[cite: 5]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Installation & Nutzung
|
## 🛠️ Installation & Nutzung
|
||||||
|
|
||||||
1. Das Plugin aktivieren. (Von den Releases das neueste herunterladen und einfach die `.vsix` Datei in die Add-ons von Visual Studio Code ziehen).
|
1. Das Plugin aktivieren. (Von den Releases das neuste runterladen und einfach die vsix datei in die Add-ons von Visual Studio Code ziehen)[cite: 5]
|
||||||
2. In einem Projekt mit TPL Dateien eine davon öffnen und die Analyse durchlaufen lassen.
|
2. In einem Projekt mit TPL Dateien eine davon öffnen und die Analyse durchlaufen lassen.[cite: 5]
|
||||||
3. Der Linter startet automatisch bei jedem Tastendruck.
|
3. Der Linter startet automatisch bei jedem Tastendruck.[cite: 5]
|
||||||
4. **Formatieren:** Rechtsklick -> *Dokument formatieren* (oder `Shift + Alt + F`).
|
4. **Formatieren:** Rechtsklick -> *Dokument formatieren* (oder `Shift + Alt + F`).[cite: 5]
|
||||||
5. **Autovervollständigung:** Triggert automatisch beim Tippen, speziell nach einem Punkt (`.`) bei Modul-Aufrufen.
|
5. **Autovervollständigung:** Triggert automatisch beim Tippen, speziell nach einem Punkt (`.`) bei Modul-Aufrufen.[cite: 5]
|
||||||
6. **Quick Fixes:** Auf die gelbe/rote Glühbirne am Zeilenanfang klicken oder `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac) drücken.
|
6. **Quick Fixes:** Auf die gelbe/rote Glühbirne am Zeilenanfang klicken oder `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac) drücken.[cite: 5]
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📜 Lizenz
|
## 📜 Lizenz
|
||||||
|
|
||||||
Dieses Projekt ist unter der **MIT License** lizenziert.
|
Dieses Projekt ist unter der **MIT License** lizenziert.[cite: 6]
|
||||||
|
|
||||||
Copyright (c) 2026 Thomas Wehnert
|
Copyright (c) 2026 Thomas Wehnert[cite: 6]
|
||||||
|
|
||||||
Jedem, der eine Kopie dieser Software erhält, wird hiermit kostenlos die Erlaubnis erteilt, ohne Einschränkung mit der Software zu handeln, einschließlich und ohne Einschränkung der Rechte zur Nutzung, zum Kopieren, Ändern, Zusammenführen, Veröffentlichen, Verteilen, Unterlizenzieren und/oder Verkaufen von Kopien der Software, sofern der obige Urheberrechtshinweis und dieser Genehmigungshinweis in allen Kopien oder wesentlichen Teilen der Software enthalten sind. Die Software wird "ohne Mängelgewähr" (as is) zur Verfügung gestellt.
|
Jedem, der eine Kopie dieser Software erhält, wird hiermit kostenlos die Erlaubnis erteilt, ohne Einschränkung mit der Software zu handeln, einschließlich und ohne Einschränkung der Rechte zur Nutzung, zum Kopieren, Ändern, Zusammenführen, Veröffentlichen, Verteilen, Unterlizenzieren und/oder Verkaufen von Kopien der Software, sofern der obige Urheberrechtshinweis und dieser Genehmigungshinweis in allen Kopien oder wesentlichen Teilen der Software enthalten sind.[cite: 6] Die Software wird "ohne Mängelgewähr" (as is) zur Verfügung gestellt.[cite: 6]
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export class TplCodeActionProvider implements vscode.CodeActionProvider {
|
||||||
|
|
||||||
|
public provideCodeActions(
|
||||||
|
document: vscode.TextDocument,
|
||||||
|
range: vscode.Range | vscode.Selection,
|
||||||
|
context: vscode.CodeActionContext,
|
||||||
|
token: vscode.CancellationToken
|
||||||
|
): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> {
|
||||||
|
|
||||||
|
const actionList: vscode.CodeAction[] = [];
|
||||||
|
|
||||||
|
for (const diag of context.diagnostics) {
|
||||||
|
|
||||||
|
// 1. Fix für fehlende Modul-Metadaten (Auf Modul-Ebene, 4 Spaces)
|
||||||
|
if (diag.code === 'missing_module_metadata') {
|
||||||
|
const action = new vscode.CodeAction('Fehlende Modul-Metadaten einfügen', vscode.CodeActionKind.QuickFix);
|
||||||
|
action.edit = new vscode.WorkspaceEdit();
|
||||||
|
|
||||||
|
const fehlendeFelder = diag.message.replace('Fehlende Pflicht-Metadaten (Modul-Ebene): ', '').split(', ');
|
||||||
|
let neuerText = '';
|
||||||
|
|
||||||
|
if (fehlendeFelder.includes('origin')) { neuerText += ` origin := 'Origin';\n`; }
|
||||||
|
if (fehlendeFelder.includes('author')) { neuerText += ` author := 'Dein Name';\n`; }
|
||||||
|
if (fehlendeFelder.includes('products')) { neuerText += ` products := 'Produktname';\n`; }
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fix für fehlendes Log-Statement im Body (8 Spaces Einrückung)
|
||||||
|
if (diag.code === 'missing_log_start') {
|
||||||
|
const action = new vscode.CodeAction('Log-Statement am Body-Anfang einfügen', vscode.CodeActionKind.QuickFix);
|
||||||
|
action.edit = new vscode.WorkspaceEdit();
|
||||||
|
|
||||||
|
const erwartetesLog = diag.source as string;
|
||||||
|
const neuerText = ` ${erwartetesLog}\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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fix für Tabs (Ersetzt \t durch 4 Spaces in der ganzen Zeile)
|
||||||
|
if (diag.code === 'invalid_tab') {
|
||||||
|
const action = new vscode.CodeAction('Tabs in dieser Zeile durch 4 Spaces ersetzen', vscode.CodeActionKind.QuickFix);
|
||||||
|
action.edit = new vscode.WorkspaceEdit();
|
||||||
|
|
||||||
|
const zeile = document.lineAt(diag.range.start.line);
|
||||||
|
const bereinigterText = zeile.text.replace(/\t/g, ' ');
|
||||||
|
|
||||||
|
action.edit.replace(document.uri, zeile.range, bereinigterText);
|
||||||
|
action.diagnostics = [diag];
|
||||||
|
action.isPreferred = true;
|
||||||
|
actionList.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { tplBuiltIns } from './TplBuiltIns';
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchDot = linePrefix.match(/([a-zA-Z0-9_]+)\.$/);
|
||||||
|
|
||||||
|
if (!matchDot) {
|
||||||
|
// A) Externe & Lokale Module
|
||||||
|
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) Standard BMC Namespaces
|
||||||
|
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, def] of Object.entries(tplBuiltIns[key])) {
|
||||||
|
for (const snippet of def.snippets) {
|
||||||
|
// Snippet-Syntax entfernen für das Anzeige-Label (z.B. "${1:number}" -> "number")
|
||||||
|
const cleanSnippet = snippet.replace(/\$\{\d+:([^}]+)\}/g, '$1');
|
||||||
|
|
||||||
|
const item = new vscode.CompletionItem(`${glob}(${cleanSnippet})`, vscode.CompletionItemKind.Function);
|
||||||
|
// Setzt das echte Snippet mit Tab-Stops ein
|
||||||
|
item.insertText = new vscode.SnippetString(`${glob}(${snippet})`);
|
||||||
|
// VS Code darf nur nach dem Methodennamen filtern, nicht nach den Parametern!
|
||||||
|
item.filterText = glob;
|
||||||
|
item.documentation = new vscode.MarkdownString(def.description);
|
||||||
|
item.detail = "BMC Globale Funktion";
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C) Lokale Variablen
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
|
||||||
|
// D) Snippets
|
||||||
|
|
||||||
|
// Konfiguration laden
|
||||||
|
const config = vscode.workspace.getConfiguration('tpl');
|
||||||
|
const author = config.get<string>('metadata.author', 'Kuro');
|
||||||
|
const origin = config.get<string>('metadata.origin', 'Custom');
|
||||||
|
const publisher = config.get<string>('metadata.publisher', 'IT-Operations');
|
||||||
|
const treePathBase = config.get<string>('metadata.treePathBase', 'Grafing');
|
||||||
|
const tplVersion = config.get<number>('version', 23);
|
||||||
|
|
||||||
|
const skeletonItem = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
|
||||||
|
skeletonItem.insertText = new vscode.SnippetString(
|
||||||
|
`tpl 1.${tplVersion} module \${1:module_name};\n\n` +
|
||||||
|
`metadata\n origin := '${origin}';\n author := '${author}';\n products := '\${2:products}';\n publishers := '${publisher}';\n tree_path := '${treePathBase}', '\${3:category}', '\${4:subcategory}';\nend metadata;\n\n` +
|
||||||
|
`pattern \${5:pattern_name} 1.0\n """\n TODO: explain\n """\n\n triggers\n on process := DiscoveredProcess where cmd matches regex_escape("\${6:process}");\n end triggers;\n\n body\n log.debug("Start pattern \${5:pattern_name}");\n $0\n end body;\nend pattern;`
|
||||||
|
);
|
||||||
|
skeletonItem.detail = "Generiert das komplette TPL Modul-Grundgerüst (mit Settings)";
|
||||||
|
items.push(skeletonItem);
|
||||||
|
|
||||||
|
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};`);
|
||||||
|
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);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
} else {
|
||||||
|
const ns = matchDot[1];
|
||||||
|
|
||||||
|
// NEU: BMC Built-Ins abfangen und Snippets bauen
|
||||||
|
if (tplBuiltIns[ns]) {
|
||||||
|
const bmcMethods: vscode.CompletionItem[] = [];
|
||||||
|
for (const [method, def] of Object.entries(tplBuiltIns[ns])) {
|
||||||
|
for (const snippet of def.snippets) {
|
||||||
|
const cleanSnippet = snippet.replace(/\$\{\d+:([^}]+)\}/g, '$1');
|
||||||
|
|
||||||
|
const item = new vscode.CompletionItem(`${method}(${cleanSnippet})`, vscode.CompletionItemKind.Method);
|
||||||
|
item.insertText = new vscode.SnippetString(`${method}(${snippet})`);
|
||||||
|
item.filterText = method;
|
||||||
|
item.documentation = new vscode.MarkdownString(def.description);
|
||||||
|
item.detail = `${ns}.${method}`;
|
||||||
|
|
||||||
|
bmcMethods.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bmcMethods;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleMethods: vscode.CompletionItem[] = [];
|
||||||
|
const extractDefineRegex = /^\s*define\s+.*?\b([a-zA-Z0-9_]+)\s*\(/gm;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export class TplDocumentFormatter implements vscode.DocumentFormattingEditProvider {
|
||||||
|
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[] = [];
|
||||||
|
|
||||||
|
let indentLevel = 0;
|
||||||
|
const indentString = ' ';
|
||||||
|
let inTripleDouble = false;
|
||||||
|
let inTripleSingle = false;
|
||||||
|
let consecutiveEmptyLines = 0;
|
||||||
|
|
||||||
|
const alignmentStack: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const rawLine = lines[i];
|
||||||
|
let line = rawLine.trim();
|
||||||
|
|
||||||
|
const tripleDoubleCount = (rawLine.match(/"""/g) || []).length;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
let newLineText = "";
|
||||||
|
if (alignmentStack.length > 0) {
|
||||||
|
let spaces = alignmentStack[alignmentStack.length - 1];
|
||||||
|
if (line.match(/^[)\]]/)) { spaces = Math.max(0, spaces - 1); }
|
||||||
|
newLineText = " ".repeat(spaces) + line;
|
||||||
|
} else {
|
||||||
|
newLineText = indentString.repeat(indentLevel) + line;
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedLines.push(newLineText);
|
||||||
|
|
||||||
|
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 ')) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { tplBuiltIns } from '../data/TplBuiltIns';
|
import { tplBuiltIns } from './TplBuiltIns';
|
||||||
|
|
||||||
export class TplHoverProvider implements vscode.HoverProvider {
|
export class TplHoverProvider implements vscode.HoverProvider {
|
||||||
public async provideHover(
|
public async provideHover(
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
|
|
||||||
export enum PatternPhase { None = 0, Metadata = 1, Overview = 2, Constants = 3, Triggers = 4, Mapping = 5, Removal = 6, Body = 7 }
|
|
||||||
|
|
||||||
export class LinterContext {
|
|
||||||
public diagnostics: vscode.Diagnostic[] = [];
|
|
||||||
public configuredTplVersion: number = 23;
|
|
||||||
public tplVersion: number = 10;
|
|
||||||
|
|
||||||
// Status-Tracking
|
|
||||||
public hasModuleDef = false;
|
|
||||||
public inBlock: 'none' | 'pattern' | 'table' | 'definitions' | 'enumeration' | 'configuration' = 'none';
|
|
||||||
public currentPatternPhase: PatternPhase = PatternPhase.None;
|
|
||||||
public inMultiLineComment = false;
|
|
||||||
public inConditionBlock = false;
|
|
||||||
|
|
||||||
// String-Tracking (Basis-Lexer)
|
|
||||||
public inTripleDouble = false;
|
|
||||||
public inTripleSingle = false;
|
|
||||||
public stringNeedsSemicolon = false;
|
|
||||||
public justOpenedString = false;
|
|
||||||
public isClosingString = false;
|
|
||||||
public isInternalStringLine = false;
|
|
||||||
|
|
||||||
// Block- & Metadaten-Tracking
|
|
||||||
public activePhaseName = "";
|
|
||||||
public activePhaseLine = -1;
|
|
||||||
public currentPatternName = "";
|
|
||||||
public inModuleMetadata = false;
|
|
||||||
public hasModuleMetadata = false;
|
|
||||||
public moduleMetadataBlockLine = -1;
|
|
||||||
public foundModuleMetadata = { origin: false, author: false, products: false, publishers: false, tree_path: false };
|
|
||||||
|
|
||||||
// Klammer-Tracking
|
|
||||||
public parenStack: number[] = [];
|
|
||||||
public bracketStack: number[] = [];
|
|
||||||
public openDefines: number[] = [];
|
|
||||||
|
|
||||||
public 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);
|
|
||||||
this.diagnostics.push(diag);
|
|
||||||
return diag;
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetLineStates() {
|
|
||||||
this.isInternalStringLine = false;
|
|
||||||
this.justOpenedString = false;
|
|
||||||
this.isClosingString = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { LinterContext } from './LinterContext';
|
|
||||||
import { LinterRule } from './rules/LinterRule';
|
|
||||||
import { SyntaxRule } from './rules/SyntaxRule';
|
|
||||||
import { BlockRule } from './rules/BlockRule';
|
|
||||||
|
|
||||||
export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
|
|
||||||
if (document.languageId !== 'tpl') { return; }
|
|
||||||
|
|
||||||
const ctx = new LinterContext();
|
|
||||||
const config = vscode.workspace.getConfiguration('tpl');
|
|
||||||
ctx.configuredTplVersion = config.get<number>('version', 23);
|
|
||||||
|
|
||||||
const text = document.getText();
|
|
||||||
const lines = text.split('\n');
|
|
||||||
|
|
||||||
// 1. Version prüfen
|
|
||||||
const versionMatch = text.match(/tpl\s+1\.(\d+)\s+module/);
|
|
||||||
if (versionMatch) {
|
|
||||||
ctx.tplVersion = parseInt(versionMatch[1], 10);
|
|
||||||
if (ctx.tplVersion < ctx.configuredTplVersion) {
|
|
||||||
const linesToSearch = text.substring(0, versionMatch.index! + 30).split('\n');
|
|
||||||
ctx.addDiag(linesToSearch.length - 1, 0, versionMatch[0].length, `Das Modul nutzt eine alte TPL Version (1.${ctx.tplVersion}). Aktuell in den Settings definiert ist 1.${ctx.configuredTplVersion}.`, vscode.DiagnosticSeverity.Warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Regeln laden
|
|
||||||
const rules: LinterRule[] = [
|
|
||||||
new SyntaxRule(),
|
|
||||||
new BlockRule()
|
|
||||||
];
|
|
||||||
|
|
||||||
// 3. Zeilenweise iterieren
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const rawLine = lines[i];
|
|
||||||
const line = rawLine.trim();
|
|
||||||
|
|
||||||
ctx.resetLineStates();
|
|
||||||
|
|
||||||
// Basis-Lexer: Strings & Kommentare tracken (läuft vor den Regeln!)
|
|
||||||
trackStrings(rawLine, line, ctx);
|
|
||||||
|
|
||||||
if (ctx.isInternalStringLine && (ctx.inTripleDouble || ctx.inTripleSingle)) { continue; }
|
|
||||||
|
|
||||||
if (line.startsWith('//') || line.startsWith('/*')) {
|
|
||||||
if (line.startsWith('/*')) { ctx.inMultiLineComment = true; }
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ctx.inMultiLineComment) {
|
|
||||||
if (line.includes('*/')) { ctx.inMultiLineComment = false; }
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.length === 0) { continue; }
|
|
||||||
|
|
||||||
// Regeln anwenden
|
|
||||||
for (const rule of rules) {
|
|
||||||
rule.checkLine(line, rawLine, i, ctx, lines); // Hier 'lines' ergänzen!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Abschluss-Prüfungen durchführen
|
|
||||||
for (const rule of rules) {
|
|
||||||
if (rule.checkEnd) {
|
|
||||||
rule.checkEnd(lines, ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Diagnostics an VS Code übergeben
|
|
||||||
collection.set(document.uri, ctx.diagnostics);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interne Hilfsfunktion für den Parser
|
|
||||||
function trackStrings(rawLine: string, line: string, ctx: LinterContext) {
|
|
||||||
const tripleDoubleCount = (rawLine.match(/"""/g) || []).length;
|
|
||||||
const tripleSingleCount = (rawLine.match(/'''/g) || []).length;
|
|
||||||
|
|
||||||
if (ctx.inTripleDouble) {
|
|
||||||
ctx.isInternalStringLine = true;
|
|
||||||
if (tripleDoubleCount % 2 !== 0) { ctx.inTripleDouble = false; ctx.isClosingString = true; }
|
|
||||||
} else if (tripleDoubleCount % 2 !== 0) {
|
|
||||||
ctx.inTripleDouble = true; ctx.justOpenedString = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.inTripleSingle) {
|
|
||||||
ctx.isInternalStringLine = true;
|
|
||||||
if (tripleSingleCount % 2 !== 0) { ctx.inTripleSingle = false; ctx.isClosingString = true; }
|
|
||||||
} else if (tripleSingleCount % 2 !== 0 && !ctx.justOpenedString) {
|
|
||||||
ctx.inTripleSingle = true; ctx.justOpenedString = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.justOpenedString) {
|
|
||||||
ctx.stringNeedsSemicolon = !line.startsWith('"""') && !line.startsWith("'''");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { LinterRule } from './LinterRule';
|
|
||||||
import { LinterContext, PatternPhase } from '../LinterContext';
|
|
||||||
|
|
||||||
export class BlockRule implements LinterRule {
|
|
||||||
|
|
||||||
// HIER GEFIXT: 'lines: string[]' wurde zur Signatur hinzugefügt!
|
|
||||||
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
|
||||||
if (!ctx.hasModuleDef) {
|
|
||||||
if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; }
|
|
||||||
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module <name>;' beginnen.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.startsWith('metadata') && ctx.inBlock === 'none') {
|
|
||||||
ctx.inModuleMetadata = true;
|
|
||||||
ctx.hasModuleMetadata = true;
|
|
||||||
ctx.moduleMetadataBlockLine = lineIdx;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.inModuleMetadata) {
|
|
||||||
this.trackModuleMetadata(line, lineIdx, ctx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) {
|
|
||||||
if (!ctx.hasModuleMetadata && ctx.inBlock === 'none') {
|
|
||||||
ctx.addDiag(lineIdx, 0, line.length, "Ein 'metadata' Block auf Modul-Ebene ist Pflicht, bevor andere Blöcke definiert werden.", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleBlockOpenings(line, rawLine, lineIdx, ctx);
|
|
||||||
|
|
||||||
if (ctx.inBlock === 'pattern') {
|
|
||||||
// HIER GEFIXT: 'lines' wird jetzt korrekt an die Methode weitergereicht
|
|
||||||
this.checkPatternPhases(line, rawLine, lineIdx, ctx, lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.checkEndBlocks(line, rawLine, lineIdx, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
public checkEnd(lines: string[], ctx: LinterContext): void {
|
|
||||||
for (const unclosedLine of ctx.openDefines) {
|
|
||||||
ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' wurde nicht mit 'end define;' geschlossen!", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private trackModuleMetadata(line: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
if (line.match(/^origin\b/)) { ctx.foundModuleMetadata.origin = true; }
|
|
||||||
if (line.match(/^author\b/)) { ctx.foundModuleMetadata.author = true; }
|
|
||||||
if (line.match(/^products\b/)) { ctx.foundModuleMetadata.products = true; }
|
|
||||||
if (line.match(/^publishers\b/)) { ctx.foundModuleMetadata.publishers = true; }
|
|
||||||
if (line.match(/^tree_path\b/)) { ctx.foundModuleMetadata.tree_path = true; }
|
|
||||||
|
|
||||||
if (line.startsWith('end metadata')) {
|
|
||||||
ctx.inModuleMetadata = false;
|
|
||||||
const missing = Object.keys(ctx.foundModuleMetadata).filter(k => !(ctx.foundModuleMetadata as any)[k]);
|
|
||||||
if (missing.length > 0 && ctx.moduleMetadataBlockLine !== -1) {
|
|
||||||
const diag = ctx.addDiag(ctx.moduleMetadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten (Modul-Ebene): ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
|
|
||||||
diag.code = 'missing_module_metadata';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleBlockOpenings(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
if (line.startsWith('definitions ') || line.startsWith('definition ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Ein Definitions-Block muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'definitions';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('define ')) { ctx.openDefines.push(lineIdx); }
|
|
||||||
else if (line.startsWith('enumeration ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Eine Enumeration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'enumeration';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('configuration ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Eine Konfiguration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'configuration';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('pattern ') || line.startsWith('syncmapping ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 7, "Ein Pattern/Syncmapping kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'pattern';
|
|
||||||
ctx.currentPatternPhase = PatternPhase.None;
|
|
||||||
ctx.activePhaseName = "";
|
|
||||||
ctx.activePhaseLine = -1;
|
|
||||||
|
|
||||||
const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/);
|
|
||||||
if (nameMatch) { ctx.currentPatternName = nameMatch[2]; }
|
|
||||||
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Ungültige Pattern-Deklaration. Erwartet: 'pattern <Name> <Version>'", vscode.DiagnosticSeverity.Error); }
|
|
||||||
}
|
|
||||||
else if (line.startsWith('table ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'table';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkPatternPhases(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]) {
|
|
||||||
const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => {
|
|
||||||
if (line.startsWith(phaseName)) {
|
|
||||||
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
|
|
||||||
if (ctx.currentPatternPhase >= phaseEnum) { ctx.addDiag(lineIdx, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt.`, vscode.DiagnosticSeverity.Error); }
|
|
||||||
|
|
||||||
ctx.currentPatternPhase = phaseEnum;
|
|
||||||
ctx.activePhaseName = phaseName;
|
|
||||||
ctx.activePhaseLine = lineIdx;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkPhase('metadata', PatternPhase.Metadata);
|
|
||||||
checkPhase('overview', PatternPhase.Overview);
|
|
||||||
checkPhase('constants', PatternPhase.Constants);
|
|
||||||
checkPhase('triggers', PatternPhase.Triggers);
|
|
||||||
checkPhase('mapping', PatternPhase.Mapping);
|
|
||||||
checkPhase('removal', PatternPhase.Removal);
|
|
||||||
|
|
||||||
if (line.startsWith('body')) {
|
|
||||||
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.activePhaseName = "body";
|
|
||||||
ctx.activePhaseLine = lineIdx;
|
|
||||||
|
|
||||||
// DER LOG-CHECK IST ZURÜCK!
|
|
||||||
let foundLog = false;
|
|
||||||
for (let j = lineIdx + 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(ctx.currentPatternName)) { foundLog = true; break; }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundLog) {
|
|
||||||
const expectedLog = `log.debug("Start pattern ${ctx.currentPatternName}");`;
|
|
||||||
const diag = ctx.addDiag(lineIdx, 0, rawLine.length, `Im Body fehlt das Log-Statement mit dem Pattern-Namen. Bsp: ${expectedLog}`, vscode.DiagnosticSeverity.Error);
|
|
||||||
diag.code = 'missing_log_start';
|
|
||||||
diag.source = expectedLog;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (ctx.activePhaseName !== phase) { ctx.addDiag(lineIdx, 0, line.length, `Unerwartetes '${closer}'. Erwartet wurde 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
|
||||||
else { ctx.activePhaseName = ""; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkEndBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
if (line.startsWith('end pattern') || line.startsWith('end syncmapping')) {
|
|
||||||
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
|
|
||||||
if (ctx.inBlock !== 'pattern') { ctx.addDiag(lineIdx, 0, 11, "'end pattern' gefunden, ohne offenen Pattern-Block.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'none';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) {
|
|
||||||
ctx.inBlock = 'none';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('end definitions') || line.startsWith('end definition')) {
|
|
||||||
if (ctx.inBlock !== 'definitions') { ctx.addDiag(lineIdx, 0, line.length, "'end definitions' gefunden, ohne offenen Definitions-Block.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'none';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('end define')) {
|
|
||||||
if (ctx.openDefines.length > 0) { ctx.openDefines.pop(); }
|
|
||||||
else { ctx.addDiag(lineIdx, 0, line.length, "'end define' gefunden, ohne offenes 'define'.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { LinterContext } from '../LinterContext';
|
|
||||||
|
|
||||||
export interface LinterRule {
|
|
||||||
checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void;
|
|
||||||
checkEnd?(lines: string[], ctx: LinterContext): void;
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { LinterRule } from './LinterRule';
|
|
||||||
import { LinterContext } from '../LinterContext';
|
|
||||||
|
|
||||||
export class SyntaxRule implements LinterRule {
|
|
||||||
|
|
||||||
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext): void {
|
|
||||||
this.checkTabs(rawLine, lineIdx, ctx);
|
|
||||||
this.checkBrackets(line, rawLine, lineIdx, ctx);
|
|
||||||
this.checkAssignments(rawLine, lineIdx, ctx);
|
|
||||||
this.checkSemicolons(line, rawLine, lineIdx, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
public checkEnd(lines: string[], ctx: LinterContext): void {
|
|
||||||
for (const unclosedLine of ctx.parenStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende Klammer '(' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); }
|
|
||||||
for (const unclosedLine of ctx.bracketStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende eckige Klammer '[' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkTabs(rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
const tabIndex = rawLine.indexOf('\t');
|
|
||||||
if (tabIndex !== -1) {
|
|
||||||
const diag = ctx.addDiag(lineIdx, tabIndex, tabIndex + 1, "Bitte Tabs durch Spaces ersetzen", vscode.DiagnosticSeverity.Warning);
|
|
||||||
diag.code = 'invalid_tab';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkBrackets(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '');
|
|
||||||
const commentIdx = cleanLine.indexOf('//');
|
|
||||||
if (commentIdx !== -1) { cleanLine = cleanLine.substring(0, commentIdx); }
|
|
||||||
|
|
||||||
for (let charIdx = 0; charIdx < cleanLine.length; charIdx++) {
|
|
||||||
const char = cleanLine[charIdx];
|
|
||||||
if (char === '(') { ctx.parenStack.push(lineIdx); }
|
|
||||||
else if (char === ')') {
|
|
||||||
if (ctx.parenStack.length > 0) { ctx.parenStack.pop(); }
|
|
||||||
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Unerwartete schließende Klammer ')'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
}
|
|
||||||
else if (char === '[') { ctx.bracketStack.push(lineIdx); }
|
|
||||||
else if (char === ']') {
|
|
||||||
if (ctx.bracketStack.length > 0) { ctx.bracketStack.pop(); }
|
|
||||||
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Unerwartete schließende eckige Klammer ']'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkAssignments(rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
const lineWithoutStrings = rawLine.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, '""');
|
|
||||||
|
|
||||||
if (/\b(if|elif|where)\b/.test(lineWithoutStrings)) { ctx.inConditionBlock = true; }
|
|
||||||
|
|
||||||
if (lineWithoutStrings.includes('=') && !lineWithoutStrings.includes(':=') && !lineWithoutStrings.includes('>=') && !lineWithoutStrings.includes('<=')) {
|
|
||||||
if (!ctx.inConditionBlock && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && !/\b(matches)\b/.test(lineWithoutStrings)) {
|
|
||||||
const idx = rawLine.indexOf('=');
|
|
||||||
if (idx !== -1) {
|
|
||||||
ctx.addDiag(lineIdx, idx, idx + 1, "Falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt.", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/\b(then|do)\b/.test(lineWithoutStrings) || lineWithoutStrings.endsWith(';')) { ctx.inConditionBlock = false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkSemicolons(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
if (ctx.isClosingString) {
|
|
||||||
if (ctx.stringNeedsSemicolon && !line.endsWith(';')) {
|
|
||||||
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (!noSemicolonNeeded.test(line) && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine) {
|
|
||||||
const lineToCheck = line.split('//')[0].trim();
|
|
||||||
if (!lineToCheck.endsWith(';') && !lineToCheck.endsWith('{') && !lineToCheck.endsWith(',')) {
|
|
||||||
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
306
src/extension.ts
306
src/extension.ts
|
|
@ -1,19 +1,20 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { validateTplDocument } from './core/TplLinter';
|
import { TplCodeActionProvider } from './TplCodeActionProvider';
|
||||||
import { TplCodeActionProvider } from './providers/TplCodeActionProvider';
|
import { TplDocumentFormatter } from './TplDocumentFormatter';
|
||||||
import { TplDocumentFormatter } from './providers/TplDocumentFormatter';
|
import { TplCompletionProvider } from './TplCompletionProvider';
|
||||||
import { TplCompletionProvider } from './providers/TplCompletionProvider';
|
import { TplHoverProvider } from './TplHoverProvider';
|
||||||
import { TplHoverProvider } from './providers/TplHoverProvider';
|
import { tplBuiltIns } from './TplBuiltIns';
|
||||||
|
|
||||||
|
enum PatternPhase { None = 0, Metadata = 1, Overview = 2, Constants = 3, Triggers = 4, Mapping = 5, Removal = 6, Body = 7 }
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner');
|
const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner');
|
||||||
context.subscriptions.push(diagnosticCollection);
|
context.subscriptions.push(diagnosticCollection);
|
||||||
|
|
||||||
// Linter-Events binden
|
|
||||||
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));
|
||||||
|
|
||||||
// Live-Reload für Einstellungen
|
// NEU: Sofort reagieren, wenn in den VS Code Einstellungen etwas geändert wird
|
||||||
vscode.workspace.onDidChangeConfiguration(e => {
|
vscode.workspace.onDidChangeConfiguration(e => {
|
||||||
if (e.affectsConfiguration('tpl')) {
|
if (e.affectsConfiguration('tpl')) {
|
||||||
for (const doc of vscode.workspace.textDocuments) {
|
for (const doc of vscode.workspace.textDocuments) {
|
||||||
|
|
@ -22,14 +23,301 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Provider registrieren
|
|
||||||
context.subscriptions.push(vscode.languages.registerCodeActionsProvider('tpl', new TplCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }));
|
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.registerDocumentFormattingEditProvider('tpl', new TplDocumentFormatter()));
|
||||||
context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.'));
|
context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.'));
|
||||||
context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider()));
|
context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider()));
|
||||||
|
|
||||||
// Bei Aktivierung offene Dokumente einmal scannen
|
|
||||||
for (const doc of vscode.workspace.textDocuments) {
|
for (const doc of vscode.workspace.textDocuments) {
|
||||||
validateTplDocument(doc, diagnosticCollection);
|
validateTplDocument(doc, diagnosticCollection);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
|
||||||
|
if (document.languageId !== 'tpl') { return; }
|
||||||
|
|
||||||
|
const config = vscode.workspace.getConfiguration('tpl');
|
||||||
|
const configuredTplVersion = config.get<number>('version', 23);
|
||||||
|
|
||||||
|
const diagnostics: vscode.Diagnostic[] = [];
|
||||||
|
const text = document.getText();
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
let tplVersion = 10;
|
||||||
|
let hasModuleDef = false;
|
||||||
|
|
||||||
|
let inBlock: 'none' | 'pattern' | 'table' | 'definitions' | 'enumeration' | 'configuration' = 'none';
|
||||||
|
let currentPatternPhase: PatternPhase = PatternPhase.None;
|
||||||
|
let inMultiLineComment = false;
|
||||||
|
|
||||||
|
let inTripleDouble = false;
|
||||||
|
let inTripleSingle = false;
|
||||||
|
let stringNeedsSemicolon = false;
|
||||||
|
|
||||||
|
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[] = [];
|
||||||
|
|
||||||
|
// State-Tracking für mehrzeilige Vergleiche / Zuweisungen
|
||||||
|
let inConditionBlock = false;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const rawLine = lines[i];
|
||||||
|
const line = rawLine.trim();
|
||||||
|
|
||||||
|
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');
|
||||||
|
if (tabIndex !== -1) {
|
||||||
|
const diag = addDiag(i, tabIndex, tabIndex + 1, "Bitte Tabs durch Spaces ersetzen", vscode.DiagnosticSeverity.Warning);
|
||||||
|
diag.code = 'invalid_tab';
|
||||||
|
}
|
||||||
|
|
||||||
|
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); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('//') || line.startsWith('/*')) { if (line.startsWith('/*')) {inMultiLineComment = true;} continue; }
|
||||||
|
if (inMultiLineComment) { if (line.includes('*/')) {inMultiLineComment = false;} continue; }
|
||||||
|
if (line.length === 0) { continue; }
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (!noSemicolonNeeded.test(line) && !justOpenedString && parenStack.length === 0 && bracketStack.length === 0 && inBlock !== 'enumeration' && !inConditionBlock && !continuesOnNextLine) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasModuleDef) {
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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); }
|
||||||
|
inBlock = 'pattern';
|
||||||
|
currentPatternPhase = PatternPhase.None;
|
||||||
|
activePhaseName = "";
|
||||||
|
activePhaseLine = -1;
|
||||||
|
|
||||||
|
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); }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('table ')) {
|
||||||
|
if (inBlock !== 'none') { addDiag(i, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
||||||
|
inBlock = 'table'; continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inBlock === 'pattern') {
|
||||||
|
const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => {
|
||||||
|
if (line.startsWith(phaseName)) {
|
||||||
|
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); }
|
||||||
|
|
||||||
|
currentPatternPhase = phaseEnum;
|
||||||
|
activePhaseName = phaseName;
|
||||||
|
activePhaseLine = i;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkPhase('metadata', PatternPhase.Metadata);
|
||||||
|
checkPhase('overview', PatternPhase.Overview);
|
||||||
|
checkPhase('constants', PatternPhase.Constants);
|
||||||
|
checkPhase('triggers', PatternPhase.Triggers);
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = ""; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { TplQuickFix } from './actions/TplQuickFix';
|
|
||||||
import { TabFix } from './actions/TabFix';
|
|
||||||
import { LogFix } from './actions/LogFix';
|
|
||||||
import { MetadataFix } from './actions/MetadataFix';
|
|
||||||
|
|
||||||
export class TplCodeActionProvider implements vscode.CodeActionProvider {
|
|
||||||
private fixes: Map<string, TplQuickFix> = new Map();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
// Hier werden alle Quick Fixes registriert
|
|
||||||
const availableFixes: TplQuickFix[] = [
|
|
||||||
new TabFix(),
|
|
||||||
new LogFix(),
|
|
||||||
new MetadataFix()
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const fix of availableFixes) {
|
|
||||||
this.fixes.set(fix.diagnosticCode, fix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public provideCodeActions(
|
|
||||||
document: vscode.TextDocument,
|
|
||||||
range: vscode.Range | vscode.Selection,
|
|
||||||
context: vscode.CodeActionContext,
|
|
||||||
token: vscode.CancellationToken
|
|
||||||
): vscode.CodeAction[] {
|
|
||||||
|
|
||||||
const actions: vscode.CodeAction[] = [];
|
|
||||||
|
|
||||||
// Prüfen, ob für die aktuellen Fehler (Diagnostics) ein Fix existiert
|
|
||||||
for (const diagnostic of context.diagnostics) {
|
|
||||||
const code = diagnostic.code;
|
|
||||||
if (typeof code === 'string' && this.fixes.has(code)) {
|
|
||||||
const fixLogic = this.fixes.get(code)!;
|
|
||||||
actions.push(...fixLogic.provideFix(document, diagnostic));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { CompletionStrategy } from './completions/CompletionStrategy';
|
|
||||||
import { SnippetCompletion } from './completions/SnippetCompletion';
|
|
||||||
import { BuiltInCompletion } from './completions/BuiltInCompletion';
|
|
||||||
import { LocalContextCompletion } from './completions/LocalContextCompletion';
|
|
||||||
|
|
||||||
export class TplCompletionProvider implements vscode.CompletionItemProvider {
|
|
||||||
private strategies: CompletionStrategy[];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
// Hier registrieren wir alle Completion Engines
|
|
||||||
this.strategies = [
|
|
||||||
new BuiltInCompletion(),
|
|
||||||
new SnippetCompletion(),
|
|
||||||
new LocalContextCompletion()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public provideCompletionItems(
|
|
||||||
document: vscode.TextDocument,
|
|
||||||
position: vscode.Position,
|
|
||||||
token: vscode.CancellationToken,
|
|
||||||
context: vscode.CompletionContext
|
|
||||||
): vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList> {
|
|
||||||
|
|
||||||
// Den Text in der aktuellen Zeile bis zum Cursor holen
|
|
||||||
const linePrefix = document.lineAt(position).text.substring(0, position.character);
|
|
||||||
const results: vscode.CompletionItem[] = [];
|
|
||||||
|
|
||||||
// Jede Engine um Vorschläge bitten
|
|
||||||
for (const strategy of this.strategies) {
|
|
||||||
const items = strategy.provideCompletions(document, position, linePrefix);
|
|
||||||
if (items && items.length > 0) {
|
|
||||||
results.push(...items);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { FormatState } from './formatters/FormatState';
|
|
||||||
import { IndentationRules } from './formatters/IndentationRules';
|
|
||||||
import { AlignmentRules } from './formatters/AlignmentRules';
|
|
||||||
|
|
||||||
export class TplDocumentFormatter implements vscode.DocumentFormattingEditProvider {
|
|
||||||
private indentRules = new IndentationRules();
|
|
||||||
private alignRules = new AlignmentRules();
|
|
||||||
|
|
||||||
public provideDocumentFormattingEdits(
|
|
||||||
document: vscode.TextDocument,
|
|
||||||
options: vscode.FormattingOptions,
|
|
||||||
token: vscode.CancellationToken
|
|
||||||
): vscode.ProviderResult<vscode.TextEdit[]> {
|
|
||||||
|
|
||||||
const edits: vscode.TextEdit[] = [];
|
|
||||||
const state = new FormatState();
|
|
||||||
|
|
||||||
// Nutze die VS Code Settings (meist 4 Spaces), andernfalls Fallback
|
|
||||||
state.tabSize = options.tabSize || 4;
|
|
||||||
|
|
||||||
for (let i = 0; i < document.lineCount; i++) {
|
|
||||||
const originalLine = document.lineAt(i);
|
|
||||||
const rawText = originalLine.text;
|
|
||||||
const trimmedText = rawText.trim();
|
|
||||||
|
|
||||||
// Leere Zeilen lassen wir leer
|
|
||||||
if (trimmedText.length === 0) {
|
|
||||||
if (rawText.length > 0) {
|
|
||||||
// Löscht unnötige Whitespaces in eigentlich leeren Zeilen
|
|
||||||
edits.push(vscode.TextEdit.replace(originalLine.range, ''));
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Basis-Einrückung (if/else/pattern) berechnen
|
|
||||||
let formattedText = this.indentRules.apply(trimmedText, state);
|
|
||||||
|
|
||||||
// 2. Visual Alignment (Klammern) anwenden & Stack updaten
|
|
||||||
formattedText = this.alignRules.apply(trimmedText, formattedText, state);
|
|
||||||
|
|
||||||
// 3. Edit nur erstellen, wenn sich die Zeile wirklich ändert (spart Performance & Cursor-Sprünge)
|
|
||||||
if (rawText !== formattedText) {
|
|
||||||
edits.push(vscode.TextEdit.replace(originalLine.range, formattedText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return edits;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { TplQuickFix } from './TplQuickFix';
|
|
||||||
|
|
||||||
export class LogFix implements TplQuickFix {
|
|
||||||
public readonly diagnosticCode = 'missing_log_start';
|
|
||||||
|
|
||||||
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
|
||||||
const fix = new vscode.CodeAction("Fehlendes Log-Statement einfügen", vscode.CodeActionKind.QuickFix);
|
|
||||||
fix.edit = new vscode.WorkspaceEdit();
|
|
||||||
|
|
||||||
// Der Linter hat uns den exakten String in die 'source' gepackt
|
|
||||||
const expectedLog = diagnostic.source || 'log.debug("Start pattern");';
|
|
||||||
|
|
||||||
// Füge es eine Zeile unter dem 'body' Keyword ein, sauber eingerückt
|
|
||||||
const insertText = ` ${expectedLog}\n`;
|
|
||||||
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText);
|
|
||||||
|
|
||||||
fix.diagnostics = [diagnostic];
|
|
||||||
fix.isPreferred = true;
|
|
||||||
|
|
||||||
return [fix];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { TplQuickFix } from './TplQuickFix';
|
|
||||||
|
|
||||||
export class MetadataFix implements TplQuickFix {
|
|
||||||
public readonly diagnosticCode = 'missing_module_metadata';
|
|
||||||
|
|
||||||
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
|
||||||
const fix = new vscode.CodeAction("Fehlende Metadaten generieren", vscode.CodeActionKind.QuickFix);
|
|
||||||
fix.edit = new vscode.WorkspaceEdit();
|
|
||||||
|
|
||||||
// Einstellungen laden
|
|
||||||
const config = vscode.workspace.getConfiguration('tpl');
|
|
||||||
const author = config.get<string>('metadata.author', 'Kuro');
|
|
||||||
const origin = config.get<string>('metadata.origin', 'Custom');
|
|
||||||
const publisher = config.get<string>('metadata.publisher', 'IT-Operations');
|
|
||||||
const treePathBase = config.get<string>('metadata.treePathBase', 'Grafing');
|
|
||||||
|
|
||||||
// Herausfinden, welche Felder fehlen (steht im Diagnostic-Message-String)
|
|
||||||
const missingFieldsStr = diagnostic.message.replace('Fehlende Pflicht-Metadaten (Modul-Ebene): ', '');
|
|
||||||
const missingFields = missingFieldsStr.split(', ');
|
|
||||||
|
|
||||||
let insertText = '';
|
|
||||||
if (missingFields.includes('origin')) insertText += ` origin := '${origin}';\n`;
|
|
||||||
if (missingFields.includes('author')) insertText += ` author := '${author}';\n`;
|
|
||||||
if (missingFields.includes('products')) insertText += ` products := 'TODO_Product';\n`;
|
|
||||||
if (missingFields.includes('publishers')) insertText += ` publishers := '${publisher}';\n`;
|
|
||||||
if (missingFields.includes('tree_path')) insertText += ` tree_path := '${treePathBase}', 'TODO_Category', 'TODO_Subcategory';\n`;
|
|
||||||
|
|
||||||
// Direkt über der Zeile 'end metadata;' einfügen
|
|
||||||
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line, 0), insertText);
|
|
||||||
fix.diagnostics = [diagnostic];
|
|
||||||
|
|
||||||
return [fix];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { TplQuickFix } from './TplQuickFix';
|
|
||||||
|
|
||||||
export class TabFix implements TplQuickFix {
|
|
||||||
public readonly diagnosticCode = 'invalid_tab';
|
|
||||||
|
|
||||||
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
|
||||||
const fix = new vscode.CodeAction("Tabs in 4 Spaces umwandeln", vscode.CodeActionKind.QuickFix);
|
|
||||||
fix.edit = new vscode.WorkspaceEdit();
|
|
||||||
|
|
||||||
const line = document.lineAt(diagnostic.range.start.line);
|
|
||||||
const newText = line.text.replace(/\t/g, ' ');
|
|
||||||
|
|
||||||
fix.edit.replace(document.uri, line.range, newText);
|
|
||||||
fix.diagnostics = [diagnostic];
|
|
||||||
fix.isPreferred = true; // Setzt dies als Standard-Vorschlag
|
|
||||||
|
|
||||||
return [fix];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
|
|
||||||
export interface TplQuickFix {
|
|
||||||
// Der Fehlercode aus dem Linter (z.B. 'invalid_tab')
|
|
||||||
readonly diagnosticCode: string;
|
|
||||||
|
|
||||||
// Die Methode, die den Fix generiert
|
|
||||||
provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[];
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { CompletionStrategy } from './CompletionStrategy';
|
|
||||||
|
|
||||||
export class BuiltInCompletion implements CompletionStrategy {
|
|
||||||
|
|
||||||
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
|
|
||||||
const items: vscode.CompletionItem[] = [];
|
|
||||||
|
|
||||||
// Prüft, ob wir im Namespace "log." oder "discovery." sind (auch wenn man danach weiter tippt!)
|
|
||||||
const match = linePrefix.match(/\b(log|discovery)\.([a-zA-Z0-9_]*)$/);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const namespace = match[1];
|
|
||||||
|
|
||||||
if (namespace === 'log') {
|
|
||||||
const levels = ['debug', 'info', 'warn', 'error'];
|
|
||||||
for (const level of levels) {
|
|
||||||
const item = new vscode.CompletionItem(level, vscode.CompletionItemKind.Method);
|
|
||||||
item.insertText = new vscode.SnippetString(`${level}("\${1:message}");`);
|
|
||||||
item.documentation = new vscode.MarkdownString(`Schreibt eine Log-Meldung auf dem Level **${level}**.`);
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (namespace === 'discovery') {
|
|
||||||
const runCmd = new vscode.CompletionItem('runCommand', vscode.CompletionItemKind.Method);
|
|
||||||
runCmd.insertText = new vscode.SnippetString('runCommand(${1:host}, "${2:command}");');
|
|
||||||
runCmd.detail = "discovery.runCommand(host, command)";
|
|
||||||
items.push(runCmd);
|
|
||||||
}
|
|
||||||
return items; // Treffer gefunden, wir geben sie zurück!
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
|
|
||||||
export interface CompletionStrategy {
|
|
||||||
/**
|
|
||||||
* @param document Das aktuelle Dokument
|
|
||||||
* @param position Die Cursor-Position
|
|
||||||
* @param linePrefix Der Text der aktuellen Zeile *vor* dem Cursor
|
|
||||||
*/
|
|
||||||
provideCompletions(
|
|
||||||
document: vscode.TextDocument,
|
|
||||||
position: vscode.Position,
|
|
||||||
linePrefix: string
|
|
||||||
): vscode.CompletionItem[] | undefined;
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { CompletionStrategy } from './CompletionStrategy';
|
|
||||||
|
|
||||||
export class LocalContextCompletion implements CompletionStrategy {
|
|
||||||
|
|
||||||
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
|
|
||||||
// TODO für Phase 2:
|
|
||||||
// 1. Regex Suche im Dokument nach ":=" (Variablen tracken)
|
|
||||||
// 2. Projektweite Suche nach "define" Methoden für Autocomplete
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { CompletionStrategy } from './CompletionStrategy';
|
|
||||||
|
|
||||||
export class SnippetCompletion implements CompletionStrategy {
|
|
||||||
|
|
||||||
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
|
|
||||||
// Wenn wir gerade NACH einem Punkt tippen (z.B. "log."), sind Snippets/Base-Module hier falsch!
|
|
||||||
if (linePrefix.match(/\.[a-zA-Z0-9_]*$/)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items: vscode.CompletionItem[] = [];
|
|
||||||
const config = vscode.workspace.getConfiguration('tpl');
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 1. BASIS-NAMESPACES (discovery, log, etc.)
|
|
||||||
// ==========================================
|
|
||||||
const namespaces = ['discovery', 'log', 'model', 'search', 'text', 'regex', 'time', 'hash', 'number', 'inference'];
|
|
||||||
for (const ns of namespaces) {
|
|
||||||
const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module);
|
|
||||||
item.detail = `TPL Namespace: ${ns}`;
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 2. DAS GOD-SNIPPET (Skeleton)
|
|
||||||
// ==========================================
|
|
||||||
const skel = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
|
|
||||||
const version = config.get<number>('version', 23);
|
|
||||||
const author = config.get<string>('metadata.author', 'Kuro');
|
|
||||||
|
|
||||||
skel.insertText = new vscode.SnippetString(
|
|
||||||
`tpl 1.${version} module \${1:Name};\n\n` +
|
|
||||||
`metadata\n` +
|
|
||||||
` origin := '\${2:Custom}';\n` +
|
|
||||||
` author := '${author}';\n` +
|
|
||||||
` tree_path := 'Grafing', '\${3:Category}', '\${4:SubCategory}';\n` +
|
|
||||||
`end metadata;\n\n` +
|
|
||||||
`pattern \${5:PatternName} 1.0\n` +
|
|
||||||
` """\n \${6:Beschreibung}\n """\n` +
|
|
||||||
` overview\n tags \${7:tag1};\n end overview;\n\n` +
|
|
||||||
` triggers\n on host := Host created, confirmed;\n end triggers;\n\n` +
|
|
||||||
` body\n log.debug("Start pattern \${5:PatternName}");\n $0\n end body;\n` +
|
|
||||||
`end pattern;`
|
|
||||||
);
|
|
||||||
skel.detail = "Generiert ein komplettes TPL Grundgerüst";
|
|
||||||
items.push(skel);
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 3. TPL HAUPT-BLÖCKE (pattern, define, etc.)
|
|
||||||
// ==========================================
|
|
||||||
const mainBlocks = [
|
|
||||||
{ label: 'pattern', desc: 'pattern ... end pattern;' },
|
|
||||||
{ label: 'syncmapping', desc: 'syncmapping ... end syncmapping;' },
|
|
||||||
{ label: 'define', desc: 'define ... end define;' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const block of mainBlocks) {
|
|
||||||
const item = new vscode.CompletionItem(block.label, vscode.CompletionItemKind.Snippet);
|
|
||||||
item.insertText = new vscode.SnippetString(`${block.label} \${1:Name} \${2:1.0}\n """\n \${3:DocString}\n """\n $0\nend ${block.label};`);
|
|
||||||
if (block.label === 'define') {
|
|
||||||
item.insertText = new vscode.SnippetString(`define \${1:methodName}(\${2:params})\n """\n \${3:DocString}\n """\n $0\nend define;`);
|
|
||||||
}
|
|
||||||
item.detail = block.desc;
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 4. SECTIONS (metadata, body, triggers, etc.)
|
|
||||||
// ==========================================
|
|
||||||
const sections = ['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body'];
|
|
||||||
for (const sec of sections) {
|
|
||||||
const item = new vscode.CompletionItem(sec, vscode.CompletionItemKind.Snippet);
|
|
||||||
item.insertText = new vscode.SnippetString(`${sec}\n $0\nend ${sec};`);
|
|
||||||
item.detail = `${sec} ... end ${sec};`;
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 5. KONTROLLSTRUKTUREN & TABELLEN
|
|
||||||
// ==========================================
|
|
||||||
const ifSnippet = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet);
|
|
||||||
ifSnippet.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;');
|
|
||||||
ifSnippet.detail = "if ... then ... end if;";
|
|
||||||
items.push(ifSnippet);
|
|
||||||
|
|
||||||
const forSnippet = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet);
|
|
||||||
forSnippet.insertText = new vscode.SnippetString('for ${1:item} in${2:list} do\n $0\nend for;');
|
|
||||||
forSnippet.detail = "for ... in ... do ... end for;";
|
|
||||||
items.push(forSnippet);
|
|
||||||
|
|
||||||
const tableSnippet = new vscode.CompletionItem('table', vscode.CompletionItemKind.Snippet);
|
|
||||||
tableSnippet.insertText = new vscode.SnippetString('table ${1:TableName} 1.0\n ${2:"key"} -> "${3:value}";\n default -> "unknown";\nend table;');
|
|
||||||
tableSnippet.detail = "table ... end table;";
|
|
||||||
items.push(tableSnippet);
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import { FormatState } from './FormatState';
|
|
||||||
|
|
||||||
export class AlignmentRules {
|
|
||||||
|
|
||||||
public apply(line: string, formattedLine: string, state: FormatState): string {
|
|
||||||
let finalLine = formattedLine;
|
|
||||||
|
|
||||||
// Wenn wir innerhalb einer offenen Klammer sind, überschreibe die Block-Einrückung
|
|
||||||
if (state.parenAlignments.length > 0) {
|
|
||||||
const alignCol = state.parenAlignments[state.parenAlignments.length - 1];
|
|
||||||
finalLine = ' '.repeat(alignCol) + line;
|
|
||||||
} else if (state.bracketAlignments.length > 0) {
|
|
||||||
const alignCol = state.bracketAlignments[state.bracketAlignments.length - 1];
|
|
||||||
finalLine = ' '.repeat(alignCol) + line;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualisiere Stacks basierend auf der FINAL formatierten Zeile (wichtig für die Spaltenzahl!)
|
|
||||||
this.updateAlignments(finalLine, state);
|
|
||||||
|
|
||||||
return finalLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateAlignments(line: string, state: FormatState) {
|
|
||||||
// Strings ignorieren, damit wir keine Klammern in Log-Texten zählen
|
|
||||||
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""');
|
|
||||||
|
|
||||||
for (let i = 0; i < cleanLine.length; i++) {
|
|
||||||
const char = cleanLine[i];
|
|
||||||
if (char === '(') {
|
|
||||||
// Einrückung ab dem Zeichen NACH der offenen Klammer
|
|
||||||
state.parenAlignments.push(i + 1);
|
|
||||||
} else if (char === ')') {
|
|
||||||
state.parenAlignments.pop();
|
|
||||||
} else if (char === '[') {
|
|
||||||
state.bracketAlignments.push(i + 1);
|
|
||||||
} else if (char === ']') {
|
|
||||||
state.bracketAlignments.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
export class FormatState {
|
|
||||||
public indentLevel: number = 0;
|
|
||||||
public tabSize: number = 4;
|
|
||||||
|
|
||||||
// Speichert die Spalten-Position offener Klammern für Visual Alignment
|
|
||||||
public parenAlignments: number[] = [];
|
|
||||||
public bracketAlignments: number[] = [];
|
|
||||||
|
|
||||||
// Gibt die aktuellen Basis-Leerzeichen zurück (z.B. Level 2 = 8 Leerzeichen)
|
|
||||||
public get currentIndentString(): string {
|
|
||||||
return ' '.repeat(this.indentLevel * this.tabSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { FormatState } from './FormatState';
|
|
||||||
|
|
||||||
export class IndentationRules {
|
|
||||||
// Keywords, die eine neue Einrückungsebene öffnen (ohne static!)
|
|
||||||
private readonly blockOpens = /^(pattern|syncmapping|table|definitions?|enumeration|configuration|define|metadata|overview|constants|triggers|mapping|removal|body|if\s+|for\s+|while\s+)\b/;
|
|
||||||
|
|
||||||
// Keywords, die eine Ebene schließen (ohne static!)
|
|
||||||
private readonly blockCloses = /^(end\s+|else\b|elif\b)/;
|
|
||||||
|
|
||||||
public apply(line: string, state: FormatState): string {
|
|
||||||
// 1. Wenn die Zeile einen Block schließt, VORHER die Einrückung verringern
|
|
||||||
if (this.blockCloses.test(line) && state.indentLevel > 0) {
|
|
||||||
state.indentLevel--;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktuelle Einrückung für diese Zeile abholen
|
|
||||||
const currentIndent = state.currentIndentString;
|
|
||||||
|
|
||||||
// 2. NACH der Zeile: Wenn sie einen Block öffnet, Level für die nächste Zeile erhöhen
|
|
||||||
if (/^(else\b|elif\b)/.test(line)) {
|
|
||||||
// else / elif springen nach dem Schließen direkt wieder auf
|
|
||||||
state.indentLevel++;
|
|
||||||
}
|
|
||||||
else if (this.blockOpens.test(line)) {
|
|
||||||
state.indentLevel++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentIndent + line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import * as assert from 'assert';
|
||||||
|
|
||||||
|
// You can import and use all API from the 'vscode' module
|
||||||
|
// as well as import your extension to test it
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
// import * as myExtension from '../../extension';
|
||||||
|
|
||||||
|
suite('Extension Test Suite', () => {
|
||||||
|
vscode.window.showInformationMessage('Start all tests.');
|
||||||
|
|
||||||
|
test('Sample test', () => {
|
||||||
|
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
|
||||||
|
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import * as assert from 'assert';
|
|
||||||
import { FormatState } from '../../providers/formatters/FormatState';
|
|
||||||
import { IndentationRules } from '../../providers/formatters/IndentationRules';
|
|
||||||
import { AlignmentRules } from '../../providers/formatters/AlignmentRules';
|
|
||||||
|
|
||||||
suite('Formatter Test Suite', () => {
|
|
||||||
test('IndentationRules: if/then/end Block korrekt einrücken', () => {
|
|
||||||
const indentRules = new IndentationRules();
|
|
||||||
const state = new FormatState();
|
|
||||||
state.tabSize = 4;
|
|
||||||
|
|
||||||
// Zeile 1: if condition then (erwartet: 0 Leerzeichen, aber Level springt auf 1 für die nächste Zeile)
|
|
||||||
let result = indentRules.apply('if x = 1 then', state);
|
|
||||||
assert.strictEqual(result, 'if x = 1 then');
|
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
|
||||||
|
|
||||||
// Zeile 2: normaler Code im Block (erwartet: 4 Leerzeichen)
|
|
||||||
result = indentRules.apply('log.debug("test");', state);
|
|
||||||
assert.strictEqual(result, ' log.debug("test");');
|
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
|
||||||
|
|
||||||
// Zeile 3: end if; (erwartet: 0 Leerzeichen, Level springt sofort vor der Ausgabe auf 0)
|
|
||||||
result = indentRules.apply('end if;', state);
|
|
||||||
assert.strictEqual(result, 'end if;');
|
|
||||||
assert.strictEqual(state.indentLevel, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('IndentationRules: elif und else verhalten sich korrekt', () => {
|
|
||||||
const indentRules = new IndentationRules();
|
|
||||||
const state = new FormatState();
|
|
||||||
state.tabSize = 4;
|
|
||||||
state.indentLevel = 1; // Wir starten quasi IN einem offenen if-Block
|
|
||||||
|
|
||||||
// elif muss zurückspringen (auf 0) und danach wieder auf 1 für den folgenden Block
|
|
||||||
let result = indentRules.apply('elif x = 2 then', state);
|
|
||||||
assert.strictEqual(result, 'elif x = 2 then');
|
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('AlignmentRules: Visual Alignment an runden Klammern', () => {
|
|
||||||
const alignRules = new AlignmentRules();
|
|
||||||
const state = new FormatState();
|
|
||||||
|
|
||||||
// 1. Zeile mit öffnender Klammer
|
|
||||||
alignRules.apply('discovery.runCommand(', 'discovery.runCommand(', state);
|
|
||||||
assert.strictEqual(state.parenAlignments.length, 1);
|
|
||||||
assert.strictEqual(state.parenAlignments[0], 21); // 21 Zeichen bis nach der offenen Klammer
|
|
||||||
|
|
||||||
// 2. Zeile (Parameter soll exakt bis Spalte 21 eingerückt werden)
|
|
||||||
const result = alignRules.apply('host,', 'host,', state);
|
|
||||||
assert.strictEqual(result, ' host,');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import * as assert from 'assert';
|
|
||||||
import { LinterContext } from '../../core/LinterContext';
|
|
||||||
import { SyntaxRule } from '../../core/rules/SyntaxRule';
|
|
||||||
|
|
||||||
suite('Linter SyntaxRule Test Suite', () => {
|
|
||||||
test('Zuweisung: Warnt bei "=" statt ":="', () => {
|
|
||||||
const rule = new SyntaxRule();
|
|
||||||
const ctx = new LinterContext();
|
|
||||||
|
|
||||||
// Wir simulieren eine Code-Zeile
|
|
||||||
const rawLine = " my_var = 123;";
|
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
|
|
||||||
|
|
||||||
// Erwartung: Ein Fehler wegen falscher Zuweisung
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 1);
|
|
||||||
assert.ok(ctx.diagnostics[0].message.includes("In TPL wird ':=' für Zuweisungen genutzt"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Zuweisung: Akzeptiert "=" als Vergleich in if-Bedingung', () => {
|
|
||||||
const rule = new SyntaxRule();
|
|
||||||
const ctx = new LinterContext();
|
|
||||||
|
|
||||||
// Wir simulieren eine if-Zeile (setzt ctx.inConditionBlock intern auf true)
|
|
||||||
const rawLine = " if my_var = 123 then";
|
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
|
|
||||||
|
|
||||||
// Erwartung: Kein Fehler, da das = als Vergleich legitim ist
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Klammern: Findet ungeschlossene runde Klammern', () => {
|
|
||||||
const rule = new SyntaxRule();
|
|
||||||
const ctx = new LinterContext();
|
|
||||||
|
|
||||||
// Wir öffnen eine Klammer, schließen sie aber nicht
|
|
||||||
const rawLine = " my_func(parameter;";
|
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
|
|
||||||
|
|
||||||
// checkEnd wird am Ende der Datei aufgerufen
|
|
||||||
rule.checkEnd(['', rawLine], ctx);
|
|
||||||
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 1);
|
|
||||||
assert.ok(ctx.diagnostics[0].message.includes("Öffnende Klammer '(' wird nicht geschlossen!"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue