Compare commits

..

No commits in common. "master" and "first_release_beta" have entirely different histories.

37 changed files with 491 additions and 3588 deletions

5
.gitignore vendored
View File

@ -3,8 +3,3 @@ dist
node_modules
.vscode-test/
*.vsix
src/*.js*
src/test/*.js*
out/*
out/*/*
out/*/*/*

9
CHANGELOG.md Normal file
View File

@ -0,0 +1,9 @@
# Change Log
All notable changes to the "bmc-tpl-code-all-around" extension will be documented in this file.
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [Unreleased]
- Initial release

129
README.md
View File

@ -1,84 +1,71 @@
# BMC TPL Code All-Around
# bmc-tpl-code-all-around README
**Linter, Auto-Formatter, IntelliSense und Hover-Dokumentationen für die BMC TPL (Template Language).**
This is the README for your extension "bmc-tpl-code-all-around". After writing up a brief description, we recommend including the following sections.
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
Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file.
For example if there is an image subfolder under your extension project workspace:
\!\[feature X\]\(images/feature-x.png\)
> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow.
## Requirements
If you have any requirements or dependencies, add a section describing those and how to install and configure them.
## Extension Settings
Include if your extension adds any VS Code settings through the `contributes.configuration` extension point.
For example:
This extension contributes the following settings:
* `myExtension.enable`: Enable/disable this extension.
* `myExtension.thing`: Set to `blah` to do something.
## Known Issues
Calling out known issues can help limit users opening duplicate issues against your extension.
## Release Notes
Users appreciate release notes as you update your extension.
### 1.0.0
Initial release of ...
### 1.0.1
Fixed issue #.
### 1.1.0
Added features X, Y, and Z.
---
## ✨ Features
## Following extension guidelines
### 🤖 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:
* **Strikte Hierarchie-Prüfung:** Überwacht die korrekte Reihenfolge der Blöcke innerhalb von Patterns und Syncmappings (`metadata` -> `overview` -> `constants` -> `triggers` -> `mapping`/`removal` -> `body`).
* **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).
* **Block- & Klammer-Tracking:** Erkennt nicht geschlossene Blöcke (wie fehlende `end body;` oder `end pattern;`) und findet ungeschlossene runde `(` oder eckige `[` Klammern.
* **Pflichtfeld-Validierung:** Prüft, ob auf Modul-Ebene zwingend benötigte Metadaten (`origin`, `author`, `products`, `publishers`, `tree_path`) vorhanden sind und ob im `body` ein initiales `log.debug` Statement steht.
* **Versions-Prüfung:** Gleicht die im Modul deklarierte TPL-Version mit der in deinen Settings definierten Version ab und warnt vor veralteten Skripten.
Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension.
### ⚙️ Konfigurierbare Plugin-Einstellungen (Settings)
Personalisiere das Plugin direkt über die VS Code Benutzeroberfläche (File > Preferences > Settings > Extensions > BMC TPL Einstellungen):
* **Standard Metadaten:** Hinterlege deine eigenen Werte für `Author`, `Publisher`, `Origin` und den `Tree Path`. Diese werden beim Generieren neuer Module automatisch eingesetzt.
* **TPL Zielversion:** Definiere die TPL-Version (z.B. `23` für 1.23), für die du standardmäßig entwickelst.
* **Live-Reload:** Änderungen an den Einstellungen greifen sofort, ganz ohne Neustart des Editors.
* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)
### 🎨 Auto-Formatter & Visual Alignment (`Shift + Alt + F`)
Schluss mit kaputten Einrückungen! Der Formatter bringt deinen Code in Bestform:
* **VS Code Standard:** Erzwingt strikt 4 Leerzeichen pro Einrückungs-Ebene.
* **Dynamische Block-Einrückung:** Erkennt automatisch startende und endende Blöcke (`if`, `for`, `pattern`, `table` etc.) und passt die Einrückungsebene an.
* **Klammer-Gedächtnis (Visual Alignment):** Die Königsdisziplin! Der Formatter merkt sich die spaltengenaue Position von geöffneten Arrays `[` oder Funktionsaufrufen `(` und richtet alle Folgezeilen makellos an dieser Klammer aus.
## Working with Markdown
### 💡 IntelliSense & Autovervollständigung (Cross-File)
Spart extrem viel Zeit beim Coden durch massives Kontext-Wissen:
* **Volle BMC Standard-Bibliothek:** Nahtlose Integration aller Built-In Funktionen (z.B. `discovery`, `number`, `text`, `model`). Unterstützt Parameter-Überladungen (z.B. `number.toText` wahlweise mit oder ohne Basis) per Tab-Navigation.
* **Workspace-Scanning:** Importierst du ein Modul (z.B. `from company.helpers import my_helper`), durchsucht das Plugin im Hintergrund dein gesamtes Projekt. Tippst du `my_helper.`, werden dir automatisch alle verfügbaren `define`-Methoden vorgeschlagen.
* **Lokale Namespaces:** Erkennt `definitions`-Blöcke in der gleichen Datei und schlägt deren Methoden vor.
* **Variablen-Tracking:** Lernt alle Variablen, die du mit `:=` definiert hast, und schlägt sie dir später vor.
You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts:
### 🔍 Hover-Dokumentation (Tooltips)
Wenn du mit der Maus über einem Methodennamen, einem Pattern oder einer Tabelle stehen bleibst:
* **Offizielle BMC Doku:** Volle Integration der originalen, mehrzeiligen BMC Dokumentationen für alle eingebauten Funktionen (kein Browser mehr nötig!).
* **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!).
* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux).
* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux).
* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets.
### ⚡ Quick Fixes (Code Actions / Glühbirne)
Bietet sofortige Reparaturoptionen mit einem Klick:
* **"Fehlende Modul-Metadaten einfügen":** Generiert sofort alle 5 Pflichtfelder im Modul-Metadata-Block.
* **"Log-Statement einfügen":** Baut das fehlende `log.debug("Start pattern...");` direkt am Anfang des `body`-Blocks ein.
* **"Tabs zu Spaces":** Ersetzt sofort alle illegalen Tabs (`\t`) in einer Zeile durch jeweils 4 korrekte Leerzeichen.
## For more information
---
* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown)
* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/)
## 🚀 Snippets (Code-Vorlagen)
Einfach das Keyword tippen und `Enter` drücken, um komplette, perfekt eingerückte Blöcke zu generieren:
| Keyword | Beschreibung |
| :--- | :--- |
| `tpl_skeleton` | **God-Snippet**: Generiert ein komplett frisches TPL Modul. Nutzt automatisch deine hinterlegten Settings für Metadaten und Version. |
| `pattern` / `syncmapping` | Baut einen leeren Pattern/Syncmapping-Block inklusive DocString und End-Tag. |
| `define` | Generiert eine Methodendefinition (`define name(params) ... end define;`). |
| `metadata`, `overview`, `body`... | Baut den jeweiligen Block auf und platziert den Cursor in der Mitte. |
| `table` | Erstellt eine Tabelle inklusive des `default -> "unknown";` Fallbacks. |
| `if`, `elif`, `else` | Baut bedingte Abfragen inkl. `then` und `end if;` und platziert den Cursor sauber bei der Bedingung. |
| `for`, `while` | Baut Standard-Schleifen inkl. `do` und End-Tag und lässt dich direkt iterieren. |
---
## 🛠️ 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).
2. In einem Projekt mit TPL Dateien eine davon öffnen und die Analyse durchlaufen lassen.
3. Der Linter startet automatisch bei jedem Tastendruck.
4. **Formatieren:** Rechtsklick -> *Dokument formatieren* (oder `Shift + Alt + F`).
5. **Autovervollständigung:** Triggert automatisch beim Tippen, speziell nach einem Punkt (`.`) bei Modul-Aufrufen.
6. **Quick Fixes:** Auf die gelbe/rote Glühbirne am Zeilenanfang klicken oder `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac) drücken.
---
## 📜 Lizenz
Dieses Projekt ist unter der **MIT License** lizenziert.
Copyright (c) 2026 Thomas Wehnert
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.
**Enjoy!**

View File

@ -1 +0,0 @@
vsce package

BIN
icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

View File

View File

@ -1,82 +1,21 @@
{
"name": "bmc-tpl-code-all-around",
"displayName": "BMC TPL Code All around",
"description": "Linter, Auto-Formatter, IntelliSense und Hover-Dokumentationen für die BMC TPL (Template Language). Verwandelt VS Code in eine vollwertige IDE.",
"description": "Code linter, tracker and all around tool for coding in BMC Helix TPL",
"version": "0.0.1",
"publisher": "kuro",
"author": "Thomas Wehnert",
"license": "MIT",
"icon": "icon.png",
"engines": {
"vscode": "^1.140.0"
},
"repository": {
"type": "git",
"url": "https://wiccafe.de/kuro/bmc-tpl-code-all-around.git"
},
"categories": [
"Linters",
"Formatters",
"Programming Languages",
"Snippets"
],
"keywords": [
"bmc",
"discovery",
"tpl",
"linter",
"formatter",
"intellisense",
"autocomplete"
],
"activationEvents": [
"onLanguage:tpl"
"Other"
],
"activationEvents": [],
"main": "./dist/extension.js",
"contributes": {
"configuration": {
"title": "BMC TPL Einstellungen",
"properties": {
"tpl.metadata.author": {
"type": "string",
"default": "Autor",
"description": "Standard-Autor für neu generierte TPL-Module."
},
"tpl.metadata.origin": {
"type": "string",
"default": "Custom",
"description": "Standard-Origin für neu generierte TPL-Module."
},
"tpl.metadata.publisher": {
"type": "string",
"default": "IT-Operations",
"description": "Standard-Publisher für neu generierte TPL-Module."
},
"tpl.metadata.treePathBase": {
"type": "string",
"default": "Company",
"description": "Erster Teil (Basis) des tree_paths in den Metadaten."
},
"tpl.version": {
"type": "number",
"default": 23,
"description": "Aktuelle TPL-Nebenversion (z. B. 23 für TPL 1.23). Wird genutzt um veraltete Module zu erkennen und beim Generieren eingefügt."
}
}
},
"languages": [
"commands": [
{
"id": "tpl",
"aliases": ["TPL", "tpl"],
"extensions": [".tpl"],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "tpl",
"scopeName": "source.tpl",
"path": "./syntaxes/tpl.tmLanguage.json"
"command": "bmc-tpl-code-all-around.helloWorld",
"title": "Hello World"
}
]
},
@ -105,5 +44,9 @@
"typescript": "^6.0.3",
"@vscode/test-cli": "^0.0.15",
"@vscode/test-electron": "^3.0.0"
},
"overrides": {
"diff": "^8.0.4",
"serialize-javascript": "^7.0.6"
}
}

View File

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

View File

@ -0,0 +1,40 @@
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 edits: vscode.TextEdit[] = [];
const lines = document.getText().split('\n');
let indentLevel = 0;
// Zwingt den Formatter strikt zu 3 Spaces (ignoriert VS Code Settings)
const indentString = ' ';
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim();
if (line.length === 0) { continue; }
// 1. Wenn die Zeile einen Block schließt, VORHER die Einrückung verringern
if (line.match(/^(end\s+(pattern|table|if|for|while)|else|elif)\b/)) {
indentLevel = Math.max(0, indentLevel - 1);
}
// 2. Die Zeile neu aufbauen (korrekte Einrückung + getrimmter Text)
const newLineText = indentString.repeat(indentLevel) + line;
// 3. Nur wenn sich was geändert hat, ein Edit erzeugen
if (lines[i] !== newLineText && lines[i] !== newLineText + '\r') {
const range = new vscode.Range(i, 0, i, lines[i].length);
edits.push(vscode.TextEdit.replace(range, newLineText));
}
// 4. Wenn die Zeile einen Block öffnet, NACHHER die Einrückung erhöhen
if (line.match(/^(pattern|table|metadata|overview|constants|triggers|body|if|for|while|else|elif)\b/)) {
if (!line.startsWith('end ')) {
indentLevel++;
}
}
}
return edits;
}
}

View File

@ -1,50 +0,0 @@
import * as vscode from 'vscode';
export enum PatternPhase { None = 0, Metadata = 1, Overview = 2, Constants = 3, Triggers = 4, Mapping = 5, Removal = 6, Body = 7 }
export class LinterContext {
public diagnostics: vscode.Diagnostic[] = [];
public configuredTplVersion: number = 23;
public tplVersion: number = 10;
// Status-Tracking
public hasModuleDef = false;
public inBlock: 'none' | 'pattern' | 'table' | 'definitions' | 'enumeration' | 'configuration' = 'none';
public currentPatternPhase: PatternPhase = PatternPhase.None;
public inMultiLineComment = false;
public inConditionBlock = false;
// String-Tracking (Basis-Lexer)
public inTripleDouble = false;
public inTripleSingle = false;
public stringNeedsSemicolon = false;
public justOpenedString = false;
public isClosingString = false;
public isInternalStringLine = false;
// Block- & Metadaten-Tracking
public activePhaseName = "";
public activePhaseLine = -1;
public currentPatternName = "";
public inModuleMetadata = false;
public hasModuleMetadata = false;
public moduleMetadataBlockLine = -1;
public foundModuleMetadata = { origin: false, author: false, products: false, publishers: false, tree_path: false };
// Klammer-Tracking
public parenStack: number[] = [];
public bracketStack: number[] = [];
public openDefines: number[] = [];
public addDiag(lineIdx: number, start: number, end: number, msg: string, severity: vscode.DiagnosticSeverity): vscode.Diagnostic {
const diag = new vscode.Diagnostic(new vscode.Range(lineIdx, start, lineIdx, end), msg, severity);
this.diagnostics.push(diag);
return diag;
}
public resetLineStates() {
this.isInternalStringLine = false;
this.justOpenedString = false;
this.isClosingString = false;
}
}

View File

@ -1,94 +0,0 @@
import * as vscode from 'vscode';
import { LinterContext } from './LinterContext';
import { LinterRule } from './rules/LinterRule';
import { SyntaxRule } from './rules/SyntaxRule';
import { BlockRule } from './rules/BlockRule';
export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
if (document.languageId !== 'tpl') { return; }
const ctx = new LinterContext();
const config = vscode.workspace.getConfiguration('tpl');
ctx.configuredTplVersion = config.get<number>('version', 23);
const text = document.getText();
const lines = text.split('\n');
// 1. Version prüfen
const versionMatch = text.match(/tpl\s+1\.(\d+)\s+module/);
if (versionMatch) {
ctx.tplVersion = parseInt(versionMatch[1], 10);
if (ctx.tplVersion < ctx.configuredTplVersion) {
const linesToSearch = text.substring(0, versionMatch.index! + 30).split('\n');
ctx.addDiag(linesToSearch.length - 1, 0, versionMatch[0].length, `Das Modul nutzt eine alte TPL Version (1.${ctx.tplVersion}). Aktuell in den Settings definiert ist 1.${ctx.configuredTplVersion}.`, vscode.DiagnosticSeverity.Warning);
}
}
// 2. Regeln laden
const rules: LinterRule[] = [
new SyntaxRule(),
new BlockRule()
];
// 3. Zeilenweise iterieren
for (let i = 0; i < lines.length; i++) {
const rawLine = lines[i];
const line = rawLine.trim();
ctx.resetLineStates();
// Basis-Lexer: Strings & Kommentare tracken (läuft vor den Regeln!)
trackStrings(rawLine, line, ctx);
if (ctx.isInternalStringLine && (ctx.inTripleDouble || ctx.inTripleSingle)) { continue; }
if (line.startsWith('//') || line.startsWith('/*')) {
if (line.startsWith('/*')) { ctx.inMultiLineComment = true; }
continue;
}
if (ctx.inMultiLineComment) {
if (line.includes('*/')) { ctx.inMultiLineComment = false; }
continue;
}
if (line.length === 0) { continue; }
// Regeln anwenden
for (const rule of rules) {
rule.checkLine(line, rawLine, i, ctx, lines); // Hier 'lines' ergänzen!
}
}
// 4. Abschluss-Prüfungen durchführen
for (const rule of rules) {
if (rule.checkEnd) {
rule.checkEnd(lines, ctx);
}
}
// Diagnostics an VS Code übergeben
collection.set(document.uri, ctx.diagnostics);
}
// Interne Hilfsfunktion für den Parser
function trackStrings(rawLine: string, line: string, ctx: LinterContext) {
const tripleDoubleCount = (rawLine.match(/"""/g) || []).length;
const tripleSingleCount = (rawLine.match(/'''/g) || []).length;
if (ctx.inTripleDouble) {
ctx.isInternalStringLine = true;
if (tripleDoubleCount % 2 !== 0) { ctx.inTripleDouble = false; ctx.isClosingString = true; }
} else if (tripleDoubleCount % 2 !== 0) {
ctx.inTripleDouble = true; ctx.justOpenedString = true;
}
if (ctx.inTripleSingle) {
ctx.isInternalStringLine = true;
if (tripleSingleCount % 2 !== 0) { ctx.inTripleSingle = false; ctx.isClosingString = true; }
} else if (tripleSingleCount % 2 !== 0 && !ctx.justOpenedString) {
ctx.inTripleSingle = true; ctx.justOpenedString = true;
}
if (ctx.justOpenedString) {
ctx.stringNeedsSemicolon = !line.startsWith('"""') && !line.startsWith("'''");
}
}

View File

@ -1,164 +0,0 @@
import * as vscode from 'vscode';
import { LinterRule } from './LinterRule';
import { LinterContext, PatternPhase } from '../LinterContext';
export class BlockRule implements LinterRule {
// HIER GEFIXT: 'lines: string[]' wurde zur Signatur hinzugefügt!
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
if (!ctx.hasModuleDef) {
if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; }
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module <name>;' beginnen.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; }
}
if (line.startsWith('metadata') && ctx.inBlock === 'none') {
ctx.inModuleMetadata = true;
ctx.hasModuleMetadata = true;
ctx.moduleMetadataBlockLine = lineIdx;
return;
}
if (ctx.inModuleMetadata) {
this.trackModuleMetadata(line, lineIdx, ctx);
return;
}
if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) {
if (!ctx.hasModuleMetadata && ctx.inBlock === 'none') {
ctx.addDiag(lineIdx, 0, line.length, "Ein 'metadata' Block auf Modul-Ebene ist Pflicht, bevor andere Blöcke definiert werden.", vscode.DiagnosticSeverity.Error);
}
}
this.handleBlockOpenings(line, rawLine, lineIdx, ctx);
if (ctx.inBlock === 'pattern') {
// HIER GEFIXT: 'lines' wird jetzt korrekt an die Methode weitergereicht
this.checkPatternPhases(line, rawLine, lineIdx, ctx, lines);
}
this.checkEndBlocks(line, rawLine, lineIdx, ctx);
}
public checkEnd(lines: string[], ctx: LinterContext): void {
for (const unclosedLine of ctx.openDefines) {
ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' wurde nicht mit 'end define;' geschlossen!", vscode.DiagnosticSeverity.Error);
}
}
private trackModuleMetadata(line: string, lineIdx: number, ctx: LinterContext) {
if (line.match(/^origin\b/)) { ctx.foundModuleMetadata.origin = true; }
if (line.match(/^author\b/)) { ctx.foundModuleMetadata.author = true; }
if (line.match(/^products\b/)) { ctx.foundModuleMetadata.products = true; }
if (line.match(/^publishers\b/)) { ctx.foundModuleMetadata.publishers = true; }
if (line.match(/^tree_path\b/)) { ctx.foundModuleMetadata.tree_path = true; }
if (line.startsWith('end metadata')) {
ctx.inModuleMetadata = false;
const missing = Object.keys(ctx.foundModuleMetadata).filter(k => !(ctx.foundModuleMetadata as any)[k]);
if (missing.length > 0 && ctx.moduleMetadataBlockLine !== -1) {
const diag = ctx.addDiag(ctx.moduleMetadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten (Modul-Ebene): ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
diag.code = 'missing_module_metadata';
}
}
}
private handleBlockOpenings(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
if (line.startsWith('definitions ') || line.startsWith('definition ')) {
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Ein Definitions-Block muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'definitions';
}
else if (line.startsWith('define ')) { ctx.openDefines.push(lineIdx); }
else if (line.startsWith('enumeration ')) {
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Eine Enumeration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'enumeration';
}
else if (line.startsWith('configuration ')) {
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Eine Konfiguration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'configuration';
}
else if (line.startsWith('pattern ') || line.startsWith('syncmapping ')) {
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 7, "Ein Pattern/Syncmapping kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'pattern';
ctx.currentPatternPhase = PatternPhase.None;
ctx.activePhaseName = "";
ctx.activePhaseLine = -1;
const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/);
if (nameMatch) { ctx.currentPatternName = nameMatch[2]; }
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Ungültige Pattern-Deklaration. Erwartet: 'pattern <Name> <Version>'", vscode.DiagnosticSeverity.Error); }
}
else if (line.startsWith('table ')) {
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'table';
}
}
private checkPatternPhases(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]) {
const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => {
if (line.startsWith(phaseName)) {
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
if (ctx.currentPatternPhase >= phaseEnum) { ctx.addDiag(lineIdx, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt.`, vscode.DiagnosticSeverity.Error); }
ctx.currentPatternPhase = phaseEnum;
ctx.activePhaseName = phaseName;
ctx.activePhaseLine = lineIdx;
}
};
checkPhase('metadata', PatternPhase.Metadata);
checkPhase('overview', PatternPhase.Overview);
checkPhase('constants', PatternPhase.Constants);
checkPhase('triggers', PatternPhase.Triggers);
checkPhase('mapping', PatternPhase.Mapping);
checkPhase('removal', PatternPhase.Removal);
if (line.startsWith('body')) {
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
ctx.activePhaseName = "body";
ctx.activePhaseLine = lineIdx;
// DER LOG-CHECK IST ZURÜCK!
let foundLog = false;
for (let j = lineIdx + 1; j < lines.length; j++) {
const aheadLine = lines[j].trim();
if (aheadLine.startsWith('end pattern') || aheadLine.startsWith('end syncmapping')) { break; }
if (aheadLine.includes('log.debug') && aheadLine.includes(ctx.currentPatternName)) { foundLog = true; break; }
}
if (!foundLog) {
const expectedLog = `log.debug("Start pattern ${ctx.currentPatternName}");`;
const diag = ctx.addDiag(lineIdx, 0, rawLine.length, `Im Body fehlt das Log-Statement mit dem Pattern-Namen. Bsp: ${expectedLog}`, vscode.DiagnosticSeverity.Error);
diag.code = 'missing_log_start';
diag.source = expectedLog;
}
}
const closers = ['end metadata', 'end overview', 'end constants', 'end triggers', 'end mapping', 'end removal', 'end body'];
for (const closer of closers) {
if (line.startsWith(closer)) {
const phase = closer.replace('end ', '').replace(';', '').trim();
if (ctx.activePhaseName !== phase) { ctx.addDiag(lineIdx, 0, line.length, `Unerwartetes '${closer}'. Erwartet wurde 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
else { ctx.activePhaseName = ""; }
}
}
}
private checkEndBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
if (line.startsWith('end pattern') || line.startsWith('end syncmapping')) {
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
if (ctx.inBlock !== 'pattern') { ctx.addDiag(lineIdx, 0, 11, "'end pattern' gefunden, ohne offenen Pattern-Block.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'none';
}
else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) {
ctx.inBlock = 'none';
}
else if (line.startsWith('end definitions') || line.startsWith('end definition')) {
if (ctx.inBlock !== 'definitions') { ctx.addDiag(lineIdx, 0, line.length, "'end definitions' gefunden, ohne offenen Definitions-Block.", vscode.DiagnosticSeverity.Error); }
ctx.inBlock = 'none';
}
else if (line.startsWith('end define')) {
if (ctx.openDefines.length > 0) { ctx.openDefines.pop(); }
else { ctx.addDiag(lineIdx, 0, line.length, "'end define' gefunden, ohne offenes 'define'.", vscode.DiagnosticSeverity.Error); }
}
}
}

View File

@ -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;
}

View File

@ -1,81 +0,0 @@
import * as vscode from 'vscode';
import { LinterRule } from './LinterRule';
import { LinterContext } from '../LinterContext';
export class SyntaxRule implements LinterRule {
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext): void {
this.checkTabs(rawLine, lineIdx, ctx);
this.checkBrackets(line, rawLine, lineIdx, ctx);
this.checkAssignments(rawLine, lineIdx, ctx);
this.checkSemicolons(line, rawLine, lineIdx, ctx);
}
public checkEnd(lines: string[], ctx: LinterContext): void {
for (const unclosedLine of ctx.parenStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende Klammer '(' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); }
for (const unclosedLine of ctx.bracketStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende eckige Klammer '[' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); }
}
private checkTabs(rawLine: string, lineIdx: number, ctx: LinterContext) {
const tabIndex = rawLine.indexOf('\t');
if (tabIndex !== -1) {
const diag = ctx.addDiag(lineIdx, tabIndex, tabIndex + 1, "Bitte Tabs durch Spaces ersetzen", vscode.DiagnosticSeverity.Warning);
diag.code = 'invalid_tab';
}
}
private checkBrackets(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '');
const commentIdx = cleanLine.indexOf('//');
if (commentIdx !== -1) { cleanLine = cleanLine.substring(0, commentIdx); }
for (let charIdx = 0; charIdx < cleanLine.length; charIdx++) {
const char = cleanLine[charIdx];
if (char === '(') { ctx.parenStack.push(lineIdx); }
else if (char === ')') {
if (ctx.parenStack.length > 0) { ctx.parenStack.pop(); }
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Unerwartete schließende Klammer ')'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); }
}
else if (char === '[') { ctx.bracketStack.push(lineIdx); }
else if (char === ']') {
if (ctx.bracketStack.length > 0) { ctx.bracketStack.pop(); }
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Unerwartete schließende eckige Klammer ']'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); }
}
}
}
private checkAssignments(rawLine: string, lineIdx: number, ctx: LinterContext) {
const lineWithoutStrings = rawLine.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, '""');
if (/\b(if|elif|where)\b/.test(lineWithoutStrings)) { ctx.inConditionBlock = true; }
if (lineWithoutStrings.includes('=') && !lineWithoutStrings.includes(':=') && !lineWithoutStrings.includes('>=') && !lineWithoutStrings.includes('<=')) {
if (!ctx.inConditionBlock && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && !/\b(matches)\b/.test(lineWithoutStrings)) {
const idx = rawLine.indexOf('=');
if (idx !== -1) {
ctx.addDiag(lineIdx, idx, idx + 1, "Falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt.", vscode.DiagnosticSeverity.Error);
}
}
}
if (/\b(then|do)\b/.test(lineWithoutStrings) || lineWithoutStrings.endsWith(';')) { ctx.inConditionBlock = false; }
}
private checkSemicolons(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
if (ctx.isClosingString) {
if (ctx.stringNeedsSemicolon && !line.endsWith(';')) {
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error);
}
} else {
const noSemicolonNeeded = /^(pattern|syncmapping|traverse|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|\/\/|\/\*|\*)/;
const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches)$/i.test(line);
if (!noSemicolonNeeded.test(line) && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine) {
const lineToCheck = line.split('//')[0].trim();
if (!lineToCheck.endsWith(';') && !lineToCheck.endsWith('{') && !lineToCheck.endsWith(',')) {
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error);
}
}
}
}
}

View File

@ -1,244 +0,0 @@
export interface TplBuiltInMethod {
description: string;
snippets: string[];
}
export const tplBuiltIns: Record<string, Record<string, TplBuiltInMethod>> = {
"built_in_globals": {
"datatype": {
snippets: ["${1:item}"],
description: `Returns a string indicating the type of the item.\n\nPossible return values are:\n"node"\n"relationship"\n"text"\n"boolean"\n"number"\n"float"\n"list"\n"table"\n"none"\n"void"\n"datetime"\n"nodeset"\n"unknown" (used for anything else)`
},
"size": {
snippets: ["${1:collection}"],
description: `Returns the number of items in the collection.\n\nWhere the collection is a list, or a set of nodes retrieved with a search expression.\nWhere the collection is a string, the number of characters is returned.\nTo determine the type of the collection, you can use the datatype function.`
}
},
"log": {
"debug": { snippets: ["${1:output}"], description: `Log the given message with a debug level message.` },
"info": { snippets: ["${1:output}"], description: `Log the given message with an info level message.` },
"warn": { snippets: ["${1:output}"], description: `Log the given message with a warn level message.` },
"error": { snippets: ["${1:output}"], description: `Log the given message with an error level message.` },
"critical": { snippets: ["${1:output}"], description: `Log the given message with a critical level message.` }
},
"text": {
"lower": { snippets: ["${1:input}"], description: `Returns the lower-cased version of the string argument.` },
"upper": { snippets: ["${1:input}"], description: `Returns the upper-cased version of the string argument.` },
"toNumber": {
snippets: ["${1:input}", "${1:input},${2:base}"],
description: `Converts its string argument into a number. By default, the number is treated as a base 10 value; if the optional base is provided, the number is treated as being in the specified base. Bases from 2 to 36 are supported.`
},
"replace": {
snippets: ["${1:input}, ${2:old},${3:new}"],
description: `Returns a modified version of the string formed by replacing all occurrences of the string old with new.`
},
"join": {
snippets: ["${1:list},${2:separator}"],
description: `Returns a string containing all items in a list of strings joined with the specified separator.`
},
"split": {
snippets: ["${1:input}", "${1:input},${2:separator}"],
description: `Returns a list consisting of portions of the string split according to the separator string, where specified.`
},
"strip": {
snippets: ["${1:input}", "${1:input},${2:characters}"],
description: `Strips unwanted characters from the start and end of the given string.`
},
"leftStrip": {
snippets: ["${1:input}", "${1:input},${2:characters}"],
description: `Equivalent to text.strip, but only strips from the left side of the string.`
},
"rightStrip": {
snippets: ["${1:input}", "${1:input},${2:characters}"],
description: `Equivalent to text.strip, but only strips from the right side of the string.`
},
"hash": { snippets: ["${1:input}"], description: `Returns a hashed form of the string, generated with the MD5 hash algorithm.` },
"ordinal": { snippets: ["${1:input}"], description: `Returns the ordinal value of the string argument. The string must be one character in length.` }
},
"number": {
"toChar": { snippets: ["${1:number}"], description: `Converts the integer number in the ASCII range to a character.` },
"toText": {
snippets: ["${1:number}", "${1:number}, ${2:base}", "${1:number}, ${2:base},${3:width}"],
description: `Converts the integer number to a text form. The default is to use the base 10 representation. If the optional base argument is given, conversion uses that base instead.`
},
"range": { snippets: ["${1:number}"], description: `Generate a list containing 0 to number - 1.` }
},
"regex": {
"extract": {
snippets: ["${1:string}, ${2:expression}", "${1:string}, ${2:expression},${3:substitution}", "${1:string},${2:expression}, ${3:substitution},${4:no_match}"],
description: `Returns the result of extracting the regular expression from the string.`
},
"extractAll": { snippets: ["${1:string},${2:pattern}"], description: `Returns a list containing all the non-overlapping matches of the pattern in the string.` },
"replace": {
snippets: ["${1:string}, ${2:pattern},${3:replacement}", "${1:string},${2:pattern}, ${3:replacement}, count:=${4:0}"],
description: `Return a string with all non-overlapping matches of pattern replaced by replacement.`
},
"escape": { snippets: ["${1:string}"], description: `Returns a string with regex special characters suitably escaped.` }
},
"xpath": {
"openDocument": { snippets: ["${1:string}"], description: `Returns the DOM object resulting from parsing the XML string.` },
"evaluate": {
snippets: ["${1:sourceXML}, ${2:expression}", "${1:sourceXML}, ${2:expression},${3:namespaces}", "${1:sourceXML},${2:expression}, ${3:namespaces},${4:return_elements}"],
description: `Returns the result of evaluating the XPath expression against the XML string.`
},
"closeDocument": { snippets: ["${1:dom_object}"], description: `Closes an XML document opened with xpath.openDocument.` }
},
"table": {
"remove": { snippets: ["${1:table},${2:key}"], description: `Removes the specified key from the specified table.` },
"get": {
snippets: ["${1:table}, ${2:key}", "${1:table}, ${2:key}, default_val:=${3:none}"],
description: `Returns the key from table if it exists, or if it does not exist, returns the specified default_val.`
},
"keys": { snippets: ["${1:table}"], description: `Returns a list of the keys in the table.` },
"values": { snippets: ["${1:table}"], description: `Returns a list of the values in the table.` }
},
"Future": {
"runCommand": {
snippets: ["${1:target}, ${2:command},${3:path_to_command}"],
description: `Returns a DiscoveredCommandResult node containing the result of running the specified command. Enables command paths to be restricted.`
}
},
"discovery": {
"fileGet": {
snippets: ["${1:target}, ${2:filename}", "${1:target}, ${2:filename}, container_context:=${3:true}", "${1:target},${2:filename}, container_context:=${3:true}, win64_redirect:=${4:true}"],
description: `Retrieves the specified file.\n\nThe function takes the following parameters:\n\ntarget: a node used to identify the discovery target.\nfilename: the name of the file on which to return information.\ncontainer_context: an optional flag to specify how container context is interpreted.\nwin64_redirect: an optional flag which enables you to specify whether the search for the file should take place in the 32 bit directory if not found in the 64 bit directory.\n\nReturns a DiscoveredFile node.`
},
"fileInfo": {
snippets: ["${1:target}, ${2:filename}", "${1:target}, ${2:filename}, container_context:=${3:true}", "${1:target},${2:filename}, container_context:=${3:true}, win64_redirect:=${4:true}"],
description: `Retrieves information about the specified file, but not the file content. This information is useful if the file is a binary file or particularly large.\n\nReturns a DiscoveredFile node with no content attribute.`
},
"listDirectory": {
snippets: ["${1:target}, ${2:path}", "${1:target}, ${2:path}, container_context:=${3:true}", "${1:target},${2:path}, container_context:=${3:true}, win64_redirect:=${4:true}"],
description: `Retrieves the directory listing of the directory specified by the path on the specified target.\n\nThe function takes the following parameters:\n\ntarget, where target is:\na node used to identify the discovery target, either a directly discovered data node, or a Host node. Or,\na data source against which the request is performed. See also, discovery-dataSource.\npath – the path to the directory on which to return information. You cannot use wildcards in the path. \ncontainer_context is an optional flag to specify how container context is interpreted for the command. When a pattern triggers on a DiscoveredProcess or RuntimeEnvironment within a container, or on a ContainedSoftwareInstance or SoftwareContainer node, then the requests are executed in the container. This behavior is the default; container_context = true. However, on occasions where you want to run the request on the host, for example, you might be examining the /proc file system, you must specify container_context = false. When the pattern triggers outside a container, then there is no container context, and the flag has no effect.\nwin64_redirect is an optional flag which enables you to specify whether the search for the file should take place in the 32 bit directory if not found in the 64 bit directory. TRUE means search the 32 bit directory, FALSE means do not search the 32 bit directory if the file is not found in the 64 bit directory. The default is TRUE.\nReturns a list of Discovered Directory Entry nodes.\n\n64 bit Windows hosts have the following system directories for holding executables, DLLs, and so forth:\n\n%windir%\\system32 — 64 bit executables and DLLs\n%windir%\\SysWOW64 — 32 bit executables and DLLs\nPaths that do not start with %windir%\\system32 or %systemroot%\\system32 are never checked for redirection.\n\nOn non-Windows and 32 bit Windows systems win64_redirect has no effect.\n\nIf the call fails, it returns none and records the failure reason in the Directory Listing node.\n\nRestrictions\nOn Windows systems, owner and group information is not recovered.\nGroup names which include spaces are not supported.\nConfig.sys is not listed as a system file.\nDirectory Listing nodes are not created for empty Windows directories.`
},
"getNames": { snippets: ["${1:target},${2:ip_address}"], description: `Performs a DNS lookup on the IP address and returns a list of FQDN strings.` },
"lookupAddresses": { snippets: ["${1:target},${2:name}"], description: `Performs a DNS lookup on the supplied DNS resolvable name and returns a list of IP addresses.` },
"listRegistry": {
snippets: ["${1:target}, ${2:key_path}", "${1:target}, ${2:key_path}, win64_redirect:=${3:true}"],
description: `Returns a list of the registry entries of the registry key specified by the key_path.`
},
"registryKey": {
snippets: ["${1:target}, ${2:key}", "${1:target}, ${2:key}, win64_redirect:=${3:true}"],
description: `Retrieves a registry key from a Windows computer.\nReturns a DiscoveredRegistryValue node.`
},
"wmiQuery": {
snippets: ["${1:target}, ${2:query},${3:namespace}"],
description: `Performs a WMI query on a Windows computer.\nReturns a list of DiscoveredWMI nodes.`
},
"wbemQuery": {
snippets: ["${1:target}, ${2:class_name},${3:namespace}", "${1:target},${2:class_name}, ${3:properties},${4:namespace}"],
description: `Performs a WBEM query on the target and returns a list of DiscoveredWBEM DDD nodes.`
},
"wbemEnumInstances": {
snippets: ["${1:target}, ${2:class_name},${3:properties}, ${4:namespace}, filter_locally:=${5:false}"],
description: `Performs a WBEM query on the target and returns a list of DiscoveredWBEMInstance DDD nodes.`
},
"runCommand": {
snippets: ["${1:target}, ${2:command},${3:restrict_paths}, ${4:dialect}", "${1:target}, ${2:command},${3:restrict_paths}, ${4:dialect}, container_context:=${5:true}"],
description: `Returns a DiscoveredCommandResult node containing the result of running the specified command.`
},
"runPowerShell": {
snippets: ["${1:target}, ${2:script},${3:restrict_paths}"],
description: `Returns a DiscoveredCommandResult node containing the result of running the specified PowerShell script.`
},
"snmpGet": {
snippets: ["${1:target}, ${2:oid_table}", "${1:target}, ${2:oid_table},${3:binary_oid_list}"],
description: `Performs an SNMP query on the target and returns a DiscoveredSNMP node.`
},
"snmpGetTable": {
snippets: ["${1:target}, ${2:table_oid},${3:column_table}", "${1:target},${2:table_oid}, ${3:column_table},${4:binary_oid_list}"],
description: `Performs an SNMP query that returns a table on the target. Returns a list of DiscoveredSNMPRow nodes or none if the SNMP query failed.`
},
"scan": {
snippets: ["${1:target},${2:company}, ${3:outpost_id},${4:scope}"],
description: `Adds an open scan for the given target and returns a count of the number of endpoints included in the open scan.`
},
"process": { snippets: ["${1:source}"], description: `Returns the process node corresponding to the source node.` },
"children": { snippets: ["${1:process}"], description: `Returns a list of the child processes for the given DiscoveredProcess node.` },
"descendents": { snippets: ["${1:process}"], description: `Returns a list consisting of the children of the given DiscoveredProcess node, and recursively all of the children's children.` },
"parent": { snippets: ["${1:process}"], description: `Returns the parent process for the given DiscoveredProcess node.` },
"allProcesses": { snippets: ["${1:source}"], description: `Returns a list of all processes corresponding to the directly discovered data source node.` },
"access": { snippets: ["${1:source}"], description: `Returns the Discovery Access node for the source DDD node, if given.` },
"startTime": { snippets: ["${1:source}"], description: `Returns the start time of the scan which discovered the source DDD node.` }
},
"binary": {
"toHexString": { snippets: ["${1:value}"], description: `Returns the given binary value as a hex string.` },
"toIPv4": { snippets: ["${1:value}"], description: `Returns the given binary value as the text representation of an IPv4 address.` },
"toIPv4z": { snippets: ["${1:value}"], description: `Returns the given binary value as the text representation of an IPv4 address with a zone index.` },
"toIPv6": { snippets: ["${1:value}"], description: `Returns the given binary value as the text representation of a canonical IPv6 address.` },
"toIPv6z": { snippets: ["${1:value}"], description: `Returns the given binary value as the text representation of a canonical IPv6 address with zone index.` },
"toMACAddress": { snippets: ["${1:value}"], description: `Returns the given binary value as the text representation of a MAC address.` },
"toValue": { snippets: ["${1:data},${2:format}"], description: `Converts the given binary value into the specified format.` },
"toWWN": { snippets: ["${1:value}"], description: `Returns the given binary value as the text representation of a WWN value.` }
},
"json": {
"encode": { snippets: ["${1:value}"], description: `Converts value to a JSON encoded string. Where value is a string, table, or a list including nested structures.` },
"decode": { snippets: ["${1:value}"], description: `Decodes a JSON encoded string and returns a structure containing a number, boolean, null, string, table or list including nested structures.` }
},
"jmespath": {
"evaluate": { snippets: ["${1:source},${2:expression}"], description: `Returns the https://jmespath.org/ evaluation for the expression given a source table or list.` }
},
"yaml": {
"encode": { snippets: ["${1:object}"], description: `Converts the given object into a YAML-encoded string.` },
"decode": { snippets: ["${1:value}"], description: `Converts the given YAML encoded string into an object.` },
"decodeAll": { snippets: ["${1:value}"], description: `Converts the given YAML encoded string into a list of objects.` }
},
"list": {
"append": { snippets: ["${1:list},${2:value}"], description: `Appends an item to a specified list.` },
"sorted": { snippets: ["${1:items}", "${1:items}, reverse:=${2:false}"], description: `Returns a sorted copy of items list, leaving the items unchanged. If reverse is true, sorts in reverse order.` }
},
"model": {
"addContainment": { snippets: ["${1:container},${2:containees}"], description: `Adds the containees to the container by creating suitable relationships between the nodes.` },
"setContainment": { snippets: ["${1:container},${2:containees}"], description: `Equivalent to addContainment, except that at the end of the pattern body, any relationships to contained nodes that have not been confirmed are removed.` },
"destroy": { snippets: ["${1:node}"], description: `Destroy the specified node or relationship in the model.` },
"withdraw": { snippets: ["${1:node},${2:attribute}"], description: `Removes the named attribute from the node.` },
"setRemovalGroup": { snippets: ["${1:node}", "${1:node},${2:name}"], description: `Add the specified node or nodes to a named removal group.` },
"anchorRemovalGroup": { snippets: ["${1:node}", "${1:node},${2:name}"], description: `Specify an anchor node for a named removal group.` },
"suppressRemovalGroup": { snippets: ["${1:name}"], description: `Suppress removal of the named removal group.` },
"host": { snippets: ["${1:node}"], description: `Returns the Host node corresponding to the given node.` },
"hosts": { snippets: ["${1:node}"], description: `Returns a list of all the Host nodes corresponding to the given node.` },
"findPackages": { snippets: ["${1:node},${2:regexes}"], description: `Traverses from the node, and returns a set of all Package nodes that have names matching the provided list of regular expressions.` },
"addDisplayAttribute": { snippets: ["${1:node},${2:value}"], description: `Adds a named attribute, or a list of named attributes to the additional attributes displayed in a node view.` },
"removeDisplayAttribute": { snippets: ["${1:node},${2:value}"], description: `Removes a named attribute, or a list of named attributes from the additional attributes displayed in a node view.` },
"kind": { snippets: ["${1:node}"], description: `Returns the node kind corresponding to the given node.` },
"confirm": { snippets: ["${1:node}"], description: `Sends a confirmed event for the node.` },
"findCandidate": { snippets: ["${1:kind},${2:data}"], description: `Finds a candidate node of a kind that matches table data.` },
"findCandidates": { snippets: ["${1:kind}, ${2:data},${3:limit}"], description: `Finds ranked candidates.` },
"publish": { snippets: ["${1:node},${2:auto_rels}"], description: `Takes a node of kind ModelDefinition and publishes it, creating a BusinessService, TechnicalService or BusinessApplicationInstance node.` },
"unpublish": { snippets: ["${1:node}"], description: `Takes a node of kind ModelDefinition that has been published, and unpublishes it.` }
},
"related": {
"host": { snippets: ["${1:node}"], description: `Returns the Host node corresponding to the given node.` },
"detailContainer": { snippets: ["${1:node}"], description: `Returns the Software Component, Software Instance, or Business Application Instance node containing the given node.` },
"hostingNode": { snippets: ["${1:fallback_kind},${2:attributes}"], description: `Returns a Host or Cluster node that is associated with the node that triggered the pattern.` }
},
"mail": {
"send": {
snippets: ["${1:recipients},${2:subject}, ${3:message}", "${1:recipients}, ${2:subject},${3:message}, ${4:cc_addrs}", "${1:recipients}, ${2:subject},${3:message}, ${4:cc_addrs},${5:bcc_addrs}"],
description: `Sends an email.`
}
},
"time": {
"current": { snippets: [""], description: `Returns an internal datetime object representing the current UTC time.` },
"delta": { snippets: ["${1:days},${2:hours}, ${3:minutes},${4:seconds}"], description: `Creates a time delta that can be added to or subtracted from a time represented by an internal datetime object.` },
"parseLocal": { snippets: ["${1:string}"], description: `Converts a string representing a local time into an internal UTC datetime object.` },
"parseUTC": { snippets: ["${1:string}"], description: `Takes a UTC time string and converts it into an internal datetime object.` },
"formatLocal": { snippets: ["${1:datetime}", "${1:datetime},${2:format}"], description: `Formats an internal datetime object into a human-readable string. Converts the time into the appliance's timezone.` },
"formatUTC": { snippets: ["${1:datetime}", "${1:datetime},${2:format}"], description: `Formats an internal datetime object into a human-readable string. Performs no timezone conversion.` },
"toTicks": { snippets: ["${1:datetime}"], description: `Converts an internal datetime object (UTC) to ticks.` },
"fromTicks": { snippets: ["${1:ticks}"], description: `Converts ticks to an internal datetime object (UTC).` },
"deltaFromTicks": { snippets: ["${1:ticks}"], description: `Converts ticks into a time delta.` }
},
"inference": {
"associate": { snippets: ["${1:inferred_node},${2:associate}"], description: `Create associate inference relationship(s) from the specified node(s) to the inferred node.` },
"contributor": { snippets: ["${1:inferred_node}, ${2:contributor},${3:contributes}"], description: `Create contributor inference relationship(s).` },
"primary": { snippets: ["${1:inferred_node}, ${2:evidence}", "${1:inferred_node}, ${2:evidence}, replace:=${3:false}"], description: `Create or replace primary inference relationship(s).` },
"relation": { snippets: ["${1:inferred_relationship},${2:source}"], description: `Create relation inference relationship(s) from the specified node(s) to the inferred relationship.` },
"withdrawal": { snippets: ["${1:inferred_node}, ${2:evidence},${3:withdrawn}"], description: `Create withdrawal inference relationship(s).` },
"destruction": { snippets: ["${1:destroyed_node},${2:source}"], description: `When destroying a node, indicate that the source node was responsible for its destruction.` }
},
"system": {
"getOption": { snippets: ["${1:option_name}"], description: `Takes the name of a BMC Helix Discovery system option and returns the value.` },
"cmdbSync": { snippets: ["${1:nodes}"], description: `Adds the given root node or list of root nodes to the CMDB synchronization queue.` }
}
};

View File

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

View File

@ -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;
}
}

View File

@ -1,40 +0,0 @@
import * as vscode from 'vscode';
import { CompletionStrategy } from './completions/CompletionStrategy';
import { SnippetCompletion } from './completions/SnippetCompletion';
import { BuiltInCompletion } from './completions/BuiltInCompletion';
import { LocalContextCompletion } from './completions/LocalContextCompletion';
export class TplCompletionProvider implements vscode.CompletionItemProvider {
private strategies: CompletionStrategy[];
constructor() {
// Hier registrieren wir alle Completion Engines
this.strategies = [
new BuiltInCompletion(),
new SnippetCompletion(),
new LocalContextCompletion()
];
}
public provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken,
context: vscode.CompletionContext
): vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList> {
// Den Text in der aktuellen Zeile bis zum Cursor holen
const linePrefix = document.lineAt(position).text.substring(0, position.character);
const results: vscode.CompletionItem[] = [];
// Jede Engine um Vorschläge bitten
for (const strategy of this.strategies) {
const items = strategy.provideCompletions(document, position, linePrefix);
if (items && items.length > 0) {
results.push(...items);
}
}
return results;
}
}

View File

@ -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;
}
}

View File

@ -1,51 +0,0 @@
import * as vscode from 'vscode';
import { tplBuiltIns } from '../data/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;
}
}

View File

@ -1,23 +0,0 @@
import * as vscode from 'vscode';
import { TplQuickFix } from './TplQuickFix';
export class LogFix implements TplQuickFix {
public readonly diagnosticCode = 'missing_log_start';
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
const fix = new vscode.CodeAction("Fehlendes Log-Statement einfügen", vscode.CodeActionKind.QuickFix);
fix.edit = new vscode.WorkspaceEdit();
// Der Linter hat uns den exakten String in die 'source' gepackt
const expectedLog = diagnostic.source || 'log.debug("Start pattern");';
// Füge es eine Zeile unter dem 'body' Keyword ein, sauber eingerückt
const insertText = ` ${expectedLog}\n`;
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText);
fix.diagnostics = [diagnostic];
fix.isPreferred = true;
return [fix];
}
}

View File

@ -1,35 +0,0 @@
import * as vscode from 'vscode';
import { TplQuickFix } from './TplQuickFix';
export class MetadataFix implements TplQuickFix {
public readonly diagnosticCode = 'missing_module_metadata';
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
const fix = new vscode.CodeAction("Fehlende Metadaten generieren", vscode.CodeActionKind.QuickFix);
fix.edit = new vscode.WorkspaceEdit();
// Einstellungen laden
const config = vscode.workspace.getConfiguration('tpl');
const author = config.get<string>('metadata.author', 'Kuro');
const origin = config.get<string>('metadata.origin', 'Custom');
const publisher = config.get<string>('metadata.publisher', 'IT-Operations');
const treePathBase = config.get<string>('metadata.treePathBase', 'Grafing');
// Herausfinden, welche Felder fehlen (steht im Diagnostic-Message-String)
const missingFieldsStr = diagnostic.message.replace('Fehlende Pflicht-Metadaten (Modul-Ebene): ', '');
const missingFields = missingFieldsStr.split(', ');
let insertText = '';
if (missingFields.includes('origin')) insertText += ` origin := '${origin}';\n`;
if (missingFields.includes('author')) insertText += ` author := '${author}';\n`;
if (missingFields.includes('products')) insertText += ` products := 'TODO_Product';\n`;
if (missingFields.includes('publishers')) insertText += ` publishers := '${publisher}';\n`;
if (missingFields.includes('tree_path')) insertText += ` tree_path := '${treePathBase}', 'TODO_Category', 'TODO_Subcategory';\n`;
// Direkt über der Zeile 'end metadata;' einfügen
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line, 0), insertText);
fix.diagnostics = [diagnostic];
return [fix];
}
}

View File

@ -1,20 +0,0 @@
import * as vscode from 'vscode';
import { TplQuickFix } from './TplQuickFix';
export class TabFix implements TplQuickFix {
public readonly diagnosticCode = 'invalid_tab';
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
const fix = new vscode.CodeAction("Tabs in 4 Spaces umwandeln", vscode.CodeActionKind.QuickFix);
fix.edit = new vscode.WorkspaceEdit();
const line = document.lineAt(diagnostic.range.start.line);
const newText = line.text.replace(/\t/g, ' ');
fix.edit.replace(document.uri, line.range, newText);
fix.diagnostics = [diagnostic];
fix.isPreferred = true; // Setzt dies als Standard-Vorschlag
return [fix];
}
}

View File

@ -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[];
}

View File

@ -1,35 +0,0 @@
import * as vscode from 'vscode';
import { CompletionStrategy } from './CompletionStrategy';
export class BuiltInCompletion implements CompletionStrategy {
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
const items: vscode.CompletionItem[] = [];
// Prüft, ob wir im Namespace "log." oder "discovery." sind (auch wenn man danach weiter tippt!)
const match = linePrefix.match(/\b(log|discovery)\.([a-zA-Z0-9_]*)$/);
if (match) {
const namespace = match[1];
if (namespace === 'log') {
const levels = ['debug', 'info', 'warn', 'error'];
for (const level of levels) {
const item = new vscode.CompletionItem(level, vscode.CompletionItemKind.Method);
item.insertText = new vscode.SnippetString(`${level}("\${1:message}");`);
item.documentation = new vscode.MarkdownString(`Schreibt eine Log-Meldung auf dem Level **${level}**.`);
items.push(item);
}
}
else if (namespace === 'discovery') {
const runCmd = new vscode.CompletionItem('runCommand', vscode.CompletionItemKind.Method);
runCmd.insertText = new vscode.SnippetString('runCommand(${1:host}, "${2:command}");');
runCmd.detail = "discovery.runCommand(host, command)";
items.push(runCmd);
}
return items; // Treffer gefunden, wir geben sie zurück!
}
return undefined;
}
}

