import * as vscode from 'vscode'; export class TplHoverProvider implements vscode.HoverProvider { async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { const wordRange = document.getWordRangeAtPosition(position, /[a-zA-Z0-9_.]+/); if (!wordRange) return null; const fullWord = document.getText(wordRange); const currentText = document.getText(); let namespace = ""; let methodOrPatternName = fullWord; if (fullWord.includes('.')) { const parts = fullWord.split('.'); namespace = parts[0]; methodOrPatternName = parts[1]; } // FEHLERBEHEBUNG: [ \t] statt \s verwenden! // \s frisst Zeilenumbrüche (\n), was dazu geführt hat, dass match.index // auf leere Zeilen oberhalb der Methode gezeigt hat -> Leeres Hover-Fenster. const searchRegex = new RegExp(`^[ \\t]*(?:(?:define[ \\t]+(?:[a-zA-Z0-9_]+[ \\t]+)*${methodOrPatternName}[ \\t]*\\()|(?:(?:pattern|syncmapping|table|enumeration|configuration)[ \\t]+${methodOrPatternName}\\b))`, 'm'); // 1. Suche im aktuellen Dokument (Ohne Namespace) if (!namespace) { const match = searchRegex.exec(currentText); if (match) { return this.buildHover(currentText, match.index); } return null; // Bricht sauber ab, ohne das System zu blockieren } // 2. Suche mit Namespace (z.B. helpers.method) // A) Ist der Namespace in derselben Datei? (lokaler definitions block) const localDefRegex = new RegExp(`^[ \\t]*definitions[ \\t]+${namespace}\\b`, 'm'); if (localDefRegex.test(currentText)) { const match = searchRegex.exec(currentText); if (match) { return this.buildHover(currentText, match.index); } return null; } // B) Ist der Namespace importiert? (Workspace durchsuchen) const importRegex = new RegExp(`from\\s+([\\w.]+)\\s+import\\s+${namespace}\\b`); const importMatch = importRegex.exec(currentText); if (importMatch) { const modPath = importMatch[1]; 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)) { const match = searchRegex.exec(docText); if (match) { return this.buildHover(docText, match.index); } break; } } catch(e) {} } } return null; } private buildHover(text: string, matchIndex: number): vscode.Hover { const docStr = this.extractDocString(text, matchIndex); const markdown = new vscode.MarkdownString(); // Schneidet exakt die Code-Zeile aus (ohne vorausgehende Leerzeilen) const declLine = text.substring(matchIndex).split('\n')[0].trim(); markdown.appendCodeblock(declLine, 'tpl'); if (docStr) { markdown.appendMarkdown(`---\n*${docStr}*`); } else { markdown.appendMarkdown(`---\n*(Keine Dokumentation gefunden)*`); } return new vscode.Hover(markdown); } 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; } }