import * as monaco from 'monaco-editor'; import type { Situation } from '../parser/types'; export interface BuildCompletionsArgs { situation: Situation; range: monaco.IRange; variables: string[]; fetchFieldNames?: (nestedPath?: string) => Promise; fetchFieldValues?: (fieldName: string) => Promise; } const SORT = { fieldName: '0', value: '1', variable: '2', operator: '3', keyword: '4', }; // ─── Static candidate definitions ───────────────────────────────── const OPERATORS = [ { label: ':', detail: 'Equals', insertText: ': ' }, { label: '<=', detail: 'Less than or equal', insertText: '<= ' }, { label: '>=', detail: 'Greater than or equal', insertText: '>= ' }, { label: '<', detail: 'Less than', insertText: '< ' }, { label: '>', detail: 'Greater than', insertText: '> ' }, ]; // No trailing space: let the user type it, which re-triggers suggest (space is a trigger char) const CONJUNCTIONS = [ { label: 'AND', detail: 'AND conjunction', insertText: 'AND' }, { label: 'OR', detail: 'OR conjunction', insertText: 'OR' }, ]; const NOT_KEYWORD = { label: 'NOT', detail: 'Negates the following expression', insertText: 'NOT' }; const GROUP_OPEN = { label: '( )', detail: 'Group a sub-query', insertText: '(' }; const EXISTS = { label: '*', detail: 'Field exists (wildcard)', insertText: '*' }; // ─── Helpers ────────────────────────────────────────────────────── function needsQuoting(value: string): boolean { return /[\s\\():<>"*{}]/.test(value); } function quoteValue(value: string): string { return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; } function insertableValue(value: string, insideString?: boolean): string { if (insideString) return value; return needsQuoting(value) ? quoteValue(value) : value; } function dedupe(values: string[]): string[] { const seen = new Set(); const result: string[] = []; for (const v of values) { if (!v || seen.has(v)) continue; seen.add(v); result.push(v); } return result; } function stringItem(label: string, insertText: string, detail: string, kind: monaco.languages.CompletionItemKind, sortText: string, range: monaco.IRange): monaco.languages.CompletionItem { return { label, kind, detail, insertText, sortText, range }; } // ─── Build completions ──────────────────────────────────────────── export async function buildCompletions(args: BuildCompletionsArgs): Promise { const { situation, range, variables } = args; const items: monaco.languages.CompletionItem[] = []; const pushFieldNames = async (nestedPath?: string) => { if (!args.fetchFieldNames) return; let names: string[] = []; try { names = await args.fetchFieldNames(nestedPath); } catch { return; } for (const name of dedupe(names)) { items.push( stringItem(name, insertableValue(name, situation.insideString), nestedPath ? `Nested field of ${nestedPath}` : 'Field', monaco.languages.CompletionItemKind.Property, SORT.fieldName, range), ); } }; const pushFieldValues = async (fieldName?: string) => { if (!args.fetchFieldValues || !fieldName) return; let values: string[] = []; try { values = await args.fetchFieldValues(fieldName); } catch { return; } for (const value of dedupe(values)) { // 与旧 CodeMirror 版 KQLInput 行为一致:字段值始终带引号插入(已在字符串内时除外), // 避免含 - 等字符的值被误解析 items.push(stringItem(value, situation.insideString ? value : quoteValue(value), 'Value', monaco.languages.CompletionItemKind.Value, SORT.value, range)); } }; const pushVariables = () => { for (const variable of dedupe(variables)) { items.push({ label: `\${${variable}}`, kind: monaco.languages.CompletionItemKind.Variable, insertText: situation.insideString ? `\${${variable}}` : `"\${${variable}}"`, range, sortText: SORT.variable, detail: 'Variable', }); } }; const pushStatic = (entries: Array<{ label: string; detail: string; insertText: string }>, kind: monaco.languages.CompletionItemKind, sortText: string) => { for (const entry of entries) { items.push(stringItem(entry.label, entry.insertText, entry.detail, kind, sortText, range)); } }; switch (situation.kind) { // ── Top level: field names + not + ( ── case 'NONE': case 'IN_FIELD': { await pushFieldNames(); if (situation.kind === 'NONE') { pushStatic([NOT_KEYWORD], monaco.languages.CompletionItemKind.Keyword, SORT.keyword); pushStatic([GROUP_OPEN], monaco.languages.CompletionItemKind.Keyword, SORT.keyword); } break; } // ── Field typed, offer : <= >= < > ── case 'AFTER_FIELD': { pushStatic(OPERATORS, monaco.languages.CompletionItemKind.Operator, SORT.operator); break; } // ── Value position: values + * + variables ── case 'IN_VALUE': { await pushFieldValues(situation.fieldName); pushStatic([EXISTS], monaco.languages.CompletionItemKind.Operator, SORT.operator); pushVariables(); break; } // ── Inside field:( ... ): values + variables + and/or/not ── case 'IN_VALUE_LIST': { await pushFieldValues(situation.fieldName); pushVariables(); pushStatic([...CONJUNCTIONS, NOT_KEYWORD], monaco.languages.CompletionItemKind.Keyword, SORT.keyword); break; } // ── Complete expression: and/or ── case 'AFTER_EXPRESSION': { pushStatic(CONJUNCTIONS, monaco.languages.CompletionItemKind.Keyword, SORT.keyword); break; } // ── Inside path:{ }: sub fields + not + ( ── case 'IN_NESTED': { await pushFieldNames(situation.nestedPath); pushStatic([NOT_KEYWORD], monaco.languages.CompletionItemKind.Keyword, SORT.keyword); pushStatic([GROUP_OPEN], monaco.languages.CompletionItemKind.Keyword, SORT.keyword); break; } } return items; }