// import UFuzzy from '@leeoniya/ufuzzy'; import { FUNCTIONS } from '../promql'; import { makeSelector, NeverCaseError } from '../util'; import type { DataProvider } from './DataProvider'; import type { Situation } from './situation'; import type { Label } from '../types'; export type CompletionType = 'HISTORY' | 'FUNCTION' | 'METRIC_NAME' | 'DURATION' | 'LABEL_NAME' | 'LABEL_VALUE'; type Completion = { type: CompletionType; label: string; insertText: string; detail?: string; documentation?: string; triggerOnInsert?: boolean; }; // const metricNamesSearchClient = new UFuzzy({ intraMode: 1 }); // we order items like: history, functions, metrics function getAllMetricNamesCompletions(dataProvider: DataProvider): Completion[] { let metricNames = dataProvider.getAllMetricNames(); // if (metricNames.length > dataProvider.metricNamesSuggestionLimit) { // const { monacoSettings } = dataProvider; // monacoSettings.enableAutocompleteSuggestionsUpdate(); // if (monacoSettings.inputInRange) { // metricNames = // metricNamesSearchClient // .filter(metricNames, monacoSettings.inputInRange) // ?.slice(0, dataProvider.metricNamesSuggestionLimit) // .map((idx) => metricNames[idx]) ?? []; // } else { // metricNames = metricNames.slice(0, dataProvider.metricNamesSuggestionLimit); // } // } return dataProvider.metricNamesToMetrics(metricNames).map((metric) => ({ type: 'METRIC_NAME', label: metric.name, insertText: metric.name, detail: `${metric.name} : ${metric.type}`, documentation: metric.help, })); } const FUNCTION_COMPLETIONS: Completion[] = FUNCTIONS.map((f) => ({ type: 'FUNCTION', label: f.label, insertText: f.insertText ?? '', // i don't know what to do when this is nullish. it should not be. detail: f.detail, documentation: f.documentation, })); async function getAllFunctionsAndMetricNamesCompletions(dataProvider: DataProvider): Promise { const metricNames = getAllMetricNamesCompletions(dataProvider); return [CTE_KEYWORD_COMPLETION, ...FUNCTION_COMPLETIONS, ...metricNames]; } const CTE_KEYWORD_COMPLETION: Completion = { type: 'FUNCTION', label: 'with', insertText: 'with (', detail: 'with (cte_name = expr, ...) expr', documentation: 'Define Common Table Expressions (CTEs) for use in the query. MetricsQL extension.', }; const DURATION_COMPLETIONS: Completion[] = ['1m', '5m', '10m', '30m', '1h', '1d'].map((text) => ({ type: 'DURATION', label: text, insertText: text, })); const DURATION_VARIABLES_COMPLETIONS: Completion[] = ['$__interval', '$__range', '$__rate_interval'].map((text) => ({ type: 'DURATION', label: text, insertText: text, })); function getAllHistoryCompletions(_dataProvider: DataProvider): Completion[] { return []; // function getAllHistoryCompletions(queryHistory: PromHistoryItem[]): Completion[] { // NOTE: the typescript types are wrong. historyItem.query.expr can be undefined // const allHistory = dataProvider.getHistory(); // FIXME: find a better history-limit // return allHistory.slice(0, 10).map((expr) => ({ // type: 'HISTORY', // label: expr, // insertText: expr, // })); } async function getLabelNames(metric: string | undefined, otherLabels: Label[], dataProvider: DataProvider): Promise { if (metric === undefined || metric === '') { const selector = makeSelector('', otherLabels); return await dataProvider.fetchLabels(selector); } else { const selector = makeSelector(metric, otherLabels); const series = await dataProvider.fetchSeries(selector); const labelNames = new Set(); for (const labelSet of series) { for (const [key] of Object.entries(labelSet)) { if (key === '__name__') { continue; } labelNames.add(key); } } return Array.from(labelNames); } } async function getLabelNamesForCompletions( metric: string | undefined, suffix: string, triggerOnInsert: boolean, otherLabels: Label[], dataProvider: DataProvider, ): Promise { const labelNames = await getLabelNames(metric, otherLabels, dataProvider); return labelNames.map((text) => ({ type: 'LABEL_NAME', label: text, insertText: `${text}${suffix}`, triggerOnInsert, })); } async function getLabelNamesForSelectorCompletions(metric: string | undefined, hasOperator: boolean, otherLabels: Label[], dataProvider: DataProvider): Promise { return getLabelNamesForCompletions(metric, hasOperator ? '' : '=', true, otherLabels, dataProvider); } async function getLabelNamesForByCompletions(metric: string | undefined, otherLabels: Label[], dataProvider: DataProvider): Promise { return getLabelNamesForCompletions(metric, '', false, otherLabels, dataProvider); } async function getLabelValues(metric: string | undefined, labelName: string, otherLabels: Label[], dataProvider: DataProvider): Promise { if (metric === undefined || metric === '') { const selector = makeSelector('', otherLabels); return await dataProvider.fetchLabelValues(labelName, selector); } else { const selector = makeSelector(metric, otherLabels, labelName); const series = await dataProvider.fetchSeries(selector); const labelValues = new Set(); for (const labelSet of series) { for (const [key, value] of Object.entries(labelSet)) { if (key === '__name__') { continue; } if (key === labelName && value) { labelValues.add(value); } } } const variablesNames = dataProvider.getVariablesNames(); return variablesNames.concat(Array.from(labelValues)); } } async function getLabelValuesForMetricCompletions( metric: string | undefined, labelName: string, betweenQuotes: boolean, otherLabels: Label[], dataProvider: DataProvider, ): Promise { const values = await getLabelValues(metric, labelName, otherLabels, dataProvider); return values.map((text) => ({ type: 'LABEL_VALUE', label: text, insertText: betweenQuotes ? text : `"${text}"`, // FIXME: escaping strange characters? })); } export function getCompletions(situation: Situation, dataProvider: DataProvider): Promise { switch (situation.type) { case 'IN_DURATION': if (dataProvider.durationVariablesCompletion) { return Promise.resolve(DURATION_VARIABLES_COMPLETIONS.concat(DURATION_COMPLETIONS)); } return Promise.resolve(DURATION_COMPLETIONS); case 'IN_FUNCTION': return getAllFunctionsAndMetricNamesCompletions(dataProvider); case 'IN_WITH_BODY': case 'AT_ROOT': { return getAllFunctionsAndMetricNamesCompletions(dataProvider); } case 'EMPTY': { const metricNames = getAllMetricNamesCompletions(dataProvider); const historyCompletions = getAllHistoryCompletions(dataProvider); return Promise.resolve([...historyCompletions, CTE_KEYWORD_COMPLETION, ...FUNCTION_COMPLETIONS, ...metricNames]); } case 'IN_LABEL_SELECTOR_NO_LABEL_NAME': return getLabelNamesForSelectorCompletions(situation.metricName, situation.hasOperator, situation.otherLabels, dataProvider); case 'IN_GROUPING': return getLabelNamesForByCompletions(situation.metricName, situation.otherLabels, dataProvider); case 'IN_LABEL_SELECTOR_WITH_LABEL_NAME': return getLabelValuesForMetricCompletions(situation.metricName, situation.labelName, situation.betweenQuotes, situation.otherLabels, dataProvider); default: throw new NeverCaseError(situation); } }