import * as vscode from 'vscode'; export class TplCompletionProvider implements vscode.CompletionItemProvider { async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { const linePrefix = document.lineAt(position).text.substring(0, position.character); const items: vscode.CompletionItem[] = []; const text = document.getText(); // 1. Die feste "log." Regel if (linePrefix.endsWith('log.')) { ['debug', 'info', 'warn', 'error'].forEach(method => { items.push(new vscode.CompletionItem(method, vscode.CompletionItemKind.Method)); }); return items; // Zwingt VS Code, nur diese 4 anzuzeigen! } // 2. Importe und lokale Definitions-Blöcke (Namespaces) sammeln const importMap = new Map(); const importRegex = /from\s+([\w.]+)\s+import\s+(\w+)/g; let m; while ((m = importRegex.exec(text)) !== null) { importMap.set(m[2], m[1]); } const localNamespaces = new Set(); const defBlockRegex = /^\s*definitions\s+([a-zA-Z0-9_]+)/gm; while ((m = defBlockRegex.exec(text)) !== null) { localNamespaces.add(m[1]); } // 3. Wenn wir auf freier Fläche tippen (kein Punkt davor) const matchDot = linePrefix.match(/([a-zA-Z0-9_]+)\.$/); if (!matchDot) { // A) Namespaces (Importiert und Lokal) vorschlagen importMap.forEach((_, key) => { const item = new vscode.CompletionItem(key, vscode.CompletionItemKind.Module); item.detail = "Importiertes Modul"; items.push(item); }); localNamespaces.forEach(ns => { const item = new vscode.CompletionItem(ns, vscode.CompletionItemKind.Module); item.detail = "Lokales Modul (definitions)"; items.push(item); }); // B) Lokale Variablen vorschlagen (z.B. attribute_list :=) const varRegex = /\b([a-zA-Z0-9_]+)\s*:=/g; const variables = new Set(); while ((m = varRegex.exec(text)) !== null) { variables.add(m[1]); } variables.forEach(v => { items.push(new vscode.CompletionItem(v, vscode.CompletionItemKind.Variable)); }); // C) Das große Skeleton Snippet const skeletonItem = new vscode.CompletionItem('tpl_skeleton', vscode.CompletionItemKind.Snippet); skeletonItem.insertText = new vscode.SnippetString( 'tpl 1.23 module ${1:module_name};\n\n' + 'metadata\n' + ' origin := \'${2:origin}\';\n' + ' author := \'${3:author}\';\n' + ' products := \'${4:products}\';\n' + ' publishers := \'${5:publishers}\';\n' + ' tree_path := \'${6:company}\', \'${7:category}\', \'${8:subcategory}\';\n' + 'end metadata;\n\n' + 'pattern ${9:pattern_name} 1.0\n' + ' """\n' + ' TODO: explain\n' + ' """\n\n' + ' triggers\n' + ' on process := DiscoveredProcess where cmd matches regex_escape("${10:process}");\n' + ' end triggers;\n\n' + ' body\n' + ' log.debug("Start pattern ${9:pattern_name}");\n' + ' $0\n' + ' end body;\n' + 'end pattern;' ); skeletonItem.detail = "Generiert das komplette TPL Modul-Grundgerüst"; items.push(skeletonItem); // D) Block-Snippets const blocks = ['metadata', 'overview', 'constants', 'triggers', 'body', 'mapping', 'removal', 'definitions', 'enumeration', 'configuration']; for (const block of blocks) { const item = new vscode.CompletionItem(block, vscode.CompletionItemKind.Snippet); item.insertText = new vscode.SnippetString(`${block}\n $0\nend ${block};`); item.detail = `Fügt ${block} Block mit 'end' ein`; items.push(item); } const defineItem = new vscode.CompletionItem('define', vscode.CompletionItemKind.Snippet); defineItem.insertText = new vscode.SnippetString('define ${1:name}(${2:params})\n $0\nend define;'); items.push(defineItem); const patternItem = new vscode.CompletionItem('pattern', vscode.CompletionItemKind.Snippet); patternItem.insertText = new vscode.SnippetString('pattern ${1:name}${2:1.0}\n """\n TODO: explain\n """\n\n $0\nend pattern;'); items.push(patternItem); const syncmappingItem = new vscode.CompletionItem('syncmapping', vscode.CompletionItemKind.Snippet); syncmappingItem.insertText = new vscode.SnippetString('syncmapping ${1:name}${2:1.0}\n """\n TODO: explain\n """\n\n $0\nend syncmapping;'); items.push(syncmappingItem); const tableItem = new vscode.CompletionItem('table', vscode.CompletionItemKind.Snippet); tableItem.insertText = new vscode.SnippetString('table ${1:name}${2:1.0}\n $0\n default -> "unknown";\nend table;'); items.push(tableItem); const ifItem = new vscode.CompletionItem('if', vscode.CompletionItemKind.Snippet); ifItem.insertText = new vscode.SnippetString('if ${1:condition} then\n $0\nend if;'); items.push(ifItem); const forItem = new vscode.CompletionItem('for', vscode.CompletionItemKind.Snippet); forItem.insertText = new vscode.SnippetString('for ${1:item} in${2:list} do\n $0\nend for;'); items.push(forItem); return items; } else { // 4. Wir haben nach einem Punkt getippt (z.B. helpers.) const ns = matchDot[1]; const moduleMethods: vscode.CompletionItem[] = []; // Regex sucht nach dem Wort direkt vor der Klammer: define [add] method_name( const extractDefineRegex = /^\s*define\s+.*?\b([a-zA-Z0-9_]+)\s*\(/gm; // Fall A: Lokaler Definitions-Block (gleiche Datei!) if (localNamespaces.has(ns)) { let defMatch; while ((defMatch = extractDefineRegex.exec(text)) !== null) { const methodName = defMatch[1]; const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method); const docStr = this.extractDocString(text, defMatch.index); if (docStr) { item.documentation = new vscode.MarkdownString(docStr); } moduleMethods.push(item); } return moduleMethods; } // Fall B: Importiertes Modul (externe Datei) if (importMap.has(ns)) { const modPath = importMap.get(ns)!; const uris = await vscode.workspace.findFiles('**/*.tpl', '**/node_modules/**', 50); for (const uri of uris) { try { const docBytes = await vscode.workspace.fs.readFile(uri); const docText = Buffer.from(docBytes).toString('utf8'); const modRegex = new RegExp(`tpl\\s+1\\.\\d+\\s+module\\s+${modPath.replace(/\./g, '\\\\.')}\\s*;`); if (modRegex.test(docText)) { let defMatch; while ((defMatch = extractDefineRegex.exec(docText)) !== null) { const methodName = defMatch[1]; const item = new vscode.CompletionItem(methodName, vscode.CompletionItemKind.Method); const docStr = this.extractDocString(docText, defMatch.index); if (docStr) { item.documentation = new vscode.MarkdownString(docStr); } moduleMethods.push(item); } break; } } catch(e) {} } if (moduleMethods.length > 0) { return moduleMethods; } } } return items; } private extractDocString(text: string, startIndex: number): string | null { const lines = text.substring(startIndex).split('\n'); let docString = ""; let inDocString = false; let quoteType = ""; for (let j = 1; j < Math.min(15, lines.length); j++) { const line = lines[j].trim(); if (line.length === 0) continue; if (!inDocString) { if (line.startsWith('"""') || line.startsWith("'''")) { inDocString = true; quoteType = line.substring(0, 3); if (line.length > 3 && line.endsWith(quoteType)) { docString = line.substring(3, line.length - 3).trim(); break; } const content = line.substring(3).trim(); if (content) docString += content + "\n"; } else { break; } } else { if (line.endsWith(quoteType)) { const content = line.substring(0, line.length - 3).trim(); if (content) docString += content; break; } else { docString += line + "\n"; } } } return docString.trim() || null; } }