Komplettes update für die funktionen und beschreibungen
This commit is contained in:
parent
5b150ce550
commit
684d760f4b
113
README.md
113
README.md
|
|
@ -1,71 +1,68 @@
|
|||
# bmc-tpl-code-all-around README
|
||||
# BMC TPL Code All-Around
|
||||
|
||||
This is the README for your extension "bmc-tpl-code-all-around". After writing up a brief description, we recommend including the following sections.
|
||||
**Linter, Auto-Formatter, IntelliSense und Hover-Dokumentationen für die BMC TPL (Template Language).**[cite: 3]
|
||||
|
||||
## Features
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Following extension guidelines
|
||||
## ✨ Features
|
||||
|
||||
Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension.
|
||||
### 🤖 Intelligenter Linter (Syntax-Scanner)
|
||||
Der Linter analysiert den Code in Echtzeit und warnt vor typischen TPL-Fehlern:
|
||||
* **Strikte Hierarchie-Prüfung:** Überwacht die korrekte Reihenfolge der Blöcke innerhalb von Patterns und Syncmappings (`metadata` -> `overview` -> `constants` -> `triggers` -> `mapping`/`removal` -> `body`).
|
||||
* **Semicolon- & Zuweisungs-Check:** Findet fehlende Semikolons (ignoriert sie aber intelligent in `enumeration`-Blöcken) und warnt, wenn `=` anstelle von `:=` für Zuweisungen genutzt wird.
|
||||
* **Block- & Klammer-Tracking:** Erkennt nicht geschlossene Blöcke (wie fehlende `end body;` oder `end pattern;`) und findet ungeschlossene runde `(` oder eckige `[` Klammern.
|
||||
* **Pflichtfeld-Validierung:** Prüft, ob auf Modul-Ebene zwingend benötigte Metadaten (`origin`, `author`, `products`, `publishers`, `tree_path`) vorhanden sind und ob im `body` ein initiales `log.debug` Statement steht.
|
||||
* **Toleranz-Modus:** Erkennt automatisch TPL-Versionen (z.B. > 1.23) und stuft in neueren Versionen Fehler zu reinen Warnungen herab, um zukünftige Kompatibilität zu gewährleisten.
|
||||
|
||||
* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)
|
||||
### 🎨 Auto-Formatter & Visual Alignment (`Shift + Alt + F`)
|
||||
Schluss mit kaputten Einrückungen! Der Formatter bringt deinen Code in Bestform:
|
||||
* **VS Code Standard:** Erzwingt strikt 4 Leerzeichen pro Einrückungs-Ebene.
|
||||
* **Dynamische Block-Einrückung:** Erkennt automatisch startende und endende Blöcke (`if`, `for`, `pattern`, `table` etc.) und passt die Einrückungsebene an.
|
||||
* **Klammer-Gedächtnis (Visual Alignment):** Die Königsdisziplin! Der Formatter merkt sich die spaltengenaue Position von geöffneten Arrays `[` oder Funktionsaufrufen `(` und richtet alle Folgezeilen makellos an dieser Klammer aus. Schließende Klammern werden auf Wunsch perfekt darunter platziert.
|
||||
|
||||
## Working with Markdown
|
||||
### 💡 IntelliSense & Autovervollständigung (Cross-File)
|
||||
Spart extrem viel Zeit beim Coden durch Kontext-Wissen:
|
||||
* **Workspace-Scanning:** Importierst du ein Modul (z.B. `from company.helpers import my_helper`), durchsucht das Plugin im Hintergrund dein gesamtes Projekt. Tippst du `my_helper.`, werden dir automatisch alle verfügbaren `define`-Methoden aus der fremden Datei vorgeschlagen!
|
||||
* **Lokale Namespaces:** Erkennt `definitions`-Blöcke in der gleichen Datei und schlägt deren Methoden vor.
|
||||
* **Variablen-Tracking:** Lernt alle Variablen, die du mit `:=` definiert hast, und schlägt sie dir später vor.
|
||||
* **Log-Filter:** Nach dem Tippen von `log.` zeigt VS Code strikt nur noch die validen Methoden (`debug`, `info`, `warn`, `error`) an.
|
||||
|
||||
You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts:
|
||||
### 🔍 Hover-Dokumentation (Tooltips)
|
||||
Wenn du mit der Maus über einem Methodennamen, einem Pattern oder einer Tabelle stehen bleibst:
|
||||
* **DocString Extraction:** Das Plugin liest Multi-Line-Kommentare (`"""` oder `'''`), die direkt unter einer `define` oder `pattern` Deklaration stehen, aus und rendert sie als saubere Markdown-Tooltips.
|
||||
* **Projektübergreifend:** Funktioniert auch für Methoden aus importierten Dateien! Du musst die aktuelle Datei nicht mehr verlassen, um zu sehen, was eine Hilfsfunktion macht.
|
||||
|
||||
* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux).
|
||||
* 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)
|
||||
|
||||
**Enjoy!**
|
||||
Einfach das Keyword tippen und `Enter` drücken, um komplette, perfekt eingerückte Blöcke zu generieren:
|
||||
|
||||
| Keyword | Beschreibung |
|
||||
| :--- | :--- |
|
||||
| `tpl_skeleton` | **God-Snippet**: Generiert eine komplett frische `.tpl` Datei inklusive Modul-Definition, Pflicht-Metadaten, Pattern, Triggern und Log-Statements. |
|
||||
| `pattern` | Baut einen leeren Pattern-Block inklusive DocString und `end pattern;`. |
|
||||
| `syncmapping` | Baut einen leeren Syncmapping-Block inklusive DocString. |
|
||||
| `define` | Generiert eine Methodendefinition (`define name(params) ... end define;`). |
|
||||
| `metadata`, `overview`, `body`... | Baut den jeweiligen Block auf und platziert den Cursor in der Mitte. |
|
||||
| `table` | Erstellt eine Tabelle inklusive des `default -> "unknown";` Fallbacks. |
|
||||
| `if`, `for` | Baut Standard-Schleifen und Abfragen. |
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Installation & Nutzung
|
||||
|
||||
1. Das Plugin aktivieren.
|
||||
2. TPL-Dateien (Endung `.tpl`) öffnen.
|
||||
3. Der Linter startet automatisch bei jedem Tastendruck.
|
||||
4. **Formatieren:** Rechtsklick -> *Dokument formatieren* (oder `Shift + Alt + F`).
|
||||
5. **Autovervollständigung:** Triggert automatisch beim Tippen, speziell nach einem Punkt (`.`) bei Modul-Aufrufen.
|
||||
6. **Quick Fixes:** Auf die gelbe/rote Glühbirne am Zeilenanfang klicken oder `Ctrl + .` (Windows/Linux) / `Cmd + .` (Mac) drücken.
|
||||
|
|
@ -0,0 +1 @@
|
|||
vsce package
|
||||
24
package.json
24
package.json
|
|
@ -3,19 +3,31 @@
|
|||
"displayName": "BMC TPL Code All around",
|
||||
"description": "Code linter, tracker and all around tool for coding in BMC Helix TPL",
|
||||
"version": "0.0.1",
|
||||
"publisher": "kuro",
|
||||
"engines": {
|
||||
"vscode": "^1.140.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [],
|
||||
"activationEvents": [
|
||||
"onLanguage:tpl"
|
||||
],
|
||||
"main": "./dist/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
"languages": [
|
||||
{
|
||||
"command": "bmc-tpl-code-all-around.helloWorld",
|
||||
"title": "Hello World"
|
||||
"id": "tpl",
|
||||
"aliases": ["TPL", "tpl"],
|
||||
"extensions": [".tpl"],
|
||||
"configuration": "./language-configuration.json"
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "tpl",
|
||||
"scopeName": "source.tpl",
|
||||
"path": "./syntaxes/tpl.tmLanguage.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -44,9 +56,5 @@
|
|||
"typescript": "^6.0.3",
|
||||
"@vscode/test-cli": "^0.0.15",
|
||||
"@vscode/test-electron": "^3.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"diff": "^8.0.4",
|
||||
"serialize-javascript": "^7.0.6"
|
||||
}
|
||||
}
|
||||
|
|
@ -2,60 +2,66 @@ 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[] = [];
|
||||
public provideCodeActions(
|
||||
document: vscode.TextDocument,
|
||||
range: vscode.Range | vscode.Selection,
|
||||
context: vscode.CodeActionContext,
|
||||
token: vscode.CancellationToken
|
||||
): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> {
|
||||
|
||||
for (const diagnostic of context.diagnostics) {
|
||||
const actionList: vscode.CodeAction[] = [];
|
||||
|
||||
// Fix für fehlende Metadaten
|
||||
if (diagnostic.code === 'missing_metadata') {
|
||||
const fix = new vscode.CodeAction('Fehlende Metadaten einfügen', vscode.CodeActionKind.QuickFix);
|
||||
fix.edit = new vscode.WorkspaceEdit();
|
||||
for (const diag of context.diagnostics) {
|
||||
|
||||
const missingParts = diagnostic.message.replace('Fehlende Pflicht-Metadaten: ', '').split(', ');
|
||||
let insertText = '';
|
||||
// 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();
|
||||
|
||||
// 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`; }
|
||||
const fehlendeFelder = diag.message.replace('Fehlende Pflicht-Metadaten (Modul-Ebene): ', '').split(', ');
|
||||
let neuerText = '';
|
||||
|
||||
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText);
|
||||
fix.diagnostics = [diagnostic];
|
||||
fix.isPreferred = true;
|
||||
actions.push(fix);
|
||||
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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
// 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 expectedLog = diagnostic.source as string;
|
||||
const insertText = ` ${expectedLog}\n`; // Auch hier 3 Spaces
|
||||
const erwartetesLog = diag.source as string;
|
||||
const neuerText = ` ${erwartetesLog}\n`;
|
||||
|
||||
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line, 0), insertText);
|
||||
fix.diagnostics = [diagnostic];
|
||||
fix.isPreferred = true;
|
||||
actions.push(fix);
|
||||
action.edit.insert(document.uri, new vscode.Position(diag.range.start.line + 1, 0), neuerText);
|
||||
action.diagnostics = [diag];
|
||||
action.isPreferred = true;
|
||||
actionList.push(action);
|
||||
}
|
||||
|
||||
// Fix für 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();
|
||||
// 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 line = document.lineAt(diagnostic.range.start.line);
|
||||
const fixedText = line.text.replace(/\t/g, ' ');
|
||||
const zeile = document.lineAt(diag.range.start.line);
|
||||
const bereinigterText = zeile.text.replace(/\t/g, ' ');
|
||||
|
||||
fix.edit.replace(document.uri, line.range, fixedText);
|
||||
fix.diagnostics = [diagnostic];
|
||||
fix.isPreferred = true;
|
||||
actions.push(fix);
|
||||
action.edit.replace(document.uri, zeile.range, bereinigterText);
|
||||
action.diagnostics = [diag];
|
||||
action.isPreferred = true;
|
||||
actionList.push(action);
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
return actionList;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
import * as vscode from 'vscode';
|
||||
|
||||
export class TplCompletionProvider implements vscode.CompletionItemProvider {
|
||||
async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.CompletionItem[]> {
|
||||
const linePrefix = document.lineAt(position).text.substring(0, position.character);
|
||||
const items: vscode.CompletionItem[] = [];
|
||||
const text = document.getText();
|
||||
|
||||
// 1. Die feste "log." Regel
|
||||
if (linePrefix.endsWith('log.')) {
|
||||
['debug', 'info', 'warn', 'error'].forEach(method => {
|
||||
items.push(new vscode.CompletionItem(method, vscode.CompletionItemKind.Method));
|
||||
});
|
||||
return items; // Zwingt VS Code, nur diese 4 anzuzeigen!
|
||||
}
|
||||
|
||||
// 2. Importe und lokale Definitions-Blöcke (Namespaces) sammeln
|
||||
const importMap = new Map<string, string>();
|
||||
const importRegex = /from\s+([\w.]+)\s+import\s+(\w+)/g;
|
||||
let m;
|
||||
while ((m = importRegex.exec(text)) !== null) {
|
||||
importMap.set(m[2], m[1]);
|
||||
}
|
||||
|
||||
const localNamespaces = new Set<string>();
|
||||
const defBlockRegex = /^\s*definitions\s+([a-zA-Z0-9_]+)/gm;
|
||||
while ((m = defBlockRegex.exec(text)) !== null) {
|
||||
localNamespaces.add(m[1]);
|
||||
}
|
||||
|
||||
// 3. Wenn wir auf freier Fläche tippen (kein Punkt davor)
|
||||
const matchDot = linePrefix.match(/([a-zA-Z0-9_]+)\.$/);
|
||||
|
||||
if (!matchDot) {
|
||||
// A) Namespaces (Importiert und Lokal) vorschlagen
|
||||
importMap.forEach((_, key) => {
|
||||
const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Module);
|
||||
item.detail = "Importiertes Modul";
|
||||
items.push(item);
|
||||
});
|
||||
localNamespaces.forEach(ns => {
|
||||
const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module);
|
||||
item.detail = "Lokales Modul (definitions)";
|
||||
items.push(item);
|
||||
});
|
||||
|
||||
// B) Lokale Variablen vorschlagen (z.B. attribute_list :=)
|
||||
const varRegex = /\b([a-zA-Z0-9_]+)\s*:=/g;
|
||||
const variables = new Set<string>();
|
||||
while ((m = varRegex.exec(text)) !== null) {
|
||||
variables.add(m[1]);
|
||||
}
|
||||
variables.forEach(v => {
|
||||
items.push(new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable));
|
||||
});
|
||||
|
||||
// C) Das große Skeleton Snippet
|
||||
const skeletonItem = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
|
||||
skeletonItem.insertText = new vscode.SnippetString(
|
||||
'tpl 1.23 module ${1:module_name};\n\n' +
|
||||
'metadata\n' +
|
||||
' origin := \'${2:origin}\';\n' +
|
||||
' author := \'${3:author}\';\n' +
|
||||
' products := \'${4:products}\';\n' +
|
||||
' publishers := \'${5:publishers}\';\n' +
|
||||
' tree_path := \'${6:company}\', \'${7:category}\', \'${8:subcategory}\';\n' +
|
||||
'end metadata;\n\n' +
|
||||
'pattern ${9:pattern_name} 1.0\n' +
|
||||
' """\n' +
|
||||
' TODO: explain\n' +
|
||||
' """\n\n' +
|
||||
' triggers\n' +
|
||||
' on process := DiscoveredProcess where cmd matches regex_escape("${10:process}");\n' +
|
||||
' end triggers;\n\n' +
|
||||
' body\n' +
|
||||
' log.debug("Start pattern ${9:pattern_name}");\n' +
|
||||
' $0\n' +
|
||||
' end body;\n' +
|
||||
'end pattern;'
|
||||
);
|
||||
skeletonItem.detail = "Generiert das komplette TPL Modul-Grundgerüst";
|
||||
items.push(skeletonItem);
|
||||
|
||||
// D) Block-Snippets
|
||||
const blocks = ['metadata', 'overview', 'constants', 'triggers', 'body', 'mapping', 'removal', 'definitions', 'enumeration', 'configuration'];
|
||||
for (const block of blocks) {
|
||||
const item = new vscode.CompletionItem(block, vscode.CompletionItemKind.Snippet);
|
||||
item.insertText = new vscode.SnippetString(`${block}\n $0\nend ${block};`);
|
||||
item.detail = `Fügt ${block} Block mit 'end' ein`;
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
const defineItem = new vscode.CompletionItem('define', vscode.CompletionItemKind.Snippet);
|
||||
defineItem.insertText = new vscode.SnippetString('define ${1:name}(${2:params})\n $0\nend define;');
|
||||
items.push(defineItem);
|
||||
|
||||
const patternItem = new vscode.CompletionItem('pattern', vscode.CompletionItemKind.Snippet);
|
||||
patternItem.insertText = new vscode.SnippetString('pattern ${1:name}${2:1.0}\n """\n TODO: explain\n """\n\n $0\nend pattern;');
|
||||
items.push(patternItem);
|
||||
|
||||
const syncmappingItem = new vscode.CompletionItem('syncmapping', vscode.CompletionItemKind.Snippet);
|
||||
syncmappingItem.insertText = new vscode.SnippetString('syncmapping ${1:name}${2:1.0}\n """\n TODO: explain\n """\n\n $0\nend syncmapping;');
|
||||
items.push(syncmappingItem);
|
||||
|
||||
const tableItem = new vscode.CompletionItem('table', vscode.CompletionItemKind.Snippet);
|
||||
tableItem.insertText = new vscode.SnippetString('table ${1:name}${2:1.0}\n $0\n default -> "unknown";\nend table;');
|
||||
items.push(tableItem);
|
||||
|
||||
const ifItem = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet);
|
||||
ifItem.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;');
|
||||
items.push(ifItem);
|
||||
|
||||
const forItem = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet);
|
||||
forItem.insertText = new vscode.SnippetString('for ${1:item} in${2:list} do\n $0\nend for;');
|
||||
items.push(forItem);
|
||||
|
||||
return items;
|
||||
} else {
|
||||
// 4. Wir haben nach einem Punkt getippt (z.B. helpers.)
|
||||
const ns = matchDot[1];
|
||||
const moduleMethods: vscode.CompletionItem[] = [];
|
||||
|
||||
// Regex sucht nach dem Wort direkt vor der Klammer: define [add] method_name(
|
||||
const extractDefineRegex = /^\s*define\s+.*?\b([a-zA-Z0-9_]+)\s*\(/gm;
|
||||
|
||||
// Fall A: Lokaler Definitions-Block (gleiche Datei!)
|
||||
if (localNamespaces.has(ns)) {
|
||||
let defMatch;
|
||||
while ((defMatch = extractDefineRegex.exec(text)) !== null) {
|
||||
const methodName = defMatch[1];
|
||||
const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method);
|
||||
const docStr = this.extractDocString(text, defMatch.index);
|
||||
if (docStr) {
|
||||
item.documentation = new vscode.MarkdownString(docStr);
|
||||
}
|
||||
moduleMethods.push(item);
|
||||
}
|
||||
return moduleMethods;
|
||||
}
|
||||
|
||||
// Fall B: Importiertes Modul (externe Datei)
|
||||
if (importMap.has(ns)) {
|
||||
const modPath = importMap.get(ns)!;
|
||||
const uris = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**', 50);
|
||||
for (const uri of uris) {
|
||||
try {
|
||||
const docBytes = await vscode.workspace.fs.readFile(uri);
|
||||
const docText = Buffer.from(docBytes).toString('utf8');
|
||||
|
||||
const modRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${modPath.replace(/\./g, '\\\\.')}\\s*;`);
|
||||
if (modRegex.test(docText)) {
|
||||
let defMatch;
|
||||
while ((defMatch = extractDefineRegex.exec(docText)) !== null) {
|
||||
const methodName = defMatch[1];
|
||||
const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method);
|
||||
|
||||
const docStr = this.extractDocString(docText, defMatch.index);
|
||||
if (docStr) {
|
||||
item.documentation = new vscode.MarkdownString(docStr);
|
||||
}
|
||||
moduleMethods.push(item);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
if (moduleMethods.length > 0) {
|
||||
return moduleMethods;
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private extractDocString(text: string, startIndex: number): string | null {
|
||||
const lines = text.substring(startIndex).split('\n');
|
||||
let docString = "";
|
||||
let inDocString = false;
|
||||
let quoteType = "";
|
||||
|
||||
for (let j = 1; j < Math.min(15, lines.length); j++) {
|
||||
const line = lines[j].trim();
|
||||
if (line.length === 0) continue;
|
||||
|
||||
if (!inDocString) {
|
||||
if (line.startsWith('"""') || line.startsWith("'''")) {
|
||||
inDocString = true;
|
||||
quoteType = line.substring(0, 3);
|
||||
if (line.length > 3 && line.endsWith(quoteType)) {
|
||||
docString = line.substring(3, line.length - 3).trim();
|
||||
break;
|
||||
}
|
||||
const content = line.substring(3).trim();
|
||||
if (content) docString += content + "\n";
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (line.endsWith(quoteType)) {
|
||||
const content = line.substring(0, line.length - 3).trim();
|
||||
if (content) docString += content;
|
||||
break;
|
||||
} else {
|
||||
docString += line + "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
return docString.trim() || null;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,39 +2,98 @@ 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[] = [];
|
||||
const lines = document.getText().split('\n');
|
||||
|
||||
let indentLevel = 0;
|
||||
// Zwingt den Formatter strikt zu 3 Spaces (ignoriert VS Code Settings)
|
||||
const indentString = ' ';
|
||||
let inTripleDouble = false;
|
||||
let inTripleSingle = false;
|
||||
let consecutiveEmptyLines = 0;
|
||||
|
||||
const alignmentStack: number[] = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i].trim();
|
||||
if (line.length === 0) { continue; }
|
||||
const rawLine = lines[i];
|
||||
let line = rawLine.trim();
|
||||
|
||||
// 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/)) {
|
||||
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);
|
||||
}
|
||||
|
||||
// 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));
|
||||
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;
|
||||
}
|
||||
|
||||
// 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/)) {
|
||||
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,123 @@
|
|||
import * as vscode from 'vscode';
|
||||
|
||||
export class TplHoverProvider implements vscode.HoverProvider {
|
||||
async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | null> {
|
||||
const wordRange = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
|
||||
if (!wordRange) return null;
|
||||
|
||||
const fullWord = document.getText(wordRange);
|
||||
const currentText = document.getText();
|
||||
|
||||
let namespace = "";
|
||||
let methodOrPatternName = fullWord;
|
||||
|
||||
if (fullWord.includes('.')) {
|
||||
const parts = fullWord.split('.');
|
||||
namespace = parts[0];
|
||||
methodOrPatternName = parts[1];
|
||||
}
|
||||
|
||||
// FEHLERBEHEBUNG: [ \t] statt \s verwenden!
|
||||
// \s frisst Zeilenumbrüche (\n), was dazu geführt hat, dass match.index
|
||||
// auf leere Zeilen oberhalb der Methode gezeigt hat -> Leeres Hover-Fenster.
|
||||
const searchRegex = new RegExp(`^[ \\t]*(?:(?:define[ \\t]+(?:[a-zA-Z0-9_]+[ \\t]+)*${methodOrPatternName}[ \\t]*\\()|(?:(?:pattern|syncmapping|table|enumeration|configuration)[ \\t]+${methodOrPatternName}\\b))`, 'm');
|
||||
|
||||
// 1. Suche im aktuellen Dokument (Ohne Namespace)
|
||||
if (!namespace) {
|
||||
const match = searchRegex.exec(currentText);
|
||||
if (match) {
|
||||
return this.buildHover(currentText, match.index);
|
||||
}
|
||||
return null; // Bricht sauber ab, ohne das System zu blockieren
|
||||
}
|
||||
|
||||
// 2. Suche mit Namespace (z.B. helpers.method)
|
||||
// A) Ist der Namespace in derselben Datei? (lokaler definitions block)
|
||||
const localDefRegex = new RegExp(`^[ \\t]*definitions[ \\t]+${namespace}\\b`, 'm');
|
||||
if (localDefRegex.test(currentText)) {
|
||||
const match = searchRegex.exec(currentText);
|
||||
if (match) {
|
||||
return this.buildHover(currentText, match.index);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// B) Ist der Namespace importiert? (Workspace durchsuchen)
|
||||
const importRegex = new RegExp(`from\\s+([\\w.]+)\\s+import\\s+${namespace}\\b`);
|
||||
const importMatch = importRegex.exec(currentText);
|
||||
|
||||
if (importMatch) {
|
||||
const modPath = importMatch[1];
|
||||
const uris = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**', 50);
|
||||
for (const uri of uris) {
|
||||
try {
|
||||
const docBytes = await vscode.workspace.fs.readFile(uri);
|
||||
const docText = Buffer.from(docBytes).toString('utf8');
|
||||
|
||||
const modRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${modPath.replace(/\./g, '\\\\.')}\\s*;`);
|
||||
if (modRegex.test(docText)) {
|
||||
const match = searchRegex.exec(docText);
|
||||
if (match) {
|
||||
return this.buildHover(docText, match.index);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private buildHover(text: string, matchIndex: number): vscode.Hover {
|
||||
const docStr = this.extractDocString(text, matchIndex);
|
||||
const markdown = new vscode.MarkdownString();
|
||||
|
||||
// Schneidet exakt die Code-Zeile aus (ohne vorausgehende Leerzeilen)
|
||||
const declLine = text.substring(matchIndex).split('\n')[0].trim();
|
||||
markdown.appendCodeblock(declLine, 'tpl');
|
||||
|
||||
if (docStr) {
|
||||
markdown.appendMarkdown(`---\n*${docStr}*`);
|
||||
} else {
|
||||
markdown.appendMarkdown(`---\n*(Keine Dokumentation gefunden)*`);
|
||||
}
|
||||
return new vscode.Hover(markdown);
|
||||
}
|
||||
|
||||
private extractDocString(text: string, startIndex: number): string | null {
|
||||
const lines = text.substring(startIndex).split('\n');
|
||||
let docString = "";
|
||||
let inDocString = false;
|
||||
let quoteType = "";
|
||||
|
||||
for (let j = 1; j < Math.min(15, lines.length); j++) {
|
||||
const line = lines[j].trim();
|
||||
if (line.length === 0) continue;
|
||||
|
||||
if (!inDocString) {
|
||||
if (line.startsWith('"""') || line.startsWith("'''")) {
|
||||
inDocString = true;
|
||||
quoteType = line.substring(0, 3);
|
||||
if (line.length > 3 && line.endsWith(quoteType)) {
|
||||
docString = line.substring(3, line.length - 3).trim();
|
||||
break;
|
||||
}
|
||||
const content = line.substring(3).trim();
|
||||
if (content) docString += content + "\n";
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (line.endsWith(quoteType)) {
|
||||
const content = line.substring(0, line.length - 3).trim();
|
||||
if (content) docString += content;
|
||||
break;
|
||||
} else {
|
||||
docString += line + "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
return docString.trim() || null;
|
||||
}
|
||||
}
|
||||
345
src/extension.ts
345
src/extension.ts
|
|
@ -1,41 +1,29 @@
|
|||
import * as vscode from 'vscode';
|
||||
import { TplCodeActionProvider } from './TplCodeActionProvider';
|
||||
import { TplDocumentFormatter } from './TplDocumentFormatter';
|
||||
import { TplCompletionProvider } from './TplCompletionProvider';
|
||||
import { TplHoverProvider } from './TplHoverProvider';
|
||||
|
||||
// --- Konstanten ---
|
||||
const SUPPORTED_TPL_VERSION = 23; // TPL 1.23
|
||||
const SUPPORTED_TPL_VERSION = 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
|
||||
}
|
||||
enum PatternPhase { None = 0, Metadata = 1, Overview = 2, Constants = 3, Triggers = 4, Mapping = 5, Removal = 6, Body = 7 }
|
||||
|
||||
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);
|
||||
|
||||
// 2. Events registrieren (Ausführen beim Öffnen und Ändern)
|
||||
vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection));
|
||||
vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection));
|
||||
|
||||
// 3. Quick-Fix Provider (Code Actions für Glühbirnen-Menü) registrieren
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeActionsProvider('tpl', new TplCodeActionProvider(), {
|
||||
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix]
|
||||
})
|
||||
);
|
||||
context.subscriptions.push(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()));
|
||||
|
||||
// 4. Format Document Provider (Auto-Aufräumen mit Shift+Alt+F)
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerDocumentFormattingEditProvider('tpl', new TplDocumentFormatter())
|
||||
);
|
||||
for (const doc of vscode.workspace.textDocuments) {
|
||||
validateTplDocument(doc, diagnosticCollection);
|
||||
}
|
||||
}
|
||||
|
||||
function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
|
||||
|
|
@ -49,131 +37,201 @@ function validateTplDocument(document: vscode.TextDocument, collection: vscode.D
|
|||
let toleranceMode = false;
|
||||
let hasModuleDef = false;
|
||||
|
||||
// --- State Machine Variablen ---
|
||||
let inBlock: 'none' | 'pattern' | 'table' | 'definitions' = 'none';
|
||||
let inBlock: 'none' | 'pattern' | 'table' | 'definitions' | 'enumeration' | 'configuration' = '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;
|
||||
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[] = [];
|
||||
|
||||
// --- 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).`);
|
||||
vscode.window.showInformationMessage(`TPL 1.${tplVersion} erkannt. Linter läuft im Toleranz-Modus.`);
|
||||
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;
|
||||
}
|
||||
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 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,
|
||||
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
|
||||
);
|
||||
const diag = addDiag(i, tabIndex, tabIndex + 1, "Bitte Tabs durch Spaces ersetzen", 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;
|
||||
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; }
|
||||
|
||||
// --- 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(',')) {
|
||||
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|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|\/\/|\/\*|\*)/;
|
||||
|
||||
// Enumerations benötigen keine Semikolons
|
||||
if (!noSemicolonNeeded.test(line) && !justOpenedString && parenStack.length === 0 && bracketStack.length === 0 && inBlock !== 'enumeration') {
|
||||
const lineToCheck = line.split('//')[0].trim();
|
||||
if (!lineToCheck.endsWith(';') && !lineToCheck.endsWith('{') && !lineToCheck.endsWith(',')) {
|
||||
addDiag(i, rawLine.length - 1 < 0 ? 0 : rawLine.length - 1, rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modul-Definition prüfen (Muss als Erstes stehen)
|
||||
if (!hasModuleDef) {
|
||||
if (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('tpl ')) { hasModuleDef = true; }
|
||||
else { addDiag(i, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module <name>;' beginnen.", vscode.DiagnosticSeverity.Error); hasModuleDef = true; }
|
||||
}
|
||||
|
||||
// --- Modul-Metadaten Logik ---
|
||||
if (line.startsWith('metadata') && inBlock === 'none') {
|
||||
inModuleMetadata = true;
|
||||
hasModuleMetadata = true;
|
||||
moduleMetadataBlockLine = i;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inModuleMetadata) {
|
||||
if (line.match(/^origin\b/)) { foundModuleMetadata.origin = true; }
|
||||
if (line.match(/^author\b/)) { foundModuleMetadata.author = true; }
|
||||
if (line.match(/^products\b/)) { foundModuleMetadata.products = true; }
|
||||
if (line.match(/^publishers\b/)) { foundModuleMetadata.publishers = true; }
|
||||
if (line.match(/^tree_path\b/)) { foundModuleMetadata.tree_path = true; }
|
||||
|
||||
if (line.startsWith('end metadata')) {
|
||||
inModuleMetadata = false;
|
||||
const missing = Object.keys(foundModuleMetadata).filter(k => !(foundModuleMetadata as any)[k]);
|
||||
if (missing.length > 0 && moduleMetadataBlockLine !== -1) {
|
||||
const diag = addDiag(moduleMetadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten (Modul-Ebene): ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
|
||||
diag.code = 'missing_module_metadata';
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Block checks
|
||||
if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) {
|
||||
if (!hasModuleMetadata && inBlock === 'none') {
|
||||
addDiag(i, 0, line.length, "Ein 'metadata' Block auf Modul-Ebene ist Pflicht, bevor andere Blöcke definiert werden.", vscode.DiagnosticSeverity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Block-Einstiege (Pattern & Table) ---
|
||||
if (line.startsWith('pattern ')) {
|
||||
if ((inBlock as string) !== 'none') { addDiag(i, 0, 7, "Ein Pattern kann nicht innerhalb eines anderen Blocks deklariert 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\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;
|
||||
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 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;
|
||||
if (inBlock !== 'none') { addDiag(i, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
||||
inBlock = 'table'; continue;
|
||||
}
|
||||
|
||||
// --- Innerhalb eines Patterns ---
|
||||
if ((inBlock as string) === 'pattern') {
|
||||
if (inBlock === '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';
|
||||
}
|
||||
}
|
||||
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;
|
||||
if (phaseEnum === PatternPhase.Metadata) { metadataBlockLine = i; }
|
||||
activePhaseName = phaseName;
|
||||
activePhaseLine = i;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -181,81 +239,66 @@ function validateTplDocument(document: vscode.TextDocument, collection: vscode.D
|
|||
checkPhase('overview', PatternPhase.Overview);
|
||||
checkPhase('constants', PatternPhase.Constants);
|
||||
checkPhase('triggers', PatternPhase.Triggers);
|
||||
checkPhase('body', PatternPhase.Body);
|
||||
checkPhase('mapping', PatternPhase.Mapping);
|
||||
checkPhase('removal', PatternPhase.Removal);
|
||||
|
||||
if ((currentPatternPhase as PatternPhase) === PatternPhase.Metadata) {
|
||||
if (line.startsWith('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 (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 ((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;
|
||||
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 = ""; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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) {
|
||||
const eqMatch = rawLine.match(/(?<![<>=!:])=(?![=])/);
|
||||
if (eqMatch && eqMatch.index && !justOpenedString) {
|
||||
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);
|
||||
}
|
||||
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')) {
|
||||
if ((inBlock as string) !== 'table') {
|
||||
addDiag(i, 0, 9, "'end table' gefunden, aber es wurde keine Tabelle gestartet.", vscode.DiagnosticSeverity.Error);
|
||||
}
|
||||
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);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,48 +0,0 @@
|
|||
# Welcome to your VS Code Extension
|
||||
|
||||
## What's in the folder
|
||||
|
||||
* This folder contains all of the files necessary for your extension.
|
||||
* `package.json` - this is the manifest file in which you declare your extension and command.
|
||||
* The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
|
||||
* `src/extension.ts` - this is the main file where you will provide the implementation of your command.
|
||||
* The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
|
||||
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
|
||||
|
||||
## Setup
|
||||
|
||||
* install the recommended extensions (amodio.tsl-problem-matcher, ms-vscode.extension-test-runner, and dbaeumer.vscode-eslint)
|
||||
|
||||
|
||||
## Get up and running straight away
|
||||
|
||||
* Press `F5` to open a new window with your extension loaded.
|
||||
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
|
||||
* Set breakpoints in your code inside `src/extension.ts` to debug your extension.
|
||||
* Find output from your extension in the debug console.
|
||||
|
||||
## Make changes
|
||||
|
||||
* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
|
||||
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
|
||||
|
||||
|
||||
## Explore the API
|
||||
|
||||
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
|
||||
|
||||
## Run tests
|
||||
|
||||
* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner)
|
||||
* Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered.
|
||||
* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A`
|
||||
* See the output of the test result in the Test Results view.
|
||||
* Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder.
|
||||
* The provided test runner will only consider files matching the name pattern `**.test.ts`.
|
||||
* You can create folders inside the `test` folder to structure your tests any way you want.
|
||||
|
||||
## Go further
|
||||
|
||||
* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
|
||||
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace.
|
||||
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).
|
||||
Loading…
Reference in New Issue