Compare commits
No commits in common. "master" and "beta_3" have entirely different histories.
|
|
@ -1,46 +0,0 @@
|
||||||
name: Nightly Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Läuft jeden Tag um 02:00 Uhr nachts
|
|
||||||
- cron: '0 2 * * *'
|
|
||||||
# Erlaubt es dir, den Build im Gitea-Webinterface auch manuell per Button zu starten
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-nightly:
|
|
||||||
runs-on: ubuntu-latest # Pass das an das Label deines Gitea-Runners an
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '24' # Passend zu deiner package.json Umgebung
|
|
||||||
|
|
||||||
- name: Install System Dependencies for Headless Tests
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y xvfb libx11-xcb1 libxcomposite1 libxcursor1 libxdamage1 libxi6 libxtst6 libnss3 libcups2 libxss1 libxrandr2 libasound2 libatk1.0-0 libatk-bridge2.0-0 libgtk-3-0
|
|
||||||
|
|
||||||
- name: Install NPM Packages
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Inject Nightly Version
|
|
||||||
run: |
|
|
||||||
NIGHTLY_DATE=$(date +%Y%m%d)
|
|
||||||
sed -i "s/\"version\": \"\(.*\)\"/\"version\": \"\1-nightly-$NIGHTLY_DATE\"/" package.json
|
|
||||||
echo "Building Nightly Version for $NIGHTLY_DATE..."
|
|
||||||
|
|
||||||
- name: Package & Test (with xvfb)
|
|
||||||
# xvfb-run fängt die GUI-Aufrufe der VS Code Test-Engine ab
|
|
||||||
run: xvfb-run -a npm run package
|
|
||||||
|
|
||||||
- name: Upload VSIX Artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: bmc-tpl-code-all-around-nightly
|
|
||||||
path: '*.vsix'
|
|
||||||
retention-days: 14
|
|
||||||
203
README.md
203
README.md
|
|
@ -1,199 +1,78 @@
|
||||||
English Description first - after that comes the German Version.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# BMC TPL Code All-Around
|
# BMC TPL Code All-Around
|
||||||
|
|
||||||
**Linter, Auto-Formatter, IntelliSense, and Hover Documentation for the BMC TPL (Topology Pattern Language).**
|
**Linter, Auto-Formatter, IntelliSense und Hover-Dokumentationen für die BMC TPL (Template Language).**[cite: 5]
|
||||||
|
|
||||||
This Visual Studio Code extension transforms your editor into a fully-fledged development environment for BMC Discovery TPL scripts. It saves you typing, formats code according to strict best practices, prevents syntax errors before deployment, and understands cross-project dependencies.
|
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
|
|
||||||
|
|
||||||
### 🤖 Intelligent Linter (Syntax Scanner)
|
|
||||||
The linter analyzes the code in real-time, remembers states across line breaks, and warns you about typical TPL errors:
|
|
||||||
* **Strict Hierarchy Checking:** Monitors the correct order of blocks within patterns and syncmappings (`metadata` -> `overview` -> `constants` -> `triggers` -> `mapping`/`removal` -> `body`).
|
|
||||||
* **Smart Assignment Check:** Finds missing semicolons (but tolerates them in `enumeration` blocks or multi-line statements). Warns strictly with an error if `=` is used instead of `:=` for variable assignments (outside of legitimate `if`/`where` comparisons).
|
|
||||||
* **Block & Bracket Tracking:** Detects unclosed blocks (like missing `end body;` or `end pattern;`) and finds unclosed parentheses `(` or brackets `[`.
|
|
||||||
* **Mandatory Field Validation:** Checks if mandatory module-level metadata (`origin`, `author`, `products`, `publishers`, `tree_path`) are present based on your dynamic compliance settings, and if an initial `log.debug` statement exists in the `body`.
|
|
||||||
* **Indentation Checking:** Instantly highlights messy indentation (any space count that is not a clean multiple of 4) with a warning, reminding you to use the Auto-Formatter.
|
|
||||||
* **Unbreakable Lexer (New):** Built-in state-machine lexer correctly parses complex strings, triple-quotes (`"""`), and comments, ensuring accurate parameter counting and avoiding false-positive errors on complex TPL syntax.
|
|
||||||
* **Version Checking:** Compares the TPL version declared in the module with the target version defined in your settings and warns about outdated scripts.
|
|
||||||
|
|
||||||
### ⚙️ Configurable Plugin Settings & Compliance
|
|
||||||
Personalize the plugin directly via the VS Code user interface (File > Preferences > Settings > Extensions > BMC TPL Settings):
|
|
||||||
* **Metadata Compliance:** Define exact compliance rules for your workspace. Choose which metadata fields are mandatory and configure the `tree_path` strictly (e.g., minimum depth and forced static base nodes like `['MyCompany', 'IT']`).
|
|
||||||
* **Default Metadata:** Store your own values for `Author`, `Publisher`, `Origin`, etc. These are automatically inserted when generating new modules.
|
|
||||||
* **TPL Target Version:** Define the default TPL version (e.g., `23` for 1.23) you are developing for.
|
|
||||||
* **Live Reload:** Changes to settings take effect immediately, no editor restart required.
|
|
||||||
|
|
||||||
### 🎨 Auto-Formatter & Visual Alignment (`Shift + Alt + F`)
|
|
||||||
No more broken indentations! The formatter gets your code in top shape:
|
|
||||||
* **VS Code Standard:** Strictly enforces 4 spaces per indentation level.
|
|
||||||
* **Dynamic Block Indentation:** Automatically detects starting and ending blocks (`if`, `for`, `pattern`, `table`, etc.) and adjusts the indentation level.
|
|
||||||
* **Bracket Memory (Visual Alignment):** The ultimate discipline! The formatter remembers the exact column position of opened arrays `[` or function calls `(` and flawlessly aligns all subsequent lines to this bracket.
|
|
||||||
* **Uncompromising Cleanup (New):** Automatically strips all non-standard indentation (like 2, 3, or 6 spaces) and replaces it with exact, mathematically calculated 4-space blocks.
|
|
||||||
|
|
||||||
### 💡 IntelliSense & Autocomplete (Cross-File)
|
|
||||||
Saves an extreme amount of coding time through massive context awareness:
|
|
||||||
* **"Go to Definition" (New):** Instantly navigate your codebase! `Ctrl+Click` (or `F12`) on any function call to jump directly to its declaration. Searches locally first and falls back to the RAM-based workspace index for cross-file jumps.
|
|
||||||
* **Dynamic Taxonomy Support (New):** Full autocomplete support for dynamic BMC scopes. Typing `model.` suggests generic `<NodeKind>` constructors, while `model.rel.` offers dynamic `<Relationship>` constructors with ready-to-use snippets.
|
|
||||||
* **Full BMC Standard Library:** Seamless integration of all built-in functions (e.g., `discovery`, `number`, `text`, `model`). Supports parameter overloads (e.g., `number.toText` optionally with or without a base) via Tab navigation.
|
|
||||||
* **Workspace Scanning:** If you import a module (e.g., `from company.helpers import my_helper`), the plugin scans your entire project in the background. If you type `my_helper.`, it automatically suggests all available `define` methods.
|
|
||||||
* **Case-Insensitive Resolution (New):** All method lookups (both Core and TKU) are completely case-insensitive, meaning `rdbms_functions.xyz` works just as flawlessly as `RDBMS_Functions.Xyz`.
|
|
||||||
* **Local Namespaces:** Detects `definitions` blocks in the same file and suggests their methods.
|
|
||||||
* **Variable Tracking:** Learns all variables you have defined with `:=` and suggests them to you later.
|
|
||||||
|
|
||||||
### 🔍 Hover Documentation (Tooltips)
|
|
||||||
When you hover over a method name, a pattern, or a table:
|
|
||||||
* **Official BMC Docs:** Full integration of the original, multi-line BMC documentation for all built-in functions loaded dynamically (no browser needed anymore!).
|
|
||||||
* **DocString Extraction:** Reads custom multi-line comments (`"""` or `'''`) placed under `define` or `pattern` declarations and renders them as clean Markdown tooltips (even cross-project!).
|
|
||||||
|
|
||||||
### ⚡ Quick Fixes (Code Actions / Lightbulb)
|
|
||||||
Offers immediate repair options with a single click:
|
|
||||||
* **"Insert missing module metadata":** Instantly generates missing mandatory fields in the module metadata block based on your workspace compliance settings.
|
|
||||||
* **"Insert log statement":** Adds the missing `log.debug("Start pattern...");` right at the beginning of the `body` block.
|
|
||||||
* **"Tabs to Spaces":** Instantly replaces all illegal tabs (`\t`) in a line with 4 correct spaces each.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Snippets (Code Templates)
|
|
||||||
|
|
||||||
Just type the keyword and press `Enter` to generate complete, perfectly indented blocks:
|
|
||||||
|
|
||||||
| Keyword | Description |
|
|
||||||
| :--- | :--- |
|
|
||||||
| `tpl_skeleton` | **God-Snippet**: Generates a completely fresh TPL module. Automatically uses your configured compliance rules, settings, and dynamic tree paths. |
|
|
||||||
| `metadata` | **Context-Aware**: Knows where your cursor is. Generates a full module block at the top, or a compact pattern block inside a pattern. |
|
|
||||||
| `pattern` / `syncmapping` | Builds an empty pattern/syncmapping block including DocString and end-tag. |
|
|
||||||
| `define` | Generates a method definition (`define name(params) ... end define;`). |
|
|
||||||
| `overview`, `body`... | Builds the respective block and places the cursor right in the middle. |
|
|
||||||
| `table` | Creates a table including the `default -> "unknown";` fallback. |
|
|
||||||
| `if`, `elif`, `else` | Builds conditional statements incl. `then` and `end if;`, placing the cursor neatly at the condition. |
|
|
||||||
| `for`, `while` | Builds standard loops incl. `do` and end-tag, letting you iterate directly. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Installation & Usage
|
|
||||||
|
|
||||||
1. Activate the plugin. (Download the latest from the releases and simply drag the `.vsix` file into the Visual Studio Code add-ons).
|
|
||||||
2. Open a TPL file in your project and let the analysis run.
|
|
||||||
3. The linter starts automatically with every keystroke.
|
|
||||||
4. **Formatting:** Right-click -> *Format Document* (or `Shift + Alt + F`).
|
|
||||||
5. **Autocomplete:** Triggers automatically while typing, especially after a dot (`.`) during module calls.
|
|
||||||
6. **Navigation:** Press `Ctrl` + Click (or `F12`) to jump to function definitions.
|
|
||||||
7. **Quick Fixes:** Click the yellow/red lightbulb at the beginning of the line or press `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📜 License
|
|
||||||
|
|
||||||
This project is licensed under the **MIT License**.
|
|
||||||
|
|
||||||
Copyright (c) 2026 Thomas Wehnert
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
|
|
||||||
|
|
||||||
---
|
|
||||||
---
|
|
||||||
|
|
||||||
# BMC TPL Code All-Around
|
|
||||||
|
|
||||||
**Linter, Auto-Formatter, IntelliSense und Hover-Dokumentationen für die BMC TPL (Template Language).**
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ 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 basierend auf deinen dynamischen Compliance-Settings, ob auf Modul-Ebene zwingend benötigte Metadaten (`origin`, `author`, `tree_path` etc.) 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]
|
||||||
* **Einrückungs-Prüfung:** Ein Check im Hintergrund markiert unsaubere Einrückungen (alles, was kein sauberes Vielfaches von 4 Leerzeichen ist) sofort mit einer Warnung, um an den Auto-Formatter zu erinnern.
|
* **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]
|
||||||
* **Unzerstörbarer Lexer (Neu):** Ein eingebauter State-Machine-Lexer verarbeitet komplexe Strings, Triple-Quotes (`"""`) und Kommentare fehlerfrei. Dadurch zählt der Linter Parameter immer korrekt und wirft keine falschen Fehler bei komplexer TPL-Syntax.
|
|
||||||
* **Versions-Prüfung:** Gleicht die im Modul deklarierte TPL-Version mit der in deinen Settings definierten Version ab und warnt vor veralteten Skripten.
|
|
||||||
|
|
||||||
### ⚙️ Konfigurierbare Plugin-Einstellungen & Compliance
|
|
||||||
Personalisiere das Plugin direkt über die VS Code Benutzeroberfläche (File > Preferences > Settings > Extensions > BMC TPL Einstellungen):
|
|
||||||
* **Metadaten-Compliance:** Definiere exakte Regeln für deinen Workspace. Wähle, welche Felder Pflicht sind und konfiguriere den `tree_path` strikt (z.B. Mindest-Ebenen und feste Basis-Nodes wie `['MeineFirma', 'IT']`).
|
|
||||||
* **Standard Metadaten:** Hinterlege deine eigenen Werte für `Author`, `Publisher`, `Origin` etc. 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]
|
||||||
* **Kompromisslose Bereinigung (Neu):** Unsaubere Einrückungen (z.B. 2, 3 oder 6 Leerzeichen) werden restlos rasiert und durch exakt berechnete 4-Space-Blöcke ersetzt.
|
|
||||||
|
|
||||||
### 💡 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]
|
||||||
* **"Go to Definition" (Neu):** Navigiere sofort durch deine Codebase! `Strg+Klick` (oder `F12`) auf einen Funktionsaufruf springt direkt zur Deklaration. Sucht erst lokal und weicht für dateiübergreifende Aufrufe blitzschnell auf den RAM-Index aus.
|
* **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]
|
||||||
* **Dynamische Taxonomie (Neu):** Voller Support für dynamische BMC-Scopes. Das Tippen von `model.` schlägt einen generischen `<NodeKind>`-Konstruktor vor, während `model.rel.` dynamische `<Relationship>`-Konstruktoren mit fertigen Snippets anbietet.
|
* **Lokale Namespaces:** Erkennt `definitions`-Blöcke in der gleichen Datei und schlägt deren Methoden vor.[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.
|
* **Variablen-Tracking:** Lernt alle Variablen, die du mit `:=` definiert hast, und schlägt sie dir später vor.[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.
|
* **Log-Filter:** Nach dem Tippen von `log.` zeigt VS Code strikt nur noch die validen Methoden (`debug`, `info`, `warn`, `error`) an.[cite: 5]
|
||||||
* **Case-Insensitive Auswertung (Neu):** Alle Methoden (Core und TKU) werden nun komplett groß-/kleinschreibungsunabhängig aufgelöst (`rdbms_functions.xyz` funktioniert genauso tadellos wie `RDBMS_Functions.Xyz`).
|
|
||||||
* **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.
|
|
||||||
|
|
||||||
### 🔍 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 fehlenden Pflichtfelder im Modul-Metadata-Block, basierend auf deinen Compliance-Settings.
|
* **"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, dynamischen Tree-Paths und Compliance-Regeln. |
|
| `tpl_skeleton` | **God-Snippet**: Generiert eine komplett frische `.tpl` Datei inklusive Modul-Definition, Pflicht-Metadaten, Pattern, Triggern und Log-Statements. |[cite: 5]
|
||||||
| `metadata` | **Kontext-sensitiv**: Weiß genau, wo dein Cursor steht. Generiert einen großen Modul-Block ganz oben oder einen kompakten Pattern-Block innerhalb eines Patterns. |
|
| `pattern` | Baut einen leeren Pattern-Block inklusive DocString und `end pattern;`. |[cite: 5]
|
||||||
| `pattern` / `syncmapping` | Baut einen leeren Pattern/Syncmapping-Block inklusive DocString und End-Tag. |
|
| `syncmapping` | Baut einen leeren Syncmapping-Block inklusive DocString. |[cite: 5]
|
||||||
| `define` | Generiert eine Methodendefinition (`define name(params) ... end define;`). |
|
| `define` | Generiert eine Methodendefinition (`define name(params) ... end define;`). |[cite: 5]
|
||||||
| `overview`, `body`... | Baut den jeweiligen Block auf und platziert den Cursor in der Mitte. |
|
| `metadata`, `overview`, `body`... | Baut den jeweiligen Block auf und platziert den Cursor in der Mitte. |[cite: 5]
|
||||||
| `table` | Erstellt eine Tabelle inklusive des `default -> "unknown";` Fallbacks. |
|
| `table` | Erstellt eine Tabelle inklusive des `default -> "unknown";` Fallbacks. |[cite: 5]
|
||||||
| `if`, `elif`, `else` | Baut bedingte Abfragen inkl. `then` und `end if;` und platziert den Cursor sauber bei der Bedingung. |
|
| `if`, `for` | Baut Standard-Schleifen und Abfragen. |[cite: 5]
|
||||||
| `for`, `while` | Baut Standard-Schleifen inkl. `do` und End-Tag und lässt dich direkt iterieren. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ 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. **Navigieren:** Mit `Strg` + Klick (oder `F12`) direkt zu Funktionsdefinitionen springen.
|
6. **Quick Fixes:** Auf die gelbe/rote Glühbirne am Zeilenanfang klicken oder `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac) drücken.[cite: 5]
|
||||||
7. **Quick Fixes:** Auf die gelbe/rote Glühbirne am Zeilenanfang klicken oder `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac) drücken.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📜 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]
|
||||||
```eof
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
npm run lint -- --fix
|
|
||||||
64
package.json
64
package.json
|
|
@ -35,81 +35,34 @@
|
||||||
"main": "./dist/extension.js",
|
"main": "./dist/extension.js",
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"title": "BMC TPL Settings",
|
"title": "BMC TPL Einstellungen",
|
||||||
"properties": {
|
"properties": {
|
||||||
"tpl.metadata.author": {
|
"tpl.metadata.author": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "Autor",
|
"default": "Autor",
|
||||||
"description": "Standard-Author for new generated TPL-Modules."
|
"description": "Standard-Autor für neu generierte TPL-Module."
|
||||||
},
|
},
|
||||||
"tpl.metadata.origin": {
|
"tpl.metadata.origin": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "Custom",
|
"default": "Custom",
|
||||||
"description": "Standard-Origin for new generated TPL-Modules."
|
"description": "Standard-Origin für neu generierte TPL-Module."
|
||||||
},
|
},
|
||||||
"tpl.metadata.publisher": {
|
"tpl.metadata.publisher": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "IT-Operations",
|
"default": "IT-Operations",
|
||||||
"description": "Standard-Publisher for new generated TPL-Modules."
|
"description": "Standard-Publisher für neu generierte TPL-Module."
|
||||||
},
|
},
|
||||||
"tpl.metadata.treePathBase": {
|
"tpl.metadata.treePathBase": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "Company",
|
"default": "Company",
|
||||||
"description": "First Part (Basis) of the tree_paths in the Metadata."
|
"description": "Erster Teil (Basis) des tree_paths in den Metadaten."
|
||||||
},
|
|
||||||
"tpl.metadata.require.origin": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": true,
|
|
||||||
"description": "Marks 'origin' as Required on module level."
|
|
||||||
},
|
|
||||||
"tpl.metadata.require.author": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": true,
|
|
||||||
"description": "Marks 'author' as Required on module level."
|
|
||||||
},
|
|
||||||
"tpl.metadata.require.products": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": true,
|
|
||||||
"description": "Marks 'products' aas Required on module level."
|
|
||||||
},
|
|
||||||
"tpl.metadata.require.publishers": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": true,
|
|
||||||
"description": "Marks 'publishers' as Required on module level."
|
|
||||||
},
|
|
||||||
"tpl.metadata.require.treePath": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": true,
|
|
||||||
"description": "Marks 'tree_path' as Required on module level."
|
|
||||||
},
|
|
||||||
"tpl.metadata.treePath.minLevels": {
|
|
||||||
"type": "number",
|
|
||||||
"default": 3,
|
|
||||||
"description": "How many levels should the tree_path in Metadata at least have? (e.g. 3 for 'Base', 'Category', 'Subcategory')."
|
|
||||||
},
|
|
||||||
"tpl.metadata.treePath.staticNodes": {
|
|
||||||
"type": "array",
|
|
||||||
"items": { "type": "string" },
|
|
||||||
"default": [""],
|
|
||||||
"description": "Fixed definitions for the tree_path left to right (e.g. ['Company', 'Software']). The rest of the levels are free to choose."
|
|
||||||
},
|
},
|
||||||
"tpl.version": {
|
"tpl.version": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"default": 23,
|
"default": 23,
|
||||||
"description": "Current TPL-Subversion (e. g. 23 for TPL 1.23). Used to find old Modules and for auto generating the sceleton."
|
"description": "Aktuelle TPL-Nebenversion (z. B. 23 für TPL 1.23). Wird genutzt um veraltete Module zu erkennen und beim Generieren eingefügt."
|
||||||
},
|
|
||||||
"tpl.rules.requireBodyLog": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": true,
|
|
||||||
"description": "Require a log.debug statement at the start of a pattern's body phase."
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"commands": [
|
|
||||||
{
|
|
||||||
"command": "tpl.scanWorkspace",
|
|
||||||
"title": "TPL: Gesamten Workspace prüfen"
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"languages": [
|
"languages": [
|
||||||
{
|
{
|
||||||
|
|
@ -133,14 +86,13 @@
|
||||||
"watch": "npm-run-all -p watch:*",
|
"watch": "npm-run-all -p watch:*",
|
||||||
"watch:esbuild": "node esbuild.js --watch",
|
"watch:esbuild": "node esbuild.js --watch",
|
||||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||||
"package": "npm run check-types && npm run lint && npm run test && node esbuild.js --production",
|
"package": "npm run check-types && npm run lint && node esbuild.js --production",
|
||||||
"compile-tests": "tsc -p . --outDir out",
|
"compile-tests": "tsc -p . --outDir out",
|
||||||
"watch-tests": "tsc -p . -w --outDir out",
|
"watch-tests": "tsc -p . -w --outDir out",
|
||||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||||
"check-types": "tsc --noEmit",
|
"check-types": "tsc --noEmit",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"test": "vscode-test",
|
"test": "vscode-test"
|
||||||
"generate-builtins": "python scripts/generate_builtins.py ."
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/vscode": "^1.140.0",
|
"@types/vscode": "^1.140.0",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { tplBuiltIns } from './TplBuiltIns';
|
||||||
|
|
||||||
|
export class TplHoverProvider implements vscode.HoverProvider {
|
||||||
|
public async provideHover(
|
||||||
|
document: vscode.TextDocument,
|
||||||
|
position: vscode.Position,
|
||||||
|
token: vscode.CancellationToken
|
||||||
|
): Promise<vscode.Hover | null> {
|
||||||
|
|
||||||
|
// Greift das Wort unter dem Mauszeiger ab, inklusive Punkte für Namespaces
|
||||||
|
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
|
||||||
|
if (!range) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const word = document.getText(range);
|
||||||
|
const parts = word.split('.');
|
||||||
|
|
||||||
|
let namespace = '';
|
||||||
|
let methodOrPatternName = '';
|
||||||
|
|
||||||
|
if (parts.length === 2) {
|
||||||
|
namespace = parts[0];
|
||||||
|
methodOrPatternName = parts[1];
|
||||||
|
} else if (parts.length === 1) {
|
||||||
|
methodOrPatternName = parts[0];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BMC Namespace-Methoden (z.B. discovery.listDirectory)
|
||||||
|
if (namespace && tplBuiltIns[namespace] && tplBuiltIns[namespace][methodOrPatternName]) {
|
||||||
|
const def = tplBuiltIns[namespace][methodOrPatternName];
|
||||||
|
const markdown = new vscode.MarkdownString();
|
||||||
|
markdown.appendCodeblock(`${namespace}.${methodOrPatternName}`, 'tpl');
|
||||||
|
markdown.appendMarkdown(`---\n\n${def.description}`);
|
||||||
|
return new vscode.Hover(markdown);
|
||||||
|
}
|
||||||
|
// BMC Globale Funktionen (z.B. size, datatype)
|
||||||
|
else if (!namespace && tplBuiltIns['built_in_globals'] && tplBuiltIns['built_in_globals'][methodOrPatternName]) {
|
||||||
|
const def = tplBuiltIns['built_in_globals'][methodOrPatternName];
|
||||||
|
const markdown = new vscode.MarkdownString();
|
||||||
|
markdown.appendCodeblock(`${methodOrPatternName}`, 'tpl');
|
||||||
|
markdown.appendMarkdown(`---\n\n${def.description}`);
|
||||||
|
return new vscode.Hover(markdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,55 +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;
|
|
||||||
|
|
||||||
// Multi-Line Import-Tracking
|
|
||||||
public inImportBlock = false;
|
|
||||||
public currentImportModule = "";
|
|
||||||
|
|
||||||
// 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 = new Set<string>();
|
|
||||||
|
|
||||||
// Klammer-Tracking
|
|
||||||
public parenStack: number[] = [];
|
|
||||||
public bracketStack: number[] = [];
|
|
||||||
public openDefines: number[] = [];
|
|
||||||
public openControlBlocks: { type: string, lineIdx: 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,88 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { LinterContext } from './LinterContext';
|
|
||||||
import { SyntaxRule } from './rules/SyntaxRule';
|
|
||||||
import { BlockRule } from './rules/BlockRule';
|
|
||||||
import { MetadataRule } from './rules/MetadataRule';
|
|
||||||
import { IndentationRule } from './rules/IndentationRule';
|
|
||||||
import { ParameterRule } from './rules/ParameterRule';
|
|
||||||
|
|
||||||
export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
|
|
||||||
if (document.languageId !== 'tpl') {return;}
|
|
||||||
|
|
||||||
const text = document.getText();
|
|
||||||
const lines = text.split('\n');
|
|
||||||
const ctx = new LinterContext();
|
|
||||||
|
|
||||||
const rules = [
|
|
||||||
new IndentationRule(),
|
|
||||||
new SyntaxRule(),
|
|
||||||
new MetadataRule(),
|
|
||||||
new BlockRule(),
|
|
||||||
new ParameterRule()
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const rawLine = lines[i];
|
|
||||||
const line = rawLine.trim();
|
|
||||||
|
|
||||||
ctx.resetLineStates();
|
|
||||||
|
|
||||||
// NEU: TODO-Check hier im Linter, BEVOR Kommentare ignoriert werden!
|
|
||||||
const todoMatch = rawLine.match(/\bTODO\b\s*:?\s*(.*)/i);
|
|
||||||
if (todoMatch) {
|
|
||||||
ctx.addDiag(i, todoMatch.index!, rawLine.length, `TODO: ${todoMatch[1].trim()}`, vscode.DiagnosticSeverity.Information);
|
|
||||||
}
|
|
||||||
|
|
||||||
trackStrings(rawLine, line, ctx);
|
|
||||||
|
|
||||||
if (ctx.isInternalStringLine && (ctx.inTripleDouble || ctx.inTripleSingle)) { continue; }
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sicherer Aufruf für alle Regeln
|
|
||||||
for (const rule of rules) {
|
|
||||||
if (rule.checkEnd) {
|
|
||||||
rule.checkEnd(lines, ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,41 +0,0 @@
|
||||||
import { tplBuiltIns } from '../data/TplBuiltIns';
|
|
||||||
import { generatedTplBuiltIns } from '../data/GeneratedTplBuiltIns';
|
|
||||||
|
|
||||||
export class TplLookup {
|
|
||||||
// Holt eine spezifische Methode (Case-Insensitive)
|
|
||||||
public static getBuiltInFunction(namespace: string, funcName: string): any | null {
|
|
||||||
const nsLower = namespace.toLowerCase();
|
|
||||||
const fnLower = funcName.toLowerCase();
|
|
||||||
|
|
||||||
// 1. Suche in Core Built-Ins (discovery, model, etc.)
|
|
||||||
const coreKey = Object.keys(tplBuiltIns).find(k => k.toLowerCase() === nsLower);
|
|
||||||
if (coreKey) {
|
|
||||||
const methods = (tplBuiltIns as any)[coreKey];
|
|
||||||
const mKey = Object.keys(methods).find(k => k.toLowerCase() === fnLower);
|
|
||||||
if (mKey) {return methods[mKey];}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Suche in Generierten TKU Built-Ins (rdbms_functions, etc.)
|
|
||||||
const tkuKey = Object.keys(generatedTplBuiltIns).find(k => k.toLowerCase() === nsLower);
|
|
||||||
if (tkuKey) {
|
|
||||||
const methods = (generatedTplBuiltIns as any)[tkuKey];
|
|
||||||
const mKey = Object.keys(methods).find(k => k.toLowerCase() === fnLower);
|
|
||||||
if (mKey) {return methods[mKey];}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Holt alle Methoden eines Moduls für die Autovervollständigung (Case-Insensitive)
|
|
||||||
public static getAllMethodsForNamespace(namespace: string): Record<string, any> | null {
|
|
||||||
const nsLower = namespace.toLowerCase();
|
|
||||||
|
|
||||||
const coreKey = Object.keys(tplBuiltIns).find(k => k.toLowerCase() === nsLower);
|
|
||||||
if (coreKey) {return (tplBuiltIns as any)[coreKey];}
|
|
||||||
|
|
||||||
const tkuKey = Object.keys(generatedTplBuiltIns).find(k => k.toLowerCase() === nsLower);
|
|
||||||
if (tkuKey) {return (generatedTplBuiltIns as any)[tkuKey];}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
|
|
||||||
export interface TplFunctionSignature {
|
|
||||||
name: string;
|
|
||||||
moduleName: string;
|
|
||||||
params: string[];
|
|
||||||
docString?: string;
|
|
||||||
hasReturnValue?: boolean;
|
|
||||||
lineIdx: number; // NEU: Für 'Go to Definition'
|
|
||||||
fsPath: string; // NEU: Für 'Go to Definition'
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WorkspaceIndex {
|
|
||||||
private fileIndex = new Map<string, Map<string, TplFunctionSignature>>();
|
|
||||||
private fileModules = new Map<string, string>();
|
|
||||||
private moduleDefinitions = new Map<string, Set<string>>();
|
|
||||||
|
|
||||||
public updateFile(uri: vscode.Uri, text: string) {
|
|
||||||
if (!uri.fsPath.endsWith('.tpl')) {return;}
|
|
||||||
|
|
||||||
const fsPath = uri.fsPath;
|
|
||||||
const lines = text.split('\n');
|
|
||||||
let currentModule = "";
|
|
||||||
let currentDefBlock = "";
|
|
||||||
const functions = new Map<string, TplFunctionSignature>();
|
|
||||||
const defBlocks = new Set<string>();
|
|
||||||
|
|
||||||
const modMatch = text.match(/tpl\s+1\.\d+\s+module\s+([a-zA-Z0-9_.]+)\s*;/);
|
|
||||||
if (modMatch) {
|
|
||||||
currentModule = modMatch[1];
|
|
||||||
}
|
|
||||||
this.fileModules.set(fsPath, currentModule);
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
|
|
||||||
const defBlockMatch = line.match(/^\s*definitions?\s+([a-zA-Z0-9_]+)/);
|
|
||||||
if (defBlockMatch) {
|
|
||||||
currentDefBlock = defBlockMatch[1];
|
|
||||||
defBlocks.add(currentDefBlock);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.match(/^\s*end\s+definitions?/)) {
|
|
||||||
currentDefBlock = "";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const structMatch = line.match(/^\s*(?:pattern|syncmapping|table)\s+([a-zA-Z0-9_]+)/);
|
|
||||||
if (structMatch) {
|
|
||||||
defBlocks.add(structMatch[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/);
|
|
||||||
if (defineMatch) {
|
|
||||||
const funcName = defineMatch[1];
|
|
||||||
const paramString = defineMatch[2] || "";
|
|
||||||
const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName;
|
|
||||||
const params = paramString.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
||||||
|
|
||||||
let docString = undefined;
|
|
||||||
const textAfterDefine = text.substring(text.indexOf(line) + line.length);
|
|
||||||
const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
|
|
||||||
if (docMatch) { docString = docMatch[1].trim(); }
|
|
||||||
|
|
||||||
let hasReturnValue = false;
|
|
||||||
for (let j = i + 1; j < lines.length; j++) {
|
|
||||||
if (lines[j].match(/^\s*end\s+define/)) {break;}
|
|
||||||
if (lines[j].match(/^\s*return\b/)) {
|
|
||||||
hasReturnValue = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
functions.set(fullName, {
|
|
||||||
name: fullName,
|
|
||||||
moduleName: currentModule,
|
|
||||||
params: params,
|
|
||||||
docString: docString,
|
|
||||||
hasReturnValue: hasReturnValue,
|
|
||||||
lineIdx: i,
|
|
||||||
fsPath: fsPath
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fileIndex.set(fsPath, functions);
|
|
||||||
if (currentModule) {
|
|
||||||
this.moduleDefinitions.set(currentModule, defBlocks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeFile(uri: vscode.Uri) {
|
|
||||||
const moduleName = this.fileModules.get(uri.fsPath);
|
|
||||||
if (moduleName) {
|
|
||||||
this.moduleDefinitions.delete(moduleName);
|
|
||||||
}
|
|
||||||
this.fileIndex.delete(uri.fsPath);
|
|
||||||
this.fileModules.delete(uri.fsPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAllModules(): string[] {
|
|
||||||
return Array.from(this.moduleDefinitions.keys());
|
|
||||||
}
|
|
||||||
|
|
||||||
public getDefinitionBlocksForModule(moduleName: string): string[] {
|
|
||||||
const blocks = this.moduleDefinitions.get(moduleName);
|
|
||||||
return blocks ? Array.from(blocks) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFunctionsForModule(moduleName: string): TplFunctionSignature[] {
|
|
||||||
const result: TplFunctionSignature[] = [];
|
|
||||||
for (const [fsPath, map] of this.fileIndex.entries()) {
|
|
||||||
if (this.fileModules.get(fsPath) === moduleName) {
|
|
||||||
result.push(...map.values());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFunctionSignatures(funcName: string): TplFunctionSignature[] {
|
|
||||||
const result: TplFunctionSignature[] = [];
|
|
||||||
for (const map of this.fileIndex.values()) {
|
|
||||||
for (const sig of map.values()) {
|
|
||||||
if (sig.name === funcName || sig.name.endsWith(`.${funcName}`)) {
|
|
||||||
result.push(sig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const workspaceIndex = new WorkspaceIndex();
|
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { LinterRule } from './LinterRule';
|
|
||||||
import { LinterContext, PatternPhase } from '../LinterContext';
|
|
||||||
|
|
||||||
export class BlockRule implements LinterRule {
|
|
||||||
|
|
||||||
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
|
||||||
if (!ctx.hasModuleDef) {
|
|
||||||
if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; }
|
|
||||||
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Every file must start with 'tpl 1.x module <name>;'.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are inside the metadata block, we ignore the rest. MetadataRule handles it!
|
|
||||||
if (ctx.inModuleMetadata || (line.startsWith('metadata') && ctx.inBlock === 'none')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) {
|
|
||||||
if (!ctx.hasModuleMetadata && ctx.inBlock === 'none') {
|
|
||||||
ctx.addDiag(lineIdx, 0, line.length, "A 'metadata' block at the module level is mandatory before defining other blocks.", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleBlockOpenings(line, rawLine, lineIdx, ctx);
|
|
||||||
this.handleControlBlocks(line, rawLine, lineIdx, ctx);
|
|
||||||
|
|
||||||
if (ctx.inBlock === 'pattern') {
|
|
||||||
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' was not closed with 'end define;'!", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
for (const block of ctx.openControlBlocks) {
|
|
||||||
ctx.addDiag(block.lineIdx, 0, lines[block.lineIdx].length, `Block '${block.type}' was not closed with 'end ${block.type};'!`, vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleControlBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
if (/^if\b/.test(line)) { ctx.openControlBlocks.push({ type: 'if', lineIdx }); }
|
|
||||||
else if (/^for\b/.test(line)) { ctx.openControlBlocks.push({ type: 'for', lineIdx }); }
|
|
||||||
else if (/^while\b/.test(line)) { ctx.openControlBlocks.push({ type: 'while', lineIdx }); }
|
|
||||||
|
|
||||||
if (line.startsWith('end if')) { this.closeControlBlock('if', line, lineIdx, ctx); }
|
|
||||||
else if (line.startsWith('end for')) { this.closeControlBlock('for', line, lineIdx, ctx); }
|
|
||||||
else if (line.startsWith('end while')) { this.closeControlBlock('while', line, lineIdx, ctx); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private closeControlBlock(expectedType: string, line: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
if (ctx.openControlBlocks.length === 0) {
|
|
||||||
ctx.addDiag(lineIdx, 0, line.length, `Found 'end ${expectedType}' without an open block.`, vscode.DiagnosticSeverity.Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastBlock = ctx.openControlBlocks.pop()!;
|
|
||||||
if (lastBlock.type !== expectedType) {
|
|
||||||
ctx.addDiag(lineIdx, 0, line.length, `Wrong end. Expected: 'end ${lastBlock.type};', found: 'end ${expectedType};'.`, vscode.DiagnosticSeverity.Error);
|
|
||||||
ctx.openControlBlocks.push(lastBlock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleBlockOpenings(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
if (line.startsWith('definitions ') || line.startsWith('definition ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A 'definitions' block must be declared at the top level.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'definitions';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('define ')) { ctx.openDefines.push(lineIdx); }
|
|
||||||
else if (line.startsWith('enumeration ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "An 'enumeration' must be declared at the top level.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'enumeration';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('configuration ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A 'configuration' must be declared at the top level.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'configuration';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('pattern ') || line.startsWith('syncmapping ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 7, "A pattern or syncmapping cannot be declared inside another block.", 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, "Invalid pattern declaration. Expected: 'pattern <Name> <Version>'", vscode.DiagnosticSeverity.Error); }
|
|
||||||
}
|
|
||||||
else if (line.startsWith('table ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 5, "A table cannot be declared inside another block.", 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}' was not closed with 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
|
||||||
if (ctx.currentPatternPhase >= phaseEnum) { ctx.addDiag(lineIdx, 0, phaseName.length, `Block '${phaseName}' is not allowed here.`, 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}' was not closed with 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.activePhaseName = "body";
|
|
||||||
ctx.activePhaseLine = lineIdx;
|
|
||||||
|
|
||||||
const config = vscode.workspace.getConfiguration('tpl');
|
|
||||||
const requireLog = config.get<boolean>('rules.requireBodyLog', true);
|
|
||||||
|
|
||||||
if (requireLog) {
|
|
||||||
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, `Missing log statement with pattern name in body. E.g.: ${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, `Unexpected '${closer}'. Expected '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}' was not closed with 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
|
||||||
if (ctx.inBlock !== 'pattern') { ctx.addDiag(lineIdx, 0, 11, "Found 'end pattern' without an open pattern block.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'none';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) {
|
|
||||||
ctx.inBlock = 'none';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('end definitions') || line.startsWith('end definition')) {
|
|
||||||
if (ctx.inBlock !== 'definitions') { ctx.addDiag(lineIdx, 0, line.length, "Found 'end definitions' without an open definitions block.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'none';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('end define')) {
|
|
||||||
if (ctx.openDefines.length > 0) { ctx.openDefines.pop(); }
|
|
||||||
else { ctx.addDiag(lineIdx, 0, line.length, "Found 'end define' without an open 'define'.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { LinterRule } from './LinterRule';
|
|
||||||
import { LinterContext } from '../LinterContext';
|
|
||||||
|
|
||||||
export class IndentationRule implements LinterRule {
|
|
||||||
|
|
||||||
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
|
||||||
if (rawLine.trim().length === 0) {return;}
|
|
||||||
if (rawLine.includes('\t')) {return;}
|
|
||||||
|
|
||||||
// Sobald wir in Klammern oder einem Multi-Line Import sind (Visual Alignment),
|
|
||||||
// greift die 4-Leerzeichen-Regel nicht!
|
|
||||||
if (ctx.parenStack.length > 0 || ctx.bracketStack.length > 0 || ctx.inImportBlock) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const leadingSpacesMatch = rawLine.match(/^ +/);
|
|
||||||
const spaceCount = leadingSpacesMatch ? leadingSpacesMatch[0].length : 0;
|
|
||||||
|
|
||||||
if (spaceCount > 0 && spaceCount % 4 !== 0) {
|
|
||||||
ctx.addDiag(
|
|
||||||
lineIdx,
|
|
||||||
0,
|
|
||||||
spaceCount,
|
|
||||||
`Invalid indentation (${spaceCount} spaces). Please use a multiple of 4. (Tip: Shift+Alt+F to auto-format)`,
|
|
||||||
vscode.DiagnosticSeverity.Warning
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public checkEnd(lines: string[], ctx: LinterContext): void {
|
|
||||||
// empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,76 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { LinterRule } from './LinterRule';
|
|
||||||
import { LinterContext } from '../LinterContext';
|
|
||||||
|
|
||||||
export class MetadataRule implements LinterRule {
|
|
||||||
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
|
||||||
if (line.startsWith('metadata') && ctx.inBlock === 'none') {
|
|
||||||
ctx.inModuleMetadata = true;
|
|
||||||
ctx.hasModuleMetadata = true;
|
|
||||||
ctx.moduleMetadataBlockLine = lineIdx;
|
|
||||||
ctx.foundModuleMetadata.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.inModuleMetadata) {
|
|
||||||
const config = vscode.workspace.getConfiguration('tpl');
|
|
||||||
|
|
||||||
if (line.match(/^origin\b/)) {ctx.foundModuleMetadata.add('origin');}
|
|
||||||
if (line.match(/^author\b/)) {ctx.foundModuleMetadata.add('author');}
|
|
||||||
if (line.match(/^products\b/)) {ctx.foundModuleMetadata.add('products');}
|
|
||||||
if (line.match(/^publishers\b/)) {ctx.foundModuleMetadata.add('publishers');}
|
|
||||||
|
|
||||||
if (line.match(/^tree_path\b/)) {
|
|
||||||
ctx.foundModuleMetadata.add('tree_path');
|
|
||||||
if (config.get<boolean>('metadata.require.treePath', true)) {
|
|
||||||
this.checkTreePath(line, rawLine, lineIdx, ctx, config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.startsWith('end metadata')) {
|
|
||||||
ctx.inModuleMetadata = false;
|
|
||||||
this.checkRequiredFields(ctx, config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkTreePath(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, config: vscode.WorkspaceConfiguration) {
|
|
||||||
const minLevels = config.get<number>('metadata.treePath.minLevels', 3);
|
|
||||||
const staticNodes = config.get<string[]>('metadata.treePath.staticNodes', ['Grafing']); // Fallback
|
|
||||||
|
|
||||||
const match = line.match(/tree_path\s*:=\s*(.*);/);
|
|
||||||
if (match) {
|
|
||||||
const paths = match[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, ''));
|
|
||||||
|
|
||||||
if (paths.length < minLevels) {
|
|
||||||
ctx.addDiag(lineIdx, 0, rawLine.length, `The tree_path must contain at least ${minLevels} levels.`, vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < staticNodes.length; i++) {
|
|
||||||
if (paths[i] !== staticNodes[i]) {
|
|
||||||
ctx.addDiag(lineIdx, 0, rawLine.length, `Tree path level ${i + 1} must strictly be '${staticNodes[i]}'.`, vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkRequiredFields(ctx: LinterContext, config: vscode.WorkspaceConfiguration) {
|
|
||||||
const requiredFields: string[] = [];
|
|
||||||
if (config.get<boolean>('metadata.require.origin', true)) {requiredFields.push('origin');}
|
|
||||||
if (config.get<boolean>('metadata.require.author', true)) {requiredFields.push('author');}
|
|
||||||
if (config.get<boolean>('metadata.require.products', true)) {requiredFields.push('products');}
|
|
||||||
if (config.get<boolean>('metadata.require.publishers', true)) {requiredFields.push('publishers');}
|
|
||||||
if (config.get<boolean>('metadata.require.treePath', true)) {requiredFields.push('tree_path');}
|
|
||||||
|
|
||||||
const missing = requiredFields.filter(f => !ctx.foundModuleMetadata.has(f));
|
|
||||||
|
|
||||||
if (missing.length > 0 && ctx.moduleMetadataBlockLine !== -1) {
|
|
||||||
const diag = ctx.addDiag(ctx.moduleMetadataBlockLine, 0, 8, `Missing mandatory module metadata fields: ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
|
|
||||||
diag.code = 'missing_module_metadata';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public checkEnd(lines: string[], ctx: LinterContext): void {
|
|
||||||
// empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,157 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { LinterRule } from './LinterRule';
|
|
||||||
import { LinterContext } from '../LinterContext';
|
|
||||||
import { workspaceIndex, TplFunctionSignature } from '../WorkspaceIndex';
|
|
||||||
import { TplLookup } from '../TplLookup'; // Nutzt ab sofort das zentrale Lexikon
|
|
||||||
|
|
||||||
export class ParameterRule implements LinterRule {
|
|
||||||
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
|
||||||
let cleanLine = "";
|
|
||||||
let inString = false;
|
|
||||||
let stringChar = '';
|
|
||||||
let inTripleQuote = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < line.length; i++) {
|
|
||||||
const char = line[i];
|
|
||||||
|
|
||||||
// 1. Single-Line Kommentare (//) ignorieren
|
|
||||||
if (!inString && !inTripleQuote && char === '/' && line[i+1] === '/') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. TPL Triple Quotes (""" oder ''') erkennen
|
|
||||||
if (!inString && !inTripleQuote &&
|
|
||||||
(char === '"' || char === "'") &&
|
|
||||||
line[i+1] === char &&
|
|
||||||
line[i+2] === char) {
|
|
||||||
|
|
||||||
inTripleQuote = true;
|
|
||||||
stringChar = char;
|
|
||||||
cleanLine += char + char + char; // Die """ behalten, damit wir die Zeile nicht komplett leeren
|
|
||||||
i += 2; // Überspringe die nächsten zwei Zeichen
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Triple Quotes beenden
|
|
||||||
if (inTripleQuote) {
|
|
||||||
if (char === stringChar && line[i+1] === stringChar && line[i+2] === stringChar) {
|
|
||||||
inTripleQuote = false;
|
|
||||||
cleanLine += char + char + char;
|
|
||||||
i += 2;
|
|
||||||
}
|
|
||||||
continue; // Alles innerhalb von """ ignorieren!
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Normale Strings beenden
|
|
||||||
if (inString) {
|
|
||||||
if (char === '\\') {
|
|
||||||
i++; // Escape-Sequenzen ignorieren
|
|
||||||
} else if (char === stringChar) {
|
|
||||||
inString = false;
|
|
||||||
cleanLine += char;
|
|
||||||
}
|
|
||||||
continue; // Alles innerhalb von " ignorieren!
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Normale Strings starten
|
|
||||||
if (!inString && !inTripleQuote && (char === '"' || char === "'")) {
|
|
||||||
inString = true;
|
|
||||||
stringChar = char;
|
|
||||||
cleanLine += char;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wenn wir hier ankommen, sind wir im echten, ausführbaren TPL-Code
|
|
||||||
cleanLine += char;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanLine.trim().length === 0) {return;}
|
|
||||||
|
|
||||||
// Die Definition der Funktion selbst ist kein Aufruf und wird ignoriert.
|
|
||||||
if (/^\s*define\s+/.test(cleanLine)) {return;}
|
|
||||||
|
|
||||||
const callRegex = /\b([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)?)\s*\(/g;
|
|
||||||
let match;
|
|
||||||
|
|
||||||
while ((match = callRegex.exec(cleanLine)) !== null) {
|
|
||||||
const funcName = match[1];
|
|
||||||
const callStart = match.index;
|
|
||||||
const parenStart = callStart + funcName.length;
|
|
||||||
|
|
||||||
if (/^(if|elif|where|for|while|in|matches|has|subword|defined)\b/.test(funcName)) {continue;}
|
|
||||||
|
|
||||||
let depth = 0;
|
|
||||||
let parenEnd = -1;
|
|
||||||
for (let i = parenStart; i < cleanLine.length; i++) {
|
|
||||||
if (cleanLine[i] === '(') {depth++;}
|
|
||||||
else if (cleanLine[i] === ')') {
|
|
||||||
depth--;
|
|
||||||
if (depth === 0) {
|
|
||||||
parenEnd = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parenEnd !== -1) {
|
|
||||||
const paramsStr = cleanLine.substring(parenStart + 1, parenEnd).trim();
|
|
||||||
let paramCount = 0;
|
|
||||||
|
|
||||||
if (paramsStr.length > 0) {
|
|
||||||
let paramDepth = 0;
|
|
||||||
let commas = 0;
|
|
||||||
for (let i = 0; i < paramsStr.length; i++) {
|
|
||||||
if (paramsStr[i] === '(' || paramsStr[i] === '[') {paramDepth++;}
|
|
||||||
else if (paramsStr[i] === ')' || paramsStr[i] === ']') {paramDepth--;}
|
|
||||||
else if (paramsStr[i] === ',' && paramDepth === 0) {commas++;}
|
|
||||||
}
|
|
||||||
paramCount = commas + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.validateFunctionParams(funcName, paramCount, lineIdx, rawLine, ctx);
|
|
||||||
this.validateReturnValue(funcName, cleanLine, lineIdx, rawLine, ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public checkEnd(lines: string[], ctx: LinterContext): void {
|
|
||||||
// empty
|
|
||||||
}
|
|
||||||
|
|
||||||
private validateFunctionParams(funcName: string, passedCount: number, lineIdx: number, rawLine: string, ctx: LinterContext) {
|
|
||||||
const parts = funcName.split('.');
|
|
||||||
|
|
||||||
if (parts.length > 1) {
|
|
||||||
// Case-Insensitive Prüfung über den neuen Lookup!
|
|
||||||
if (TplLookup.getBuiltInFunction(parts[0], parts[1])) {
|
|
||||||
return; // Funktion existiert, Linter ist zufrieden.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const signatures = workspaceIndex.getFunctionSignatures(funcName);
|
|
||||||
if (signatures.length > 0) {
|
|
||||||
const matches = signatures.some((sig: TplFunctionSignature) => sig.params.length === passedCount);
|
|
||||||
if (!matches) {
|
|
||||||
const expected = signatures[0].params.length;
|
|
||||||
const idx = rawLine.indexOf(funcName);
|
|
||||||
ctx.addDiag(lineIdx, Math.max(0, idx), Math.max(0, idx) + funcName.length, `Function '${funcName}' expects ${expected} parameter(s), but ${passedCount} were provided.`, vscode.DiagnosticSeverity.Warning);
|
|
||||||
}
|
|
||||||
} else if (parts.length > 1) {
|
|
||||||
const idx = rawLine.indexOf(funcName);
|
|
||||||
ctx.addDiag(lineIdx, Math.max(0, idx), Math.max(0, idx) + funcName.length, `Unknown function '${funcName}'. Please check your imports or contact the plugin provider.`, vscode.DiagnosticSeverity.Information);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private validateReturnValue(funcName: string, cleanLine: string, lineIdx: number, rawLine: string, ctx: LinterContext) {
|
|
||||||
const signatures = workspaceIndex.getFunctionSignatures(funcName);
|
|
||||||
if (signatures.length > 0 && signatures[0].hasReturnValue) {
|
|
||||||
const isAssigned = cleanLine.includes(':=');
|
|
||||||
const isUsedInCondition = /\b(if|elif|while)\b/.test(cleanLine);
|
|
||||||
|
|
||||||
if (!isAssigned && !isUsedInCondition) {
|
|
||||||
const idx = rawLine.indexOf(funcName);
|
|
||||||
ctx.addDiag(lineIdx, Math.max(0, idx), Math.max(0, idx) + funcName.length, `The return value of '${funcName}' is not used. Did you forget an assignment (:=)?`, vscode.DiagnosticSeverity.Warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { LinterRule } from './LinterRule';
|
|
||||||
import { LinterContext } from '../LinterContext';
|
|
||||||
import { workspaceIndex } from '../WorkspaceIndex';
|
|
||||||
|
|
||||||
export class SyntaxRule implements LinterRule {
|
|
||||||
|
|
||||||
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
|
||||||
this.checkTabs(rawLine, lineIdx, ctx);
|
|
||||||
|
|
||||||
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""');
|
|
||||||
const commentIdx = cleanLine.indexOf('//');
|
|
||||||
let codePart = commentIdx !== -1 ? cleanLine.substring(0, commentIdx).trim() : cleanLine.trim();
|
|
||||||
|
|
||||||
if (codePart.length === 0) {return;}
|
|
||||||
|
|
||||||
// --- MULTI-LINE IMPORT HANDLING & VALIDATION ---
|
|
||||||
if (codePart.startsWith('from ')) {
|
|
||||||
const match = codePart.match(/^from\s+([a-zA-Z0-9_.]+)\s+import/);
|
|
||||||
if (match) {
|
|
||||||
ctx.inImportBlock = true;
|
|
||||||
ctx.currentImportModule = match[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.inImportBlock) {
|
|
||||||
// Remove 'from X import' and any trailing semicolons
|
|
||||||
const importString = codePart.replace(/^from\s+[a-zA-Z0-9_.]+\s+import/, '').replace(/;$/, '');
|
|
||||||
const imports = importString.split(',').filter(i => i.trim().length > 0);
|
|
||||||
|
|
||||||
// Check against RAM index
|
|
||||||
const availableItems = workspaceIndex.getDefinitionBlocksForModule(ctx.currentImportModule);
|
|
||||||
|
|
||||||
// Only warn if we actually have the module in RAM, but the specific item is missing
|
|
||||||
if (availableItems.length > 0) {
|
|
||||||
for (const imp of imports) {
|
|
||||||
const cleanImp = imp.trim().split(/\s+/)[0]; // Trims version number (e.g. "HostToPoolMember 2.0" -> "HostToPoolMember")
|
|
||||||
if (cleanImp.length > 0 && !availableItems.includes(cleanImp)) {
|
|
||||||
const idx = rawLine.indexOf(cleanImp);
|
|
||||||
ctx.addDiag(lineIdx, Math.max(0, idx), Math.max(0, idx) + cleanImp.length, `The item '${cleanImp}' does not exist in module '${ctx.currentImportModule}'.`, vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the line ends with a semicolon, the import block is closed
|
|
||||||
if (codePart.endsWith(';')) {
|
|
||||||
ctx.inImportBlock = false;
|
|
||||||
ctx.currentImportModule = "";
|
|
||||||
}
|
|
||||||
return; // Skip all other syntax checks for lines inside an import block
|
|
||||||
}
|
|
||||||
// ------------------------------------------------
|
|
||||||
|
|
||||||
let isStructuralKeyword = /^(tpl|pattern|syncmapping|table|definitions?|define|enumeration|configuration|metadata|overview|constants|triggers|mapping|removal|body|end|on|tags|origin|author|products|publishers|tree_path|import|from)\b/.test(codePart);
|
|
||||||
|
|
||||||
if (codePart.startsWith('"') || codePart.startsWith("'")) {
|
|
||||||
isStructuralKeyword = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValidExecutionArea =
|
|
||||||
ctx.activePhaseName === 'body' ||
|
|
||||||
ctx.activePhaseName === 'triggers' ||
|
|
||||||
ctx.activePhaseName === 'constants' ||
|
|
||||||
ctx.activePhaseName === 'overview' ||
|
|
||||||
ctx.activePhaseName === 'mapping' ||
|
|
||||||
ctx.activePhaseName === 'metadata' ||
|
|
||||||
ctx.inModuleMetadata ||
|
|
||||||
ctx.inBlock === 'table' ||
|
|
||||||
ctx.inBlock === 'definitions' ||
|
|
||||||
ctx.openDefines.length > 0;
|
|
||||||
|
|
||||||
// 'then' and 'do' on standalone lines are legitimate code
|
|
||||||
if (!isStructuralKeyword && !isValidExecutionArea && !/^(then|do)\b/.test(codePart)) {
|
|
||||||
ctx.addDiag(lineIdx, 0, rawLine.length, "Executable code is not allowed here. (Allowed in: 'body', 'triggers', 'constants', 'overview', 'mapping', 'metadata', 'definitions', or 'define').", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.checkBrackets(codePart, rawLine, lineIdx, ctx);
|
|
||||||
this.checkAssignments(codePart, rawLine, lineIdx, ctx);
|
|
||||||
this.checkSemicolons(codePart, rawLine, lineIdx, ctx, line);
|
|
||||||
}
|
|
||||||
|
|
||||||
public checkEnd(lines: string[], ctx: LinterContext): void {
|
|
||||||
for (const unclosedLine of ctx.parenStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Opening parenthesis '(' is not closed!", vscode.DiagnosticSeverity.Error); }
|
|
||||||
for (const unclosedLine of ctx.bracketStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Opening bracket '[' is not closed!", 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, "Please replace tabs with spaces", vscode.DiagnosticSeverity.Warning);
|
|
||||||
diag.code = 'invalid_tab';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkBrackets(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
for (let charIdx = 0; charIdx < codePart.length; charIdx++) {
|
|
||||||
const char = codePart[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, "Unexpected closing parenthesis ')'. Missing an opening parenthesis.", 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, "Unexpected closing bracket ']'. Missing an opening bracket.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkAssignments(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
if (/\b(if|elif|where)\b/.test(codePart)) { ctx.inConditionBlock = true; }
|
|
||||||
|
|
||||||
if (codePart.includes('=') && !codePart.includes(':=') && !codePart.includes('>=') && !codePart.includes('<=')) {
|
|
||||||
if (!ctx.inConditionBlock && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && !/\b(matches)\b/.test(codePart)) {
|
|
||||||
const idx = rawLine.indexOf('=');
|
|
||||||
if (idx !== -1) {
|
|
||||||
ctx.addDiag(lineIdx, idx, idx + 1, "Invalid assignment. In TPL, ':=' is used for assignments.", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/\b(then|do)\b/.test(codePart) || codePart.endsWith(';')) { ctx.inConditionBlock = false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkSemicolons(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext, originalTrimmedLine: string) {
|
|
||||||
if (ctx.isClosingString) {
|
|
||||||
if (ctx.stringNeedsSemicolon && !originalTrimmedLine.endsWith(';')) {
|
|
||||||
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Missing semicolon at the end of the assignment.", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// "then" and "do" can stand alone without a semicolon requirement
|
|
||||||
const noSemicolonNeeded = /^(pattern|syncmapping|traverse|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|then\b|do\b|\/\/|\/\*|\*)/.test(codePart);
|
|
||||||
|
|
||||||
// "in" (for loops) is a valid line break
|
|
||||||
const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches|in)$/i.test(codePart);
|
|
||||||
|
|
||||||
const endsWithBlockOpener = /\b(then|do)$/.test(codePart);
|
|
||||||
|
|
||||||
if (!noSemicolonNeeded && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine && !endsWithBlockOpener) {
|
|
||||||
if (codePart.length > 0 && !codePart.endsWith(';') && !codePart.endsWith('{') && !codePart.endsWith(',')) {
|
|
||||||
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Missing semicolon at the end of the statement.", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
356
src/extension.ts
356
src/extension.ts
|
|
@ -1,71 +1,323 @@
|
||||||
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';
|
||||||
import { workspaceIndex } from './core/WorkspaceIndex';
|
|
||||||
import { TplDefinitionProvider } from './providers/TplDefinitionProvider';
|
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);
|
||||||
|
|
||||||
// LIVE INDEXING & LINTING
|
vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection));
|
||||||
vscode.workspace.onDidOpenTextDocument(doc => {
|
vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection));
|
||||||
if (doc.languageId === 'tpl') {
|
|
||||||
workspaceIndex.updateFile(doc.uri, doc.getText());
|
|
||||||
validateTplDocument(doc, diagnosticCollection);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
vscode.workspace.onDidChangeTextDocument(event => {
|
|
||||||
if (event.document.languageId === 'tpl') {
|
|
||||||
workspaceIndex.updateFile(event.document.uri, event.document.getText());
|
|
||||||
validateTplDocument(event.document, diagnosticCollection);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Aufräumen, wenn Dateien gelöscht werden
|
|
||||||
vscode.workspace.onDidDeleteFiles(event => {
|
|
||||||
for (const uri of event.files) {
|
|
||||||
workspaceIndex.removeFile(uri);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// 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')) {
|
||||||
scanWorkspace(diagnosticCollection);
|
for (const doc of vscode.workspace.textDocuments) {
|
||||||
|
validateTplDocument(doc, diagnosticCollection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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()));
|
||||||
context.subscriptions.push(vscode.languages.registerDefinitionProvider('tpl', new TplDefinitionProvider()));
|
|
||||||
|
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('tpl.scanWorkspace', () => {
|
for (const doc of vscode.workspace.textDocuments) {
|
||||||
scanWorkspace(diagnosticCollection);
|
validateTplDocument(doc, diagnosticCollection);
|
||||||
vscode.window.showInformationMessage("TPL: Workspace-Scan gestartet.");
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Initialer Scan beim Start
|
|
||||||
scanWorkspace(diagnosticCollection);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Füttert RAM und Linter
|
|
||||||
async function scanWorkspace(collection: vscode.DiagnosticCollection) {
|
|
||||||
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const doc = await vscode.workspace.openTextDocument(file);
|
|
||||||
|
|
||||||
// ZUERST in den RAM laden...
|
|
||||||
workspaceIndex.updateFile(doc.uri, doc.getText());
|
|
||||||
|
|
||||||
// ... DANN linten
|
|
||||||
validateTplDocument(doc, collection);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,37 +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() {
|
|
||||||
this.strategies = [
|
|
||||||
new BuiltInCompletion(),
|
|
||||||
new SnippetCompletion(),
|
|
||||||
new LocalContextCompletion()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async provideCompletionItems(
|
|
||||||
document: vscode.TextDocument,
|
|
||||||
position: vscode.Position,
|
|
||||||
token: vscode.CancellationToken,
|
|
||||||
context: vscode.CompletionContext
|
|
||||||
): Promise<vscode.CompletionItem[]> {
|
|
||||||
|
|
||||||
const linePrefix = document.lineAt(position).text.substring(0, position.character);
|
|
||||||
const results: vscode.CompletionItem[] = [];
|
|
||||||
|
|
||||||
for (const strategy of this.strategies) {
|
|
||||||
const items = await strategy.provideCompletions(document, position, linePrefix);
|
|
||||||
if (items && items.length > 0) {
|
|
||||||
results.push(...items);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { workspaceIndex } from '../core/WorkspaceIndex';
|
|
||||||
|
|
||||||
export class TplDefinitionProvider implements vscode.DefinitionProvider {
|
|
||||||
public async provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | null> {
|
|
||||||
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
|
|
||||||
if (!range) {return null;}
|
|
||||||
|
|
||||||
const word = document.getText(range);
|
|
||||||
const parts = word.split('.');
|
|
||||||
const funcName = parts.length > 1 ? parts[1] : word;
|
|
||||||
|
|
||||||
const text = document.getText();
|
|
||||||
|
|
||||||
// 1. LOKALE SUCHE: Ist die Funktion in der gleichen Datei?
|
|
||||||
const lines = text.split('\n');
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const match = lines[i].match(new RegExp(`^\\s*define\\s+${funcName}\\b`));
|
|
||||||
if (match) {
|
|
||||||
// Wir haben sie lokal gefunden! Springe exakt dorthin.
|
|
||||||
return new vscode.Location(document.uri, new vscode.Position(i, lines[i].indexOf(funcName)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. GLOBALE SUCHE: Den RAM-Index nach anderen Modulen fragen
|
|
||||||
const signatures = workspaceIndex.getFunctionSignatures(funcName);
|
|
||||||
|
|
||||||
if (parts.length > 1) {
|
|
||||||
const prefix = parts[0];
|
|
||||||
// Wir schauen, ob es über from X import <prefix> importiert wurde
|
|
||||||
const importRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+[\\s\\S]*?\\b${prefix}\\b`);
|
|
||||||
const match = importRegex.exec(text);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const moduleName = match[1];
|
|
||||||
const targetFunc = signatures.find(sig => sig.moduleName === moduleName && sig.name.endsWith(`.${funcName}`));
|
|
||||||
if (targetFunc && targetFunc.fsPath) {
|
|
||||||
const uri = vscode.Uri.file(targetFunc.fsPath);
|
|
||||||
return new vscode.Location(uri, new vscode.Position(targetFunc.lineIdx, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. FALLBACK: Wenn es nicht als 'prefix.funktion' stand, aber eindeutig im Workspace ist
|
|
||||||
if (signatures.length > 0 && signatures[0].fsPath) {
|
|
||||||
const uri = vscode.Uri.file(signatures[0].fsPath);
|
|
||||||
return new vscode.Location(uri, new vscode.Position(signatures[0].lineIdx, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wenn es eine Built-in Methode ist (wie discovery.runCommand), springt er nirgends hin
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,39 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { TplLookup } from '../core/TplLookup';
|
|
||||||
import { workspaceIndex } from '../core/WorkspaceIndex';
|
|
||||||
|
|
||||||
export class TplHoverProvider implements vscode.HoverProvider {
|
|
||||||
public provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult<vscode.Hover> {
|
|
||||||
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
|
|
||||||
if (!range) {return null;}
|
|
||||||
|
|
||||||
const word = document.getText(range);
|
|
||||||
const parts = word.split('.');
|
|
||||||
|
|
||||||
if (parts.length > 1) {
|
|
||||||
// 1. Suche in Core & TKU Built-Ins (Case-Insensitive)
|
|
||||||
const funcData = TplLookup.getBuiltInFunction(parts[0], parts[1]);
|
|
||||||
if (funcData) {
|
|
||||||
const md = new vscode.MarkdownString();
|
|
||||||
md.appendCodeblock(`${parts[0]}.${parts[1]}(${funcData.snippets?.[0] || ''})`, 'tpl');
|
|
||||||
md.appendMarkdown(`---\n${funcData.description}`);
|
|
||||||
return new vscode.Hover(md);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Suche in lokalem Workspace (RAM Index)
|
|
||||||
const funcName = parts.length > 1 ? parts[1] : word;
|
|
||||||
const signatures = workspaceIndex.getFunctionSignatures(funcName);
|
|
||||||
if (signatures.length > 0) {
|
|
||||||
const sig = signatures[0];
|
|
||||||
const md = new vscode.MarkdownString();
|
|
||||||
md.appendCodeblock(`define ${sig.name}(${sig.params.join(', ')})`, 'tpl');
|
|
||||||
if (sig.docString) {
|
|
||||||
md.appendMarkdown(`---\n${sig.docString}`);
|
|
||||||
}
|
|
||||||
return new vscode.Hover(md);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
|
|
||||||
export class LogFix {
|
|
||||||
public diagnosticCode = 'missing_log_start';
|
|
||||||
|
|
||||||
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
|
||||||
const action = new vscode.CodeAction("Insert missing log statement", vscode.CodeActionKind.QuickFix);
|
|
||||||
action.edit = new vscode.WorkspaceEdit();
|
|
||||||
|
|
||||||
const expectedLog = diagnostic.source;
|
|
||||||
|
|
||||||
if (expectedLog) {
|
|
||||||
const insertPos = new vscode.Position(diagnostic.range.start.line + 1, 0);
|
|
||||||
// 8 Leerzeichen Einrückung für den Body-Inhalt
|
|
||||||
action.edit.insert(document.uri, insertPos, ` ${expectedLog}\n`);
|
|
||||||
action.diagnostics = [diagnostic];
|
|
||||||
action.isPreferred = true;
|
|
||||||
return [action];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +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();
|
|
||||||
|
|
||||||
const config = vscode.workspace.getConfiguration('tpl');
|
|
||||||
const author = config.get<string>('metadata.author', 'default');
|
|
||||||
const origin = config.get<string>('metadata.origin', 'Custom');
|
|
||||||
const publisher = config.get<string>('metadata.publisher', 'IT-Operations');
|
|
||||||
const treePathBase = config.get<string>('metadata.treePathBase', 'default');
|
|
||||||
|
|
||||||
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`;}
|
|
||||||
|
|
||||||
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText);
|
|
||||||
fix.diagnostics = [diagnostic];
|
|
||||||
|
|
||||||
return [fix];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
|
|
||||||
export class TabFix {
|
|
||||||
public diagnosticCode = 'invalid_tab';
|
|
||||||
|
|
||||||
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
|
||||||
const action = new vscode.CodeAction("Convert tabs to 4 spaces", vscode.CodeActionKind.QuickFix);
|
|
||||||
action.edit = new vscode.WorkspaceEdit();
|
|
||||||
|
|
||||||
const line = document.lineAt(diagnostic.range.start.line);
|
|
||||||
const newText = line.text.replace(/\t/g, ' ');
|
|
||||||
|
|
||||||
action.edit.replace(document.uri, line.range, newText);
|
|
||||||
action.diagnostics = [diagnostic];
|
|
||||||
action.isPreferred = true;
|
|
||||||
|
|
||||||
return [action];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,57 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { CompletionStrategy } from './CompletionStrategy';
|
|
||||||
import { TplLookup } from '../../core/TplLookup';
|
|
||||||
|
|
||||||
export class BuiltInCompletion implements CompletionStrategy {
|
|
||||||
public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise<vscode.CompletionItem[] | undefined> {
|
|
||||||
const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\.([a-zA-Z0-9_]*)$/);
|
|
||||||
|
|
||||||
if (dotMatch) {
|
|
||||||
const prefix = dotMatch[1];
|
|
||||||
const items: vscode.CompletionItem[] = [];
|
|
||||||
let isHandled = false;
|
|
||||||
|
|
||||||
// NEU: Case-Insensitive Lookup (findet Core UND TKU Methoden!)
|
|
||||||
const methods = TplLookup.getAllMethodsForNamespace(prefix);
|
|
||||||
|
|
||||||
if (methods) {
|
|
||||||
isHandled = true;
|
|
||||||
for (const [funcName, funcData] of Object.entries(methods)) {
|
|
||||||
const item = new vscode.CompletionItem(funcName, vscode.CompletionItemKind.Method);
|
|
||||||
item.detail = `TPL Built-in: ${prefix}.${funcName}`;
|
|
||||||
item.documentation = new vscode.MarkdownString(funcData.description);
|
|
||||||
|
|
||||||
if (funcData.snippets && funcData.snippets.length > 0) {
|
|
||||||
item.insertText = new vscode.SnippetString(`${funcName}(${funcData.snippets[0]});$0`);
|
|
||||||
} else {
|
|
||||||
item.insertText = new vscode.SnippetString(`${funcName}();$0`);
|
|
||||||
}
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamische Konstruktoren für 'model', 'model.rel' etc. bleiben hier unverändert...
|
|
||||||
if (prefix.toLowerCase() === 'model') {
|
|
||||||
isHandled = true;
|
|
||||||
const dynamicItem = new vscode.CompletionItem('<NodeKind>', vscode.CompletionItemKind.Constructor);
|
|
||||||
dynamicItem.detail = "Dynamic Taxonomy Node";
|
|
||||||
dynamicItem.documentation = new vscode.MarkdownString("The `model` scope is automatically populated with a function named after each node kind defined in the taxonomy.");
|
|
||||||
dynamicItem.insertText = new vscode.SnippetString("${1:NodeKind}(key := ${2:key_value});$0");
|
|
||||||
items.push(dynamicItem);
|
|
||||||
}
|
|
||||||
else if (prefix.toLowerCase() === 'model.rel' || prefix.toLowerCase() === 'model.uniquerel') {
|
|
||||||
isHandled = true;
|
|
||||||
const dynamicItem = new vscode.CompletionItem('<Relationship>', vscode.CompletionItemKind.Constructor);
|
|
||||||
dynamicItem.detail = prefix.toLowerCase() === 'model.rel' ? "Dynamic Taxonomy Relationship" : "Dynamic Unique Taxonomy Relationship";
|
|
||||||
dynamicItem.documentation = new vscode.MarkdownString(`The \`${prefix}\` scope is automatically populated with a function named after each relationship defined in the taxonomy.`);
|
|
||||||
dynamicItem.insertText = new vscode.SnippetString("${1:RelationshipName}(${2:SourceRole} := ${3:source_node}, ${4:DestinationRole} := ${5:dest_node});$0");
|
|
||||||
items.push(dynamicItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isHandled) {
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
|
|
||||||
export interface CompletionStrategy {
|
|
||||||
provideCompletions(
|
|
||||||
document: vscode.TextDocument,
|
|
||||||
position: vscode.Position,
|
|
||||||
linePrefix: string
|
|
||||||
): vscode.ProviderResult<vscode.CompletionItem[] | undefined>;
|
|
||||||
}
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { CompletionStrategy } from './CompletionStrategy';
|
|
||||||
import { tplBuiltIns } from '../../data/TplBuiltIns';
|
|
||||||
import { workspaceIndex } from '../../core/WorkspaceIndex';
|
|
||||||
|
|
||||||
export class LocalContextCompletion implements CompletionStrategy {
|
|
||||||
public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise<vscode.CompletionItem[] | undefined> {
|
|
||||||
const text = document.getText();
|
|
||||||
const items: vscode.CompletionItem[] = [];
|
|
||||||
|
|
||||||
const textBeforeCursor = document.getText(new vscode.Range(new vscode.Position(0, 0), position));
|
|
||||||
const lastFromIndex = textBeforeCursor.lastIndexOf('from ');
|
|
||||||
const lastSemicolonIndex = textBeforeCursor.lastIndexOf(';');
|
|
||||||
|
|
||||||
// 1 & 2. MULTI-LINE SMART IMPORT
|
|
||||||
// Wenn das letzte 'from' NACH dem letzten Semikolon kam, sind wir mitten in einem Import-Block!
|
|
||||||
if (lastFromIndex > lastSemicolonIndex || (lastFromIndex !== -1 && lastSemicolonIndex === -1)) {
|
|
||||||
const importContext = textBeforeCursor.substring(lastFromIndex);
|
|
||||||
const match = importContext.match(/^from\s+([a-zA-Z0-9_.]+)\s+import/);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
// Wir tippen noch den Modul-Namen
|
|
||||||
for (const mod of workspaceIndex.getAllModules()) {
|
|
||||||
const item = new vscode.CompletionItem(mod, vscode.CompletionItemKind.Module);
|
|
||||||
item.detail = "Workspace Module";
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
} else {
|
|
||||||
// Wir tippen die zu importierenden Klassen/Patterns (auch nach Zeilenumbrüchen!)
|
|
||||||
const modName = match[1];
|
|
||||||
for (const defBlock of workspaceIndex.getDefinitionBlocksForModule(modName)) {
|
|
||||||
const item = new vscode.CompletionItem(defBlock, vscode.CompletionItemKind.Class);
|
|
||||||
item.detail = `Available in ${modName}`;
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
return items; // Nichts anderes vorschlagen!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. NAMESPACE CALLS: "prefix."
|
|
||||||
const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)$/);
|
|
||||||
if (dotMatch) {
|
|
||||||
const prefix = dotMatch[1];
|
|
||||||
|
|
||||||
// BMC Built-ins ignorieren (übernimmt BuiltInCompletion)
|
|
||||||
const bmcNamespaces = Object.keys(tplBuiltIns);
|
|
||||||
if (bmcNamespaces.includes(prefix)) {
|
|
||||||
return undefined; // Hier darf undefined sein, weil BuiltInCompletion das fängt
|
|
||||||
}
|
|
||||||
|
|
||||||
let funcs = [];
|
|
||||||
|
|
||||||
// A) RAM-Abfrage für importierte Module
|
|
||||||
const importRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+[\\s\\S]*?\\b${prefix}\\b`);
|
|
||||||
const match = importRegex.exec(text);
|
|
||||||
if (match) {
|
|
||||||
const moduleName = match[1];
|
|
||||||
// Filtere Funktionen aus dem importierten Modul, die zu diesem Prefix gehören
|
|
||||||
funcs = workspaceIndex.getFunctionsForModule(moduleName).filter(f => f.name.startsWith(`${prefix}.`) || !f.name.includes('.'));
|
|
||||||
} else {
|
|
||||||
// B) Lokale Definitions in der aktuellen Datei prüfen!
|
|
||||||
const localDefRegex = new RegExp(`^\\s*definitions?\\s+${prefix}\\b`, 'm');
|
|
||||||
if (localDefRegex.test(text)) {
|
|
||||||
const lines = text.split('\n');
|
|
||||||
let inTargetBlock = false;
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.match(new RegExp(`^\\s*definitions?\\s+${prefix}\\b`))) { inTargetBlock = true; continue; }
|
|
||||||
if (line.match(/^\s*end\s+definitions?/)) { inTargetBlock = false; continue; }
|
|
||||||
if (inTargetBlock) {
|
|
||||||
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/);
|
|
||||||
if (defineMatch) {
|
|
||||||
const params = (defineMatch[2] || "").split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
||||||
funcs.push({ name: defineMatch[1], moduleName: 'local', params: params });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (funcs.length > 0) {
|
|
||||||
for (const f of funcs) {
|
|
||||||
const shortName = f.name.split('.').pop() || f.name;
|
|
||||||
const item = new vscode.CompletionItem(shortName, vscode.CompletionItemKind.Function);
|
|
||||||
item.detail = f.moduleName === 'local' ? `Local Method` : `Method from ${f.moduleName}`;
|
|
||||||
|
|
||||||
// Dynamische Parameter-Generierung! Baut z.B. ${1:host}, ${2:ip}
|
|
||||||
let paramSnippet = "";
|
|
||||||
if (f.params && f.params.length > 0) {
|
|
||||||
paramSnippet = f.params.map((p, idx) => `\${${idx + 1}:${p}}`).join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
item.insertText = new vscode.SnippetString(`${shortName}(${paramSnippet});$0`);
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
return items; // Gibt exakt die gefundenen Funktionen zurück
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wenn es ein unbekannter Namespace mit Punkt ist, geben wir ein leeres Array [] zurück!
|
|
||||||
// Das zwingt VS Code dazu, KEINE generischen Textwörter vorzuschlagen.
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. LOKALE DEFINITIONS PARSEN (Für den globalen Scope, noch ohne Punkt aufgerufen)
|
|
||||||
const lines = text.split('\n');
|
|
||||||
let currentDefBlock = "";
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
const defBlockMatch = line.match(/^\s*definitions?\s+([a-zA-Z0-9_]+)/);
|
|
||||||
if (defBlockMatch) { currentDefBlock = defBlockMatch[1]; continue; }
|
|
||||||
if (line.match(/^\s*end\s+definitions?/)) { currentDefBlock = ""; continue; }
|
|
||||||
|
|
||||||
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/);
|
|
||||||
if (defineMatch) {
|
|
||||||
const funcName = defineMatch[1];
|
|
||||||
const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName;
|
|
||||||
const item = new vscode.CompletionItem(fullName, vscode.CompletionItemKind.Function);
|
|
||||||
item.detail = currentDefBlock ? `Local Function in '${currentDefBlock}'` : `Local Function`;
|
|
||||||
|
|
||||||
// Dynamische Parameter-Generierung
|
|
||||||
const params = (defineMatch[2] || "").split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
||||||
let paramSnippet = "";
|
|
||||||
if (params.length > 0) {
|
|
||||||
paramSnippet = params.map((p, idx) => `\${${idx + 1}:${p}}`).join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
item.insertText = new vscode.SnippetString(`${fullName}(${paramSnippet});$0`);
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import-Referenzen für den globalen Scope bereitstellen (angepasst für Multi-Line)
|
|
||||||
const fromImportRegex = /from\s+([a-zA-Z0-9_.]+)\s+import\s+([\s\S]*?);/g;
|
|
||||||
let matchVar;
|
|
||||||
while ((matchVar = fromImportRegex.exec(text)) !== null) {
|
|
||||||
const moduleName = matchVar[1];
|
|
||||||
const imports = matchVar[2].split(',').map(i => i.trim().split(/\s+/)[0]);
|
|
||||||
|
|
||||||
for (const imp of imports) {
|
|
||||||
if (imp.length > 0) {
|
|
||||||
const item = new vscode.CompletionItem(imp, vscode.CompletionItemKind.Module);
|
|
||||||
item.detail = `Imported from ${moduleName}`;
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lokale Variablen (:=) vorschlagen
|
|
||||||
const varRegex = /([a-zA-Z0-9_]+)\s*:=/g;
|
|
||||||
const foundVars = new Set<string>();
|
|
||||||
while ((matchVar = varRegex.exec(text)) !== null) {
|
|
||||||
if (matchVar[1] !== 'host') { foundVars.add(matchVar[1]); }
|
|
||||||
}
|
|
||||||
for (const v of foundVars) {
|
|
||||||
items.push(new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable));
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { CompletionStrategy } from './CompletionStrategy';
|
|
||||||
|
|
||||||
export class SnippetCompletion implements CompletionStrategy {
|
|
||||||
public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise<vscode.CompletionItem[] | undefined> {
|
|
||||||
if (linePrefix.match(/\.[a-zA-Z0-9_]*$/)) { return undefined; }
|
|
||||||
|
|
||||||
const items: vscode.CompletionItem[] = [];
|
|
||||||
|
|
||||||
const textBeforeCursor = document.getText(new vscode.Range(new vscode.Position(0, 0), position));
|
|
||||||
const inPattern = textBeforeCursor.lastIndexOf('pattern ') > textBeforeCursor.lastIndexOf('end pattern;');
|
|
||||||
const inSyncmapping = textBeforeCursor.lastIndexOf('syncmapping ') > textBeforeCursor.lastIndexOf('end syncmapping;');
|
|
||||||
const inBlock = inPattern || inSyncmapping;
|
|
||||||
|
|
||||||
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 version = config.get<string>('version', '23');
|
|
||||||
|
|
||||||
// Dynamischer Tree-Path
|
|
||||||
const minLevels = config.get<number>('metadata.treePath.minLevels', 3);
|
|
||||||
const staticNodes = config.get<string[]>('metadata.treePath.staticNodes', ['Grafing']);
|
|
||||||
|
|
||||||
let treePathStr = '';
|
|
||||||
let snippetTabIdx = 1;
|
|
||||||
|
|
||||||
for (let i = 0; i < minLevels; i++) {
|
|
||||||
if (i < staticNodes.length) {
|
|
||||||
treePathStr += `'${staticNodes[i]}'`;
|
|
||||||
} else {
|
|
||||||
treePathStr += `'\${${snippetTabIdx}:Level_${i+1}}'`;
|
|
||||||
snippetTabIdx++;
|
|
||||||
}
|
|
||||||
if (i < minLevels - 1) {treePathStr += ', ';}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamischer Modul-Metadata-Block
|
|
||||||
let metadataBlock = 'metadata\n';
|
|
||||||
if (config.get<boolean>('metadata.require.origin', true)) {metadataBlock += ` origin := '${origin}';\n`;}
|
|
||||||
if (config.get<boolean>('metadata.require.treePath', true)) {metadataBlock += ` tree_path := ${treePathStr};\n`;}
|
|
||||||
if (config.get<boolean>('metadata.require.products', true)) {metadataBlock += ` products := '\${${snippetTabIdx++}:Product}';\n`;}
|
|
||||||
if (config.get<boolean>('metadata.require.publishers', true)) {metadataBlock += ` publishers := '${publisher}';\n`;}
|
|
||||||
if (config.get<boolean>('metadata.require.author', true)) {metadataBlock += ` author := '${author}';\n`;}
|
|
||||||
metadataBlock += 'end metadata;';
|
|
||||||
|
|
||||||
if (!inBlock) {
|
|
||||||
const skeleton = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
|
|
||||||
skeleton.detail = "Generiert ein frisches TPL Modul";
|
|
||||||
skeleton.insertText = new vscode.SnippetString(
|
|
||||||
`tpl 1.\${${snippetTabIdx++}:${version}} module \${${snippetTabIdx++}:MyModule};\n\n` +
|
|
||||||
`${metadataBlock}\n\n` +
|
|
||||||
`pattern \${${snippetTabIdx++}:MyPattern} 1.0\n` +
|
|
||||||
` """\n \${${snippetTabIdx++}:Beschreibung des Patterns}\n """\n` +
|
|
||||||
` metadata\n` +
|
|
||||||
` publishers := '${publisher}';\n` +
|
|
||||||
` tags := '\${${snippetTabIdx++}:Tags}';\n` +
|
|
||||||
` end metadata;\n\n` +
|
|
||||||
` overview\n` +
|
|
||||||
` tags \${${snippetTabIdx++}:Tags};\n` +
|
|
||||||
` end overview;\n\n` +
|
|
||||||
` constants\n` +
|
|
||||||
` \${${snippetTabIdx++}:const_name} := '\${${snippetTabIdx++}:value}';\n` +
|
|
||||||
` end constants;\n\n` +
|
|
||||||
` triggers\n` +
|
|
||||||
` on process := SoftwareInstance created, confirmed where type = '\${${snippetTabIdx++}:Type}';\n` +
|
|
||||||
` end triggers;\n\n` +
|
|
||||||
` body\n` +
|
|
||||||
` log.debug("Start pattern %pattern_name%");\n` +
|
|
||||||
` $0\n` +
|
|
||||||
` end body;\n` +
|
|
||||||
`end pattern;`
|
|
||||||
);
|
|
||||||
items.push(skeleton);
|
|
||||||
|
|
||||||
const modMeta = new vscode.CompletionItem('metadata', vscode.CompletionItemKind.Snippet);
|
|
||||||
modMeta.detail = "Modul-Metadaten Block (Pflichtfelder)";
|
|
||||||
modMeta.insertText = new vscode.SnippetString(`${metadataBlock}\n$0`);
|
|
||||||
items.push(modMeta);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inBlock) {
|
|
||||||
const patMeta = new vscode.CompletionItem('metadata', vscode.CompletionItemKind.Snippet);
|
|
||||||
patMeta.detail = "Pattern-Metadaten Block";
|
|
||||||
patMeta.insertText = new vscode.SnippetString(
|
|
||||||
`metadata\n` +
|
|
||||||
` publishers := '${publisher}';\n` +
|
|
||||||
` tags := '\${1:Tags}';\n` +
|
|
||||||
`end metadata;\n$0`
|
|
||||||
);
|
|
||||||
items.push(patMeta);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ifSnippet = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet);
|
|
||||||
ifSnippet.insertText = new vscode.SnippetString(`if \${1:condition} then\n $0\nend if;`);
|
|
||||||
items.push(ifSnippet);
|
|
||||||
|
|
||||||
const elifSnippet = new vscode.CompletionItem('elif', vscode.CompletionItemKind.Snippet);
|
|
||||||
elifSnippet.insertText = new vscode.SnippetString(`elif \${1:condition} then\n $0`);
|
|
||||||
items.push(elifSnippet);
|
|
||||||
|
|
||||||
const elseSnippet = new vscode.CompletionItem('else', vscode.CompletionItemKind.Snippet);
|
|
||||||
elseSnippet.insertText = new vscode.SnippetString(`else\n $0`);
|
|
||||||
items.push(elseSnippet);
|
|
||||||
|
|
||||||
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;`);
|
|
||||||
items.push(forSnippet);
|
|
||||||
|
|
||||||
const whileSnippet = new vscode.CompletionItem('while', vscode.CompletionItemKind.Snippet);
|
|
||||||
whileSnippet.insertText = new vscode.SnippetString(`while \${1:condition} do\n $0\nend while;`);
|
|
||||||
items.push(whileSnippet);
|
|
||||||
|
|
||||||
const defineSnippet = new vscode.CompletionItem('define', vscode.CompletionItemKind.Snippet);
|
|
||||||
defineSnippet.insertText = new vscode.SnippetString(`define \${1:name}(\${2:params})\n """\n \${3:DocString}\n """\n $0\nend define;`);
|
|
||||||
items.push(defineSnippet);
|
|
||||||
|
|
||||||
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,15 +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[] = [];
|
|
||||||
|
|
||||||
public inImportBlock: boolean = false;
|
|
||||||
|
|
||||||
// Gibt die aktuellen Basis-Leerzeichen zurück (z.B. Level 2 = 8 Leerzeichen)
|
|
||||||
public get currentIndentString(): string {
|
|
||||||
return ' '.repeat(this.indentLevel * this.tabSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
import { FormatState } from './FormatState';
|
|
||||||
|
|
||||||
export class IndentationRules {
|
|
||||||
public apply(line: string, state: FormatState): string {
|
|
||||||
if (line.trim().length === 0) {return "";}
|
|
||||||
|
|
||||||
let cleanLine = "";
|
|
||||||
let inString = false;
|
|
||||||
let stringChar = '';
|
|
||||||
let inTripleQuote = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < line.length; i++) {
|
|
||||||
const char = line[i];
|
|
||||||
if (!inString && !inTripleQuote && char === '/' && line[i+1] === '/') {break;}
|
|
||||||
|
|
||||||
if (!inString && !inTripleQuote && (char === '"' || char === "'") && line[i+1] === char && line[i+2] === char) {
|
|
||||||
inTripleQuote = true; stringChar = char; cleanLine += char+char+char; i += 2; continue;
|
|
||||||
}
|
|
||||||
if (inTripleQuote) {
|
|
||||||
if (char === stringChar && line[i+1] === stringChar && line[i+2] === stringChar) {
|
|
||||||
inTripleQuote = false; cleanLine += char+char+char; i += 2;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (inString) {
|
|
||||||
if (char === '\\') {i++;} else if (char === stringChar) {inString = false;}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!inString && !inTripleQuote && (char === '"' || char === "'")) {
|
|
||||||
inString = true; stringChar = char; cleanLine += char; continue;
|
|
||||||
}
|
|
||||||
cleanLine += char;
|
|
||||||
}
|
|
||||||
cleanLine = cleanLine.trim();
|
|
||||||
|
|
||||||
const trimmedLine = line.trimStart();
|
|
||||||
|
|
||||||
// 3. DEDENT
|
|
||||||
if (/^end\b/.test(cleanLine)) {
|
|
||||||
state.indentLevel = Math.max(0, state.indentLevel - 1);
|
|
||||||
} else if (/^(elif|else)\b/.test(cleanLine)) {
|
|
||||||
state.indentLevel = Math.max(0, state.indentLevel - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. SET SPACES
|
|
||||||
let result = trimmedLine;
|
|
||||||
|
|
||||||
if (state.parenAlignments && state.parenAlignments.length > 0) {
|
|
||||||
// Visual Alignment greift später
|
|
||||||
} else if (state.inImportBlock) {
|
|
||||||
if (!cleanLine.startsWith('from ')) {
|
|
||||||
result = " ".repeat((state.indentLevel + 2) * state.tabSize) + trimmedLine;
|
|
||||||
} else {
|
|
||||||
result = state.currentIndentString + trimmedLine;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result = state.currentIndentString + trimmedLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. INDENT (Gefixt: Mutually Exclusive / else if)
|
|
||||||
if (/^(pattern|syncmapping|table|definitions?|define|enumeration|configuration|overview|metadata|constants|triggers|body|mapping|removal)\b/.test(cleanLine)) {
|
|
||||||
state.indentLevel++;
|
|
||||||
} else if (/\b(then|do)\b$/.test(cleanLine)) {
|
|
||||||
state.indentLevel++;
|
|
||||||
} else if (/^else\b/.test(cleanLine)) {
|
|
||||||
state.indentLevel++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Import Block Tracking
|
|
||||||
if (cleanLine.startsWith('from ')) {
|
|
||||||
state.inImportBlock = true;
|
|
||||||
}
|
|
||||||
if (state.inImportBlock && cleanLine.endsWith(';')) {
|
|
||||||
state.inImportBlock = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,9 +0,0 @@
|
||||||
tpl 1.21 module TestFormat;
|
|
||||||
|
|
||||||
define format_me()
|
|
||||||
// 6 Leerzeichen hier!
|
|
||||||
log.debug("2 Leerzeichen hier");
|
|
||||||
if true then
|
|
||||||
log.debug("3 Leerzeichen!");
|
|
||||||
end if;
|
|
||||||
end define;
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
tpl 1.21 module TestEdgeCases;
|
|
||||||
|
|
||||||
define my_edge_func()
|
|
||||||
"""
|
|
||||||
Ein böser DocString mit ( [ = und if und end
|
|
||||||
"""
|
|
||||||
// Ein Kommentar mit DiscoveryFunctions.escapePath(1,2,3,4,5,6)
|
|
||||||
|
|
||||||
clean_path := DiscoveryFunctions.escapePath(host, "böse(Klammern)", true);
|
|
||||||
|
|
||||||
if test = "ein_string_mit_komma," then
|
|
||||||
log.debug("alles gut");
|
|
||||||
end if;
|
|
||||||
end define;
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
tpl 1.21 module TestNegative;
|
|
||||||
|
|
||||||
define my_bad_func()
|
|
||||||
// Unbekannte Funktion (Info)
|
|
||||||
rdbms_functions.does_not_exist(host);
|
|
||||||
|
|
||||||
// Falsche Parameter-Anzahl (Warning)
|
|
||||||
DiscoveryFunctions.escapePath(host, "path", true, "zu_viel");
|
|
||||||
|
|
||||||
// Zuweisung fehlt für Return-Value (Warning)
|
|
||||||
DiscoveryFunctions.escapePath(host, "path", true);
|
|
||||||
end define;
|
|
||||||
Binary file not shown.
|
|
@ -1,38 +0,0 @@
|
||||||
import * as assert from 'assert';
|
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { TabFix } from '../../providers/actions/TabFix';
|
|
||||||
import { LogFix } from '../../providers/actions/LogFix';
|
|
||||||
|
|
||||||
suite('Code Actions Test Suite', () => {
|
|
||||||
test('TabFix: Generates Replace-Edit for tabs', () => {
|
|
||||||
const fix = new TabFix();
|
|
||||||
const diag = new vscode.Diagnostic(new vscode.Range(0, 0, 0, 1), "Tab found", vscode.DiagnosticSeverity.Warning);
|
|
||||||
|
|
||||||
// Mock for TextDocument
|
|
||||||
const mockDoc = {
|
|
||||||
uri: vscode.Uri.file('test.tpl'),
|
|
||||||
lineAt: () => ({ text: '\t\tcode', range: new vscode.Range(0, 0, 0, 6) })
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const actions = fix.provideFix(mockDoc, diag);
|
|
||||||
assert.strictEqual(actions.length, 1);
|
|
||||||
|
|
||||||
// Update this string if your actual TabFix title differs!
|
|
||||||
assert.strictEqual(actions[0].title, "Convert tabs to 4 spaces");
|
|
||||||
assert.ok(actions[0].edit !== undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('LogFix: Generates Insert-Edit with log statement', () => {
|
|
||||||
const fix = new LogFix();
|
|
||||||
const diag = new vscode.Diagnostic(new vscode.Range(10, 0, 10, 4), "Missing log");
|
|
||||||
diag.source = 'log.debug("Start pattern Test");'; // Linter-Source Simulation
|
|
||||||
|
|
||||||
const mockDoc = { uri: vscode.Uri.file('test.tpl') } as any;
|
|
||||||
|
|
||||||
const actions = fix.provideFix(mockDoc, diag);
|
|
||||||
assert.strictEqual(actions.length, 1);
|
|
||||||
|
|
||||||
// Update this string if your actual LogFix title differs!
|
|
||||||
assert.strictEqual(actions[0].title, "Insert missing log statement");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
import * as assert from 'assert';
|
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { SnippetCompletion } from '../../providers/completions/SnippetCompletion';
|
|
||||||
import { LocalContextCompletion } from '../../providers/completions/LocalContextCompletion';
|
|
||||||
import { workspaceIndex } from '../../core/WorkspaceIndex';
|
|
||||||
|
|
||||||
function createMockDoc(text: string): any {
|
|
||||||
const lines = text.split('\n');
|
|
||||||
return {
|
|
||||||
getText: () => text,
|
|
||||||
lineAt: (i: number) => ({ text: lines[i] })
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
suite('Completion Provider Test Suite', () => {
|
|
||||||
|
|
||||||
// Feeds the RAM index with a dummy file before tests
|
|
||||||
suiteSetup(() => {
|
|
||||||
const dummyUri = vscode.Uri.file('/fake/path/Company.tools.tpl');
|
|
||||||
const dummyText = `
|
|
||||||
tpl 1.23 module Company.tools;
|
|
||||||
definitions network_helpers
|
|
||||||
define ping(host, ip)
|
|
||||||
end define;
|
|
||||||
end definitions;
|
|
||||||
`.trim();
|
|
||||||
workspaceIndex.updateFile(dummyUri, dummyText);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('LocalContextCompletion: "from " suggests only RAM modules', async () => {
|
|
||||||
const strategy = new LocalContextCompletion();
|
|
||||||
const mockDoc = createMockDoc(`from `);
|
|
||||||
const position = new vscode.Position(0, 5);
|
|
||||||
|
|
||||||
const result = await strategy.provideCompletions(mockDoc, position, 'from ');
|
|
||||||
|
|
||||||
assert.ok(result !== undefined);
|
|
||||||
const hasModule = result!.some(item => item.label === 'Company.tools');
|
|
||||||
assert.strictEqual(hasModule, true, "Must suggest the module from RAM.");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('LocalContextCompletion: "from X import " suggests definition blocks', async () => {
|
|
||||||
const strategy = new LocalContextCompletion();
|
|
||||||
const mockDoc = createMockDoc(`from Company.tools import `);
|
|
||||||
const position = new vscode.Position(0, 26);
|
|
||||||
|
|
||||||
const result = await strategy.provideCompletions(mockDoc, position, 'from Company.tools import ');
|
|
||||||
|
|
||||||
assert.ok(result !== undefined);
|
|
||||||
const hasBlock = result!.some(item => item.label === 'network_helpers');
|
|
||||||
assert.strictEqual(hasBlock, true, "Must suggest the definition block from the module.");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('LocalContextCompletion: Blocks fallback for unknown namespace', async () => {
|
|
||||||
const strategy = new LocalContextCompletion();
|
|
||||||
const mockDoc = createMockDoc(`randomstuff.`);
|
|
||||||
const position = new vscode.Position(0, 12);
|
|
||||||
|
|
||||||
const result = await strategy.provideCompletions(mockDoc, position, 'randomstuff.');
|
|
||||||
|
|
||||||
assert.ok(result !== undefined);
|
|
||||||
assert.strictEqual(result!.length, 0, "Must return an empty array to block VS Code text spam.");
|
|
||||||
});
|
|
||||||
test('LocalContextCompletion: Imported namespace generates parameter snippet', async () => {
|
|
||||||
const strategy = new LocalContextCompletion();
|
|
||||||
|
|
||||||
const docText = `from Company.tools import network_helpers;\nnetwork_helpers.`;
|
|
||||||
const mockDoc = createMockDoc(docText);
|
|
||||||
const position = new vscode.Position(1, 16);
|
|
||||||
|
|
||||||
const result = await strategy.provideCompletions(mockDoc, position, 'network_helpers.');
|
|
||||||
|
|
||||||
assert.ok(result !== undefined);
|
|
||||||
const funcItem = result!.find(item => item.label === 'ping');
|
|
||||||
assert.ok(funcItem !== undefined, "Must find the ping function");
|
|
||||||
|
|
||||||
const snippet = funcItem?.insertText as vscode.SnippetString;
|
|
||||||
assert.strictEqual(snippet.value, 'ping(${1:host}, ${2:ip});$0', "Must map parameters exactly");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
import * as assert from 'assert';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { ParameterRule } from '../../core/rules/ParameterRule';
|
|
||||||
import { IndentationRules } from '../../providers/formatters/IndentationRules';
|
|
||||||
import { FormatState } from '../../providers/formatters/FormatState';
|
|
||||||
import { LinterContext } from '../../core/LinterContext';
|
|
||||||
import { TplLookup } from '../../core/TplLookup';
|
|
||||||
import { workspaceIndex } from '../../core/WorkspaceIndex';
|
|
||||||
|
|
||||||
class MockLinterContext extends LinterContext {
|
|
||||||
public diagnostics: vscode.Diagnostic[] = [];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public addDiag(lineIdx: number, start: number, end: number, msg: string, severity: vscode.DiagnosticSeverity): vscode.Diagnostic {
|
|
||||||
const range = new vscode.Range(lineIdx, start, lineIdx, end);
|
|
||||||
const diag = new vscode.Diagnostic(range, msg, severity);
|
|
||||||
this.diagnostics.push(diag);
|
|
||||||
return diag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suite('BMC TPL Core Engine Tests', () => {
|
|
||||||
|
|
||||||
const fixturesPath = path.resolve(__dirname, '../../../src/test/fixtures');
|
|
||||||
|
|
||||||
suite('TplLookup & Autocomplete', () => {
|
|
||||||
test('Should resolve Core and TKU built-ins case-insensitively', () => {
|
|
||||||
const runCmd = TplLookup.getBuiltInFunction('discovery', 'runcommand');
|
|
||||||
assert.ok(runCmd, 'discovery.runCommand should be found ignoring case');
|
|
||||||
|
|
||||||
const tkuCmd = TplLookup.getBuiltInFunction('COMMON_FUNCTIONS', 'identify_host');
|
|
||||||
assert.ok(tkuCmd, 'Common_Functions.identify_host should be found ignoring case');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('ParameterRule (Lexer & Linter)', () => {
|
|
||||||
test('Should catch negative cases (unknown func, param count, unused return)', () => {
|
|
||||||
const rule = new ParameterRule();
|
|
||||||
const ctx = new MockLinterContext();
|
|
||||||
|
|
||||||
const originalGetSignatures = workspaceIndex.getFunctionSignatures.bind(workspaceIndex);
|
|
||||||
workspaceIndex.getFunctionSignatures = (name: string) => {
|
|
||||||
if (name === 'my_local_func') {
|
|
||||||
return [{
|
|
||||||
name: 'my_local_func',
|
|
||||||
params: ['a', 'b'],
|
|
||||||
hasReturnValue: true,
|
|
||||||
docString: '',
|
|
||||||
moduleName: 'TestNegative',
|
|
||||||
lineIdx: 1,
|
|
||||||
fsPath: 'test.tpl'
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
return originalGetSignatures(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
// GEFIXT: "dummy :=" hinzugefügt, um den doppelten Return-Value-Fehler zu vermeiden
|
|
||||||
const content = `tpl 1.21 module TestNegative;
|
|
||||||
define test_func()
|
|
||||||
unknown_module.does_not_exist(host);
|
|
||||||
dummy := my_local_func(1, 2, 3);
|
|
||||||
my_local_func(1, 2);
|
|
||||||
end define;`;
|
|
||||||
|
|
||||||
const lines = content.split('\n');
|
|
||||||
lines.forEach((line, idx) => rule.checkLine(line, line, idx, ctx, lines));
|
|
||||||
|
|
||||||
workspaceIndex.getFunctionSignatures = originalGetSignatures;
|
|
||||||
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 3, 'Should find exactly 3 errors in negative fixture');
|
|
||||||
|
|
||||||
assert.ok(ctx.diagnostics.some(d => d.message.includes("Unknown function 'unknown_module.does_not_exist'")));
|
|
||||||
assert.ok(ctx.diagnostics.some(d => d.message.includes("expects 2 parameter(s), but 3 were provided")));
|
|
||||||
assert.ok(ctx.diagnostics.some(d => d.message.includes("is not used. Did you forget an assignment (:=)?")));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should ignore strings, docstrings and comments (Lexer Edge Cases)', () => {
|
|
||||||
const rule = new ParameterRule();
|
|
||||||
const ctx = new MockLinterContext();
|
|
||||||
|
|
||||||
const content = fs.readFileSync(path.join(fixturesPath, 'lexer_edge_cases.tpl'), 'utf-8');
|
|
||||||
const lines = content.split('\n');
|
|
||||||
|
|
||||||
lines.forEach((line, idx) => rule.checkLine(line, line, idx, ctx, lines));
|
|
||||||
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 0, 'Should find NO errors in lexer edge cases');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
suite('IndentationRules (Auto-Formatter)', () => {
|
|
||||||
test('Should forcefully fix bad indentation', () => {
|
|
||||||
const rule = new IndentationRules();
|
|
||||||
const state = new FormatState();
|
|
||||||
|
|
||||||
const content = fs.readFileSync(path.join(fixturesPath, 'format_bad.tpl'), 'utf-8');
|
|
||||||
const lines = content.split('\n');
|
|
||||||
|
|
||||||
const formattedLines = lines.map(line => rule.apply(line, state));
|
|
||||||
|
|
||||||
assert.strictEqual(formattedLines[3], ' // 6 Leerzeichen hier!');
|
|
||||||
assert.strictEqual(formattedLines[4], ' log.debug("2 Leerzeichen hier");');
|
|
||||||
assert.strictEqual(formattedLines[5], ' if true then');
|
|
||||||
assert.strictEqual(formattedLines[6], ' log.debug("3 Leerzeichen!");');
|
|
||||||
assert.strictEqual(formattedLines[7], ' end if;');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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: Indents if/then/end block correctly', () => {
|
|
||||||
const indentRules = new IndentationRules();
|
|
||||||
const state = new FormatState();
|
|
||||||
state.tabSize = 4;
|
|
||||||
|
|
||||||
// Line 1: if condition then (expected: 0 spaces, but level jumps to 1 for the next line)
|
|
||||||
let result = indentRules.apply('if x = 1 then', state);
|
|
||||||
assert.strictEqual(result, 'if x = 1 then');
|
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
|
||||||
|
|
||||||
// Line 2: normal code in block (expected: 4 spaces)
|
|
||||||
result = indentRules.apply('log.debug("test");', state);
|
|
||||||
assert.strictEqual(result, ' log.debug("test");');
|
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
|
||||||
|
|
||||||
// Line 3: end if; (expected: 0 spaces, level jumps to 0 right before output)
|
|
||||||
result = indentRules.apply('end if;', state);
|
|
||||||
assert.strictEqual(result, 'end if;');
|
|
||||||
assert.strictEqual(state.indentLevel, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('IndentationRules: elif and else behave correctly', () => {
|
|
||||||
const indentRules = new IndentationRules();
|
|
||||||
const state = new FormatState();
|
|
||||||
state.tabSize = 4;
|
|
||||||
state.indentLevel = 1; // We start practically IN an open if-block
|
|
||||||
|
|
||||||
// elif must jump back (to 0) and then back to 1 for the following 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 at parentheses', () => {
|
|
||||||
const alignRules = new AlignmentRules();
|
|
||||||
const state = new FormatState();
|
|
||||||
|
|
||||||
// 1. Line with opening parenthesis
|
|
||||||
alignRules.apply('discovery.runCommand(', 'discovery.runCommand(', state);
|
|
||||||
assert.strictEqual(state.parenAlignments.length, 1);
|
|
||||||
assert.strictEqual(state.parenAlignments[0], 21); // 21 characters until after the open parenthesis
|
|
||||||
|
|
||||||
// 2. Line (parameter should be indented exactly to column 21)
|
|
||||||
const result = alignRules.apply('host,', 'host,', state);
|
|
||||||
assert.strictEqual(result, ' host,');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
import * as assert from 'assert';
|
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { LinterContext } from '../../core/LinterContext';
|
|
||||||
import { SyntaxRule } from '../../core/rules/SyntaxRule';
|
|
||||||
import { BlockRule } from '../../core/rules/BlockRule';
|
|
||||||
import { validateTplDocument } from '../../core/TplLinter';
|
|
||||||
|
|
||||||
suite('Linter Test Suite', () => {
|
|
||||||
test('SyntaxRule: Ignores URLs and special characters in strings', () => {
|
|
||||||
const rule = new SyntaxRule();
|
|
||||||
const ctx = new LinterContext();
|
|
||||||
ctx.activePhaseName = 'body'; // Allowed zone
|
|
||||||
|
|
||||||
const rawLine = ' log.debug("https://example.com/api?q=1//test");';
|
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
|
||||||
|
|
||||||
// Must not throw an error (neither missing semicolon nor broken comment)
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Linter: Captures TODOs as Information', () => {
|
|
||||||
const mockDoc = {
|
|
||||||
languageId: 'tpl',
|
|
||||||
getText: () => "tpl 1.0 module Test;\n// TODO: refactor this",
|
|
||||||
uri: vscode.Uri.file('test.tpl')
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const collection = vscode.languages.createDiagnosticCollection('test-collection');
|
|
||||||
validateTplDocument(mockDoc, collection);
|
|
||||||
|
|
||||||
const diags = collection.get(mockDoc.uri);
|
|
||||||
assert.strictEqual(diags?.length, 1, "Must find exactly one TODO");
|
|
||||||
assert.ok(diags![0].message.includes('TODO: refactor this'));
|
|
||||||
assert.strictEqual(diags![0].severity, vscode.DiagnosticSeverity.Information);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('SyntaxRule: Multi-line if-condition without semicolon error', () => {
|
|
||||||
const rule = new SyntaxRule();
|
|
||||||
const ctx = new LinterContext();
|
|
||||||
ctx.activePhaseName = 'body';
|
|
||||||
|
|
||||||
// Line 1: or-break
|
|
||||||
rule.checkLine('if x = 1 or', 'if x = 1 or', 0, ctx, []);
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 0);
|
|
||||||
assert.strictEqual(ctx.inConditionBlock, true);
|
|
||||||
|
|
||||||
// Line 2: then-closure
|
|
||||||
rule.checkLine('y = 2 then', 'y = 2 then', 1, ctx, []);
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 0, "'then' must not require a semicolon at the end!");
|
|
||||||
assert.strictEqual(ctx.inConditionBlock, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('SyntaxRule: Allowed allocations in Zones (e.g. metadata)', () => {
|
|
||||||
const rule = new SyntaxRule();
|
|
||||||
const ctx = new LinterContext();
|
|
||||||
ctx.inModuleMetadata = true; // We are in module metadata
|
|
||||||
|
|
||||||
const rawLine = " origin := 'Custom';";
|
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
|
||||||
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 0, "Allocations have to be allowed in metadata");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('BlockRule: if/for/while stack tracking', () => {
|
|
||||||
const mockDoc = {
|
|
||||||
languageId: 'tpl',
|
|
||||||
getText: () => "tpl 1.0 module Test;\nif condition then\nend for;",
|
|
||||||
uri: vscode.Uri.file('test.tpl')
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const collection = vscode.languages.createDiagnosticCollection('test-collection-blockrule');
|
|
||||||
validateTplDocument(mockDoc, collection);
|
|
||||||
|
|
||||||
const diags = collection.get(mockDoc.uri);
|
|
||||||
|
|
||||||
// We check language-agnostically: Is there an Error exactly on line 2 (the 'end for;')?
|
|
||||||
const hasErrorOnEndFor = diags?.some(d =>
|
|
||||||
d.range.start.line === 2 &&
|
|
||||||
d.severity === vscode.DiagnosticSeverity.Error
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.strictEqual(hasErrorOnEndFor, true, "Need to throw an error on 'end for;' line");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue