bmc-tpl-code-all-around/src/TplCompletionProvider.ts

211 lines
9.9 KiB
TypeScript
Raw Normal View History

import * as vscode from 'vscode';
export class TplCompletionProvider implements vscode.CompletionItemProvider {
async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.CompletionItem[]> {
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<string, string>();
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<string>();
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<string>();
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;
}
}