211 lines
9.9 KiB
TypeScript
211 lines
9.9 KiB
TypeScript
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;
|
|
}
|
|
} |