Compare commits
No commits in common. "master" and "beta_4" have entirely different histories.
|
|
@ -0,0 +1,67 @@
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export class TplCodeActionProvider implements vscode.CodeActionProvider {
|
||||||
|
|
||||||
|
public provideCodeActions(
|
||||||
|
document: vscode.TextDocument,
|
||||||
|
range: vscode.Range | vscode.Selection,
|
||||||
|
context: vscode.CodeActionContext,
|
||||||
|
token: vscode.CancellationToken
|
||||||
|
): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> {
|
||||||
|
|
||||||
|
const actionList: vscode.CodeAction[] = [];
|
||||||
|
|
||||||
|
for (const diag of context.diagnostics) {
|
||||||
|
|
||||||
|
// 1. Fix für fehlende Modul-Metadaten (Auf Modul-Ebene, 4 Spaces)
|
||||||
|
if (diag.code === 'missing_module_metadata') {
|
||||||
|
const action = new vscode.CodeAction('Fehlende Modul-Metadaten einfügen', vscode.CodeActionKind.QuickFix);
|
||||||
|
action.edit = new vscode.WorkspaceEdit();
|
||||||
|
|
||||||
|
const fehlendeFelder = diag.message.replace('Fehlende Pflicht-Metadaten (Modul-Ebene): ', '').split(', ');
|
||||||
|
let neuerText = '';
|
||||||
|
|
||||||
|
if (fehlendeFelder.includes('origin')) { neuerText += ` origin := 'Origin';\n`; }
|
||||||
|
if (fehlendeFelder.includes('author')) { neuerText += ` author := 'Dein Name';\n`; }
|
||||||
|
if (fehlendeFelder.includes('products')) { neuerText += ` products := 'Produktname';\n`; }
|
||||||
|
if (fehlendeFelder.includes('publishers')) { neuerText += ` publishers := 'Dein Unternehmen';\n`; }
|
||||||
|
if (fehlendeFelder.includes('tree_path')) { neuerText += ` tree_path := 'Company', 'Kategorie', 'Sub';\n`; }
|
||||||
|
|
||||||
|
action.edit.insert(document.uri, new vscode.Position(diag.range.start.line + 1, 0), neuerText);
|
||||||
|
action.diagnostics = [diag];
|
||||||
|
action.isPreferred = true;
|
||||||
|
actionList.push(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fix für fehlendes Log-Statement im Body (8 Spaces Einrückung)
|
||||||
|
if (diag.code === 'missing_log_start') {
|
||||||
|
const action = new vscode.CodeAction('Log-Statement am Body-Anfang einfügen', vscode.CodeActionKind.QuickFix);
|
||||||
|
action.edit = new vscode.WorkspaceEdit();
|
||||||
|
|
||||||
|
const erwartetesLog = diag.source as string;
|
||||||
|
const neuerText = ` ${erwartetesLog}\n`;
|
||||||
|
|
||||||
|
action.edit.insert(document.uri, new vscode.Position(diag.range.start.line + 1, 0), neuerText);
|
||||||
|
action.diagnostics = [diag];
|
||||||
|
action.isPreferred = true;
|
||||||
|
actionList.push(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fix für Tabs (Ersetzt \t durch 4 Spaces in der ganzen Zeile)
|
||||||
|
if (diag.code === 'invalid_tab') {
|
||||||
|
const action = new vscode.CodeAction('Tabs in dieser Zeile durch 4 Spaces ersetzen', vscode.CodeActionKind.QuickFix);
|
||||||
|
action.edit = new vscode.WorkspaceEdit();
|
||||||
|
|
||||||
|
const zeile = document.lineAt(diag.range.start.line);
|
||||||
|
const bereinigterText = zeile.text.replace(/\t/g, ' ');
|
||||||
|
|
||||||
|
action.edit.replace(document.uri, zeile.range, bereinigterText);
|
||||||
|
action.diagnostics = [diag];
|
||||||
|
action.isPreferred = true;
|
||||||
|
actionList.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { tplBuiltIns } from './TplBuiltIns';
|
||||||
|
|
||||||
|
export class TplCompletionProvider implements vscode.CompletionItemProvider {
|
||||||
|
async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.CompletionItem[]> {
|
||||||
|
const linePrefix = document.lineAt(position).text.substring(0, position.character);
|
||||||
|
const items: vscode.CompletionItem[] = [];
|
||||||
|
const text = document.getText();
|
||||||
|
|
||||||
|
const importMap = new Map<string, string>();
|
||||||
|
const importRegex = /from\s+([\w.]+)\s+import\s+(\w+)/g;
|
||||||
|
let m;
|
||||||
|
while ((m = importRegex.exec(text)) !== null) {
|
||||||
|
importMap.set(m[2], m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const localNamespaces = new Set<string>();
|
||||||
|
const defBlockRegex = /^\s*definitions\s+([a-zA-Z0-9_]+)/gm;
|
||||||
|
while ((m = defBlockRegex.exec(text)) !== null) {
|
||||||
|
localNamespaces.add(m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchDot = linePrefix.match(/([a-zA-Z0-9_]+)\.$/);
|
||||||
|
|
||||||
|
if (!matchDot) {
|
||||||
|
// A) Externe & Lokale Module
|
||||||
|
importMap.forEach((_, key) => {
|
||||||
|
const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Module);
|
||||||
|
item.detail = "Importiertes Modul";
|
||||||
|
items.push(item);
|
||||||
|
});
|
||||||
|
localNamespaces.forEach(ns => {
|
||||||
|
const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module);
|
||||||
|
item.detail = "Lokales Modul (definitions)";
|
||||||
|
items.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
// B) Standard BMC Namespaces
|
||||||
|
for (const key of Object.keys(tplBuiltIns)) {
|
||||||
|
if (key !== 'built_in_globals') {
|
||||||
|
const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Module);
|
||||||
|
item.detail = "BMC Built-In Namespace";
|
||||||
|
items.push(item);
|
||||||
|
} else {
|
||||||
|
for (const [glob, def] of Object.entries(tplBuiltIns[key])) {
|
||||||
|
for (const snippet of def.snippets) {
|
||||||
|
// Snippet-Syntax entfernen für das Anzeige-Label (z.B. "${1:number}" -> "number")
|
||||||
|
const cleanSnippet = snippet.replace(/\$\{\d+:([^}]+)\}/g, '$1');
|
||||||
|
|
||||||
|
const item = new vscode.CompletionItem(`${glob}(${cleanSnippet})`, vscode.CompletionItemKind.Function);
|
||||||
|
// Setzt das echte Snippet mit Tab-Stops ein
|
||||||
|
item.insertText = new vscode.SnippetString(`${glob}(${snippet})`);
|
||||||
|
// VS Code darf nur nach dem Methodennamen filtern, nicht nach den Parametern!
|
||||||
|
item.filterText = glob;
|
||||||
|
item.documentation = new vscode.MarkdownString(def.description);
|
||||||
|
item.detail = "BMC Globale Funktion";
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C) Lokale Variablen
|
||||||
|
const varRegex = /\b([a-zA-Z0-9_]+)\s*:=/g;
|
||||||
|
const variables = new Set<string>();
|
||||||
|
while ((m = varRegex.exec(text)) !== null) {
|
||||||
|
variables.add(m[1]);
|
||||||
|
}
|
||||||
|
variables.forEach(v => {
|
||||||
|
items.push(new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable));
|
||||||
|
});
|
||||||
|
|
||||||
|
// D) Snippets
|
||||||
|
|
||||||
|
// Konfiguration laden
|
||||||
|
const config = vscode.workspace.getConfiguration('tpl');
|
||||||
|
const author = config.get<string>('metadata.author', 'Kuro');
|
||||||
|
const origin = config.get<string>('metadata.origin', 'Custom');
|
||||||
|
const publisher = config.get<string>('metadata.publisher', 'IT-Operations');
|
||||||
|
const treePathBase = config.get<string>('metadata.treePathBase', 'Grafing');
|
||||||
|
const tplVersion = config.get<number>('version', 23);
|
||||||
|
|
||||||
|
const skeletonItem = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
|
||||||
|
skeletonItem.insertText = new vscode.SnippetString(
|
||||||
|
`tpl 1.${tplVersion} module \${1:module_name};\n\n` +
|
||||||
|
`metadata\n origin := '${origin}';\n author := '${author}';\n products := '\${2:products}';\n publishers := '${publisher}';\n tree_path := '${treePathBase}', '\${3:category}', '\${4:subcategory}';\nend metadata;\n\n` +
|
||||||
|
`pattern \${5:pattern_name} 1.0\n """\n TODO: explain\n """\n\n triggers\n on process := DiscoveredProcess where cmd matches regex_escape("\${6:process}");\n end triggers;\n\n body\n log.debug("Start pattern \${5:pattern_name}");\n $0\n end body;\nend pattern;`
|
||||||
|
);
|
||||||
|
skeletonItem.detail = "Generiert das komplette TPL Modul-Grundgerüst (mit Settings)";
|
||||||
|
items.push(skeletonItem);
|
||||||
|
|
||||||
|
const blocks = ['metadata', 'overview', 'constants', 'triggers', 'body', 'mapping', 'removal', 'definitions', 'enumeration', 'configuration'];
|
||||||
|
for (const block of blocks) {
|
||||||
|
const item = new vscode.CompletionItem(block, vscode.CompletionItemKind.Snippet);
|
||||||
|
item.insertText = new vscode.SnippetString(`${block}\n $0\nend ${block};`);
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defineItem = new vscode.CompletionItem('define', vscode.CompletionItemKind.Snippet);
|
||||||
|
defineItem.insertText = new vscode.SnippetString('define ${1:name}(${2:params})\n $0\nend define;');
|
||||||
|
items.push(defineItem);
|
||||||
|
|
||||||
|
const patternItem = new vscode.CompletionItem('pattern', vscode.CompletionItemKind.Snippet);
|
||||||
|
patternItem.insertText = new vscode.SnippetString('pattern ${1:name}${2:1.0}\n """\n TODO: explain\n """\n\n $0\nend pattern;');
|
||||||
|
items.push(patternItem);
|
||||||
|
|
||||||
|
const syncmappingItem = new vscode.CompletionItem('syncmapping', vscode.CompletionItemKind.Snippet);
|
||||||
|
syncmappingItem.insertText = new vscode.SnippetString('syncmapping ${1:name}${2:1.0}\n """\n TODO: explain\n """\n\n $0\nend syncmapping;');
|
||||||
|
items.push(syncmappingItem);
|
||||||
|
|
||||||
|
const tableItem = new vscode.CompletionItem('table', vscode.CompletionItemKind.Snippet);
|
||||||
|
tableItem.insertText = new vscode.SnippetString('table ${1:name}${2:1.0}\n $0\n default -> "unknown";\nend table;');
|
||||||
|
items.push(tableItem);
|
||||||
|
|
||||||
|
// --- Kontrollstrukturen (If, For, While) ---
|
||||||
|
|
||||||
|
const ifItem = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet);
|
||||||
|
ifItem.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;');
|
||||||
|
ifItem.detail = "If-Bedingung";
|
||||||
|
items.push(ifItem);
|
||||||
|
|
||||||
|
const elifItem = new vscode.CompletionItem('elif', vscode.CompletionItemKind.Snippet);
|
||||||
|
elifItem.insertText = new vscode.SnippetString('elif ${1:condition} then\n $0');
|
||||||
|
elifItem.detail = "Else-If-Bedingung";
|
||||||
|
items.push(elifItem);
|
||||||
|
|
||||||
|
const elseItem = new vscode.CompletionItem('else', vscode.CompletionItemKind.Snippet);
|
||||||
|
elseItem.insertText = new vscode.SnippetString('else\n $0');
|
||||||
|
items.push(elseItem);
|
||||||
|
|
||||||
|
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;');
|
||||||
|
forItem.detail = "For-Schleife";
|
||||||
|
items.push(forItem);
|
||||||
|
|
||||||
|
const whileItem = new vscode.CompletionItem('while', vscode.CompletionItemKind.Snippet);
|
||||||
|
whileItem.insertText = new vscode.SnippetString('while ${1:condition} do\n $0\nend while;');
|
||||||
|
whileItem.detail = "While-Schleife";
|
||||||
|
items.push(whileItem);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
} else {
|
||||||
|
const ns = matchDot[1];
|
||||||
|
|
||||||
|
// NEU: BMC Built-Ins abfangen und Snippets bauen
|
||||||
|
if (tplBuiltIns[ns]) {
|
||||||
|
const bmcMethods: vscode.CompletionItem[] = [];
|
||||||
|
for (const [method, def] of Object.entries(tplBuiltIns[ns])) {
|
||||||
|
for (const snippet of def.snippets) {
|
||||||
|
const cleanSnippet = snippet.replace(/\$\{\d+:([^}]+)\}/g, '$1');
|
||||||
|
|
||||||
|
const item = new vscode.CompletionItem(`${method}(${cleanSnippet})`, vscode.CompletionItemKind.Method);
|
||||||
|
item.insertText = new vscode.SnippetString(`${method}(${snippet})`);
|
||||||
|
item.filterText = method;
|
||||||
|
item.documentation = new vscode.MarkdownString(def.description);
|
||||||
|
item.detail = `${ns}.${method}`;
|
||||||
|
|
||||||
|
bmcMethods.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bmcMethods;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleMethods: vscode.CompletionItem[] = [];
|
||||||
|
const extractDefineRegex = /^\s*define\s+.*?\b([a-zA-Z0-9_]+)\s*\(/gm;
|
||||||
|
|
||||||
|
if (localNamespaces.has(ns)) {
|
||||||
|
let defMatch;
|
||||||
|
while ((defMatch = extractDefineRegex.exec(text)) !== null) {
|
||||||
|
const methodName = defMatch[1];
|
||||||
|
const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method);
|
||||||
|
const docStr = this.extractDocString(text, defMatch.index);
|
||||||
|
if (docStr) item.documentation = new vscode.MarkdownString(docStr);
|
||||||
|
moduleMethods.push(item);
|
||||||
|
}
|
||||||
|
return moduleMethods;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importMap.has(ns)) {
|
||||||
|
const modPath = importMap.get(ns)!;
|
||||||
|
const uris = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**', 50);
|
||||||
|
for (const uri of uris) {
|
||||||
|
try {
|
||||||
|
const docBytes = await vscode.workspace.fs.readFile(uri);
|
||||||
|
const docText = Buffer.from(docBytes).toString('utf8');
|
||||||
|
const modRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${modPath.replace(/\./g, '\\\\.')}\\s*;`);
|
||||||
|
if (modRegex.test(docText)) {
|
||||||
|
let defMatch;
|
||||||
|
while ((defMatch = extractDefineRegex.exec(docText)) !== null) {
|
||||||
|
const methodName = defMatch[1];
|
||||||
|
const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method);
|
||||||
|
const docStr = this.extractDocString(docText, defMatch.index);
|
||||||
|
if (docStr) item.documentation = new vscode.MarkdownString(docStr);
|
||||||
|
moduleMethods.push(item);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
if (moduleMethods.length > 0) return moduleMethods;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractDocString(text: string, startIndex: number): string | null {
|
||||||
|
const lines = text.substring(startIndex).split('\n');
|
||||||
|
let docString = "";
|
||||||
|
let inDocString = false;
|
||||||
|
let quoteType = "";
|
||||||
|
for (let j = 1; j < Math.min(15, lines.length); j++) {
|
||||||
|
const line = lines[j].trim();
|
||||||
|
if (line.length === 0) continue;
|
||||||
|
if (!inDocString) {
|
||||||
|
if (line.startsWith('"""') || line.startsWith("'''")) {
|
||||||
|
inDocString = true;
|
||||||
|
quoteType = line.substring(0, 3);
|
||||||
|
if (line.length > 3 && line.endsWith(quoteType)) { docString = line.substring(3, line.length - 3).trim(); break; }
|
||||||
|
const content = line.substring(3).trim();
|
||||||
|
if (content) docString += content + "\n";
|
||||||
|
} else { break; }
|
||||||
|
} else {
|
||||||
|
if (line.endsWith(quoteType)) {
|
||||||
|
const content = line.substring(0, line.length - 3).trim();
|
||||||
|
if (content) docString += content;
|
||||||
|
break;
|
||||||
|
} else { docString += line + "\n"; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return docString.trim() || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export class TplDocumentFormatter implements vscode.DocumentFormattingEditProvider {
|
||||||
|
provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): vscode.ProviderResult<vscode.TextEdit[]> {
|
||||||
|
const text = document.getText();
|
||||||
|
const isCRLF = text.includes('\r\n');
|
||||||
|
const lines = text.split(/\r?\n/);
|
||||||
|
|
||||||
|
const formattedLines: string[] = [];
|
||||||
|
const edits: vscode.TextEdit[] = [];
|
||||||
|
|
||||||
|
let indentLevel = 0;
|
||||||
|
const indentString = ' ';
|
||||||
|
let inTripleDouble = false;
|
||||||
|
let inTripleSingle = false;
|
||||||
|
let consecutiveEmptyLines = 0;
|
||||||
|
|
||||||
|
const alignmentStack: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const rawLine = lines[i];
|
||||||
|
let line = rawLine.trim();
|
||||||
|
|
||||||
|
const tripleDoubleCount = (rawLine.match(/"""/g) || []).length;
|
||||||
|
const tripleSingleCount = (rawLine.match(/'''/g) || []).length;
|
||||||
|
|
||||||
|
let isInternalStringLine = false;
|
||||||
|
|
||||||
|
if (inTripleDouble) {
|
||||||
|
isInternalStringLine = true;
|
||||||
|
if (tripleDoubleCount % 2 !== 0) { inTripleDouble = false; isInternalStringLine = false; }
|
||||||
|
} else if (tripleDoubleCount % 2 !== 0) { inTripleDouble = true; }
|
||||||
|
|
||||||
|
if (inTripleSingle) {
|
||||||
|
isInternalStringLine = true;
|
||||||
|
if (tripleSingleCount % 2 !== 0) { inTripleSingle = false; isInternalStringLine = false; }
|
||||||
|
} else if (tripleSingleCount % 2 !== 0) { inTripleSingle = true; }
|
||||||
|
|
||||||
|
if (isInternalStringLine && (inTripleDouble || inTripleSingle)) {
|
||||||
|
formattedLines.push(rawLine);
|
||||||
|
consecutiveEmptyLines = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.length === 0) {
|
||||||
|
consecutiveEmptyLines++;
|
||||||
|
if (consecutiveEmptyLines <= 1) { formattedLines.push(""); }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
consecutiveEmptyLines = 0;
|
||||||
|
|
||||||
|
if (line.match(/^(end\s+(pattern|syncmapping|table|enumeration|configuration|definitions?|define|if|for|while|metadata|overview|constants|triggers|mapping|removal|body)|else|elif)\b/)) {
|
||||||
|
indentLevel = Math.max(0, indentLevel - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let newLineText = "";
|
||||||
|
if (alignmentStack.length > 0) {
|
||||||
|
let spaces = alignmentStack[alignmentStack.length - 1];
|
||||||
|
if (line.match(/^[)\]]/)) { spaces = Math.max(0, spaces - 1); }
|
||||||
|
newLineText = " ".repeat(spaces) + line;
|
||||||
|
} else {
|
||||||
|
newLineText = indentString.repeat(indentLevel) + line;
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedLines.push(newLineText);
|
||||||
|
|
||||||
|
let cleanLineForParens = newLineText.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, match => " ".repeat(match.length));
|
||||||
|
const commentIdx = cleanLineForParens.indexOf('//');
|
||||||
|
if (commentIdx !== -1) {
|
||||||
|
cleanLineForParens = cleanLineForParens.substring(0, commentIdx) + " ".repeat(cleanLineForParens.length - commentIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let charIdx = 0; charIdx < cleanLineForParens.length; charIdx++) {
|
||||||
|
const char = cleanLineForParens[charIdx];
|
||||||
|
if (char === '(' || char === '[') {
|
||||||
|
alignmentStack.push(charIdx + 1);
|
||||||
|
} else if (char === ')' || char === ']') {
|
||||||
|
if (alignmentStack.length > 0) { alignmentStack.pop(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.match(/^(pattern|syncmapping|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|if|for|while|else|elif)\b/)) {
|
||||||
|
if (!line.startsWith('end ')) {
|
||||||
|
indentLevel++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newText = formattedLines.join(isCRLF ? '\r\n' : '\n');
|
||||||
|
|
||||||
|
if (newText !== text) {
|
||||||
|
const fullRange = new vscode.Range(document.positionAt(0), document.positionAt(text.length));
|
||||||
|
edits.push(vscode.TextEdit.replace(fullRange, newText));
|
||||||
|
}
|
||||||
|
|
||||||
|
return edits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { tplBuiltIns } from '../data/TplBuiltIns';
|
import { tplBuiltIns } from './TplBuiltIns';
|
||||||
|
|
||||||
export class TplHoverProvider implements vscode.HoverProvider {
|
export class TplHoverProvider implements vscode.HoverProvider {
|
||||||
public async provideHover(
|
public async provideHover(
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
|
|
||||||
export enum PatternPhase { None = 0, Metadata = 1, Overview = 2, Constants = 3, Triggers = 4, Mapping = 5, Removal = 6, Body = 7 }
|
|
||||||
|
|
||||||
export class LinterContext {
|
|
||||||
public diagnostics: vscode.Diagnostic[] = [];
|
|
||||||
public configuredTplVersion: number = 23;
|
|
||||||
public tplVersion: number = 10;
|
|
||||||
|
|
||||||
// Status-Tracking
|
|
||||||
public hasModuleDef = false;
|
|
||||||
public inBlock: 'none' | 'pattern' | 'table' | 'definitions' | 'enumeration' | 'configuration' = 'none';
|
|
||||||
public currentPatternPhase: PatternPhase = PatternPhase.None;
|
|
||||||
public inMultiLineComment = false;
|
|
||||||
public inConditionBlock = false;
|
|
||||||
|
|
||||||
// String-Tracking (Basis-Lexer)
|
|
||||||
public inTripleDouble = false;
|
|
||||||
public inTripleSingle = false;
|
|
||||||
public stringNeedsSemicolon = false;
|
|
||||||
public justOpenedString = false;
|
|
||||||
public isClosingString = false;
|
|
||||||
public isInternalStringLine = false;
|
|
||||||
|
|
||||||
// Block- & Metadaten-Tracking
|
|
||||||
public activePhaseName = "";
|
|
||||||
public activePhaseLine = -1;
|
|
||||||
public currentPatternName = "";
|
|
||||||
public inModuleMetadata = false;
|
|
||||||
public hasModuleMetadata = false;
|
|
||||||
public moduleMetadataBlockLine = -1;
|
|
||||||
public foundModuleMetadata = { origin: false, author: false, products: false, publishers: false, tree_path: false };
|
|
||||||
|
|
||||||
// Klammer-Tracking
|
|
||||||
public parenStack: number[] = [];
|
|
||||||
public bracketStack: number[] = [];
|
|
||||||
public openDefines: number[] = [];
|
|
||||||
|
|
||||||
public addDiag(lineIdx: number, start: number, end: number, msg: string, severity: vscode.DiagnosticSeverity): vscode.Diagnostic {
|
|
||||||
const diag = new vscode.Diagnostic(new vscode.Range(lineIdx, start, lineIdx, end), msg, severity);
|
|
||||||
this.diagnostics.push(diag);
|
|
||||||
return diag;
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetLineStates() {
|
|
||||||
this.isInternalStringLine = false;
|
|
||||||
this.justOpenedString = false;
|
|
||||||
this.isClosingString = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { LinterContext } from './LinterContext';
|
|
||||||
import { LinterRule } from './rules/LinterRule';
|
|
||||||
import { SyntaxRule } from './rules/SyntaxRule';
|
|
||||||
import { BlockRule } from './rules/BlockRule';
|
|
||||||
|
|
||||||
export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
|
|
||||||
if (document.languageId !== 'tpl') { return; }
|
|
||||||
|
|
||||||
const ctx = new LinterContext();
|
|
||||||
const config = vscode.workspace.getConfiguration('tpl');
|
|
||||||
ctx.configuredTplVersion = config.get<number>('version', 23);
|
|
||||||
|
|
||||||
const text = document.getText();
|
|
||||||
const lines = text.split('\n');
|
|
||||||
|
|
||||||
// 1. Version prüfen
|
|
||||||
const versionMatch = text.match(/tpl\s+1\.(\d+)\s+module/);
|
|
||||||
if (versionMatch) {
|
|
||||||
ctx.tplVersion = parseInt(versionMatch[1], 10);
|
|
||||||
if (ctx.tplVersion < ctx.configuredTplVersion) {
|
|
||||||
const linesToSearch = text.substring(0, versionMatch.index! + 30).split('\n');
|
|
||||||
ctx.addDiag(linesToSearch.length - 1, 0, versionMatch[0].length, `Das Modul nutzt eine alte TPL Version (1.${ctx.tplVersion}). Aktuell in den Settings definiert ist 1.${ctx.configuredTplVersion}.`, vscode.DiagnosticSeverity.Warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Regeln laden
|
|
||||||
const rules: LinterRule[] = [
|
|
||||||
new SyntaxRule(),
|
|
||||||
new BlockRule()
|
|
||||||
];
|
|
||||||
|
|
||||||
// 3. Zeilenweise iterieren
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const rawLine = lines[i];
|
|
||||||
const line = rawLine.trim();
|
|
||||||
|
|
||||||
ctx.resetLineStates();
|
|
||||||
|
|
||||||
// Basis-Lexer: Strings & Kommentare tracken (läuft vor den Regeln!)
|
|
||||||
trackStrings(rawLine, line, ctx);
|
|
||||||
|
|
||||||
if (ctx.isInternalStringLine && (ctx.inTripleDouble || ctx.inTripleSingle)) { continue; }
|
|
||||||
|
|
||||||
if (line.startsWith('//') || line.startsWith('/*')) {
|
|
||||||
if (line.startsWith('/*')) { ctx.inMultiLineComment = true; }
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (ctx.inMultiLineComment) {
|
|
||||||
if (line.includes('*/')) { ctx.inMultiLineComment = false; }
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (line.length === 0) { continue; }
|
|
||||||
|
|
||||||
// Regeln anwenden
|
|
||||||
for (const rule of rules) {
|
|
||||||
rule.checkLine(line, rawLine, i, ctx, lines); // Hier 'lines' ergänzen!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Abschluss-Prüfungen durchführen
|
|
||||||
for (const rule of rules) {
|
|
||||||
if (rule.checkEnd) {
|
|
||||||
rule.checkEnd(lines, ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Diagnostics an VS Code übergeben
|
|
||||||
collection.set(document.uri, ctx.diagnostics);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interne Hilfsfunktion für den Parser
|
|
||||||
function trackStrings(rawLine: string, line: string, ctx: LinterContext) {
|
|
||||||
const tripleDoubleCount = (rawLine.match(/"""/g) || []).length;
|
|
||||||
const tripleSingleCount = (rawLine.match(/'''/g) || []).length;
|
|
||||||
|
|
||||||
if (ctx.inTripleDouble) {
|
|
||||||
ctx.isInternalStringLine = true;
|
|
||||||
if (tripleDoubleCount % 2 !== 0) { ctx.inTripleDouble = false; ctx.isClosingString = true; }
|
|
||||||
} else if (tripleDoubleCount % 2 !== 0) {
|
|
||||||
ctx.inTripleDouble = true; ctx.justOpenedString = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.inTripleSingle) {
|
|
||||||
ctx.isInternalStringLine = true;
|
|
||||||
if (tripleSingleCount % 2 !== 0) { ctx.inTripleSingle = false; ctx.isClosingString = true; }
|
|
||||||
} else if (tripleSingleCount % 2 !== 0 && !ctx.justOpenedString) {
|
|
||||||
ctx.inTripleSingle = true; ctx.justOpenedString = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.justOpenedString) {
|
|
||||||
ctx.stringNeedsSemicolon = !line.startsWith('"""') && !line.startsWith("'''");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { LinterRule } from './LinterRule';
|
|
||||||
import { LinterContext, PatternPhase } from '../LinterContext';
|
|
||||||
|
|
||||||
export class BlockRule implements LinterRule {
|
|
||||||
|
|
||||||
// HIER GEFIXT: 'lines: string[]' wurde zur Signatur hinzugefügt!
|
|
||||||
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
|
||||||
if (!ctx.hasModuleDef) {
|
|
||||||
if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; }
|
|
||||||
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module <name>;' beginnen.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.startsWith('metadata') && ctx.inBlock === 'none') {
|
|
||||||
ctx.inModuleMetadata = true;
|
|
||||||
ctx.hasModuleMetadata = true;
|
|
||||||
ctx.moduleMetadataBlockLine = lineIdx;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.inModuleMetadata) {
|
|
||||||
this.trackModuleMetadata(line, lineIdx, ctx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) {
|
|
||||||
if (!ctx.hasModuleMetadata && ctx.inBlock === 'none') {
|
|
||||||
ctx.addDiag(lineIdx, 0, line.length, "Ein 'metadata' Block auf Modul-Ebene ist Pflicht, bevor andere Blöcke definiert werden.", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handleBlockOpenings(line, rawLine, lineIdx, ctx);
|
|
||||||
|
|
||||||
if (ctx.inBlock === 'pattern') {
|
|
||||||
// HIER GEFIXT: 'lines' wird jetzt korrekt an die Methode weitergereicht
|
|
||||||
this.checkPatternPhases(line, rawLine, lineIdx, ctx, lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.checkEndBlocks(line, rawLine, lineIdx, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
public checkEnd(lines: string[], ctx: LinterContext): void {
|
|
||||||
for (const unclosedLine of ctx.openDefines) {
|
|
||||||
ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' wurde nicht mit 'end define;' geschlossen!", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private trackModuleMetadata(line: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
if (line.match(/^origin\b/)) { ctx.foundModuleMetadata.origin = true; }
|
|
||||||
if (line.match(/^author\b/)) { ctx.foundModuleMetadata.author = true; }
|
|
||||||
if (line.match(/^products\b/)) { ctx.foundModuleMetadata.products = true; }
|
|
||||||
if (line.match(/^publishers\b/)) { ctx.foundModuleMetadata.publishers = true; }
|
|
||||||
if (line.match(/^tree_path\b/)) { ctx.foundModuleMetadata.tree_path = true; }
|
|
||||||
|
|
||||||
if (line.startsWith('end metadata')) {
|
|
||||||
ctx.inModuleMetadata = false;
|
|
||||||
const missing = Object.keys(ctx.foundModuleMetadata).filter(k => !(ctx.foundModuleMetadata as any)[k]);
|
|
||||||
if (missing.length > 0 && ctx.moduleMetadataBlockLine !== -1) {
|
|
||||||
const diag = ctx.addDiag(ctx.moduleMetadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten (Modul-Ebene): ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
|
|
||||||
diag.code = 'missing_module_metadata';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleBlockOpenings(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
if (line.startsWith('definitions ') || line.startsWith('definition ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Ein Definitions-Block muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'definitions';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('define ')) { ctx.openDefines.push(lineIdx); }
|
|
||||||
else if (line.startsWith('enumeration ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Eine Enumeration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'enumeration';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('configuration ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "Eine Konfiguration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'configuration';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('pattern ') || line.startsWith('syncmapping ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 7, "Ein Pattern/Syncmapping kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'pattern';
|
|
||||||
ctx.currentPatternPhase = PatternPhase.None;
|
|
||||||
ctx.activePhaseName = "";
|
|
||||||
ctx.activePhaseLine = -1;
|
|
||||||
|
|
||||||
const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/);
|
|
||||||
if (nameMatch) { ctx.currentPatternName = nameMatch[2]; }
|
|
||||||
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Ungültige Pattern-Deklaration. Erwartet: 'pattern <Name> <Version>'", vscode.DiagnosticSeverity.Error); }
|
|
||||||
}
|
|
||||||
else if (line.startsWith('table ')) {
|
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'table';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkPatternPhases(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]) {
|
|
||||||
const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => {
|
|
||||||
if (line.startsWith(phaseName)) {
|
|
||||||
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
|
|
||||||
if (ctx.currentPatternPhase >= phaseEnum) { ctx.addDiag(lineIdx, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt.`, vscode.DiagnosticSeverity.Error); }
|
|
||||||
|
|
||||||
ctx.currentPatternPhase = phaseEnum;
|
|
||||||
ctx.activePhaseName = phaseName;
|
|
||||||
ctx.activePhaseLine = lineIdx;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkPhase('metadata', PatternPhase.Metadata);
|
|
||||||
checkPhase('overview', PatternPhase.Overview);
|
|
||||||
checkPhase('constants', PatternPhase.Constants);
|
|
||||||
checkPhase('triggers', PatternPhase.Triggers);
|
|
||||||
checkPhase('mapping', PatternPhase.Mapping);
|
|
||||||
checkPhase('removal', PatternPhase.Removal);
|
|
||||||
|
|
||||||
if (line.startsWith('body')) {
|
|
||||||
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.activePhaseName = "body";
|
|
||||||
ctx.activePhaseLine = lineIdx;
|
|
||||||
|
|
||||||
// DER LOG-CHECK IST ZURÜCK!
|
|
||||||
let foundLog = false;
|
|
||||||
for (let j = lineIdx + 1; j < lines.length; j++) {
|
|
||||||
const aheadLine = lines[j].trim();
|
|
||||||
if (aheadLine.startsWith('end pattern') || aheadLine.startsWith('end syncmapping')) { break; }
|
|
||||||
if (aheadLine.includes('log.debug') && aheadLine.includes(ctx.currentPatternName)) { foundLog = true; break; }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundLog) {
|
|
||||||
const expectedLog = `log.debug("Start pattern ${ctx.currentPatternName}");`;
|
|
||||||
const diag = ctx.addDiag(lineIdx, 0, rawLine.length, `Im Body fehlt das Log-Statement mit dem Pattern-Namen. Bsp: ${expectedLog}`, vscode.DiagnosticSeverity.Error);
|
|
||||||
diag.code = 'missing_log_start';
|
|
||||||
diag.source = expectedLog;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const closers = ['end metadata', 'end overview', 'end constants', 'end triggers', 'end mapping', 'end removal', 'end body'];
|
|
||||||
for (const closer of closers) {
|
|
||||||
if (line.startsWith(closer)) {
|
|
||||||
const phase = closer.replace('end ', '').replace(';', '').trim();
|
|
||||||
if (ctx.activePhaseName !== phase) { ctx.addDiag(lineIdx, 0, line.length, `Unerwartetes '${closer}'. Erwartet wurde 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
|
||||||
else { ctx.activePhaseName = ""; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkEndBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
if (line.startsWith('end pattern') || line.startsWith('end syncmapping')) {
|
|
||||||
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' wurde nicht mit 'end ${ctx.activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
|
|
||||||
if (ctx.inBlock !== 'pattern') { ctx.addDiag(lineIdx, 0, 11, "'end pattern' gefunden, ohne offenen Pattern-Block.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'none';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) {
|
|
||||||
ctx.inBlock = 'none';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('end definitions') || line.startsWith('end definition')) {
|
|
||||||
if (ctx.inBlock !== 'definitions') { ctx.addDiag(lineIdx, 0, line.length, "'end definitions' gefunden, ohne offenen Definitions-Block.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
ctx.inBlock = 'none';
|
|
||||||
}
|
|
||||||
else if (line.startsWith('end define')) {
|
|
||||||
if (ctx.openDefines.length > 0) { ctx.openDefines.pop(); }
|
|
||||||
else { ctx.addDiag(lineIdx, 0, line.length, "'end define' gefunden, ohne offenes 'define'.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { LinterContext } from '../LinterContext';
|
|
||||||
|
|
||||||
export interface LinterRule {
|
|
||||||
checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void;
|
|
||||||
checkEnd?(lines: string[], ctx: LinterContext): void;
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { LinterRule } from './LinterRule';
|
|
||||||
import { LinterContext } from '../LinterContext';
|
|
||||||
|
|
||||||
export class SyntaxRule implements LinterRule {
|
|
||||||
|
|
||||||
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext): void {
|
|
||||||
this.checkTabs(rawLine, lineIdx, ctx);
|
|
||||||
this.checkBrackets(line, rawLine, lineIdx, ctx);
|
|
||||||
this.checkAssignments(rawLine, lineIdx, ctx);
|
|
||||||
this.checkSemicolons(line, rawLine, lineIdx, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
public checkEnd(lines: string[], ctx: LinterContext): void {
|
|
||||||
for (const unclosedLine of ctx.parenStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende Klammer '(' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); }
|
|
||||||
for (const unclosedLine of ctx.bracketStack) { ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende eckige Klammer '[' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkTabs(rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
const tabIndex = rawLine.indexOf('\t');
|
|
||||||
if (tabIndex !== -1) {
|
|
||||||
const diag = ctx.addDiag(lineIdx, tabIndex, tabIndex + 1, "Bitte Tabs durch Spaces ersetzen", vscode.DiagnosticSeverity.Warning);
|
|
||||||
diag.code = 'invalid_tab';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkBrackets(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '');
|
|
||||||
const commentIdx = cleanLine.indexOf('//');
|
|
||||||
if (commentIdx !== -1) { cleanLine = cleanLine.substring(0, commentIdx); }
|
|
||||||
|
|
||||||
for (let charIdx = 0; charIdx < cleanLine.length; charIdx++) {
|
|
||||||
const char = cleanLine[charIdx];
|
|
||||||
if (char === '(') { ctx.parenStack.push(lineIdx); }
|
|
||||||
else if (char === ')') {
|
|
||||||
if (ctx.parenStack.length > 0) { ctx.parenStack.pop(); }
|
|
||||||
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Unerwartete schließende Klammer ')'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
}
|
|
||||||
else if (char === '[') { ctx.bracketStack.push(lineIdx); }
|
|
||||||
else if (char === ']') {
|
|
||||||
if (ctx.bracketStack.length > 0) { ctx.bracketStack.pop(); }
|
|
||||||
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Unerwartete schließende eckige Klammer ']'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkAssignments(rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
const lineWithoutStrings = rawLine.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, '""');
|
|
||||||
|
|
||||||
if (/\b(if|elif|where)\b/.test(lineWithoutStrings)) { ctx.inConditionBlock = true; }
|
|
||||||
|
|
||||||
if (lineWithoutStrings.includes('=') && !lineWithoutStrings.includes(':=') && !lineWithoutStrings.includes('>=') && !lineWithoutStrings.includes('<=')) {
|
|
||||||
if (!ctx.inConditionBlock && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && !/\b(matches)\b/.test(lineWithoutStrings)) {
|
|
||||||
const idx = rawLine.indexOf('=');
|
|
||||||
if (idx !== -1) {
|
|
||||||
ctx.addDiag(lineIdx, idx, idx + 1, "Falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt.", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/\b(then|do)\b/.test(lineWithoutStrings) || lineWithoutStrings.endsWith(';')) { ctx.inConditionBlock = false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkSemicolons(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
|
||||||
if (ctx.isClosingString) {
|
|
||||||
if (ctx.stringNeedsSemicolon && !line.endsWith(';')) {
|
|
||||||
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const noSemicolonNeeded = /^(pattern|syncmapping|traverse|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|\/\/|\/\*|\*)/;
|
|
||||||
const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches)$/i.test(line);
|
|
||||||
|
|
||||||
if (!noSemicolonNeeded.test(line) && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine) {
|
|
||||||
const lineToCheck = line.split('//')[0].trim();
|
|
||||||
if (!lineToCheck.endsWith(';') && !lineToCheck.endsWith('{') && !lineToCheck.endsWith(',')) {
|
|
||||||
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
312
src/extension.ts
312
src/extension.ts
|
|
@ -1,19 +1,19 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { validateTplDocument } from './core/TplLinter';
|
import { TplCodeActionProvider } from './TplCodeActionProvider';
|
||||||
import { TplCodeActionProvider } from './providers/TplCodeActionProvider';
|
import { TplDocumentFormatter } from './TplDocumentFormatter';
|
||||||
import { TplDocumentFormatter } from './providers/TplDocumentFormatter';
|
import { TplCompletionProvider } from './TplCompletionProvider';
|
||||||
import { TplCompletionProvider } from './providers/TplCompletionProvider';
|
import { TplHoverProvider } from './TplHoverProvider';
|
||||||
import { TplHoverProvider } from './providers/TplHoverProvider';
|
|
||||||
|
enum PatternPhase { None = 0, Metadata = 1, Overview = 2, Constants = 3, Triggers = 4, Mapping = 5, Removal = 6, Body = 7 }
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner');
|
const diagnosticCollection = vscode.languages.createDiagnosticCollection('tpl-scanner');
|
||||||
context.subscriptions.push(diagnosticCollection);
|
context.subscriptions.push(diagnosticCollection);
|
||||||
|
|
||||||
// Linter-Events binden
|
|
||||||
vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection));
|
vscode.workspace.onDidOpenTextDocument(doc => validateTplDocument(doc, diagnosticCollection));
|
||||||
vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection));
|
vscode.workspace.onDidChangeTextDocument(event => validateTplDocument(event.document, diagnosticCollection));
|
||||||
|
|
||||||
// Live-Reload für Einstellungen
|
// Sofort reagieren, wenn in den VS Code Einstellungen etwas geändert wird
|
||||||
vscode.workspace.onDidChangeConfiguration(e => {
|
vscode.workspace.onDidChangeConfiguration(e => {
|
||||||
if (e.affectsConfiguration('tpl')) {
|
if (e.affectsConfiguration('tpl')) {
|
||||||
for (const doc of vscode.workspace.textDocuments) {
|
for (const doc of vscode.workspace.textDocuments) {
|
||||||
|
|
@ -22,14 +22,308 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Provider registrieren
|
|
||||||
context.subscriptions.push(vscode.languages.registerCodeActionsProvider('tpl', new TplCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }));
|
context.subscriptions.push(vscode.languages.registerCodeActionsProvider('tpl', new TplCodeActionProvider(), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }));
|
||||||
context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider('tpl', new TplDocumentFormatter()));
|
context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider('tpl', new TplDocumentFormatter()));
|
||||||
context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.'));
|
context.subscriptions.push(vscode.languages.registerCompletionItemProvider('tpl', new TplCompletionProvider(), '.'));
|
||||||
context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider()));
|
context.subscriptions.push(vscode.languages.registerHoverProvider('tpl', new TplHoverProvider()));
|
||||||
|
|
||||||
// Bei Aktivierung offene Dokumente einmal scannen
|
|
||||||
for (const doc of vscode.workspace.textDocuments) {
|
for (const doc of vscode.workspace.textDocuments) {
|
||||||
validateTplDocument(doc, diagnosticCollection);
|
validateTplDocument(doc, diagnosticCollection);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
|
||||||
|
if (document.languageId !== 'tpl') { return; }
|
||||||
|
|
||||||
|
const config = vscode.workspace.getConfiguration('tpl');
|
||||||
|
const configuredTplVersion = config.get<number>('version', 23);
|
||||||
|
|
||||||
|
const diagnostics: vscode.Diagnostic[] = [];
|
||||||
|
const text = document.getText();
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
let tplVersion = 10;
|
||||||
|
let hasModuleDef = false;
|
||||||
|
|
||||||
|
let inBlock: 'none' | 'pattern' | 'table' | 'definitions' | 'enumeration' | 'configuration' = 'none';
|
||||||
|
let currentPatternPhase: PatternPhase = PatternPhase.None;
|
||||||
|
let inMultiLineComment = false;
|
||||||
|
|
||||||
|
let inTripleDouble = false;
|
||||||
|
let inTripleSingle = false;
|
||||||
|
let stringNeedsSemicolon = false;
|
||||||
|
|
||||||
|
let activePhaseName = "";
|
||||||
|
let activePhaseLine = -1;
|
||||||
|
let currentPatternName = "";
|
||||||
|
|
||||||
|
// Modul-Ebene Metadaten Tracking
|
||||||
|
let inModuleMetadata = false;
|
||||||
|
let hasModuleMetadata = false;
|
||||||
|
let moduleMetadataBlockLine = -1;
|
||||||
|
let foundModuleMetadata = { origin: false, author: false, products: false, publishers: false, tree_path: false };
|
||||||
|
|
||||||
|
const parenStack: number[] = [];
|
||||||
|
const bracketStack: number[] = [];
|
||||||
|
const openDefines: number[] = [];
|
||||||
|
|
||||||
|
// State-Tracking für mehrzeilige Vergleiche / Zuweisungen
|
||||||
|
let inConditionBlock = false;
|
||||||
|
|
||||||
|
function addDiag(lineIdx: number, start: number, end: number, msg: string, severity: vscode.DiagnosticSeverity): vscode.Diagnostic {
|
||||||
|
const diag = new vscode.Diagnostic(new vscode.Range(lineIdx, start, lineIdx, end), msg, severity);
|
||||||
|
diagnostics.push(diag);
|
||||||
|
return diag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version prüfen
|
||||||
|
const versionMatch = text.match(/tpl\s+1\.(\d+)\s+module/);
|
||||||
|
if (versionMatch) {
|
||||||
|
tplVersion = parseInt(versionMatch[1], 10);
|
||||||
|
// Warnung nur, wenn die Datei älter ist als die in den Settings eingestellte Version! Höher ist in Ordnung.
|
||||||
|
if (tplVersion < configuredTplVersion) {
|
||||||
|
const linesToSearch = text.substring(0, versionMatch.index! + 30).split('\n');
|
||||||
|
const lineIdx = linesToSearch.length - 1;
|
||||||
|
addDiag(lineIdx, 0, versionMatch[0].length, `Das Modul nutzt eine alte TPL Version (1.${tplVersion}). Aktuell in den Settings definiert ist 1.${configuredTplVersion}.`, vscode.DiagnosticSeverity.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const rawLine = lines[i];
|
||||||
|
const line = rawLine.trim();
|
||||||
|
|
||||||
|
const tripleDoubleCount = (rawLine.match(/"""/g) || []).length;
|
||||||
|
const tripleSingleCount = (rawLine.match(/'''/g) || []).length;
|
||||||
|
|
||||||
|
let isInternalStringLine = false;
|
||||||
|
let justOpenedString = false;
|
||||||
|
let isClosingString = false;
|
||||||
|
|
||||||
|
if (inTripleDouble) {
|
||||||
|
isInternalStringLine = true;
|
||||||
|
if (tripleDoubleCount % 2 !== 0) { inTripleDouble = false; isClosingString = true; }
|
||||||
|
} else if (tripleDoubleCount % 2 !== 0) {
|
||||||
|
inTripleDouble = true; justOpenedString = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inTripleSingle) {
|
||||||
|
isInternalStringLine = true;
|
||||||
|
if (tripleSingleCount % 2 !== 0) { inTripleSingle = false; isClosingString = true; }
|
||||||
|
} else if (tripleSingleCount % 2 !== 0 && !justOpenedString) {
|
||||||
|
inTripleSingle = true; justOpenedString = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInternalStringLine && (inTripleDouble || inTripleSingle)) { continue; }
|
||||||
|
if (justOpenedString) { stringNeedsSemicolon = !line.startsWith('"""') && !line.startsWith("'''"); }
|
||||||
|
|
||||||
|
const tabIndex = rawLine.indexOf('\t');
|
||||||
|
if (tabIndex !== -1) {
|
||||||
|
const diag = addDiag(i, tabIndex, tabIndex + 1, "Bitte Tabs durch Spaces ersetzen", vscode.DiagnosticSeverity.Warning);
|
||||||
|
diag.code = 'invalid_tab';
|
||||||
|
}
|
||||||
|
|
||||||
|
let cleanLineForParens = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '');
|
||||||
|
const commentIdx = cleanLineForParens.indexOf('//');
|
||||||
|
if (commentIdx !== -1) { cleanLineForParens = cleanLineForParens.substring(0, commentIdx); }
|
||||||
|
|
||||||
|
for (let charIdx = 0; charIdx < cleanLineForParens.length; charIdx++) {
|
||||||
|
const char = cleanLineForParens[charIdx];
|
||||||
|
if (char === '(') { parenStack.push(i); }
|
||||||
|
else if (char === ')') {
|
||||||
|
if (parenStack.length > 0) { parenStack.pop(); }
|
||||||
|
else { addDiag(i, 0, rawLine.length, "Unerwartete schließende Klammer ')'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); }
|
||||||
|
}
|
||||||
|
else if (char === '[') { bracketStack.push(i); }
|
||||||
|
else if (char === ']') {
|
||||||
|
if (bracketStack.length > 0) { bracketStack.pop(); }
|
||||||
|
else { addDiag(i, 0, rawLine.length, "Unerwartete schließende eckige Klammer ']'. Es fehlt eine öffnende Klammer.", vscode.DiagnosticSeverity.Error); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('//') || line.startsWith('/*')) { if (line.startsWith('/*')) {inMultiLineComment = true;} continue; }
|
||||||
|
if (inMultiLineComment) { if (line.includes('*/')) {inMultiLineComment = false;} continue; }
|
||||||
|
if (line.length === 0) { continue; }
|
||||||
|
|
||||||
|
// --- HIER IST DER KORRIGIERTE LINTER FÜR ZUWEISUNGEN ---
|
||||||
|
const lineWithoutStrings = rawLine.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, '""');
|
||||||
|
|
||||||
|
// 1. Schaltet den Condition-Block EIN
|
||||||
|
if (/\b(if|elif|where)\b/.test(lineWithoutStrings)) {
|
||||||
|
inConditionBlock = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. "=" PRÜFUNG (solange der Block noch aktiv ist)
|
||||||
|
if (lineWithoutStrings.includes('=') && !lineWithoutStrings.includes(':=') && !lineWithoutStrings.includes('>=') && !lineWithoutStrings.includes('<=')) {
|
||||||
|
if (!inConditionBlock && parenStack.length === 0 && bracketStack.length === 0 && !/\b(matches)\b/.test(lineWithoutStrings)) {
|
||||||
|
const idx = rawLine.indexOf('=');
|
||||||
|
if (idx !== -1) {
|
||||||
|
addDiag(i, idx, idx + 1, "Falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt.", vscode.DiagnosticSeverity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Schaltet den Condition-Block wieder AUS (greift für die nächste Zeile)
|
||||||
|
if (/\b(then|do)\b/.test(lineWithoutStrings) || lineWithoutStrings.endsWith(';')) {
|
||||||
|
inConditionBlock = false;
|
||||||
|
}
|
||||||
|
// -------------------------------------------------------
|
||||||
|
|
||||||
|
if (isClosingString) {
|
||||||
|
if (stringNeedsSemicolon && !line.endsWith(';')) {
|
||||||
|
addDiag(i, rawLine.length - 1 < 0 ? 0 : rawLine.length - 1, rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const noSemicolonNeeded = /^(pattern|syncmapping|traverse|table|enumeration|configuration|definitions?|define|metadata|overview|constants|triggers|mapping|removal|body|end\s+|if\s+|else|elif\s+|for\s+|while\s+|\/\/|\/\*|\*)/;
|
||||||
|
const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches)$/i.test(line);
|
||||||
|
|
||||||
|
if (!noSemicolonNeeded.test(line) && !justOpenedString && parenStack.length === 0 && bracketStack.length === 0 && inBlock !== 'enumeration' && !inConditionBlock && !continuesOnNextLine) {
|
||||||
|
const lineToCheck = line.split('//')[0].trim();
|
||||||
|
if (!lineToCheck.endsWith(';') && !lineToCheck.endsWith('{') && !lineToCheck.endsWith(',')) {
|
||||||
|
addDiag(i, rawLine.length - 1 < 0 ? 0 : rawLine.length - 1, rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasModuleDef) {
|
||||||
|
if (line.startsWith('tpl ')) { hasModuleDef = true; }
|
||||||
|
else { addDiag(i, 0, rawLine.length, "Jede Datei muss mit 'tpl 1.x module <name>;' beginnen.", vscode.DiagnosticSeverity.Error); hasModuleDef = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('metadata') && inBlock === 'none') {
|
||||||
|
inModuleMetadata = true;
|
||||||
|
hasModuleMetadata = true;
|
||||||
|
moduleMetadataBlockLine = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inModuleMetadata) {
|
||||||
|
if (line.match(/^origin\b/)) { foundModuleMetadata.origin = true; }
|
||||||
|
if (line.match(/^author\b/)) { foundModuleMetadata.author = true; }
|
||||||
|
if (line.match(/^products\b/)) { foundModuleMetadata.products = true; }
|
||||||
|
if (line.match(/^publishers\b/)) { foundModuleMetadata.publishers = true; }
|
||||||
|
if (line.match(/^tree_path\b/)) { foundModuleMetadata.tree_path = true; }
|
||||||
|
|
||||||
|
if (line.startsWith('end metadata')) {
|
||||||
|
inModuleMetadata = false;
|
||||||
|
const missing = Object.keys(foundModuleMetadata).filter(k => !(foundModuleMetadata as any)[k]);
|
||||||
|
if (missing.length > 0 && moduleMetadataBlockLine !== -1) {
|
||||||
|
const diag = addDiag(moduleMetadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten (Modul-Ebene): ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error);
|
||||||
|
diag.code = 'missing_module_metadata';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) {
|
||||||
|
if (!hasModuleMetadata && inBlock === 'none') {
|
||||||
|
addDiag(i, 0, line.length, "Ein 'metadata' Block auf Modul-Ebene ist Pflicht, bevor andere Blöcke definiert werden.", vscode.DiagnosticSeverity.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('definitions ') || line.startsWith('definition ')) {
|
||||||
|
if (inBlock !== 'none') { addDiag(i, 0, line.length, "Ein Definitions-Block muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
||||||
|
inBlock = 'definitions'; continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('define ')) { openDefines.push(i); continue; }
|
||||||
|
|
||||||
|
if (line.startsWith('enumeration ')) {
|
||||||
|
if (inBlock !== 'none') { addDiag(i, 0, line.length, "Eine Enumeration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
||||||
|
inBlock = 'enumeration'; continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('configuration ')) {
|
||||||
|
if (inBlock !== 'none') { addDiag(i, 0, line.length, "Eine Konfiguration muss auf oberster Ebene deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
||||||
|
inBlock = 'configuration'; continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('pattern ') || line.startsWith('syncmapping ')) {
|
||||||
|
if (inBlock !== 'none') { addDiag(i, 0, 7, "Ein Pattern/Syncmapping kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
||||||
|
inBlock = 'pattern';
|
||||||
|
currentPatternPhase = PatternPhase.None;
|
||||||
|
activePhaseName = "";
|
||||||
|
activePhaseLine = -1;
|
||||||
|
|
||||||
|
const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/);
|
||||||
|
if (nameMatch) { currentPatternName = nameMatch[2]; }
|
||||||
|
else { addDiag(i, 0, rawLine.length, "Ungültige Pattern-Deklaration. Erwartet: 'pattern <Name> <Version>'", vscode.DiagnosticSeverity.Error); }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('table ')) {
|
||||||
|
if (inBlock !== 'none') { addDiag(i, 0, 5, "Eine Tabelle kann nicht innerhalb eines anderen Blocks deklariert werden.", vscode.DiagnosticSeverity.Error); }
|
||||||
|
inBlock = 'table'; continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inBlock === 'pattern') {
|
||||||
|
const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => {
|
||||||
|
if (line.startsWith(phaseName)) {
|
||||||
|
if (activePhaseName !== "") { addDiag(activePhaseLine, 0, activePhaseName.length, `Block '${activePhaseName}' wurde nicht mit 'end ${activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
|
||||||
|
if (currentPatternPhase >= phaseEnum) { addDiag(i, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt.`, vscode.DiagnosticSeverity.Error); }
|
||||||
|
|
||||||
|
currentPatternPhase = phaseEnum;
|
||||||
|
activePhaseName = phaseName;
|
||||||
|
activePhaseLine = i;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkPhase('metadata', PatternPhase.Metadata);
|
||||||
|
checkPhase('overview', PatternPhase.Overview);
|
||||||
|
checkPhase('constants', PatternPhase.Constants);
|
||||||
|
checkPhase('triggers', PatternPhase.Triggers);
|
||||||
|
checkPhase('mapping', PatternPhase.Mapping);
|
||||||
|
checkPhase('removal', PatternPhase.Removal);
|
||||||
|
|
||||||
|
if (line.startsWith('body')) {
|
||||||
|
if (activePhaseName !== "") { addDiag(activePhaseLine, 0, activePhaseName.length, `Block '${activePhaseName}' wurde nicht mit 'end ${activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
|
||||||
|
activePhaseName = "body";
|
||||||
|
activePhaseLine = i;
|
||||||
|
|
||||||
|
let foundLog = false;
|
||||||
|
for (let j = i + 1; j < lines.length; j++) {
|
||||||
|
const aheadLine = lines[j].trim();
|
||||||
|
if (aheadLine.startsWith('end pattern') || aheadLine.startsWith('end syncmapping')) { break; }
|
||||||
|
if (aheadLine.includes('log.debug') && aheadLine.includes(currentPatternName)) { foundLog = true; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundLog) {
|
||||||
|
const expectedLog = `log.debug("Start pattern ${currentPatternName}");`;
|
||||||
|
const diag = addDiag(i, 0, rawLine.length, `Im Body fehlt das Log-Statement mit dem Pattern-Namen. Bsp: ${expectedLog}`, vscode.DiagnosticSeverity.Error);
|
||||||
|
diag.code = 'missing_log_start'; diag.source = expectedLog;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closers = ['end metadata', 'end overview', 'end constants', 'end triggers', 'end mapping', 'end removal', 'end body'];
|
||||||
|
for (const closer of closers) {
|
||||||
|
if (line.startsWith(closer)) {
|
||||||
|
const phase = closer.replace('end ', '').replace(';', '').trim();
|
||||||
|
if (activePhaseName !== phase) { addDiag(i, 0, line.length, `Unerwartetes '${closer}'. Erwartet wurde 'end ${activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
||||||
|
else { activePhaseName = ""; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith('end pattern') || line.startsWith('end syncmapping')) {
|
||||||
|
if (activePhaseName !== "") { addDiag(activePhaseLine, 0, activePhaseName.length, `Block '${activePhaseName}' wurde nicht mit 'end ${activePhaseName};' geschlossen.`, vscode.DiagnosticSeverity.Error); }
|
||||||
|
if (inBlock !== 'pattern') { addDiag(i, 0, 11, "'end pattern' gefunden, ohne offenen Pattern-Block.", vscode.DiagnosticSeverity.Error); }
|
||||||
|
inBlock = 'none';
|
||||||
|
}
|
||||||
|
else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) {
|
||||||
|
inBlock = 'none';
|
||||||
|
}
|
||||||
|
else if (line.startsWith('end definitions') || line.startsWith('end definition')) {
|
||||||
|
if (inBlock !== 'definitions') { addDiag(i, 0, line.length, "'end definitions' gefunden, ohne offenen Definitions-Block.", vscode.DiagnosticSeverity.Error); }
|
||||||
|
inBlock = 'none';
|
||||||
|
}
|
||||||
|
else if (line.startsWith('end define')) {
|
||||||
|
if (openDefines.length > 0) { openDefines.pop(); }
|
||||||
|
else { addDiag(i, 0, line.length, "'end define' gefunden, ohne offenes 'define'.", vscode.DiagnosticSeverity.Error); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const unclosedLine of parenStack) { addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende Klammer '(' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); }
|
||||||
|
for (const unclosedLine of bracketStack) { addDiag(unclosedLine, 0, lines[unclosedLine].length, "Öffnende eckige Klammer '[' wird nicht geschlossen!", vscode.DiagnosticSeverity.Error); }
|
||||||
|
for (const unclosedLine of openDefines) { addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' wurde nicht mit 'end define;' geschlossen!", vscode.DiagnosticSeverity.Error); }
|
||||||
|
|
||||||
|
collection.set(document.uri, diagnostics);
|
||||||
}
|
}
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { TplQuickFix } from './actions/TplQuickFix';
|
|
||||||
import { TabFix } from './actions/TabFix';
|
|
||||||
import { LogFix } from './actions/LogFix';
|
|
||||||
import { MetadataFix } from './actions/MetadataFix';
|
|
||||||
|
|
||||||
export class TplCodeActionProvider implements vscode.CodeActionProvider {
|
|
||||||
private fixes: Map<string, TplQuickFix> = new Map();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
// Hier werden alle Quick Fixes registriert
|
|
||||||
const availableFixes: TplQuickFix[] = [
|
|
||||||
new TabFix(),
|
|
||||||
new LogFix(),
|
|
||||||
new MetadataFix()
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const fix of availableFixes) {
|
|
||||||
this.fixes.set(fix.diagnosticCode, fix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public provideCodeActions(
|
|
||||||
document: vscode.TextDocument,
|
|
||||||
range: vscode.Range | vscode.Selection,
|
|
||||||
context: vscode.CodeActionContext,
|
|
||||||
token: vscode.CancellationToken
|
|
||||||
): vscode.CodeAction[] {
|
|
||||||
|
|
||||||
const actions: vscode.CodeAction[] = [];
|
|
||||||
|
|
||||||
// Prüfen, ob für die aktuellen Fehler (Diagnostics) ein Fix existiert
|
|
||||||
for (const diagnostic of context.diagnostics) {
|
|
||||||
const code = diagnostic.code;
|
|
||||||
if (typeof code === 'string' && this.fixes.has(code)) {
|
|
||||||
const fixLogic = this.fixes.get(code)!;
|
|
||||||
actions.push(...fixLogic.provideFix(document, diagnostic));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { CompletionStrategy } from './completions/CompletionStrategy';
|
|
||||||
import { SnippetCompletion } from './completions/SnippetCompletion';
|
|
||||||
import { BuiltInCompletion } from './completions/BuiltInCompletion';
|
|
||||||
import { LocalContextCompletion } from './completions/LocalContextCompletion';
|
|
||||||
|
|
||||||
export class TplCompletionProvider implements vscode.CompletionItemProvider {
|
|
||||||
private strategies: CompletionStrategy[];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
// Hier registrieren wir alle Completion Engines
|
|
||||||
this.strategies = [
|
|
||||||
new BuiltInCompletion(),
|
|
||||||
new SnippetCompletion(),
|
|
||||||
new LocalContextCompletion()
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public provideCompletionItems(
|
|
||||||
document: vscode.TextDocument,
|
|
||||||
position: vscode.Position,
|
|
||||||
token: vscode.CancellationToken,
|
|
||||||
context: vscode.CompletionContext
|
|
||||||
): vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList> {
|
|
||||||
|
|
||||||
// Den Text in der aktuellen Zeile bis zum Cursor holen
|
|
||||||
const linePrefix = document.lineAt(position).text.substring(0, position.character);
|
|
||||||
const results: vscode.CompletionItem[] = [];
|
|
||||||
|
|
||||||
// Jede Engine um Vorschläge bitten
|
|
||||||
for (const strategy of this.strategies) {
|
|
||||||
const items = strategy.provideCompletions(document, position, linePrefix);
|
|
||||||
if (items && items.length > 0) {
|
|
||||||
results.push(...items);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { FormatState } from './formatters/FormatState';
|
|
||||||
import { IndentationRules } from './formatters/IndentationRules';
|
|
||||||
import { AlignmentRules } from './formatters/AlignmentRules';
|
|
||||||
|
|
||||||
export class TplDocumentFormatter implements vscode.DocumentFormattingEditProvider {
|
|
||||||
private indentRules = new IndentationRules();
|
|
||||||
private alignRules = new AlignmentRules();
|
|
||||||
|
|
||||||
public provideDocumentFormattingEdits(
|
|
||||||
document: vscode.TextDocument,
|
|
||||||
options: vscode.FormattingOptions,
|
|
||||||
token: vscode.CancellationToken
|
|
||||||
): vscode.ProviderResult<vscode.TextEdit[]> {
|
|
||||||
|
|
||||||
const edits: vscode.TextEdit[] = [];
|
|
||||||
const state = new FormatState();
|
|
||||||
|
|
||||||
// Nutze die VS Code Settings (meist 4 Spaces), andernfalls Fallback
|
|
||||||
state.tabSize = options.tabSize || 4;
|
|
||||||
|
|
||||||
for (let i = 0; i < document.lineCount; i++) {
|
|
||||||
const originalLine = document.lineAt(i);
|
|
||||||
const rawText = originalLine.text;
|
|
||||||
const trimmedText = rawText.trim();
|
|
||||||
|
|
||||||
// Leere Zeilen lassen wir leer
|
|
||||||
if (trimmedText.length === 0) {
|
|
||||||
if (rawText.length > 0) {
|
|
||||||
// Löscht unnötige Whitespaces in eigentlich leeren Zeilen
|
|
||||||
edits.push(vscode.TextEdit.replace(originalLine.range, ''));
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Basis-Einrückung (if/else/pattern) berechnen
|
|
||||||
let formattedText = this.indentRules.apply(trimmedText, state);
|
|
||||||
|
|
||||||
// 2. Visual Alignment (Klammern) anwenden & Stack updaten
|
|
||||||
formattedText = this.alignRules.apply(trimmedText, formattedText, state);
|
|
||||||
|
|
||||||
// 3. Edit nur erstellen, wenn sich die Zeile wirklich ändert (spart Performance & Cursor-Sprünge)
|
|
||||||
if (rawText !== formattedText) {
|
|
||||||
edits.push(vscode.TextEdit.replace(originalLine.range, formattedText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return edits;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { TplQuickFix } from './TplQuickFix';
|
|
||||||
|
|
||||||
export class LogFix implements TplQuickFix {
|
|
||||||
public readonly diagnosticCode = 'missing_log_start';
|
|
||||||
|
|
||||||
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
|
||||||
const fix = new vscode.CodeAction("Fehlendes Log-Statement einfügen", vscode.CodeActionKind.QuickFix);
|
|
||||||
fix.edit = new vscode.WorkspaceEdit();
|
|
||||||
|
|
||||||
// Der Linter hat uns den exakten String in die 'source' gepackt
|
|
||||||
const expectedLog = diagnostic.source || 'log.debug("Start pattern");';
|
|
||||||
|
|
||||||
// Füge es eine Zeile unter dem 'body' Keyword ein, sauber eingerückt
|
|
||||||
const insertText = ` ${expectedLog}\n`;
|
|
||||||
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText);
|
|
||||||
|
|
||||||
fix.diagnostics = [diagnostic];
|
|
||||||
fix.isPreferred = true;
|
|
||||||
|
|
||||||
return [fix];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { TplQuickFix } from './TplQuickFix';
|
|
||||||
|
|
||||||
export class MetadataFix implements TplQuickFix {
|
|
||||||
public readonly diagnosticCode = 'missing_module_metadata';
|
|
||||||
|
|
||||||
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
|
||||||
const fix = new vscode.CodeAction("Fehlende Metadaten generieren", vscode.CodeActionKind.QuickFix);
|
|
||||||
fix.edit = new vscode.WorkspaceEdit();
|
|
||||||
|
|
||||||
// Einstellungen laden
|
|
||||||
const config = vscode.workspace.getConfiguration('tpl');
|
|
||||||
const author = config.get<string>('metadata.author', 'Kuro');
|
|
||||||
const origin = config.get<string>('metadata.origin', 'Custom');
|
|
||||||
const publisher = config.get<string>('metadata.publisher', 'IT-Operations');
|
|
||||||
const treePathBase = config.get<string>('metadata.treePathBase', 'Grafing');
|
|
||||||
|
|
||||||
// Herausfinden, welche Felder fehlen (steht im Diagnostic-Message-String)
|
|
||||||
const missingFieldsStr = diagnostic.message.replace('Fehlende Pflicht-Metadaten (Modul-Ebene): ', '');
|
|
||||||
const missingFields = missingFieldsStr.split(', ');
|
|
||||||
|
|
||||||
let insertText = '';
|
|
||||||
if (missingFields.includes('origin')) insertText += ` origin := '${origin}';\n`;
|
|
||||||
if (missingFields.includes('author')) insertText += ` author := '${author}';\n`;
|
|
||||||
if (missingFields.includes('products')) insertText += ` products := 'TODO_Product';\n`;
|
|
||||||
if (missingFields.includes('publishers')) insertText += ` publishers := '${publisher}';\n`;
|
|
||||||
if (missingFields.includes('tree_path')) insertText += ` tree_path := '${treePathBase}', 'TODO_Category', 'TODO_Subcategory';\n`;
|
|
||||||
|
|
||||||
// Direkt über der Zeile 'end metadata;' einfügen
|
|
||||||
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line, 0), insertText);
|
|
||||||
fix.diagnostics = [diagnostic];
|
|
||||||
|
|
||||||
return [fix];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { TplQuickFix } from './TplQuickFix';
|
|
||||||
|
|
||||||
export class TabFix implements TplQuickFix {
|
|
||||||
public readonly diagnosticCode = 'invalid_tab';
|
|
||||||
|
|
||||||
public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] {
|
|
||||||
const fix = new vscode.CodeAction("Tabs in 4 Spaces umwandeln", vscode.CodeActionKind.QuickFix);
|
|
||||||
fix.edit = new vscode.WorkspaceEdit();
|
|
||||||
|
|
||||||
const line = document.lineAt(diagnostic.range.start.line);
|
|
||||||
const newText = line.text.replace(/\t/g, ' ');
|
|
||||||
|
|
||||||
fix.edit.replace(document.uri, line.range, newText);
|
|
||||||
fix.diagnostics = [diagnostic];
|
|
||||||
fix.isPreferred = true; // Setzt dies als Standard-Vorschlag
|
|
||||||
|
|
||||||
return [fix];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
|
|
||||||
export interface TplQuickFix {
|
|
||||||
// Der Fehlercode aus dem Linter (z.B. 'invalid_tab')
|
|
||||||
readonly diagnosticCode: string;
|
|
||||||
|
|
||||||
// Die Methode, die den Fix generiert
|
|
||||||
provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[];
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { CompletionStrategy } from './CompletionStrategy';
|
|
||||||
|
|
||||||
export class BuiltInCompletion implements CompletionStrategy {
|
|
||||||
|
|
||||||
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
|
|
||||||
const items: vscode.CompletionItem[] = [];
|
|
||||||
|
|
||||||
// Prüft, ob wir im Namespace "log." oder "discovery." sind (auch wenn man danach weiter tippt!)
|
|
||||||
const match = linePrefix.match(/\b(log|discovery)\.([a-zA-Z0-9_]*)$/);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const namespace = match[1];
|
|
||||||
|
|
||||||
if (namespace === 'log') {
|
|
||||||
const levels = ['debug', 'info', 'warn', 'error'];
|
|
||||||
for (const level of levels) {
|
|
||||||
const item = new vscode.CompletionItem(level, vscode.CompletionItemKind.Method);
|
|
||||||
item.insertText = new vscode.SnippetString(`${level}("\${1:message}");`);
|
|
||||||
item.documentation = new vscode.MarkdownString(`Schreibt eine Log-Meldung auf dem Level **${level}**.`);
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (namespace === 'discovery') {
|
|
||||||
const runCmd = new vscode.CompletionItem('runCommand', vscode.CompletionItemKind.Method);
|
|
||||||
runCmd.insertText = new vscode.SnippetString('runCommand(${1:host}, "${2:command}");');
|
|
||||||
runCmd.detail = "discovery.runCommand(host, command)";
|
|
||||||
items.push(runCmd);
|
|
||||||
}
|
|
||||||
return items; // Treffer gefunden, wir geben sie zurück!
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
|
|
||||||
export interface CompletionStrategy {
|
|
||||||
/**
|
|
||||||
* @param document Das aktuelle Dokument
|
|
||||||
* @param position Die Cursor-Position
|
|
||||||
* @param linePrefix Der Text der aktuellen Zeile *vor* dem Cursor
|
|
||||||
*/
|
|
||||||
provideCompletions(
|
|
||||||
document: vscode.TextDocument,
|
|
||||||
position: vscode.Position,
|
|
||||||
linePrefix: string
|
|
||||||
): vscode.CompletionItem[] | undefined;
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { CompletionStrategy } from './CompletionStrategy';
|
|
||||||
|
|
||||||
export class LocalContextCompletion implements CompletionStrategy {
|
|
||||||
|
|
||||||
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
|
|
||||||
// TODO für Phase 2:
|
|
||||||
// 1. Regex Suche im Dokument nach ":=" (Variablen tracken)
|
|
||||||
// 2. Projektweite Suche nach "define" Methoden für Autocomplete
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
import * as vscode from 'vscode';
|
|
||||||
import { CompletionStrategy } from './CompletionStrategy';
|
|
||||||
|
|
||||||
export class SnippetCompletion implements CompletionStrategy {
|
|
||||||
|
|
||||||
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
|
|
||||||
// Wenn wir gerade NACH einem Punkt tippen (z.B. "log."), sind Snippets/Base-Module hier falsch!
|
|
||||||
if (linePrefix.match(/\.[a-zA-Z0-9_]*$/)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items: vscode.CompletionItem[] = [];
|
|
||||||
const config = vscode.workspace.getConfiguration('tpl');
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 1. BASIS-NAMESPACES (discovery, log, etc.)
|
|
||||||
// ==========================================
|
|
||||||
const namespaces = ['discovery', 'log', 'model', 'search', 'text', 'regex', 'time', 'hash', 'number', 'inference'];
|
|
||||||
for (const ns of namespaces) {
|
|
||||||
const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module);
|
|
||||||
item.detail = `TPL Namespace: ${ns}`;
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 2. DAS GOD-SNIPPET (Skeleton)
|
|
||||||
// ==========================================
|
|
||||||
const skel = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
|
|
||||||
const version = config.get<number>('version', 23);
|
|
||||||
const author = config.get<string>('metadata.author', 'Kuro');
|
|
||||||
|
|
||||||
skel.insertText = new vscode.SnippetString(
|
|
||||||
`tpl 1.${version} module \${1:Name};\n\n` +
|
|
||||||
`metadata\n` +
|
|
||||||
` origin := '\${2:Custom}';\n` +
|
|
||||||
` author := '${author}';\n` +
|
|
||||||
` tree_path := 'Grafing', '\${3:Category}', '\${4:SubCategory}';\n` +
|
|
||||||
`end metadata;\n\n` +
|
|
||||||
`pattern \${5:PatternName} 1.0\n` +
|
|
||||||
` """\n \${6:Beschreibung}\n """\n` +
|
|
||||||
` overview\n tags \${7:tag1};\n end overview;\n\n` +
|
|
||||||
` triggers\n on host := Host created, confirmed;\n end triggers;\n\n` +
|
|
||||||
` body\n log.debug("Start pattern \${5:PatternName}");\n $0\n end body;\n` +
|
|
||||||
`end pattern;`
|
|
||||||
);
|
|
||||||
skel.detail = "Generiert ein komplettes TPL Grundgerüst";
|
|
||||||
items.push(skel);
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 3. TPL HAUPT-BLÖCKE (pattern, define, etc.)
|
|
||||||
// ==========================================
|
|
||||||
const mainBlocks = [
|
|
||||||
{ label: 'pattern', desc: 'pattern ... end pattern;' },
|
|
||||||
{ label: 'syncmapping', desc: 'syncmapping ... end syncmapping;' },
|
|
||||||
{ label: 'define', desc: 'define ... end define;' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const block of mainBlocks) {
|
|
||||||
const item = new vscode.CompletionItem(block.label, vscode.CompletionItemKind.Snippet);
|
|
||||||
item.insertText = new vscode.SnippetString(`${block.label} \${1:Name} \${2:1.0}\n """\n \${3:DocString}\n """\n $0\nend ${block.label};`);
|
|
||||||
if (block.label === 'define') {
|
|
||||||
item.insertText = new vscode.SnippetString(`define \${1:methodName}(\${2:params})\n """\n \${3:DocString}\n """\n $0\nend define;`);
|
|
||||||
}
|
|
||||||
item.detail = block.desc;
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 4. SECTIONS (metadata, body, triggers, etc.)
|
|
||||||
// ==========================================
|
|
||||||
const sections = ['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body'];
|
|
||||||
for (const sec of sections) {
|
|
||||||
const item = new vscode.CompletionItem(sec, vscode.CompletionItemKind.Snippet);
|
|
||||||
item.insertText = new vscode.SnippetString(`${sec}\n $0\nend ${sec};`);
|
|
||||||
item.detail = `${sec} ... end ${sec};`;
|
|
||||||
items.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 5. KONTROLLSTRUKTUREN & TABELLEN
|
|
||||||
// ==========================================
|
|
||||||
const ifSnippet = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet);
|
|
||||||
ifSnippet.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;');
|
|
||||||
ifSnippet.detail = "if ... then ... end if;";
|
|
||||||
items.push(ifSnippet);
|
|
||||||
|
|
||||||
const forSnippet = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet);
|
|
||||||
forSnippet.insertText = new vscode.SnippetString('for ${1:item} in${2:list} do\n $0\nend for;');
|
|
||||||
forSnippet.detail = "for ... in ... do ... end for;";
|
|
||||||
items.push(forSnippet);
|
|
||||||
|
|
||||||
const tableSnippet = new vscode.CompletionItem('table', vscode.CompletionItemKind.Snippet);
|
|
||||||
tableSnippet.insertText = new vscode.SnippetString('table ${1:TableName} 1.0\n ${2:"key"} -> "${3:value}";\n default -> "unknown";\nend table;');
|
|
||||||
tableSnippet.detail = "table ... end table;";
|
|
||||||
items.push(tableSnippet);
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import { FormatState } from './FormatState';
|
|
||||||
|
|
||||||
export class AlignmentRules {
|
|
||||||
|
|
||||||
public apply(line: string, formattedLine: string, state: FormatState): string {
|
|
||||||
let finalLine = formattedLine;
|
|
||||||
|
|
||||||
// Wenn wir innerhalb einer offenen Klammer sind, überschreibe die Block-Einrückung
|
|
||||||
if (state.parenAlignments.length > 0) {
|
|
||||||
const alignCol = state.parenAlignments[state.parenAlignments.length - 1];
|
|
||||||
finalLine = ' '.repeat(alignCol) + line;
|
|
||||||
} else if (state.bracketAlignments.length > 0) {
|
|
||||||
const alignCol = state.bracketAlignments[state.bracketAlignments.length - 1];
|
|
||||||
finalLine = ' '.repeat(alignCol) + line;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualisiere Stacks basierend auf der FINAL formatierten Zeile (wichtig für die Spaltenzahl!)
|
|
||||||
this.updateAlignments(finalLine, state);
|
|
||||||
|
|
||||||
return finalLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateAlignments(line: string, state: FormatState) {
|
|
||||||
// Strings ignorieren, damit wir keine Klammern in Log-Texten zählen
|
|
||||||
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""');
|
|
||||||
|
|
||||||
for (let i = 0; i < cleanLine.length; i++) {
|
|
||||||
const char = cleanLine[i];
|
|
||||||
if (char === '(') {
|
|
||||||
// Einrückung ab dem Zeichen NACH der offenen Klammer
|
|
||||||
state.parenAlignments.push(i + 1);
|
|
||||||
} else if (char === ')') {
|
|
||||||
state.parenAlignments.pop();
|
|
||||||
} else if (char === '[') {
|
|
||||||
state.bracketAlignments.push(i + 1);
|
|
||||||
} else if (char === ']') {
|
|
||||||
state.bracketAlignments.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
export class FormatState {
|
|
||||||
public indentLevel: number = 0;
|
|
||||||
public tabSize: number = 4;
|
|
||||||
|
|
||||||
// Speichert die Spalten-Position offener Klammern für Visual Alignment
|
|
||||||
public parenAlignments: number[] = [];
|
|
||||||
public bracketAlignments: number[] = [];
|
|
||||||
|
|
||||||
// Gibt die aktuellen Basis-Leerzeichen zurück (z.B. Level 2 = 8 Leerzeichen)
|
|
||||||
public get currentIndentString(): string {
|
|
||||||
return ' '.repeat(this.indentLevel * this.tabSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { FormatState } from './FormatState';
|
|
||||||
|
|
||||||
export class IndentationRules {
|
|
||||||
// Keywords, die eine neue Einrückungsebene öffnen (ohne static!)
|
|
||||||
private readonly blockOpens = /^(pattern|syncmapping|table|definitions?|enumeration|configuration|define|metadata|overview|constants|triggers|mapping|removal|body|if\s+|for\s+|while\s+)\b/;
|
|
||||||
|
|
||||||
// Keywords, die eine Ebene schließen (ohne static!)
|
|
||||||
private readonly blockCloses = /^(end\s+|else\b|elif\b)/;
|
|
||||||
|
|
||||||
public apply(line: string, state: FormatState): string {
|
|
||||||
// 1. Wenn die Zeile einen Block schließt, VORHER die Einrückung verringern
|
|
||||||
if (this.blockCloses.test(line) && state.indentLevel > 0) {
|
|
||||||
state.indentLevel--;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktuelle Einrückung für diese Zeile abholen
|
|
||||||
const currentIndent = state.currentIndentString;
|
|
||||||
|
|
||||||
// 2. NACH der Zeile: Wenn sie einen Block öffnet, Level für die nächste Zeile erhöhen
|
|
||||||
if (/^(else\b|elif\b)/.test(line)) {
|
|
||||||
// else / elif springen nach dem Schließen direkt wieder auf
|
|
||||||
state.indentLevel++;
|
|
||||||
}
|
|
||||||
else if (this.blockOpens.test(line)) {
|
|
||||||
state.indentLevel++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentIndent + line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import * as assert from 'assert';
|
||||||
|
|
||||||
|
// You can import and use all API from the 'vscode' module
|
||||||
|
// as well as import your extension to test it
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
// import * as myExtension from '../../extension';
|
||||||
|
|
||||||
|
suite('Extension Test Suite', () => {
|
||||||
|
vscode.window.showInformationMessage('Start all tests.');
|
||||||
|
|
||||||
|
test('Sample test', () => {
|
||||||
|
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
|
||||||
|
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import * as assert from 'assert';
|
|
||||||
import { FormatState } from '../../providers/formatters/FormatState';
|
|
||||||
import { IndentationRules } from '../../providers/formatters/IndentationRules';
|
|
||||||
import { AlignmentRules } from '../../providers/formatters/AlignmentRules';
|
|
||||||
|
|
||||||
suite('Formatter Test Suite', () => {
|
|
||||||
test('IndentationRules: if/then/end Block korrekt einrücken', () => {
|
|
||||||
const indentRules = new IndentationRules();
|
|
||||||
const state = new FormatState();
|
|
||||||
state.tabSize = 4;
|
|
||||||
|
|
||||||
// Zeile 1: if condition then (erwartet: 0 Leerzeichen, aber Level springt auf 1 für die nächste Zeile)
|
|
||||||
let result = indentRules.apply('if x = 1 then', state);
|
|
||||||
assert.strictEqual(result, 'if x = 1 then');
|
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
|
||||||
|
|
||||||
// Zeile 2: normaler Code im Block (erwartet: 4 Leerzeichen)
|
|
||||||
result = indentRules.apply('log.debug("test");', state);
|
|
||||||
assert.strictEqual(result, ' log.debug("test");');
|
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
|
||||||
|
|
||||||
// Zeile 3: end if; (erwartet: 0 Leerzeichen, Level springt sofort vor der Ausgabe auf 0)
|
|
||||||
result = indentRules.apply('end if;', state);
|
|
||||||
assert.strictEqual(result, 'end if;');
|
|
||||||
assert.strictEqual(state.indentLevel, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('IndentationRules: elif und else verhalten sich korrekt', () => {
|
|
||||||
const indentRules = new IndentationRules();
|
|
||||||
const state = new FormatState();
|
|
||||||
state.tabSize = 4;
|
|
||||||
state.indentLevel = 1; // Wir starten quasi IN einem offenen if-Block
|
|
||||||
|
|
||||||
// elif muss zurückspringen (auf 0) und danach wieder auf 1 für den folgenden Block
|
|
||||||
let result = indentRules.apply('elif x = 2 then', state);
|
|
||||||
assert.strictEqual(result, 'elif x = 2 then');
|
|
||||||
assert.strictEqual(state.indentLevel, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('AlignmentRules: Visual Alignment an runden Klammern', () => {
|
|
||||||
const alignRules = new AlignmentRules();
|
|
||||||
const state = new FormatState();
|
|
||||||
|
|
||||||
// 1. Zeile mit öffnender Klammer
|
|
||||||
alignRules.apply('discovery.runCommand(', 'discovery.runCommand(', state);
|
|
||||||
assert.strictEqual(state.parenAlignments.length, 1);
|
|
||||||
assert.strictEqual(state.parenAlignments[0], 21); // 21 Zeichen bis nach der offenen Klammer
|
|
||||||
|
|
||||||
// 2. Zeile (Parameter soll exakt bis Spalte 21 eingerückt werden)
|
|
||||||
const result = alignRules.apply('host,', 'host,', state);
|
|
||||||
assert.strictEqual(result, ' host,');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import * as assert from 'assert';
|
|
||||||
import { LinterContext } from '../../core/LinterContext';
|
|
||||||
import { SyntaxRule } from '../../core/rules/SyntaxRule';
|
|
||||||
|
|
||||||
suite('Linter SyntaxRule Test Suite', () => {
|
|
||||||
test('Zuweisung: Warnt bei "=" statt ":="', () => {
|
|
||||||
const rule = new SyntaxRule();
|
|
||||||
const ctx = new LinterContext();
|
|
||||||
|
|
||||||
// Wir simulieren eine Code-Zeile
|
|
||||||
const rawLine = " my_var = 123;";
|
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
|
|
||||||
|
|
||||||
// Erwartung: Ein Fehler wegen falscher Zuweisung
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 1);
|
|
||||||
assert.ok(ctx.diagnostics[0].message.includes("In TPL wird ':=' für Zuweisungen genutzt"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Zuweisung: Akzeptiert "=" als Vergleich in if-Bedingung', () => {
|
|
||||||
const rule = new SyntaxRule();
|
|
||||||
const ctx = new LinterContext();
|
|
||||||
|
|
||||||
// Wir simulieren eine if-Zeile (setzt ctx.inConditionBlock intern auf true)
|
|
||||||
const rawLine = " if my_var = 123 then";
|
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
|
|
||||||
|
|
||||||
// Erwartung: Kein Fehler, da das = als Vergleich legitim ist
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Klammern: Findet ungeschlossene runde Klammern', () => {
|
|
||||||
const rule = new SyntaxRule();
|
|
||||||
const ctx = new LinterContext();
|
|
||||||
|
|
||||||
// Wir öffnen eine Klammer, schließen sie aber nicht
|
|
||||||
const rawLine = " my_func(parameter;";
|
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
|
|
||||||
|
|
||||||
// checkEnd wird am Ende der Datei aufgerufen
|
|
||||||
rule.checkEnd(['', rawLine], ctx);
|
|
||||||
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 1);
|
|
||||||
assert.ok(ctx.diagnostics[0].message.includes("Öffnende Klammer '(' wird nicht geschlossen!"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue