Initial commit
This commit is contained in:
commit
c0cb31cdb1
|
|
@ -0,0 +1,5 @@
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.vscode-test/
|
||||||
|
*.vsix
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { defineConfig } from '@vscode/test-cli';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
files: 'out/test/**/*.test.js',
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
// A launch configuration that compiles the extension and then opens it inside a new window
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Run Extension",
|
||||||
|
"type": "extensionHost",
|
||||||
|
"request": "launch",
|
||||||
|
"args": [
|
||||||
|
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||||
|
],
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/dist/**/*.js"
|
||||||
|
],
|
||||||
|
"preLaunchTask": "${defaultBuildTask}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Place your settings in this file to overwrite default and user settings.
|
||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"out": false, // set this to true to hide the "out" folder with the compiled JS files
|
||||||
|
"dist": false // set this to true to hide the "dist" folder with the compiled JS files
|
||||||
|
},
|
||||||
|
"search.exclude": {
|
||||||
|
"out": true, // set this to false to include "out" folder in search results
|
||||||
|
"dist": true // set this to false to include "dist" folder in search results
|
||||||
|
},
|
||||||
|
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||||
|
"js/ts.tsc.autoDetect": "off"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
// for the documentation about the tasks.json format
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"dependsOn": [
|
||||||
|
"npm: watch:tsc",
|
||||||
|
"npm: watch:esbuild"
|
||||||
|
],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "never"
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "watch:esbuild",
|
||||||
|
"group": "build",
|
||||||
|
"problemMatcher": "$esbuild-watch",
|
||||||
|
"isBackground": true,
|
||||||
|
"label": "npm: watch:esbuild",
|
||||||
|
"presentation": {
|
||||||
|
"group": "watch",
|
||||||
|
"reveal": "never"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "watch:tsc",
|
||||||
|
"group": "build",
|
||||||
|
"problemMatcher": "$tsc-watch",
|
||||||
|
"isBackground": true,
|
||||||
|
"label": "npm: watch:tsc",
|
||||||
|
"presentation": {
|
||||||
|
"group": "watch",
|
||||||
|
"reveal": "never"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "watch-tests",
|
||||||
|
"problemMatcher": "$tsc-watch",
|
||||||
|
"isBackground": true,
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "never",
|
||||||
|
"group": "watchers"
|
||||||
|
},
|
||||||
|
"group": "build"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "tasks: watch-tests",
|
||||||
|
"dependsOn": [
|
||||||
|
"npm: watch",
|
||||||
|
"npm: watch-tests"
|
||||||
|
],
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
.vscode/**
|
||||||
|
.vscode-test/**
|
||||||
|
out/**
|
||||||
|
node_modules/**
|
||||||
|
src/**
|
||||||
|
.gitignore
|
||||||
|
.yarnrc
|
||||||
|
esbuild.js
|
||||||
|
vsc-extension-quickstart.md
|
||||||
|
**/tsconfig.json
|
||||||
|
**/eslint.config.mjs
|
||||||
|
**/*.map
|
||||||
|
**/*.ts
|
||||||
|
**/.vscode-test.*
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
# bmc-tpl-code-all-around README
|
||||||
|
|
||||||
|
This is the README for your extension "bmc-tpl-code-all-around". After writing up a brief description, we recommend including the following sections.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Following extension guidelines
|
||||||
|
|
||||||
|
Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension.
|
||||||
|
|
||||||
|
* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)
|
||||||
|
|
||||||
|
## Working with Markdown
|
||||||
|
|
||||||
|
You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts:
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
## 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/)
|
||||||
|
|
||||||
|
**Enjoy!**
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
const esbuild = require("esbuild");
|
||||||
|
|
||||||
|
const production = process.argv.includes('--production');
|
||||||
|
const watch = process.argv.includes('--watch');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('esbuild').Plugin}
|
||||||
|
*/
|
||||||
|
const esbuildProblemMatcherPlugin = {
|
||||||
|
name: 'esbuild-problem-matcher',
|
||||||
|
|
||||||
|
setup(build) {
|
||||||
|
build.onStart(() => {
|
||||||
|
console.log('[watch] build started');
|
||||||
|
});
|
||||||
|
build.onEnd((result) => {
|
||||||
|
result.errors.forEach(({ text, location }) => {
|
||||||
|
console.error(`✘ [ERROR] ${text}`);
|
||||||
|
console.error(` ${location.file}:${location.line}:${location.column}:`);
|
||||||
|
});
|
||||||
|
console.log('[watch] build finished');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const ctx = await esbuild.context({
|
||||||
|
entryPoints: [
|
||||||
|
'src/extension.ts'
|
||||||
|
],
|
||||||
|
bundle: true,
|
||||||
|
format: 'cjs',
|
||||||
|
minify: production,
|
||||||
|
sourcemap: !production,
|
||||||
|
sourcesContent: false,
|
||||||
|
platform: 'node',
|
||||||
|
outfile: 'dist/extension.js',
|
||||||
|
external: ['vscode'],
|
||||||
|
logLevel: 'silent',
|
||||||
|
plugins: [
|
||||||
|
/* add to the end of plugins array */
|
||||||
|
esbuildProblemMatcherPlugin,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (watch) {
|
||||||
|
await ctx.watch();
|
||||||
|
} else {
|
||||||
|
await ctx.rebuild();
|
||||||
|
await ctx.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import typescriptEslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default [{
|
||||||
|
files: ["**/*.ts"],
|
||||||
|
}, {
|
||||||
|
plugins: {
|
||||||
|
"@typescript-eslint": typescriptEslint.plugin,
|
||||||
|
},
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
parser: typescriptEslint.parser,
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/naming-convention": ["warn", {
|
||||||
|
selector: "import",
|
||||||
|
format: ["camelCase", "PascalCase"],
|
||||||
|
}],
|
||||||
|
|
||||||
|
curly: "warn",
|
||||||
|
eqeqeq: "warn",
|
||||||
|
"no-throw-literal": "warn",
|
||||||
|
semi: "warn",
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"comments": {
|
||||||
|
"lineComment": "//",
|
||||||
|
"blockComment": [ "/*", "*/" ]
|
||||||
|
},
|
||||||
|
"brackets": [
|
||||||
|
["{", "}"],
|
||||||
|
["[", "]"],
|
||||||
|
["(", ")"]
|
||||||
|
],
|
||||||
|
"autoClosingPairs": [
|
||||||
|
{ "open": "{", "close": "}" },
|
||||||
|
{ "open": "[", "close": "]" },
|
||||||
|
{ "open": "(", "close": ")" },
|
||||||
|
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
|
||||||
|
{ "open": "\"", "close": "\"", "notIn": ["string"] }
|
||||||
|
],
|
||||||
|
"indentationRules": {
|
||||||
|
"increaseIndentPattern": "^\\s*(pattern|table|metadata|overview|constants|triggers|body|if|for|while)\\b.*",
|
||||||
|
"decreaseIndentPattern": "^\\s*(end\\s+(pattern|table|if|for|while)|else|elif)\\b.*"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"name": "bmc-tpl-code-all-around",
|
||||||
|
"displayName": "BMC TPL Code All around",
|
||||||
|
"description": "Code linter, tracker and all around tool for coding in BMC Helix TPL",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.140.0"
|
||||||
|
},
|
||||||
|
"categories": [
|
||||||
|
"Other"
|
||||||
|
],
|
||||||
|
"activationEvents": [],
|
||||||
|
"main": "./dist/extension.js",
|
||||||
|
"contributes": {
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "bmc-tpl-code-all-around.helloWorld",
|
||||||
|
"title": "Hello World"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"vscode:prepublish": "npm run package",
|
||||||
|
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||||
|
"watch": "npm-run-all -p watch:*",
|
||||||
|
"watch:esbuild": "node esbuild.js --watch",
|
||||||
|
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||||
|
"package": "npm run check-types && npm run lint && node esbuild.js --production",
|
||||||
|
"compile-tests": "tsc -p . --outDir out",
|
||||||
|
"watch-tests": "tsc -p . -w --outDir out",
|
||||||
|
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||||
|
"check-types": "tsc --noEmit",
|
||||||
|
"lint": "eslint src",
|
||||||
|
"test": "vscode-test"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/vscode": "^1.140.0",
|
||||||
|
"@types/mocha": "^10.0.10",
|
||||||
|
"@types/node": "24.x",
|
||||||
|
"typescript-eslint": "^8.61.1",
|
||||||
|
"eslint": "^10.5.0",
|
||||||
|
"esbuild": "^0.28.1",
|
||||||
|
"npm-run-all": "^4.1.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "Node16",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": [
|
||||||
|
"ES2022"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"mocha"
|
||||||
|
],
|
||||||
|
"sourceMap": true,
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true, /* enable all strict type-checking options */
|
||||||
|
/* Additional Checks */
|
||||||
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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).
|
||||||
Loading…
Reference in New Issue