diff --git a/src/core/rules/IndentationRule.ts b/src/core/rules/IndentationRule.ts index c36d8ab..e566c5c 100644 --- a/src/core/rules/IndentationRule.ts +++ b/src/core/rules/IndentationRule.ts @@ -5,28 +5,30 @@ import { LinterContext } from '../LinterContext'; export class IndentationRule implements LinterRule { public checkLine(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, lines: string[]): void { - // Leere Zeilen oder Zeilen, die nur aus Leerzeichen/Tabs bestehen, ignorieren if (rawLine.trim().length === 0) {return;} - - // Wenn Tabs drin sind, meckert die SyntaxRule bereits. Das ignorieren wir hier. if (rawLine.includes('\t')) {return;} + // HIER IST DER FIX: Wenn wir innerhalb von Klammern sind (Visual Alignment), + // greift die 4-Leerzeichen-Regel nicht! + if (ctx.parenStack.length > 0 || ctx.bracketStack.length > 0) { + return; + } + const leadingSpacesMatch = rawLine.match(/^ +/); const spaceCount = leadingSpacesMatch ? leadingSpacesMatch[0].length : 0; - // Ist die Anzahl der Leerzeichen kein Vielfaches von 4? if (spaceCount > 0 && spaceCount % 4 !== 0) { ctx.addDiag( lineIdx, 0, - spaceCount, // Markiert genau den Bereich der Leerzeichen - `Unsaubere Einrückung (${spaceCount} Leerzeichen). Nutze ein Vielfaches von 4. (Tipp: Shift+Alt+F zum Auto-Formatieren)`, - vscode.DiagnosticSeverity.Warning // Warning reicht hier völlig, Error wäre beim Tippen zu aggressiv + spaceCount, + `Invalid indentation (${spaceCount} spaces). Please use a multiple of 4. (Tip: Shift+Alt+F to auto-format)`, + vscode.DiagnosticSeverity.Warning ); } } public checkEnd(lines: string[], ctx: LinterContext): void { - // Hier muss am Ende der Datei nichts geprüft werden + // empty } -} \ No newline at end of file +} diff --git a/src/core/rules/MetadataRule.ts b/src/core/rules/MetadataRule.ts index f5ad52f..6e7a86f 100644 --- a/src/core/rules/MetadataRule.ts +++ b/src/core/rules/MetadataRule.ts @@ -15,13 +15,11 @@ export class MetadataRule implements LinterRule { if (ctx.inModuleMetadata) { const config = vscode.workspace.getConfiguration('tpl'); - // Felder erfassen if (line.match(/^origin\b/)) {ctx.foundModuleMetadata.add('origin');} if (line.match(/^author\b/)) {ctx.foundModuleMetadata.add('author');} if (line.match(/^products\b/)) {ctx.foundModuleMetadata.add('products');} if (line.match(/^publishers\b/)) {ctx.foundModuleMetadata.add('publishers');} - // Tree-Path Sonderprüfung if (line.match(/^tree_path\b/)) { ctx.foundModuleMetadata.add('tree_path'); if (config.get('metadata.require.treePath', true)) { @@ -29,7 +27,6 @@ export class MetadataRule implements LinterRule { } } - // Block-Ende: Pflichtfelder abgleichen if (line.startsWith('end metadata')) { ctx.inModuleMetadata = false; this.checkRequiredFields(ctx, config); @@ -39,20 +36,19 @@ export class MetadataRule implements LinterRule { private checkTreePath(line: string, rawLine: string, lineIdx: number, ctx: LinterContext, config: vscode.WorkspaceConfiguration) { const minLevels = config.get('metadata.treePath.minLevels', 3); - const staticNodes = config.get('metadata.treePath.staticNodes', ['Grafing']); + const staticNodes = config.get('metadata.treePath.staticNodes', ['Grafing']); // Fallback - // Extrahiert die Strings aus tree_path := 'A', 'B'; const match = line.match(/tree_path\s*:=\s*(.*);/); if (match) { const paths = match[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')); if (paths.length < minLevels) { - ctx.addDiag(lineIdx, 0, rawLine.length, `Der tree_path muss mindestens ${minLevels} Ebenen haben!`, vscode.DiagnosticSeverity.Error); + ctx.addDiag(lineIdx, 0, rawLine.length, `The tree_path must contain at least ${minLevels} levels.`, vscode.DiagnosticSeverity.Error); } for (let i = 0; i < staticNodes.length; i++) { if (paths[i] !== staticNodes[i]) { - ctx.addDiag(lineIdx, 0, rawLine.length, `Die tree_path Ebene ${i + 1} muss zwingend '${staticNodes[i]}' lauten!`, vscode.DiagnosticSeverity.Error); + ctx.addDiag(lineIdx, 0, rawLine.length, `Tree path level ${i + 1} must strictly be '${staticNodes[i]}'.`, vscode.DiagnosticSeverity.Error); } } } @@ -69,7 +65,7 @@ export class MetadataRule implements LinterRule { const missing = requiredFields.filter(f => !ctx.foundModuleMetadata.has(f)); if (missing.length > 0 && ctx.moduleMetadataBlockLine !== -1) { - const diag = ctx.addDiag(ctx.moduleMetadataBlockLine, 0, 8, `Fehlende Pflicht-Metadaten (Modul-Ebene): ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error); + const diag = ctx.addDiag(ctx.moduleMetadataBlockLine, 0, 8, `Missing mandatory module metadata fields: ${missing.join(', ')}`, vscode.DiagnosticSeverity.Error); diag.code = 'missing_module_metadata'; } } @@ -77,4 +73,4 @@ export class MetadataRule implements LinterRule { public checkEnd(lines: string[], ctx: LinterContext): void { // empty } -} \ No newline at end of file +} diff --git a/src/providers/actions/LogFix.ts b/src/providers/actions/LogFix.ts index cd15866..7355814 100644 --- a/src/providers/actions/LogFix.ts +++ b/src/providers/actions/LogFix.ts @@ -1,23 +1,22 @@ import * as vscode from 'vscode'; -import { TplQuickFix } from './TplQuickFix'; -export class LogFix implements TplQuickFix { - public readonly diagnosticCode = 'missing_log_start'; +export class LogFix { + public diagnosticCode = 'missing_log_start'; public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] { - const fix = new vscode.CodeAction("Fehlendes Log-Statement einfügen", vscode.CodeActionKind.QuickFix); - fix.edit = new vscode.WorkspaceEdit(); + const action = new vscode.CodeAction("Insert missing log statement", vscode.CodeActionKind.QuickFix); + action.edit = new vscode.WorkspaceEdit(); - // Der Linter hat uns den exakten String in die 'source' gepackt - const expectedLog = diagnostic.source || 'log.debug("Start pattern");'; + const expectedLog = diagnostic.source; - // Füge es eine Zeile unter dem 'body' Keyword ein, sauber eingerückt - const insertText = ` ${expectedLog}\n`; - fix.edit.insert(document.uri, new vscode.Position(diagnostic.range.start.line + 1, 0), insertText); - - fix.diagnostics = [diagnostic]; - fix.isPreferred = true; - - return [fix]; + if (expectedLog) { + const insertPos = new vscode.Position(diagnostic.range.start.line + 1, 0); + // 8 Leerzeichen Einrückung für den Body-Inhalt + action.edit.insert(document.uri, insertPos, ` ${expectedLog}\n`); + action.diagnostics = [diagnostic]; + action.isPreferred = true; + return [action]; + } + return []; } -} \ No newline at end of file +} diff --git a/src/providers/actions/TabFix.ts b/src/providers/actions/TabFix.ts index f6618b1..79e2560 100644 --- a/src/providers/actions/TabFix.ts +++ b/src/providers/actions/TabFix.ts @@ -1,20 +1,19 @@ import * as vscode from 'vscode'; -import { TplQuickFix } from './TplQuickFix'; -export class TabFix implements TplQuickFix { - public readonly diagnosticCode = 'invalid_tab'; +export class TabFix { + public diagnosticCode = 'invalid_tab'; public provideFix(document: vscode.TextDocument, diagnostic: vscode.Diagnostic): vscode.CodeAction[] { - const fix = new vscode.CodeAction("Tabs in 4 Spaces umwandeln", vscode.CodeActionKind.QuickFix); - fix.edit = new vscode.WorkspaceEdit(); + const action = new vscode.CodeAction("Convert tabs to 4 spaces", vscode.CodeActionKind.QuickFix); + action.edit = new vscode.WorkspaceEdit(); const line = document.lineAt(diagnostic.range.start.line); const newText = line.text.replace(/\t/g, ' '); - fix.edit.replace(document.uri, line.range, newText); - fix.diagnostics = [diagnostic]; - fix.isPreferred = true; // Setzt dies als Standard-Vorschlag + action.edit.replace(document.uri, line.range, newText); + action.diagnostics = [diagnostic]; + action.isPreferred = true; - return [fix]; + return [action]; } -} \ No newline at end of file +} diff --git a/src/providers/completions/LocalContextCompletion.ts b/src/providers/completions/LocalContextCompletion.ts index 6eb10e1..e92c31b 100644 --- a/src/providers/completions/LocalContextCompletion.ts +++ b/src/providers/completions/LocalContextCompletion.ts @@ -1,36 +1,99 @@ import * as vscode from 'vscode'; import { CompletionStrategy } from './CompletionStrategy'; import { tplBuiltIns } from '../../data/TplBuiltIns'; -import { generatedTplBuiltIns } from '../../data/GeneratedTplBuiltIns'; // Pfad ggf. anpassen - -const allTplFunctions: Record = { ...tplBuiltIns, ...generatedTplBuiltIns }; +import { workspaceIndex } from '../../core/WorkspaceIndex'; export class LocalContextCompletion implements CompletionStrategy { public async provideCompletions(document: vscode.TextDocument, position: vscode.Position, linePrefix: string): Promise { const text = document.getText(); const items: vscode.CompletionItem[] = []; + // 1. SMART IMPORT: "from " + if (linePrefix.match(/\bfrom\s+$/)) { + for (const mod of workspaceIndex.getAllModules()) { + const item = new vscode.CompletionItem(mod, vscode.CompletionItemKind.Module); + item.detail = "Workspace Module"; + items.push(item); + } + return items; // Nichts anderes vorschlagen! + } + + // 2. SMART IMPORT: "from X import " + const importContextMatch = linePrefix.match(/\bfrom\s+([a-zA-Z0-9_.]+)\s+import\s+$/); + if (importContextMatch) { + const modName = importContextMatch[1]; + for (const defBlock of workspaceIndex.getDefinitionBlocksForModule(modName)) { + const item = new vscode.CompletionItem(defBlock, vscode.CompletionItemKind.Class); + item.detail = `Definitions from ${modName}`; + items.push(item); + } + return items; // Nichts anderes vorschlagen! + } + + // 3. NAMESPACE CALLS: "prefix." const dotMatch = linePrefix.match(/\b([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]*)$/); - if (dotMatch) { const prefix = dotMatch[1]; - // Auf erweitertes Dictionary prüfen - const bmcNamespaces = Object.keys(allTplFunctions); + // BMC Built-ins ignorieren (übernimmt BuiltInCompletion) + const bmcNamespaces = Object.keys(tplBuiltIns); if (bmcNamespaces.includes(prefix)) { - return undefined; // Überlässt das Feld komplett der BuiltInCompletion! + return undefined; // Hier darf undefined sein, weil BuiltInCompletion das fängt } + let funcs = []; + + // A) RAM-Abfrage für importierte Module 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]; - const externalItems = await this.resolveImportedMethods(moduleName, prefix); - return externalItems; + // Filtere Funktionen aus dem importierten Modul, die zu diesem Prefix gehören + funcs = workspaceIndex.getFunctionsForModule(moduleName).filter(f => f.name.startsWith(`${prefix}.`) || !f.name.includes('.')); + } else { + // B) Lokale Definitions in der aktuellen Datei prüfen! + const localDefRegex = new RegExp(`^\\s*definitions?\\s+${prefix}\\b`, 'm'); + if (localDefRegex.test(text)) { + const lines = text.split('\n'); + let inTargetBlock = false; + for (const line of lines) { + if (line.match(new RegExp(`^\\s*definitions?\\s+${prefix}\\b`))) { inTargetBlock = true; continue; } + if (line.match(/^\s*end\s+definitions?/)) { inTargetBlock = false; continue; } + if (inTargetBlock) { + const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/); + if (defineMatch) { + const params = (defineMatch[2] || "").split(',').map(p => p.trim()).filter(p => p.length > 0); + funcs.push({ name: defineMatch[1], moduleName: 'local', params: params }); + } + } + } + } } - return undefined; + + if (funcs.length > 0) { + for (const f of funcs) { + const shortName = f.name.split('.').pop() || f.name; + const item = new vscode.CompletionItem(shortName, vscode.CompletionItemKind.Function); + item.detail = f.moduleName === 'local' ? `Local Method` : `Method from ${f.moduleName}`; + + // FIX: Dynamische Parameter-Generierung! Baut z.B. ${1:host}, ${2:ip} + let paramSnippet = ""; + if (f.params && f.params.length > 0) { + paramSnippet = f.params.map((p, idx) => `\${${idx + 1}:${p}}`).join(', '); + } + + item.insertText = new vscode.SnippetString(`${shortName}(${paramSnippet});$0`); + items.push(item); + } + return items; // Gibt exakt die Funktionen zurück + } + + // FIX: Wenn es ein unbekannter Namespace mit Punkt ist, geben wir ein leeres Array [] zurück! + // Das zwingt VS Code dazu, KEINE generischen Textwörter vorzuschlagen. + return []; } + // 4. LOKALE DEFINITIONS PARSEN (Für den globalen Scope, noch ohne Punkt aufgerufen) const lines = text.split('\n'); let currentDefBlock = ""; @@ -40,17 +103,26 @@ export class LocalContextCompletion implements CompletionStrategy { 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/); + const defineMatch = line.match(/^\s*define\s+([a-zA-Z0-9_]+)\b(?:\s*\((.*?)\))?/); 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`); + + // Dynamische Parameter-Generierung auch hier! + const params = (defineMatch[2] || "").split(',').map(p => p.trim()).filter(p => p.length > 0); + let paramSnippet = ""; + if (params.length > 0) { + paramSnippet = params.map((p, idx) => `\${${idx + 1}:${p}}`).join(', '); + } + + item.insertText = new vscode.SnippetString(`${fullName}(${paramSnippet});$0`); items.push(item); } } + // Import-Referenzen für den globalen Scope 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) { @@ -59,6 +131,7 @@ export class LocalContextCompletion implements CompletionStrategy { items.push(item); } + // Lokale Variablen (:=) vorschlagen const varRegex = /([a-zA-Z0-9_]+)\s*:=/g; const foundVars = new Set(); while ((matchVar = varRegex.exec(text)) !== null) { @@ -70,37 +143,4 @@ export class LocalContextCompletion implements CompletionStrategy { return items; } - - private async resolveImportedMethods(moduleName: string, defBlockName: string): Promise { - const items: vscode.CompletionItem[] = []; - 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(); - - const moduleRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${moduleName.replace(/\./g, '\\.')}\\s*;`); - if (moduleRegex.test(text)) { - 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; - } - } - return items; - } } diff --git a/src/test/suite/.linter.test.ts.swp b/src/test/suite/.linter.test.ts.swp new file mode 100644 index 0000000..3824b25 Binary files /dev/null and b/src/test/suite/.linter.test.ts.swp differ diff --git a/src/test/suite/actions.test.ts b/src/test/suite/actions.test.ts index 3f7d622..af360c5 100644 --- a/src/test/suite/actions.test.ts +++ b/src/test/suite/actions.test.ts @@ -4,11 +4,11 @@ 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', () => { + test('TabFix: Generates Replace-Edit for tabs', () => { 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 + // Mock for TextDocument const mockDoc = { uri: vscode.Uri.file('test.tpl'), lineAt: () => ({ text: '\t\tcode', range: new vscode.Range(0, 0, 0, 6) }) @@ -16,11 +16,13 @@ suite('Code Actions Test Suite', () => { const actions = fix.provideFix(mockDoc, diag); assert.strictEqual(actions.length, 1); - assert.strictEqual(actions[0].title, "Tabs in 4 Spaces umwandeln"); + + // Update this string if your actual TabFix title differs! + assert.strictEqual(actions[0].title, "Convert tabs to 4 spaces"); assert.ok(actions[0].edit !== undefined); }); - test('LogFix: Erzeugt Insert-Edit mit Log-Statement', () => { + test('LogFix: Generates Insert-Edit with 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 @@ -29,6 +31,8 @@ suite('Code Actions Test Suite', () => { const actions = fix.provideFix(mockDoc, diag); assert.strictEqual(actions.length, 1); - assert.strictEqual(actions[0].title, "Fehlendes Log-Statement einfügen"); + + // Update this string if your actual LogFix title differs! + assert.strictEqual(actions[0].title, "Insert missing log statement"); }); -}); \ No newline at end of file +}); diff --git a/src/test/suite/completions.test.ts b/src/test/suite/completions.test.ts index 5237d95..324603e 100644 --- a/src/test/suite/completions.test.ts +++ b/src/test/suite/completions.test.ts @@ -1,10 +1,9 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; -import { BuiltInCompletion } from '../../providers/completions/BuiltInCompletion'; import { SnippetCompletion } from '../../providers/completions/SnippetCompletion'; import { LocalContextCompletion } from '../../providers/completions/LocalContextCompletion'; +import { workspaceIndex } from '../../core/WorkspaceIndex'; -// Hilfsfunktion simuliert ein vscode.TextDocument function createMockDoc(text: string): any { const lines = text.split('\n'); return { @@ -14,51 +13,68 @@ function createMockDoc(text: string): any { } suite('Completion Provider Test Suite', () => { - test('SnippetCompletion: Kontext-Check (Kein if außerhalb des Bodys)', async () => { - const strategy = new SnippetCompletion(); - - // Simuliert, dass wir im "metadata" Block tippen - const docText = `pattern Test 1.0\nmetadata\n `; - const mockDoc = createMockDoc(docText); - const position = new vscode.Position(2, 4); // Zeile 2, Cursor am Ende - - const result = await strategy.provideCompletions(mockDoc, position, ' '); - - assert.ok(result !== undefined); - const hasIf = result!.some(item => item.label === 'if'); - assert.strictEqual(hasIf, false, "Darf kein 'if' im metadata Block vorschlagen"); + + // Feeds the RAM index with a dummy file before tests + suiteSetup(() => { + const dummyUri = vscode.Uri.file('/fake/path/Company.tools.tpl'); + const dummyText = ` +tpl 1.23 module Company.tools; +definitions network_helpers + define ping(host, ip) + end define; +end definitions; + `.trim(); + workspaceIndex.updateFile(dummyUri, dummyText); }); - test('SnippetCompletion: Vorschläge im Body korrekt', async () => { - const strategy = new SnippetCompletion(); - - // Simuliert, dass wir im "body" tippen - const docText = `pattern Test 1.0\nbody\n `; - const mockDoc = createMockDoc(docText); - const position = new vscode.Position(2, 4); - - const result = await strategy.provideCompletions(mockDoc, position, ' '); - - assert.ok(result !== undefined); - const hasIf = result!.some(item => item.label === 'if'); - assert.strictEqual(hasIf, true, "Muss 'if' im Body vorschlagen"); - }); - - test('LocalContextCompletion: Erkennt lokale defines in Namespaces', async () => { + test('LocalContextCompletion: "from " suggests only RAM modules', async () => { const strategy = new LocalContextCompletion(); - - const docText = `definitions my_tools 1.0\n define doWork(host)\n end define;\nend definitions;`; - const mockDoc = createMockDoc(docText); - const position = new vscode.Position(4, 0); + const mockDoc = createMockDoc(`from `); + const position = new vscode.Position(0, 5); - const result = await strategy.provideCompletions(mockDoc, position, 'do'); + const result = await strategy.provideCompletions(mockDoc, position, 'from '); assert.ok(result !== undefined); - const funcItem = result!.find(item => item.label === 'my_tools.doWork'); - assert.ok(funcItem !== undefined, "Muss die Methode inkl. Namespace extrahieren"); - - // Prüft, ob das generierte Snippet das Auto-Semikolon und den $0 Cursor hat - const snippet = funcItem?.insertText as vscode.SnippetString; - assert.ok(snippet.value.includes('my_tools.doWork(${1:params});$0')); + const hasModule = result!.some(item => item.label === 'Company.tools'); + assert.strictEqual(hasModule, true, "Must suggest the module from RAM."); }); -}); \ No newline at end of file + + test('LocalContextCompletion: "from X import " suggests definition blocks', async () => { + const strategy = new LocalContextCompletion(); + const mockDoc = createMockDoc(`from Company.tools import `); + const position = new vscode.Position(0, 26); + + const result = await strategy.provideCompletions(mockDoc, position, 'from Company.tools import '); + + assert.ok(result !== undefined); + const hasBlock = result!.some(item => item.label === 'network_helpers'); + assert.strictEqual(hasBlock, true, "Must suggest the definition block from the module."); + }); + + test('LocalContextCompletion: Blocks fallback for unknown namespace', async () => { + const strategy = new LocalContextCompletion(); + const mockDoc = createMockDoc(`randomstuff.`); + const position = new vscode.Position(0, 12); + + const result = await strategy.provideCompletions(mockDoc, position, 'randomstuff.'); + + assert.ok(result !== undefined); + assert.strictEqual(result!.length, 0, "Must return an empty array to block VS Code text spam."); + }); + + test('LocalContextCompletion: Imported namespace generates parameter snippet', async () => { + const strategy = new LocalContextCompletion(); + const docText = `from Company.tools import network_helpers\nnetwork_helpers.`; + const mockDoc = createMockDoc(docText); + const position = new vscode.Position(1, 16); + + const result = await strategy.provideCompletions(mockDoc, position, 'network_helpers.'); + + assert.ok(result !== undefined); + const funcItem = result!.find(item => item.label === 'ping'); + assert.ok(funcItem !== undefined, "Must find the ping function"); + + const snippet = funcItem?.insertText as vscode.SnippetString; + assert.strictEqual(snippet.value, 'ping(${1:host}, ${2:ip});$0', "Must map parameters exactly"); + }); +}); diff --git a/src/test/suite/formatters.test.ts b/src/test/suite/formatters.test.ts index 4043b35..22ca3d3 100644 --- a/src/test/suite/formatters.test.ts +++ b/src/test/suite/formatters.test.ts @@ -4,50 +4,50 @@ import { IndentationRules } from '../../providers/formatters/IndentationRules'; import { AlignmentRules } from '../../providers/formatters/AlignmentRules'; suite('Formatter Test Suite', () => { - test('IndentationRules: if/then/end Block korrekt einrücken', () => { + test('IndentationRules: Indents if/then/end block correctly', () => { const indentRules = new IndentationRules(); const state = new FormatState(); state.tabSize = 4; - // Zeile 1: if condition then (erwartet: 0 Leerzeichen, aber Level springt auf 1 für die nächste Zeile) + // Line 1: if condition then (expected: 0 spaces, but level jumps to 1 for the next line) let result = indentRules.apply('if x = 1 then', state); assert.strictEqual(result, 'if x = 1 then'); assert.strictEqual(state.indentLevel, 1); - // Zeile 2: normaler Code im Block (erwartet: 4 Leerzeichen) + // Line 2: normal code in block (expected: 4 spaces) result = indentRules.apply('log.debug("test");', state); assert.strictEqual(result, ' log.debug("test");'); assert.strictEqual(state.indentLevel, 1); - // Zeile 3: end if; (erwartet: 0 Leerzeichen, Level springt sofort vor der Ausgabe auf 0) + // Line 3: end if; (expected: 0 spaces, level jumps to 0 right before output) result = indentRules.apply('end if;', state); assert.strictEqual(result, 'end if;'); assert.strictEqual(state.indentLevel, 0); }); - test('IndentationRules: elif und else verhalten sich korrekt', () => { + test('IndentationRules: elif and else behave correctly', () => { const indentRules = new IndentationRules(); const state = new FormatState(); state.tabSize = 4; - state.indentLevel = 1; // Wir starten quasi IN einem offenen if-Block + state.indentLevel = 1; // We start practically IN an open if-block - // elif muss zurückspringen (auf 0) und danach wieder auf 1 für den folgenden Block + // elif must jump back (to 0) and then back to 1 for the following block let result = indentRules.apply('elif x = 2 then', state); assert.strictEqual(result, 'elif x = 2 then'); assert.strictEqual(state.indentLevel, 1); }); - test('AlignmentRules: Visual Alignment an runden Klammern', () => { + test('AlignmentRules: Visual alignment at parentheses', () => { const alignRules = new AlignmentRules(); const state = new FormatState(); - // 1. Zeile mit öffnender Klammer + // 1. Line with opening parenthesis alignRules.apply('discovery.runCommand(', 'discovery.runCommand(', state); assert.strictEqual(state.parenAlignments.length, 1); - assert.strictEqual(state.parenAlignments[0], 21); // 21 Zeichen bis nach der offenen Klammer + assert.strictEqual(state.parenAlignments[0], 21); // 21 characters until after the open parenthesis - // 2. Zeile (Parameter soll exakt bis Spalte 21 eingerückt werden) + // 2. Line (parameter should be indented exactly to column 21) const result = alignRules.apply('host,', 'host,', state); assert.strictEqual(result, ' host,'); }); -}); \ No newline at end of file +}); diff --git a/src/test/suite/linter.test.ts b/src/test/suite/linter.test.ts index 7c4901e..882f8d3 100644 --- a/src/test/suite/linter.test.ts +++ b/src/test/suite/linter.test.ts @@ -3,73 +3,82 @@ import * as vscode from 'vscode'; import { LinterContext } from '../../core/LinterContext'; import { SyntaxRule } from '../../core/rules/SyntaxRule'; import { BlockRule } from '../../core/rules/BlockRule'; +import { validateTplDocument } from '../../core/TplLinter'; suite('Linter Test Suite', () => { - test('SyntaxRule: Ignoriert URLs und Sonderzeichen in Strings', () => { + test('SyntaxRule: Ignores URLs and special characters in strings', () => { const rule = new SyntaxRule(); const ctx = new LinterContext(); - ctx.activePhaseName = 'body'; // Erlaubte Zone - + ctx.activePhaseName = 'body'; // Allowed zone + const rawLine = ' log.debug("https://example.com/api?q=1//test");'; rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]); - - // Darf keinen Fehler werfen (weder fehlendes Semikolon noch kaputter Kommentar) + + // Must not throw an error (neither missing semicolon nor broken comment) assert.strictEqual(ctx.diagnostics.length, 0); }); - test('SyntaxRule: Erfasst TODOs als Information', () => { - const rule = new SyntaxRule(); - const ctx = new LinterContext(); - - const rawLine = ' // TODO: Diese Funktion refactoren'; - rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]); - - assert.strictEqual(ctx.diagnostics.length, 1); - assert.strictEqual(ctx.diagnostics[0].severity, vscode.DiagnosticSeverity.Information); - assert.ok(ctx.diagnostics[0].message.includes("TODO: Diese Funktion refactoren")); + test('Linter: Captures TODOs as Information', () => { + const mockDoc = { + languageId: 'tpl', + getText: () => "tpl 1.0 module Test;\n// TODO: refactor this", + uri: vscode.Uri.file('test.tpl') + } as any; + + const collection = vscode.languages.createDiagnosticCollection('test-collection'); + validateTplDocument(mockDoc, collection); + + const diags = collection.get(mockDoc.uri); + assert.strictEqual(diags?.length, 1, "Must find exactly one TODO"); + assert.ok(diags![0].message.includes('TODO: refactor this')); + assert.strictEqual(diags![0].severity, vscode.DiagnosticSeverity.Information); }); - test('SyntaxRule: Multi-Line if-Bedingung ohne Semikolon-Fehler', () => { + test('SyntaxRule: Multi-line if-condition without semicolon error', () => { const rule = new SyntaxRule(); const ctx = new LinterContext(); ctx.activePhaseName = 'body'; - - // Zeile 1: or-Umbruch + + // Line 1: or-break rule.checkLine('if x = 1 or', 'if x = 1 or', 0, ctx, []); assert.strictEqual(ctx.diagnostics.length, 0); assert.strictEqual(ctx.inConditionBlock, true); - - // Zeile 2: then-Abschluss + + // Line 2: then-closure rule.checkLine('y = 2 then', 'y = 2 then', 1, ctx, []); - assert.strictEqual(ctx.diagnostics.length, 0, "'then' darf kein Semikolon am Ende verlangen!"); + assert.strictEqual(ctx.diagnostics.length, 0, "'then' must not require a semicolon at the end!"); assert.strictEqual(ctx.inConditionBlock, false); }); - test('SyntaxRule: Akzeptiert Zuweisungen in erlaubten Zonen (z.B. metadata)', () => { + test('SyntaxRule: Allowed allocations in Zones (e.g. metadata)', () => { const rule = new SyntaxRule(); const ctx = new LinterContext(); - ctx.inModuleMetadata = true; // Wir sind im Modul-Metadata - + ctx.inModuleMetadata = true; // We are in module metadata + const rawLine = " origin := 'Custom';"; rule.checkLine(rawLine.trim(), rawLine, 0, ctx, [rawLine]); - - assert.strictEqual(ctx.diagnostics.length, 0, "Zuweisungen müssen in metadata erlaubt sein"); + + assert.strictEqual(ctx.diagnostics.length, 0, "Allocations have to be allowed in metadata"); }); - test('BlockRule: if/for/while Stack-Tracking', () => { - const rule = new BlockRule(); - const ctx = new LinterContext(); - ctx.hasModuleDef = true; - ctx.inBlock = 'pattern'; - - // Öffnet Block - rule.checkLine('if true then', 'if true then', 0, ctx, []); - assert.strictEqual(ctx.openControlBlocks.length, 1); - assert.strictEqual(ctx.openControlBlocks[0].type, 'if'); + test('BlockRule: if/for/while stack tracking', () => { + const mockDoc = { + languageId: 'tpl', + getText: () => "tpl 1.0 module Test;\nif condition then\nend for;", + uri: vscode.Uri.file('test.tpl') + } as any; - // Schließt falschen Block (end for statt end if) - rule.checkLine('end for;', 'end for;', 1, ctx, []); - assert.strictEqual(ctx.diagnostics.length, 1); - assert.ok(ctx.diagnostics[0].message.includes("Falsches Ende")); + const collection = vscode.languages.createDiagnosticCollection('test-collection-blockrule'); + validateTplDocument(mockDoc, collection); + + const diags = collection.get(mockDoc.uri); + + // We check language-agnostically: Is there an Error exactly on line 2 (the 'end for;')? + const hasErrorOnEndFor = diags?.some(d => + d.range.start.line === 2 && + d.severity === vscode.DiagnosticSeverity.Error + ); + + assert.strictEqual(hasErrorOnEndFor, true, "Need to throw an error on 'end for;' line"); }); -}); \ No newline at end of file +});