View File

@ -1,14 +0,0 @@
import * as vscode from 'vscode';
export interface CompletionStrategy {
/**
* @param document Das aktuelle Dokument
* @param position Die Cursor-Position
* @param linePrefix Der Text der aktuellen Zeile *vor* dem Cursor
*/
provideCompletions(
document: vscode.TextDocument,
position: vscode.Position,
linePrefix: string
): vscode.CompletionItem[] | undefined;
}

View File

@ -1,13 +0,0 @@
import * as vscode from 'vscode';
import { CompletionStrategy } from './CompletionStrategy';
export class LocalContextCompletion implements CompletionStrategy {
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
// TODO für Phase 2:
// 1. Regex Suche im Dokument nach ":=" (Variablen tracken)
// 2. Projektweite Suche nach "define" Methoden für Autocomplete
return undefined;
}
}

View File

@ -1,99 +0,0 @@
import * as vscode from 'vscode';
import { CompletionStrategy } from './CompletionStrategy';
export class SnippetCompletion implements CompletionStrategy {
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
// Wenn wir gerade NACH einem Punkt tippen (z.B. "log."), sind Snippets/Base-Module hier falsch!
if (linePrefix.match(/\.[a-zA-Z0-9_]*$/)) {
return undefined;
}
const items: vscode.CompletionItem[] = [];
const config = vscode.workspace.getConfiguration('tpl');
// ==========================================
// 1. BASIS-NAMESPACES (discovery, log, etc.)
// ==========================================
const namespaces = ['discovery', 'log', 'model', 'search', 'text', 'regex', 'time', 'hash', 'number', 'inference'];
for (const ns of namespaces) {
const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module);
item.detail = `TPL Namespace: ${ns}`;
items.push(item);
}
// ==========================================
// 2. DAS GOD-SNIPPET (Skeleton)
// ==========================================
const skel = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
const version = config.get<number>('version', 23);
const author = config.get<string>('metadata.author', 'Kuro');
skel.insertText = new vscode.SnippetString(
`tpl 1.${version} module \${1:Name};\n\n` +
`metadata\n` +
` origin := '\${2:Custom}';\n` +
` author := '${author}';\n` +
` tree_path := 'Grafing', '\${3:Category}', '\${4:SubCategory}';\n` +
`end metadata;\n\n` +
`pattern \${5:PatternName} 1.0\n` +
` """\n \${6:Beschreibung}\n """\n` +
` overview\n tags \${7:tag1};\n end overview;\n\n` +
` triggers\n on host := Host created, confirmed;\n end triggers;\n\n` +
` body\n log.debug("Start pattern \${5:PatternName}");\n $0\n end body;\n` +
`end pattern;`
);
skel.detail = "Generiert ein komplettes TPL Grundgerüst";
items.push(skel);
// ==========================================
// 3. TPL HAUPT-BLÖCKE (pattern, define, etc.)
// ==========================================
const mainBlocks = [
{ label: 'pattern', desc: 'pattern ... end pattern;' },
{ label: 'syncmapping', desc: 'syncmapping ... end syncmapping;' },
{ label: 'define', desc: 'define ... end define;' }
];
for (const block of mainBlocks) {
const item = new vscode.CompletionItem(block.label, vscode.CompletionItemKind.Snippet);
item.insertText = new vscode.SnippetString(`${block.label} \${1:Name} \${2:1.0}\n """\n \${3:DocString}\n """\n $0\nend ${block.label};`);
if (block.label === 'define') {
item.insertText = new vscode.SnippetString(`define \${1:methodName}(\${2:params})\n """\n \${3:DocString}\n """\n $0\nend define;`);
}
item.detail = block.desc;
items.push(item);
}
// ==========================================
// 4. SECTIONS (metadata, body, triggers, etc.)
// ==========================================
const sections = ['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body'];
for (const sec of sections) {
const item = new vscode.CompletionItem(sec, vscode.CompletionItemKind.Snippet);
item.insertText = new vscode.SnippetString(`${sec}\n $0\nend ${sec};`);
item.detail = `${sec} ... end ${sec};`;
items.push(item);
}
// ==========================================
// 5. KONTROLLSTRUKTUREN & TABELLEN
// ==========================================
const ifSnippet = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet);
ifSnippet.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;');
ifSnippet.detail = "if ... then ... end if;";
items.push(ifSnippet);
const forSnippet = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet);
forSnippet.insertText = new vscode.SnippetString('for ${1:item} in${2:list} do\n $0\nend for;');
forSnippet.detail = "for ... in ... do ... end for;";
items.push(forSnippet);
const tableSnippet = new vscode.CompletionItem('table', vscode.CompletionItemKind.Snippet);
tableSnippet.insertText = new vscode.SnippetString('table ${1:TableName} 1.0\n ${2:"key"} -> "${3:value}";\n default -> "unknown";\nend table;');
tableSnippet.detail = "table ... end table;";
items.push(tableSnippet);
return items;
}
}

View File

@ -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();
}
}
}
}

View File

@ -1,13 +0,0 @@
export class FormatState {
public indentLevel: number = 0;
public tabSize: number = 4;
// Speichert die Spalten-Position offener Klammern für Visual Alignment
public parenAlignments: number[] = [];
public bracketAlignments: number[] = [];
// Gibt die aktuellen Basis-Leerzeichen zurück (z.B. Level 2 = 8 Leerzeichen)
public get currentIndentString(): string {
return ' '.repeat(this.indentLevel * this.tabSize);
}
}

View File

@ -1,30 +0,0 @@
import { FormatState } from './FormatState';
export class IndentationRules {
// Keywords, die eine neue Einrückungsebene öffnen (ohne static!)
private readonly blockOpens = /^(pattern|syncmapping|table|definitions?|enumeration|configuration|define|metadata|overview|constants|triggers|mapping|removal|body|if\s+|for\s+|while\s+)\b/;
// Keywords, die eine Ebene schließen (ohne static!)
private readonly blockCloses = /^(end\s+|else\b|elif\b)/;
public apply(line: string, state: FormatState): string {
// 1. Wenn die Zeile einen Block schließt, VORHER die Einrückung verringern
if (this.blockCloses.test(line) && state.indentLevel > 0) {
state.indentLevel--;
}
// Aktuelle Einrückung für diese Zeile abholen
const currentIndent = state.currentIndentString;
// 2. NACH der Zeile: Wenn sie einen Block öffnet, Level für die nächste Zeile erhöhen
if (/^(else\b|elif\b)/.test(line)) {
// else / elif springen nach dem Schließen direkt wieder auf
state.indentLevel++;
}
else if (this.blockOpens.test(line)) {
state.indentLevel++;
}
return currentIndent + line;
}
}

