import * as monaco from 'monaco-editor'; import type { LogQLFieldCompletionContext, LogQLLanguage, VendorConfig } from '../types'; import type { Situation } from '../parser/types'; import type { LogQLHistoryItem } from '../history'; export interface BuildCompletionsArgs { vendor: VendorConfig; situation: Situation; range: monaco.IRange; variables: string[]; language?: LogQLLanguage; history?: LogQLHistoryItem[]; historyRange?: monaco.IRange; fetchFieldNames?: (ctx: LogQLFieldCompletionContext) => Promise; fetchFieldValues?: (fieldName: string, ctx: LogQLFieldCompletionContext) => Promise; } const SORT = { history: '0', variable: '1', fieldValue: '2', fieldName: '3', builtinField: '4', keyword: '5', function: '6', }; export async function buildCompletions(args: BuildCompletionsArgs): Promise { const { vendor, situation, range, variables, language = 'zh_CN', history = [], historyRange = range } = args; const items: monaco.languages.CompletionItem[] = []; const ctx: LogQLFieldCompletionContext = { vendor: vendor.id, section: situation.section, fieldName: situation.fieldName, prefix: situation.prefix, }; switch (situation.kind) { case 'NONE': break; case 'IN_STRING': items.push(...variableItems(variables, range)); break; case 'VARIABLE': items.push(...variableItems(variables, range)); break; case 'FIELD_NAME': { items.push(...historyItems(history, historyRange, language)); items.push(...variableItems(variables, range)); items.push(...keywordItems(vendor.searchKeywords, range)); items.push(...builtinFieldItems(vendor, range)); const names = await safeFetch(args.fetchFieldNames, ctx); items.push(...fieldNameItems(names, vendor, range)); items.push(...searchSnippetItems(range)); break; } case 'FIELD_VALUE': { items.push(...variableItems(variables, range)); if (situation.fieldName) { const values = await safeFetchValues(args.fetchFieldValues, situation.fieldName, ctx); items.push(...fieldValueItems(values, range)); } items.push(...valueSnippetItems(range)); break; } case 'ANALYZE_KEYWORD': { items.push(...variableItems(variables, range)); items.push(...keywordItems(vendor.sqlKeywords, range)); items.push(...sqlFunctionItems(vendor, range)); break; } case 'ANALYZE_FUNCTION': case 'ANALYZE_FIELD': { items.push(...variableItems(variables, range)); const names = await safeFetch(args.fetchFieldNames, ctx); items.push(...fieldNameItems(names, vendor, range, /*addColon*/ false)); items.push(...sqlFunctionItems(vendor, range)); items.push(...keywordItems(vendor.sqlKeywords, range)); break; } } return deduplicateItems(items); } function historyItems( history: LogQLHistoryItem[], range: monaco.IRange, language: LogQLLanguage, ): monaco.languages.CompletionItem[] { return history.map((item, index) => ({ label: { label: item.query, description: language === 'en_US' ? 'History' : '历史记录', }, kind: monaco.languages.CompletionItemKind.Reference, insertText: item.query, sortText: SORT.history + String(index).padStart(2, '0'), range, })); } /** * Deduplicate completion items by their label to avoid showing duplicates * when the same name comes from multiple sources (e.g. builtin fields vs * fetched field names). */ function deduplicateItems(items: monaco.languages.CompletionItem[]): monaco.languages.CompletionItem[] { const seen = new Set(); const result: monaco.languages.CompletionItem[] = []; for (const item of items) { const key = typeof item.label === 'string' ? item.label : item.label.label; if (seen.has(key)) continue; seen.add(key); result.push(item); } return result; } function variableItems(variables: string[], range: monaco.IRange): monaco.languages.CompletionItem[] { return variables.map((name) => ({ label: '${' + name + '}', kind: monaco.languages.CompletionItemKind.Variable, insertText: '${' + name + '}', detail: '变量', sortText: SORT.variable + name, range, })); } function keywordItems(keywords: string[], range: monaco.IRange): monaco.languages.CompletionItem[] { return keywords.map((kw) => ({ label: kw, kind: monaco.languages.CompletionItemKind.Keyword, insertText: kw, sortText: SORT.keyword + kw, range, })); } function builtinFieldItems(vendor: VendorConfig, range: monaco.IRange): monaco.languages.CompletionItem[] { return (vendor.searchBuiltinFields ?? []).map((f) => ({ label: f.name, kind: monaco.languages.CompletionItemKind.Field, insertText: f.name + ':', detail: f.detail, documentation: f.doc, sortText: SORT.builtinField + f.name, range, })); } function fieldNameItems(names: string[], vendor: VendorConfig, range: monaco.IRange, addColon = true): monaco.languages.CompletionItem[] { const op = vendor.fieldAssignOperators[0] ?? ':'; return names.map((name) => ({ label: name, kind: monaco.languages.CompletionItemKind.Field, insertText: addColon ? name + op : name, sortText: SORT.fieldName + name, range, })); } function fieldValueItems(values: string[], range: monaco.IRange): monaco.languages.CompletionItem[] { return values.map((value) => { const needsQuote = /[\s|"'(){}\[\]:=<>!,*?]/.test(value); const insert = needsQuote ? `"${value.replace(/"/g, '\\"')}"` : value; return { label: value, kind: monaco.languages.CompletionItemKind.Value, insertText: insert, sortText: SORT.fieldValue + value, range, }; }); } function searchSnippetItems(range: monaco.IRange): monaco.languages.CompletionItem[] { return [ { label: '(...)', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '($1)', insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, detail: '分组', sortText: SORT.keyword + 'zz_group', range, }, ]; } function valueSnippetItems(range: monaco.IRange): monaco.languages.CompletionItem[] { return [ { label: '"..."', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '"$0"', insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, detail: '短语匹配', sortText: SORT.fieldValue + 'zz_phrase', range, }, { label: '[a TO b]', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '[${1:from} TO ${2:to}]', insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, detail: '区间查询', sortText: SORT.fieldValue + 'zz_range', range, }, { label: '*', kind: monaco.languages.CompletionItemKind.Snippet, insertText: '*', detail: '通配匹配', sortText: SORT.fieldValue + 'zz_any', range, }, ]; } function sqlFunctionItems(vendor: VendorConfig, range: monaco.IRange): monaco.languages.CompletionItem[] { return vendor.sqlFunctions.map((fn) => ({ label: fn.name, kind: monaco.languages.CompletionItemKind.Function, insertText: fn.name + '($0)', insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, detail: fn.signature, documentation: fn.doc, sortText: SORT.function + fn.name, range, })); } async function safeFetch(fn: ((ctx: LogQLFieldCompletionContext) => Promise) | undefined, ctx: LogQLFieldCompletionContext): Promise { if (!fn) return []; try { const result = await fn(ctx); return Array.isArray(result) ? result : []; } catch { return []; } } async function safeFetchValues( fn: ((fieldName: string, ctx: LogQLFieldCompletionContext) => Promise) | undefined, fieldName: string, ctx: LogQLFieldCompletionContext, ): Promise { if (!fn) return []; try { const result = await fn(fieldName, ctx); return Array.isArray(result) ? result : []; } catch { return []; } }