Refactoring fixes, translations, extensions for settings

This commit is contained in:
kuro 2026-10-18 03:03:33 +02:00
parent 1910b0def2
commit b9035feb99
12 changed files with 343 additions and 162 deletions

View File

@ -35,27 +35,63 @@
"main": "./dist/extension.js", "main": "./dist/extension.js",
"contributes": { "contributes": {
"configuration": { "configuration": {
"title": "BMC TPL Einstellungen", "title": "BMC TPL Settings",
"properties": { "properties": {
"tpl.metadata.author": { "tpl.metadata.author": {
"type": "string", "type": "string",
"default": "Autor", "default": "Autor",
"description": "Standard-Autor für neu generierte TPL-Module." "description": "Standard-Author for new generated TPL-Modules."
}, },
"tpl.metadata.origin": { "tpl.metadata.origin": {
"type": "string", "type": "string",
"default": "Custom", "default": "Custom",
"description": "Standard-Origin für neu generierte TPL-Module." "description": "Standard-Origin for new generated TPL-Modules."
}, },
"tpl.metadata.publisher": { "tpl.metadata.publisher": {
"type": "string", "type": "string",
"default": "IT-Operations", "default": "IT-Operations",
"description": "Standard-Publisher für neu generierte TPL-Module." "description": "Standard-Publisher ffor new generated TPL-Modules."
}, },
"tpl.metadata.treePathBase": { "tpl.metadata.treePathBase": {
"type": "string", "type": "string",
"default": "Company", "default": "Company",
"description": "Erster Teil (Basis) des tree_paths in den Metadaten." "description": "First Part (Basis) of the tree_paths in the Metadata."
},
"tpl.metadata.require.origin": {
"type": "boolean",
"default": true,
"description": "Marks 'origin' as Required on module level."
},
"tpl.metadata.require.author": {
"type": "boolean",
"default": true,
"description": "Marks 'author' as Required on module level."
},
"tpl.metadata.require.products": {
"type": "boolean",
"default": true,
"description": "Marks 'products' aas Required on module level."
},
"tpl.metadata.require.publishers": {
"type": "boolean",
"default": true,
"description": "Marks 'publishers' as Required on module level."
},
"tpl.metadata.require.treePath": {
"type": "boolean",
"default": true,
"description": "Marks 'tree_path' as Required on module level."
},
"tpl.metadata.treePath.minLevels": {
"type": "number",
"default": 3,
"description": "How many levels should the tree_path in Metadata at least have? (e.g. 3 for 'Base', 'Category', 'Subcategory')."
},
"tpl.metadata.treePath.staticNodes": {
"type": "array",
"items": { "type": "string" },
"default": ["default"],
"description": "Fixed definitions for the tree_path left to right (e.g. ['Company', 'Software']). The rest of the levels are free to choose."
}, },
"tpl.version": { "tpl.version": {
"type": "number", "type": "number",

View File

@ -29,8 +29,8 @@ export class LinterContext {
public inModuleMetadata = false; public inModuleMetadata = false;
public hasModuleMetadata = false; public hasModuleMetadata = false;
public moduleMetadataBlockLine = -1; public moduleMetadataBlockLine = -1;
public foundModuleMetadata = { origin: false, author: false, products: false, publishers: false, tree_path: false }; public foundModuleMetadata = new Set<string>();
// Klammer-Tracking // Klammer-Tracking
public parenStack: number[] = []; public parenStack: number[] = [];
public bracketStack: number[] = []; public bracketStack: number[] = [];

View File

@ -1,36 +1,27 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { LinterContext } from './LinterContext'; import { LinterContext } from './LinterContext';
import { LinterRule } from './rules/LinterRule';
import { SyntaxRule } from './rules/SyntaxRule'; import { SyntaxRule } from './rules/SyntaxRule';
import { BlockRule } from './rules/BlockRule'; import { BlockRule } from './rules/BlockRule';
import { MetadataRule } from './rules/MetadataRule';
import { IndentationRule } from './rules/IndentationRule'; // 1. Hier importieren
// ... (deine eventuellen anderen Hilfs-Imports wie trackStrings) ...
export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) { export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
if (document.languageId !== 'tpl') { return; } if (document.languageId !== 'tpl') return;
const ctx = new LinterContext();
const config = vscode.workspace.getConfiguration('tpl');
ctx.configuredTplVersion = config.get<number>('version', 23);
const text = document.getText(); const text = document.getText();
const lines = text.split('\n'); const lines = text.split('\n');
const ctx = new LinterContext();
// 1. Version prüfen // 2. Hier nur die 3 existierenden Regeln reinlegen (MetadataRule entfernt!)
const versionMatch = text.match(/tpl\s+1\.(\d+)\s+module/); const rules = [
if (versionMatch) {
ctx.tplVersion = parseInt(versionMatch[1], 10);
if (ctx.tplVersion < ctx.configuredTplVersion) {
const linesToSearch = text.substring(0, versionMatch.index! + 30).split('\n');
ctx.addDiag(linesToSearch.length - 1, 0, versionMatch[0].length, `Das Modul nutzt eine alte TPL Version (1.${ctx.tplVersion}). Aktuell in den Settings definiert ist 1.${ctx.configuredTplVersion}.`, vscode.DiagnosticSeverity.Warning);
}
}
// 2. Regeln laden
const rules: LinterRule[] = [
new SyntaxRule(), new SyntaxRule(),
new BlockRule() new MetadataRule(),
new BlockRule(),
new IndentationRule()
]; ];
// 3. Zeilenweise iterieren
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const rawLine = lines[i]; const rawLine = lines[i];
const line = rawLine.trim(); const line = rawLine.trim();
@ -54,18 +45,14 @@ export function validateTplDocument(document: vscode.TextDocument, collection: v
// Regeln anwenden // Regeln anwenden
for (const rule of rules) { for (const rule of rules) {
rule.checkLine(line, rawLine, i, ctx, lines); // Hier 'lines' ergänzen! rule.checkLine(line, rawLine, i, ctx, lines);
} }
} }
// 4. Abschluss-Prüfungen durchführen
for (const rule of rules) { for (const rule of rules) {
if (rule.checkEnd) {
rule.checkEnd(lines, ctx); rule.checkEnd(lines, ctx);
}
} }
// Diagnostics an VS Code übergeben
collection.set(document.uri, ctx.diagnostics); collection.set(document.uri, ctx.diagnostics);
} }

View File

@ -10,16 +10,9 @@ export class BlockRule implements LinterRule {
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module <name>;' beginnen.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; } else { ctx.addDiag(lineIdx, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module <name>;' beginnen.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; }
} }
if (line.startsWith('metadata') && ctx.inBlock === 'none') { // NEU: Wenn wir im Metadata-Block sind, ignorieren wir den Rest. Die MetadataRule übernimmt!
ctx.inModuleMetadata = true; if (ctx.inModuleMetadata || (line.startsWith('metadata') && ctx.inBlock === 'none')) {
ctx.hasModuleMetadata = true; return;
ctx.moduleMetadataBlockLine = lineIdx;
return;
}
if (ctx.inModuleMetadata) {
this.trackModuleMetadata(line, lineIdx, ctx);
return;
} }
if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) { if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) {
@ -29,8 +22,6 @@ export class BlockRule implements LinterRule {
} }
this.handleBlockOpenings(line, rawLine, lineIdx, ctx); this.handleBlockOpenings(line, rawLine, lineIdx, ctx);
// NEU: if, for und while tracken
this.handleControlBlocks(line, rawLine, lineIdx, ctx); this.handleControlBlocks(line, rawLine, lineIdx, ctx);
if (ctx.inBlock === 'pattern') { if (ctx.inBlock === 'pattern') {
@ -40,16 +31,6 @@ export class BlockRule implements LinterRule {
this.checkEndBlocks(line, rawLine, lineIdx, ctx); this.checkEndBlocks(line, rawLine, lineIdx, ctx);
} }
public checkEnd(lines: string[], ctx: LinterContext): void {
for (const unclosedLine of ctx.openDefines) {
ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' wurde nicht mit 'end define;' geschlossen!", vscode.DiagnosticSeverity.Error);
}
// NEU: Offene if/for/while Blöcke am Dateiende ankreiden
for (const block of ctx.openControlBlocks) {
ctx.addDiag(block.lineIdx, 0, lines[block.lineIdx].length, `Block '${block.type}' wurde nicht mit 'end ${block.type};' geschlossen!`, vscode.DiagnosticSeverity.Error);
}
}
private handleControlBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) { private handleControlBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
// Block wird geöffnet // Block wird geöffnet
if (/^if\b/.test(line)) { ctx.openControlBlocks.push({ type: 'if', lineIdx }); } if (/^if\b/.test(line)) { ctx.openControlBlocks.push({ type: 'if', lineIdx }); }
@ -193,4 +174,8 @@ export class BlockRule implements LinterRule {
else { ctx.addDiag(lineIdx, 0, line.length, "'end define' gefunden, ohne offenes 'define'.", vscode.DiagnosticSeverity.Error); } else { ctx.addDiag(lineIdx, 0, line.length, "'end define' gefunden, ohne offenes 'define'.", vscode.DiagnosticSeverity.Error); }
} }
} }
public checkEnd(lines: string[], ctx: LinterContext): void {
// empty
}
} }