View File

@ -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));
});
});

View File

@ -1,53 +0,0 @@
import * as assert from 'assert';
import { FormatState } from '../../providers/formatters/FormatState';
import { IndentationRules } from '../../providers/formatters/IndentationRules';
import { AlignmentRules } from '../../providers/formatters/AlignmentRules';
suite('Formatter Test Suite', () => {
test('IndentationRules: if/then/end Block korrekt einrücken', () => {
const indentRules = new IndentationRules();
const state = new FormatState();
state.tabSize = 4;
// Zeile 1: if condition then (erwartet: 0 Leerzeichen, aber Level springt auf 1 für die nächste Zeile)
let result = indentRules.apply('if x = 1 then', state);
assert.strictEqual(result, 'if x = 1 then');
assert.strictEqual(state.indentLevel, 1);
// Zeile 2: normaler Code im Block (erwartet: 4 Leerzeichen)
result = indentRules.apply('log.debug("test");', state);
assert.strictEqual(result, ' log.debug("test");');
assert.strictEqual(state.indentLevel, 1);
// Zeile 3: end if; (erwartet: 0 Leerzeichen, Level springt sofort vor der Ausgabe auf 0)
result = indentRules.apply('end if;', state);
assert.strictEqual(result, 'end if;');
assert.strictEqual(state.indentLevel, 0);
});
test('IndentationRules: elif und else verhalten sich korrekt', () => {
const indentRules = new IndentationRules();
const state = new FormatState();
state.tabSize = 4;
state.indentLevel = 1; // Wir starten quasi IN einem offenen if-Block
// elif muss zurückspringen (auf 0) und danach wieder auf 1 für den folgenden Block
let result = indentRules.apply('elif x = 2 then', state);
assert.strictEqual(result, 'elif x = 2 then');
assert.strictEqual(state.indentLevel, 1);
});
test('AlignmentRules: Visual Alignment an runden Klammern', () => {
const alignRules = new AlignmentRules();
const state = new FormatState();
// 1. Zeile mit öffnender Klammer
alignRules.apply('discovery.runCommand(', 'discovery.runCommand(', state);
assert.strictEqual(state.parenAlignments.length, 1);
assert.strictEqual(state.parenAlignments[0], 21); // 21 Zeichen bis nach der offenen Klammer
// 2. Zeile (Parameter soll exakt bis Spalte 21 eingerückt werden)
const result = alignRules.apply('host,', 'host,', state);
assert.strictEqual(result, ' host,');
});
});

View File

@ -1,45 +0,0 @@
import * as assert from 'assert';
import { LinterContext } from '../../core/LinterContext';
import { SyntaxRule } from '../../core/rules/SyntaxRule';
suite('Linter SyntaxRule Test Suite', () => {
test('Zuweisung: Warnt bei "=" statt ":="', () => {
const rule = new SyntaxRule();
const ctx = new LinterContext();
// Wir simulieren eine Code-Zeile
const rawLine = " my_var = 123;";
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
// Erwartung: Ein Fehler wegen falscher Zuweisung
assert.strictEqual(ctx.diagnostics.length, 1);
assert.ok(ctx.diagnostics[0].message.includes("In TPL wird ':=' für Zuweisungen genutzt"));
});
test('Zuweisung: Akzeptiert "=" als Vergleich in if-Bedingung', () => {
const rule = new SyntaxRule();
const ctx = new LinterContext();
// Wir simulieren eine if-Zeile (setzt ctx.inConditionBlock intern auf true)
const rawLine = " if my_var = 123 then";
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
// Erwartung: Kein Fehler, da das = als Vergleich legitim ist
assert.strictEqual(ctx.diagnostics.length, 0);
});
test('Klammern: Findet ungeschlossene runde Klammern', () => {
const rule = new SyntaxRule();
const ctx = new LinterContext();
// Wir öffnen eine Klammer, schließen sie aber nicht
const rawLine = " my_func(parameter;";
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
// checkEnd wird am Ende der Datei aufgerufen
rule.checkEnd(['', rawLine], ctx);
assert.strictEqual(ctx.diagnostics.length, 1);
assert.ok(ctx.diagnostics[0].message.includes("Öffnende Klammer '(' wird nicht geschlossen!"));
});
});

File diff suppressed because it is too large Load Diff

View File

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

View File

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