diff --git a/packages/kbn-esql-validation-autocomplete/index.ts b/packages/kbn-esql-validation-autocomplete/index.ts index 31bd8c16b76fba..77643f07867853 100644 --- a/packages/kbn-esql-validation-autocomplete/index.ts +++ b/packages/kbn-esql-validation-autocomplete/index.ts @@ -49,7 +49,7 @@ export { getCommandDefinition, getAllCommands, getCommandOption, - lookupColumn, + getColumnForASTNode as lookupColumn, shouldBeQuotedText, printFunctionSignature, checkFunctionArgMatchesDefinition as isEqualType, diff --git a/packages/kbn-esql-validation-autocomplete/scripts/generate_function_validation_tests.ts b/packages/kbn-esql-validation-autocomplete/scripts/generate_function_validation_tests.ts index 02e37108db7b89..71bca915511dee 100644 --- a/packages/kbn-esql-validation-autocomplete/scripts/generate_function_validation_tests.ts +++ b/packages/kbn-esql-validation-autocomplete/scripts/generate_function_validation_tests.ts @@ -23,6 +23,7 @@ import { dataTypes, fieldTypes, isFieldType, + FunctionParameter, } from '../src/definitions/types'; import { FUNCTION_DESCRIBE_BLOCK_NAME } from '../src/validation/function_describe_block_name'; import { getMaxMinNumberOfParams } from '../src/validation/helpers'; @@ -1155,7 +1156,7 @@ function generateIncorrectlyTypedParameters( signatures: FunctionDefinition['signatures'], currentParams: FunctionDefinition['signatures'][number]['params'], availableFields: Array<{ name: string; type: SupportedDataType }> -) { +): { wrongFieldMapping: FunctionParameter[]; expectedErrors: string[] } { const literalValues = { string: `"a"`, number: '5', @@ -1260,7 +1261,7 @@ function generateIncorrectlyTypedParameters( }) .filter(nonNullable); - return { wrongFieldMapping, expectedErrors }; + return { wrongFieldMapping: wrongFieldMapping as FunctionParameter[], expectedErrors }; } /** 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 42a6d4c4fd64de..e8b1c42db2aedb 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -20,7 +20,9 @@ import { camelCase, partition } from 'lodash'; import { getAstAndSyntaxErrors } from '@kbn/esql-ast'; import { FunctionParameter, + FunctionReturnType, isFieldType, + isReturnType, isSupportedDataType, SupportedDataType, } from '../definitions/types'; @@ -753,7 +755,9 @@ describe('autocomplete', () => { ); const getTypesFromParamDefs = (paramDefs: FunctionParameter[]): SupportedDataType[] => - Array.from(new Set(paramDefs.map((p) => p.type))).filter(isSupportedDataType); + Array.from(new Set(paramDefs.map((p) => p.type))).filter( + isSupportedDataType + ) as SupportedDataType[]; const suggestedConstants = param.literalSuggestions || param.literalOptions; @@ -778,7 +782,9 @@ describe('autocomplete', () => { ), ...getFunctionSignaturesByReturnType( 'eval', - getTypesFromParamDefs(acceptsFieldParamDefs), + getTypesFromParamDefs(acceptsFieldParamDefs).filter( + isReturnType + ) as FunctionReturnType[], { scalar: true }, undefined, [fn.name] @@ -802,7 +808,7 @@ describe('autocomplete', () => { ), ...getFunctionSignaturesByReturnType( 'eval', - getTypesFromParamDefs(acceptsFieldParamDefs), + getTypesFromParamDefs(acceptsFieldParamDefs) as FunctionReturnType[], { scalar: true }, undefined, [fn.name] @@ -851,13 +857,7 @@ describe('autocomplete', () => { ], ' ' ); - testSuggestions('from a | eval a = 1 year /', [ - ',', - '| ', - ...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [ - 'time_interval', - ]), - ]); + testSuggestions('from a | eval a = 1 year /', [',', '| ', 'IS NOT NULL', 'IS NULL']); testSuggestions('from a | eval a = 1 day + 2 /', [',', '| ']); testSuggestions( 'from a | eval 1 day + 2 /', @@ -1357,6 +1357,81 @@ describe('autocomplete', () => { ['keyword'] ).map((s) => (s.text.toLowerCase().includes('null') ? s : attachTriggerCommand(s))) ); + describe('field lists', () => { + // KEEP field + testSuggestions('FROM a | KEEP /', getFieldNamesByType('any').map(attachTriggerCommand)); + testSuggestions( + 'FROM a | KEEP d/', + getFieldNamesByType('any') + .map((text) => ({ + text, + rangeToReplace: { start: 15, end: 16 }, + })) + .map(attachTriggerCommand) + ); + testSuggestions( + 'FROM a | KEEP doubleFiel/', + getFieldNamesByType('any').map(attachTriggerCommand) + ); + testSuggestions( + 'FROM a | KEEP doubleField/', + ['doubleField, ', 'doubleField | '] + .map((text) => ({ + text, + filterText: 'doubleField', + rangeToReplace: { start: 15, end: 26 }, + })) + .map(attachTriggerCommand) + ); + testSuggestions('FROM a | KEEP doubleField /', ['| ', ',']); + + // Let's get funky with the field names + testSuggestions( + 'FROM a | KEEP @timestamp/', + ['@timestamp, ', '@timestamp | '] + .map((text) => ({ + text, + filterText: '@timestamp', + rangeToReplace: { start: 15, end: 25 }, + })) + .map(attachTriggerCommand), + undefined, + [[{ name: '@timestamp', type: 'date' }]] + ); + testSuggestions( + 'FROM a | KEEP foo.bar/', + ['foo.bar, ', 'foo.bar | '] + .map((text) => ({ + text, + filterText: 'foo.bar', + rangeToReplace: { start: 15, end: 22 }, + })) + .map(attachTriggerCommand), + undefined, + [[{ name: 'foo.bar', type: 'double' }]] + ); + + // @todo re-enable these tests when we can use AST to support this case + testSuggestions.skip('FROM a | KEEP `foo.bar`/', ['foo.bar, ', 'foo.bar | '], undefined, [ + [{ name: 'foo.bar', type: 'double' }], + ]); + testSuggestions.skip('FROM a | KEEP `foo`.`bar`/', ['foo.bar, ', 'foo.bar | '], undefined, [ + [{ name: 'foo.bar', type: 'double' }], + ]); + testSuggestions.skip('FROM a | KEEP `any#Char$Field`/', [ + '`any#Char$Field`, ', + '`any#Char$Field` | ', + ]); + + // Subsequent fields + testSuggestions( + 'FROM a | KEEP doubleField, dateFiel/', + getFieldNamesByType('any') + .filter((s) => s !== 'doubleField') + .map(attachTriggerCommand) + ); + testSuggestions('FROM a | KEEP doubleField, dateField/', ['dateField, ', 'dateField | ']); + }); }); describe('Replacement ranges are attached when needed', () => { @@ -1417,21 +1492,21 @@ describe('autocomplete', () => { describe('dot-separated field names', () => { testSuggestions( 'FROM a | KEEP field.nam/', - [{ text: 'field.name', rangeToReplace: { start: 15, end: 23 } }], + [{ text: 'field.name', rangeToReplace: { start: 15, end: 24 } }], undefined, [[{ name: 'field.name', type: 'double' }]] ); // multi-line testSuggestions( 'FROM a\n| KEEP field.nam/', - [{ text: 'field.name', rangeToReplace: { start: 15, end: 23 } }], + [{ text: 'field.name', rangeToReplace: { start: 15, end: 24 } }], undefined, [[{ name: 'field.name', type: 'double' }]] ); // triple separator testSuggestions( 'FROM a\n| KEEP field.name.f/', - [{ text: 'field.name.foo', rangeToReplace: { start: 15, end: 26 } }], + [{ text: 'field.name.foo', rangeToReplace: { start: 15, end: 27 } }], undefined, [[{ name: 'field.name.foo', type: 'double' }]] ); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts index 650c2326fe0076..556e4860738d77 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts @@ -20,7 +20,7 @@ import { partition } from 'lodash'; import { ESQL_NUMBER_TYPES, compareTypesWithLiterals, isNumericType } from '../shared/esql_types'; import type { EditorContext, SuggestionRawDefinition } from './types'; import { - lookupColumn, + getColumnForASTNode, getCommandDefinition, getCommandOption, getFunctionDefinition, @@ -46,6 +46,7 @@ import { getColumnExists, findPreviousWord, noCaseCompare, + getColumnByName, } from '../shared/helpers'; import { collectVariables, excludeVariablesFromCurrentCommand } from '../shared/variables'; import type { ESQLPolicy, ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types'; @@ -76,6 +77,7 @@ import { buildValueDefinitions, getDateLiterals, buildFieldsDefinitionsWithMetadata, + TRIGGER_SUGGESTION_COMMAND, } from './factories'; import { EDITOR_MARKER, SINGLE_BACKTICK, METADATA_FIELDS } from '../shared/constants'; import { getAstContext, removeMarkerArgFromArgsList } from '../shared/context'; @@ -96,7 +98,13 @@ import { isAggFunctionUsedAlready, removeQuoteForSuggestedSources, } from './helper'; -import { FunctionParameter, FunctionReturnType, SupportedDataType } from '../definitions/types'; +import { + FunctionParameter, + FunctionReturnType, + SupportedDataType, + isParameterType, + isReturnType, +} from '../definitions/types'; type GetSourceFn = () => Promise; type GetDataStreamsForIntegrationFn = ( @@ -105,7 +113,7 @@ type GetDataStreamsForIntegrationFn = ( type GetFieldsByTypeFn = ( type: string | string[], ignored?: string[], - options?: { advanceCursorAndOpenSuggestions?: boolean; addComma?: boolean } + options?: { advanceCursor?: boolean; openSuggestions?: boolean; addComma?: boolean } ) => Promise; type GetFieldsMapFn = () => Promise>; type GetPoliciesFn = () => Promise; @@ -382,7 +390,7 @@ function workoutBuiltinOptions( ): { skipAssign: boolean } { // skip assign operator if it's a function or an existing field to avoid promoting shadowing return { - skipAssign: Boolean(!isColumnItem(nodeArg) || lookupColumn(nodeArg, references)), + skipAssign: Boolean(!isColumnItem(nodeArg) || getColumnForASTNode(nodeArg, references)), }; } @@ -456,7 +464,7 @@ function extractFinalTypeFromArg( return arg.literalType; } if (isColumnItem(arg)) { - const hit = lookupColumn(arg, references); + const hit = getColumnForASTNode(arg, references); if (hit) { return hit.type; } @@ -690,18 +698,47 @@ async function getExpressionSuggestionsByType( const lastWord = words[words.length - 1]; if (lastWord !== '') { // ... | + + const rangeToReplace = { + start: innerText.length - lastWord.length + 1, + end: innerText.length + 1, + }; + + // check if lastWord is an existing field + const column = getColumnByName(lastWord, references); + if (column) { + // now we know that the user has already entered a column, + // so suggest comma and pipe + // const NON_ALPHANUMERIC_REGEXP = /[^a-zA-Z\d]/g; + // const textToUse = lastWord.replace(NON_ALPHANUMERIC_REGEXP, ''); + const textToUse = lastWord; + return [ + { ...pipeCompleteItem, text: ' | ' }, + { ...commaCompleteItem, text: ', ' }, + ].map((s) => ({ + ...s, + filterText: textToUse, + text: textToUse + s.text, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })); + } else { + suggestions.push( + ...fieldSuggestions.map((suggestion) => ({ + ...suggestion, + command: TRIGGER_SUGGESTION_COMMAND, + rangeToReplace, + })) + ); + } + } else { + // ... | suggestions.push( ...fieldSuggestions.map((suggestion) => ({ ...suggestion, - rangeToReplace: { - start: innerText.length - lastWord.length + 1, - end: innerText.length, - }, + command: TRIGGER_SUGGESTION_COMMAND, })) ); - } else { - // ... | - suggestions.push(...fieldSuggestions); } } } @@ -710,7 +747,7 @@ async function getExpressionSuggestionsByType( // ... | STATS a // ... | EVAL a const nodeArgType = extractFinalTypeFromArg(nodeArg, references); - if (nodeArgType) { + if (isParameterType(nodeArgType)) { suggestions.push( ...getBuiltinCompatibleFunctionDefinition( command.name, @@ -768,7 +805,7 @@ async function getExpressionSuggestionsByType( ...getBuiltinCompatibleFunctionDefinition( command.name, undefined, - nodeArgType || 'any', + isParameterType(nodeArgType) ? nodeArgType : 'any', undefined, workoutBuiltinOptions(rightArg, references) ) @@ -859,7 +896,7 @@ async function getExpressionSuggestionsByType( // ... | // In this case start suggesting something not strictly based on type suggestions.push( - ...(await getFieldsByType('any', [], { advanceCursorAndOpenSuggestions: true })), + ...(await getFieldsByType('any', [], { advanceCursor: true, openSuggestions: true })), ...(await getFieldsOrFunctionsSuggestions( ['any'], command.name, @@ -913,7 +950,7 @@ async function getExpressionSuggestionsByType( )) ); } - } else { + } else if (isParameterType(nodeArgType)) { // i.e. ... | field suggestions.push( ...getBuiltinCompatibleFunctionDefinition( @@ -1031,7 +1068,7 @@ async function getBuiltinFunctionNextArgument( ...getBuiltinCompatibleFunctionDefinition( command.name, option?.name, - nodeArgType || 'any', + isParameterType(nodeArgType) ? nodeArgType : 'any', undefined, workoutBuiltinOptions(nodeArg, references) ) @@ -1083,7 +1120,11 @@ async function getBuiltinFunctionNextArgument( if (isFnComplete.reason === 'wrongTypes') { if (nestedType) { // suggest something to complete the builtin function - if (nestedType !== argDef.type) { + if ( + nestedType !== argDef.type && + isParameterType(nestedType) && + isReturnType(argDef.type) + ) { suggestions.push( ...getBuiltinCompatibleFunctionDefinition( command.name, @@ -1149,7 +1190,8 @@ async function getFieldsOrFunctionsSuggestions( const filteredFieldsByType = pushItUpInTheList( (await (fields ? getFieldsByType(types, ignoreFields, { - advanceCursorAndOpenSuggestions: commandName === 'sort', + advanceCursor: commandName === 'sort', + openSuggestions: commandName === 'sort', }) : [])) as SuggestionRawDefinition[], functions @@ -1378,7 +1420,8 @@ async function getFunctionArgsSuggestions( ...pushItUpInTheList( await getFieldsByType(getTypesFromParamDefs(paramDefsWhichSupportFields) as string[], [], { addComma: shouldAddComma, - advanceCursorAndOpenSuggestions: hasMoreMandatoryArgs, + advanceCursor: hasMoreMandatoryArgs, + openSuggestions: hasMoreMandatoryArgs, }), true ) @@ -1721,7 +1764,8 @@ async function getOptionArgsSuggestions( } else if (isNewExpression || (isAssignment(nodeArg) && !isAssignmentComplete(nodeArg))) { suggestions.push( ...(await getFieldsByType(types[0] === 'column' ? ['any'] : types, [], { - advanceCursorAndOpenSuggestions: true, + advanceCursor: true, + openSuggestions: true, })) ); diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts index 30772d6e070fce..4ac3b3b24d1f68 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts @@ -132,7 +132,7 @@ export function getSuggestionCommandDefinition( export const buildFieldsDefinitionsWithMetadata = ( fields: ESQLRealField[], - options?: { advanceCursorAndOpenSuggestions?: boolean; addComma?: boolean } + options?: { advanceCursor?: boolean; openSuggestions?: boolean; addComma?: boolean } ): SuggestionRawDefinition[] => { return fields.map((field) => { const description = field.metadata?.description; @@ -143,7 +143,7 @@ export const buildFieldsDefinitionsWithMetadata = ( text: getSafeInsertText(field.name) + (options?.addComma ? ',' : '') + - (options?.advanceCursorAndOpenSuggestions ? ' ' : ''), + (options?.advanceCursor ? ' ' : ''), kind: 'Variable', detail: titleCaseType, documentation: description @@ -156,7 +156,7 @@ ${description}`, : undefined, // If there is a description, it is a field from ECS, so it should be sorted to the top sortText: description ? '1D' : 'D', - command: options?.advanceCursorAndOpenSuggestions ? TRIGGER_SUGGESTION_COMMAND : undefined, + command: options?.openSuggestions ? TRIGGER_SUGGESTION_COMMAND : undefined, }; }); }; diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts index e132d5edb6b8f7..dc2624add82739 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/types.ts @@ -27,6 +27,8 @@ export interface SuggestionRawDefinition { label: string; /* The actual text to insert into the editor */ text: string; + /* Text to use for filtering the suggestions */ + filterText?: string; /** * Should the text be inserted as a snippet? * That is usually used for special behaviour like moving the cursor in a specific position diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts index 9b673cc2d4e6e5..a50780ac8ea956 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/helpers.ts @@ -41,7 +41,7 @@ function handleAdditionalArgs( criteria: boolean, additionalArgs: Array<{ name: string; - type: string | string[]; + type: FunctionParameterType | FunctionParameterType[]; optional?: boolean; reference?: string; }>, diff --git a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts index 043fb77eb262bb..5880b62c77918f 100644 --- a/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts +++ b/packages/kbn-esql-validation-autocomplete/src/definitions/types.ts @@ -69,30 +69,42 @@ export const isSupportedDataType = ( * * The fate of these is uncertain. They may be removed in the future. */ -type ArrayType = - | 'double[]' - | 'unsigned_long[]' - | 'long[]' - | 'integer[]' - | 'counter_integer[]' - | 'counter_long[]' - | 'counter_double[]' - | 'keyword[]' - | 'text[]' - | 'boolean[]' - | 'any[]' - | 'date[]' - | 'date_period[]'; +const arrayTypes = [ + 'double[]', + 'unsigned_long[]', + 'long[]', + 'integer[]', + 'counter_integer[]', + 'counter_long[]', + 'counter_double[]', + 'keyword[]', + 'text[]', + 'boolean[]', + 'any[]', + 'date[]', + 'date_period[]', +] as const; + +export type ArrayType = (typeof arrayTypes)[number]; /** * This is the type of a parameter in a function definition. */ -export type FunctionParameterType = Omit | ArrayType | 'any'; +export type FunctionParameterType = Exclude | ArrayType | 'any'; + +export const isParameterType = (str: string | undefined): str is FunctionParameterType => + typeof str !== undefined && + str !== 'unsupported' && + ([...dataTypes, ...arrayTypes, 'any'] as string[]).includes(str as string); /** * This is the return type of a function definition. */ -export type FunctionReturnType = Omit | 'any' | 'void'; +export type FunctionReturnType = Exclude | 'any' | 'void'; + +export const isReturnType = (str: string | FunctionParameterType): str is FunctionReturnType => + str !== 'unsupported' && + (dataTypes.includes(str as SupportedDataType) || str === 'any' || str === 'void'); export interface FunctionDefinition { type: 'builtin' | 'agg' | 'eval'; diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts index 0b9f704a43bccd..eb058558638b09 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/helpers.ts @@ -40,6 +40,7 @@ import { FunctionDefinition, FunctionParameterType, FunctionReturnType, + ArrayType, } from '../definitions/types'; import type { ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types'; import { removeMarkerArgFromArgsList } from './context'; @@ -248,28 +249,36 @@ function compareLiteralType(argType: string, item: ESQLLiteral) { /** * This function returns the variable or field matching a column */ -export function lookupColumn( +export function getColumnForASTNode( column: ESQLColumn, { fields, variables }: Pick ): ESQLRealField | ESQLVariable | undefined { const columnName = getQuotedColumnName(column); return ( - fields.get(columnName) || - variables.get(columnName)?.[0] || + getColumnByName(columnName, { fields, variables }) || // It's possible columnName has backticks "`fieldName`" // so we need to access the original name as well - fields.get(column.name) || - variables.get(column.name)?.[0] + getColumnByName(column.name, { fields, variables }) ); } +/** + * This function returns the variable or field matching a column + */ +export function getColumnByName( + columnName: string, + { fields, variables }: Pick +): ESQLRealField | ESQLVariable | undefined { + return fields.get(columnName) || variables.get(columnName)?.[0]; +} + const ARRAY_REGEXP = /\[\]$/; -export function isArrayType(type: string) { +export function isArrayType(type: string): type is ArrayType { return ARRAY_REGEXP.test(type); } -const arrayToSingularMap: Map = new Map([ +const arrayToSingularMap: Map = new Map([ ['double[]', 'double'], ['unsigned_long[]', 'unsigned_long'], ['long[]', 'long'], @@ -279,7 +288,7 @@ const arrayToSingularMap: Map = ne ['counter_double[]', 'counter_double'], ['keyword[]', 'keyword'], ['text[]', 'text'], - ['datetime[]', 'date'], + ['date[]', 'date'], ['date_period[]', 'date_period'], ['boolean[]', 'boolean'], ['any[]', 'any'], @@ -289,7 +298,7 @@ const arrayToSingularMap: Map = ne * Given an array type for example `string[]` it will return `string` */ export function extractSingularType(type: FunctionParameterType): FunctionParameterType { - return arrayToSingularMap.get(type) ?? type; + return isArrayType(type) ? arrayToSingularMap.get(type)! : type; } export function createMapFromList(arr: T[]): Map { @@ -378,7 +387,7 @@ export function getAllArrayTypes( types.push(subArg.literalType); } if (subArg.type === 'column') { - const hit = lookupColumn(subArg, references); + const hit = getColumnForASTNode(subArg, references); types.push(hit?.type || 'unsupported'); } if (subArg.type === 'timeInterval') { @@ -445,7 +454,7 @@ export function checkFunctionArgMatchesDefinition( return argType === 'time_literal' && inKnownTimeInterval(arg); } if (arg.type === 'column') { - const hit = lookupColumn(arg, references); + const hit = getColumnForASTNode(arg, references); const validHit = hit; if (!validHit) { return false; diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts index aedefb08e73877..5a06cf3f6a2460 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts @@ -30,7 +30,7 @@ import { import { areFieldAndVariableTypesCompatible, extractSingularType, - lookupColumn, + getColumnForASTNode, getCommandDefinition, getFunctionDefinition, isArrayType, @@ -295,7 +295,7 @@ function validateFunctionColumnArg( if ( !checkFunctionArgMatchesDefinition(actualArg, parameterDefinition, references, parentCommand) ) { - const columnHit = lookupColumn(actualArg, references); + const columnHit = getColumnForASTNode(actualArg, references); messages.push( getMessageFromId({ messageId: 'wrongArgumentType', @@ -876,7 +876,7 @@ function validateColumnForCommand( ({ type, innerTypes }) => type === 'column' && innerTypes ); // this should be guaranteed by the columnCheck above - const columnRef = lookupColumn(column, references)!; + const columnRef = getColumnForASTNode(column, references)!; if (columnParamsWithInnerTypes.length) { const hasSomeWrongInnerTypes = columnParamsWithInnerTypes.every(({ innerTypes }) => { diff --git a/packages/kbn-monaco/src/esql/language.ts b/packages/kbn-monaco/src/esql/language.ts index 4c629f6b060bb0..958af9e80c1f62 100644 --- a/packages/kbn-monaco/src/esql/language.ts +++ b/packages/kbn-monaco/src/esql/language.ts @@ -100,10 +100,10 @@ export const ESQLLang: CustomLangModuleType = { (...uris) => workerProxyService.getWorker(uris), callbacks ); - const suggestionEntries = await astAdapter.autocomplete(model, position, context); + const suggestions = await astAdapter.autocomplete(model, position, context); return { // @ts-expect-error because of range typing: https://github.com/microsoft/monaco-editor/issues/4638 - suggestions: wrapAsMonacoSuggestions(suggestionEntries), + suggestions: wrapAsMonacoSuggestions(suggestions), }; }, }; diff --git a/packages/kbn-monaco/src/esql/lib/converters/suggestions.ts b/packages/kbn-monaco/src/esql/lib/converters/suggestions.ts index d26169d59647b0..2eaf598a61974b 100644 --- a/packages/kbn-monaco/src/esql/lib/converters/suggestions.ts +++ b/packages/kbn-monaco/src/esql/lib/converters/suggestions.ts @@ -16,10 +16,22 @@ export function wrapAsMonacoSuggestions( suggestions: SuggestionRawDefinitionWithMonacoRange[] ): MonacoAutocompleteCommandDefinition[] { return suggestions.map( - ({ label, text, asSnippet, kind, detail, documentation, sortText, command, range }) => { + ({ + label, + text, + asSnippet, + kind, + detail, + documentation, + sortText, + filterText, + command, + range, + }) => { const monacoSuggestion: MonacoAutocompleteCommandDefinition = { label, insertText: text, + filterText, kind: kind in monaco.languages.CompletionItemKind ? monaco.languages.CompletionItemKind[kind] diff --git a/packages/kbn-monaco/src/esql/lib/shared/utils.ts b/packages/kbn-monaco/src/esql/lib/shared/utils.ts index e09362cb5c83fc..8b6762fafccf24 100644 --- a/packages/kbn-monaco/src/esql/lib/shared/utils.ts +++ b/packages/kbn-monaco/src/esql/lib/shared/utils.ts @@ -39,6 +39,7 @@ export const offsetRangeToMonacoRange = ( } => { let startColumn = 0; let endColumn = 0; + // How far we are past the last newline character let currentOffset = 0; let startLineNumber = 1; @@ -63,10 +64,16 @@ export const offsetRangeToMonacoRange = ( } } - // Handle the case where the end offset is at the end of the string - if (range.end === expression.length) { + // Handle the case where the start offset is past the end of the string + if (range.start >= expression.length) { + startLineNumber = currentLine; + startColumn = range.start - currentOffset; + } + + // Handle the case where the end offset is at the end or past the end of the string + if (range.end >= expression.length) { endLineNumber = currentLine; - endColumn = expression.length - currentOffset; + endColumn = range.end - currentOffset; } return { diff --git a/packages/kbn-monaco/src/esql/lib/types.ts b/packages/kbn-monaco/src/esql/lib/types.ts index ebe7459fef9834..d16c40dcead1ed 100644 --- a/packages/kbn-monaco/src/esql/lib/types.ts +++ b/packages/kbn-monaco/src/esql/lib/types.ts @@ -13,6 +13,7 @@ export type MonacoAutocompleteCommandDefinition = Pick< monaco.languages.CompletionItem, | 'label' | 'insertText' + | 'filterText' | 'kind' | 'detail' | 'documentation'