View File

@ -0,0 +1,32 @@
import * as vscode from 'vscode';
import { LinterRule } from './LinterRule';
import { LinterContext } from '../LinterContext';
export class IndentationRule implements LinterRule {
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
// Leere Zeilen oder Zeilen, die nur aus Leerzeichen/Tabs bestehen, ignorieren
if (rawLine.trim().length === 0) return;
// Wenn Tabs drin sind, meckert die SyntaxRule bereits. Das ignorieren wir hier.
if (rawLine.includes('\t')) return;
const leadingSpacesMatch = rawLine.match(/^ +/);
const spaceCount = leadingSpacesMatch ? leadingSpacesMatch[0].length : 0;
// Ist die Anzahl der Leerzeichen kein Vielfaches von 4?
if (spaceCount > 0 && spaceCount % 4 !== 0) {
ctx.addDiag(
lineIdx,
0,
spaceCount, // Markiert genau den Bereich der Leerzeichen
`Unsaubere Einrückung (${spaceCount} Leerzeichen). Nutze ein Vielfaches von 4. (Tipp: Shift+Alt+F zum Auto-Formatieren)`,
vscode.DiagnosticSeverity.Warning // Warning reicht hier völlig, Error wäre beim Tippen zu aggressiv
);
}
}
public checkEnd(lines: string[], ctx: LinterContext): void {
// Hier muss am Ende der Datei nichts geprüft werden
}
}

