erkennung für fehlende end blocks, mehrzeilige if/for/while usw erkennung um keine semikolon fehler zu melden, besseres auslesen import defines, infos werden jetzt auch im Problems fenster gezeigt, bessere autovervollständigung, automatische fehlersuche über das projekt
This commit is contained in:
parent
eba674218e
commit
ba836ee30f
|
|
@ -62,7 +62,13 @@
|
||||||
"default": 23,
|
"default": 23,
|
||||||
"description": "Aktuelle TPL-Nebenversion (z. B. 23 für TPL 1.23). Wird genutzt um veraltete Module zu erkennen und beim Generieren eingefügt."
|
"description": "Aktuelle TPL-Nebenversion (z. B. 23 für TPL 1.23). Wird genutzt um veraltete Module zu erkennen und beim Generieren eingefügt."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "tpl.scanWorkspace",
|
||||||
|
"title": "TPL: Gesamten Workspace prüfen"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"languages": [
|
"languages": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ export class LinterContext {
|
||||||
public parenStack: number[] = [];
|
public parenStack: number[] = [];
|
||||||
public bracketStack: number[] = [];
|
public bracketStack: number[] = [];
|
||||||
public openDefines: number[] = [];
|
public openDefines: number[] = [];
|
||||||
|
public openControlBlocks: { type: string, lineIdx: number }[] = [];
|
||||||
|
|
||||||
public addDiag(lineIdx: number, start: number, end: number, msg: string, severity: vscode.DiagnosticSeverity): vscode.Diagnostic {
|
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);
|
const diag = new vscode.Diagnostic(new vscode.Range(lineIdx, start, lineIdx, end), msg, severity);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { LinterContext, PatternPhase } from '../LinterContext';
|
||||||
|
|
||||||
export class BlockRule implements LinterRule {
|
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 {
|
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
||||||
if (!ctx.hasModuleDef) {
|
if (!ctx.hasModuleDef) {
|
||||||
if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; }
|
if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; }
|
||||||
|
|
@ -31,8 +30,10 @@ 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);
|
||||||
|
|
||||||
if (ctx.inBlock === 'pattern') {
|
if (ctx.inBlock === 'pattern') {
|
||||||
// HIER GEFIXT: 'lines' wird jetzt korrekt an die Methode weitergereicht
|
|
||||||
this.checkPatternPhases(line, rawLine, lineIdx, ctx, lines);
|
this.checkPatternPhases(line, rawLine, lineIdx, ctx, lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,6 +44,38 @@ export class BlockRule implements LinterRule {
|
||||||
for (const unclosedLine of ctx.openDefines) {
|
for (const unclosedLine of ctx.openDefines) {
|
||||||
ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' wurde nicht mit 'end define;' geschlossen!", vscode.DiagnosticSeverity.Error);
|
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) {
|
||||||
|
// Block wird geöffnet
|
||||||
|
if (/^if\b/.test(line)) { ctx.openControlBlocks.push({ type: 'if', lineIdx }); }
|
||||||
|
else if (/^for\b/.test(line)) { ctx.openControlBlocks.push({ type: 'for', lineIdx }); }
|
||||||
|
else if (/^while\b/.test(line)) { ctx.openControlBlocks.push({ type: 'while', lineIdx }); }
|
||||||
|
|
||||||
|
// Block wird geschlossen
|
||||||
|
if (line.startsWith('end if')) { this.closeControlBlock('if', line, lineIdx, ctx); }
|
||||||
|
else if (line.startsWith('end for')) { this.closeControlBlock('for', line, lineIdx, ctx); }
|
||||||
|
else if (line.startsWith('end while')) { this.closeControlBlock('while', line, lineIdx, ctx); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeControlBlock(expectedType: string, line: string, lineIdx: number, ctx: LinterContext) {
|
||||||
|
if (ctx.openControlBlocks.length === 0) {
|
||||||
|
ctx.addDiag(lineIdx, 0, line.length, `'end ${expectedType}' gefunden, ohne offenen Block.`, vscode.DiagnosticSeverity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wir nehmen den zuletzt geöffneten Block vom Stapel
|
||||||
|
const lastBlock = ctx.openControlBlocks.pop()!;
|
||||||
|
|
||||||
|
if (lastBlock.type !== expectedType) {
|
||||||
|
ctx.addDiag(lineIdx, 0, line.length, `Falsches Ende. Erwartet: 'end ${lastBlock.type};', gefunden: 'end ${expectedType};'.`, vscode.DiagnosticSeverity.Error);
|
||||||
|
// Damit Folgefehler vermieden werden, legen wir ihn zurück
|
||||||
|
ctx.openControlBlocks.push(lastBlock);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private trackModuleMetadata(line: string, lineIdx: number, ctx: LinterContext) {
|
private trackModuleMetadata(line: string, lineIdx: number, ctx: LinterContext) {
|
||||||
|
|
@ -117,7 +150,6 @@ export class BlockRule implements LinterRule {
|
||||||
ctx.activePhaseName = "body";
|
ctx.activePhaseName = "body";
|
||||||
ctx.activePhaseLine = lineIdx;
|
ctx.activePhaseLine = lineIdx;
|
||||||
|
|
||||||
// DER LOG-CHECK IST ZURÜCK!
|
|
||||||
let foundLog = false;
|
let foundLog = false;
|
||||||
for (let j = lineIdx + 1; j < lines.length; j++) {
|
for (let j = lineIdx + 1; j < lines.length; j++) {
|
||||||
const aheadLine = lines[j].trim();
|
const aheadLine = lines[j].trim();
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,47 @@ import { LinterContext } from '../LinterContext';
|
||||||
|
|
||||||
export class SyntaxRule implements LinterRule {
|
export class SyntaxRule implements LinterRule {
|
||||||
|
|
||||||
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext): void {
|
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
||||||
this.checkTabs(rawLine, lineIdx, ctx);
|
this.checkTabs(rawLine, lineIdx, ctx);
|
||||||
this.checkBrackets(line, rawLine, lineIdx, ctx);
|
|
||||||
this.checkAssignments(rawLine, lineIdx, ctx);
|
// TODOs finden (blauer Info-Hinweis)
|
||||||
this.checkSemicolons(line, rawLine, lineIdx, ctx);
|
const todoMatch = rawLine.match(/\bTODO\s*:(.*)/i);
|
||||||
|
if (todoMatch) {
|
||||||
|
ctx.addDiag(lineIdx, todoMatch.index!, rawLine.length, `TODO: ${todoMatch[1].trim()}`, vscode.DiagnosticSeverity.Information);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""');
|
||||||
|
const commentIdx = cleanLine.indexOf('//');
|
||||||
|
let codePart = commentIdx !== -1 ? cleanLine.substring(0, commentIdx).trim() : cleanLine.trim();
|
||||||
|
|
||||||
|
if (codePart.length === 0) return;
|
||||||
|
|
||||||
|
let isStructuralKeyword = /^(tpl|pattern|syncmapping|table|definitions?|define|enumeration|configuration|metadata|overview|constants|triggers|mapping|removal|body|end|on|tags|origin|author|products|publishers|tree_path|import|from)\b/.test(codePart);
|
||||||
|
|
||||||
|
if (codePart.startsWith('"') || codePart.startsWith("'")) {
|
||||||
|
isStructuralKeyword = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidExecutionArea =
|
||||||
|
ctx.activePhaseName === 'body' ||
|
||||||
|
ctx.activePhaseName === 'triggers' ||
|
||||||
|
ctx.activePhaseName === 'constants' ||
|
||||||
|
ctx.activePhaseName === 'overview' ||
|
||||||
|
ctx.activePhaseName === 'mapping' ||
|
||||||
|
ctx.activePhaseName === 'metadata' ||
|
||||||
|
ctx.inModuleMetadata ||
|
||||||
|
ctx.inBlock === 'table' ||
|
||||||
|
ctx.inBlock === 'definitions' ||
|
||||||
|
ctx.openDefines.length > 0;
|
||||||
|
|
||||||
|
// 'then' und 'do' auf alleinstehenden Zeilen sind legitimer Code
|
||||||
|
if (!isStructuralKeyword && !isValidExecutionArea && !/^(then|do)\b/.test(codePart)) {
|
||||||
|
ctx.addDiag(lineIdx, 0, rawLine.length, "Ausführbarer Code ist hier nicht erlaubt. (Erlaubt in: 'body', 'triggers', 'constants', 'overview', 'mapping', 'metadata', 'definitions' oder 'define').", vscode.DiagnosticSeverity.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkBrackets(codePart, rawLine, lineIdx, ctx);
|
||||||
|
this.checkAssignments(codePart, rawLine, lineIdx, ctx);
|
||||||
|
this.checkSemicolons(codePart, rawLine, lineIdx, ctx, line);
|
||||||
}
|
}
|
||||||
|
|
||||||
public checkEnd(lines: string[], ctx: LinterContext): void {
|
public checkEnd(lines: string[], ctx: LinterContext): void {
|
||||||
|
|
@ -24,13 +60,9 @@ export class SyntaxRule implements LinterRule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkBrackets(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
private checkBrackets(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
||||||
let cleanLine = line.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '');
|
for (let charIdx = 0; charIdx < codePart.length; charIdx++) {
|
||||||
const commentIdx = cleanLine.indexOf('//');
|
const char = codePart[charIdx];
|
||||||
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); }
|
if (char === '(') { ctx.parenStack.push(lineIdx); }
|
||||||
else if (char === ')') {
|
else if (char === ')') {
|
||||||
if (ctx.parenStack.length > 0) { ctx.parenStack.pop(); }
|
if (ctx.parenStack.length > 0) { ctx.parenStack.pop(); }
|
||||||
|
|
@ -44,13 +76,11 @@ export class SyntaxRule implements LinterRule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkAssignments(rawLine: string, lineIdx: number, ctx: LinterContext) {
|
private checkAssignments(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
||||||
const lineWithoutStrings = rawLine.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, '""');
|
if (/\b(if|elif|where)\b/.test(codePart)) { ctx.inConditionBlock = true; }
|
||||||
|
|
||||||
if (/\b(if|elif|where)\b/.test(lineWithoutStrings)) { ctx.inConditionBlock = true; }
|
if (codePart.includes('=') && !codePart.includes(':=') && !codePart.includes('>=') && !codePart.includes('<=')) {
|
||||||
|
if (!ctx.inConditionBlock && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && !/\b(matches)\b/.test(codePart)) {
|
||||||
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('=');
|
const idx = rawLine.indexOf('=');
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
ctx.addDiag(lineIdx, idx, idx + 1, "Falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt.", vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(lineIdx, idx, idx + 1, "Falsche Zuweisung. In TPL wird ':=' für Zuweisungen genutzt.", vscode.DiagnosticSeverity.Error);
|
||||||
|
|
@ -58,21 +88,26 @@ export class SyntaxRule implements LinterRule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\b(then|do)\b/.test(lineWithoutStrings) || lineWithoutStrings.endsWith(';')) { ctx.inConditionBlock = false; }
|
if (/\b(then|do)\b/.test(codePart) || codePart.endsWith(';')) { ctx.inConditionBlock = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkSemicolons(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
private checkSemicolons(codePart: string, rawLine: string, lineIdx: number, ctx: LinterContext, originalTrimmedLine: string) {
|
||||||
if (ctx.isClosingString) {
|
if (ctx.isClosingString) {
|
||||||
if (ctx.stringNeedsSemicolon && !line.endsWith(';')) {
|
if (ctx.stringNeedsSemicolon && !originalTrimmedLine.endsWith(';')) {
|
||||||
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Zuweisung.", vscode.DiagnosticSeverity.Error);
|
||||||
}
|
}
|
||||||
} else {
|
} 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+|\/\/|\/\*|\*)/;
|
// "then" und "do" dürfen am Anfang einer Zeile stehen, ohne ein Semikolon zu erfordern
|
||||||
const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches)$/i.test(line);
|
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+|then\b|do\b|\/\/|\/\*|\*)/.test(codePart);
|
||||||
|
|
||||||
if (!noSemicolonNeeded.test(line) && !ctx.justOpenedString && ctx.parenStack.length === 0 && ctx.bracketStack.length === 0 && ctx.inBlock !== 'enumeration' && !ctx.inConditionBlock && !continuesOnNextLine) {
|
// "in" (für for-Schleifen) als gültigen Zeilenumbruch hinzugefügt
|
||||||
const lineToCheck = line.split('//')[0].trim();
|
const continuesOnNextLine = /(,|and|or|not|\+|-|\*|\/|=|:=|matches|in)$/i.test(codePart);
|
||||||
if (!lineToCheck.endsWith(';') && !lineToCheck.endsWith('{') && !lineToCheck.endsWith(',')) {
|
|
||||||
|
// HIER GEFIXT: Wenn die Zeile auf 'then' oder 'do' endet, verlangt sie kein Semikolon!
|
||||||
|
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 (codePart.length > 0 && !codePart.endsWith(';') && !codePart.endsWith('{') && !codePart.endsWith(',')) {
|
||||||
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(lineIdx, Math.max(0, rawLine.length - 1), rawLine.length, "Fehlendes Semikolon am Ende der Anweisung.", vscode.DiagnosticSeverity.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,14 @@ 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
|
// Linter-Events für geöffnete/geänderte Dateien 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
|
// Live-Reload für Einstellungen (Scannt das gesamte Projekt neu)
|
||||||
vscode.workspace.onDidChangeConfiguration(e => {
|
vscode.workspace.onDidChangeConfiguration(e => {
|
||||||
if (e.affectsConfiguration('tpl')) {
|
if (e.affectsConfiguration('tpl')) {
|
||||||
for (const doc of vscode.workspace.textDocuments) {
|
scanWorkspace(diagnosticCollection);
|
||||||
validateTplDocument(doc, diagnosticCollection);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -28,8 +26,25 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
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
|
// NEU: Befehl registrieren, um manuell alle Dateien zu scannen
|
||||||
for (const doc of vscode.workspace.textDocuments) {
|
context.subscriptions.push(vscode.commands.registerCommand('tpl.scanWorkspace', () => {
|
||||||
validateTplDocument(doc, diagnosticCollection);
|
scanWorkspace(diagnosticCollection);
|
||||||
|
vscode.window.showInformationMessage("TPL: Workspace-Scan gestartet.");
|
||||||
|
}));
|
||||||
|
|
||||||
|
// NEU: Bei Aktivierung der Extension direkt einmal alles prüfen!
|
||||||
|
scanWorkspace(diagnosticCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Die neue Engine für das projektweite Lesen
|
||||||
|
async function scanWorkspace(collection: vscode.DiagnosticCollection) {
|
||||||
|
// Sucht alle .tpl Dateien im gesamten geöffneten Projektordner
|
||||||
|
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
// Öffnet die Datei "unsichtbar" im Hintergrund
|
||||||
|
const doc = await vscode.workspace.openTextDocument(file);
|
||||||
|
// Jagt sie durch unseren Linter
|
||||||
|
validateTplDocument(doc, collection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,7 +8,6 @@ export class TplCompletionProvider implements vscode.CompletionItemProvider {
|
||||||
private strategies: CompletionStrategy[];
|
private strategies: CompletionStrategy[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Hier registrieren wir alle Completion Engines
|
|
||||||
this.strategies = [
|
this.strategies = [
|
||||||
new BuiltInCompletion(),
|
new BuiltInCompletion(),
|
||||||
new SnippetCompletion(),
|
new SnippetCompletion(),
|
||||||
|
|
@ -16,20 +15,18 @@ export class TplCompletionProvider implements vscode.CompletionItemProvider {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public provideCompletionItems(
|
public async provideCompletionItems(
|
||||||
document: vscode.TextDocument,
|
document: vscode.TextDocument,
|
||||||
position: vscode.Position,
|
position: vscode.Position,
|
||||||
token: vscode.CancellationToken,
|
token: vscode.CancellationToken,
|
||||||
context: vscode.CompletionContext
|
context: vscode.CompletionContext
|
||||||
): vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList> {
|
): Promise<vscode.CompletionItem[]> {
|
||||||
|
|
||||||
// Den Text in der aktuellen Zeile bis zum Cursor holen
|
|
||||||
const linePrefix = document.lineAt(position).text.substring(0, position.character);
|
const linePrefix = document.lineAt(position).text.substring(0, position.character);
|
||||||
const results: vscode.CompletionItem[] = [];
|
const results: vscode.CompletionItem[] = [];
|
||||||
|
|
||||||
// Jede Engine um Vorschläge bitten
|
|
||||||
for (const strategy of this.strategies) {
|
for (const strategy of this.strategies) {
|
||||||
const items = strategy.provideCompletions(document, position, linePrefix);
|
const items = await strategy.provideCompletions(document, position, linePrefix);
|
||||||
if (items && items.length > 0) {
|
if (items && items.length > 0) {
|
||||||
results.push(...items);
|
results.push(...items);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,76 @@
|
||||||
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(
|
public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | null> {
|
||||||
document: vscode.TextDocument,
|
|
||||||
position: vscode.Position,
|
|
||||||
token: vscode.CancellationToken
|
|
||||||
): Promise<vscode.Hover | null> {
|
|
||||||
|
|
||||||
// Greift das Wort unter dem Mauszeiger ab, inklusive Punkte für Namespaces
|
|
||||||
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
|
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
|
||||||
if (!range) {
|
if (!range) return null;
|
||||||
return null;
|
|
||||||
}
|
const word = document.getText(range); // z.B. "modelling.testDefine"
|
||||||
|
const text = document.getText();
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
const word = document.getText(range);
|
|
||||||
const parts = word.split('.');
|
const parts = word.split('.');
|
||||||
|
const importBase = parts[0];
|
||||||
|
const funcName = parts.length > 1 ? parts[1] : word;
|
||||||
|
|
||||||
let namespace = '';
|
// 1. Lokale Defines
|
||||||
let methodOrPatternName = '';
|
if (parts.length === 1 || parts.length === 2) {
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const defMatch = line.match(new RegExp(`^\\s*define\\s+${funcName}\\b(?:\\s*\\((.*?)\\))?`));
|
||||||
|
if (defMatch) {
|
||||||
|
const params = defMatch[1] ? defMatch[1].trim() : '';
|
||||||
|
const md = new vscode.MarkdownString();
|
||||||
|
md.appendCodeblock(`define ${funcName}(${params})`, 'tpl');
|
||||||
|
|
||||||
if (parts.length === 2) {
|
const textAfterDefine = text.substring(text.indexOf(line) + line.length);
|
||||||
namespace = parts[0];
|
const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
|
||||||
methodOrPatternName = parts[1];
|
if (docMatch) { md.appendMarkdown(`\n\n---\n\n${docMatch[1].trim()}`); }
|
||||||
} else if (parts.length === 1) {
|
|
||||||
methodOrPatternName = parts[0];
|
return new vscode.Hover(md);
|
||||||
} else {
|
}
|
||||||
return null;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BMC Namespace-Methoden (z.B. discovery.listDirectory)
|
// 2. Cross-File Hover für from ... import
|
||||||
if (namespace && tplBuiltIns[namespace] && tplBuiltIns[namespace][methodOrPatternName]) {
|
const fromImportRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${importBase}\\b`);
|
||||||
const def = tplBuiltIns[namespace][methodOrPatternName];
|
const fromMatch = text.match(fromImportRegex);
|
||||||
const markdown = new vscode.MarkdownString();
|
if (fromMatch) {
|
||||||
markdown.appendCodeblock(`${namespace}.${methodOrPatternName}`, 'tpl');
|
const moduleName = fromMatch[1];
|
||||||
markdown.appendMarkdown(`---\n\n${def.description}`);
|
|
||||||
return new vscode.Hover(markdown);
|
// Wenn der User über die Methode fährt (modelling.XYZ)
|
||||||
|
if (funcName !== importBase) {
|
||||||
|
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
|
||||||
|
for (const file of files) {
|
||||||
|
const doc = await vscode.workspace.openTextDocument(file);
|
||||||
|
const docText = doc.getText();
|
||||||
|
if (new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${moduleName.replace(/\./g, '\\.')}\\s*;`).test(docText)) {
|
||||||
|
const docLines = docText.split('\n');
|
||||||
|
for (let i = 0; i < docLines.length; i++) {
|
||||||
|
const line = docLines[i];
|
||||||
|
const defMatch = line.match(new RegExp(`^\\s*define\\s+${funcName}\\b(?:\\s*\\((.*?)\\))?`));
|
||||||
|
if (defMatch) {
|
||||||
|
const params = defMatch[1] ? defMatch[1].trim() : '';
|
||||||
|
const md = new vscode.MarkdownString();
|
||||||
|
md.appendCodeblock(`define ${funcName}(${params})`, 'tpl');
|
||||||
|
|
||||||
|
const textAfterDefine = docText.substring(docText.indexOf(line) + line.length);
|
||||||
|
const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
|
||||||
|
if (docMatch) { md.appendMarkdown(`\n\n---\n\n${docMatch[1].trim()}`); }
|
||||||
|
md.appendMarkdown(`\n\n*(Importiert aus \`${moduleName}\`)*`);
|
||||||
|
return new vscode.Hover(md);
|
||||||
}
|
}
|
||||||
// BMC Globale Funktionen (z.B. size, datatype)
|
|
||||||
else if (!namespace && tplBuiltIns['built_in_globals'] && tplBuiltIns['built_in_globals'][methodOrPatternName]) {
|
|
||||||
const def = tplBuiltIns['built_in_globals'][methodOrPatternName];
|
|
||||||
const markdown = new vscode.MarkdownString();
|
|
||||||
markdown.appendCodeblock(`${methodOrPatternName}`, 'tpl');
|
|
||||||
markdown.appendMarkdown(`---\n\n${def.description}`);
|
|
||||||
return new vscode.Hover(markdown);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Wenn er nur über "modelling" fährt
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,12 @@ export class MetadataFix implements TplQuickFix {
|
||||||
const fix = new vscode.CodeAction("Fehlende Metadaten generieren", vscode.CodeActionKind.QuickFix);
|
const fix = new vscode.CodeAction("Fehlende Metadaten generieren", vscode.CodeActionKind.QuickFix);
|
||||||
fix.edit = new vscode.WorkspaceEdit();
|
fix.edit = new vscode.WorkspaceEdit();
|
||||||
|
|
||||||
// Einstellungen laden
|
|
||||||
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', 'Kuro');
|
||||||
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', 'Grafing');
|
||||||
|
|
||||||
// Herausfinden, welche Felder fehlen (steht im Diagnostic-Message-String)
|
|
||||||
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(', ');
|
||||||
|
|
||||||
|
|
@ -26,8 +24,8 @@ 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`;
|
||||||
|
|
||||||
// Direkt über der Zeile 'end metadata;' einfügen
|
// 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, 0), insertText);
|
fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText);
|
||||||
fix.diagnostics = [diagnostic];
|
fix.diagnostics = [diagnostic];
|
||||||
|
|
||||||
return [fix];
|
return [fix];
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
export interface CompletionStrategy {
|
export interface CompletionStrategy {
|
||||||
/**
|
|
||||||
* @param document Das aktuelle Dokument
|
|
||||||
* @param position Die Cursor-Position
|
|
||||||
* @param linePrefix Der Text der aktuellen Zeile *vor* dem Cursor
|
|
||||||
*/
|
|
||||||
provideCompletions(
|
provideCompletions(
|
||||||
document: vscode.TextDocument,
|
document: vscode.TextDocument,
|
||||||
position: vscode.Position,
|
position: vscode.Position,
|
||||||
linePrefix: string
|
linePrefix: string
|
||||||
): vscode.CompletionItem[] | undefined;
|
): vscode.ProviderResult<vscode.CompletionItem[] | undefined>;
|
||||||
}
|
}
|
||||||
|
|
@ -2,12 +2,103 @@ import * as vscode from 'vscode';
|
||||||
import { CompletionStrategy } from './CompletionStrategy';
|
import { CompletionStrategy } from './CompletionStrategy';
|
||||||
|
|
||||||
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> {
|
||||||
|
const text = document.getText();
|
||||||
|
const items: vscode.CompletionItem[] = [];
|
||||||
|
|
||||||
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
|
// 1. WENN DER USER NACH EINEM PUNKT TIPPT (Cross-File Lookup)
|
||||||
// TODO für Phase 2:
|
const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+)\.$/);
|
||||||
// 1. Regex Suche im Dokument nach ":=" (Variablen tracken)
|
if (dotMatch) {
|
||||||
// 2. Projektweite Suche nach "define" Methoden für Autocomplete
|
const prefix = dotMatch[1]; // z.B. "modelling"
|
||||||
|
if (prefix === 'log' || prefix === 'discovery') return undefined; // Built-ins ignorieren
|
||||||
|
|
||||||
return undefined;
|
// Prüfen, ob der Präfix importiert wurde
|
||||||
|
const importRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${prefix}\\b`);
|
||||||
|
const match = importRegex.exec(text);
|
||||||
|
if (match) {
|
||||||
|
const moduleName = match[1]; // z.B. "Company.software_modelling"
|
||||||
|
const externalItems = await this.resolveImportedMethods(moduleName, prefix);
|
||||||
|
return externalItems; // Nur externe Methoden zurückgeben
|
||||||
|
}
|
||||||
|
return undefined; // Unbekannter Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. LOKALE DEFINITIONS & IMPORTS PARSEN
|
||||||
|
const lines = text.split('\n');
|
||||||
|
let currentDefBlock = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const defBlockMatch = line.match(/^\s*definitions?\s+([a-zA-Z0-9_]+)/);
|
||||||
|
if (defBlockMatch) { currentDefBlock = defBlockMatch[1]; continue; }
|
||||||
|
if (line.match(/^\s*end\s+definitions?/)) { currentDefBlock = ""; continue; }
|
||||||
|
|
||||||
|
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b/);
|
||||||
|
if (defineMatch) {
|
||||||
|
const funcName = defineMatch[1];
|
||||||
|
const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName;
|
||||||
|
const item = new vscode.CompletionItem(fullName, vscode.CompletionItemKind.Function);
|
||||||
|
item.detail = currentDefBlock ? `Local Function in '${currentDefBlock}'` : `Local Function`;
|
||||||
|
item.insertText = new vscode.SnippetString(`${fullName}(\${1:params});$0`);
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imports für die globale Worterkennung bereitstellen
|
||||||
|
const fromImportRegex = /from\s+([a-zA-Z0-9_.]+)\s+import\s+([a-zA-Z0-9_]+)/g;
|
||||||
|
let matchVar;
|
||||||
|
while ((matchVar = fromImportRegex.exec(text)) !== null) {
|
||||||
|
const item = new vscode.CompletionItem(matchVar[2], vscode.CompletionItemKind.Module);
|
||||||
|
item.detail = `Imported Definitions from ${matchVar[1]}`;
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
const varRegex = /([a-zA-Z0-9_]+)\s*:=/g;
|
||||||
|
const foundVars = new Set<string>();
|
||||||
|
while ((matchVar = varRegex.exec(text)) !== null) {
|
||||||
|
if (matchVar[1] !== 'host') { foundVars.add(matchVar[1]); }
|
||||||
|
}
|
||||||
|
for (const v of foundVars) {
|
||||||
|
items.push(new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable));
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hilfsmethode: Sucht im Workspace nach der anderen Datei!
|
||||||
|
private async resolveImportedMethods(moduleName: string, defBlockName: string): Promise<vscode.CompletionItem[]> {
|
||||||
|
const items: vscode.CompletionItem[] = [];
|
||||||
|
// Sucht alle .tpl Dateien im Projekt
|
||||||
|
const files = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**');
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const doc = await vscode.workspace.openTextDocument(file);
|
||||||
|
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*;`);
|
||||||
|
if (moduleRegex.test(text)) {
|
||||||
|
// Modul gefunden! Jetzt die Defines im entsprechenden Block auslesen
|
||||||
|
const lines = text.split('\n');
|
||||||
|
let inTargetBlock = false;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.match(new RegExp(`^\\s*definitions?\\s+${defBlockName}\\b`))) { inTargetBlock = true; continue; }
|
||||||
|
if (inTargetBlock && line.match(/^\s*end\s+definitions?/)) { inTargetBlock = false; break; }
|
||||||
|
|
||||||
|
if (inTargetBlock) {
|
||||||
|
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b/);
|
||||||
|
if (defineMatch) {
|
||||||
|
const funcName = defineMatch[1];
|
||||||
|
const item = new vscode.CompletionItem(funcName, vscode.CompletionItemKind.Function);
|
||||||
|
item.detail = `Method from ${moduleName}`;
|
||||||
|
item.insertText = new vscode.SnippetString(`${funcName}(\${1:params});$0`);
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break; // Datei gefunden und geparst, wir können abbrechen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,95 +4,90 @@ import { CompletionStrategy } from './CompletionStrategy';
|
||||||
export class SnippetCompletion implements CompletionStrategy {
|
export class SnippetCompletion implements CompletionStrategy {
|
||||||
|
|
||||||
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
|
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; }
|
||||||
if (linePrefix.match(/\.[a-zA-Z0-9_]*$/)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items: vscode.CompletionItem[] = [];
|
const items: vscode.CompletionItem[] = [];
|
||||||
const config = vscode.workspace.getConfiguration('tpl');
|
const config = vscode.workspace.getConfiguration('tpl');
|
||||||
|
|
||||||
// ==========================================
|
// Kontext-Scanner: Wo befindet sich der Cursor gerade?
|
||||||
// 1. BASIS-NAMESPACES (discovery, log, etc.)
|
let inBlock: 'none' | 'pattern' | 'definitions' = 'none';
|
||||||
// ==========================================
|
let inPhase = '';
|
||||||
const namespaces = ['discovery', 'log', 'model', 'search', 'text', 'regex', 'time', 'hash', 'number', 'inference'];
|
|
||||||
for (const ns of namespaces) {
|
for (let i = 0; i <= position.line; i++) {
|
||||||
const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module);
|
const text = document.lineAt(i).text.trim();
|
||||||
item.detail = `TPL Namespace: ${ns}`;
|
if (text.match(/^(pattern|syncmapping)\b/)) { inBlock = 'pattern'; inPhase = ''; }
|
||||||
items.push(item);
|
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 = ''; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 2. DAS GOD-SNIPPET (Skeleton)
|
// 1. GLOBAL (Top-Level: Vor dem Pattern / Import-Bereich)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
if (inBlock === 'none') {
|
||||||
const skel = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
|
const skel = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet);
|
||||||
const version = config.get<number>('version', 23);
|
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;`);
|
||||||
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);
|
items.push(skel);
|
||||||
|
|
||||||
// ==========================================
|
['pattern', 'syncmapping', 'definitions'].forEach(block => {
|
||||||
// 3. TPL HAUPT-BLÖCKE (pattern, define, etc.)
|
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};`);
|
||||||
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);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 4. SECTIONS (metadata, body, triggers, etc.)
|
// 2. INNERHALB PATTERN (aber VOR dem Body)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
const sections = ['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body'];
|
if (inBlock === 'pattern' && inPhase === '') {
|
||||||
for (const sec of sections) {
|
['metadata', 'overview', 'constants', 'triggers', 'mapping', 'removal', 'body'].forEach(sec => {
|
||||||
const item = new vscode.CompletionItem(sec, vscode.CompletionItemKind.Snippet);
|
const item = new vscode.CompletionItem(sec, vscode.CompletionItemKind.Snippet);
|
||||||
item.insertText = new vscode.SnippetString(`${sec}\n $0\nend ${sec};`);
|
item.insertText = new vscode.SnippetString(`${sec}\n $0\nend ${sec};`);
|
||||||
item.detail = `${sec} ... end ${sec};`;
|
items.push(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inBlock === 'definitions' && inPhase === '') {
|
||||||
|
const item = new vscode.CompletionItem('define', vscode.CompletionItemKind.Snippet);
|
||||||
|
item.insertText = new vscode.SnippetString(`define \${1:methodName}(\${2:params})\n """\n \${3:DocString}\n """\n $0\nend define;`);
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 5. KONTROLLSTRUKTUREN & TABELLEN
|
// 3. INNERHALB VON BODY ODER DEFINE (Ausführbarer Code)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
if (inPhase === 'body' || inPhase === 'define') {
|
||||||
|
['discovery', 'log', 'model', 'search', 'text', 'regex', 'time', 'hash', 'number', 'inference'].forEach(ns => {
|
||||||
|
const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module);
|
||||||
|
items.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
const ifSnippet = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet);
|
const ifSnippet = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet);
|
||||||
ifSnippet.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;');
|
ifSnippet.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;');
|
||||||
ifSnippet.detail = "if ... then ... end if;";
|
|
||||||
items.push(ifSnippet);
|
items.push(ifSnippet);
|
||||||
|
|
||||||
const forSnippet = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet);
|
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.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);
|
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;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import * as assert from 'assert';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { TabFix } from '../../providers/actions/TabFix';
|
||||||
|
import { LogFix } from '../../providers/actions/LogFix';
|
||||||
|
|
||||||
|
suite('Code Actions Test Suite', () => {
|
||||||
|
test('TabFix: Erzeugt Replace-Edit für Tabulatoren', () => {
|
||||||
|
const fix = new TabFix();
|
||||||
|
const diag = new vscode.Diagnostic(new vscode.Range(0, 0, 0, 1), "Tab found", vscode.DiagnosticSeverity.Warning);
|
||||||
|
|
||||||
|
// Mock für TextDocument
|
||||||
|
const mockDoc = {
|
||||||
|
uri: vscode.Uri.file('test.tpl'),
|
||||||
|
lineAt: () => ({ text: '\t\tcode', range: new vscode.Range(0, 0, 0, 6) })
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const actions = fix.provideFix(mockDoc, diag);
|
||||||
|
assert.strictEqual(actions.length, 1);
|
||||||
|
assert.strictEqual(actions[0].title, "Tabs in 4 Spaces umwandeln");
|
||||||
|
assert.ok(actions[0].edit !== undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('LogFix: Erzeugt Insert-Edit mit Log-Statement', () => {
|
||||||
|
const fix = new LogFix();
|
||||||
|
const diag = new vscode.Diagnostic(new vscode.Range(10, 0, 10, 4), "Missing log");
|
||||||
|
diag.source = 'log.debug("Start pattern Test");'; // Linter-Source Simulation
|
||||||
|
|
||||||
|
const mockDoc = { uri: vscode.Uri.file('test.tpl') } as any;
|
||||||
|
|
||||||
|
const actions = fix.provideFix(mockDoc, diag);
|
||||||
|
assert.strictEqual(actions.length, 1);
|
||||||
|
assert.strictEqual(actions[0].title, "Fehlendes Log-Statement einfügen");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import * as assert from 'assert';
|
||||||
|
import { BuiltInCompletion } from '../../providers/completions/BuiltInCompletion';
|
||||||
|
import { SnippetCompletion } from '../../providers/completions/SnippetCompletion';
|
||||||
|
|
||||||
|
suite('Completion Provider Test Suite', () => {
|
||||||
|
test('BuiltInCompletion: Reagiert auf "log."', () => {
|
||||||
|
const strategy = new BuiltInCompletion();
|
||||||
|
// Wir faken Document und Position, da wir nur linePrefix brauchen
|
||||||
|
const result = strategy.provideCompletions(null as any, null as any, ' log.');
|
||||||
|
|
||||||
|
assert.ok(result !== undefined);
|
||||||
|
assert.strictEqual(result.length, 4); // debug, info, warn, error
|
||||||
|
assert.strictEqual(result[0].label, 'debug');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('BuiltInCompletion: Ignoriert normalen Text', () => {
|
||||||
|
const strategy = new BuiltInCompletion();
|
||||||
|
const result = strategy.provideCompletions(null as any, null as any, ' my_var');
|
||||||
|
|
||||||
|
assert.strictEqual(result, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SnippetCompletion: Liefert Snippets für normalen Text', () => {
|
||||||
|
const strategy = new SnippetCompletion();
|
||||||
|
const result = strategy.provideCompletions(null as any, null as any, ' i');
|
||||||
|
|
||||||
|
assert.ok(result !== undefined);
|
||||||
|
const hasIf = result.some(item => item.label === 'if');
|
||||||
|
assert.ok(hasIf, "Muss 'if' Snippet vorschlagen");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SnippetCompletion: Blockiert Snippets nach Punkt', () => {
|
||||||
|
const strategy = new SnippetCompletion();
|
||||||
|
const result = strategy.provideCompletions(null as any, null as any, ' discovery.r');
|
||||||
|
|
||||||
|
assert.strictEqual(result, undefined, "Darf keine Block-Snippets in Namespaces anzeigen");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,45 +1,69 @@
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
import { LinterContext } from '../../core/LinterContext';
|
import { LinterContext } from '../../core/LinterContext';
|
||||||
import { SyntaxRule } from '../../core/rules/SyntaxRule';
|
import { SyntaxRule } from '../../core/rules/SyntaxRule';
|
||||||
|
import { BlockRule } from '../../core/rules/BlockRule';
|
||||||
|
|
||||||
suite('Linter SyntaxRule Test Suite', () => {
|
suite('Linter Test Suite', () => {
|
||||||
test('Zuweisung: Warnt bei "=" statt ":="', () => {
|
test('SyntaxRule: Warnt bei "=" Zuweisung außerhalb von Bedingungen', () => {
|
||||||
const rule = new SyntaxRule();
|
const rule = new SyntaxRule();
|
||||||
const ctx = new LinterContext();
|
const ctx = new LinterContext();
|
||||||
|
ctx.activePhaseName = 'body'; // Kontext setzen, damit der strict-mode nicht meckert
|
||||||
|
|
||||||
// Wir simulieren eine Code-Zeile
|
|
||||||
const rawLine = " my_var = 123;";
|
const rawLine = " my_var = 123;";
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
|
// HIER GEFIXT: Wir übergeben ein Array als 5. Argument
|
||||||
|
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
||||||
|
|
||||||
// Erwartung: Ein Fehler wegen falscher Zuweisung
|
const diag = ctx.diagnostics.find(d => d.message.includes("':=' für Zuweisungen"));
|
||||||
assert.strictEqual(ctx.diagnostics.length, 1);
|
assert.ok(diag !== undefined, "Fehler für falsche Zuweisung erwartet");
|
||||||
assert.ok(ctx.diagnostics[0].message.includes("In TPL wird ':=' für Zuweisungen genutzt"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Zuweisung: Akzeptiert "=" als Vergleich in if-Bedingung', () => {
|
test('SyntaxRule: Akzeptiert "=" als Vergleich in Bedingungen', () => {
|
||||||
const rule = new SyntaxRule();
|
const rule = new SyntaxRule();
|
||||||
const ctx = new LinterContext();
|
const ctx = new LinterContext();
|
||||||
|
ctx.activePhaseName = 'body';
|
||||||
|
|
||||||
// Wir simulieren eine if-Zeile (setzt ctx.inConditionBlock intern auf true)
|
|
||||||
const rawLine = " if my_var = 123 then";
|
const rawLine = " if my_var = 123 then";
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
|
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
||||||
|
|
||||||
// Erwartung: Kein Fehler, da das = als Vergleich legitim ist
|
// Darf keinen Fehler werfen, da inConditionBlock automatisch auf true geht
|
||||||
assert.strictEqual(ctx.diagnostics.length, 0);
|
assert.strictEqual(ctx.diagnostics.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Klammern: Findet ungeschlossene runde Klammern', () => {
|
test('SyntaxRule: Findet fehlendes Semikolon', () => {
|
||||||
const rule = new SyntaxRule();
|
const rule = new SyntaxRule();
|
||||||
const ctx = new LinterContext();
|
const ctx = new LinterContext();
|
||||||
|
ctx.activePhaseName = 'body';
|
||||||
|
|
||||||
// Wir öffnen eine Klammer, schließen sie aber nicht
|
const rawLine = " my_var := 123";
|
||||||
const rawLine = " my_func(parameter;";
|
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
||||||
rule.checkLine(rawLine.trim(), rawLine, 0, ctx);
|
|
||||||
|
|
||||||
// checkEnd wird am Ende der Datei aufgerufen
|
const diag = ctx.diagnostics.find(d => d.message.includes("Fehlendes Semikolon"));
|
||||||
rule.checkEnd(['', rawLine], ctx);
|
assert.ok(diag !== undefined, "Fehler für fehlendes Semikolon erwartet");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('BlockRule: Erzwingt Module-Header', () => {
|
||||||
|
const rule = new BlockRule();
|
||||||
|
const ctx = new LinterContext();
|
||||||
|
|
||||||
|
const rawLine = "pattern Test 1.0";
|
||||||
|
rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]);
|
||||||
|
|
||||||
assert.strictEqual(ctx.diagnostics.length, 1);
|
assert.strictEqual(ctx.diagnostics.length, 1);
|
||||||
assert.ok(ctx.diagnostics[0].message.includes("Öffnende Klammer '(' wird nicht geschlossen!"));
|
assert.ok(ctx.diagnostics[0].message.includes("tpl 1.x module"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('BlockRule: Findet fehlendes Pattern-Log im Body', () => {
|
||||||
|
const rule = new BlockRule();
|
||||||
|
const ctx = new LinterContext();
|
||||||
|
ctx.hasModuleDef = true;
|
||||||
|
ctx.inBlock = 'pattern';
|
||||||
|
ctx.currentPatternName = 'TestPattern';
|
||||||
|
|
||||||
|
const lines = ['body', ' x := 1;', 'end body;'];
|
||||||
|
rule.checkLine('body', 'body', 0, ctx, lines);
|
||||||
|
|
||||||
|
const logDiag = ctx.diagnostics.find(d => d.code === 'missing_log_start');
|
||||||
|
assert.ok(logDiag !== undefined, "Erwartet Fehler für fehlendes Log-Statement");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Loading…
Reference in New Issue