From d78642e67bef2482f1bae30324e944711e7a5820 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Thu, 24 Oct 2024 16:38:23 -0600 Subject: [PATCH 01/19] move sort routine to command definition --- .../src/autocomplete/autocomplete.ts | 412 ++---------------- .../src/autocomplete/commands/sort/helper.ts | 38 ++ .../src/autocomplete/complete_items.ts | 10 +- .../src/autocomplete/helper.ts | 213 ++++++++- .../src/definitions/commands.ts | 153 +++++++ .../src/definitions/helpers.ts | 7 +- .../src/definitions/types.ts | 13 +- .../src/shared/helpers.test.ts | 2 +- .../src/shared/helpers.ts | 19 +- .../esql_validation_meta_tests.json | 4 +- 10 files changed, 457 insertions(+), 414 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 98a26b0c8dd4b..b9775b5e3a93e 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -14,7 +14,6 @@ import type { ESQLCommand, ESQLCommandOption, ESQLFunction, - ESQLLiteral, ESQLSingleAstItem, } from '@kbn/esql-ast'; import { i18n } from '@kbn/i18n'; @@ -49,6 +48,7 @@ import { getColumnByName, sourceExists, findFinalWord, + getAllCommands, } from '../shared/helpers'; import { collectVariables, excludeVariablesFromCurrentCommand } from '../shared/variables'; import type { ESQLPolicy, ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types'; @@ -56,9 +56,9 @@ import { allStarConstant, colonCompleteItem, commaCompleteItem, - commandAutocompleteDefinitions, getAssignmentDefinitionCompletitionItem, getBuiltinCompatibleFunctionDefinition, + getCommandAutocompleteDefinitions, getNextTokenForNot, listCompleteItem, pipeCompleteItem, @@ -101,15 +101,12 @@ import { isAggFunctionUsedAlready, removeQuoteForSuggestedSources, getValidSignaturesAndTypesToSuggestNext, + handleFragment, + getFieldsOrFunctionsSuggestions, + pushItUpInTheList, + extractTypeFromASTArg, } from './helper'; -import { getSortPos } from './commands/sort/helper'; -import { - FunctionParameter, - FunctionReturnType, - SupportedDataType, - isParameterType, - isReturnType, -} from '../definitions/types'; +import { FunctionParameter, isParameterType, isReturnType } from '../definitions/types'; import { metadataOption } from '../definitions/options'; import { comparisonFunctions } from '../definitions/builtin'; import { countBracketsUnclosed } from '../shared/helpers'; @@ -181,7 +178,7 @@ export async function suggest( if (astContext.type === 'newCommand') { // propose main commands here // filter source commands if already defined - const suggestions = commandAutocompleteDefinitions; + const suggestions = getCommandAutocompleteDefinitions(getAllCommands()); if (!ast.length) { // Display the recommended queries if there are no commands (empty state) const recommendedQueriesSuggestions: SuggestionRawDefinition[] = []; @@ -389,43 +386,6 @@ function areCurrentArgsValid( return true; } -export function extractTypeFromASTArg( - arg: ESQLAstItem, - references: Pick -): - | ESQLLiteral['literalType'] - | SupportedDataType - | FunctionReturnType - | 'timeInterval' - | string // @TODO remove this - | undefined { - if (Array.isArray(arg)) { - return extractTypeFromASTArg(arg[0], references); - } - if (isColumnItem(arg) || isLiteralItem(arg)) { - if (isLiteralItem(arg)) { - return arg.literalType; - } - if (isColumnItem(arg)) { - const hit = getColumnForASTNode(arg, references); - if (hit) { - return hit.type; - } - } - } - if (isTimeIntervalItem(arg)) { - return arg.type; - } - if (isFunctionItem(arg)) { - const fnDef = getFunctionDefinition(arg.name); - if (fnDef) { - // @TODO: improve this to better filter down the correct return type based on existing arguments - // just mind that this can be highly recursive... - return fnDef.signatures[0].returnType; - } - } -} - // @TODO: refactor this to be shared with validation function isFunctionArgComplete( arg: ESQLFunction, @@ -505,6 +465,21 @@ async function getExpressionSuggestionsByType( const commandDef = getCommandDefinition(command.name); const { argIndex, prevIndex, lastArg, nodeArg } = extractArgMeta(command, node); + // collect all fields + variables to suggest + const fieldsMap: Map = await getFieldsMap(); + const anyVariables = collectVariables(commands, fieldsMap, innerText); + + const references = { fields: fieldsMap, variables: anyVariables }; + if (commandDef.suggest) { + return await commandDef.suggest(innerText, getFieldsByType, (col) => + Boolean(getColumnByName(col, references)) + ); + } + + if (!commandDef.signature || !commandDef.options) { + return []; + } + // TODO - this is a workaround because it was too difficult to handle this case in a generic way :( if (commandDef.name === 'from' && node && isSourceItem(node) && /\s/.test(node.name)) { // FROM " " @@ -537,7 +512,7 @@ async function getExpressionSuggestionsByType( command.args.filter((arg) => isOptionItem(arg)) as ESQLCommandOption[] ).map(({ name }) => ({ name, - index: commandDef.options.findIndex(({ name: defName }) => defName === name), + index: commandDef.options!.findIndex(({ name: defName }) => defName === name), })); const optionsAvailable = commandDef.options.filter(({ name }, index) => { const optArg = optionsAlreadyDeclared.find(({ name: optionName }) => optionName === name); @@ -577,23 +552,12 @@ async function getExpressionSuggestionsByType( } } - // collect all fields + variables to suggest - const fieldsMap: Map = await (argDef ? getFieldsMap() : new Map()); - const anyVariables = collectVariables(commands, fieldsMap, innerText); - const previousWord = findPreviousWord(innerText); // enrich with assignment has some special rules who are handled somewhere else const canHaveAssignments = ['eval', 'stats', 'row'].includes(command.name) && !comparisonFunctions.map((fn) => fn.name).includes(previousWord); - const references = { fields: fieldsMap, variables: anyVariables }; - if (command.name === 'sort') { - return await suggestForSortCmd(innerText, getFieldsByType, (col) => - Boolean(getColumnByName(col, references)) - ); - } - const suggestions: SuggestionRawDefinition[] = []; // When user types and accepts autocomplete suggestion, and cursor is placed at the end of a valid field @@ -1171,96 +1135,6 @@ async function getBuiltinFunctionNextArgument( }); } -function pushItUpInTheList(suggestions: SuggestionRawDefinition[], shouldPromote: boolean) { - if (!shouldPromote) { - return suggestions; - } - return suggestions.map(({ sortText, ...rest }) => ({ - ...rest, - sortText: `1${sortText}`, - })); -} - -/** - * TODO — split this into distinct functions, one for fields, one for functions, one for literals - */ -async function getFieldsOrFunctionsSuggestions( - types: string[], - commandName: string, - optionName: string | undefined, - getFieldsByType: GetFieldsByTypeFn, - { - functions, - fields, - variables, - literals = false, - }: { - functions: boolean; - fields: boolean; - variables?: Map; - literals?: boolean; - }, - { - ignoreFn = [], - ignoreColumns = [], - }: { - ignoreFn?: string[]; - ignoreColumns?: string[]; - } = {} -): Promise { - const filteredFieldsByType = pushItUpInTheList( - (await (fields - ? getFieldsByType(types, ignoreColumns, { - advanceCursor: commandName === 'sort', - openSuggestions: commandName === 'sort', - }) - : [])) as SuggestionRawDefinition[], - functions - ); - - const filteredVariablesByType: string[] = []; - if (variables) { - for (const variable of variables.values()) { - if ( - (types.includes('any') || types.includes(variable[0].type)) && - !ignoreColumns.includes(variable[0].name) - ) { - filteredVariablesByType.push(variable[0].name); - } - } - // due to a bug on the ES|QL table side, filter out fields list with underscored variable names (??) - // avg( numberField ) => avg_numberField_ - const ALPHANUMERIC_REGEXP = /[^a-zA-Z\d]/g; - if ( - filteredVariablesByType.length && - filteredVariablesByType.some((v) => ALPHANUMERIC_REGEXP.test(v)) - ) { - for (const variable of filteredVariablesByType) { - const underscoredName = variable.replace(ALPHANUMERIC_REGEXP, '_'); - const index = filteredFieldsByType.findIndex( - ({ label }) => underscoredName === label || `_${underscoredName}_` === label - ); - if (index >= 0) { - filteredFieldsByType.splice(index); - } - } - } - } - // could also be in stats (bucket) but our autocomplete is not great yet - const displayDateSuggestions = types.includes('date') && ['where', 'eval'].includes(commandName); - - const suggestions = filteredFieldsByType.concat( - displayDateSuggestions ? getDateLiterals() : [], - functions ? getCompatibleFunctionDefinition(commandName, optionName, types, ignoreFn) : [], - variables - ? pushItUpInTheList(buildVariablesDefinitions(filteredVariablesByType), functions) - : [], - literals ? getCompatibleLiterals(commandName, types) : [] - ); - - return suggestions; -} - const addCommaIf = (condition: boolean, text: string) => (condition ? `${text},` : text); async function getFunctionArgsSuggestions( @@ -1565,7 +1439,7 @@ async function getSettingArgsSuggestions( ) { const suggestions = []; - const settingDefs = getCommandDefinition(command.name).modes; + const settingDefs = getCommandDefinition(command.name).modes || []; if (settingDefs.length) { const lastChar = getLastCharFromTrimmed(innerText); @@ -1601,6 +1475,9 @@ async function getOptionArgsSuggestions( } const optionDef = getCommandOption(option.name); + if (!optionDef || !optionDef.signature) { + return []; + } const { nodeArg, argIndex, lastArg } = extractArgMeta(option, node); const suggestions = []; const isNewExpression = isRestartingExpression(innerText) || option.args.length === 0; @@ -1899,236 +1776,3 @@ async function getOptionArgsSuggestions( } return suggestions; } - -/** - * This function handles the logic to suggest completions - * for a given fragment of text in a generic way. A good example is - * a field name. - * - * When typing a field name, there are 2 scenarios - * - * 1. field name is incomplete (includes the empty string) - * KEEP / - * KEEP fie/ - * - * 2. field name is complete - * KEEP field/ - * - * This function provides a framework for detecting and handling both scenarios in a clean way. - * - * @param innerText - the query text before the current cursor position - * @param isFragmentComplete — return true if the fragment is complete - * @param getSuggestionsForIncomplete — gets suggestions for an incomplete fragment - * @param getSuggestionsForComplete - gets suggestions for a complete fragment - * @returns - */ -function handleFragment( - innerText: string, - isFragmentComplete: (fragment: string) => boolean, - getSuggestionsForIncomplete: ( - fragment: string, - rangeToReplace?: { start: number; end: number } - ) => SuggestionRawDefinition[] | Promise, - getSuggestionsForComplete: ( - fragment: string, - rangeToReplace: { start: number; end: number } - ) => SuggestionRawDefinition[] | Promise -): SuggestionRawDefinition[] | Promise { - /** - * @TODO — this string manipulation is crude and can't support all cases - * Checking for a partial word and computing the replacement range should - * really be done using the AST node, but we'll have to refactor further upstream - * to make that available. This is a quick fix to support the most common case. - */ - const fragment = findFinalWord(innerText); - if (!fragment) { - return getSuggestionsForIncomplete(''); - } else { - const rangeToReplace = { - start: innerText.length - fragment.length + 1, - end: innerText.length + 1, - }; - if (isFragmentComplete(fragment)) { - return getSuggestionsForComplete(fragment, rangeToReplace); - } else { - return getSuggestionsForIncomplete(fragment, rangeToReplace); - } - } -} - -const sortModifierSuggestions = { - ASC: { - label: 'ASC', - text: 'ASC', - detail: '', - kind: 'Keyword', - sortText: '1-ASC', - command: TRIGGER_SUGGESTION_COMMAND, - } as SuggestionRawDefinition, - DESC: { - label: 'DESC', - text: 'DESC', - detail: '', - kind: 'Keyword', - sortText: '1-DESC', - command: TRIGGER_SUGGESTION_COMMAND, - } as SuggestionRawDefinition, - NULLS_FIRST: { - label: 'NULLS FIRST', - text: 'NULLS FIRST', - detail: '', - kind: 'Keyword', - sortText: '2-NULLS FIRST', - command: TRIGGER_SUGGESTION_COMMAND, - } as SuggestionRawDefinition, - NULLS_LAST: { - label: 'NULLS LAST', - text: 'NULLS LAST', - detail: '', - kind: 'Keyword', - sortText: '2-NULLS LAST', - command: TRIGGER_SUGGESTION_COMMAND, - } as SuggestionRawDefinition, -}; - -export const suggestForSortCmd = async ( - innerText: string, - getFieldsByType: GetFieldsByTypeFn, - columnExists: (column: string) => boolean -): Promise => { - const prependSpace = (s: SuggestionRawDefinition) => ({ ...s, text: ' ' + s.text }); - - const { pos, nulls } = getSortPos(innerText); - - switch (pos) { - case 'space2': { - return [ - sortModifierSuggestions.ASC, - sortModifierSuggestions.DESC, - sortModifierSuggestions.NULLS_FIRST, - sortModifierSuggestions.NULLS_LAST, - pipeCompleteItem, - { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, - ]; - } - case 'order': { - return handleFragment( - innerText, - (fragment) => ['ASC', 'DESC'].some((completeWord) => noCaseCompare(completeWord, fragment)), - (_fragment, rangeToReplace) => { - return Object.values(sortModifierSuggestions).map((suggestion) => ({ - ...suggestion, - rangeToReplace, - })); - }, - (fragment, rangeToReplace) => { - return [ - { ...pipeCompleteItem, text: ' | ' }, - { ...commaCompleteItem, text: ', ' }, - prependSpace(sortModifierSuggestions.NULLS_FIRST), - prependSpace(sortModifierSuggestions.NULLS_LAST), - ].map((suggestion) => ({ - ...suggestion, - filterText: fragment, - text: fragment + suggestion.text, - rangeToReplace, - command: TRIGGER_SUGGESTION_COMMAND, - })); - } - ); - } - case 'space3': { - return [ - sortModifierSuggestions.NULLS_FIRST, - sortModifierSuggestions.NULLS_LAST, - pipeCompleteItem, - { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, - ]; - } - case 'nulls': { - return handleFragment( - innerText, - (fragment) => - ['FIRST', 'LAST'].some((completeWord) => noCaseCompare(completeWord, fragment)), - (_fragment) => { - const end = innerText.length + 1; - const start = end - nulls.length; - return Object.values(sortModifierSuggestions).map((suggestion) => ({ - ...suggestion, - // we can't use the range generated by handleFragment here - // because it doesn't really support multi-word completions - rangeToReplace: { start, end }, - })); - }, - (fragment, rangeToReplace) => { - return [ - { ...pipeCompleteItem, text: ' | ' }, - { ...commaCompleteItem, text: ', ' }, - ].map((suggestion) => ({ - ...suggestion, - filterText: fragment, - text: fragment + suggestion.text, - rangeToReplace, - command: TRIGGER_SUGGESTION_COMMAND, - })); - } - ); - } - case 'space4': { - return [ - pipeCompleteItem, - { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, - ]; - } - } - - const fieldSuggestions = await getFieldsByType('any', [], { - openSuggestions: true, - }); - const functionSuggestions = await getFieldsOrFunctionsSuggestions( - ['any'], - 'sort', - undefined, - getFieldsByType, - { - functions: true, - fields: false, - } - ); - - return await handleFragment( - innerText, - columnExists, - (_fragment: string, rangeToReplace?: { start: number; end: number }) => { - // SORT fie - return [ - ...pushItUpInTheList( - fieldSuggestions.map((suggestion) => ({ - ...suggestion, - command: TRIGGER_SUGGESTION_COMMAND, - rangeToReplace, - })), - true - ), - ...functionSuggestions, - ]; - }, - (fragment: string, rangeToReplace: { start: number; end: number }) => { - // SORT field - return [ - { ...pipeCompleteItem, text: ' | ' }, - { ...commaCompleteItem, text: ', ' }, - prependSpace(sortModifierSuggestions.ASC), - prependSpace(sortModifierSuggestions.DESC), - prependSpace(sortModifierSuggestions.NULLS_FIRST), - prependSpace(sortModifierSuggestions.NULLS_LAST), - ].map((s) => ({ - ...s, - filterText: fragment, - text: fragment + s.text, - command: TRIGGER_SUGGESTION_COMMAND, - rangeToReplace, - })); - } - ); -}; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/helper.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/helper.ts index 96546eff7d391..63dea06667cd8 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/helper.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/helper.ts @@ -7,6 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { TRIGGER_SUGGESTION_COMMAND } from '../../factories'; +import { SuggestionRawDefinition } from '../../types'; + const regexStart = /.+\|\s*so?r?(?t?)(.+,)?(?\s+)?/i; const regex = /.+\|\s*sort(.+,)?((?\s+)(?[^\s]+)(?\s*)(?(AS?C?)|(DE?S?C?))?(?\s*)(?NU?L?L?S? ?(FI?R?S?T?|LA?S?T?)?)?(?\s*))?/i; @@ -43,6 +46,41 @@ export interface SortCaretPosition { nulls: string; } +export const sortModifierSuggestions = { + ASC: { + label: 'ASC', + text: 'ASC', + detail: '', + kind: 'Keyword', + sortText: '1-ASC', + command: TRIGGER_SUGGESTION_COMMAND, + } as SuggestionRawDefinition, + DESC: { + label: 'DESC', + text: 'DESC', + detail: '', + kind: 'Keyword', + sortText: '1-DESC', + command: TRIGGER_SUGGESTION_COMMAND, + } as SuggestionRawDefinition, + NULLS_FIRST: { + label: 'NULLS FIRST', + text: 'NULLS FIRST', + detail: '', + kind: 'Keyword', + sortText: '2-NULLS FIRST', + command: TRIGGER_SUGGESTION_COMMAND, + } as SuggestionRawDefinition, + NULLS_LAST: { + label: 'NULLS LAST', + text: 'NULLS LAST', + detail: '', + kind: 'Keyword', + sortText: '2-NULLS LAST', + command: TRIGGER_SUGGESTION_COMMAND, + } as SuggestionRawDefinition, +}; + export const getSortPos = (query: string): SortCaretPosition => { const match = query.match(regex); let pos: SortCaretPosition['pos'] = 'none'; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts index 000c196b49e5e..5da9da2fe7653 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts @@ -10,14 +10,13 @@ import { i18n } from '@kbn/i18n'; import type { ItemKind, SuggestionRawDefinition } from './types'; import { builtinFunctions } from '../definitions/builtin'; -import { getAllCommands } from '../shared/helpers'; import { getSuggestionBuiltinDefinition, getSuggestionCommandDefinition, TRIGGER_SUGGESTION_COMMAND, buildConstantsDefinitions, } from './factories'; -import { FunctionParameterType, FunctionReturnType } from '../definitions/types'; +import { CommandDefinition, FunctionParameterType, FunctionReturnType } from '../definitions/types'; import { getTestFunctions } from '../shared/test_functions'; export function getAssignmentDefinitionCompletitionItem() { @@ -87,9 +86,10 @@ export const getBuiltinCompatibleFunctionDefinition = ( .map(getSuggestionBuiltinDefinition); }; -export const commandAutocompleteDefinitions: SuggestionRawDefinition[] = getAllCommands() - .filter(({ hidden }) => !hidden) - .map(getSuggestionCommandDefinition); +export const getCommandAutocompleteDefinitions = ( + commands: CommandDefinition[] +): SuggestionRawDefinition[] => + commands.filter(({ hidden }) => !hidden).map(getSuggestionCommandDefinition); function buildCharCompleteItem( label: string, diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts index dd450e28b66a9..42f92aed806a7 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts @@ -7,21 +7,40 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { ESQLAstItem, ESQLCommand, ESQLFunction, ESQLSource } from '@kbn/esql-ast'; +import type { + ESQLAstItem, + ESQLCommand, + ESQLFunction, + ESQLLiteral, + ESQLSource, +} from '@kbn/esql-ast'; import { uniqBy } from 'lodash'; -import type { FunctionDefinition } from '../definitions/types'; +import type { + FunctionDefinition, + FunctionReturnType, + SupportedDataType, +} from '../definitions/types'; import { + findFinalWord, + getColumnForASTNode, getFunctionDefinition, isAssignment, + isColumnItem, isFunctionItem, isLiteralItem, + isTimeIntervalItem, } from '../shared/helpers'; -import type { SuggestionRawDefinition } from './types'; +import type { GetFieldsByTypeFn, SuggestionRawDefinition } from './types'; import { compareTypesWithLiterals } from '../shared/esql_types'; -import { TIME_SYSTEM_PARAMS } from './factories'; +import { + TIME_SYSTEM_PARAMS, + buildVariablesDefinitions, + getCompatibleFunctionDefinition, + getCompatibleLiterals, + getDateLiterals, +} from './factories'; import { EDITOR_MARKER } from '../shared/constants'; -import { extractTypeFromASTArg } from './autocomplete'; -import { ESQLRealField, ESQLVariable } from '../validation/types'; +import { ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types'; function extractFunctionArgs(args: ESQLAstItem[]): ESQLFunction[] { return args.flatMap((arg) => (isAssignment(arg) ? arg.args[1] : arg)).filter(isFunctionItem); @@ -272,3 +291,185 @@ export function getValidSignaturesAndTypesToSuggestNext( currentArg, }; } + +/** + * This function handles the logic to suggest completions + * for a given fragment of text in a generic way. A good example is + * a field name. + * + * When typing a field name, there are 2 scenarios + * + * 1. field name is incomplete (includes the empty string) + * KEEP / + * KEEP fie/ + * + * 2. field name is complete + * KEEP field/ + * + * This function provides a framework for detecting and handling both scenarios in a clean way. + * + * @param innerText - the query text before the current cursor position + * @param isFragmentComplete — return true if the fragment is complete + * @param getSuggestionsForIncomplete — gets suggestions for an incomplete fragment + * @param getSuggestionsForComplete - gets suggestions for a complete fragment + * @returns + */ +export function handleFragment( + innerText: string, + isFragmentComplete: (fragment: string) => boolean, + getSuggestionsForIncomplete: ( + fragment: string, + rangeToReplace?: { start: number; end: number } + ) => SuggestionRawDefinition[] | Promise, + getSuggestionsForComplete: ( + fragment: string, + rangeToReplace: { start: number; end: number } + ) => SuggestionRawDefinition[] | Promise +): SuggestionRawDefinition[] | Promise { + /** + * @TODO — this string manipulation is crude and can't support all cases + * Checking for a partial word and computing the replacement range should + * really be done using the AST node, but we'll have to refactor further upstream + * to make that available. This is a quick fix to support the most common case. + */ + const fragment = findFinalWord(innerText); + if (!fragment) { + return getSuggestionsForIncomplete(''); + } else { + const rangeToReplace = { + start: innerText.length - fragment.length + 1, + end: innerText.length + 1, + }; + if (isFragmentComplete(fragment)) { + return getSuggestionsForComplete(fragment, rangeToReplace); + } else { + return getSuggestionsForIncomplete(fragment, rangeToReplace); + } + } +} +/** + * TODO — split this into distinct functions, one for fields, one for functions, one for literals + */ +export async function getFieldsOrFunctionsSuggestions( + types: string[], + commandName: string, + optionName: string | undefined, + getFieldsByType: GetFieldsByTypeFn, + { + functions, + fields, + variables, + literals = false, + }: { + functions: boolean; + fields: boolean; + variables?: Map; + literals?: boolean; + }, + { + ignoreFn = [], + ignoreColumns = [], + }: { + ignoreFn?: string[]; + ignoreColumns?: string[]; + } = {} +): Promise { + const filteredFieldsByType = pushItUpInTheList( + (await (fields + ? getFieldsByType(types, ignoreColumns, { + advanceCursor: commandName === 'sort', + openSuggestions: commandName === 'sort', + }) + : [])) as SuggestionRawDefinition[], + functions + ); + + const filteredVariablesByType: string[] = []; + if (variables) { + for (const variable of variables.values()) { + if ( + (types.includes('any') || types.includes(variable[0].type)) && + !ignoreColumns.includes(variable[0].name) + ) { + filteredVariablesByType.push(variable[0].name); + } + } + // due to a bug on the ES|QL table side, filter out fields list with underscored variable names (??) + // avg( numberField ) => avg_numberField_ + const ALPHANUMERIC_REGEXP = /[^a-zA-Z\d]/g; + if ( + filteredVariablesByType.length && + filteredVariablesByType.some((v) => ALPHANUMERIC_REGEXP.test(v)) + ) { + for (const variable of filteredVariablesByType) { + const underscoredName = variable.replace(ALPHANUMERIC_REGEXP, '_'); + const index = filteredFieldsByType.findIndex( + ({ label }) => underscoredName === label || `_${underscoredName}_` === label + ); + if (index >= 0) { + filteredFieldsByType.splice(index); + } + } + } + } + // could also be in stats (bucket) but our autocomplete is not great yet + const displayDateSuggestions = types.includes('date') && ['where', 'eval'].includes(commandName); + + const suggestions = filteredFieldsByType.concat( + displayDateSuggestions ? getDateLiterals() : [], + functions ? getCompatibleFunctionDefinition(commandName, optionName, types, ignoreFn) : [], + variables + ? pushItUpInTheList(buildVariablesDefinitions(filteredVariablesByType), functions) + : [], + literals ? getCompatibleLiterals(commandName, types) : [] + ); + + return suggestions; +} + +export function pushItUpInTheList(suggestions: SuggestionRawDefinition[], shouldPromote: boolean) { + if (!shouldPromote) { + return suggestions; + } + return suggestions.map(({ sortText, ...rest }) => ({ + ...rest, + sortText: `1${sortText}`, + })); +} + +export function extractTypeFromASTArg( + arg: ESQLAstItem, + references: Pick +): + | ESQLLiteral['literalType'] + | SupportedDataType + | FunctionReturnType + | 'timeInterval' + | string // @TODO remove this + | undefined { + if (Array.isArray(arg)) { + return extractTypeFromASTArg(arg[0], references); + } + if (isColumnItem(arg) || isLiteralItem(arg)) { + if (isLiteralItem(arg)) { + return arg.literalType; + } + if (isColumnItem(arg)) { + const hit = getColumnForASTNode(arg, references); + if (hit) { + return hit.type; + } + } + } + if (isTimeIntervalItem(arg)) { + return arg.type; + } + if (isFunctionItem(arg)) { + const fnDef = getFunctionDefinition(arg.name); + if (fnDef) { + // @TODO: improve this to better filter down the correct return type based on existing arguments + // just mind that this can be highly recursive... + return fnDef.signatures[0].returnType; + } + } +} diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts index 54504ac1a2a18..b46466ae8f852 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts @@ -21,6 +21,7 @@ import { isColumnItem, isFunctionItem, isLiteralItem, + noCaseCompare, } from '../shared/helpers'; import { ENRICH_MODES } from './settings'; import { @@ -32,6 +33,15 @@ import { withOption, } from './options'; import type { CommandDefinition } from './types'; +import type { GetFieldsByTypeFn, SuggestionRawDefinition } from '../autocomplete/types'; +import { getSortPos, sortModifierSuggestions } from '../autocomplete/commands/sort/helper'; +import { commaCompleteItem, pipeCompleteItem } from '../autocomplete/complete_items'; +import { TRIGGER_SUGGESTION_COMMAND } from '../autocomplete/factories'; +import { + getFieldsOrFunctionsSuggestions, + handleFragment, + pushItUpInTheList, +} from '../autocomplete/helper'; const statsValidator = (command: ESQLCommand) => { const messages: ESQLMessage[] = []; @@ -386,7 +396,150 @@ export const commandDefinitions: CommandDefinition[] = [ multipleParams: true, params: [{ name: 'expression', type: 'any' }], }, + suggest: async ( + innerText: string, + getFieldsByType: GetFieldsByTypeFn, + columnExists: (column: string) => boolean + ): Promise => { + const prependSpace = (s: SuggestionRawDefinition) => ({ ...s, text: ' ' + s.text }); + + const { pos, nulls } = getSortPos(innerText); + + switch (pos) { + case 'space2': { + return [ + sortModifierSuggestions.ASC, + sortModifierSuggestions.DESC, + sortModifierSuggestions.NULLS_FIRST, + sortModifierSuggestions.NULLS_LAST, + pipeCompleteItem, + { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, + ]; + } + case 'order': { + return handleFragment( + innerText, + (fragment) => + ['ASC', 'DESC'].some((completeWord) => noCaseCompare(completeWord, fragment)), + (_fragment, rangeToReplace) => { + return Object.values(sortModifierSuggestions).map((suggestion) => ({ + ...suggestion, + rangeToReplace, + })); + }, + (fragment, rangeToReplace) => { + return [ + { ...pipeCompleteItem, text: ' | ' }, + { ...commaCompleteItem, text: ', ' }, + prependSpace(sortModifierSuggestions.NULLS_FIRST), + prependSpace(sortModifierSuggestions.NULLS_LAST), + ].map((suggestion) => ({ + ...suggestion, + filterText: fragment, + text: fragment + suggestion.text, + rangeToReplace, + command: TRIGGER_SUGGESTION_COMMAND, + })); + } + ); + } + case 'space3': { + return [ + sortModifierSuggestions.NULLS_FIRST, + sortModifierSuggestions.NULLS_LAST, + pipeCompleteItem, + { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, + ]; + } + case 'nulls': { + return handleFragment( + innerText, + (fragment) => + ['FIRST', 'LAST'].some((completeWord) => noCaseCompare(completeWord, fragment)), + (_fragment) => { + const end = innerText.length + 1; + const start = end - nulls.length; + return Object.values(sortModifierSuggestions).map((suggestion) => ({ + ...suggestion, + // we can't use the range generated by handleFragment here + // because it doesn't really support multi-word completions + rangeToReplace: { start, end }, + })); + }, + (fragment, rangeToReplace) => { + return [ + { ...pipeCompleteItem, text: ' | ' }, + { ...commaCompleteItem, text: ', ' }, + ].map((suggestion) => ({ + ...suggestion, + filterText: fragment, + text: fragment + suggestion.text, + rangeToReplace, + command: TRIGGER_SUGGESTION_COMMAND, + })); + } + ); + } + case 'space4': { + return [ + pipeCompleteItem, + { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, + ]; + } + } + + const fieldSuggestions = await getFieldsByType('any', [], { + openSuggestions: true, + }); + const functionSuggestions = await getFieldsOrFunctionsSuggestions( + ['any'], + 'sort', + undefined, + getFieldsByType, + { + functions: true, + fields: false, + } + ); + + return await handleFragment( + innerText, + columnExists, + (_fragment: string, rangeToReplace?: { start: number; end: number }) => { + // SORT fie + return [ + ...pushItUpInTheList( + fieldSuggestions.map((suggestion) => ({ + ...suggestion, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })), + true + ), + ...functionSuggestions, + ]; + }, + (fragment: string, rangeToReplace: { start: number; end: number }) => { + // SORT field + return [ + { ...pipeCompleteItem, text: ' | ' }, + { ...commaCompleteItem, text: ', ' }, + prependSpace(sortModifierSuggestions.ASC), + prependSpace(sortModifierSuggestions.DESC), + prependSpace(sortModifierSuggestions.NULLS_FIRST), + prependSpace(sortModifierSuggestions.NULLS_LAST), + ].map((s) => ({ + ...s, + filterText: fragment, + text: fragment + s.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + } + ); + }, }, + { name: 'where', description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.whereDoc', { diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts index 867c68ab4f1df..c1df4a6cb16e8 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts @@ -60,10 +60,9 @@ export function getCommandSignature( { withTypes }: { withTypes: boolean } = { withTypes: true } ) { return { - declaration: `${name.toUpperCase()} ${printCommandArguments( - signature, - withTypes - )} ${options.map( + declaration: `${name.toUpperCase()} ${printCommandArguments(signature, withTypes)} ${( + options || [] + ).map( (option) => `${ option.wrapped ? option.wrapped[0] : '' diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts index dee08766745df..5b2b748ef1c80 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts @@ -8,6 +8,7 @@ */ import type { ESQLCommand, ESQLCommandOption, ESQLFunction, ESQLMessage } from '@kbn/esql-ast'; +import { GetFieldsByTypeFn, SuggestionRawDefinition } from '../autocomplete/types'; /** * All supported field types in ES|QL. This is all the types @@ -166,6 +167,12 @@ export interface CommandBaseDefinition { * Whether to show or hide in autocomplete suggestion list */ hidden?: boolean; + suggest?: ( + innerText: string, + getFieldsByType: GetFieldsByTypeFn, + columnExists: (column: string) => boolean + ) => Promise; + /** @deprecated this property will disappear in the future */ signature: { multipleParams: boolean; // innerTypes here is useful to drill down the type in case of "column" @@ -202,10 +209,12 @@ export interface CommandModeDefinition { } export interface CommandDefinition extends CommandBaseDefinition { - options: CommandOptionsDefinition[]; + /** @deprecated this property will disappear in the future */ + options?: CommandOptionsDefinition[]; examples: string[]; validate?: (option: ESQLCommand) => ESQLMessage[]; - modes: CommandModeDefinition[]; + /** @deprecated this property will disappear in the future */ + modes?: CommandModeDefinition[]; hasRecommendedQueries?: boolean; } diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.test.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.test.ts index 0078e0fac119c..07e6a231ed61f 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.test.ts @@ -9,7 +9,7 @@ import { parse } from '@kbn/esql-ast'; import { getExpressionType, shouldBeQuotedSource } from './helpers'; -import { SupportedDataType } from '../definitions/types'; +import type { SupportedDataType } from '../definitions/types'; import { setTestFunctions } from './test_functions'; describe('shouldBeQuotedSource', () => { diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts index 2392a44814997..0d88cd22d157b 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts @@ -132,7 +132,7 @@ export function isSourceCommand({ label }: { label: string }) { } let fnLookups: Map | undefined; -let commandLookups: Map | undefined; +// let commandLookups: Map | undefined; function buildFunctionLookup() { // we always refresh if we have test functions @@ -198,15 +198,14 @@ export function getFunctionDefinition(name: string) { const unwrapStringLiteralQuotes = (value: string) => value.slice(1, -1); function buildCommandLookup() { - if (!commandLookups) { - commandLookups = commandDefinitions.reduce((memo, def) => { - memo.set(def.name, def); - if (def.alias) { - memo.set(def.alias, def); - } - return memo; - }, new Map()); - } + // if (!commandLookups) { + const commandLookups = commandDefinitions.reduce((memo, def) => { + memo.set(def.name, def); + if (def.alias) { + memo.set(def.alias, def); + } + return memo; + }, new Map()); return commandLookups; } diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json index c66aaadf98df8..f1e71c9ff6a97 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json +++ b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json @@ -9976,7 +9976,7 @@ { "query": "from index [METADATA _id, _source2]", "error": [ - "Metadata field [_source2] is not available. Available metadata fields are: [_version, _id, _index, _source, _ignored]" + "Metadata field [_source2] is not available. Available metadata fields are: [_version, _id, _index, _source, _ignored, _index_mode]" ], "warning": [ "Square brackets '[]' need to be removed from FROM METADATA declaration" @@ -10014,7 +10014,7 @@ { "query": "from index METADATA _id, _source2", "error": [ - "Metadata field [_source2] is not available. Available metadata fields are: [_version, _id, _index, _source, _ignored]" + "Metadata field [_source2] is not available. Available metadata fields are: [_version, _id, _index, _source, _ignored, _index_mode]" ], "warning": [] }, From 9bbd6f65682fd08f7eb5673726b1a4bacb3fd6a3 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Fri, 25 Oct 2024 09:04:17 -0600 Subject: [PATCH 02/19] make the separation between the legacy and new paths clearer --- .../src/autocomplete/autocomplete.ts | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index b9775b5e3a93e..6f9ccf89d3d7e 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -208,7 +208,7 @@ export async function suggest( if (astContext.type === 'expression') { // suggest next possible argument, or option // otherwise a variable - return getExpressionSuggestionsByType( + return getSuggestionsWithinCommand( innerText, ast, astContext, @@ -444,7 +444,7 @@ function extractArgMeta( return { argIndex, prevIndex, lastArg, nodeArg }; } -async function getExpressionSuggestionsByType( +async function getSuggestionsWithinCommand( innerText: string, commands: ESQLCommand[], { @@ -463,7 +463,6 @@ async function getExpressionSuggestionsByType( getPolicyMetadata: GetPolicyMetadataFn ) { const commandDef = getCommandDefinition(command.name); - const { argIndex, prevIndex, lastArg, nodeArg } = extractArgMeta(command, node); // collect all fields + variables to suggest const fieldsMap: Map = await getFieldsMap(); @@ -471,11 +470,55 @@ async function getExpressionSuggestionsByType( const references = { fields: fieldsMap, variables: anyVariables }; if (commandDef.suggest) { - return await commandDef.suggest(innerText, getFieldsByType, (col) => + // The new path. + return commandDef.suggest(innerText, getFieldsByType, (col) => Boolean(getColumnByName(col, references)) ); + } else { + // The deprectated path. + return getExpressionSuggestionsByType( + innerText, + commands, + { command, option, node }, + getSources, + getFieldsByType, + getFieldsMap, + getPolicies, + getPolicyMetadata + ); } +} + +/** + * @deprecated — this generic logic will be replaced with the command-specific suggest functions + * from each command definition. + */ +async function getExpressionSuggestionsByType( + innerText: string, + commands: ESQLCommand[], + { + command, + option, + node, + }: { + command: ESQLCommand; + option: ESQLCommandOption | undefined; + node: ESQLSingleAstItem | undefined; + }, + getSources: () => Promise, + getFieldsByType: GetFieldsByTypeFn, + getFieldsMap: GetFieldsMapFn, + getPolicies: GetPoliciesFn, + getPolicyMetadata: GetPolicyMetadataFn +) { + const commandDef = getCommandDefinition(command.name); + const { argIndex, prevIndex, lastArg, nodeArg } = extractArgMeta(command, node); + // collect all fields + variables to suggest + const fieldsMap: Map = await getFieldsMap(); + const anyVariables = collectVariables(commands, fieldsMap, innerText); + + const references = { fields: fieldsMap, variables: anyVariables }; if (!commandDef.signature || !commandDef.options) { return []; } From 3271c792a639c02c28d54f0fd574691fa4e5aa38 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Fri, 25 Oct 2024 09:28:13 -0600 Subject: [PATCH 03/19] keep --- .../src/autocomplete/autocomplete.ts | 2 +- .../src/definitions/commands.ts | 42 +++++++++++++++++++ .../src/shared/helpers.ts | 19 +++++---- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 6f9ccf89d3d7e..b5b28c011d8da 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -475,7 +475,7 @@ async function getSuggestionsWithinCommand( Boolean(getColumnByName(col, references)) ); } else { - // The deprectated path. + // The deprecated path. return getExpressionSuggestionsByType( innerText, commands, diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts index b46466ae8f852..3ed5110746375 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts @@ -16,6 +16,7 @@ import type { ESQLFunction, } from '@kbn/esql-ast'; import { + findPreviousWord, getFunctionDefinition, isAssignment, isColumnItem, @@ -321,6 +322,47 @@ export const commandDefinitions: CommandDefinition[] = [ defaultMessage: 'Rearranges fields in the input table by applying the keep clauses in fields', }), examples: ['… | keep a', '… | keep a,b'], + suggest: async (innerText, getFieldsByType, getColumnByName) => { + if ( + /\s/.test(innerText[innerText.length - 1]) && + !noCaseCompare(findPreviousWord(innerText), 'keep') + ) { + return [pipeCompleteItem, commaCompleteItem]; + } + + const fieldSuggestions = await getFieldsByType('any', [], {}); + return handleFragment( + innerText, + (fragment) => Boolean(getColumnByName(fragment)), + (_fragment: string, rangeToReplace?: { start: number; end: number }) => { + // KEEP fie + return fieldSuggestions.map((suggestion) => ({ + ...suggestion, + text: suggestion.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + }, + (fragment: string, rangeToReplace: { start: number; end: number }) => { + // KEEP field + const finalSuggestions = [{ ...pipeCompleteItem, text: ' | ' }]; + if (fieldSuggestions.length > 1) + // when we fix the editor marker, this should probably be checked against 0 instead of 1 + // this is because the last field in the AST is currently getting removed (because it contains + // the editor marker) so it is not included in the ignored list which is used to filter out + // existing fields above. + finalSuggestions.push({ ...commaCompleteItem, text: ', ' }); + + return finalSuggestions.map((s) => ({ + ...s, + filterText: fragment, + text: fragment + s.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + } + ); + }, options: [], modes: [], signature: { diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts index 0d88cd22d157b..2392a44814997 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts @@ -132,7 +132,7 @@ export function isSourceCommand({ label }: { label: string }) { } let fnLookups: Map | undefined; -// let commandLookups: Map | undefined; +let commandLookups: Map | undefined; function buildFunctionLookup() { // we always refresh if we have test functions @@ -198,14 +198,15 @@ export function getFunctionDefinition(name: string) { const unwrapStringLiteralQuotes = (value: string) => value.slice(1, -1); function buildCommandLookup() { - // if (!commandLookups) { - const commandLookups = commandDefinitions.reduce((memo, def) => { - memo.set(def.name, def); - if (def.alias) { - memo.set(def.alias, def); - } - return memo; - }, new Map()); + if (!commandLookups) { + commandLookups = commandDefinitions.reduce((memo, def) => { + memo.set(def.name, def); + if (def.alias) { + memo.set(def.alias, def); + } + return memo; + }, new Map()); + } return commandLookups; } From 1723f4d8060263f15059c2cec70a1d589f36338c Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Fri, 25 Oct 2024 09:39:14 -0600 Subject: [PATCH 04/19] separate command autocomplete routines --- .../src/autocomplete/commands/keep/index.ts | 60 ++++++ .../src/autocomplete/commands/sort/index.ts | 157 ++++++++++++++ .../src/definitions/commands.ts | 198 +----------------- 3 files changed, 221 insertions(+), 194 deletions(-) create mode 100644 packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts create mode 100644 packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/index.ts diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts new file mode 100644 index 0000000000000..079d5079b00de --- /dev/null +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { findPreviousWord, noCaseCompare } from '../../../shared/helpers'; +import { GetFieldsByTypeFn, SuggestionRawDefinition } from '../../types'; +import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; +import { handleFragment } from '../../helper'; +import { TRIGGER_SUGGESTION_COMMAND } from '../../factories'; + +export async function suggest( + innerText: string, + getFieldsByType: GetFieldsByTypeFn, + columnExists: (column: string) => boolean +): Promise { + if ( + /\s/.test(innerText[innerText.length - 1]) && + !noCaseCompare(findPreviousWord(innerText), 'keep') + ) { + return [pipeCompleteItem, commaCompleteItem]; + } + + const fieldSuggestions = await getFieldsByType('any', [], {}); + return handleFragment( + innerText, + (fragment) => columnExists(fragment), + (_fragment: string, rangeToReplace?: { start: number; end: number }) => { + // KEEP fie + return fieldSuggestions.map((suggestion) => ({ + ...suggestion, + text: suggestion.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + }, + (fragment: string, rangeToReplace: { start: number; end: number }) => { + // KEEP field + const finalSuggestions = [{ ...pipeCompleteItem, text: ' | ' }]; + if (fieldSuggestions.length > 1) + // when we fix the editor marker, this should probably be checked against 0 instead of 1 + // this is because the last field in the AST is currently getting removed (because it contains + // the editor marker) so it is not included in the ignored list which is used to filter out + // existing fields above. + finalSuggestions.push({ ...commaCompleteItem, text: ', ' }); + + return finalSuggestions.map((s) => ({ + ...s, + filterText: fragment, + text: fragment + s.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + } + ); +} diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/index.ts new file mode 100644 index 0000000000000..4abba12919d2d --- /dev/null +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/index.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { noCaseCompare } from '../../../shared/helpers'; +import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; +import { TRIGGER_SUGGESTION_COMMAND } from '../../factories'; +import { getFieldsOrFunctionsSuggestions, handleFragment, pushItUpInTheList } from '../../helper'; +import { GetFieldsByTypeFn, SuggestionRawDefinition } from '../../types'; +import { getSortPos, sortModifierSuggestions } from './helper'; + +export async function suggest( + innerText: string, + getFieldsByType: GetFieldsByTypeFn, + columnExists: (column: string) => boolean +): Promise { + const prependSpace = (s: SuggestionRawDefinition) => ({ ...s, text: ' ' + s.text }); + + const { pos, nulls } = getSortPos(innerText); + + switch (pos) { + case 'space2': { + return [ + sortModifierSuggestions.ASC, + sortModifierSuggestions.DESC, + sortModifierSuggestions.NULLS_FIRST, + sortModifierSuggestions.NULLS_LAST, + pipeCompleteItem, + { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, + ]; + } + case 'order': { + return handleFragment( + innerText, + (fragment) => ['ASC', 'DESC'].some((completeWord) => noCaseCompare(completeWord, fragment)), + (_fragment, rangeToReplace) => { + return Object.values(sortModifierSuggestions).map((suggestion) => ({ + ...suggestion, + rangeToReplace, + })); + }, + (fragment, rangeToReplace) => { + return [ + { ...pipeCompleteItem, text: ' | ' }, + { ...commaCompleteItem, text: ', ' }, + prependSpace(sortModifierSuggestions.NULLS_FIRST), + prependSpace(sortModifierSuggestions.NULLS_LAST), + ].map((suggestion) => ({ + ...suggestion, + filterText: fragment, + text: fragment + suggestion.text, + rangeToReplace, + command: TRIGGER_SUGGESTION_COMMAND, + })); + } + ); + } + case 'space3': { + return [ + sortModifierSuggestions.NULLS_FIRST, + sortModifierSuggestions.NULLS_LAST, + pipeCompleteItem, + { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, + ]; + } + case 'nulls': { + return handleFragment( + innerText, + (fragment) => + ['FIRST', 'LAST'].some((completeWord) => noCaseCompare(completeWord, fragment)), + (_fragment) => { + const end = innerText.length + 1; + const start = end - nulls.length; + return Object.values(sortModifierSuggestions).map((suggestion) => ({ + ...suggestion, + // we can't use the range generated by handleFragment here + // because it doesn't really support multi-word completions + rangeToReplace: { start, end }, + })); + }, + (fragment, rangeToReplace) => { + return [ + { ...pipeCompleteItem, text: ' | ' }, + { ...commaCompleteItem, text: ', ' }, + ].map((suggestion) => ({ + ...suggestion, + filterText: fragment, + text: fragment + suggestion.text, + rangeToReplace, + command: TRIGGER_SUGGESTION_COMMAND, + })); + } + ); + } + case 'space4': { + return [ + pipeCompleteItem, + { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, + ]; + } + } + + const fieldSuggestions = await getFieldsByType('any', [], { + openSuggestions: true, + }); + const functionSuggestions = await getFieldsOrFunctionsSuggestions( + ['any'], + 'sort', + undefined, + getFieldsByType, + { + functions: true, + fields: false, + } + ); + + return await handleFragment( + innerText, + columnExists, + (_fragment: string, rangeToReplace?: { start: number; end: number }) => { + // SORT fie + return [ + ...pushItUpInTheList( + fieldSuggestions.map((suggestion) => ({ + ...suggestion, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })), + true + ), + ...functionSuggestions, + ]; + }, + (fragment: string, rangeToReplace: { start: number; end: number }) => { + // SORT field + return [ + { ...pipeCompleteItem, text: ' | ' }, + { ...commaCompleteItem, text: ', ' }, + prependSpace(sortModifierSuggestions.ASC), + prependSpace(sortModifierSuggestions.DESC), + prependSpace(sortModifierSuggestions.NULLS_FIRST), + prependSpace(sortModifierSuggestions.NULLS_LAST), + ].map((s) => ({ + ...s, + filterText: fragment, + text: fragment + s.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + } + ); +} diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts index 3ed5110746375..d2667c0b15b64 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts @@ -16,13 +16,11 @@ import type { ESQLFunction, } from '@kbn/esql-ast'; import { - findPreviousWord, getFunctionDefinition, isAssignment, isColumnItem, isFunctionItem, isLiteralItem, - noCaseCompare, } from '../shared/helpers'; import { ENRICH_MODES } from './settings'; import { @@ -34,15 +32,8 @@ import { withOption, } from './options'; import type { CommandDefinition } from './types'; -import type { GetFieldsByTypeFn, SuggestionRawDefinition } from '../autocomplete/types'; -import { getSortPos, sortModifierSuggestions } from '../autocomplete/commands/sort/helper'; -import { commaCompleteItem, pipeCompleteItem } from '../autocomplete/complete_items'; -import { TRIGGER_SUGGESTION_COMMAND } from '../autocomplete/factories'; -import { - getFieldsOrFunctionsSuggestions, - handleFragment, - pushItUpInTheList, -} from '../autocomplete/helper'; +import { suggest as suggestForSort } from '../autocomplete/commands/sort'; +import { suggest as suggestForKeep } from '../autocomplete/commands/keep'; const statsValidator = (command: ESQLCommand) => { const messages: ESQLMessage[] = []; @@ -322,47 +313,7 @@ export const commandDefinitions: CommandDefinition[] = [ defaultMessage: 'Rearranges fields in the input table by applying the keep clauses in fields', }), examples: ['… | keep a', '… | keep a,b'], - suggest: async (innerText, getFieldsByType, getColumnByName) => { - if ( - /\s/.test(innerText[innerText.length - 1]) && - !noCaseCompare(findPreviousWord(innerText), 'keep') - ) { - return [pipeCompleteItem, commaCompleteItem]; - } - - const fieldSuggestions = await getFieldsByType('any', [], {}); - return handleFragment( - innerText, - (fragment) => Boolean(getColumnByName(fragment)), - (_fragment: string, rangeToReplace?: { start: number; end: number }) => { - // KEEP fie - return fieldSuggestions.map((suggestion) => ({ - ...suggestion, - text: suggestion.text, - command: TRIGGER_SUGGESTION_COMMAND, - rangeToReplace, - })); - }, - (fragment: string, rangeToReplace: { start: number; end: number }) => { - // KEEP field - const finalSuggestions = [{ ...pipeCompleteItem, text: ' | ' }]; - if (fieldSuggestions.length > 1) - // when we fix the editor marker, this should probably be checked against 0 instead of 1 - // this is because the last field in the AST is currently getting removed (because it contains - // the editor marker) so it is not included in the ignored list which is used to filter out - // existing fields above. - finalSuggestions.push({ ...commaCompleteItem, text: ', ' }); - - return finalSuggestions.map((s) => ({ - ...s, - filterText: fragment, - text: fragment + s.text, - command: TRIGGER_SUGGESTION_COMMAND, - rangeToReplace, - })); - } - ); - }, + suggest: suggestForKeep, options: [], modes: [], signature: { @@ -438,148 +389,7 @@ export const commandDefinitions: CommandDefinition[] = [ multipleParams: true, params: [{ name: 'expression', type: 'any' }], }, - suggest: async ( - innerText: string, - getFieldsByType: GetFieldsByTypeFn, - columnExists: (column: string) => boolean - ): Promise => { - const prependSpace = (s: SuggestionRawDefinition) => ({ ...s, text: ' ' + s.text }); - - const { pos, nulls } = getSortPos(innerText); - - switch (pos) { - case 'space2': { - return [ - sortModifierSuggestions.ASC, - sortModifierSuggestions.DESC, - sortModifierSuggestions.NULLS_FIRST, - sortModifierSuggestions.NULLS_LAST, - pipeCompleteItem, - { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, - ]; - } - case 'order': { - return handleFragment( - innerText, - (fragment) => - ['ASC', 'DESC'].some((completeWord) => noCaseCompare(completeWord, fragment)), - (_fragment, rangeToReplace) => { - return Object.values(sortModifierSuggestions).map((suggestion) => ({ - ...suggestion, - rangeToReplace, - })); - }, - (fragment, rangeToReplace) => { - return [ - { ...pipeCompleteItem, text: ' | ' }, - { ...commaCompleteItem, text: ', ' }, - prependSpace(sortModifierSuggestions.NULLS_FIRST), - prependSpace(sortModifierSuggestions.NULLS_LAST), - ].map((suggestion) => ({ - ...suggestion, - filterText: fragment, - text: fragment + suggestion.text, - rangeToReplace, - command: TRIGGER_SUGGESTION_COMMAND, - })); - } - ); - } - case 'space3': { - return [ - sortModifierSuggestions.NULLS_FIRST, - sortModifierSuggestions.NULLS_LAST, - pipeCompleteItem, - { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, - ]; - } - case 'nulls': { - return handleFragment( - innerText, - (fragment) => - ['FIRST', 'LAST'].some((completeWord) => noCaseCompare(completeWord, fragment)), - (_fragment) => { - const end = innerText.length + 1; - const start = end - nulls.length; - return Object.values(sortModifierSuggestions).map((suggestion) => ({ - ...suggestion, - // we can't use the range generated by handleFragment here - // because it doesn't really support multi-word completions - rangeToReplace: { start, end }, - })); - }, - (fragment, rangeToReplace) => { - return [ - { ...pipeCompleteItem, text: ' | ' }, - { ...commaCompleteItem, text: ', ' }, - ].map((suggestion) => ({ - ...suggestion, - filterText: fragment, - text: fragment + suggestion.text, - rangeToReplace, - command: TRIGGER_SUGGESTION_COMMAND, - })); - } - ); - } - case 'space4': { - return [ - pipeCompleteItem, - { ...commaCompleteItem, text: ', ', command: TRIGGER_SUGGESTION_COMMAND }, - ]; - } - } - - const fieldSuggestions = await getFieldsByType('any', [], { - openSuggestions: true, - }); - const functionSuggestions = await getFieldsOrFunctionsSuggestions( - ['any'], - 'sort', - undefined, - getFieldsByType, - { - functions: true, - fields: false, - } - ); - - return await handleFragment( - innerText, - columnExists, - (_fragment: string, rangeToReplace?: { start: number; end: number }) => { - // SORT fie - return [ - ...pushItUpInTheList( - fieldSuggestions.map((suggestion) => ({ - ...suggestion, - command: TRIGGER_SUGGESTION_COMMAND, - rangeToReplace, - })), - true - ), - ...functionSuggestions, - ]; - }, - (fragment: string, rangeToReplace: { start: number; end: number }) => { - // SORT field - return [ - { ...pipeCompleteItem, text: ' | ' }, - { ...commaCompleteItem, text: ', ' }, - prependSpace(sortModifierSuggestions.ASC), - prependSpace(sortModifierSuggestions.DESC), - prependSpace(sortModifierSuggestions.NULLS_FIRST), - prependSpace(sortModifierSuggestions.NULLS_LAST), - ].map((s) => ({ - ...s, - filterText: fragment, - text: fragment + s.text, - command: TRIGGER_SUGGESTION_COMMAND, - rangeToReplace, - })); - } - ); - }, + suggest: suggestForSort, }, { From f19de39cbb841692bc206227f6720b8c999ce8db Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Fri, 25 Oct 2024 10:11:58 -0600 Subject: [PATCH 05/19] give command AST node to autocomplete routine --- .../src/autocomplete/autocomplete.ts | 22 +++++++++---------- .../src/autocomplete/commands/keep/index.ts | 12 ++++++---- .../src/autocomplete/commands/sort/index.ts | 10 +++++---- .../src/autocomplete/helper.ts | 4 ++-- .../recommended_queries/suggestions.ts | 4 ++-- .../src/autocomplete/types.ts | 2 +- .../src/definitions/types.ts | 15 ++++++++----- .../src/shared/helpers.ts | 12 +++++----- 8 files changed, 45 insertions(+), 36 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index b5b28c011d8da..5bdbd9d995fc9 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -18,7 +18,7 @@ import type { } from '@kbn/esql-ast'; import { i18n } from '@kbn/i18n'; import { ESQL_NUMBER_TYPES, isNumericType } from '../shared/esql_types'; -import type { EditorContext, ItemKind, SuggestionRawDefinition, GetFieldsByTypeFn } from './types'; +import type { EditorContext, ItemKind, SuggestionRawDefinition, GetColumnsByTypeFn } from './types'; import { getColumnForASTNode, getCommandDefinition, @@ -272,7 +272,7 @@ export async function suggest( export function getFieldsByTypeRetriever( queryString: string, resourceRetriever?: ESQLCallbacks -): { getFieldsByType: GetFieldsByTypeFn; getFieldsMap: GetFieldsMapFn } { +): { getFieldsByType: GetColumnsByTypeFn; getFieldsMap: GetFieldsMapFn } { const helpers = getFieldsByTypeHelper(queryString, resourceRetriever); return { getFieldsByType: async ( @@ -457,7 +457,7 @@ async function getSuggestionsWithinCommand( node: ESQLSingleAstItem | undefined; }, getSources: () => Promise, - getFieldsByType: GetFieldsByTypeFn, + getColumnsByType: GetColumnsByTypeFn, getFieldsMap: GetFieldsMapFn, getPolicies: GetPoliciesFn, getPolicyMetadata: GetPolicyMetadataFn @@ -471,7 +471,7 @@ async function getSuggestionsWithinCommand( const references = { fields: fieldsMap, variables: anyVariables }; if (commandDef.suggest) { // The new path. - return commandDef.suggest(innerText, getFieldsByType, (col) => + return commandDef.suggest(innerText, command, getColumnsByType, (col: string) => Boolean(getColumnByName(col, references)) ); } else { @@ -481,7 +481,7 @@ async function getSuggestionsWithinCommand( commands, { command, option, node }, getSources, - getFieldsByType, + getColumnsByType, getFieldsMap, getPolicies, getPolicyMetadata @@ -506,7 +506,7 @@ async function getExpressionSuggestionsByType( node: ESQLSingleAstItem | undefined; }, getSources: () => Promise, - getFieldsByType: GetFieldsByTypeFn, + getFieldsByType: GetColumnsByTypeFn, getFieldsMap: GetFieldsMapFn, getPolicies: GetPoliciesFn, getPolicyMetadata: GetPolicyMetadataFn @@ -1082,7 +1082,7 @@ async function getBuiltinFunctionNextArgument( nodeArg: ESQLFunction, nodeArgType: string, references: Pick, - getFieldsByType: GetFieldsByTypeFn + getFieldsByType: GetColumnsByTypeFn ) { const suggestions = []; const isFnComplete = isFunctionArgComplete(nodeArg, references); @@ -1192,7 +1192,7 @@ async function getFunctionArgsSuggestions( option: ESQLCommandOption | undefined; node: ESQLFunction; }, - getFieldsByType: GetFieldsByTypeFn, + getFieldsByType: GetColumnsByTypeFn, getFieldsMap: GetFieldsMapFn, getPolicyMetadata: GetPolicyMetadataFn, fullText: string, @@ -1421,7 +1421,7 @@ async function getListArgsSuggestions( command: ESQLCommand; node: ESQLSingleAstItem | undefined; }, - getFieldsByType: GetFieldsByTypeFn, + getFieldsByType: GetColumnsByTypeFn, getFieldsMaps: GetFieldsMapFn, getPolicyMetadata: GetPolicyMetadataFn ) { @@ -1476,7 +1476,7 @@ async function getSettingArgsSuggestions( command: ESQLCommand; node: ESQLSingleAstItem | undefined; }, - getFieldsByType: GetFieldsByTypeFn, + getFieldsByType: GetColumnsByTypeFn, getFieldsMaps: GetFieldsMapFn, getPolicyMetadata: GetPolicyMetadataFn ) { @@ -1507,7 +1507,7 @@ async function getOptionArgsSuggestions( option: ESQLCommandOption; node: ESQLSingleAstItem | undefined; }, - getFieldsByType: GetFieldsByTypeFn, + getFieldsByType: GetColumnsByTypeFn, getFieldsMaps: GetFieldsMapFn, getPolicyMetadata: GetPolicyMetadataFn, getPreferences?: () => Promise<{ histogramBarTarget: number } | undefined> diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts index 079d5079b00de..32d6d64b76ba4 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts @@ -7,15 +7,17 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { findPreviousWord, noCaseCompare } from '../../../shared/helpers'; -import { GetFieldsByTypeFn, SuggestionRawDefinition } from '../../types'; +import type { ESQLCommand } from '@kbn/esql-ast'; +import { findPreviousWord, isColumnItem, noCaseCompare } from '../../../shared/helpers'; +import type { GetColumnsByTypeFn, SuggestionRawDefinition } from '../../types'; import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; import { handleFragment } from '../../helper'; import { TRIGGER_SUGGESTION_COMMAND } from '../../factories'; export async function suggest( innerText: string, - getFieldsByType: GetFieldsByTypeFn, + command: ESQLCommand<'keep'>, + getColumnsByType: GetColumnsByTypeFn, columnExists: (column: string) => boolean ): Promise { if ( @@ -25,7 +27,9 @@ export async function suggest( return [pipeCompleteItem, commaCompleteItem]; } - const fieldSuggestions = await getFieldsByType('any', [], {}); + const alreadyDeclaredFields = command.args.filter(isColumnItem).map((arg) => arg.name); + const fieldSuggestions = await getColumnsByType('any', alreadyDeclaredFields); + return handleFragment( innerText, (fragment) => columnExists(fragment), diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/index.ts index 4abba12919d2d..61561dea96b72 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/index.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/sort/index.ts @@ -7,16 +7,18 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import type { ESQLCommand } from '@kbn/esql-ast'; import { noCaseCompare } from '../../../shared/helpers'; import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; import { TRIGGER_SUGGESTION_COMMAND } from '../../factories'; import { getFieldsOrFunctionsSuggestions, handleFragment, pushItUpInTheList } from '../../helper'; -import { GetFieldsByTypeFn, SuggestionRawDefinition } from '../../types'; +import type { GetColumnsByTypeFn, SuggestionRawDefinition } from '../../types'; import { getSortPos, sortModifierSuggestions } from './helper'; export async function suggest( innerText: string, - getFieldsByType: GetFieldsByTypeFn, + _command: ESQLCommand<'sort'>, + getColumnsByType: GetColumnsByTypeFn, columnExists: (column: string) => boolean ): Promise { const prependSpace = (s: SuggestionRawDefinition) => ({ ...s, text: ' ' + s.text }); @@ -105,14 +107,14 @@ export async function suggest( } } - const fieldSuggestions = await getFieldsByType('any', [], { + const fieldSuggestions = await getColumnsByType('any', [], { openSuggestions: true, }); const functionSuggestions = await getFieldsOrFunctionsSuggestions( ['any'], 'sort', undefined, - getFieldsByType, + getColumnsByType, { functions: true, fields: false, diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts index 42f92aed806a7..6585a04c98c59 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts @@ -30,7 +30,7 @@ import { isLiteralItem, isTimeIntervalItem, } from '../shared/helpers'; -import type { GetFieldsByTypeFn, SuggestionRawDefinition } from './types'; +import type { GetColumnsByTypeFn, SuggestionRawDefinition } from './types'; import { compareTypesWithLiterals } from '../shared/esql_types'; import { TIME_SYSTEM_PARAMS, @@ -354,7 +354,7 @@ export async function getFieldsOrFunctionsSuggestions( types: string[], commandName: string, optionName: string | undefined, - getFieldsByType: GetFieldsByTypeFn, + getFieldsByType: GetColumnsByTypeFn, { functions, fields, diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/suggestions.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/suggestions.ts index fbcfbabb2b63c..29c598af93501 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/suggestions.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/recommended_queries/suggestions.ts @@ -7,11 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { SuggestionRawDefinition, GetFieldsByTypeFn } from '../types'; +import type { SuggestionRawDefinition, GetColumnsByTypeFn } from '../types'; import { getRecommendedQueries } from './templates'; export const getRecommendedQueriesSuggestions = async ( - getFieldsByType: GetFieldsByTypeFn, + getFieldsByType: GetColumnsByTypeFn, fromCommand: string = '' ): Promise => { const fieldSuggestions = await getFieldsByType('date', [], { diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts index 030bff4da181c..cbd6ead535932 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts @@ -81,7 +81,7 @@ export interface EditorContext { triggerKind: number; } -export type GetFieldsByTypeFn = ( +export type GetColumnsByTypeFn = ( type: string | string[], ignored?: string[], options?: { advanceCursor?: boolean; openSuggestions?: boolean; addComma?: boolean } diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts index 5b2b748ef1c80..4606d1c5c9700 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts @@ -8,7 +8,7 @@ */ import type { ESQLCommand, ESQLCommandOption, ESQLFunction, ESQLMessage } from '@kbn/esql-ast'; -import { GetFieldsByTypeFn, SuggestionRawDefinition } from '../autocomplete/types'; +import { GetColumnsByTypeFn, SuggestionRawDefinition } from '../autocomplete/types'; /** * All supported field types in ES|QL. This is all the types @@ -159,8 +159,8 @@ export interface FunctionDefinition { validate?: (fnDef: ESQLFunction) => ESQLMessage[]; } -export interface CommandBaseDefinition { - name: string; +export interface CommandBaseDefinition { + name: CommandName; alias?: string; description: string; /** @@ -169,7 +169,8 @@ export interface CommandBaseDefinition { hidden?: boolean; suggest?: ( innerText: string, - getFieldsByType: GetFieldsByTypeFn, + command: ESQLCommand, + getColumnsByType: GetColumnsByTypeFn, columnExists: (column: string) => boolean ) => Promise; /** @deprecated this property will disappear in the future */ @@ -190,7 +191,8 @@ export interface CommandBaseDefinition { }; } -export interface CommandOptionsDefinition extends CommandBaseDefinition { +export interface CommandOptionsDefinition + extends CommandBaseDefinition { wrapped?: string[]; optional: boolean; skipCommonValidation?: boolean; @@ -208,7 +210,8 @@ export interface CommandModeDefinition { prefix?: string; } -export interface CommandDefinition extends CommandBaseDefinition { +export interface CommandDefinition + extends CommandBaseDefinition { /** @deprecated this property will disappear in the future */ options?: CommandOptionsDefinition[]; examples: string[]; diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts index 2392a44814997..1d2f975ac84d6 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts @@ -132,7 +132,7 @@ export function isSourceCommand({ label }: { label: string }) { } let fnLookups: Map | undefined; -let commandLookups: Map | undefined; +let commandLookups: Map> | undefined; function buildFunctionLookup() { // we always refresh if we have test functions @@ -197,7 +197,7 @@ export function getFunctionDefinition(name: string) { const unwrapStringLiteralQuotes = (value: string) => value.slice(1, -1); -function buildCommandLookup() { +function buildCommandLookup(): Map> { if (!commandLookups) { commandLookups = commandDefinitions.reduce((memo, def) => { memo.set(def.name, def); @@ -205,12 +205,12 @@ function buildCommandLookup() { memo.set(def.alias, def); } return memo; - }, new Map()); + }, new Map>()); } - return commandLookups; + return commandLookups!; } -export function getCommandDefinition(name: string): CommandDefinition { +export function getCommandDefinition(name: string): CommandDefinition { return buildCommandLookup().get(name.toLowerCase())!; } @@ -218,7 +218,7 @@ export function getAllCommands() { return Array.from(buildCommandLookup().values()); } -export function getCommandOption(optionName: CommandOptionsDefinition['name']) { +export function getCommandOption(optionName: CommandOptionsDefinition['name']) { return [byOption, metadataOption, asOption, onOption, withOption, appendSeparatorOption].find( ({ name }) => name === optionName ); From dea05465f2b59565e48b56ed8f09383fb7b2cdf1 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Fri, 25 Oct 2024 10:28:54 -0600 Subject: [PATCH 06/19] handle new expression case --- .../src/autocomplete/commands/keep/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts index 32d6d64b76ba4..c2480ffbcde72 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts @@ -8,7 +8,12 @@ */ import type { ESQLCommand } from '@kbn/esql-ast'; -import { findPreviousWord, isColumnItem, noCaseCompare } from '../../../shared/helpers'; +import { + findPreviousWord, + getLastCharFromTrimmed, + isColumnItem, + noCaseCompare, +} from '../../../shared/helpers'; import type { GetColumnsByTypeFn, SuggestionRawDefinition } from '../../types'; import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; import { handleFragment } from '../../helper'; @@ -22,6 +27,7 @@ export async function suggest( ): Promise { if ( /\s/.test(innerText[innerText.length - 1]) && + getLastCharFromTrimmed(innerText) !== ',' && !noCaseCompare(findPreviousWord(innerText), 'keep') ) { return [pipeCompleteItem, commaCompleteItem]; From d4dfe3454b4521782ab0568befd5e6734d552592 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Mon, 28 Oct 2024 14:34:05 -0600 Subject: [PATCH 07/19] rename getFieldsFor to getColumnsFor --- packages/kbn-esql-editor/src/esql_editor.tsx | 2 +- .../src/__tests__/helpers.ts | 2 +- .../__tests__/autocomplete.suggest.test.ts | 2 +- .../src/autocomplete/__tests__/helpers.ts | 16 ++++- .../src/autocomplete/autocomplete.test.ts | 60 +++++++++++++++---- .../src/autocomplete/complete_items.ts | 2 +- .../src/autocomplete/factories.ts | 2 +- .../src/code_actions/actions.test.ts | 14 ++--- .../src/code_actions/actions.ts | 2 +- .../src/definitions/commands.ts | 2 +- .../src/definitions/helpers.ts | 6 +- .../src/definitions/types.ts | 10 ++-- .../src/shared/resources_helpers.ts | 2 +- .../src/shared/types.ts | 2 +- .../validation/__tests__/callbacks.test.ts | 4 +- .../src/validation/validation.test.ts | 24 ++++---- .../src/validation/validation.ts | 2 +- 17 files changed, 99 insertions(+), 55 deletions(-) diff --git a/packages/kbn-esql-editor/src/esql_editor.tsx b/packages/kbn-esql-editor/src/esql_editor.tsx index 97340dc20d422..e8ca582ac5229 100644 --- a/packages/kbn-esql-editor/src/esql_editor.tsx +++ b/packages/kbn-esql-editor/src/esql_editor.tsx @@ -336,7 +336,7 @@ export const ESQLEditor = memo(function ESQLEditor({ const sources = await memoizedSources(dataViews, core).result; return sources; }, - getFieldsFor: async ({ query: queryToExecute }: { query?: string } | undefined = {}) => { + getColumnsFor: async ({ query: queryToExecute }: { query?: string } | undefined = {}) => { if (queryToExecute) { // ES|QL with limit 0 returns only the columns and is more performant const esqlQuery = { diff --git a/packages/kbn-esql-validation-autocomplete/src/__tests__/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/__tests__/helpers.ts index abac86ab0e323..2f46356acee37 100644 --- a/packages/kbn-esql-validation-autocomplete/src/__tests__/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/__tests__/helpers.ts @@ -56,7 +56,7 @@ export const policies = [ export function getCallbackMocks() { return { - getFieldsFor: jest.fn(async ({ query }) => { + getColumnsFor: jest.fn(async ({ query }) => { if (/enrich/.test(query)) { return enrichFields; } diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.suggest.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.suggest.test.ts index 51302d0d4cde5..c7bf9079f9155 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.suggest.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.suggest.test.ts @@ -42,6 +42,6 @@ describe('autocomplete.suggest', () => { await suggest('sHoW ?'); await suggest('row ? |'); - expect(callbacks.getFieldsFor.mock.calls.length).toBe(0); + expect(callbacks.getColumnsFor.mock.calls.length).toBe(0); }); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts index 93fd194d93a54..ec7d1e77ebd49 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts @@ -249,7 +249,17 @@ export function getDateLiteralsByFieldType(_requestedType: FieldType | FieldType } export function createCustomCallbackMocks( - customFields?: ESQLRealField[], + /** + * Columns that will come from Elasticsearch since the last command + * e.g. the test case may be `FROM index | EVAL foo = 1 | KEEP /` + * + * In this case, the columns available for the KEEP command will be the ones + * that were available after the EVAL command + * + * `FROM index | EVAL foo = 1 | LIMIT 0` will be used to fetch columns. The response + * will include "foo" as a column. + */ + customColumnsSinceLastCommand?: ESQLRealField[], customSources?: Array<{ name: string; hidden: boolean }>, customPolicies?: Array<{ name: string; @@ -258,11 +268,11 @@ export function createCustomCallbackMocks( enrichFields: string[]; }> ) { - const finalFields = customFields || fields; + const finalColumnsSinceLastCommand = customColumnsSinceLastCommand || fields; const finalSources = customSources || indexes; const finalPolicies = customPolicies || policies; return { - getFieldsFor: jest.fn(async () => finalFields), + getColumnsFor: jest.fn(async () => finalColumnsSinceLastCommand), getSources: jest.fn(async () => finalSources), getPolicies: jest.fn(async () => finalPolicies), }; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index deb4592428089..b89be15d670b1 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -31,6 +31,8 @@ import { TIME_PICKER_SUGGESTION, setup, attachTriggerCommand, + SuggestOptions, + fields, } from './__tests__/helpers'; import { METADATA_FIELDS } from '../shared/constants'; import { ESQL_COMMON_NUMERIC_TYPES, ESQL_STRING_TYPES } from '../shared/esql_types'; @@ -385,24 +387,56 @@ describe('autocomplete', () => { '```````round(doubleField) + 1```` + 1`` + 1`', '```````````````round(doubleField) + 1```````` + 1```` + 1`` + 1`', '```````````````````````````````round(doubleField) + 1```````````````` + 1```````` + 1```` + 1`` + 1`', + ], + undefined, + [ + [ + ...fields, + // the following non-field columns will come over the wire as part of the response + { + name: 'round(doubleField) + 1', + type: 'double', + }, + { + name: '`round(doubleField) + 1` + 1', + type: 'double', + }, + { + name: '```round(doubleField) + 1`` + 1` + 1', + type: 'double', + }, + { + name: '```````round(doubleField) + 1```` + 1`` + 1` + 1', + type: 'double', + }, + { + name: '```````````````round(doubleField) + 1```````` + 1```` + 1`` + 1` + 1', + type: 'double', + }, + ], ] ); it('should not suggest already-used fields and variables', async () => { const { suggest: suggestTest } = await setup(); - const getSuggestions = async (query: string) => - (await suggestTest(query)).map((value) => value.text); + const getSuggestions = async (query: string, opts?: SuggestOptions) => + (await suggestTest(query, opts)).map((value) => value.text); - expect(await getSuggestions('from a_index | EVAL foo = 1 | KEEP /')).toContain('foo'); - expect(await getSuggestions('from a_index | EVAL foo = 1 | KEEP foo, /')).not.toContain( - 'foo' - ); - expect(await getSuggestions('from a_index | EVAL foo = 1 | KEEP /')).toContain( + expect( + await getSuggestions('from a_index | EVAL foo = 1 | KEEP /', { + callbacks: { getColumnsFor: () => [...fields, { name: 'foo', type: 'integer' }] }, + }) + ).toContain('foo'); + expect( + await getSuggestions('from a_index | EVAL foo = 1 | KEEP foo, /', { + callbacks: { getColumnsFor: () => [...fields, { name: 'foo', type: 'integer' }] }, + }) + ).not.toContain('foo'); + + expect(await getSuggestions('from a_index | KEEP /')).toContain('doubleField'); + expect(await getSuggestions('from a_index | KEEP doubleField, /')).not.toContain( 'doubleField' ); - expect( - await getSuggestions('from a_index | EVAL foo = 1 | KEEP doubleField, /') - ).not.toContain('doubleField'); }); }); } @@ -504,7 +538,7 @@ describe('autocomplete', () => { }); describe('callbacks', () => { - it('should send the fields query without the last command', async () => { + it('should send the columns query without the last command', async () => { const callbackMocks = createCustomCallbackMocks(undefined, undefined, undefined); const statement = 'from a | drop keywordField | eval var0 = abs(doubleField) '; const triggerOffset = statement.lastIndexOf(' '); @@ -516,7 +550,7 @@ describe('autocomplete', () => { async (text) => (text ? getAstAndSyntaxErrors(text) : { ast: [], errors: [] }), callbackMocks ); - expect(callbackMocks.getFieldsFor).toHaveBeenCalledWith({ + expect(callbackMocks.getColumnsFor).toHaveBeenCalledWith({ query: 'from a | drop keywordField', }); }); @@ -532,7 +566,7 @@ describe('autocomplete', () => { async (text) => (text ? getAstAndSyntaxErrors(text) : { ast: [], errors: [] }), callbackMocks ); - expect(callbackMocks.getFieldsFor).toHaveBeenCalledWith({ query: 'from a' }); + expect(callbackMocks.getColumnsFor).toHaveBeenCalledWith({ query: 'from a' }); }); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts index 5da9da2fe7653..b115e30c47efe 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/complete_items.ts @@ -87,7 +87,7 @@ export const getBuiltinCompatibleFunctionDefinition = ( }; export const getCommandAutocompleteDefinitions = ( - commands: CommandDefinition[] + commands: Array> ): SuggestionRawDefinition[] => commands.filter(({ hidden }) => !hidden).map(getSuggestionCommandDefinition); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts index 43f6f8ccff365..14cbc413c203d 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts @@ -123,7 +123,7 @@ export const getCompatibleFunctionDefinition = ( }; export function getSuggestionCommandDefinition( - command: CommandDefinition + command: CommandDefinition ): SuggestionRawDefinition { const commandDefinition = getCommandDefinition(command.name); const commandSignature = getCommandSignature(commandDefinition); diff --git a/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.test.ts b/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.test.ts index b608570854950..4563379642767 100644 --- a/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.test.ts @@ -18,7 +18,7 @@ import type { ESQLCallbacks, PartialFieldsMetadataClient } from '../shared/types function getCallbackMocks(): jest.Mocked { return { - getFieldsFor: jest.fn, any>(async ({ query }) => { + getColumnsFor: jest.fn, any>(async ({ query }) => { if (/enrich/.test(query)) { const fields: ESQLRealField[] = [ { name: 'otherField', type: 'keyword' }, @@ -375,11 +375,11 @@ describe('quick fixes logic', () => { const statement = `FROM index | DROP any#Char$Field`; const { errors } = await validateQuery(statement, getAstAndSyntaxErrors, undefined, { ...callbackMocks, - getFieldsFor: undefined, + getColumnsFor: undefined, }); const edits = await getActions(statement, errors, getAstAndSyntaxErrors, undefined, { ...callbackMocks, - getFieldsFor: undefined, + getColumnsFor: undefined, }); expect(edits.length).toBe(0); }); @@ -400,7 +400,7 @@ describe('quick fixes logic', () => { const statement = `FROM index | DROP any#Char$Field`; const { errors } = await validateQuery(statement, getAstAndSyntaxErrors, undefined, { ...callbackMocks, - getFieldsFor: undefined, + getColumnsFor: undefined, getFieldsMetadata: undefined, }); const actions = await getActions( @@ -412,7 +412,7 @@ describe('quick fixes logic', () => { }, { ...callbackMocks, - getFieldsFor: undefined, + getColumnsFor: undefined, getFieldsMetadata: undefined, } ); @@ -435,7 +435,7 @@ describe('quick fixes logic', () => { ); try { await getActions(statement, errors, getAstAndSyntaxErrors, undefined, { - getFieldsFor: undefined, + getColumnsFor: undefined, getSources: undefined, getPolicies: undefined, }); @@ -460,7 +460,7 @@ describe('quick fixes logic', () => { getAstAndSyntaxErrors, { relaxOnMissingCallbacks: true }, { - getFieldsFor: undefined, + getColumnsFor: undefined, getSources: undefined, getPolicies: undefined, getFieldsMetadata: undefined, diff --git a/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.ts b/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.ts index 02627c5f1abdf..37ab56350ffb2 100644 --- a/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.ts +++ b/packages/kbn-esql-validation-autocomplete/src/code_actions/actions.ts @@ -403,7 +403,7 @@ export async function getActions( const { getPolicies, getPolicyFields } = getPolicyRetriever(resourceRetriever); const callbacks = { - getFieldsByType: resourceRetriever?.getFieldsFor ? getFieldsByType : undefined, + getFieldsByType: resourceRetriever?.getColumnsFor ? getFieldsByType : undefined, getSources: resourceRetriever?.getSources ? getSources : undefined, getPolicies: resourceRetriever?.getPolicies ? getPolicies : undefined, getPolicyFields: resourceRetriever?.getPolicies ? getPolicyFields : undefined, diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts index d2667c0b15b64..9fd31d33fd9df 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts @@ -150,7 +150,7 @@ const statsValidator = (command: ESQLCommand) => { } return messages; }; -export const commandDefinitions: CommandDefinition[] = [ +export const commandDefinitions: Array> = [ { name: 'row', description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.rowDoc', { diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts index c1df4a6cb16e8..2b50c9da541ce 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts @@ -56,7 +56,7 @@ function handleAdditionalArgs( } export function getCommandSignature( - { name, signature, options, examples }: CommandDefinition, + { name, signature, options, examples }: CommandDefinition, { withTypes }: { withTypes: boolean } = { withTypes: true } ) { return { @@ -75,7 +75,7 @@ export function getCommandSignature( } function printCommandArguments( - { multipleParams, params }: CommandDefinition['signature'], + { multipleParams, params }: CommandDefinition['signature'], withTypes: boolean ): string { return `${params.map((arg) => printCommandArgument(arg, withTypes)).join(', `')}${ @@ -86,7 +86,7 @@ function printCommandArguments( } function printCommandArgument( - param: CommandDefinition['signature']['params'][number], + param: CommandDefinition['signature']['params'][number], withTypes: boolean ): string { if (!withTypes) { diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts index 4606d1c5c9700..a83908b41617f 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts @@ -191,7 +191,7 @@ export interface CommandBaseDefinition { }; } -export interface CommandOptionsDefinition +export interface CommandOptionsDefinition extends CommandBaseDefinition { wrapped?: string[]; optional: boolean; @@ -212,13 +212,13 @@ export interface CommandModeDefinition { export interface CommandDefinition extends CommandBaseDefinition { - /** @deprecated this property will disappear in the future */ - options?: CommandOptionsDefinition[]; examples: string[]; validate?: (option: ESQLCommand) => ESQLMessage[]; - /** @deprecated this property will disappear in the future */ - modes?: CommandModeDefinition[]; hasRecommendedQueries?: boolean; + /** @deprecated this property will disappear in the future */ + modes: CommandModeDefinition[]; + /** @deprecated this property will disappear in the future */ + options: CommandOptionsDefinition[]; } export interface Literals { diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/resources_helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/resources_helpers.ts index a4da3907a4d6b..5e7d951d8bdbf 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/resources_helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/resources_helpers.ts @@ -36,7 +36,7 @@ export function getFieldsByTypeHelper(queryText: string, resourceRetriever?: ESQ const getFields = async () => { const metadata = await getEcsMetadata(); if (!cacheFields.size && queryText) { - const fieldsOfType = await resourceRetriever?.getFieldsFor?.({ query: queryText }); + const fieldsOfType = await resourceRetriever?.getColumnsFor?.({ query: queryText }); const fieldsWithMetadata = enrichFieldsWithECSInfo(fieldsOfType || [], metadata); for (const field of fieldsWithMetadata || []) { cacheFields.set(field.name, field); diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/types.ts b/packages/kbn-esql-validation-autocomplete/src/shared/types.ts index bc1e1d337e4b3..1caa2c480864e 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/types.ts @@ -39,7 +39,7 @@ export interface ESQLSourceResult { export interface ESQLCallbacks { getSources?: CallbackFn<{}, ESQLSourceResult>; - getFieldsFor?: CallbackFn<{ query: string }, ESQLRealField>; + getColumnsFor?: CallbackFn<{ query: string }, ESQLRealField>; getPolicies?: CallbackFn< {}, { name: string; sourceIndices: string[]; matchField: string; enrichFields: string[] } diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/callbacks.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/callbacks.test.ts index aaa7a3d88f5ca..61c0455fa1b0d 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/callbacks.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/callbacks.test.ts @@ -19,7 +19,7 @@ describe('FROM', () => { await validate('SHOW'); await validate('ROW \t'); - expect(callbacks.getFieldsFor.mock.calls.length).toBe(0); + expect(callbacks.getColumnsFor.mock.calls.length).toBe(0); }); test('loads fields with FROM source when commands after pipe present', async () => { @@ -27,6 +27,6 @@ describe('FROM', () => { await validate('FROM kibana_ecommerce METADATA _id | eval'); - expect(callbacks.getFieldsFor.mock.calls.length).toBe(1); + expect(callbacks.getColumnsFor.mock.calls.length).toBe(1); }); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts index fae4ca16797cc..a9ecac9663609 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts @@ -1532,7 +1532,7 @@ describe('validation logic', () => { it(`should not fetch source and fields list when a row command is set`, async () => { const callbackMocks = getCallbackMocks(); await validateQuery(`row a = 1 | eval a`, getAstAndSyntaxErrors, undefined, callbackMocks); - expect(callbackMocks.getFieldsFor).not.toHaveBeenCalled(); + expect(callbackMocks.getColumnsFor).not.toHaveBeenCalled(); expect(callbackMocks.getSources).not.toHaveBeenCalled(); }); @@ -1545,7 +1545,7 @@ describe('validation logic', () => { it(`should not fetch source and fields for empty command`, async () => { const callbackMocks = getCallbackMocks(); await validateQuery(` `, getAstAndSyntaxErrors, undefined, callbackMocks); - expect(callbackMocks.getFieldsFor).not.toHaveBeenCalled(); + expect(callbackMocks.getColumnsFor).not.toHaveBeenCalled(); expect(callbackMocks.getSources).not.toHaveBeenCalled(); }); @@ -1559,8 +1559,8 @@ describe('validation logic', () => { ); expect(callbackMocks.getSources).not.toHaveBeenCalled(); expect(callbackMocks.getPolicies).toHaveBeenCalled(); - expect(callbackMocks.getFieldsFor).toHaveBeenCalledTimes(1); - expect(callbackMocks.getFieldsFor).toHaveBeenLastCalledWith({ + expect(callbackMocks.getColumnsFor).toHaveBeenCalledTimes(1); + expect(callbackMocks.getColumnsFor).toHaveBeenLastCalledWith({ query: `from enrich_index | keep otherField, yetAnotherField`, }); }); @@ -1575,8 +1575,8 @@ describe('validation logic', () => { ); expect(callbackMocks.getSources).not.toHaveBeenCalled(); expect(callbackMocks.getPolicies).not.toHaveBeenCalled(); - expect(callbackMocks.getFieldsFor).toHaveBeenCalledTimes(1); - expect(callbackMocks.getFieldsFor).toHaveBeenLastCalledWith({ + expect(callbackMocks.getColumnsFor).toHaveBeenCalledTimes(1); + expect(callbackMocks.getColumnsFor).toHaveBeenLastCalledWith({ query: 'show info', }); }); @@ -1591,8 +1591,8 @@ describe('validation logic', () => { ); expect(callbackMocks.getSources).toHaveBeenCalled(); expect(callbackMocks.getPolicies).toHaveBeenCalled(); - expect(callbackMocks.getFieldsFor).toHaveBeenCalledTimes(2); - expect(callbackMocks.getFieldsFor).toHaveBeenLastCalledWith({ + expect(callbackMocks.getColumnsFor).toHaveBeenCalledTimes(2); + expect(callbackMocks.getColumnsFor).toHaveBeenLastCalledWith({ query: `from enrich_index | keep otherField, yetAnotherField`, }); }); @@ -1604,7 +1604,7 @@ describe('validation logic', () => { getAstAndSyntaxErrors, undefined, { - getFieldsFor: undefined, + getColumnsFor: undefined, getSources: undefined, getPolicies: undefined, } @@ -1718,7 +1718,7 @@ describe('validation logic', () => { const contentByCallback = { getSources: /Unknown index/, getPolicies: /Unknown policy/, - getFieldsFor: /Unknown column|Argument of|it is unsupported or not indexed/, + getColumnsFor: /Unknown column|Argument of|it is unsupported or not indexed/, getPreferences: /Unknown/, getFieldsMetadata: /Unknown/, }; @@ -1761,7 +1761,7 @@ describe('validation logic', () => { }); // test excluding one callback at the time - it.each(['getSources', 'getFieldsFor', 'getPolicies'] as Array)( + it.each(['getSources', 'getColumnsFor', 'getPolicies'] as Array)( `should not error if %s is missing`, async (excludedCallback) => { const filteredTestCases = fixtures.testCases.filter((t) => @@ -1790,7 +1790,7 @@ describe('validation logic', () => { ); it('should work if no callback passed', async () => { - const excludedCallbacks = ['getSources', 'getPolicies', 'getFieldsFor'] as Array< + const excludedCallbacks = ['getSources', 'getPolicies', 'getColumnsFor'] as Array< keyof typeof ignoreErrorsMap >; for (const testCase of fixtures.testCases.filter((t) => diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts index 9605da8460eed..111fe79b3f5e0 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts @@ -1074,7 +1074,7 @@ function validateUnsupportedTypeFields(fields: Map) { } export const ignoreErrorsMap: Record = { - getFieldsFor: ['unknownColumn', 'wrongArgumentType', 'unsupportedFieldType'], + getColumnsFor: ['unknownColumn', 'wrongArgumentType', 'unsupportedFieldType'], getSources: ['unknownIndex'], getPolicies: ['unknownPolicy'], getPreferences: [], From 88e92264bcc6fa73503acd9a60c3160bb4ccb638 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Mon, 28 Oct 2024 15:25:19 -0600 Subject: [PATCH 08/19] separate DROP --- .../src/autocomplete/commands/drop/index.ts | 70 +++++++++++++++++++ .../src/definitions/commands.ts | 2 + 2 files changed, 72 insertions(+) create mode 100644 packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/drop/index.ts diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/drop/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/drop/index.ts new file mode 100644 index 0000000000000..ed5f0ee3d3f6b --- /dev/null +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/drop/index.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { ESQLCommand } from '@kbn/esql-ast'; +import { + findPreviousWord, + getLastCharFromTrimmed, + isColumnItem, + noCaseCompare, +} from '../../../shared/helpers'; +import type { GetColumnsByTypeFn, SuggestionRawDefinition } from '../../types'; +import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; +import { handleFragment } from '../../helper'; +import { TRIGGER_SUGGESTION_COMMAND } from '../../factories'; + +export async function suggest( + innerText: string, + command: ESQLCommand<'drop'>, + getColumnsByType: GetColumnsByTypeFn, + columnExists: (column: string) => boolean +): Promise { + if ( + /\s/.test(innerText[innerText.length - 1]) && + getLastCharFromTrimmed(innerText) !== ',' && + !noCaseCompare(findPreviousWord(innerText), 'drop') + ) { + return [pipeCompleteItem, commaCompleteItem]; + } + + const alreadyDeclaredFields = command.args.filter(isColumnItem).map((arg) => arg.name); + const fieldSuggestions = await getColumnsByType('any', alreadyDeclaredFields); + + return handleFragment( + innerText, + (fragment) => columnExists(fragment), + (_fragment: string, rangeToReplace?: { start: number; end: number }) => { + // KEEP fie + return fieldSuggestions.map((suggestion) => ({ + ...suggestion, + text: suggestion.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + }, + (fragment: string, rangeToReplace: { start: number; end: number }) => { + // KEEP field + const finalSuggestions = [{ ...pipeCompleteItem, text: ' | ' }]; + if (fieldSuggestions.length > 1) + // when we fix the editor marker, this should probably be checked against 0 instead of 1 + // this is because the last field in the AST is currently getting removed (because it contains + // the editor marker) so it is not included in the ignored list which is used to filter out + // existing fields above. + finalSuggestions.push({ ...commaCompleteItem, text: ', ' }); + + return finalSuggestions.map((s) => ({ + ...s, + filterText: fragment, + text: fragment + s.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + } + ); +} diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts index 9fd31d33fd9df..f4482a5b33c17 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts @@ -34,6 +34,7 @@ import { import type { CommandDefinition } from './types'; import { suggest as suggestForSort } from '../autocomplete/commands/sort'; import { suggest as suggestForKeep } from '../autocomplete/commands/keep'; +import { suggest as suggestForDrop } from '../autocomplete/commands/drop'; const statsValidator = (command: ESQLCommand) => { const messages: ESQLMessage[] = []; @@ -333,6 +334,7 @@ export const commandDefinitions: Array> = [ multipleParams: true, params: [{ name: 'column', type: 'column', wildcards: true }], }, + suggest: suggestForDrop, validate: (command: ESQLCommand) => { const messages: ESQLMessage[] = []; const wildcardItems = command.args.filter((arg) => isColumnItem(arg) && arg.name === '*'); From e5ecfe53a93be20f4e72b1aa6554f0d789114a35 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Tue, 29 Oct 2024 16:21:53 -0600 Subject: [PATCH 09/19] first stab at STATS --- .../src/autocomplete/autocomplete.ts | 4 +- .../src/autocomplete/commands/drop/index.ts | 4 +- .../src/autocomplete/commands/keep/index.ts | 4 +- .../src/autocomplete/commands/stats/index.ts | 138 ++++++++++++++++++ .../src/autocomplete/factories.ts | 12 +- .../src/definitions/commands.ts | 2 + .../src/shared/context.ts | 7 +- .../src/shared/helpers.ts | 4 +- 8 files changed, 154 insertions(+), 21 deletions(-) create mode 100644 packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 5bdbd9d995fc9..1dce3fef48c8c 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -24,7 +24,7 @@ import { getCommandDefinition, getCommandOption, getFunctionDefinition, - getLastCharFromTrimmed, + getLastNonWhitespaceChar, isArrayType, isAssignment, isAssignmentComplete, @@ -1485,7 +1485,7 @@ async function getSettingArgsSuggestions( const settingDefs = getCommandDefinition(command.name).modes || []; if (settingDefs.length) { - const lastChar = getLastCharFromTrimmed(innerText); + const lastChar = getLastNonWhitespaceChar(innerText); const matchingSettingDefs = settingDefs.filter(({ prefix }) => lastChar === prefix); if (matchingSettingDefs.length) { // COMMAND _ diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/drop/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/drop/index.ts index ed5f0ee3d3f6b..43eb272ba203a 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/drop/index.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/drop/index.ts @@ -10,7 +10,7 @@ import type { ESQLCommand } from '@kbn/esql-ast'; import { findPreviousWord, - getLastCharFromTrimmed, + getLastNonWhitespaceChar, isColumnItem, noCaseCompare, } from '../../../shared/helpers'; @@ -27,7 +27,7 @@ export async function suggest( ): Promise { if ( /\s/.test(innerText[innerText.length - 1]) && - getLastCharFromTrimmed(innerText) !== ',' && + getLastNonWhitespaceChar(innerText) !== ',' && !noCaseCompare(findPreviousWord(innerText), 'drop') ) { return [pipeCompleteItem, commaCompleteItem]; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts index c2480ffbcde72..85d5c716b20a6 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/keep/index.ts @@ -10,7 +10,7 @@ import type { ESQLCommand } from '@kbn/esql-ast'; import { findPreviousWord, - getLastCharFromTrimmed, + getLastNonWhitespaceChar, isColumnItem, noCaseCompare, } from '../../../shared/helpers'; @@ -27,7 +27,7 @@ export async function suggest( ): Promise { if ( /\s/.test(innerText[innerText.length - 1]) && - getLastCharFromTrimmed(innerText) !== ',' && + getLastNonWhitespaceChar(innerText) !== ',' && !noCaseCompare(findPreviousWord(innerText), 'keep') ) { return [pipeCompleteItem, commaCompleteItem]; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts new file mode 100644 index 0000000000000..c15ced5f53d6a --- /dev/null +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { ESQLCommand } from '@kbn/esql-ast'; +import { i18n } from '@kbn/i18n'; +import { + findPreviousWord, + getLastNonWhitespaceChar, + isOptionItem, + noCaseCompare, +} from '../../../shared/helpers'; +import type { GetColumnsByTypeFn, SuggestionRawDefinition } from '../../types'; +import { + TIME_SYSTEM_PARAMS, + TRIGGER_SUGGESTION_COMMAND, + allFunctions, + getFunctionSuggestion, +} from '../../factories'; +import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; +import { pushItUpInTheList } from '../../helper'; + +// STATS + +/** + * Position of the caret in the sort command: +* +* ``` +* STATS [column1 =] expression1[, ..., [columnN =] expressionN] [BY grouping_expression1[, ..., grouping_expressionN]] + | | | + expression_start expression_complete grouping_expression_start + +* ``` +*/ +export type CaretPosition = + | 'expression_start' + | 'expression_complete' + | 'grouping_expression_start' + | 'grouping_expression_complete'; + +export const getPosition = (innerText: string, command: ESQLCommand): CaretPosition => { + if ( + command.args.some( + (arg) => isOptionItem(arg) && arg.name === 'by' && arg.location.min < innerText.length + ) + ) { + if ( + getLastNonWhitespaceChar(innerText) === ',' || + noCaseCompare(findPreviousWord(innerText), 'by') + ) { + return 'grouping_expression_start'; + } else { + return 'grouping_expression_complete'; + } + } + + if ( + getLastNonWhitespaceChar(innerText) === ',' || + noCaseCompare(findPreviousWord(innerText), 'stats') + ) { + return 'expression_start'; + } else { + return 'expression_complete'; + } +}; + +const byCompleteItem: SuggestionRawDefinition = { + label: 'BY', + text: 'BY ', + kind: 'Reference', + detail: 'By', + sortText: '1', + command: TRIGGER_SUGGESTION_COMMAND, +}; + +const dateHistogramCompleteItem: SuggestionRawDefinition = { + label: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.addDateHistogram', { + defaultMessage: 'Add date histogram', + }), + // TODO preferences?.histogramBarTarget + text: `BUCKET($0, ${1000}, ${TIME_SYSTEM_PARAMS.join(', ')})`, + asSnippet: true, + kind: 'Issue', + detail: i18n.translate( + 'kbn-esql-validation-autocomplete.esql.autocomplete.addDateHistogramDetail', + { + defaultMessage: 'Add date histogram using bucket()', + } + ), + sortText: '1A', + command: TRIGGER_SUGGESTION_COMMAND, +}; + +export async function suggest( + innerText: string, + command: ESQLCommand<'stats'>, + getColumnsByType: GetColumnsByTypeFn, + _columnExists: (column: string) => boolean +): Promise { + const pos = getPosition(innerText, command); + switch (pos) { + case 'expression_start': + return allFunctions() + .filter((func) => func.supportedCommands.includes('stats')) + .map(getFunctionSuggestion); + + case 'expression_complete': + return [ + byCompleteItem, + pipeCompleteItem, + { ...commaCompleteItem, command: TRIGGER_SUGGESTION_COMMAND, text: ', ' }, + ]; + + case 'grouping_expression_start': + const columnSuggestions = pushItUpInTheList(await getColumnsByType('any'), true); + return [ + ...allFunctions() + .filter((func) => func.supportedOptions?.includes('by')) + .map(getFunctionSuggestion), + dateHistogramCompleteItem, + ...columnSuggestions, + ]; + + case 'grouping_expression_complete': + return [ + pipeCompleteItem, + { ...commaCompleteItem, command: TRIGGER_SUGGESTION_COMMAND, text: ', ' }, + ]; + + default: + return []; + } +} diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts index f522e9bc65863..c543b50db94e7 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts @@ -28,7 +28,7 @@ import { ESQLRealField } from '../validation/types'; import { isNumericType } from '../shared/esql_types'; import { getTestFunctions } from '../shared/test_functions'; -const allFunctions = memoize( +export const allFunctions = memoize( () => aggregationFunctionDefinitions .concat(scalarFunctionDefinitions) @@ -39,10 +39,6 @@ const allFunctions = memoize( export const TIME_SYSTEM_PARAMS = ['?_tstart', '?_tend']; -export const getAddDateHistogramSnippet = (histogramBarTarget = 50) => { - return `BUCKET($0, ${histogramBarTarget}, ${TIME_SYSTEM_PARAMS.join(', ')})`; -}; - export const TRIGGER_SUGGESTION_COMMAND = { title: 'Trigger Suggestion Dialog', id: 'editor.action.triggerSuggest', @@ -61,7 +57,7 @@ function getSafeInsertSourceText(text: string) { return shouldBeQuotedSource(text) ? getQuotedText(text) : text; } -export function getSuggestionFunctionDefinition(fn: FunctionDefinition): SuggestionRawDefinition { +export function getFunctionSuggestion(fn: FunctionDefinition): SuggestionRawDefinition { const fullSignatures = getFunctionSignatures(fn, { capitalize: true, withTypes: true }); return { label: fn.name.toUpperCase(), @@ -110,7 +106,7 @@ export const getCompatibleFunctionDefinition = ( ) .sort((a, b) => a.name.localeCompare(b.name)); if (!returnTypes) { - return fnSupportedByCommand.map(getSuggestionFunctionDefinition); + return fnSupportedByCommand.map(getFunctionSuggestion); } return fnSupportedByCommand .filter((mathDefinition) => @@ -119,7 +115,7 @@ export const getCompatibleFunctionDefinition = ( returnTypes[0] === 'any' || returnTypes.includes(signature.returnType as string) ) ) - .map(getSuggestionFunctionDefinition); + .map(getFunctionSuggestion); }; export function getSuggestionCommandDefinition( diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts index f4482a5b33c17..d07511665ad31 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/commands.ts @@ -35,6 +35,7 @@ import type { CommandDefinition } from './types'; import { suggest as suggestForSort } from '../autocomplete/commands/sort'; import { suggest as suggestForKeep } from '../autocomplete/commands/keep'; import { suggest as suggestForDrop } from '../autocomplete/commands/drop'; +import { suggest as suggestForStats } from '../autocomplete/commands/stats'; const statsValidator = (command: ESQLCommand) => { const messages: ESQLMessage[] = []; @@ -240,6 +241,7 @@ export const commandDefinitions: Array> = [ options: [byOption], modes: [], validate: statsValidator, + suggest: suggestForStats, }, { name: 'inlinestats', diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/context.ts b/packages/kbn-esql-validation-autocomplete/src/shared/context.ts index 1c2e9075e95ff..e4993ae5d0a06 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/context.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/context.ts @@ -167,10 +167,6 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number) return { type: 'function' as const, command, node, option, setting }; } } - if (node.type === 'option' || option) { - // command ... by - return { type: 'option' as const, command, node, option, setting }; - } // for now it's only an enrich thing if (node.type === 'source' && node.text === ENRICH_MODES.prefix) { // command _ @@ -182,7 +178,8 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number) return { type: 'newCommand' as const, command: undefined, node, option, setting }; } - if (command && isOptionItem(command.args[command.args.length - 1])) { + // TODO — remove this option branch once https://github.com/elastic/kibana/issues/195418 is complete + if (command && isOptionItem(command.args[command.args.length - 1]) && command.name !== 'stats') { if (option) { return { type: 'option' as const, command, node, option, setting }; } diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts index 02dff9720cd9b..b17f4ebcc8f07 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts @@ -599,7 +599,7 @@ export function pipePrecedesCurrentWord(text: string) { return characterPrecedesCurrentWord(text, '|'); } -export function getLastCharFromTrimmed(text: string) { +export function getLastNonWhitespaceChar(text: string) { return text[text.trimEnd().length - 1]; } @@ -607,7 +607,7 @@ export function getLastCharFromTrimmed(text: string) { * Are we after a comma? i.e. STATS fieldA, */ export function isRestartingExpression(text: string) { - return getLastCharFromTrimmed(text) === ',' || characterPrecedesCurrentWord(text, ','); + return getLastNonWhitespaceChar(text) === ',' || characterPrecedesCurrentWord(text, ','); } export function findPreviousWord(text: string) { From 3df44e825e85f13343ce54157fbb254d15894615 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Wed, 30 Oct 2024 07:43:23 -0600 Subject: [PATCH 10/19] separate out util --- .../src/autocomplete/commands/stats/index.ts | 91 +------------------ .../src/autocomplete/commands/stats/util.ts | 90 ++++++++++++++++++ 2 files changed, 95 insertions(+), 86 deletions(-) create mode 100644 packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts index c15ced5f53d6a..34b1e6f5e7180 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts @@ -8,93 +8,11 @@ */ import type { ESQLCommand } from '@kbn/esql-ast'; -import { i18n } from '@kbn/i18n'; -import { - findPreviousWord, - getLastNonWhitespaceChar, - isOptionItem, - noCaseCompare, -} from '../../../shared/helpers'; import type { GetColumnsByTypeFn, SuggestionRawDefinition } from '../../types'; -import { - TIME_SYSTEM_PARAMS, - TRIGGER_SUGGESTION_COMMAND, - allFunctions, - getFunctionSuggestion, -} from '../../factories'; +import { TRIGGER_SUGGESTION_COMMAND, allFunctions, getFunctionSuggestion } from '../../factories'; import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; import { pushItUpInTheList } from '../../helper'; - -// STATS - -/** - * Position of the caret in the sort command: -* -* ``` -* STATS [column1 =] expression1[, ..., [columnN =] expressionN] [BY grouping_expression1[, ..., grouping_expressionN]] - | | | - expression_start expression_complete grouping_expression_start - -* ``` -*/ -export type CaretPosition = - | 'expression_start' - | 'expression_complete' - | 'grouping_expression_start' - | 'grouping_expression_complete'; - -export const getPosition = (innerText: string, command: ESQLCommand): CaretPosition => { - if ( - command.args.some( - (arg) => isOptionItem(arg) && arg.name === 'by' && arg.location.min < innerText.length - ) - ) { - if ( - getLastNonWhitespaceChar(innerText) === ',' || - noCaseCompare(findPreviousWord(innerText), 'by') - ) { - return 'grouping_expression_start'; - } else { - return 'grouping_expression_complete'; - } - } - - if ( - getLastNonWhitespaceChar(innerText) === ',' || - noCaseCompare(findPreviousWord(innerText), 'stats') - ) { - return 'expression_start'; - } else { - return 'expression_complete'; - } -}; - -const byCompleteItem: SuggestionRawDefinition = { - label: 'BY', - text: 'BY ', - kind: 'Reference', - detail: 'By', - sortText: '1', - command: TRIGGER_SUGGESTION_COMMAND, -}; - -const dateHistogramCompleteItem: SuggestionRawDefinition = { - label: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.addDateHistogram', { - defaultMessage: 'Add date histogram', - }), - // TODO preferences?.histogramBarTarget - text: `BUCKET($0, ${1000}, ${TIME_SYSTEM_PARAMS.join(', ')})`, - asSnippet: true, - kind: 'Issue', - detail: i18n.translate( - 'kbn-esql-validation-autocomplete.esql.autocomplete.addDateHistogramDetail', - { - defaultMessage: 'Add date histogram using bucket()', - } - ), - sortText: '1A', - command: TRIGGER_SUGGESTION_COMMAND, -}; +import { byCompleteItem, dateHistogramCompleteItem, getPosition } from './util'; export async function suggest( innerText: string, @@ -103,8 +21,9 @@ export async function suggest( _columnExists: (column: string) => boolean ): Promise { const pos = getPosition(innerText, command); + switch (pos) { - case 'expression_start': + case 'expression': return allFunctions() .filter((func) => func.supportedCommands.includes('stats')) .map(getFunctionSuggestion); @@ -116,7 +35,7 @@ export async function suggest( { ...commaCompleteItem, command: TRIGGER_SUGGESTION_COMMAND, text: ', ' }, ]; - case 'grouping_expression_start': + case 'grouping_expression': const columnSuggestions = pushItUpInTheList(await getColumnsByType('any'), true); return [ ...allFunctions() diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts new file mode 100644 index 0000000000000..070522a25d31d --- /dev/null +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { ESQLCommand } from '@kbn/esql-ast'; +import { i18n } from '@kbn/i18n'; +import { + findPreviousWord, + getLastNonWhitespaceChar, + isOptionItem, + noCaseCompare, +} from '../../../shared/helpers'; +import { TIME_SYSTEM_PARAMS, TRIGGER_SUGGESTION_COMMAND } from '../../factories'; +import { SuggestionRawDefinition } from '../../types'; + +// STATS + +/** + * Position of the caret in the sort command: +* +* ``` +* STATS [column1 =] expression1[, ..., [columnN =] expressionN] [BY grouping_expression1[, ..., grouping_expressionN]] + | | | | + expression expression_complete grouping_expression grouping_expression_complete + +* ``` +*/ +export type CaretPosition = + | 'expression' + | 'expression_complete' + | 'grouping_expression' + | 'grouping_expression_complete'; + +export const getPosition = (innerText: string, command: ESQLCommand): CaretPosition => { + if ( + command.args.some( + (arg) => isOptionItem(arg) && arg.name === 'by' && arg.location.min < innerText.length + ) + ) { + if ( + getLastNonWhitespaceChar(innerText) === ',' || + noCaseCompare(findPreviousWord(innerText), 'by') + ) { + return 'grouping_expression'; + } else { + return 'grouping_expression_complete'; + } + } + + if ( + getLastNonWhitespaceChar(innerText) === ',' || + noCaseCompare(findPreviousWord(innerText), 'stats') + ) { + return 'expression'; + } else { + return 'expression_complete'; + } +}; + +export const byCompleteItem: SuggestionRawDefinition = { + label: 'BY', + text: 'BY ', + kind: 'Reference', + detail: 'By', + sortText: '1', + command: TRIGGER_SUGGESTION_COMMAND, +}; + +export const dateHistogramCompleteItem: SuggestionRawDefinition = { + label: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.addDateHistogram', { + defaultMessage: 'Add date histogram', + }), + // TODO preferences?.histogramBarTarget + text: `BUCKET($0, ${1000}, ${TIME_SYSTEM_PARAMS.join(', ')})`, + asSnippet: true, + kind: 'Issue', + detail: i18n.translate( + 'kbn-esql-validation-autocomplete.esql.autocomplete.addDateHistogramDetail', + { + defaultMessage: 'Add date histogram using bucket()', + } + ), + sortText: '1A', + command: TRIGGER_SUGGESTION_COMMAND, +}; From e4289dd957f28ad707fc825a02b7b4ae0f43fdb4 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Wed, 30 Oct 2024 08:14:44 -0600 Subject: [PATCH 11/19] fix field suggestions --- .../autocomplete.command.stats.test.ts | 26 +++++++++---------- .../src/autocomplete/commands/stats/index.ts | 9 ++++--- .../src/autocomplete/commands/stats/util.ts | 8 +++--- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts index b3884f5cb96be..d129a75069298 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts @@ -9,8 +9,8 @@ import { FieldType, FunctionReturnType } from '../../definitions/types'; import { ESQL_COMMON_NUMERIC_TYPES, ESQL_NUMBER_TYPES } from '../../shared/esql_types'; +import { getDateHistogramCompleteItem } from '../commands/stats/util'; import { allStarConstant } from '../complete_items'; -import { getAddDateHistogramSnippet } from '../factories'; import { roundParameterTypes } from './constants'; import { setup, @@ -71,7 +71,7 @@ describe('autocomplete.suggest', () => { test('on space after aggregate field', async () => { const { assertSuggestions } = await setup(); - await assertSuggestions('from a | stats a=min(b) /', ['BY $0', ',', '| ']); + await assertSuggestions('from a | stats a=min(b) /', ['BY $0', ', ', '| ']); }); test('on space after aggregate field with comma', async () => { @@ -184,7 +184,7 @@ describe('autocomplete.suggest', () => { test('when typing right paren', async () => { const { assertSuggestions } = await setup(); - await assertSuggestions('from a | stats a = min(b)/ | sort b', ['BY $0', ',', '| ']); + await assertSuggestions('from a | stats a = min(b)/ | sort b', ['BY $0', ', ', '| ']); }); test('increments suggested variable name counter', async () => { @@ -210,7 +210,7 @@ describe('autocomplete.suggest', () => { const { assertSuggestions } = await setup(); const expected = [ 'var0 = ', - getAddDateHistogramSnippet(), + getDateHistogramCompleteItem(), ...getFieldNamesByType('any').map((field) => `${field} `), ...allEvaFunctions, ...allGroupingFunctions, @@ -224,7 +224,7 @@ describe('autocomplete.suggest', () => { test('on space after grouping field', async () => { const { assertSuggestions } = await setup(); - await assertSuggestions('from a | stats a=c by d /', [',', '| ']); + await assertSuggestions('from a | stats a=c by d /', [', ', '| ']); }); test('after comma "," in grouping fields', async () => { @@ -233,7 +233,7 @@ describe('autocomplete.suggest', () => { const fields = getFieldNamesByType('any').map((field) => `${field} `); await assertSuggestions('from a | stats a=c by d, /', [ 'var0 = ', - getAddDateHistogramSnippet(), + getDateHistogramCompleteItem(), ...fields, ...allEvaFunctions, ...allGroupingFunctions, @@ -245,7 +245,7 @@ describe('autocomplete.suggest', () => { ]); await assertSuggestions('from a | stats avg(b) by c, /', [ 'var0 = ', - getAddDateHistogramSnippet(), + getDateHistogramCompleteItem(), ...fields, ...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }), ...allGroupingFunctions, @@ -266,13 +266,13 @@ describe('autocomplete.suggest', () => { ...allGroupingFunctions, ]); await assertSuggestions('from a | stats avg(b) by var0 = /', [ - getAddDateHistogramSnippet(), + getDateHistogramCompleteItem(), ...getFieldNamesByType('any').map((field) => `${field} `), ...allEvaFunctions, ...allGroupingFunctions, ]); await assertSuggestions('from a | stats avg(b) by c, var0 = /', [ - getAddDateHistogramSnippet(), + getDateHistogramCompleteItem(), ...getFieldNamesByType('any').map((field) => `${field} `), ...allEvaFunctions, ...allGroupingFunctions, @@ -282,18 +282,18 @@ describe('autocomplete.suggest', () => { test('on space after expression right hand side operand', async () => { const { assertSuggestions } = await setup(); - await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [',', '| ']); - await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [',', '| '], { + await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [', ', '| ']); + await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [', ', '| '], { triggerCharacter: ' ', }); await assertSuggestions( 'from a | stats var0 = AVG(doubleField) BY var1 = BUCKET(dateField, 1 day)/', - [',', '| ', '+ $0', '- $0'] + [', ', '| ', '+ $0', '- $0'] ); await assertSuggestions( 'from a | stats var0 = AVG(doubleField) BY var1 = BUCKET(dateField, 1 day) /', - [',', '| ', '+ $0', '- $0'], + [', ', '| ', '+ $0', '- $0'], { triggerCharacter: ' ' } ); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts index 34b1e6f5e7180..d584b9abd1b11 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts @@ -12,7 +12,7 @@ import type { GetColumnsByTypeFn, SuggestionRawDefinition } from '../../types'; import { TRIGGER_SUGGESTION_COMMAND, allFunctions, getFunctionSuggestion } from '../../factories'; import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; import { pushItUpInTheList } from '../../helper'; -import { byCompleteItem, dateHistogramCompleteItem, getPosition } from './util'; +import { byCompleteItem, getDateHistogramCompleteItem, getPosition } from './util'; export async function suggest( innerText: string, @@ -36,12 +36,15 @@ export async function suggest( ]; case 'grouping_expression': - const columnSuggestions = pushItUpInTheList(await getColumnsByType('any'), true); + const columnSuggestions = pushItUpInTheList( + await getColumnsByType('any', [], { advanceCursor: true, openSuggestions: true }), + true + ); return [ ...allFunctions() .filter((func) => func.supportedOptions?.includes('by')) .map(getFunctionSuggestion), - dateHistogramCompleteItem, + getDateHistogramCompleteItem(), ...columnSuggestions, ]; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts index 070522a25d31d..da6452d6962af 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts @@ -15,10 +15,8 @@ import { isOptionItem, noCaseCompare, } from '../../../shared/helpers'; -import { TIME_SYSTEM_PARAMS, TRIGGER_SUGGESTION_COMMAND } from '../../factories'; import { SuggestionRawDefinition } from '../../types'; - -// STATS +import { TIME_SYSTEM_PARAMS, TRIGGER_SUGGESTION_COMMAND } from '../../factories'; /** * Position of the caret in the sort command: @@ -71,7 +69,7 @@ export const byCompleteItem: SuggestionRawDefinition = { command: TRIGGER_SUGGESTION_COMMAND, }; -export const dateHistogramCompleteItem: SuggestionRawDefinition = { +export const getDateHistogramCompleteItem: () => SuggestionRawDefinition = () => ({ label: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.addDateHistogram', { defaultMessage: 'Add date histogram', }), @@ -87,4 +85,4 @@ export const dateHistogramCompleteItem: SuggestionRawDefinition = { ), sortText: '1A', command: TRIGGER_SUGGESTION_COMMAND, -}; +}); From 402105f8280e1773fe73d27fdcf8c046f6613f34 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Wed, 30 Oct 2024 11:45:38 -0600 Subject: [PATCH 12/19] create unified function suggestion routine --- .../src/autocomplete/__tests__/helpers.ts | 2 +- .../src/autocomplete/autocomplete.ts | 16 +++-- .../src/autocomplete/commands/stats/index.ts | 10 +-- .../src/autocomplete/factories.ts | 70 ++++++++++++------- .../src/autocomplete/helper.ts | 11 ++- 5 files changed, 66 insertions(+), 43 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts index 3234417c1f1a4..ebf0a0589d1e9 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts @@ -177,7 +177,7 @@ export function getFunctionSignaturesByReturnType( ({ returnType }) => expectedReturnType.includes('any') || expectedReturnType.includes(returnType as string) ); - if (!filteredByReturnType.length) { + if (!filteredByReturnType.length && !expectedReturnType.includes('any')) { return false; } if (paramsTypes?.length) { diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 1dce3fef48c8c..7d6329b76a4ae 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -70,7 +70,7 @@ import { buildSourcesDefinitions, buildNewVarDefinition, buildNoPoliciesAvailableDefinition, - getCompatibleFunctionDefinition, + getFunctionSuggestions, buildMatchingFieldsDefinition, getCompatibleLiterals, buildConstantsDefinitions, @@ -1348,12 +1348,14 @@ async function getFunctionArgsSuggestions( // Functions suggestions.push( - ...getCompatibleFunctionDefinition( - command.name, - option?.name, - canBeBooleanCondition ? ['any'] : (getTypesFromParamDefs(typesToSuggestNext) as string[]), - fnToIgnore - ).map((suggestion) => ({ + ...getFunctionSuggestions({ + command: command.name, + option: option?.name, + returnTypes: canBeBooleanCondition + ? ['any'] + : (getTypesFromParamDefs(typesToSuggestNext) as string[]), + ignored: fnToIgnore, + }).map((suggestion) => ({ ...suggestion, text: addCommaIf(shouldAddComma, suggestion.text), })) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts index d584b9abd1b11..3e83f19b6991a 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts @@ -9,7 +9,7 @@ import type { ESQLCommand } from '@kbn/esql-ast'; import type { GetColumnsByTypeFn, SuggestionRawDefinition } from '../../types'; -import { TRIGGER_SUGGESTION_COMMAND, allFunctions, getFunctionSuggestion } from '../../factories'; +import { TRIGGER_SUGGESTION_COMMAND, getFunctionSuggestions } from '../../factories'; import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; import { pushItUpInTheList } from '../../helper'; import { byCompleteItem, getDateHistogramCompleteItem, getPosition } from './util'; @@ -24,9 +24,7 @@ export async function suggest( switch (pos) { case 'expression': - return allFunctions() - .filter((func) => func.supportedCommands.includes('stats')) - .map(getFunctionSuggestion); + return getFunctionSuggestions({ command: 'stats' }); case 'expression_complete': return [ @@ -41,9 +39,7 @@ export async function suggest( true ); return [ - ...allFunctions() - .filter((func) => func.supportedOptions?.includes('by')) - .map(getFunctionSuggestion), + ...getFunctionSuggestions({ command: 'stats', option: 'by' }), getDateHistogramCompleteItem(), ...columnSuggestions, ]; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts index c543b50db94e7..197b5b623a004 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts @@ -28,7 +28,7 @@ import { ESQLRealField } from '../validation/types'; import { isNumericType } from '../shared/esql_types'; import { getTestFunctions } from '../shared/test_functions'; -export const allFunctions = memoize( +const allFunctions = memoize( () => aggregationFunctionDefinitions .concat(scalarFunctionDefinitions) @@ -91,31 +91,49 @@ export function getSuggestionBuiltinDefinition(fn: FunctionDefinition): Suggesti }; } -export const getCompatibleFunctionDefinition = ( - command: string, - option: string | undefined, - returnTypes?: string[], - ignored: string[] = [] -): SuggestionRawDefinition[] => { - const fnSupportedByCommand = allFunctions() - .filter( - ({ name, supportedCommands, supportedOptions, ignoreAsSuggestion }) => - (option ? supportedOptions?.includes(option) : supportedCommands.includes(command)) && - !ignored.includes(name) && - !ignoreAsSuggestion - ) - .sort((a, b) => a.name.localeCompare(b.name)); - if (!returnTypes) { - return fnSupportedByCommand.map(getFunctionSuggestion); - } - return fnSupportedByCommand - .filter((mathDefinition) => - mathDefinition.signatures.some( - (signature) => - returnTypes[0] === 'any' || returnTypes.includes(signature.returnType as string) - ) - ) - .map(getFunctionSuggestion); +/** + * Builds suggestions for functions based on the provided predicates. + * + * @param predicates a set of conditions that must be met for a function to be included in the suggestions + * @returns + */ +export const getFunctionSuggestions = (predicates?: { + command?: string; + option?: string | undefined; + returnTypes?: string[]; + ignored?: string[]; +}): SuggestionRawDefinition[] => { + const functions = allFunctions(); + const { command, option, returnTypes, ignored = [] } = predicates ?? {}; + const filteredFunctions: FunctionDefinition[] = functions.filter( + ({ name, supportedCommands, supportedOptions, ignoreAsSuggestion }) => { + if (ignoreAsSuggestion) { + return false; + } + + if (ignored.includes(name)) { + return false; + } + + if (option && !supportedOptions?.includes(option)) { + return false; + } + + if (command && !supportedCommands.includes(command)) { + return false; + } + + if (returnTypes && !returnTypes.includes('any')) { + return functions.some((fn) => + fn.signatures.some((signature) => returnTypes.includes(signature.returnType as string)) + ); + } + + return true; + } + ); + + return filteredFunctions.map(getFunctionSuggestion); }; export function getSuggestionCommandDefinition( diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts index 6585a04c98c59..9724878611b01 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts @@ -35,7 +35,7 @@ import { compareTypesWithLiterals } from '../shared/esql_types'; import { TIME_SYSTEM_PARAMS, buildVariablesDefinitions, - getCompatibleFunctionDefinition, + getFunctionSuggestions, getCompatibleLiterals, getDateLiterals, } from './factories'; @@ -417,7 +417,14 @@ export async function getFieldsOrFunctionsSuggestions( const suggestions = filteredFieldsByType.concat( displayDateSuggestions ? getDateLiterals() : [], - functions ? getCompatibleFunctionDefinition(commandName, optionName, types, ignoreFn) : [], + functions + ? getFunctionSuggestions({ + command: commandName, + option: optionName, + returnTypes: types, + ignored: ignoreFn, + }) + : [], variables ? pushItUpInTheList(buildVariablesDefinitions(filteredVariablesByType), functions) : [], From e4126877cd11d26216193111f087492691943a7d Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Wed, 30 Oct 2024 16:12:01 -0600 Subject: [PATCH 13/19] assignment suggestion --- .../__tests__/autocomplete.command.stats.test.ts | 3 +-- .../src/autocomplete/autocomplete.ts | 16 ++++++++++------ .../src/autocomplete/commands/stats/index.ts | 15 ++++++++++++--- .../src/autocomplete/factories.ts | 2 +- .../src/definitions/types.ts | 3 ++- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts index d129a75069298..2c974579f820f 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts @@ -192,9 +192,8 @@ describe('autocomplete.suggest', () => { await assertSuggestions('from a | eval var0=round(b), var1=round(c) | stats /', [ 'var2 = ', + // TODO verify that this change is ok ...allAggFunctions, - 'var0', - 'var1', ...allEvaFunctions, ]); await assertSuggestions('from a | stats var0=min(b),var1=c,/', [ diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 7d6329b76a4ae..ef9b9e6c959f0 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -68,7 +68,7 @@ import { buildFieldsDefinitions, buildPoliciesDefinitions, buildSourcesDefinitions, - buildNewVarDefinition, + getNewVariableSuggestion, buildNoPoliciesAvailableDefinition, getFunctionSuggestions, buildMatchingFieldsDefinition, @@ -471,8 +471,12 @@ async function getSuggestionsWithinCommand( const references = { fields: fieldsMap, variables: anyVariables }; if (commandDef.suggest) { // The new path. - return commandDef.suggest(innerText, command, getColumnsByType, (col: string) => - Boolean(getColumnByName(col, references)) + return commandDef.suggest( + innerText, + command, + getColumnsByType, + (col: string) => Boolean(getColumnByName(col, references)), + () => findNewVariable(anyVariables) ); } else { // The deprecated path. @@ -631,7 +635,7 @@ async function getExpressionSuggestionsByType( // ... | STATS ..., // ... | EVAL // ... | EVAL ..., - suggestions.push(buildNewVarDefinition(findNewVariable(anyVariables))); + suggestions.push(getNewVariableSuggestion(findNewVariable(anyVariables))); } } } @@ -1575,7 +1579,7 @@ async function getOptionArgsSuggestions( ); if (isNewExpression || noCaseCompare(findPreviousWord(innerText), 'WITH')) { - suggestions.push(buildNewVarDefinition(findNewVariable(anyEnhancedVariables))); + suggestions.push(getNewVariableSuggestion(findNewVariable(anyEnhancedVariables))); } // make sure to remove the marker arg from the assign fn @@ -1814,7 +1818,7 @@ async function getOptionArgsSuggestions( } if (command.name === 'stats' && isNewExpression && canHaveAssignment) { - suggestions.push(buildNewVarDefinition(findNewVariable(anyVariables))); + suggestions.push(getNewVariableSuggestion(findNewVariable(anyVariables))); } } } diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts index 3e83f19b6991a..f52865f0eb246 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts @@ -9,7 +9,11 @@ import type { ESQLCommand } from '@kbn/esql-ast'; import type { GetColumnsByTypeFn, SuggestionRawDefinition } from '../../types'; -import { TRIGGER_SUGGESTION_COMMAND, getFunctionSuggestions } from '../../factories'; +import { + TRIGGER_SUGGESTION_COMMAND, + getNewVariableSuggestion, + getFunctionSuggestions, +} from '../../factories'; import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; import { pushItUpInTheList } from '../../helper'; import { byCompleteItem, getDateHistogramCompleteItem, getPosition } from './util'; @@ -18,13 +22,17 @@ export async function suggest( innerText: string, command: ESQLCommand<'stats'>, getColumnsByType: GetColumnsByTypeFn, - _columnExists: (column: string) => boolean + _columnExists: (column: string) => boolean, + getSuggestedVariableName: () => string ): Promise { const pos = getPosition(innerText, command); switch (pos) { case 'expression': - return getFunctionSuggestions({ command: 'stats' }); + return [ + ...getFunctionSuggestions({ command: 'stats' }), + getNewVariableSuggestion(getSuggestedVariableName()), + ]; case 'expression_complete': return [ @@ -42,6 +50,7 @@ export async function suggest( ...getFunctionSuggestions({ command: 'stats', option: 'by' }), getDateHistogramCompleteItem(), ...columnSuggestions, + getNewVariableSuggestion(getSuggestedVariableName()), ]; case 'grouping_expression_complete': diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts index 197b5b623a004..ef2cb22ca4e05 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts @@ -267,7 +267,7 @@ export const buildValueDefinitions = ( command: options?.advanceCursorAndOpenSuggestions ? TRIGGER_SUGGESTION_COMMAND : undefined, })); -export const buildNewVarDefinition = (label: string): SuggestionRawDefinition => { +export const getNewVariableSuggestion = (label: string): SuggestionRawDefinition => { return { label, text: `${label} = `, diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts index a83908b41617f..19ef3b1ed5929 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts @@ -171,7 +171,8 @@ export interface CommandBaseDefinition { innerText: string, command: ESQLCommand, getColumnsByType: GetColumnsByTypeFn, - columnExists: (column: string) => boolean + columnExists: (column: string) => boolean, + getSuggestedVariableName: () => string ) => Promise; /** @deprecated this property will disappear in the future */ signature: { From 3bb659d23a46f7e6fe4983eef5177ebe56d62d8b Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Wed, 30 Oct 2024 18:43:04 -0600 Subject: [PATCH 14/19] fix function suggestions --- .../src/autocomplete/factories.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts index ef2cb22ca4e05..9b7e2b0bf71a5 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts @@ -106,7 +106,7 @@ export const getFunctionSuggestions = (predicates?: { const functions = allFunctions(); const { command, option, returnTypes, ignored = [] } = predicates ?? {}; const filteredFunctions: FunctionDefinition[] = functions.filter( - ({ name, supportedCommands, supportedOptions, ignoreAsSuggestion }) => { + ({ name, supportedCommands, supportedOptions, ignoreAsSuggestion, signatures }) => { if (ignoreAsSuggestion) { return false; } @@ -124,9 +124,7 @@ export const getFunctionSuggestions = (predicates?: { } if (returnTypes && !returnTypes.includes('any')) { - return functions.some((fn) => - fn.signatures.some((signature) => returnTypes.includes(signature.returnType as string)) - ); + return signatures.some((signature) => returnTypes.includes(signature.returnType as string)); } return true; From d69963c749a7d0a9c817c5bc8eb8c04ef2b5c56e Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Wed, 30 Oct 2024 18:58:31 -0600 Subject: [PATCH 15/19] handle assignments --- .../autocomplete.command.stats.test.ts | 4 ++-- .../src/autocomplete/autocomplete.test.ts | 4 ++-- .../src/autocomplete/commands/stats/index.ts | 5 ++++- .../src/autocomplete/commands/stats/util.ts | 18 ++++++++++++++---- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts index 2c974579f820f..590dc120d216f 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts @@ -71,7 +71,7 @@ describe('autocomplete.suggest', () => { test('on space after aggregate field', async () => { const { assertSuggestions } = await setup(); - await assertSuggestions('from a | stats a=min(b) /', ['BY $0', ', ', '| ']); + await assertSuggestions('from a | stats a=min(b) /', ['BY ', ', ', '| ']); }); test('on space after aggregate field with comma', async () => { @@ -184,7 +184,7 @@ describe('autocomplete.suggest', () => { test('when typing right paren', async () => { const { assertSuggestions } = await setup(); - await assertSuggestions('from a | stats a = min(b)/ | sort b', ['BY $0', ', ', '| ']); + await assertSuggestions('from a | stats a = min(b)/ | sort b', ['BY ', ', ', '| ']); }); test('increments suggested variable name counter', async () => { diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index b89be15d670b1..4f3cd0d31cda7 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -737,7 +737,7 @@ describe('autocomplete', () => { ]); // STATS argument BY - testSuggestions('FROM index1 | STATS AVG(booleanField) B/', ['BY $0', ',', '| ']); + testSuggestions('FROM index1 | STATS AVG(booleanField) B/', ['BY ', ',', '| ']); // STATS argument BY expression testSuggestions('FROM index1 | STATS field BY f/', [ @@ -1073,7 +1073,7 @@ describe('autocomplete', () => { // STATS argument BY testSuggestions('FROM a | STATS AVG(numberField) /', [ ',', - attachAsSnippet(attachTriggerCommand('BY $0')), + attachAsSnippet(attachTriggerCommand('BY ')), attachTriggerCommand('| '), ]); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts index f52865f0eb246..8fa115fe009b1 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts @@ -28,12 +28,15 @@ export async function suggest( const pos = getPosition(innerText, command); switch (pos) { - case 'expression': + case 'expression_without_assignment': return [ ...getFunctionSuggestions({ command: 'stats' }), getNewVariableSuggestion(getSuggestedVariableName()), ]; + case 'expression_after_assignment': + return [...getFunctionSuggestions({ command: 'stats' })]; + case 'expression_complete': return [ byCompleteItem, diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts index da6452d6962af..8239e6130cfdd 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts @@ -12,6 +12,8 @@ import { i18n } from '@kbn/i18n'; import { findPreviousWord, getLastNonWhitespaceChar, + isAssignment, + isAssignmentComplete, isOptionItem, noCaseCompare, } from '../../../shared/helpers'; @@ -23,13 +25,16 @@ import { TIME_SYSTEM_PARAMS, TRIGGER_SUGGESTION_COMMAND } from '../../factories' * * ``` * STATS [column1 =] expression1[, ..., [columnN =] expressionN] [BY grouping_expression1[, ..., grouping_expressionN]] - | | | | - expression expression_complete grouping_expression grouping_expression_complete + | | | | | + | | expression_complete grouping_expression grouping_expression_complete + | expression_after_assignment + expression_without_assignment * ``` */ export type CaretPosition = - | 'expression' + | 'expression_without_assignment' + | 'expression_after_assignment' | 'expression_complete' | 'grouping_expression' | 'grouping_expression_complete'; @@ -50,11 +55,16 @@ export const getPosition = (innerText: string, command: ESQLCommand): CaretPosit } } + const lastArg = command.args[command.args.length - 1]; + if (isAssignment(lastArg) && !isAssignmentComplete(lastArg)) { + return 'expression_after_assignment'; + } + if ( getLastNonWhitespaceChar(innerText) === ',' || noCaseCompare(findPreviousWord(innerText), 'stats') ) { - return 'expression'; + return 'expression_without_assignment'; } else { return 'expression_complete'; } From 514c082e76674c6ec1ce132baf4efb3e29f5c98f Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Thu, 31 Oct 2024 13:52:45 -0600 Subject: [PATCH 16/19] fix stats behavior --- .../autocomplete.command.stats.test.ts | 9 ++--- .../src/autocomplete/commands/stats/index.ts | 18 +++++++--- .../src/autocomplete/commands/stats/util.ts | 33 +++++++++++-------- .../src/shared/context.ts | 6 +--- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts index 590dc120d216f..8a5c6c2bf30de 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts @@ -261,7 +261,6 @@ describe('autocomplete.suggest', () => { ...getFunctionSignaturesByReturnType('eval', ['integer', 'double', 'long'], { scalar: true, }), - ...allGroupingFunctions, ]); await assertSuggestions('from a | stats avg(b) by var0 = /', [ @@ -281,21 +280,17 @@ describe('autocomplete.suggest', () => { test('on space after expression right hand side operand', async () => { const { assertSuggestions } = await setup(); - await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [', ', '| ']); await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [', ', '| '], { triggerCharacter: ' ', }); - await assertSuggestions( - 'from a | stats var0 = AVG(doubleField) BY var1 = BUCKET(dateField, 1 day)/', - [', ', '| ', '+ $0', '- $0'] - ); await assertSuggestions( 'from a | stats var0 = AVG(doubleField) BY var1 = BUCKET(dateField, 1 day) /', - [', ', '| ', '+ $0', '- $0'], + [', ', '| '], { triggerCharacter: ' ' } ); }); + test('on space within bucket()', async () => { const { assertSuggestions } = await setup(); await assertSuggestions('from a | stats avg(b) by BUCKET(/, 50, ?_tstart, ?_tend)', [ diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts index 8fa115fe009b1..e04305ccde961 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts @@ -27,6 +27,11 @@ export async function suggest( ): Promise { const pos = getPosition(innerText, command); + const columnSuggestions = pushItUpInTheList( + await getColumnsByType('any', [], { advanceCursor: true, openSuggestions: true }), + true + ); + switch (pos) { case 'expression_without_assignment': return [ @@ -44,11 +49,14 @@ export async function suggest( { ...commaCompleteItem, command: TRIGGER_SUGGESTION_COMMAND, text: ', ' }, ]; - case 'grouping_expression': - const columnSuggestions = pushItUpInTheList( - await getColumnsByType('any', [], { advanceCursor: true, openSuggestions: true }), - true - ); + case 'grouping_expression_after_assignment': + return [ + ...getFunctionSuggestions({ command: 'stats', option: 'by' }), + getDateHistogramCompleteItem(), + ...columnSuggestions, + ]; + + case 'grouping_expression_without_assignment': return [ ...getFunctionSuggestions({ command: 'stats', option: 'by' }), getDateHistogramCompleteItem(), diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts index 8239e6130cfdd..a683f00bfa87e 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts @@ -24,11 +24,11 @@ import { TIME_SYSTEM_PARAMS, TRIGGER_SUGGESTION_COMMAND } from '../../factories' * Position of the caret in the sort command: * * ``` -* STATS [column1 =] expression1[, ..., [columnN =] expressionN] [BY grouping_expression1[, ..., grouping_expressionN]] - | | | | | - | | expression_complete grouping_expression grouping_expression_complete - | expression_after_assignment - expression_without_assignment +* STATS [column1 =] expression1[, ..., [columnN =] expressionN] [BY [column1 =] grouping_expression1[, ..., grouping_expressionN]] + | | | | | | + | | expression_complete | | grouping_expression_complete + | expression_after_assignment | grouping_expression_after_assignment + expression_without_assignment grouping_expression_without_assignment * ``` */ @@ -36,27 +36,32 @@ export type CaretPosition = | 'expression_without_assignment' | 'expression_after_assignment' | 'expression_complete' - | 'grouping_expression' + | 'grouping_expression_without_assignment' + | 'grouping_expression_after_assignment' | 'grouping_expression_complete'; export const getPosition = (innerText: string, command: ESQLCommand): CaretPosition => { - if ( - command.args.some( - (arg) => isOptionItem(arg) && arg.name === 'by' && arg.location.min < innerText.length - ) - ) { + const lastCommandArg = command.args[command.args.length - 1]; + + if (isOptionItem(lastCommandArg) && lastCommandArg.name === 'by') { + // in the BY clause + + const lastOptionArg = lastCommandArg.args[lastCommandArg.args.length - 1]; + if (isAssignment(lastOptionArg) && !isAssignmentComplete(lastOptionArg)) { + return 'grouping_expression_after_assignment'; + } + if ( getLastNonWhitespaceChar(innerText) === ',' || noCaseCompare(findPreviousWord(innerText), 'by') ) { - return 'grouping_expression'; + return 'grouping_expression_without_assignment'; } else { return 'grouping_expression_complete'; } } - const lastArg = command.args[command.args.length - 1]; - if (isAssignment(lastArg) && !isAssignmentComplete(lastArg)) { + if (isAssignment(lastCommandArg) && !isAssignmentComplete(lastCommandArg)) { return 'expression_after_assignment'; } diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/context.ts b/packages/kbn-esql-validation-autocomplete/src/shared/context.ts index e4993ae5d0a06..db9ccead3da16 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/context.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/context.ts @@ -21,7 +21,6 @@ import { EDITOR_MARKER } from './constants'; import { isOptionItem, isColumnItem, - getFunctionDefinition, isSourceItem, isSettingItem, pipePrecedesCurrentWord, @@ -133,9 +132,6 @@ function findAstPosition(ast: ESQLAst, offset: number) { function isNotEnrichClauseAssigment(node: ESQLFunction, command: ESQLCommand) { return node.name !== '=' && command.name !== 'enrich'; } -function isBuiltinFunction(node: ESQLFunction) { - return getFunctionDefinition(node.name)?.type === 'builtin'; -} /** * Given a ES|QL query string, its AST and the cursor position, @@ -162,7 +158,7 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number) // command ... a in ( ) return { type: 'list' as const, command, node, option, setting }; } - if (isNotEnrichClauseAssigment(node, command) && !isBuiltinFunction(node)) { + if (isNotEnrichClauseAssigment(node, command)) { // command ... fn( ) return { type: 'function' as const, command, node, option, setting }; } From a8d2c25eb50baecaa6ff7f6aa94ef9a95b07f82f Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Thu, 31 Oct 2024 14:24:10 -0600 Subject: [PATCH 17/19] make exception only for stats --- .../__tests__/autocomplete.command.stats.test.ts | 12 ++++++------ .../src/autocomplete/autocomplete.test.ts | 14 +++++++------- .../src/autocomplete/autocomplete.ts | 6 ++---- .../src/autocomplete/commands/stats/index.ts | 6 +++--- .../src/autocomplete/commands/stats/util.ts | 2 +- .../src/shared/context.ts | 14 +++++++++++++- 6 files changed, 32 insertions(+), 22 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts index 8a5c6c2bf30de..d4c864d6221e0 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts @@ -9,7 +9,7 @@ import { FieldType, FunctionReturnType } from '../../definitions/types'; import { ESQL_COMMON_NUMERIC_TYPES, ESQL_NUMBER_TYPES } from '../../shared/esql_types'; -import { getDateHistogramCompleteItem } from '../commands/stats/util'; +import { getDateHistogramCompletionItem } from '../commands/stats/util'; import { allStarConstant } from '../complete_items'; import { roundParameterTypes } from './constants'; import { @@ -209,7 +209,7 @@ describe('autocomplete.suggest', () => { const { assertSuggestions } = await setup(); const expected = [ 'var0 = ', - getDateHistogramCompleteItem(), + getDateHistogramCompletionItem(), ...getFieldNamesByType('any').map((field) => `${field} `), ...allEvaFunctions, ...allGroupingFunctions, @@ -232,7 +232,7 @@ describe('autocomplete.suggest', () => { const fields = getFieldNamesByType('any').map((field) => `${field} `); await assertSuggestions('from a | stats a=c by d, /', [ 'var0 = ', - getDateHistogramCompleteItem(), + getDateHistogramCompletionItem(), ...fields, ...allEvaFunctions, ...allGroupingFunctions, @@ -244,7 +244,7 @@ describe('autocomplete.suggest', () => { ]); await assertSuggestions('from a | stats avg(b) by c, /', [ 'var0 = ', - getDateHistogramCompleteItem(), + getDateHistogramCompletionItem(), ...fields, ...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }), ...allGroupingFunctions, @@ -264,13 +264,13 @@ describe('autocomplete.suggest', () => { ...allGroupingFunctions, ]); await assertSuggestions('from a | stats avg(b) by var0 = /', [ - getDateHistogramCompleteItem(), + getDateHistogramCompletionItem(), ...getFieldNamesByType('any').map((field) => `${field} `), ...allEvaFunctions, ...allGroupingFunctions, ]); await assertSuggestions('from a | stats avg(b) by c, var0 = /', [ - getDateHistogramCompleteItem(), + getDateHistogramCompletionItem(), ...getFieldNamesByType('any').map((field) => `${field} `), ...allEvaFunctions, ...allGroupingFunctions, diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index 4f3cd0d31cda7..7a1b2c191b400 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -12,7 +12,6 @@ import { scalarFunctionDefinitions } from '../definitions/generated/scalar_funct import { timeUnitsToSuggest } from '../definitions/literals'; import { commandDefinitions as unmodifiedCommandDefinitions } from '../definitions/commands'; import { - getAddDateHistogramSnippet, getDateLiterals, getSafeInsertText, TIME_SYSTEM_PARAMS, @@ -38,6 +37,7 @@ import { METADATA_FIELDS } from '../shared/constants'; import { ESQL_COMMON_NUMERIC_TYPES, ESQL_STRING_TYPES } from '../shared/esql_types'; import { log10ParameterTypes, powParameterTypes } from './__tests__/constants'; import { getRecommendedQueries } from './recommended_queries/templates'; +import { getDateHistogramCompletionItem } from './commands/stats/util'; const commandDefinitions = unmodifiedCommandDefinitions.filter(({ hidden }) => !hidden); @@ -737,12 +737,12 @@ describe('autocomplete', () => { ]); // STATS argument BY - testSuggestions('FROM index1 | STATS AVG(booleanField) B/', ['BY ', ',', '| ']); + testSuggestions('FROM index1 | STATS AVG(booleanField) B/', ['BY ', ', ', '| ']); // STATS argument BY expression testSuggestions('FROM index1 | STATS field BY f/', [ 'var0 = ', - getAddDateHistogramSnippet(), + getDateHistogramCompletionItem(), ...getFunctionSignaturesByReturnType('stats', 'any', { grouping: true, scalar: true }), ...getFieldNamesByType('any').map((field) => `${field} `), ]); @@ -1072,8 +1072,8 @@ describe('autocomplete', () => { // STATS argument BY testSuggestions('FROM a | STATS AVG(numberField) /', [ - ',', - attachAsSnippet(attachTriggerCommand('BY ')), + ', ', + attachTriggerCommand('BY '), attachTriggerCommand('| '), ]); @@ -1090,7 +1090,7 @@ describe('autocomplete', () => { 'by' ); testSuggestions('FROM a | STATS AVG(numberField) BY /', [ - getAddDateHistogramSnippet(), + getDateHistogramCompletionItem(), attachTriggerCommand('var0 = '), ...getFieldNamesByType('any') .map((field) => `${field} `) @@ -1100,7 +1100,7 @@ describe('autocomplete', () => { // STATS argument BY assignment (checking field suggestions) testSuggestions('FROM a | STATS AVG(numberField) BY var0 = /', [ - getAddDateHistogramSnippet(), + getDateHistogramCompletionItem(), ...getFieldNamesByType('any') .map((field) => `${field} `) .map(attachTriggerCommand), diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index ef9b9e6c959f0..d86b3c77542b3 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -206,9 +206,7 @@ export async function suggest( } if (astContext.type === 'expression') { - // suggest next possible argument, or option - // otherwise a variable - return getSuggestionsWithinCommand( + return getSuggestionsWithinCommandExpression( innerText, ast, astContext, @@ -444,7 +442,7 @@ function extractArgMeta( return { argIndex, prevIndex, lastArg, nodeArg }; } -async function getSuggestionsWithinCommand( +async function getSuggestionsWithinCommandExpression( innerText: string, commands: ESQLCommand[], { diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts index e04305ccde961..b35f8f6e941dc 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts @@ -16,7 +16,7 @@ import { } from '../../factories'; import { commaCompleteItem, pipeCompleteItem } from '../../complete_items'; import { pushItUpInTheList } from '../../helper'; -import { byCompleteItem, getDateHistogramCompleteItem, getPosition } from './util'; +import { byCompleteItem, getDateHistogramCompletionItem, getPosition } from './util'; export async function suggest( innerText: string, @@ -52,14 +52,14 @@ export async function suggest( case 'grouping_expression_after_assignment': return [ ...getFunctionSuggestions({ command: 'stats', option: 'by' }), - getDateHistogramCompleteItem(), + getDateHistogramCompletionItem(), ...columnSuggestions, ]; case 'grouping_expression_without_assignment': return [ ...getFunctionSuggestions({ command: 'stats', option: 'by' }), - getDateHistogramCompleteItem(), + getDateHistogramCompletionItem(), ...columnSuggestions, getNewVariableSuggestion(getSuggestedVariableName()), ]; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts index a683f00bfa87e..8a8e52d397555 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts @@ -84,7 +84,7 @@ export const byCompleteItem: SuggestionRawDefinition = { command: TRIGGER_SUGGESTION_COMMAND, }; -export const getDateHistogramCompleteItem: () => SuggestionRawDefinition = () => ({ +export const getDateHistogramCompletionItem: () => SuggestionRawDefinition = () => ({ label: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.addDateHistogram', { defaultMessage: 'Add date histogram', }), diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/context.ts b/packages/kbn-esql-validation-autocomplete/src/shared/context.ts index db9ccead3da16..2de9d7290ab5c 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/context.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/context.ts @@ -24,6 +24,7 @@ import { isSourceItem, isSettingItem, pipePrecedesCurrentWord, + getFunctionDefinition, } from './helpers'; function findNode(nodes: ESQLAstItem[], offset: number): ESQLSingleAstItem | undefined { @@ -133,6 +134,10 @@ function isNotEnrichClauseAssigment(node: ESQLFunction, command: ESQLCommand) { return node.name !== '=' && command.name !== 'enrich'; } +function isBuiltinFunction(node: ESQLFunction) { + return getFunctionDefinition(node.name)?.type === 'builtin'; +} + /** * Given a ES|QL query string, its AST and the cursor position, * it returns the type of context for the position ("list", "function", "option", "setting", "expression", "newCommand") @@ -158,7 +163,14 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number) // command ... a in ( ) return { type: 'list' as const, command, node, option, setting }; } - if (isNotEnrichClauseAssigment(node, command)) { + if ( + isNotEnrichClauseAssigment(node, command) && + // Temporarily mangling the logic here to let operators + // be handled as functions for the stats command. + // I expect this to simplify once https://github.com/elastic/kibana/issues/195418 + // is complete + !(isBuiltinFunction(node) && command.name !== 'stats') + ) { // command ... fn( ) return { type: 'function' as const, command, node, option, setting }; } From de7d6348d95b9fbba08fbd1a61d0af133d5dcb66 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Thu, 31 Oct 2024 15:48:16 -0600 Subject: [PATCH 18/19] restore preferences behavior --- .../autocomplete.command.stats.test.ts | 23 ++++ .../src/autocomplete/autocomplete.ts | 122 ++---------------- .../src/autocomplete/commands/stats/index.ts | 7 +- .../src/autocomplete/commands/stats/util.ts | 7 +- .../src/definitions/types.ts | 3 +- 5 files changed, 42 insertions(+), 120 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts index d4c864d6221e0..829c12f7dabba 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts @@ -324,6 +324,29 @@ describe('autocomplete.suggest', () => { const suggestions = await suggest('from a | stats count(/)'); expect(suggestions).toContain(allStarConstant); }); + + describe('date histogram snippet', () => { + test('uses histogramBarTarget preference when available', async () => { + const { suggest } = await setup(); + const histogramBarTarget = Math.random() * 100; + const expectedCompletionItem = getDateHistogramCompletionItem(histogramBarTarget); + + const suggestions = await suggest('FROM a | STATS BY /', { + callbacks: { getPreferences: () => Promise.resolve({ histogramBarTarget }) }, + }); + + expect(suggestions).toContainEqual(expectedCompletionItem); + }); + + test('defaults gracefully', async () => { + const { suggest } = await setup(); + const expectedCompletionItem = getDateHistogramCompletionItem(); + + const suggestions = await suggest('FROM a | STATS BY /'); + + expect(suggestions).toContainEqual(expectedCompletionItem); + }); + }); }); }); }); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index d86b3c77542b3..eeb4ccd0d0d2e 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -16,7 +16,6 @@ import type { ESQLFunction, ESQLSingleAstItem, } from '@kbn/esql-ast'; -import { i18n } from '@kbn/i18n'; import { ESQL_NUMBER_TYPES, isNumericType } from '../shared/esql_types'; import type { EditorContext, ItemKind, SuggestionRawDefinition, GetColumnsByTypeFn } from './types'; import { @@ -81,7 +80,6 @@ import { getDateLiterals, buildFieldsDefinitionsWithMetadata, TRIGGER_SUGGESTION_COMMAND, - getAddDateHistogramSnippet, } from './factories'; import { EDITOR_MARKER, METADATA_FIELDS } from '../shared/constants'; import { getAstContext, removeMarkerArgFromArgsList } from '../shared/context'; @@ -109,7 +107,6 @@ import { import { FunctionParameter, isParameterType, isReturnType } from '../definitions/types'; import { metadataOption } from '../definitions/options'; import { comparisonFunctions } from '../definitions/builtin'; -import { countBracketsUnclosed } from '../shared/helpers'; import { getRecommendedQueriesSuggestions } from './recommended_queries/suggestions'; type GetFieldsMapFn = () => Promise>; @@ -214,7 +211,8 @@ export async function suggest( getFieldsByType, getFieldsMap, getPolicies, - getPolicyMetadata + getPolicyMetadata, + resourceRetriever?.getPreferences ); } if (astContext.type === 'setting') { @@ -237,8 +235,7 @@ export async function suggest( { option, ...rest }, getFieldsByType, getFieldsMap, - getPolicyMetadata, - resourceRetriever?.getPreferences + getPolicyMetadata ); } } @@ -458,7 +455,8 @@ async function getSuggestionsWithinCommandExpression( getColumnsByType: GetColumnsByTypeFn, getFieldsMap: GetFieldsMapFn, getPolicies: GetPoliciesFn, - getPolicyMetadata: GetPolicyMetadataFn + getPolicyMetadata: GetPolicyMetadataFn, + getPreferences?: () => Promise<{ histogramBarTarget: number } | undefined> ) { const commandDef = getCommandDefinition(command.name); @@ -474,7 +472,8 @@ async function getSuggestionsWithinCommandExpression( command, getColumnsByType, (col: string) => Boolean(getColumnByName(col, references)), - () => findNewVariable(anyVariables) + () => findNewVariable(anyVariables), + getPreferences ); } else { // The deprecated path. @@ -1513,29 +1512,19 @@ async function getOptionArgsSuggestions( }, getFieldsByType: GetColumnsByTypeFn, getFieldsMaps: GetFieldsMapFn, - getPolicyMetadata: GetPolicyMetadataFn, - getPreferences?: () => Promise<{ histogramBarTarget: number } | undefined> + getPolicyMetadata: GetPolicyMetadataFn ) { - let preferences: { histogramBarTarget: number } | undefined; - if (getPreferences) { - preferences = await getPreferences(); - } - const optionDef = getCommandOption(option.name); if (!optionDef || !optionDef.signature) { return []; } - const { nodeArg, argIndex, lastArg } = extractArgMeta(option, node); + const { nodeArg, lastArg } = extractArgMeta(option, node); const suggestions = []; const isNewExpression = isRestartingExpression(innerText) || option.args.length === 0; const fieldsMap = await getFieldsMaps(); const anyVariables = collectVariables(commands, fieldsMap, innerText); - const references = { - fields: fieldsMap, - variables: anyVariables, - }; if (command.name === 'enrich') { if (option.name === 'on') { // if it's a new expression, suggest fields to match on @@ -1700,53 +1689,6 @@ async function getOptionArgsSuggestions( } } - if (command.name === 'stats') { - const argDef = optionDef?.signature.params[argIndex]; - - const nodeArgType = extractTypeFromASTArg(nodeArg, references); - // These cases can happen here, so need to identify each and provide the right suggestion - // i.e. ... | STATS ... BY field + - // i.e. ... | STATS ... BY field >= - - if (nodeArgType) { - if (isFunctionItem(nodeArg) && !isFunctionArgComplete(nodeArg, references).complete) { - suggestions.push( - ...(await getBuiltinFunctionNextArgument( - innerText, - command, - option, - { type: argDef?.type || 'unknown' }, - nodeArg, - nodeArgType as string, - { - fields: references.fields, - // you can't use a variable defined - // in the stats command in the by clause - variables: new Map(), - }, - getFieldsByType - )) - ); - } - } - - // If it's a complete expression then propose some final suggestions - if ( - (!nodeArgType && - option.name === 'by' && - option.args.length && - !isNewExpression && - !isAssignment(lastArg)) || - (isAssignment(lastArg) && isAssignmentComplete(lastArg)) - ) { - suggestions.push( - ...getFinalSuggestions({ - comma: optionDef?.signature.multipleParams ?? option.name === 'by', - }) - ); - } - } - if (optionDef) { if (!suggestions.length) { const argDefIndex = optionDef.signature.multipleParams @@ -1772,52 +1714,6 @@ async function getOptionArgsSuggestions( openSuggestions: true, })) ); - // Checks if cursor is still within function () - // by checking if the marker editor/cursor is within an unclosed parenthesis - const canHaveAssignment = countBracketsUnclosed('(', innerText) === 0; - - if (option.name === 'by') { - // Add quick snippet for for stats ... by bucket(<>) - if (command.name === 'stats' && canHaveAssignment) { - suggestions.push({ - label: i18n.translate( - 'kbn-esql-validation-autocomplete.esql.autocomplete.addDateHistogram', - { - defaultMessage: 'Add date histogram', - } - ), - text: getAddDateHistogramSnippet(preferences?.histogramBarTarget), - asSnippet: true, - kind: 'Issue', - detail: i18n.translate( - 'kbn-esql-validation-autocomplete.esql.autocomplete.addDateHistogramDetail', - { - defaultMessage: 'Add date histogram using bucket()', - } - ), - sortText: '1A', - command: TRIGGER_SUGGESTION_COMMAND, - } as SuggestionRawDefinition); - } - - suggestions.push( - ...(await getFieldsOrFunctionsSuggestions( - types[0] === 'column' ? ['any'] : types, - command.name, - option.name, - getFieldsByType, - { - functions: true, - fields: false, - }, - { ignoreFn: canHaveAssignment ? [] : ['bucket', 'case'] } - )) - ); - } - - if (command.name === 'stats' && isNewExpression && canHaveAssignment) { - suggestions.push(getNewVariableSuggestion(findNewVariable(anyVariables))); - } } } } diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts index b35f8f6e941dc..46a37d36eacc9 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/index.ts @@ -23,7 +23,8 @@ export async function suggest( command: ESQLCommand<'stats'>, getColumnsByType: GetColumnsByTypeFn, _columnExists: (column: string) => boolean, - getSuggestedVariableName: () => string + getSuggestedVariableName: () => string, + getPreferences?: () => Promise<{ histogramBarTarget: number } | undefined> ): Promise { const pos = getPosition(innerText, command); @@ -52,14 +53,14 @@ export async function suggest( case 'grouping_expression_after_assignment': return [ ...getFunctionSuggestions({ command: 'stats', option: 'by' }), - getDateHistogramCompletionItem(), + getDateHistogramCompletionItem((await getPreferences?.())?.histogramBarTarget), ...columnSuggestions, ]; case 'grouping_expression_without_assignment': return [ ...getFunctionSuggestions({ command: 'stats', option: 'by' }), - getDateHistogramCompletionItem(), + getDateHistogramCompletionItem((await getPreferences?.())?.histogramBarTarget), ...columnSuggestions, getNewVariableSuggestion(getSuggestedVariableName()), ]; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts index 8a8e52d397555..c9abaa5c5408a 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/commands/stats/util.ts @@ -84,12 +84,13 @@ export const byCompleteItem: SuggestionRawDefinition = { command: TRIGGER_SUGGESTION_COMMAND, }; -export const getDateHistogramCompletionItem: () => SuggestionRawDefinition = () => ({ +export const getDateHistogramCompletionItem: ( + histogramBarTarget?: number +) => SuggestionRawDefinition = (histogramBarTarget: number = 50) => ({ label: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.addDateHistogram', { defaultMessage: 'Add date histogram', }), - // TODO preferences?.histogramBarTarget - text: `BUCKET($0, ${1000}, ${TIME_SYSTEM_PARAMS.join(', ')})`, + text: `BUCKET($0, ${histogramBarTarget}, ${TIME_SYSTEM_PARAMS.join(', ')})`, asSnippet: true, kind: 'Issue', detail: i18n.translate( diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts index 19ef3b1ed5929..ff461683d8e76 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts @@ -172,7 +172,8 @@ export interface CommandBaseDefinition { command: ESQLCommand, getColumnsByType: GetColumnsByTypeFn, columnExists: (column: string) => boolean, - getSuggestedVariableName: () => string + getSuggestedVariableName: () => string, + getPreferences?: () => Promise<{ histogramBarTarget: number } | undefined> ) => Promise; /** @deprecated this property will disappear in the future */ signature: { From 46cf8aa280b3cb78262dbbe6f66695fa789f7af8 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Thu, 31 Oct 2024 15:53:41 -0600 Subject: [PATCH 19/19] add deprecation comment --- .../src/autocomplete/autocomplete.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index eeb4ccd0d0d2e..7b0f4191dcaca 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -1498,6 +1498,10 @@ async function getSettingArgsSuggestions( return suggestions; } +/** + * @deprecated — this will disappear when https://github.com/elastic/kibana/issues/195418 is complete + * because "options" will be handled in imperative command-specific routines instead of being independent. + */ async function getOptionArgsSuggestions( innerText: string, commands: ESQLCommand[],