View File

@ -0,0 +1,80 @@
import * as vscode from 'vscode';
import { LinterRule } from './LinterRule';
import { LinterContext } from '../LinterContext';
export class MetadataRule implements LinterRule {
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
if (line.startsWith('metadata') && ctx.inBlock === 'none') {
ctx.inModuleMetadata = true;
ctx.hasModuleMetadata = true;
ctx.moduleMetadataBlockLine = lineIdx;
ctx.foundModuleMetadata.clear();
return;
}
if (ctx.inModuleMetadata) {
const config = vscode.workspace.getConfiguration('tpl');
// Felder erfassen
if (line.match(/^origin\b/)) ctx.foundModuleMetadata.add('origin');
if (line.match(/^author\b/)) ctx.foundModuleMetadata.add('author');
if (line.match(/^products\b/)) ctx.foundModuleMetadata.add('products');
if (line.match(/^publishers\b/)) ctx.foundModuleMetadata.add('publishers');
// Tree-Path Sonderprüfung
if (line.match(/^tree_path\b/)) {
ctx.foundModuleMetadata.add('tree_path');
if (config.get<boolean>('metadata.require.treePath', true)) {
this.checkTreePath(line, rawLine, lineIdx, ctx, config);
}
}
// Block-Ende: Pflichtfelder abgleichen
if (line.startsWith('end metadata')) {
ctx.inModuleMetadata = false;
this.checkRequiredFields(ctx, config);
}
}
}
private checkTreePath(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, config: vscode.WorkspaceConfiguration) {
const minLevels = config.get<number>('metadata.treePath.minLevels', 3);
const staticNodes = config.get<string[]>('metadata.treePath.staticNodes', ['Grafing']);
// Extrahiert die Strings aus tree_path := 'A', 'B';
const match = line.match(/tree_path\s*:=\s*(.*);/);
if (match) {
const paths = match[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, ''));
if (paths.length < minLevels) {
ctx.addDiag(lineIdx, 0, rawLine.length, `Der tree_path muss mindestens ${minLevels} Ebenen haben!`, vscode.DiagnosticSeverity.Error);
}
for (let i = 0; i < staticNodes.length; i++) {
if (paths[i] !== staticNodes[i]) {
ctx.addDiag(lineIdx, 0, rawLine.length, `Die tree_path Ebene ${i + 1} muss zwingend '${staticNodes[i]}' lauten!`, vscode.DiagnosticSeverity.Error);
}
}
}
}
private checkRequiredFields(ctx: LinterContext, config: vscode.WorkspaceConfiguration) {
const requiredFields: string[] = [];
if (config.get<boolean>('metadata.require.origin', true)) requiredFields.push('origin');
if (config.get<boolean>('metadata.require.author', true)) requiredFields.push('author');
if (config.get<boolean>('metadata.require.products', true)) requiredFields.push('products');
if (config.get<boolean>('metadata.require.publishers', true)) requiredFields.push('publishers');
if (config.get<boolean>('metadata.require.treePath', true)) requiredFields.push('tree_path');
const missing = requiredFields.filter(f => !ctx.foundModuleMetadata.has(f));
if (missing.length > 0 && ctx.moduleMetadataBlockLine !== -1) {
const diag = ctx.addDiag(ctx.moduleMetadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten (Modul-Ebene): ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
diag.code = 'missing_module_metadata';
}
}
public checkEnd(lines: string[], ctx: LinterContext): void {
// empty
}
}

View File

@ -7,7 +7,6 @@ export class SyntaxRule implements LinterRule {
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void { public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
this.checkTabs(rawLine, lineIdx, ctx); this.checkTabs(rawLine, lineIdx, ctx);
// TODOs finden (blauer Info-Hinweis)
const todoMatch = rawLine.match(/\bTODO\s*:(.*)/i); const todoMatch = rawLine.match(/\bTODO\s*:(.*)/i);
if (todoMatch) { if (todoMatch) {
ctx.addDiag(lineIdx, todoMatch.index!, rawLine.length, `TODO: ${todoMatch[1].trim()}`, vscode.DiagnosticSeverity.Information); ctx.addDiag(lineIdx, todoMatch.index!, rawLine.length, `TODO: ${todoMatch[1].trim()}`, vscode.DiagnosticSeverity.Information);
@ -103,8 +102,7 @@ export class SyntaxRule implements LinterRule {
// "in" (für for-Schleifen) als gültigen Zeilenumbruch hinzugefügt // "in" (für for-Schleifen) als gültigen Zeilenumbruch hinzugefügt
const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches|in)$/i.test(codePart); const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches|in)$/i.test(codePart);
// HIER GEFIXT: Wenn die Zeile auf 'then' oder 'do' endet, verlangt sie kein Semikolon! ´const endsWithBlockOpener = /\b(then|do)$/.test(codePart);
const endsWithBlockOpener = /\b(then|do)$/.test(codePart);
if (!noSemicolonNeeded && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine && !endsWithBlockOpener) { if (!noSemicolonNeeded && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine && !endsWithBlockOpener) {
if (codePart.length > 0 && !codePart.endsWith(';') && !codePart.endsWith('{') && !codePart.endsWith(',')) { if (codePart.length > 0 && !codePart.endsWith(';') && !codePart.endsWith('{') && !codePart.endsWith(',')) {

View File

@ -1,11 +1,12 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { tplBuiltIns } from '../data/TplBuiltIns';
export class TplHoverProvider implements vscode.HoverProvider { export class TplHoverProvider implements vscode.HoverProvider {
public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | null> { public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | null> {
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/); const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
if (!range) return null; if (!range) return null;
const word = document.getText(range); // z.B. "modelling.testDefine" const word = document.getText(range);
const text = document.getText(); const text = document.getText();
const lines = text.split('\n'); const lines = text.split('\n');
@ -13,7 +14,26 @@ export class TplHoverProvider implements vscode.HoverProvider {
const importBase = parts[0]; const importBase = parts[0];
const funcName = parts.length > 1 ? parts[1] : word; const funcName = parts.length > 1 ? parts[1] : word;
// 1. Lokale Defines // 1. BMC BUILT-INS DYNAMISCH AUSLESEN
const bmcNamespaces = Object.keys(tplBuiltIns);
if (bmcNamespaces.includes(importBase)) {
// Wenn die Methode in der Datei existiert, zeige die echte Doku!
if (funcName !== importBase && tplBuiltIns[importBase][funcName]) {
const bmcDoc = tplBuiltIns[importBase][funcName];
const md = new vscode.MarkdownString();
// Wir zeigen den echten Methodennamen als Code-Block
md.appendCodeblock(`${importBase}.${funcName}`, 'tpl');
md.appendMarkdown(`\n\n---\n\n${bmcDoc.description}`);
return new vscode.Hover(md);
}
// Fallback, wenn man nur über dem Namespace (z.B. "discovery") hovert
return new vscode.Hover(new vscode.MarkdownString(`**${word}**\n\n*(BMC Built-in Namespace)*`));
}
// 2. Lokale Defines
if (parts.length === 1 || parts.length === 2) { if (parts.length === 1 || parts.length === 2) {
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i]; const line = lines[i];
@ -32,13 +52,12 @@ export class TplHoverProvider implements vscode.HoverProvider {
} }
} }
// 2. Cross-File Hover für from ... import // 3. Cross-File Hover für from ... import
const fromImportRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${importBase}\\b`); const fromImportRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${importBase}\\b`);
const fromMatch = text.match(fromImportRegex); const fromMatch = text.match(fromImportRegex);
if (fromMatch) { if (fromMatch) {
const moduleName = fromMatch[1]; const moduleName = fromMatch[1];
// Wenn der User über die Methode fährt (modelling.XYZ)
if (funcName !== importBase) { if (funcName !== importBase) {
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**'); const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
for (const file of files) { for (const file of files) {
@ -64,14 +83,9 @@ export class TplHoverProvider implements vscode.HoverProvider {
} }
} }
} }
// Wenn er nur über "modelling" fährt
return new vscode.Hover(new vscode.MarkdownString(`Importierte Definitions **${importBase}** aus Modul \`${moduleName}\`.`)); return new vscode.Hover(new vscode.MarkdownString(`Importierte Definitions **${importBase}** aus Modul \`${moduleName}\`.`));
} }
// 3. Hover für Built-Ins
if (word.startsWith('log.')) { return new vscode.Hover(new vscode.MarkdownString(`TPL Logging-Modul`)); }
if (word.startsWith('discovery.')) { return new vscode.Hover(new vscode.MarkdownString(`TPL Discovery-Modul`)); }
return null; return null;
} }
} }

View File

@ -9,10 +9,10 @@ export class MetadataFix implements TplQuickFix {
fix.edit = new vscode.WorkspaceEdit(); fix.edit = new vscode.WorkspaceEdit();
const config = vscode.workspace.getConfiguration('tpl'); const config = vscode.workspace.getConfiguration('tpl');
const author = config.get<string>('metadata.author', 'Kuro'); const author = config.get<string>('metadata.author', 'default');
const origin = config.get<string>('metadata.origin', 'Custom'); const origin = config.get<string>('metadata.origin', 'Custom');
const publisher = config.get<string>('metadata.publisher', 'IT-Operations'); const publisher = config.get<string>('metadata.publisher', 'IT-Operations');
const treePathBase = config.get<string>('metadata.treePathBase', 'Grafing'); const treePathBase = config.get<string>('metadata.treePathBase', 'default');
const missingFieldsStr = diagnostic.message.replace('Fehlende Pflicht-Metadaten (Modul-Ebene): ', ''); const missingFieldsStr = diagnostic.message.replace('Fehlende Pflicht-Metadaten (Modul-Ebene): ', '');
const missingFields = missingFieldsStr.split(', '); const missingFields = missingFieldsStr.split(', ');
@ -24,7 +24,6 @@ export class MetadataFix implements TplQuickFix {
if (missingFields.includes('publishers')) insertText += ` publishers := '${publisher}';\n`; if (missingFields.includes('publishers')) insertText += ` publishers := '${publisher}';\n`;
if (missingFields.includes('tree_path')) insertText += ` tree_path := '${treePathBase}', 'TODO_Category', 'TODO_Subcategory';\n`; if (missingFields.includes('tree_path')) insertText += ` tree_path := '${treePathBase}', 'TODO_Category', 'TODO_Subcategory';\n`;
// HIER GEFIXT: Einfügen auf Zeile + 1, damit es IN den Block rutscht
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText); fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText);
fix.diagnostics = [diagnostic]; fix.diagnostics = [diagnostic];

View File

@ -7,7 +7,7 @@ export class BuiltInCompletion implements CompletionStrategy {
const items: vscode.CompletionItem[] = []; const items: vscode.CompletionItem[] = [];
// Prüft, ob wir im Namespace "log." oder "discovery." sind (auch wenn man danach weiter tippt!) // Prüft, ob wir im Namespace "log." oder "discovery." sind (auch wenn man danach weiter tippt!)
const match = linePrefix.match(/\b(log|discovery)\.([a-zA-Z0-9_]*)$/); const match = linePrefix.match(/\b(discovery|log|model|text|regex|time|xpath|json|hash|inference)\.([a-zA-Z0-9_]*)$/);
if (match) { if (match) {
const namespace = match[1]; const namespace = match[1];

View File

@ -1,29 +1,32 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { CompletionStrategy } from './CompletionStrategy'; import { CompletionStrategy } from './CompletionStrategy';
import { tplBuiltIns } from '../../data/TplBuiltIns';
export class LocalContextCompletion implements CompletionStrategy { export class LocalContextCompletion implements CompletionStrategy {
public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise<vscode.CompletionItem[] | undefined> { public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise<vscode.CompletionItem[] | undefined> {
const text = document.getText(); const text = document.getText();
const items: vscode.CompletionItem[] = []; const items: vscode.CompletionItem[] = [];
// 1. WENN DER USER NACH EINEM PUNKT TIPPT (Cross-File Lookup) const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)$/);
const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+)\.$/);
if (dotMatch) { if (dotMatch) {
const prefix = dotMatch[1]; // z.B. "modelling" const prefix = dotMatch[1];
if (prefix === 'log' || prefix === 'discovery') return undefined; // Built-ins ignorieren
const bmcNamespaces = Object.keys(tplBuiltIns);
if (bmcNamespaces.includes(prefix)) {
return undefined; // Überlässt das Feld komplett der BuiltInCompletion!
}
// Prüfen, ob der Präfix importiert wurde
const importRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${prefix}\\b`); const importRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${prefix}\\b`);
const match = importRegex.exec(text); const match = importRegex.exec(text);
if (match) { if (match) {
const moduleName = match[1]; // z.B. "Company.software_modelling" const moduleName = match[1];
const externalItems = await this.resolveImportedMethods(moduleName, prefix); const externalItems = await this.resolveImportedMethods(moduleName, prefix);
return externalItems; // Nur externe Methoden zurückgeben return externalItems;
} }
return undefined; // Unbekannter Namespace return undefined;
} }
// 2. LOKALE DEFINITIONS & IMPORTS PARSEN
const lines = text.split('\n'); const lines = text.split('\n');
let currentDefBlock = ""; let currentDefBlock = "";
@ -44,7 +47,6 @@ export class LocalContextCompletion implements CompletionStrategy {
} }
} }
// Imports für die globale Worterkennung bereitstellen
const fromImportRegex = /from\s+([a-zA-Z0-9_.]+)\s+import\s+([a-zA-Z0-9_]+)/g; const fromImportRegex = /from\s+([a-zA-Z0-9_.]+)\s+import\s+([a-zA-Z0-9_]+)/g;
let matchVar; let matchVar;
while ((matchVar = fromImportRegex.exec(text)) !== null) { while ((matchVar = fromImportRegex.exec(text)) !== null) {
@ -65,20 +67,16 @@ export class LocalContextCompletion implements CompletionStrategy {
return items; return items;
} }
// Hilfsmethode: Sucht im Workspace nach der anderen Datei!
private async resolveImportedMethods(moduleName: string, defBlockName: string): Promise<vscode.CompletionItem[]> { private async resolveImportedMethods(moduleName: string, defBlockName: string): Promise<vscode.CompletionItem[]> {
const items: vscode.CompletionItem[] = []; const items: vscode.CompletionItem[] = [];
// Sucht alle .tpl Dateien im Projekt
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**'); const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
for (const file of files) { for (const file of files) {
const doc = await vscode.workspace.openTextDocument(file); const doc = await vscode.workspace.openTextDocument(file);
const text = doc.getText(); const text = doc.getText();
// Ist das das gesuchte Modul? (Punkte escapen)
const moduleRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${moduleName.replace(/\./g, '\\.')}\\s*;`); const moduleRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${moduleName.replace(/\./g, '\\.')}\\s*;`);
if (moduleRegex.test(text)) { if (moduleRegex.test(text)) {
// Modul gefunden! Jetzt die Defines im entsprechenden Block auslesen
const lines = text.split('\n'); const lines = text.split('\n');
let inTargetBlock = false; let inTargetBlock = false;
for (const line of lines) { for (const line of lines) {
@ -96,7 +94,7 @@ export class LocalContextCompletion implements CompletionStrategy {
} }
} }
} }
break; // Datei gefunden und geparst, wir können abbrechen break;
} }
} }
return items; return items;

View File

@ -2,93 +2,145 @@ import * as vscode from 'vscode';
import { CompletionStrategy } from './CompletionStrategy'; import { CompletionStrategy } from './CompletionStrategy';
export class SnippetCompletion implements CompletionStrategy { export class SnippetCompletion implements CompletionStrategy {
public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise<vscode.CompletionItem[] | undefined> {
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined { // Bei Modul-Aufrufen (Punkt) keine Snippets feuern
if (linePrefix.match(/\.[a-zA-Z0-9_]*$/)) { return undefined; } if (linePrefix.match(/\.[a-zA-Z0-9_]*$/)) { return undefined; }
const items: vscode.CompletionItem[] = []; const items: vscode.CompletionItem[] = [];
const config = vscode.workspace.getConfiguration('tpl');
// Kontext-Scanner: Wo befindet sich der Cursor gerade?
let inBlock: 'none' | 'pattern' | 'definitions' = 'none';
let inPhase = '';
for (let i = 0; i <= position.line; i++) {
const text = document.lineAt(i).text.trim();
if (text.match(/^(pattern|syncmapping)\b/)) { inBlock = 'pattern'; inPhase = ''; }
else if (text.match(/^definitions?\b/)) { inBlock = 'definitions'; inPhase = ''; }
else if (text.match(/^end (pattern|syncmapping|definitions?)\b/)) { inBlock = 'none'; inPhase = ''; }
if (inBlock === 'pattern') {
['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body'].forEach(p => {
if (text.startsWith(p)) { inPhase = p; }
else if (text.startsWith(`end ${p}`)) { inPhase = ''; }
});
}
if (inBlock === 'definitions') {
if (text.startsWith('define ')) { inPhase = 'define'; }
else if (text.startsWith('end define')) { inPhase = ''; }
}
}
// ==========================================
// 1. GLOBAL (Top-Level: Vor dem Pattern / Import-Bereich)
// ==========================================
if (inBlock === 'none') {
const skel = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
skel.insertText = new vscode.SnippetString(`tpl 1.23 module \${1:Name};\n\nmetadata\n origin := '\${2:Custom}';\n tree_path := 'Grafing', '\${3:Category}', '\${4:SubCategory}';\nend metadata;\n\npattern \${5:PatternName} 1.0\n """\n \${6:Beschreibung}\n """\n overview\n tags \${7:tag1};\n end overview;\n\n triggers\n on host := Host created, confirmed;\n end triggers;\n\n body\n log.debug("Start pattern \${5:PatternName}");\n $0\n end body;\nend pattern;`);
items.push(skel);
['pattern', 'syncmapping', 'definitions'].forEach(block => {
const item = new vscode.CompletionItem(block, vscode.CompletionItemKind.Snippet);
item.insertText = new vscode.SnippetString(`${block} \${1:Name} \${2:1.0}\n """\n \${3:DocString}\n """\n $0\nend ${block};`);
items.push(item);
});
const imp = new vscode.CompletionItem('import', vscode.CompletionItemKind.Snippet);
imp.insertText = new vscode.SnippetString('import ${1:module_name};');
items.push(imp);
const fromImp = new vscode.CompletionItem('from import', vscode.CompletionItemKind.Snippet);
fromImp.insertText = new vscode.SnippetString('from ${1:module_name} import${2:function_name};');
items.push(fromImp);
}
// ==========================================
// 2. INNERHALB PATTERN (aber VOR dem Body)
// ==========================================
if (inBlock === 'pattern' && inPhase === '') {
['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body'].forEach(sec => {
const item = new vscode.CompletionItem(sec, vscode.CompletionItemKind.Snippet);
item.insertText = new vscode.SnippetString(`${sec}\n $0\nend ${sec};`);
items.push(item);
});
}
if (inBlock === 'definitions' && inPhase === '') { // 1. Kontext herausfinden (Sind wir in einem Pattern oder Syncmapping?)
const item = new vscode.CompletionItem('define', vscode.CompletionItemKind.Snippet); const textBeforeCursor = document.getText(new vscode.Range(new vscode.Position(0, 0), position));
item.insertText = new vscode.SnippetString(`define \${1:methodName}(\${2:params})\n """\n \${3:DocString}\n """\n $0\nend define;`); const inPattern = textBeforeCursor.lastIndexOf('pattern ') > textBeforeCursor.lastIndexOf('end pattern;');
items.push(item); const inSyncmapping = textBeforeCursor.lastIndexOf('syncmapping ') > textBeforeCursor.lastIndexOf('end syncmapping;');
const inBlock = inPattern || inSyncmapping;
// 2. Settings laden
const config = vscode.workspace.getConfiguration('tpl');
const author = config.get<string>('metadata.author', 'Kuro');
const origin = config.get<string>('metadata.origin', 'Custom');
const publisher = config.get<string>('metadata.publisher', 'IT-Operations');
const version = config.get<string>('version', '23');
// Dynamischer Tree-Path
const minLevels = config.get<number>('metadata.treePath.minLevels', 3);
const staticNodes = config.get<string[]>('metadata.treePath.staticNodes', ['Grafing']);
let treePathStr = '';
let snippetTabIdx = 1;
for (let i = 0; i < minLevels; i++) {
if (i < staticNodes.length) {
treePathStr += `'${staticNodes[i]}'`;
} else {
treePathStr += `'\${${snippetTabIdx}:Level_${i+1}}'`;
snippetTabIdx++;
}
if (i < minLevels - 1) treePathStr += ', ';
}
// Dynamischer Metadata-String
let metadataBlock = 'metadata\n';
if (config.get<boolean>('metadata.require.origin', true)) metadataBlock += ` origin := '${origin}';\n`;
if (config.get<boolean>('metadata.require.treePath', true)) metadataBlock += ` tree_path := ${treePathStr};\n`;
if (config.get<boolean>('metadata.require.products', true)) metadataBlock += ` products := '\${${snippetTabIdx++}:Product}';\n`;
if (config.get<boolean>('metadata.require.publishers', true)) metadataBlock += ` publishers := '${publisher}';\n`;
if (config.get<boolean>('metadata.require.author', true)) metadataBlock += ` author := '${author}';\n`;
metadataBlock += 'end metadata;';
// ==========================================
// SNIPPETS FÜR DIE MODUL-EBENE (Außerhalb)
// ==========================================
if (!inBlock) {
const skeleton = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
skeleton.detail = "Generiert ein frisches TPL Modul";
skeleton.insertText = new vscode.SnippetString(
`tpl 1.\${1:${version}} module \${2:MyModule};\n\n` +
`metadata\n` +
` origin := '${origin}';\n` +
` tree_path := '${treePathBase}', '\${3:Category}', '\${4:Subcategory}';\n` +
` products := '\${5:Product}';\n` +
` publishers := '${publisher}';\n` +
` author := '${author}';\n` +
`end metadata;\n\n` +
`pattern \${6:MyPattern} 1.0\n` +
` """\n \${7:Beschreibung des Patterns}\n """\n` +
` metadata\n` +
` publishers := '${publisher}';\n` +
` tags := '\${8:Tags}';\n` +
` end metadata;\n\n` +
` overview\n` +
` tags \${9:Tags};\n` +
` end overview;\n\n` +
` constants\n` +
` \${10:const_name} := '\${11:value}';\n` +
` end constants;\n\n` +
` triggers\n` +
` on process := SoftwareInstance created, confirmed where type = '\${12:Type}';\n` +
` end triggers;\n\n` +
` body\n` +
` log.debug("Start pattern %pattern_name%");\n` +
` $0\n` +
` end body;\n` +
`end pattern;`
);
items.push(skeleton);
const modMeta = new vscode.CompletionItem('metadata', vscode.CompletionItemKind.Snippet);
modMeta.detail = "Modul-Metadaten Block (Pflichtfelder)";
modMeta.insertText = new vscode.SnippetString(
`metadata\n` +
` origin := '${origin}';\n` +
` tree_path := '${treePathBase}', '\${1:Category}', '\${2:Subcategory}';\n` +
` products := '\${3:Product}';\n` +
` publishers := '${publisher}';\n` +
` author := '${author}';\n` +
`end metadata;\n$0`
);
items.push(modMeta);
} }
// ========================================== // ==========================================
// 3. INNERHALB VON BODY ODER DEFINE (Ausführbarer Code) // SNIPPETS FÜR DIE PATTERN-EBENE (Innerhalb)
// ========================================== // ==========================================
if (inPhase === 'body' || inPhase === 'define') { if (inBlock) {
['discovery', 'log', 'model', 'search', 'text', 'regex', 'time', 'hash', 'number', 'inference'].forEach(ns => { const patMeta = new vscode.CompletionItem('metadata', vscode.CompletionItemKind.Snippet);
const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module); patMeta.detail = "Pattern-Metadaten Block";
items.push(item); patMeta.insertText = new vscode.SnippetString(
}); `metadata\n` +
` publishers := '${publisher}';\n` +
const ifSnippet = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet); ` tags := '\${1:Tags}';\n` +
ifSnippet.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;'); `end metadata;\n$0`
items.push(ifSnippet); );
items.push(patMeta);
const forSnippet = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet);
forSnippet.insertText = new vscode.SnippetString('for ${1:item} in${2:list} do\n $0\nend for;');
items.push(forSnippet);
} }
// ==========================================
// ALLGEMEINE SNIPPETS (Überall gültig)
// ==========================================
const ifSnippet = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet);
ifSnippet.insertText = new vscode.SnippetString(`if \${1:condition} then\n $0\nend if;`);
items.push(ifSnippet);
const elifSnippet = new vscode.CompletionItem('elif', vscode.CompletionItemKind.Snippet);
elifSnippet.insertText = new vscode.SnippetString(`elif \${1:condition} then\n $0`);
items.push(elifSnippet);
const elseSnippet = new vscode.CompletionItem('else', vscode.CompletionItemKind.Snippet);
elseSnippet.insertText = new vscode.SnippetString(`else\n $0`);
items.push(elseSnippet);
const forSnippet = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet);
forSnippet.insertText = new vscode.SnippetString(`for \${1:item} in \${2:list} do\n $0\nend for;`);
items.push(forSnippet);
const whileSnippet = new vscode.CompletionItem('while', vscode.CompletionItemKind.Snippet);
whileSnippet.insertText = new vscode.SnippetString(`while \${1:condition} do\n $0\nend while;`);
items.push(whileSnippet);
const defineSnippet = new vscode.CompletionItem('define', vscode.CompletionItemKind.Snippet);
defineSnippet.insertText = new vscode.SnippetString(`define \${1:name}(\${2:params})\n """\n \${3:DocString}\n """\n $0\nend define;`);
items.push(defineSnippet);
return items; return items;
} }
} }