Fixes for common mistakes the current add on had
This commit is contained in:
parent
1c160c3e1b
commit
3b597e6df4
11
package.json
11
package.json
|
|
@ -50,7 +50,7 @@
|
||||||
"tpl.metadata.publisher": {
|
"tpl.metadata.publisher": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "IT-Operations",
|
"default": "IT-Operations",
|
||||||
"description": "Standard-Publisher ffor new generated TPL-Modules."
|
"description": "Standard-Publisher for new generated TPL-Modules."
|
||||||
},
|
},
|
||||||
"tpl.metadata.treePathBase": {
|
"tpl.metadata.treePathBase": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|
@ -90,13 +90,18 @@
|
||||||
"tpl.metadata.treePath.staticNodes": {
|
"tpl.metadata.treePath.staticNodes": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" },
|
"items": { "type": "string" },
|
||||||
"default": ["default"],
|
"default": [""],
|
||||||
"description": "Fixed definitions for the tree_path left to right (e.g. ['Company', 'Software']). The rest of the levels are free to choose."
|
"description": "Fixed definitions for the tree_path left to right (e.g. ['Company', 'Software']). The rest of the levels are free to choose."
|
||||||
},
|
},
|
||||||
"tpl.version": {
|
"tpl.version": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"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": "Current TPL-Subversion (e. g. 23 for TPL 1.23). Used to find old Modules and for auto generating the sceleton."
|
||||||
|
},
|
||||||
|
"tpl.rules.requireBodyLog": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Require a log.debug statement at the start of a pattern's body phase."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commands": [
|
"commands": [
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { SyntaxRule } from './rules/SyntaxRule';
|
||||||
import { BlockRule } from './rules/BlockRule';
|
import { BlockRule } from './rules/BlockRule';
|
||||||
import { MetadataRule } from './rules/MetadataRule';
|
import { MetadataRule } from './rules/MetadataRule';
|
||||||
import { IndentationRule } from './rules/IndentationRule';
|
import { IndentationRule } from './rules/IndentationRule';
|
||||||
|
import { ParameterRule } from './rules/ParameterRule';
|
||||||
|
|
||||||
export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
|
export function validateTplDocument(document: vscode.TextDocument, collection: vscode.DiagnosticCollection) {
|
||||||
if (document.languageId !== 'tpl') {return;}
|
if (document.languageId !== 'tpl') {return;}
|
||||||
|
|
@ -13,10 +14,11 @@ export function validateTplDocument(document: vscode.TextDocument, collection: v
|
||||||
const ctx = new LinterContext();
|
const ctx = new LinterContext();
|
||||||
|
|
||||||
const rules = [
|
const rules = [
|
||||||
|
new IndentationRule(),
|
||||||
new SyntaxRule(),
|
new SyntaxRule(),
|
||||||
new MetadataRule(),
|
new MetadataRule(),
|
||||||
new BlockRule(),
|
new BlockRule(),
|
||||||
new IndentationRule()
|
new ParameterRule()
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { tplBuiltIns } from '../data/TplBuiltIns';
|
||||||
|
import { generatedTplBuiltIns } from '../data/GeneratedTplBuiltIns';
|
||||||
|
|
||||||
|
export class TplLookup {
|
||||||
|
// Holt eine spezifische Methode (Case-Insensitive)
|
||||||
|
public static getBuiltInFunction(namespace: string, funcName: string): any | null {
|
||||||
|
const nsLower = namespace.toLowerCase();
|
||||||
|
const fnLower = funcName.toLowerCase();
|
||||||
|
|
||||||
|
// 1. Suche in Core Built-Ins (discovery, model, etc.)
|
||||||
|
const coreKey = Object.keys(tplBuiltIns).find(k => k.toLowerCase() === nsLower);
|
||||||
|
if (coreKey) {
|
||||||
|
const methods = (tplBuiltIns as any)[coreKey];
|
||||||
|
const mKey = Object.keys(methods).find(k => k.toLowerCase() === fnLower);
|
||||||
|
if (mKey) return methods[mKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Suche in Generierten TKU Built-Ins (rdbms_functions, etc.)
|
||||||
|
const tkuKey = Object.keys(generatedTplBuiltIns).find(k => k.toLowerCase() === nsLower);
|
||||||
|
if (tkuKey) {
|
||||||
|
const methods = (generatedTplBuiltIns as any)[tkuKey];
|
||||||
|
const mKey = Object.keys(methods).find(k => k.toLowerCase() === fnLower);
|
||||||
|
if (mKey) return methods[mKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Holt alle Methoden eines Moduls für die Autovervollständigung (Case-Insensitive)
|
||||||
|
public static getAllMethodsForNamespace(namespace: string): Record<string, any> | null {
|
||||||
|
const nsLower = namespace.toLowerCase();
|
||||||
|
|
||||||
|
const coreKey = Object.keys(tplBuiltIns).find(k => k.toLowerCase() === nsLower);
|
||||||
|
if (coreKey) return (tplBuiltIns as any)[coreKey];
|
||||||
|
|
||||||
|
const tkuKey = Object.keys(generatedTplBuiltIns).find(k => k.toLowerCase() === nsLower);
|
||||||
|
if (tkuKey) return (generatedTplBuiltIns as any)[tkuKey];
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,18 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
export interface TplFunctionSignature {
|
export interface TplFunctionSignature {
|
||||||
name: string; // z.B. "modelling.doWork"
|
name: string;
|
||||||
moduleName: string; // z.B. "Company.software_modelling"
|
moduleName: string;
|
||||||
params: string[]; // z.B. ["host", "ip_address"]
|
params: string[];
|
||||||
docString?: string; // Extrahierter Kommentar
|
docString?: string;
|
||||||
|
hasReturnValue?: boolean;
|
||||||
|
lineIdx: number; // NEU: Für 'Go to Definition'
|
||||||
|
fsPath: string; // NEU: Für 'Go to Definition'
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WorkspaceIndex {
|
export class WorkspaceIndex {
|
||||||
// Map: Datei-Pfad -> Map von Funktionen (Funktionsname -> Signatur)
|
|
||||||
private fileIndex = new Map<string, Map<string, TplFunctionSignature>>();
|
private fileIndex = new Map<string, Map<string, TplFunctionSignature>>();
|
||||||
|
|
||||||
// Map: Datei-Pfad -> Modulname
|
|
||||||
private fileModules = new Map<string, string>();
|
private fileModules = new Map<string, string>();
|
||||||
|
|
||||||
// Map: Modulname -> Set von Definitions-Blöcken (für die Autovervollständigung beim Import)
|
|
||||||
private moduleDefinitions = new Map<string, Set<string>>();
|
private moduleDefinitions = new Map<string, Set<string>>();
|
||||||
|
|
||||||
public updateFile(uri: vscode.Uri, text: string) {
|
public updateFile(uri: vscode.Uri, text: string) {
|
||||||
|
|
@ -27,22 +25,19 @@ export class WorkspaceIndex {
|
||||||
const functions = new Map<string, TplFunctionSignature>();
|
const functions = new Map<string, TplFunctionSignature>();
|
||||||
const defBlocks = new Set<string>();
|
const defBlocks = new Set<string>();
|
||||||
|
|
||||||
// 1. Modulnamen parsen
|
|
||||||
const modMatch = text.match(/tpl\s+1\.\d+\s+module\s+([a-zA-Z0-9_.]+)\s*;/);
|
const modMatch = text.match(/tpl\s+1\.\d+\s+module\s+([a-zA-Z0-9_.]+)\s*;/);
|
||||||
if (modMatch) {
|
if (modMatch) {
|
||||||
currentModule = modMatch[1];
|
currentModule = modMatch[1];
|
||||||
}
|
}
|
||||||
this.fileModules.set(fsPath, currentModule);
|
this.fileModules.set(fsPath, currentModule);
|
||||||
|
|
||||||
// 2. Blöcke und Funktionen parsen
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
|
|
||||||
// Startet ein Definitions-Block?
|
|
||||||
const defBlockMatch = line.match(/^\s*definitions?\s+([a-zA-Z0-9_]+)/);
|
const defBlockMatch = line.match(/^\s*definitions?\s+([a-zA-Z0-9_]+)/);
|
||||||
if (defBlockMatch) {
|
if (defBlockMatch) {
|
||||||
currentDefBlock = defBlockMatch[1];
|
currentDefBlock = defBlockMatch[1];
|
||||||
defBlocks.add(currentDefBlock); // Block merken!
|
defBlocks.add(currentDefBlock);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (line.match(/^\s*end\s+definitions?/)) {
|
if (line.match(/^\s*end\s+definitions?/)) {
|
||||||
|
|
@ -50,38 +45,44 @@ export class WorkspaceIndex {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patterns, Syncmappings und Tabellen für den Import freigeben
|
|
||||||
const structMatch = line.match(/^\s*(?:pattern|syncmapping|table)\s+([a-zA-Z0-9_]+)/);
|
const structMatch = line.match(/^\s*(?:pattern|syncmapping|table)\s+([a-zA-Z0-9_]+)/);
|
||||||
if (structMatch) {
|
if (structMatch) {
|
||||||
defBlocks.add(structMatch[1]);
|
defBlocks.add(structMatch[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sucht nach 'define name(param1, param2)'
|
|
||||||
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/);
|
const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/);
|
||||||
if (defineMatch) {
|
if (defineMatch) {
|
||||||
const funcName = defineMatch[1];
|
const funcName = defineMatch[1];
|
||||||
const paramString = defineMatch[2] || "";
|
const paramString = defineMatch[2] || "";
|
||||||
const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName;
|
const fullName = currentDefBlock ? `${currentDefBlock}.${funcName}` : funcName;
|
||||||
|
|
||||||
// Parameter säubern und zählen
|
|
||||||
const params = paramString.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
const params = paramString.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
||||||
|
|
||||||
// DocString extrahieren
|
|
||||||
let docString = undefined;
|
let docString = undefined;
|
||||||
const textAfterDefine = text.substring(text.indexOf(line) + line.length);
|
const textAfterDefine = text.substring(text.indexOf(line) + line.length);
|
||||||
const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
|
const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
|
||||||
if (docMatch) { docString = docMatch[1].trim(); }
|
if (docMatch) { docString = docMatch[1].trim(); }
|
||||||
|
|
||||||
|
let hasReturnValue = false;
|
||||||
|
for (let j = i + 1; j < lines.length; j++) {
|
||||||
|
if (lines[j].match(/^\s*end\s+define/)) break;
|
||||||
|
if (lines[j].match(/^\s*return\b/)) {
|
||||||
|
hasReturnValue = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
functions.set(fullName, {
|
functions.set(fullName, {
|
||||||
name: fullName,
|
name: fullName,
|
||||||
moduleName: currentModule,
|
moduleName: currentModule,
|
||||||
params: params,
|
params: params,
|
||||||
docString: docString
|
docString: docString,
|
||||||
|
hasReturnValue: hasReturnValue,
|
||||||
|
lineIdx: i,
|
||||||
|
fsPath: fsPath
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In den RAM schreiben
|
|
||||||
this.fileIndex.set(fsPath, functions);
|
this.fileIndex.set(fsPath, functions);
|
||||||
if (currentModule) {
|
if (currentModule) {
|
||||||
this.moduleDefinitions.set(currentModule, defBlocks);
|
this.moduleDefinitions.set(currentModule, defBlocks);
|
||||||
|
|
@ -97,20 +98,15 @@ export class WorkspaceIndex {
|
||||||
this.fileModules.delete(uri.fsPath);
|
this.fileModules.delete(uri.fsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- HILFSFUNKTIONEN FÜR PROVIDER UND LINTER ---
|
|
||||||
|
|
||||||
// Liefert alle Module (z.B. für "from [Autocomplete] import ...")
|
|
||||||
public getAllModules(): string[] {
|
public getAllModules(): string[] {
|
||||||
return Array.from(this.moduleDefinitions.keys());
|
return Array.from(this.moduleDefinitions.keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Liefert alle Definitions-Blöcke eines Moduls (z.B. für "from XYZ import [Autocomplete]")
|
|
||||||
public getDefinitionBlocksForModule(moduleName: string): string[] {
|
public getDefinitionBlocksForModule(moduleName: string): string[] {
|
||||||
const blocks = this.moduleDefinitions.get(moduleName);
|
const blocks = this.moduleDefinitions.get(moduleName);
|
||||||
return blocks ? Array.from(blocks) : [];
|
return blocks ? Array.from(blocks) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Liefert alle Funktionen eines Moduls (für "modelling.[Autocomplete]")
|
|
||||||
public getFunctionsForModule(moduleName: string): TplFunctionSignature[] {
|
public getFunctionsForModule(moduleName: string): TplFunctionSignature[] {
|
||||||
const result: TplFunctionSignature[] = [];
|
const result: TplFunctionSignature[] = [];
|
||||||
for (const [fsPath, map] of this.fileIndex.entries()) {
|
for (const [fsPath, map] of this.fileIndex.entries()) {
|
||||||
|
|
@ -120,6 +116,18 @@ export class WorkspaceIndex {
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFunctionSignatures(funcName: string): TplFunctionSignature[] {
|
||||||
|
const result: TplFunctionSignature[] = [];
|
||||||
|
for (const map of this.fileIndex.values()) {
|
||||||
|
for (const sig of map.values()) {
|
||||||
|
if (sig.name === funcName || sig.name.endsWith(`.${funcName}`)) {
|
||||||
|
result.push(sig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const workspaceIndex = new WorkspaceIndex();
|
export const workspaceIndex = new WorkspaceIndex();
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,17 @@ export class BlockRule implements LinterRule {
|
||||||
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
||||||
if (!ctx.hasModuleDef) {
|
if (!ctx.hasModuleDef) {
|
||||||
if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; }
|
if (line.startsWith('tpl ')) { ctx.hasModuleDef = true; }
|
||||||
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Every file has to start with 'tpl 1.x module <name>;'", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; }
|
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Every file must start with 'tpl 1.x module <name>;'.", vscode.DiagnosticSeverity.Error); ctx.hasModuleDef = true; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wenn wir im Metadata-Block sind, ignorieren wir den Rest. Die MetadataRule übernimmt!
|
// If we are inside the metadata block, we ignore the rest. MetadataRule handles it!
|
||||||
if (ctx.inModuleMetadata || (line.startsWith('metadata') && ctx.inBlock === 'none')) {
|
if (ctx.inModuleMetadata || (line.startsWith('metadata') && ctx.inBlock === 'none')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) {
|
if (line.match(/^(pattern|syncmapping|table|definitions?|enumeration|configuration)\b/)) {
|
||||||
if (!ctx.hasModuleMetadata && ctx.inBlock === 'none') {
|
if (!ctx.hasModuleMetadata && ctx.inBlock === 'none') {
|
||||||
ctx.addDiag(lineIdx, 0, line.length, "A 'metadata' block on modul-level is needed before any other blocks can be defined.", vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(lineIdx, 0, line.length, "A 'metadata' block at the module level is mandatory before defining other blocks.", vscode.DiagnosticSeverity.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,10 +33,10 @@ export class BlockRule implements LinterRule {
|
||||||
|
|
||||||
public checkEnd(lines: string[], ctx: LinterContext): void {
|
public checkEnd(lines: string[], ctx: LinterContext): void {
|
||||||
for (const unclosedLine of ctx.openDefines) {
|
for (const unclosedLine of ctx.openDefines) {
|
||||||
ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' was not ended with 'end define;'!", vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(unclosedLine, 0, lines[unclosedLine].length, "Block 'define' was not closed with 'end define;'!", vscode.DiagnosticSeverity.Error);
|
||||||
}
|
}
|
||||||
for (const block of ctx.openControlBlocks) {
|
for (const block of ctx.openControlBlocks) {
|
||||||
ctx.addDiag(block.lineIdx, 0, lines[block.lineIdx].length, `Block '${block.type}' was not ended with 'end${block.type};'!`, vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(block.lineIdx, 0, lines[block.lineIdx].length, `Block '${block.type}' was not closed with 'end ${block.type};'!`, vscode.DiagnosticSeverity.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +52,7 @@ export class BlockRule implements LinterRule {
|
||||||
|
|
||||||
private closeControlBlock(expectedType: string, line: string, lineIdx: number, ctx: LinterContext) {
|
private closeControlBlock(expectedType: string, line: string, lineIdx: number, ctx: LinterContext) {
|
||||||
if (ctx.openControlBlocks.length === 0) {
|
if (ctx.openControlBlocks.length === 0) {
|
||||||
ctx.addDiag(lineIdx, 0, line.length, `'end ${expectedType}' found without open block.`, vscode.DiagnosticSeverity.Error);
|
ctx.addDiag(lineIdx, 0, line.length, `Found 'end ${expectedType}' without an open block.`, vscode.DiagnosticSeverity.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,20 +65,20 @@ export class BlockRule implements LinterRule {
|
||||||
|
|
||||||
private handleBlockOpenings(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
private handleBlockOpenings(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
||||||
if (line.startsWith('definitions ') || line.startsWith('definition ')) {
|
if (line.startsWith('definitions ') || line.startsWith('definition ')) {
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A defintions block has to be declared on the highest level.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A 'definitions' block must be declared at the top level.", vscode.DiagnosticSeverity.Error); }
|
||||||
ctx.inBlock = 'definitions';
|
ctx.inBlock = 'definitions';
|
||||||
}
|
}
|
||||||
else if (line.startsWith('define ')) { ctx.openDefines.push(lineIdx); }
|
else if (line.startsWith('define ')) { ctx.openDefines.push(lineIdx); }
|
||||||
else if (line.startsWith('enumeration ')) {
|
else if (line.startsWith('enumeration ')) {
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A enumeration has to be declared on the highest level.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "An 'enumeration' must be declared at the top level.", vscode.DiagnosticSeverity.Error); }
|
||||||
ctx.inBlock = 'enumeration';
|
ctx.inBlock = 'enumeration';
|
||||||
}
|
}
|
||||||
else if (line.startsWith('configuration ')) {
|
else if (line.startsWith('configuration ')) {
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A configuration has to be declared on the highest level.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, line.length, "A 'configuration' must be declared at the top level.", vscode.DiagnosticSeverity.Error); }
|
||||||
ctx.inBlock = 'configuration';
|
ctx.inBlock = 'configuration';
|
||||||
}
|
}
|
||||||
else if (line.startsWith('pattern ') || line.startsWith('syncmapping ')) {
|
else if (line.startsWith('pattern ') || line.startsWith('syncmapping ')) {
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 7, "A Pattern/Syncmapping can not be declared inside another block.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 7, "A pattern or syncmapping cannot be declared inside another block.", vscode.DiagnosticSeverity.Error); }
|
||||||
ctx.inBlock = 'pattern';
|
ctx.inBlock = 'pattern';
|
||||||
ctx.currentPatternPhase = PatternPhase.None;
|
ctx.currentPatternPhase = PatternPhase.None;
|
||||||
ctx.activePhaseName = "";
|
ctx.activePhaseName = "";
|
||||||
|
|
@ -86,7 +86,7 @@ export class BlockRule implements LinterRule {
|
||||||
|
|
||||||
const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/);
|
const nameMatch = line.match(/(pattern|syncmapping)\s+([a-zA-Z0-9_]+)\s+\d+\.\d+/);
|
||||||
if (nameMatch) { ctx.currentPatternName = nameMatch[2]; }
|
if (nameMatch) { ctx.currentPatternName = nameMatch[2]; }
|
||||||
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Invalid Pattern declaration. Expected: 'pattern <Name> <Version>'", vscode.DiagnosticSeverity.Error); }
|
else { ctx.addDiag(lineIdx, 0, rawLine.length, "Invalid pattern declaration. Expected: 'pattern <Name> <Version>'", vscode.DiagnosticSeverity.Error); }
|
||||||
}
|
}
|
||||||
else if (line.startsWith('table ')) {
|
else if (line.startsWith('table ')) {
|
||||||
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 5, "A table cannot be declared inside another block.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'none') { ctx.addDiag(lineIdx, 0, 5, "A table cannot be declared inside another block.", vscode.DiagnosticSeverity.Error); }
|
||||||
|
|
@ -97,8 +97,8 @@ export class BlockRule implements LinterRule {
|
||||||
private checkPatternPhases(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]) {
|
private checkPatternPhases(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]) {
|
||||||
const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => {
|
const checkPhase = (phaseName: string, phaseEnum: PatternPhase) => {
|
||||||
if (line.startsWith(phaseName)) {
|
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.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' was not closed with 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
||||||
if (ctx.currentPatternPhase >= phaseEnum) { ctx.addDiag(lineIdx, 0, phaseName.length, `'${phaseName}' Block ist hier nicht erlaubt.`, vscode.DiagnosticSeverity.Error); }
|
if (ctx.currentPatternPhase >= phaseEnum) { ctx.addDiag(lineIdx, 0, phaseName.length, `Block '${phaseName}' is not allowed here.`, vscode.DiagnosticSeverity.Error); }
|
||||||
|
|
||||||
ctx.currentPatternPhase = phaseEnum;
|
ctx.currentPatternPhase = phaseEnum;
|
||||||
ctx.activePhaseName = phaseName;
|
ctx.activePhaseName = phaseName;
|
||||||
|
|
@ -114,10 +114,14 @@ export class BlockRule implements LinterRule {
|
||||||
checkPhase('removal', PatternPhase.Removal);
|
checkPhase('removal', PatternPhase.Removal);
|
||||||
|
|
||||||
if (line.startsWith('body')) {
|
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); }
|
if (ctx.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' was not closed with 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
||||||
ctx.activePhaseName = "body";
|
ctx.activePhaseName = "body";
|
||||||
ctx.activePhaseLine = lineIdx;
|
ctx.activePhaseLine = lineIdx;
|
||||||
|
|
||||||
|
const config = vscode.workspace.getConfiguration('tpl');
|
||||||
|
const requireLog = config.get<boolean>('rules.requireBodyLog', true);
|
||||||
|
|
||||||
|
if (requireLog) {
|
||||||
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();
|
||||||
|
|
@ -127,17 +131,18 @@ export class BlockRule implements LinterRule {
|
||||||
|
|
||||||
if (!foundLog) {
|
if (!foundLog) {
|
||||||
const expectedLog = `log.debug("Start pattern ${ctx.currentPatternName}");`;
|
const expectedLog = `log.debug("Start pattern ${ctx.currentPatternName}");`;
|
||||||
const diag = ctx.addDiag(lineIdx, 0, rawLine.length, `Inside the Body the Log-Statement is missing, e.g. Pattern-Namen: ${expectedLog}`, vscode.DiagnosticSeverity.Error);
|
const diag = ctx.addDiag(lineIdx, 0, rawLine.length, `Missing log statement with pattern name in body. E.g.: ${expectedLog}`, vscode.DiagnosticSeverity.Error);
|
||||||
diag.code = 'missing_log_start';
|
diag.code = 'missing_log_start';
|
||||||
diag.source = expectedLog;
|
diag.source = expectedLog;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const closers = ['end metadata', 'end overview', 'end constants', 'end triggers', 'end mapping', 'end removal', 'end body'];
|
const closers = ['end metadata', 'end overview', 'end constants', 'end triggers', 'end mapping', 'end removal', 'end body'];
|
||||||
for (const closer of closers) {
|
for (const closer of closers) {
|
||||||
if (line.startsWith(closer)) {
|
if (line.startsWith(closer)) {
|
||||||
const phase = closer.replace('end ', '').replace(';', '').trim();
|
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); }
|
if (ctx.activePhaseName !== phase) { ctx.addDiag(lineIdx, 0, line.length, `Unexpected '${closer}'. Expected 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
||||||
else { ctx.activePhaseName = ""; }
|
else { ctx.activePhaseName = ""; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -145,20 +150,20 @@ export class BlockRule implements LinterRule {
|
||||||
|
|
||||||
private checkEndBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
private checkEndBlocks(line: string, rawLine: string, lineIdx: number, ctx: LinterContext) {
|
||||||
if (line.startsWith('end pattern') || line.startsWith('end syncmapping')) {
|
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.activePhaseName !== "") { ctx.addDiag(ctx.activePhaseLine, 0, ctx.activePhaseName.length, `Block '${ctx.activePhaseName}' was not closed with 'end ${ctx.activePhaseName};'.`, vscode.DiagnosticSeverity.Error); }
|
||||||
if (ctx.inBlock !== 'pattern') { ctx.addDiag(lineIdx, 0, 11, "'end pattern' found without open pattern block.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'pattern') { ctx.addDiag(lineIdx, 0, 11, "Found 'end pattern' without an open pattern block.", vscode.DiagnosticSeverity.Error); }
|
||||||
ctx.inBlock = 'none';
|
ctx.inBlock = 'none';
|
||||||
}
|
}
|
||||||
else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) {
|
else if (line.startsWith('end table') || line.startsWith('end enumeration') || line.startsWith('end configuration')) {
|
||||||
ctx.inBlock = 'none';
|
ctx.inBlock = 'none';
|
||||||
}
|
}
|
||||||
else if (line.startsWith('end definitions') || line.startsWith('end definition')) {
|
else if (line.startsWith('end definitions') || line.startsWith('end definition')) {
|
||||||
if (ctx.inBlock !== 'definitions') { ctx.addDiag(lineIdx, 0, line.length, "'end definitions' found without open definitions block.", vscode.DiagnosticSeverity.Error); }
|
if (ctx.inBlock !== 'definitions') { ctx.addDiag(lineIdx, 0, line.length, "Found 'end definitions' without an open definitions block.", vscode.DiagnosticSeverity.Error); }
|
||||||
ctx.inBlock = 'none';
|
ctx.inBlock = 'none';
|
||||||
}
|
}
|
||||||
else if (line.startsWith('end define')) {
|
else if (line.startsWith('end define')) {
|
||||||
if (ctx.openDefines.length > 0) { ctx.openDefines.pop(); }
|
if (ctx.openDefines.length > 0) { ctx.openDefines.pop(); }
|
||||||
else { ctx.addDiag(lineIdx, 0, line.length, "'end define' found without open define.", vscode.DiagnosticSeverity.Error); }
|
else { ctx.addDiag(lineIdx, 0, line.length, "Found 'end define' without an open 'define'.", vscode.DiagnosticSeverity.Error); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,9 +8,9 @@ export class IndentationRule implements LinterRule {
|
||||||
if (rawLine.trim().length === 0) {return;}
|
if (rawLine.trim().length === 0) {return;}
|
||||||
if (rawLine.includes('\t')) {return;}
|
if (rawLine.includes('\t')) {return;}
|
||||||
|
|
||||||
// HIER IST DER FIX: Wenn wir innerhalb von Klammern sind (Visual Alignment),
|
// Sobald wir in Klammern oder einem Multi-Line Import sind (Visual Alignment),
|
||||||
// greift die 4-Leerzeichen-Regel nicht!
|
// greift die 4-Leerzeichen-Regel nicht!
|
||||||
if (ctx.parenStack.length > 0 || ctx.bracketStack.length > 0) {
|
if (ctx.parenStack.length > 0 || ctx.bracketStack.length > 0 || ctx.inImportBlock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { LinterRule } from './LinterRule';
|
||||||
|
import { LinterContext } from '../LinterContext';
|
||||||
|
import { workspaceIndex, TplFunctionSignature } from '../WorkspaceIndex';
|
||||||
|
import { TplLookup } from '../TplLookup'; // Nutzt ab sofort das zentrale Lexikon
|
||||||
|
|
||||||
|
export class ParameterRule implements LinterRule {
|
||||||
|
public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void {
|
||||||
|
let cleanLine = "";
|
||||||
|
let inString = false;
|
||||||
|
let stringChar = '';
|
||||||
|
let inTripleQuote = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const char = line[i];
|
||||||
|
|
||||||
|
// 1. Single-Line Kommentare (//) ignorieren
|
||||||
|
if (!inString && !inTripleQuote && char === '/' && line[i+1] === '/') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. TPL Triple Quotes (""" oder ''') erkennen
|
||||||
|
if (!inString && !inTripleQuote &&
|
||||||
|
(char === '"' || char === "'") &&
|
||||||
|
line[i+1] === char &&
|
||||||
|
line[i+2] === char) {
|
||||||
|
|
||||||
|
inTripleQuote = true;
|
||||||
|
stringChar = char;
|
||||||
|
cleanLine += char + char + char; // Die """ behalten, damit wir die Zeile nicht komplett leeren
|
||||||
|
i += 2; // Überspringe die nächsten zwei Zeichen
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Triple Quotes beenden
|
||||||
|
if (inTripleQuote) {
|
||||||
|
if (char === stringChar && line[i+1] === stringChar && line[i+2] === stringChar) {
|
||||||
|
inTripleQuote = false;
|
||||||
|
cleanLine += char + char + char;
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
continue; // Alles innerhalb von """ ignorieren!
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Normale Strings beenden
|
||||||
|
if (inString) {
|
||||||
|
if (char === '\\') {
|
||||||
|
i++; // Escape-Sequenzen ignorieren
|
||||||
|
} else if (char === stringChar) {
|
||||||
|
inString = false;
|
||||||
|
cleanLine += char;
|
||||||
|
}
|
||||||
|
continue; // Alles innerhalb von " ignorieren!
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Normale Strings starten
|
||||||
|
if (!inString && !inTripleQuote && (char === '"' || char === "'")) {
|
||||||
|
inString = true;
|
||||||
|
stringChar = char;
|
||||||
|
cleanLine += char;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wenn wir hier ankommen, sind wir im echten, ausführbaren TPL-Code
|
||||||
|
cleanLine += char;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanLine.trim().length === 0) return;
|
||||||
|
|
||||||
|
// Die Definition der Funktion selbst ist kein Aufruf und wird ignoriert.
|
||||||
|
if (/^\s*define\s+/.test(cleanLine)) return;
|
||||||
|
|
||||||
|
const callRegex = /\b([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)?)\s*\(/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = callRegex.exec(cleanLine)) !== null) {
|
||||||
|
const funcName = match[1];
|
||||||
|
const callStart = match.index;
|
||||||
|
const parenStart = callStart + funcName.length;
|
||||||
|
|
||||||
|
if (/^(if|elif|where|for|while|in|matches|has|subword|defined)\b/.test(funcName)) continue;
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
let parenEnd = -1;
|
||||||
|
for (let i = parenStart; i < cleanLine.length; i++) {
|
||||||
|
if (cleanLine[i] === '(') depth++;
|
||||||
|
else if (cleanLine[i] === ')') {
|
||||||
|
depth--;
|
||||||
|
if (depth === 0) {
|
||||||
|
parenEnd = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parenEnd !== -1) {
|
||||||
|
const paramsStr = cleanLine.substring(parenStart + 1, parenEnd).trim();
|
||||||
|
let paramCount = 0;
|
||||||
|
|
||||||
|
if (paramsStr.length > 0) {
|
||||||
|
let paramDepth = 0;
|
||||||
|
let commas = 0;
|
||||||
|
for (let i = 0; i < paramsStr.length; i++) {
|
||||||
|
if (paramsStr[i] === '(' || paramsStr[i] === '[') paramDepth++;
|
||||||
|
else if (paramsStr[i] === ')' || paramsStr[i] === ']') paramDepth--;
|
||||||
|
else if (paramsStr[i] === ',' && paramDepth === 0) commas++;
|
||||||
|
}
|
||||||
|
paramCount = commas + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.validateFunctionParams(funcName, paramCount, lineIdx, rawLine, ctx);
|
||||||
|
this.validateReturnValue(funcName, cleanLine, lineIdx, rawLine, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public checkEnd(lines: string[], ctx: LinterContext): void {
|
||||||
|
// empty
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateFunctionParams(funcName: string, passedCount: number, lineIdx: number, rawLine: string, ctx: LinterContext) {
|
||||||
|
const parts = funcName.split('.');
|
||||||
|
|
||||||
|
if (parts.length > 1) {
|
||||||
|
// Case-Insensitive Prüfung über den neuen Lookup!
|
||||||
|
if (TplLookup.getBuiltInFunction(parts[0], parts[1])) {
|
||||||
|
return; // Funktion existiert, Linter ist zufrieden.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const signatures = workspaceIndex.getFunctionSignatures(funcName);
|
||||||
|
if (signatures.length > 0) {
|
||||||
|
const matches = signatures.some((sig: TplFunctionSignature) => sig.params.length === passedCount);
|
||||||
|
if (!matches) {
|
||||||
|
const expected = signatures[0].params.length;
|
||||||
|
const idx = rawLine.indexOf(funcName);
|
||||||
|
ctx.addDiag(lineIdx, Math.max(0, idx), Math.max(0, idx) + funcName.length, `Function '${funcName}' expects ${expected} parameter(s), but ${passedCount} were provided.`, vscode.DiagnosticSeverity.Warning);
|
||||||
|
}
|
||||||
|
} else if (parts.length > 1) {
|
||||||
|
const idx = rawLine.indexOf(funcName);
|
||||||
|
ctx.addDiag(lineIdx, Math.max(0, idx), Math.max(0, idx) + funcName.length, `Unknown function '${funcName}'. Please check your imports or contact the plugin provider.`, vscode.DiagnosticSeverity.Information);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateReturnValue(funcName: string, cleanLine: string, lineIdx: number, rawLine: string, ctx: LinterContext) {
|
||||||
|
const signatures = workspaceIndex.getFunctionSignatures(funcName);
|
||||||
|
if (signatures.length > 0 && signatures[0].hasReturnValue) {
|
||||||
|
const isAssigned = cleanLine.includes(':=');
|
||||||
|
const isUsedInCondition = /\b(if|elif|while)\b/.test(cleanLine);
|
||||||
|
|
||||||
|
if (!isAssigned && !isUsedInCondition) {
|
||||||
|
const idx = rawLine.indexOf(funcName);
|
||||||
|
ctx.addDiag(lineIdx, Math.max(0, idx), Math.max(0, idx) + funcName.length, `The return value of '${funcName}' is not used. Did you forget an assignment (:=)?`, vscode.DiagnosticSeverity.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -4,7 +4,8 @@ import { TplCodeActionProvider } from './providers/TplCodeActionProvider';
|
||||||
import { TplDocumentFormatter } from './providers/TplDocumentFormatter';
|
import { TplDocumentFormatter } from './providers/TplDocumentFormatter';
|
||||||
import { TplCompletionProvider } from './providers/TplCompletionProvider';
|
import { TplCompletionProvider } from './providers/TplCompletionProvider';
|
||||||
import { TplHoverProvider } from './providers/TplHoverProvider';
|
import { TplHoverProvider } from './providers/TplHoverProvider';
|
||||||
import { workspaceIndex } from './core/WorkspaceIndex'; // <-- Index importiert
|
import { workspaceIndex } from './core/WorkspaceIndex';
|
||||||
|
import { TplDefinitionProvider } from './providers/TplDefinitionProvider';
|
||||||
|
|
||||||
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');
|
||||||
|
|
@ -43,6 +44,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
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()));
|
||||||
|
context.subscriptions.push(vscode.languages.registerDefinitionProvider('tpl', new TplDefinitionProvider()));
|
||||||
|
|
||||||
context.subscriptions.push(vscode.commands.registerCommand('tpl.scanWorkspace', () => {
|
context.subscriptions.push(vscode.commands.registerCommand('tpl.scanWorkspace', () => {
|
||||||
scanWorkspace(diagnosticCollection);
|
scanWorkspace(diagnosticCollection);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { workspaceIndex } from '../core/WorkspaceIndex';
|
||||||
|
|
||||||
|
export class TplDefinitionProvider implements vscode.DefinitionProvider {
|
||||||
|
public async provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | null> {
|
||||||
|
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
|
||||||
|
if (!range) return null;
|
||||||
|
|
||||||
|
const word = document.getText(range);
|
||||||
|
const parts = word.split('.');
|
||||||
|
const funcName = parts.length > 1 ? parts[1] : word;
|
||||||
|
|
||||||
|
const text = document.getText();
|
||||||
|
|
||||||
|
// 1. LOKALE SUCHE: Ist die Funktion in der gleichen Datei?
|
||||||
|
const lines = text.split('\n');
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const match = lines[i].match(new RegExp(`^\\s*define\\s+${funcName}\\b`));
|
||||||
|
if (match) {
|
||||||
|
// Wir haben sie lokal gefunden! Springe exakt dorthin.
|
||||||
|
return new vscode.Location(document.uri, new vscode.Position(i, lines[i].indexOf(funcName)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. GLOBALE SUCHE: Den RAM-Index nach anderen Modulen fragen
|
||||||
|
const signatures = workspaceIndex.getFunctionSignatures(funcName);
|
||||||
|
|
||||||
|
if (parts.length > 1) {
|
||||||
|
const prefix = parts[0];
|
||||||
|
// Wir schauen, ob es über from X import <prefix> importiert wurde
|
||||||
|
const importRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+[\\s\\S]*?\\b${prefix}\\b`);
|
||||||
|
const match = importRegex.exec(text);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const moduleName = match[1];
|
||||||
|
const targetFunc = signatures.find(sig => sig.moduleName === moduleName && sig.name.endsWith(`.${funcName}`));
|
||||||
|
if (targetFunc && targetFunc.fsPath) {
|
||||||
|
const uri = vscode.Uri.file(targetFunc.fsPath);
|
||||||
|
return new vscode.Location(uri, new vscode.Position(targetFunc.lineIdx, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. FALLBACK: Wenn es nicht als 'prefix.funktion' stand, aber eindeutig im Workspace ist
|
||||||
|
if (signatures.length > 0 && signatures[0].fsPath) {
|
||||||
|
const uri = vscode.Uri.file(signatures[0].fsPath);
|
||||||
|
return new vscode.Location(uri, new vscode.Position(signatures[0].lineIdx, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wenn es eine Built-in Methode ist (wie discovery.runCommand), springt er nirgends hin
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,94 +1,38 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { tplBuiltIns } from '../data/TplBuiltIns';
|
import { TplLookup } from '../core/TplLookup';
|
||||||
import { generatedTplBuiltIns } from '../data/GeneratedTplBuiltIns'; // Pfad ggf. anpassen
|
import { workspaceIndex } from '../core/WorkspaceIndex';
|
||||||
|
|
||||||
// Beide Datenquellen zusammenführen
|
|
||||||
const allTplFunctions: Record<string, any> = { ...tplBuiltIns, ...generatedTplBuiltIns };
|
|
||||||
|
|
||||||
export class TplHoverProvider implements vscode.HoverProvider {
|
export class TplHoverProvider implements vscode.HoverProvider {
|
||||||
public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Hover | null> {
|
public provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.ProviderResult<vscode.Hover> {
|
||||||
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
|
const range = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/);
|
||||||
if (!range) {return null;}
|
if (!range) return null;
|
||||||
|
|
||||||
const word = document.getText(range);
|
const word = document.getText(range);
|
||||||
const text = document.getText();
|
|
||||||
const lines = text.split('\n');
|
|
||||||
|
|
||||||
const parts = word.split('.');
|
const parts = word.split('.');
|
||||||
const importBase = parts[0];
|
|
||||||
|
if (parts.length > 1) {
|
||||||
|
// 1. Suche in Core & TKU Built-Ins (Case-Insensitive)
|
||||||
|
const funcData = TplLookup.getBuiltInFunction(parts[0], parts[1]);
|
||||||
|
if (funcData) {
|
||||||
|
const md = new vscode.MarkdownString();
|
||||||
|
md.appendCodeblock(`${parts[0]}.${parts[1]}(${funcData.snippets?.[0] || ''})`, 'tpl');
|
||||||
|
md.appendMarkdown(`---\n${funcData.description}`);
|
||||||
|
return new vscode.Hover(md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Suche in lokalem Workspace (RAM Index)
|
||||||
const funcName = parts.length > 1 ? parts[1] : word;
|
const funcName = parts.length > 1 ? parts[1] : word;
|
||||||
|
const signatures = workspaceIndex.getFunctionSignatures(funcName);
|
||||||
// 1. BMC BUILT-INS & TKU DEFINES DYNAMISCH AUSLESEN
|
if (signatures.length > 0) {
|
||||||
const bmcNamespaces = Object.keys(allTplFunctions);
|
const sig = signatures[0];
|
||||||
|
|
||||||
if (bmcNamespaces.includes(importBase)) {
|
|
||||||
// Wenn die Methode in der Datei existiert, zeige die echte Doku!
|
|
||||||
if (funcName !== importBase && allTplFunctions[importBase][funcName]) {
|
|
||||||
const bmcDoc = allTplFunctions[importBase][funcName];
|
|
||||||
const md = new vscode.MarkdownString();
|
const md = new vscode.MarkdownString();
|
||||||
|
md.appendCodeblock(`define ${sig.name}(${sig.params.join(', ')})`, 'tpl');
|
||||||
// Wir zeigen den echten Methodennamen als Code-Block
|
if (sig.docString) {
|
||||||
md.appendCodeblock(`${importBase}.${funcName}`, 'tpl');
|
md.appendMarkdown(`---\n${sig.docString}`);
|
||||||
md.appendMarkdown(`\n\n---\n\n${bmcDoc.description}`);
|
}
|
||||||
|
|
||||||
return new vscode.Hover(md);
|
return new vscode.Hover(md);
|
||||||
}
|
}
|
||||||
// Fallback, wenn man nur über dem Namespace (z.B. "discovery") hovert
|
|
||||||
return new vscode.Hover(new vscode.MarkdownString(`**${word}**\n\n*(BMC Built-in / TKU Namespace)*`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Lokale Defines
|
|
||||||
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');
|
|
||||||
|
|
||||||
const textAfterDefine = text.substring(text.indexOf(line) + line.length);
|
|
||||||
const docMatch = textAfterDefine.match(/^\s*(?:"""|''')([\s\S]*?)(?:"""|''')/);
|
|
||||||
if (docMatch) { md.appendMarkdown(`\n\n---\n\n${docMatch[1].trim()}`); }
|
|
||||||
|
|
||||||
return new vscode.Hover(md);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Cross-File Hover für from ... import
|
|
||||||
const fromImportRegex = new RegExp(`from\\s+([a-zA-Z0-9_.]+)\\s+import\\s+${importBase}\\b`);
|
|
||||||
const fromMatch = text.match(fromImportRegex);
|
|
||||||
if (fromMatch) {
|
|
||||||
const moduleName = fromMatch[1];
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new vscode.Hover(new vscode.MarkdownString(`Importierte Definitions **${importBase}** aus Modul \`${moduleName}\`.`));
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,57 @@
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { CompletionStrategy } from './CompletionStrategy';
|
import { CompletionStrategy } from './CompletionStrategy';
|
||||||
|
import { TplLookup } from '../../core/TplLookup';
|
||||||
|
|
||||||
export class BuiltInCompletion implements CompletionStrategy {
|
export class BuiltInCompletion implements CompletionStrategy {
|
||||||
|
public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise<vscode.CompletionItem[] | undefined> {
|
||||||
|
const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\.([a-zA-Z0-9_]*)$/);
|
||||||
|
|
||||||
public provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): vscode.CompletionItem[] | undefined {
|
if (dotMatch) {
|
||||||
|
const prefix = dotMatch[1];
|
||||||
const items: vscode.CompletionItem[] = [];
|
const items: vscode.CompletionItem[] = [];
|
||||||
|
let isHandled = false;
|
||||||
|
|
||||||
// Prüft, ob wir im Namespace "log." oder "discovery." sind (auch wenn man danach weiter tippt!)
|
// NEU: Case-Insensitive Lookup (findet Core UND TKU Methoden!)
|
||||||
const match = linePrefix.match(/\b(discovery|log|model|text|regex|time|xpath|json|hash|inference)\.([a-zA-Z0-9_]*)$/);
|
const methods = TplLookup.getAllMethodsForNamespace(prefix);
|
||||||
|
|
||||||
if (match) {
|
if (methods) {
|
||||||
const namespace = match[1];
|
isHandled = true;
|
||||||
|
for (const [funcName, funcData] of Object.entries(methods)) {
|
||||||
|
const item = new vscode.CompletionItem(funcName, vscode.CompletionItemKind.Method);
|
||||||
|
item.detail = `TPL Built-in: ${prefix}.${funcName}`;
|
||||||
|
item.documentation = new vscode.MarkdownString(funcData.description);
|
||||||
|
|
||||||
if (namespace === 'log') {
|
if (funcData.snippets && funcData.snippets.length > 0) {
|
||||||
const levels = ['debug', 'info', 'warn', 'error'];
|
item.insertText = new vscode.SnippetString(`${funcName}(${funcData.snippets[0]});$0`);
|
||||||
for (const level of levels) {
|
} else {
|
||||||
const item = new vscode.CompletionItem(level, vscode.CompletionItemKind.Method);
|
item.insertText = new vscode.SnippetString(`${funcName}();$0`);
|
||||||
item.insertText = new vscode.SnippetString(`${level}("\${1:message}");`);
|
}
|
||||||
item.documentation = new vscode.MarkdownString(`Schreibt eine Log-Meldung auf dem Level **${level}**.`);
|
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (namespace === 'discovery') {
|
|
||||||
const runCmd = new vscode.CompletionItem('runCommand', vscode.CompletionItemKind.Method);
|
// Dynamische Konstruktoren für 'model', 'model.rel' etc. bleiben hier unverändert...
|
||||||
runCmd.insertText = new vscode.SnippetString('runCommand(${1:host}, "${2:command}");');
|
if (prefix.toLowerCase() === 'model') {
|
||||||
runCmd.detail = "discovery.runCommand(host, command)";
|
isHandled = true;
|
||||||
items.push(runCmd);
|
const dynamicItem = new vscode.CompletionItem('<NodeKind>', vscode.CompletionItemKind.Constructor);
|
||||||
|
dynamicItem.detail = "Dynamic Taxonomy Node";
|
||||||
|
dynamicItem.documentation = new vscode.MarkdownString("The `model` scope is automatically populated with a function named after each node kind defined in the taxonomy.");
|
||||||
|
dynamicItem.insertText = new vscode.SnippetString("${1:NodeKind}(key := ${2:key_value});$0");
|
||||||
|
items.push(dynamicItem);
|
||||||
}
|
}
|
||||||
return items; // Treffer gefunden, wir geben sie zurück!
|
else if (prefix.toLowerCase() === 'model.rel' || prefix.toLowerCase() === 'model.uniquerel') {
|
||||||
|
isHandled = true;
|
||||||
|
const dynamicItem = new vscode.CompletionItem('<Relationship>', vscode.CompletionItemKind.Constructor);
|
||||||
|
dynamicItem.detail = prefix.toLowerCase() === 'model.rel' ? "Dynamic Taxonomy Relationship" : "Dynamic Unique Taxonomy Relationship";
|
||||||
|
dynamicItem.documentation = new vscode.MarkdownString(`The \`${prefix}\` scope is automatically populated with a function named after each relationship defined in the taxonomy.`);
|
||||||
|
dynamicItem.insertText = new vscode.SnippetString("${1:RelationshipName}(${2:SourceRole} := ${3:source_node}, ${4:DestinationRole} := ${5:dest_node});$0");
|
||||||
|
items.push(dynamicItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isHandled) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,6 +6,8 @@ export class FormatState {
|
||||||
public parenAlignments: number[] = [];
|
public parenAlignments: number[] = [];
|
||||||
public bracketAlignments: number[] = [];
|
public bracketAlignments: number[] = [];
|
||||||
|
|
||||||
|
public inImportBlock: boolean = false;
|
||||||
|
|
||||||
// Gibt die aktuellen Basis-Leerzeichen zurück (z.B. Level 2 = 8 Leerzeichen)
|
// Gibt die aktuellen Basis-Leerzeichen zurück (z.B. Level 2 = 8 Leerzeichen)
|
||||||
public get currentIndentString(): string {
|
public get currentIndentString(): string {
|
||||||
return ' '.repeat(this.indentLevel * this.tabSize);
|
return ' '.repeat(this.indentLevel * this.tabSize);
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,88 @@
|
||||||
import { FormatState } from './FormatState';
|
import { FormatState } from './FormatState';
|
||||||
|
|
||||||
export class IndentationRules {
|
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 {
|
public apply(line: string, state: FormatState): string {
|
||||||
// 1. Wenn die Zeile einen Block schließt, VORHER die Einrückung verringern
|
// 1. Leere Zeilen komplett rasieren
|
||||||
if (this.blockCloses.test(line) && state.indentLevel > 0) {
|
if (line.trim().length === 0) return "";
|
||||||
state.indentLevel--;
|
|
||||||
|
// 2. Strings und Kommentare sicher entfernen (für die Keyword-Erkennung)
|
||||||
|
let cleanLine = "";
|
||||||
|
let inString = false;
|
||||||
|
let stringChar = '';
|
||||||
|
let inTripleQuote = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const char = line[i];
|
||||||
|
if (!inString && !inTripleQuote && char === '/' && line[i+1] === '/') break;
|
||||||
|
|
||||||
|
if (!inString && !inTripleQuote && (char === '"' || char === "'") && line[i+1] === char && line[i+2] === char) {
|
||||||
|
inTripleQuote = true; stringChar = char; cleanLine += char+char+char; i += 2; continue;
|
||||||
|
}
|
||||||
|
if (inTripleQuote) {
|
||||||
|
if (char === stringChar && line[i+1] === stringChar && line[i+2] === stringChar) {
|
||||||
|
inTripleQuote = false; cleanLine += char+char+char; i += 2;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inString) {
|
||||||
|
if (char === '\\') i++; else if (char === stringChar) inString = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inString && !inTripleQuote && (char === '"' || char === "'")) {
|
||||||
|
inString = true; stringChar = char; cleanLine += char; continue;
|
||||||
|
}
|
||||||
|
cleanLine += char;
|
||||||
|
}
|
||||||
|
cleanLine = cleanLine.trim();
|
||||||
|
|
||||||
|
// Alle vorhandenen falschen Leerzeichen am Anfang (2, 3, 6, etc.) brutal abschneiden
|
||||||
|
const trimmedLine = line.trimStart();
|
||||||
|
|
||||||
|
// 3. DEDENT: Aktuelle Zeile rückt nach links (end, elif, else)
|
||||||
|
if (/^end\b/.test(cleanLine)) {
|
||||||
|
state.indentLevel = Math.max(0, state.indentLevel - 1);
|
||||||
|
} else if (/^(elif|else)\b/.test(cleanLine)) {
|
||||||
|
state.indentLevel = Math.max(0, state.indentLevel - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aktuelle Einrückung für diese Zeile abholen
|
// 4. DIE EINZIGE WAHRHEIT: Harte Spaces setzen
|
||||||
const currentIndent = state.currentIndentString;
|
let result = trimmedLine;
|
||||||
|
|
||||||
// 2. NACH der Zeile: Wenn sie einen Block öffnet, Level für die nächste Zeile erhöhen
|
if (state.parenAlignments && state.parenAlignments.length > 0) {
|
||||||
if (/^(else\b|elif\b)/.test(line)) {
|
// Visual Alignment (Runde Klammern) - Das fasst diese Regel nicht an,
|
||||||
// else / elif springen nach dem Schließen direkt wieder auf
|
// das übernimmt die nachfolgende AlignmentRule.
|
||||||
|
} else if (state.inImportBlock) {
|
||||||
|
// Im Multi-Line Import rücken wir zur besseren Lesbarkeit eine Ebene tiefer ein
|
||||||
|
if (!cleanLine.startsWith('from ')) {
|
||||||
|
result = " ".repeat((state.indentLevel + 2) * state.tabSize) + trimmedLine;
|
||||||
|
} else {
|
||||||
|
result = state.currentIndentString + trimmedLine;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Standard-Einrückung unter Nutzung deines Getters
|
||||||
|
result = state.currentIndentString + trimmedLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. INDENT: Nächste Zeile rückt nach rechts
|
||||||
|
if (/^(pattern|syncmapping|table|definitions?|define|enumeration|configuration|overview|metadata|constants|triggers|body|mapping|removal)\b/.test(cleanLine)) {
|
||||||
state.indentLevel++;
|
state.indentLevel++;
|
||||||
}
|
}
|
||||||
else if (this.blockOpens.test(line)) {
|
if (/\b(then|do)\b$/.test(cleanLine)) {
|
||||||
|
state.indentLevel++;
|
||||||
|
}
|
||||||
|
// elif/else eröffnen sofort wieder einen neuen Block für die Folgezeile
|
||||||
|
if (/^(elif|else)\b/.test(cleanLine)) {
|
||||||
state.indentLevel++;
|
state.indentLevel++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentIndent + line;
|
// 6. Multi-Line Import Tracking
|
||||||
|
if (cleanLine.startsWith('from ')) {
|
||||||
|
state.inImportBlock = true;
|
||||||
|
}
|
||||||
|
if (state.inImportBlock && cleanLine.endsWith(';')) {
|
||||||
|
state.inImportBlock = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue