// 查询输入自动补全上下文检测 // Logan.Li import { COMMON_ES_FIELDS, ES_KEYWORD_SUGGESTIONS, LOKI_PIPE_SUGGESTIONS } from './query-syntax'; export interface AutocompleteContext { prefix: string; replaceStart: number; replaceEnd: number; kind: 'label' | 'value' | 'pipe' | 'field' | 'keyword'; labelName?: string; } const LOKI_SELECTOR_RE = /\{([^}]*)\}/g; export function detectAutocompleteContext( query: string, cursor: number, engine: 'loki' | 'elasticsearch', ): AutocompleteContext | null { const before = query.slice(0, cursor); if (engine === 'loki') { if (before.endsWith('|') || before.endsWith('| ')) { return { prefix: '', replaceStart: cursor, replaceEnd: cursor, kind: 'pipe' }; } const selectorMatch = [...query.matchAll(LOKI_SELECTOR_RE)].find((m) => { const start = m.index ?? 0; const end = start + m[0].length; return cursor >= start && cursor <= end; }); if (!selectorMatch) return null; const inner = selectorMatch[1]; const innerStart = (selectorMatch.index ?? 0) + 1; const rel = cursor - innerStart; const innerBefore = inner.slice(0, rel); const valueMatch = innerBefore.match(/([a-zA-Z_][\w.-]*)\s*(=~|!~|=|!=)\s*"?([^"]*)$/); if (valueMatch) { const labelName = valueMatch[1]; const valStart = innerStart + innerBefore.length - (valueMatch[3]?.length || 0); return { prefix: valueMatch[3] || '', replaceStart: valStart, replaceEnd: cursor, kind: 'value', labelName, }; } const labelMatch = innerBefore.match(/(?:^|,)\s*([a-zA-Z_][\w.-]*)?$/); if (labelMatch) { const partial = labelMatch[1] || ''; return { prefix: partial, replaceStart: cursor - partial.length, replaceEnd: cursor, kind: 'label', }; } return null; } const fieldMatch = before.match(/(?:^|\s)([a-zA-Z_@][\w.-]*)$/); if (fieldMatch) { const partial = fieldMatch[1]; return { prefix: partial, replaceStart: cursor - partial.length, replaceEnd: cursor, kind: 'field', }; } if (/\b(AND|OR|NOT)\s*$/i.test(before) || before.endsWith(' ')) { return { prefix: '', replaceStart: cursor, replaceEnd: cursor, kind: 'keyword' }; } return null; } export function filterSuggestions( ctx: AutocompleteContext, labels: string[], labelValues: string[], mappingFields: string[], ): string[] { const p = ctx.prefix.toLowerCase(); const match = (items: string[]) => items.filter((s) => s.toLowerCase().startsWith(p)).slice(0, 20); switch (ctx.kind) { case 'label': return match(labels); case 'value': return match(labelValues); case 'pipe': return LOKI_PIPE_SUGGESTIONS; case 'field': return match([...new Set([...mappingFields, ...COMMON_ES_FIELDS])]); case 'keyword': return ES_KEYWORD_SUGGESTIONS; default: return []; } } export function applySuggestion(query: string, ctx: AutocompleteContext, suggestion: string): { text: string; cursor: number } { const left = query.slice(0, ctx.replaceStart); const right = query.slice(ctx.replaceEnd); let insert = suggestion; if (ctx.kind === 'value') { insert = suggestion.includes(' ') ? `"${suggestion}"` : suggestion; } else if (ctx.kind === 'label') { insert = `${suggestion}=`; } else if (ctx.kind === 'field') { insert = `${suggestion}:`; } const text = left + insert + right; return { text, cursor: left.length + insert.length }; }