mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[ES|QL] Auto open suggestions for field lists (#190466)
## Summary Part of https://github.com/elastic/kibana/issues/189662 ## Suggests comma and pipe https://github.com/user-attachments/assets/c09bc6fd-20a6-4f42-a871-c70e68e6d81a ## Doesn't suggest comma when there are no more fields https://github.com/user-attachments/assets/29fce13e-e58b-4d93-bce5-6b1f913b4d92 ## Doesn't work for escaped columns :( https://github.com/user-attachments/assets/3d65f3b9-923d-4c0e-9c50-51dd83115c8b As part of this effort I discovered https://github.com/elastic/kibana/issues/191100 and https://github.com/elastic/kibana/issues/191105, as well as a problem with column name validation (see https://github.com/elastic/kibana/issues/177699) I think we can revisit column escaping and probably resolve all of these issues (issue [here](https://github.com/elastic/kibana/issues/191111)). ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
parent
bb7466e443
commit
15ef37f2fd
14 changed files with 240 additions and 77 deletions
|
@ -49,7 +49,7 @@ export {
|
|||
getCommandDefinition,
|
||||
getAllCommands,
|
||||
getCommandOption,
|
||||
lookupColumn,
|
||||
getColumnForASTNode as lookupColumn,
|
||||
shouldBeQuotedText,
|
||||
printFunctionSignature,
|
||||
checkFunctionArgMatchesDefinition as isEqualType,
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<PartialSuggestionWithText>((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' }]]
|
||||
);
|
||||
|
|
|
@ -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<SuggestionRawDefinition[]>;
|
||||
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<SuggestionRawDefinition[]>;
|
||||
type GetFieldsMapFn = () => Promise<Map<string, ESQLRealField>>;
|
||||
type GetPoliciesFn = () => Promise<SuggestionRawDefinition[]>;
|
||||
|
@ -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 !== '') {
|
||||
// ... | <COMMAND> <word><suggest>
|
||||
|
||||
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<SuggestionRawDefinition>((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 {
|
||||
// ... | <COMMAND> <suggest>
|
||||
suggestions.push(
|
||||
...fieldSuggestions.map((suggestion) => ({
|
||||
...suggestion,
|
||||
rangeToReplace: {
|
||||
start: innerText.length - lastWord.length + 1,
|
||||
end: innerText.length,
|
||||
},
|
||||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
// ... | <COMMAND> <suggest>
|
||||
suggestions.push(...fieldSuggestions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -710,7 +747,7 @@ async function getExpressionSuggestionsByType(
|
|||
// ... | STATS a <suggest>
|
||||
// ... | EVAL a <suggest>
|
||||
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(
|
|||
// ... | <COMMAND> <suggest>
|
||||
// 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. ... | <COMMAND> field <suggest>
|
||||
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,
|
||||
}))
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -41,7 +41,7 @@ function handleAdditionalArgs(
|
|||
criteria: boolean,
|
||||
additionalArgs: Array<{
|
||||
name: string;
|
||||
type: string | string[];
|
||||
type: FunctionParameterType | FunctionParameterType[];
|
||||
optional?: boolean;
|
||||
reference?: string;
|
||||
}>,
|
||||
|
|
|
@ -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<SupportedDataType, 'unsupported'> | ArrayType | 'any';
|
||||
export type FunctionParameterType = Exclude<SupportedDataType, 'unsupported'> | 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<SupportedDataType, 'unsupported'> | 'any' | 'void';
|
||||
export type FunctionReturnType = Exclude<SupportedDataType, 'unsupported'> | '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';
|
||||
|
|
|
@ -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<ReferenceMaps, 'fields' | 'variables'>
|
||||
): 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<ReferenceMaps, 'fields' | 'variables'>
|
||||
): 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<FunctionParameterType, FunctionParameterType> = new Map([
|
||||
const arrayToSingularMap: Map<ArrayType, FunctionParameterType> = new Map([
|
||||
['double[]', 'double'],
|
||||
['unsigned_long[]', 'unsigned_long'],
|
||||
['long[]', 'long'],
|
||||
|
@ -279,7 +288,7 @@ const arrayToSingularMap: Map<FunctionParameterType, FunctionParameterType> = 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<FunctionParameterType, FunctionParameterType> = 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<T extends { name: string }>(arr: T[]): Map<string, T> {
|
||||
|
@ -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;
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -100,10 +100,10 @@ export const ESQLLang: CustomLangModuleType<ESQLCallbacks> = {
|
|||
(...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),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -16,10 +16,22 @@ export function wrapAsMonacoSuggestions(
|
|||
suggestions: SuggestionRawDefinitionWithMonacoRange[]
|
||||
): MonacoAutocompleteCommandDefinition[] {
|
||||
return suggestions.map<MonacoAutocompleteCommandDefinition>(
|
||||
({ 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]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -13,6 +13,7 @@ export type MonacoAutocompleteCommandDefinition = Pick<
|
|||
monaco.languages.CompletionItem,
|
||||
| 'label'
|
||||
| 'insertText'
|
||||
| 'filterText'
|
||||
| 'kind'
|
||||
| 'detail'
|
||||
| 'documentation'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue