diff --git a/packages/kbn-monaco/src/esql/language.ts b/packages/kbn-monaco/src/esql/language.ts index 4d3d2713f327d..305de5777fe12 100644 --- a/packages/kbn-monaco/src/esql/language.ts +++ b/packages/kbn-monaco/src/esql/language.ts @@ -76,6 +76,7 @@ export const ESQLLang: CustomLangModuleType = { ], autoClosingPairs: [ { open: '(', close: ')' }, + { open: '[', close: ']' }, { open: `'`, close: `'` }, { open: '"', close: '"' }, ], diff --git a/packages/kbn-monaco/src/esql/lib/ast/ast_helpers.ts b/packages/kbn-monaco/src/esql/lib/ast/ast_helpers.ts index 123ef1ee8921a..e72c5892c2116 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/ast_helpers.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/ast_helpers.ts @@ -9,9 +9,10 @@ import type { ParserRuleContext } from 'antlr4ts/ParserRuleContext'; import { ErrorNode } from 'antlr4ts/tree/ErrorNode'; import type { TerminalNode } from 'antlr4ts/tree/TerminalNode'; -import type { +import { ArithmeticUnaryContext, DecimalValueContext, + esql_parser, IntegerValueContext, QualifiedIntegerLiteralContext, } from '../../antlr/esql_parser'; @@ -26,6 +27,7 @@ import type { ESQLSource, ESQLColumn, ESQLCommandOption, + ESQLAstItem, } from './types'; export function nonNullable(v: T): v is NonNullable { @@ -125,22 +127,60 @@ export function createFunction( }; } +function walkFunctionStructure( + args: ESQLAstItem[], + initialLocation: ESQLLocation, + prop: 'min' | 'max', + getNextItemIndex: (arg: ESQLAstItem[]) => number +) { + let nextArg: ESQLAstItem | undefined = args[getNextItemIndex(args)]; + const location = { ...initialLocation }; + while (Array.isArray(nextArg) || nextArg) { + if (Array.isArray(nextArg)) { + nextArg = nextArg[getNextItemIndex(nextArg)]; + } else { + location[prop] = Math[prop](location[prop], nextArg.location[prop]); + if (nextArg.type === 'function') { + nextArg = nextArg.args[getNextItemIndex(nextArg.args)]; + } else { + nextArg = undefined; + } + } + } + return location[prop]; +} + +export function computeLocationExtends(fn: ESQLFunction) { + const location = fn.location; + if (fn.args) { + // get min location navigating in depth keeping the left/first arg + location.min = walkFunctionStructure(fn.args, location, 'min', () => 0); + // get max location navigating in depth keeping the right/last arg + location.max = walkFunctionStructure(fn.args, location, 'max', (args) => args.length - 1); + } + return location; +} + function getQuotedText(ctx: ParserRuleContext) { return ( - ctx.tryGetToken(73 /* esql_parser.SRC_QUOTED_IDENTIFIER*/, 0) || - ctx.tryGetToken(64 /* esql_parser.QUOTED_IDENTIFIER */, 0) + ctx.tryGetToken(esql_parser.SRC_QUOTED_IDENTIFIER, 0) || + ctx.tryGetToken(esql_parser.QUOTED_IDENTIFIER, 0) ); } function getUnquotedText(ctx: ParserRuleContext) { return ( - ctx.tryGetToken(72 /* esql_parser.SRC_UNQUOTED_IDENTIFIER */, 0) || - ctx.tryGetToken(63 /* esql_parser.UNQUOTED_IDENTIFIER */, 0) + ctx.tryGetToken(esql_parser.SRC_UNQUOTED_IDENTIFIER, 0) || + ctx.tryGetToken(esql_parser.UNQUOTED_IDENTIFIER, 0) ); } export function sanifyIdentifierString(ctx: ParserRuleContext) { - return getUnquotedText(ctx)?.text || getQuotedText(ctx)?.text.replace(/`/g, '') || ctx.text; + return ( + getUnquotedText(ctx)?.text || + getQuotedText(ctx)?.text.replace(/(`)/g, '') || + ctx.text.replace(/(`)/g, '') // for some reason some quoted text is not detected correctly by the parser + ); } export function createSource( diff --git a/packages/kbn-monaco/src/esql/lib/ast/ast_walker.ts b/packages/kbn-monaco/src/esql/lib/ast/ast_walker.ts index f01b28921d45d..2d060dac10230 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/ast_walker.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/ast_walker.ts @@ -69,7 +69,9 @@ import { createList, createNumericLiteral, sanifyIdentifierString, + computeLocationExtends, } from './ast_helpers'; +import { getPosition } from './ast_position_utils'; import type { ESQLLiteral, ESQLColumn, @@ -110,10 +112,12 @@ export function getMatchField(ctx: EnrichCommandContext) { } const identifier = ctx.sourceIdentifier(1); if (identifier) { - const fn = createOption('on', ctx); + const fn = createOption(ctx.ON()!.text.toLowerCase(), ctx); if (identifier.text) { fn.args.push(createColumn(identifier)); } + // overwrite the location inferring the correct position + fn.location = getPosition(ctx.ON()!.symbol, ctx.WITH()?.symbol); return [fn]; } return []; @@ -122,7 +126,7 @@ export function getMatchField(ctx: EnrichCommandContext) { export function getEnrichClauses(ctx: EnrichCommandContext) { const ast: ESQLCommandOption[] = []; if (ctx.WITH()) { - const option = createOption(ctx.WITH()!.text, ctx); + const option = createOption(ctx.WITH()!.text.toLowerCase(), ctx); ast.push(option); const clauses = ctx.enrichWithClause(); for (const clause of clauses) { @@ -140,6 +144,7 @@ export function getEnrichClauses(ctx: EnrichCommandContext) { } } } + option.location = getPosition(ctx.WITH()?.symbol); } return ast; @@ -148,12 +153,18 @@ export function getEnrichClauses(ctx: EnrichCommandContext) { function visitLogicalNot(ctx: LogicalNotContext) { const fn = createFunction('not', ctx); fn.args.push(...collectBooleanExpression(ctx.booleanExpression())); + // update the location of the assign based on arguments + const argsLocationExtends = computeLocationExtends(fn); + fn.location = argsLocationExtends; return fn; } function visitLogicalAndsOrs(ctx: LogicalBinaryContext) { const fn = createFunction(ctx.AND() ? 'and' : 'or', ctx); fn.args.push(...collectBooleanExpression(ctx._left), ...collectBooleanExpression(ctx._right)); + // update the location of the assign based on arguments + const argsLocationExtends = computeLocationExtends(fn); + fn.location = argsLocationExtends; return fn; } @@ -167,6 +178,9 @@ function visitLogicalIns(ctx: LogicalInContext) { fn.args.push(filteredArgs); } } + // update the location of the assign based on arguments + const argsLocationExtends = computeLocationExtends(fn); + fn.location = argsLocationExtends; return fn; } @@ -204,6 +218,10 @@ function visitValueExpression(ctx: ValueExpressionContext) { visitOperatorExpression(ctx._left)!, visitOperatorExpression(ctx._right)! ); + // update the location of the comparisonFn based on arguments + const argsLocationExtends = computeLocationExtends(comparisonFn); + comparisonFn.location = argsLocationExtends; + return comparisonFn; } } @@ -229,6 +247,9 @@ function visitOperatorExpression( fn.args.push(arg); } } + // update the location of the assign based on arguments + const argsLocationExtends = computeLocationExtends(fn); + fn.location = argsLocationExtends; return fn; } if (ctx instanceof OperatorExpressionDefaultContext) { @@ -292,8 +313,14 @@ export function visitRenameClauses(clausesCtx: RenameClauseContext[]): ESQLAstIt const asToken = clause.tryGetToken(esql_parser.AS, 0); if (asToken) { const fn = createOption(asToken.text.toLowerCase(), clause); - fn.args.push(createColumn(clause._oldName), createColumn(clause._newName)); + for (const arg of [clause._oldName, clause._newName]) { + if (arg?.text) { + fn.args.push(createColumn(arg)); + } + } return fn; + } else if (clause._oldName?.text) { + return createColumn(clause._oldName); } }) .filter(nonNullable); @@ -401,6 +428,9 @@ export function visitField(ctx: FieldContext) { createColumn(ctx.qualifiedName()!), collectBooleanExpression(ctx.booleanExpression()) ); + // update the location of the assign based on arguments + const argsLocationExtends = computeLocationExtends(fn); + fn.location = argsLocationExtends; return [fn]; } return collectBooleanExpression(ctx.booleanExpression()); @@ -425,9 +455,11 @@ export function visitByOption(ctx: StatsCommandContext) { if (!ctx.BY()) { return []; } - const option = createOption(ctx.BY()!.text, ctx); + const option = createOption(ctx.BY()!.text.toLowerCase(), ctx); for (const qnCtx of ctx.grouping()?.qualifiedName() || []) { - option.args.push(createColumn(qnCtx)); + if (qnCtx?.text?.length) { + option.args.push(createColumn(qnCtx)); + } } return [option]; } @@ -485,7 +517,10 @@ function visitDissectOptions(ctx: CommandOptionsContext | undefined) { } const options: ESQLCommandOption[] = []; for (const optionCtx of ctx.commandOption()) { - const option = createOption(sanifyIdentifierString(optionCtx.identifier()), optionCtx); + const option = createOption( + sanifyIdentifierString(optionCtx.identifier()).toLowerCase(), + optionCtx + ); options.push(option); // it can throw while accessing constant for incomplete commands, so try catch it try { diff --git a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.test.ts b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.test.ts index 7916e4877bffc..3349995cbf463 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.test.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.test.ts @@ -12,10 +12,13 @@ import { suggest } from './autocomplete'; import { getParser, ROOT_STATEMENT } from '../../antlr_facade'; import { ESQLErrorListener } from '../../monaco/esql_error_listener'; import { AstListener } from '../ast_factory'; -import { mathCommandDefinition } from './complete_items'; import { evalFunctionsDefinitions } from '../definitions/functions'; -import { getFunctionSignatures } from '../definitions/helpers'; +import { builtinFunctions } from '../definitions/builtin'; import { statsAggregationFunctionDefinitions } from '../definitions/aggs'; +import { chronoLiterals, timeLiterals } from '../definitions/literals'; +import { commandDefinitions } from '../definitions/commands'; + +const triggerCharacters = [',', '(', '=', ' ']; const fields = [ ...['string', 'number', 'date', 'boolean', 'ip'].map((type) => ({ @@ -40,18 +43,111 @@ const policies = [ }, ]; -function getCallbackMocks() { +/** + * Utility to filter down the function list for the given type + * It is mainly driven by the return type, but it can be filtered upon with the last optional argument "paramsTypes" + * jsut make sure to pass the arguments in the right order + * @param command current command context + * @param expectedReturnType the expected type returned by the function + * @param functionCategories + * @param paramsTypes the function argument types (optional) + * @returns + */ +function getFunctionSignaturesByReturnType( + command: string, + expectedReturnType: string, + { agg, evalMath, builtin }: { agg?: boolean; evalMath?: boolean; builtin?: boolean } = {}, + paramsTypes?: string[], + ignored?: string[] +) { + const list = []; + if (agg) { + list.push(...statsAggregationFunctionDefinitions); + } + // eval functions (eval is a special keyword in JS) + if (evalMath) { + list.push(...evalFunctionsDefinitions); + } + if (builtin) { + list.push(...builtinFunctions); + } + return list + .filter(({ signatures, ignoreAsSuggestion, supportedCommands }) => { + if (ignoreAsSuggestion) { + return false; + } + if (!supportedCommands.includes(command)) { + return false; + } + const filteredByReturnType = signatures.some( + ({ returnType }) => expectedReturnType === 'any' || returnType === expectedReturnType + ); + if (!filteredByReturnType) { + return false; + } + if (paramsTypes?.length) { + return signatures.some(({ params }) => + paramsTypes.every( + (expectedType, i) => expectedType === 'any' || expectedType === params[i].type + ) + ); + } + return true; + }) + .filter(({ name }) => { + if (ignored?.length) { + return !ignored?.includes(name); + } + return true; + }) + .map(({ builtin: isBuiltinFn, name, signatures, ...defRest }) => + isBuiltinFn ? `${name} $0` : `${name}($0)` + ); +} + +function getFieldNamesByType(requestedType: string) { + return fields + .filter(({ type }) => requestedType === 'any' || type === requestedType) + .map(({ name }) => name); +} + +function getLiteralsByType(type: string) { + if (type === 'time_literal') { + // return only singular + return timeLiterals.map(({ name }) => `1 ${name}`).filter((s) => !/s$/.test(s)); + } + if (type === 'chrono_literal') { + return chronoLiterals.map(({ name }) => name); + } + return []; +} + +function createCustomCallbackMocks( + customFields: Array<{ name: string; type: string }> | undefined, + customSources: string[] | undefined, + customPolicies: + | Array<{ + name: string; + sourceIndices: string[]; + matchField: string; + enrichFields: string[]; + }> + | undefined +) { + const finalFields = customFields || fields; + const finalSources = customSources || indexes; + const finalPolicies = customPolicies || policies; return { - getFieldsFor: jest.fn(async () => fields), - getSources: jest.fn(async () => indexes), - getPolicies: jest.fn(async () => policies), + getFieldsFor: jest.fn(async () => finalFields), + getSources: jest.fn(async () => finalSources), + getPolicies: jest.fn(async () => finalPolicies), }; } -function createModelAndPosition(text: string) { +function createModelAndPosition(text: string, offset: number) { return { model: { getValue: () => text } as monaco.editor.ITextModel, - position: { lineNumber: 1, column: text.length - 1 } as monaco.Position, + position: { lineNumber: 1, column: offset } as monaco.Position, }; } @@ -59,12 +155,20 @@ function createSuggestContext(text: string, triggerCharacter?: string) { if (triggerCharacter) { return { triggerCharacter, triggerKind: 1 }; // any number is fine here } + const foundTriggerCharIndexes = triggerCharacters.map((char) => text.lastIndexOf(char)); + const maxIndex = Math.max(...foundTriggerCharIndexes); return { - triggerCharacter: text[text.length - 1], + triggerCharacter: text[maxIndex], triggerKind: 1, }; } +function getPolicyFields(policyName: string) { + return policies + .filter(({ name }) => name === policyName) + .flatMap(({ enrichFields }) => enrichFields); +} + describe('autocomplete', () => { const getAstAndErrors = async (text: string) => { const errorListener = new ESQLErrorListener(); @@ -76,121 +180,187 @@ describe('autocomplete', () => { return { ...parseListener.getAst() }; }; - const testSuggestions = (statement: string, expected: string[], triggerCharacter?: string) => { + type TestArgs = [string, string[], string?, Parameters?]; + + const testSuggestionsFn = ( + statement: string, + expected: string[], + triggerCharacter: string = '', + customCallbacksArgs: Parameters = [ + undefined, + undefined, + undefined, + ], + { only, skip }: { only?: boolean; skip?: boolean } = {} + ) => { const context = createSuggestContext(statement, triggerCharacter); - test(`${statement} (triggerChar: "${context.triggerCharacter}")=> [${expected.join( - ',' - )}]`, async () => { - const callbackMocks = getCallbackMocks(); - const { model, position } = createModelAndPosition(statement); - const suggestions = await suggest( - model, - position, - context, - async (text) => (text ? await getAstAndErrors(text) : { ast: [] }), - callbackMocks - ); - expect(suggestions.map((i) => i.label)).toEqual(expected); - }); + const offset = statement.lastIndexOf(context.triggerCharacter) + 2; + const testFn = only ? test.only : skip ? test.skip : test; + + testFn( + `${statement} (triggerChar: "${context.triggerCharacter}")=> ["${expected.join('","')}"]`, + async () => { + const callbackMocks = createCustomCallbackMocks(...customCallbacksArgs); + const { model, position } = createModelAndPosition(statement, offset); + const suggestions = await suggest( + model, + position, + context, + async (text) => (text ? await getAstAndErrors(text) : { ast: [] }), + callbackMocks + ); + expect(suggestions.map((i) => i.insertText)).toEqual(expected); + } + ); }; + // Enrich the function to work with .only and .skip as regular test function + const testSuggestions = Object.assign(testSuggestionsFn, { + skip: (...args: TestArgs) => { + const paddingArgs = ['', [undefined, undefined, undefined]].slice(args.length - 2); + return testSuggestionsFn( + ...((args.length > 1 ? [...args, ...paddingArgs] : args) as TestArgs), + { + skip: true, + } + ); + }, + only: (...args: TestArgs) => { + const paddingArgs = ['', [undefined, undefined, undefined]].slice(args.length - 2); + return testSuggestionsFn( + ...((args.length > 1 ? [...args, ...paddingArgs] : args) as TestArgs), + { + only: true, + } + ); + }, + }); + + const sourceCommands = ['row', 'from', 'show']; + + describe('New command', () => { + testSuggestions(' ', sourceCommands); + testSuggestions( + 'from a | ', + commandDefinitions + .filter(({ name }) => !sourceCommands.includes(name)) + .map(({ name }) => name) + ); + testSuggestions( + 'from a [metadata _id] | ', + commandDefinitions + .filter(({ name }) => !sourceCommands.includes(name)) + .map(({ name }) => name) + ); + testSuggestions( + 'from a | eval var0 = a | ', + commandDefinitions + .filter(({ name }) => !sourceCommands.includes(name)) + .map(({ name }) => name) + ); + testSuggestions( + 'from a [metadata _id] | eval var0 = a | ', + commandDefinitions + .filter(({ name }) => !sourceCommands.includes(name)) + .map(({ name }) => name) + ); + }); + describe('from', () => { // Monaco will filter further down here - testSuggestions('f', ['row', 'from', 'show']); + testSuggestions('f', sourceCommands); testSuggestions('from ', indexes); testSuggestions('from a,', indexes); - testSuggestions('from a, b ', ['metadata', '|', ',']); + testSuggestions('from a, b ', ['[metadata $0 ]', '|', ',']); + testSuggestions('from *,', indexes); }); describe('where', () => { - testSuggestions('from a | where ', [ + const allEvalFns = getFunctionSignaturesByReturnType('where', 'any', { + evalMath: true, + }); + testSuggestions('from a | where ', [...getFieldNamesByType('any'), ...allEvalFns]); + testSuggestions('from a | eval var0 = 1 | where ', [ + ...getFieldNamesByType('any'), + ...allEvalFns, 'var0', - ...fields.map(({ name }) => name), - ...evalFunctionsDefinitions.map( - ({ name, signatures, ...defRest }) => - getFunctionSignatures({ name, ...defRest, signatures })[0].declaration - ), ]); testSuggestions('from a | where stringField ', [ - '+', - '-', - '*', - '/', - '%', - '==', - '!=', - '<', - '>', - '<=', - '>=', - 'in', - '|', - ',', + // all functions compatible with a stringField type + ...getFunctionSignaturesByReturnType( + 'where', + 'boolean', + { + builtin: true, + }, + ['string'] + ), ]); testSuggestions('from a | where stringField >= ', [ - 'var0', - ...fields.map(({ name }) => name), - ...evalFunctionsDefinitions.map( - ({ name, signatures, ...defRest }) => - getFunctionSignatures({ name, ...defRest, signatures })[0].declaration + ...getFieldNamesByType('string'), + ...getFunctionSignaturesByReturnType('where', 'string', { evalMath: true }), + ]); + testSuggestions('from a | where stringField >= stringField ', [ + ...getFunctionSignaturesByReturnType( + 'where', + 'boolean', + { + builtin: true, + }, + ['boolean'] ), + '|', + ',', ]); - // // @TODO: improve here: suggest also AND, OR - testSuggestions('from a | where stringField >= stringField ', ['|', ',']); - // // @TODO: improve here: suggest here any type, not just boolean testSuggestions('from a | where stringField >= stringField and ', [ - ...fields.filter(({ type }) => type === 'boolean').map(({ name }) => name), - ...evalFunctionsDefinitions - .filter(({ signatures }) => signatures.some(({ returnType }) => returnType === 'boolean')) - .map( - ({ name, signatures, ...defRest }) => - getFunctionSignatures({ name, ...defRest, signatures })[0].declaration - ), + ...getFieldNamesByType('boolean'), + ...getFunctionSignaturesByReturnType('where', 'boolean', { evalMath: true }), + ]); + testSuggestions('from a | where stringField >= stringField and numberField ', [ + ...getFunctionSignaturesByReturnType('where', 'boolean', { builtin: true }, ['number']), ]); - // // @TODO: improve here: suggest comparison functions - testSuggestions('from a | where stringField >= stringField and numberField ', ['|', ',']); testSuggestions('from a | stats a=avg(numberField) | where a ', [ - '+', - '-', - '*', - '/', - '%', - '==', - '!=', - '<', - '>', - '<=', - '>=', - 'in', - '|', - ',', + ...getFunctionSignaturesByReturnType('where', 'any', { builtin: true }, ['number']), ]); - testSuggestions('from a | stats a=avg(numberField) | where numberField ', [ - '+', - '-', - '*', - '/', - '%', - '==', - '!=', - '<', - '>', - '<=', - '>=', - 'in', - '|', - ',', + // Mind this test: suggestion is aware of previous commands when checking for fields + // in this case the numberField has been wiped by the STATS command and suggest cannot find it's type + // @TODO: verify this is the correct behaviour in this case or if we want a "generic" suggestion anyway + testSuggestions( + 'from a | stats a=avg(numberField) | where numberField ', + [], + '', + // make the fields suggest aware of the previous STATS, leave the other callbacks untouched + [[{ name: 'a', type: 'number' }], undefined, undefined] + ); + testSuggestions('from a | where stringField >= stringField and numberField == ', [ + ...getFieldNamesByType('number'), + ...getFunctionSignaturesByReturnType('where', 'number', { evalMath: true }), ]); - // @TODO improve here: suggest here also non-boolean functions - testSuggestions('from a | where stringField >= stringField and numberField == ', [ - ...fields.filter(({ type }) => type === 'boolean').map(({ name }) => name), - ...evalFunctionsDefinitions - .filter(({ signatures }) => signatures.some(({ returnType }) => returnType === 'boolean')) - .map( - ({ name, signatures, ...defRest }) => - getFunctionSignatures({ name, ...defRest, signatures })[0].declaration - ), + // The editor automatically inject the final bracket, so it is not useful to test with just open bracket + testSuggestions( + 'from a | where log10()', + [ + ...getFieldNamesByType('number'), + ...getFunctionSignaturesByReturnType('where', 'number', { evalMath: true }, undefined, [ + 'log10', + ]), + ], + '(' + ); + testSuggestions('from a | where log10(numberField) ', [ + ...getFunctionSignaturesByReturnType('where', 'number', { builtin: true }, ['number']), + ...getFunctionSignaturesByReturnType('where', 'boolean', { builtin: true }, ['number']), ]); + testSuggestions( + 'from a | WHERE pow(numberField, )', + [ + ...getFieldNamesByType('number'), + ...getFunctionSignaturesByReturnType('where', 'number', { evalMath: true }, undefined, [ + 'pow', + ]), + ], + ',' + ); }); describe('sort', () => { @@ -211,54 +381,46 @@ describe('autocomplete', () => { testSuggestions('from a | mv_expand a ', ['|']); }); - describe.skip('stats', () => { - testSuggestions('from a | stats ', ['var0']); - testSuggestions('from a | stats a ', ['=']); - testSuggestions('from a | stats a=', [ - 'avg', - 'max', - 'min', - 'sum', - 'count', - 'count_distinct', - 'median', - 'median_absolute_deviation', - 'percentile', + describe('rename', () => { + testSuggestions('from a | rename ', [...getFieldNamesByType('any')]); + testSuggestions('from a | rename stringField ', ['as']); + testSuggestions('from a | rename stringField as ', ['var0']); + }); + + describe('stats', () => { + const allAggFunctions = getFunctionSignaturesByReturnType('stats', 'any', { + agg: true, + }); + testSuggestions('from a | stats ', ['var0 =', ...allAggFunctions]); + testSuggestions('from a | stats a ', ['= $0']); + testSuggestions('from a | stats a=', [...allAggFunctions]); + testSuggestions('from a | stats a=max(b) by ', [...fields.map(({ name }) => name)]); + testSuggestions('from a | stats a=max(b) BY ', [...fields.map(({ name }) => name)]); + testSuggestions('from a | stats a=c by d', ['|', ',']); + testSuggestions('from a | stats a=c by d, ', [...fields.map(({ name }) => name)]); + testSuggestions('from a | stats a=max(b), ', ['var0 =', ...allAggFunctions]); + testSuggestions( + 'from a | stats a=min()', + [...fields.filter(({ type }) => type === 'number').map(({ name }) => name)], + '(' + ); + testSuggestions('from a | stats a=min(b) ', ['by', '|', ',']); + testSuggestions('from a | stats a=min(b) by ', [...fields.map(({ name }) => name)]); + testSuggestions('from a | stats a=min(b),', ['var0 =', ...allAggFunctions]); + testSuggestions('from a | stats var0=min(b),var1=c,', ['var2 =', ...allAggFunctions]); + testSuggestions('from a | stats a=min(b), b=max()', [ + ...fields.filter(({ type }) => type === 'number').map(({ name }) => name), ]); - testSuggestions('from a | stats a=b by ', ['FieldIdentifier']); - testSuggestions('from a | stats a=c by d', ['|']); - testSuggestions('from a | stats a=b, ', ['var0']); - testSuggestions('from a | stats a=max', ['(']); - testSuggestions('from a | stats a=min(', ['FieldIdentifier']); - testSuggestions('from a | stats a=min(b', [')', 'FieldIdentifier']); - testSuggestions('from a | stats a=min(b) ', ['|', 'by']); - testSuggestions('from a | stats a=min(b) by ', ['FieldIdentifier']); - testSuggestions('from a | stats a=min(b),', [ + // @TODO: remove last 2 suggestions if possible + testSuggestions('from a | eval var0=round(b), var1=round(c) | stats ', [ + 'var2 =', + ...allAggFunctions, 'var0', - ...fields.map(({ name }) => name), - ...statsAggregationFunctionDefinitions.map( - ({ name, signatures, ...defRest }) => - getFunctionSignatures({ name, ...defRest, signatures })[0].declaration - ), - ]); - testSuggestions('from a | stats var0=min(b),var1=c,', [ - 'var2', - ...fields.map(({ name }) => name), - ...statsAggregationFunctionDefinitions.map( - ({ name, signatures, ...defRest }) => - getFunctionSignatures({ name, ...defRest, signatures })[0].declaration - ), - ]); - testSuggestions('from a | stats a=min(b), b=max(', [ - ...fields.map(({ name }) => name), - ...statsAggregationFunctionDefinitions.map( - ({ name, signatures, ...defRest }) => - getFunctionSignatures({ name, ...defRest, signatures })[0].declaration - ), + 'var1', ]); }); - describe.skip('enrich', () => { + describe('enrich', () => { for (const prevCommand of [ '', '| enrich other-policy ', @@ -277,105 +439,223 @@ describe('autocomplete', () => { 'kubernetes.something.something', 'listField', ]); - testSuggestions(`from a ${prevCommand}| enrich policy on b `, ['with', '|']); + testSuggestions(`from a ${prevCommand}| enrich policy on b `, ['with', '|', ',']); testSuggestions(`from a ${prevCommand}| enrich policy on b with `, [ - 'var0', - 'stringField', - 'numberField', - 'dateField', - 'booleanField', - 'ipField', - 'any#Char$ field', - 'kubernetes.something.something', - 'listField', + 'var0 =', + ...getPolicyFields('policy'), ]); - testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 `, ['=', '|']); + testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 `, ['= $0', '|', ',']); testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = `, [ - 'stringField', - 'numberField', - 'dateField', - 'booleanField', - 'ipField', - 'any#Char$ field', - 'kubernetes.something.something', - 'listField', + ...getPolicyFields('policy'), ]); - testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = c `, ['|']); - testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = c, `, [ - 'var1', - 'stringField', - 'numberField', - 'dateField', - 'booleanField', - 'ipField', - 'any#Char$ field', - 'kubernetes.something.something', - 'listField', + testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = stringField `, [ + '|', + ',', ]); - testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = c, var1 `, ['=', '|']); - testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = c, var1 = `, [ - 'stringField', - 'numberField', - 'dateField', - 'booleanField', - 'ipField', - 'any#Char$ field', - 'kubernetes.something.something', - 'listField', + testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = stringField, `, [ + 'var1 =', + ...getPolicyFields('policy'), ]); + testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = stringField, var1 `, [ + '= $0', + '|', + ',', + ]); + testSuggestions( + `from a ${prevCommand}| enrich policy on b with var0 = stringField, var1 = `, + [...getPolicyFields('policy')] + ); testSuggestions(`from a ${prevCommand}| enrich policy with `, [ - 'var0', - 'otherField', - 'yetAnotherField', + 'var0 =', + ...getPolicyFields('policy'), ]); - testSuggestions(`from a ${prevCommand}| enrich policy with c`, ['=', '|', ',']); + testSuggestions(`from a ${prevCommand}| enrich policy with stringField`, ['= $0', '|', ',']); } }); - describe.skip('eval', () => { - const functionSuggestions = mathCommandDefinition.map(({ label }) => String(label)); - - testSuggestions('from a | eval ', ['var0']); - testSuggestions('from a | eval a ', ['=']); - testSuggestions('from a | eval a=', functionSuggestions); - testSuggestions('from a | eval a=b, ', ['var0']); - testSuggestions('from a | eval a=round', ['(']); - testSuggestions('from a | eval a=round(', ['FieldIdentifier']); - testSuggestions('from a | eval a=round(b) ', ['|', '+', '-', '/', '*']); - testSuggestions('from a | eval a=round(b),', ['var0']); - testSuggestions('from a | eval a=round(b) + ', ['FieldIdentifier', ...functionSuggestions]); - // NOTE: this is handled also partially in the suggestion wrapper with auto-injection of closing brackets - testSuggestions('from a | eval a=round(b', [')', 'FieldIdentifier']); - testSuggestions('from a | eval a=round(b), b=round(', ['FieldIdentifier']); - testSuggestions('from a | stats a=round(b), b=round(', ['FieldIdentifier']); - testSuggestions('from a | eval var0=round(b), var1=round(c) | stats ', ['var2']); + describe('eval', () => { + testSuggestions('from a | eval ', [ + 'var0 =', + ...fields.map(({ name }) => name), + ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + ]); + testSuggestions('from a | eval numberField ', [ + ...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true }, ['number']), + '|', + ',', + ]); + testSuggestions('from a | eval a=', [ + ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + ]); + testSuggestions('from a | eval a=abs(numberField), b= ', [ + ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + ]); + testSuggestions('from a | eval a=numberField, ', [ + 'var0 =', + ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + ]); + testSuggestions( + 'from a | eval a=round()', + [ + ...getFieldNamesByType('number'), + ...getFunctionSignaturesByReturnType('eval', 'number', { evalMath: true }, undefined, [ + 'round', + ]), + ], + '(' + ); + testSuggestions('from a | eval a=round(numberField) ', [ + ...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true }, ['number']), + '|', + ',', + ]); + testSuggestions('from a | eval a=round(numberField),', [ + 'var0 =', + ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + ]); + testSuggestions('from a | eval a=round(numberField) + ', [ + ...getFieldNamesByType('number'), + ...getFunctionSignaturesByReturnType('eval', 'number', { evalMath: true }), + 'a', // @TODO remove this + ]); + testSuggestions( + 'from a | stats avg(numberField) by stringField | eval ', + [ + 'var0 =', + ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + '`avg(numberField)`', + ], + ' ', + // make aware EVAL of the previous STATS command + [[], undefined, undefined] + ); + testSuggestions( + 'from a | eval abs(numberField) + 1 | eval ', + [ + 'var0 =', + ...getFieldNamesByType('any'), + ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + // @TODO: leverage the location data to get the original text + // For now return back the trimmed version: + // the ANTLR parser trims all text so that's what it's stored in the AST + '`abs(numberField)+1`', + ], + ' ' + ); + testSuggestions( + 'from a | stats avg(numberField) by stringField | eval ', + [ + 'var0 =', + ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + '`avg(numberField)`', + ], + ' ', + // make aware EVAL of the previous STATS command with the buggy field name from expression + [[{ name: 'avg_numberField_', type: 'number' }], undefined, undefined] + ); + testSuggestions( + 'from a | stats avg(numberField), avg(kubernetes.something.something) by stringField | eval ', + [ + 'var0 =', + ...getFunctionSignaturesByReturnType('eval', 'any', { evalMath: true }), + '`avg(numberField)`', + '`avg(kubernetes.something.something)`', + ], + ' ', + // make aware EVAL of the previous STATS command with the buggy field name from expression + [ + [ + { name: 'avg_numberField_', type: 'number' }, + { name: 'avg_kubernetes.something.something_', type: 'number' }, + ], + undefined, + undefined, + ] + ); + testSuggestions( + 'from a | eval a=round(numberField), b=round()', + [ + ...getFieldNamesByType('number'), + ...getFunctionSignaturesByReturnType('eval', 'number', { evalMath: true }, undefined, [ + 'round', + ]), + ], + '(' + ); + // Test suggestions for each possible param, within each signature variation, for each function + for (const fn of evalFunctionsDefinitions) { + // skip this fn for the moment as it's quite hard to test + if (fn.name !== 'auto_bucket') { + for (const signature of fn.signatures) { + signature.params.forEach((param, i) => { + if (i < signature.params.length - 1) { + const canHaveMoreArgs = + signature.params.filter(({ optional }, j) => !optional && j > i).length > i; + testSuggestions( + `from a | eval ${fn.name}(${Array(i).fill('field').join(', ')}${i ? ',' : ''} )`, + [ + ...getFieldNamesByType(param.type).map((f) => (canHaveMoreArgs ? `${f},` : f)), + ...getFunctionSignaturesByReturnType( + 'eval', + param.type, + { evalMath: true }, + undefined, + [fn.name] + ).map((l) => (canHaveMoreArgs ? `${l},` : l)), + ...getLiteralsByType(param.type).map((d) => (canHaveMoreArgs ? `${d},` : d)), + ] + ); + testSuggestions( + `from a | eval var0 = ${fn.name}(${Array(i).fill('field').join(', ')}${ + i ? ',' : '' + } )`, + [ + ...getFieldNamesByType(param.type).map((f) => (canHaveMoreArgs ? `${f},` : f)), + ...getFunctionSignaturesByReturnType( + 'eval', + param.type, + { evalMath: true }, + undefined, + [fn.name] + ).map((l) => (canHaveMoreArgs ? `${l},` : l)), + ...getLiteralsByType(param.type).map((d) => (canHaveMoreArgs ? `${d},` : d)), + ] + ); + } + }); + } + } + } describe('date math', () => { - const dateSuggestions = [ - 'year', - 'month', - 'week', - 'day', - 'hour', - 'minute', - 'second', - 'millisecond', - ].flatMap((v) => [v, `${v}s`]); - const dateMathSymbols = ['+', '-']; - testSuggestions('from a | eval a = 1 ', dateMathSymbols.concat(dateSuggestions, ['|'])); - testSuggestions('from a | eval a = 1 year ', dateMathSymbols.concat(dateSuggestions, ['|'])); - testSuggestions( - 'from a | eval a = 1 day + 2 ', - dateMathSymbols.concat(dateSuggestions, ['|']) - ); - // testSuggestions( - // 'from a | eval var0=date_trunc(', - // ['FieldIdentifier'].concat(...getDurationItemsWithQuantifier().map(({ label }) => label)) - // ); + const dateSuggestions = timeLiterals.map(({ name }) => name); + // If a literal number is detected then suggest also date period keywords + testSuggestions('from a | eval a = 1 ', [ + ...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true }, ['number']), + ...dateSuggestions, + '|', + ',', + ]); + testSuggestions('from a | eval a = 1 year ', [ + ...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true }, ['time_interval']), + '|', + ',', + ]); + testSuggestions('from a | eval a = 1 day + 2 ', [ + ...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true }, ['number']), + ...dateSuggestions, + '|', + ',', + ]); testSuggestions( - 'from a | eval var0=date_trunc(2 ', - [')', 'FieldIdentifier'].concat(dateSuggestions) + 'from a | eval var0=date_trunc()', + [...getLiteralsByType('time_literal').map((t) => `${t},`)], + '(' ); + testSuggestions('from a | eval var0=date_trunc(2 )', [ + ...dateSuggestions.map((t) => `${t},`), + ',', + ]); }); }); }); diff --git a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.ts b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.ts index 0a29d3ffdc05f..ca39bd81bb4ea 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/autocomplete.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; +import uniqBy from 'lodash/uniqBy'; import type { monaco } from '../../../../monaco_imports'; import type { AutocompleteCommandDefinition, ESQLCallbacks } from './types'; import { nonNullable } from '../ast_helpers'; @@ -16,25 +16,29 @@ import { getCommandOption, getFunctionDefinition, isAssignment, + isAssignmentComplete, isColumnItem, isFunctionItem, isIncompleteItem, isLiteralItem, isOptionItem, + isRestartingExpression, isSourceItem, + isTimeIntervalItem, monacoPositionToOffset, } from '../shared/helpers'; -import { collectVariables } from '../shared/variables'; -import { - ESQLAst, +import { collectVariables, excludeVariablesFromCurrentCommand } from '../shared/variables'; +import type { + AstProviderFn, ESQLAstItem, ESQLCommand, ESQLCommandOption, ESQLFunction, ESQLSingleAstItem, } from '../types'; -import type { ESQLPolicy, ESQLRealField, ESQLVariable } from '../validation/types'; +import type { ESQLPolicy, ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types'; import { + commaCompleteItem, commandAutocompleteDefinitions, getAssignmentDefinitionCompletitionItem, getBuiltinCompatibleFunctionDefinition, @@ -51,116 +55,22 @@ import { buildMatchingFieldsDefinition, getCompatibleLiterals, buildConstantsDefinitions, + buildVariablesDefinitions, + buildOptionDefinition, + TRIGGER_SUGGESTION_COMMAND, } from './factories'; -import { getFunctionSignatures } from '../definitions/helpers'; - -const EDITOR_MARKER = 'marker_esql_editor'; +import { EDITOR_MARKER } from '../shared/constants'; +import { getAstContext, removeMarkerArgFromArgsList } from '../shared/context'; type GetSourceFn = () => Promise; -type GetFieldsByTypeFn = (type: string | string[]) => Promise; +type GetFieldsByTypeFn = ( + type: string | string[], + ignored?: string[] +) => Promise; type GetFieldsMapFn = () => Promise>; type GetPoliciesFn = () => Promise; type GetPolicyMetadataFn = (name: string) => Promise; -function findNode(nodes: ESQLAstItem[], offset: number): ESQLSingleAstItem | undefined { - for (const node of nodes) { - if (Array.isArray(node)) { - const ret = findNode(node, offset); - if (ret) { - return ret; - } - } else { - if (node.location.min <= offset && node.location.max >= offset) { - if ('args' in node) { - const ret = findNode(node.args, offset); - // if the found node is the marker, then return its parent - if (ret?.text === EDITOR_MARKER) { - return node; - } - if (ret) { - return ret; - } - } - return node; - } - } - } -} - -function findCommand(ast: ESQLAst, offset: number) { - const commandIndex = ast.findIndex( - ({ location }) => location.min <= offset && location.max >= offset - ); - return ast[commandIndex] || ast[ast.length - 1]; -} - -function findAstPosition(ast: ESQLAst, offset: number) { - const command = findCommand(ast, offset); - if (!command) { - return { command: undefined, node: undefined }; - } - const node = findNode(command.args, offset); - return { command, node }; -} - -function isNotEnrichClauseAssigment(node: ESQLFunction, command: ESQLCommand) { - return node.name !== '=' && command.name !== 'enrich'; -} - -function getContext(innerText: string, ast: ESQLAst, offset: number) { - const { command, node } = findAstPosition(ast, offset); - - if (node) { - if (node.type === 'function' && ['in', 'not_in'].includes(node.name)) { - // command ... a in ( ) - return { type: 'list' as const, command, node }; - } - // - if (node.type === 'function' && isNotEnrichClauseAssigment(node, command)) { - // command ... fn( ) - return { type: 'function' as const, command, node }; - } - if (node.type === 'option') { - // command ... by - return { type: 'option' as const, command, node }; - } - } - if (command && command.args.length) { - const lastArg = command.args[command.args.length - 1]; - if ( - isOptionItem(lastArg) && - (lastArg.incomplete || !lastArg.args.length || handleEnrichWithClause(lastArg)) - ) { - return { type: 'option' as const, command, node: lastArg }; - } - } - if (!command || (innerText.length <= offset && getLastCharFromTrimmed(innerText) === '|')) { - // // ... | - return { type: 'newCommand' as const, command: undefined, node: undefined }; - } - - // command a ... OR command a = ... - return { type: 'expression' as const, command, node }; -} - -function isEmptyValue(text: string) { - return [EDITOR_MARKER, ''].includes(text); -} - -// The enrich with clause it a bit tricky to detect, so it deserves a specific check -function handleEnrichWithClause(option: ESQLCommandOption) { - const fnArg = isFunctionItem(option.args[0]) ? option.args[0] : undefined; - if (fnArg) { - if (fnArg.name === '=' && isColumnItem(fnArg.args[0]) && fnArg.args[1]) { - const assignValue = fnArg.args[1]; - if (Array.isArray(assignValue) && isColumnItem(assignValue[0])) { - return fnArg.args[0].name === assignValue[0].name || isEmptyValue(assignValue[0].name); - } - } - } - return false; -} - function hasSameArgBothSides(assignFn: ESQLFunction) { if (assignFn.name === '=' && isColumnItem(assignFn.args[0]) && assignFn.args[1]) { const assignValue = assignFn.args[1]; @@ -188,23 +98,11 @@ function appendEnrichFields( function getFinalSuggestions({ comma }: { comma?: boolean } = { comma: true }) { const finalSuggestions = [pipeCompleteItem]; if (comma) { - finalSuggestions.push({ - label: ',', - insertText: ',', - kind: 1, - detail: i18n.translate('monaco.esql.autocomplete.commaDoc', { - defaultMessage: 'Comma (,)', - }), - sortText: 'B', - }); + finalSuggestions.push(commaCompleteItem); } return finalSuggestions; } -function getLastCharFromTrimmed(text: string) { - return text[text.trimEnd().length - 1]; -} - function isMathFunction(char: string) { return ['+', '-', '*', '/', '%', '='].some((op) => char === op); } @@ -213,48 +111,6 @@ function isComma(char: string) { return char === ','; } -export function getSignatureHelp( - model: monaco.editor.ITextModel, - position: monaco.Position, - context: monaco.languages.SignatureHelpContext, - astProvider: (text: string | undefined) => Promise<{ ast: ESQLAst }> -): monaco.languages.SignatureHelpResult { - return { - value: { signatures: [], activeParameter: 0, activeSignature: 0 }, - dispose: () => {}, - }; -} - -export async function getHoverItem( - model: monaco.editor.ITextModel, - position: monaco.Position, - token: monaco.CancellationToken, - astProvider: (text: string | undefined) => Promise<{ ast: ESQLAst }> -) { - const innerText = model.getValue(); - const offset = monacoPositionToOffset(innerText, position); - - const { ast } = await astProvider(innerText); - const astContext = getContext(innerText, ast, offset); - - if (astContext.type !== 'function') { - return { contents: [] }; - } - - const fnDefinition = getFunctionDefinition(astContext.node.name); - - if (!fnDefinition) { - return { contents: [] }; - } - - return { - contents: [ - { value: getFunctionSignatures(fnDefinition)[0].declaration }, - { value: fnDefinition.description }, - ], - }; -} - function isSourceCommand({ label }: AutocompleteCommandDefinition) { return ['from', 'row', 'show'].includes(String(label)); } @@ -263,7 +119,7 @@ export async function suggest( model: monaco.editor.ITextModel, position: monaco.Position, context: monaco.languages.CompletionContext, - astProvider: (text: string | undefined) => Promise<{ ast: ESQLAst }>, + astProvider: AstProviderFn, resourceRetriever?: ESQLCallbacks ): Promise { const innerText = model.getValue(); @@ -275,6 +131,7 @@ export async function suggest( context.triggerCharacter === ',' || context.triggerKind === 0 || (context.triggerCharacter === ' ' && + // make this more robust (isMathFunction(innerText[offset - 2]) || isComma(innerText[offset - 2]))) ) { finalText = `${innerText.substring(0, offset)}${EDITOR_MARKER}${innerText.substring(offset)}`; @@ -282,12 +139,11 @@ export async function suggest( const { ast } = await astProvider(finalText); - const astContext = getContext(innerText, ast, offset); + const astContext = getAstContext(innerText, ast, offset); const { getFieldsByType, getFieldsMap } = getFieldsByTypeRetriever(resourceRetriever); const getSources = getSourcesRetriever(resourceRetriever); const { getPolicies, getPolicyMetadata } = getPolicyRetriever(resourceRetriever); - // console.log({ finalText, innerText, astContext, ast, offset }); if (astContext.type === 'newCommand') { // propose main commands here // filter source commands if already defined @@ -308,35 +164,34 @@ export async function suggest( getSources, getFieldsByType, getFieldsMap, - getPolicies + getPolicies, + getPolicyMetadata ); } if (astContext.type === 'option') { - return getOptionArgsSuggestions( - innerText, - ast, - astContext.node, - astContext.command, - getFieldsByType, - getFieldsMap, - getPolicyMetadata - ); + // need this wrap/unwrap thing to make TS happy + const { option, ...rest } = astContext; + if (option && isOptionItem(option)) { + return getOptionArgsSuggestions( + innerText, + ast, + { option, ...rest }, + getFieldsByType, + getFieldsMap, + getPolicyMetadata + ); + } } if (astContext.type === 'function') { - // behave like list return getFunctionArgsSuggestions( innerText, ast, - astContext.node, - astContext.command, + astContext, getFieldsByType, getFieldsMap, getPolicyMetadata ); } - - // console.log({ ast, triggerContext }); - // throw Error(`Where am I?`); return []; } @@ -351,14 +206,16 @@ function getFieldsByTypeRetriever(resourceRetriever?: ESQLCallbacks) { } }; return { - getFieldsByType: async (expectedType: string | string[] = 'any') => { + getFieldsByType: async (expectedType: string | string[] = 'any', ignored: string[] = []) => { const types = Array.isArray(expectedType) ? expectedType : [expectedType]; await getFields(); return buildFieldsDefinitions( Array.from(cacheFields.values()) - ?.filter(({ type }) => { + ?.filter(({ name, type }) => { const ts = Array.isArray(type) ? type : [type]; - return ts.some((t) => types[0] === 'any' || types.includes(t)); + return ( + !ignored.includes(name) && ts.some((t) => types[0] === 'any' || types.includes(t)) + ); }) .map(({ name }) => name) || [] ); @@ -394,11 +251,6 @@ function getSourcesRetriever(resourceRetriever?: ESQLCallbacks) { }; } -const TRIGGER_SUGGESTION_COMMAND = { - title: 'Trigger Suggestion Dialog', - id: 'editor.action.triggerSuggest', -}; - function findNewVariable(variables: Map) { let autoGeneratedVariableCounter = 0; let name = `var${autoGeneratedVariableCounter++}`; @@ -408,6 +260,125 @@ function findNewVariable(variables: Map) { return name; } +function areCurrentArgsValid( + command: ESQLCommand, + node: ESQLAstItem, + references: Pick +) { + // unfortunately here we need to bake some command-specific logic + if (command.name === 'stats') { + if (node) { + // consider the following expressions not complete yet + // ... | stats a + // ... | stats a = + if (isColumnItem(node) || (isAssignment(node) && !isAssignmentComplete(node))) { + return false; + } + } + } + if (command.name === 'eval') { + if (node) { + if (isFunctionItem(node)) { + if (isAssignment(node)) { + return isAssignmentComplete(node); + } else { + return isFunctionArgComplete(node, references).complete; + } + } + } + } + if (command.name === 'where') { + if (node) { + if ( + isColumnItem(node) || + (isFunctionItem(node) && !isFunctionArgComplete(node, references).complete) + ) { + return false; + } else { + return ( + extractFinalTypeFromArg(node, references) === + getCommandDefinition(command.name).signature.params[0].type + ); + } + } + } + if (command.name === 'rename') { + if (node) { + if (isColumnItem(node)) { + return true; + } + } + } + return true; +} + +function extractFinalTypeFromArg( + arg: ESQLAstItem, + references: Pick +): string | undefined { + if (Array.isArray(arg)) { + return extractFinalTypeFromArg(arg[0], references); + } + if (isColumnItem(arg) || isLiteralItem(arg)) { + if (isLiteralItem(arg)) { + return arg.literalType; + } + if (isColumnItem(arg)) { + const hit = getColumnHit(arg.name, 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, + references: Pick +) { + const fnDefinition = getFunctionDefinition(arg.name)!; + const cleanedArgs = removeMarkerArgFromArgsList(arg)!.args; + const argLengthCheck = fnDefinition.signatures.some((def) => { + if (def.infiniteParams && cleanedArgs.length > 0) { + return true; + } + if (def.minParams && cleanedArgs.length >= def.minParams) { + return true; + } + if (cleanedArgs.length === def.params.length) { + return true; + } + return cleanedArgs.length >= def.params.filter(({ optional }) => !optional).length; + }); + if (!argLengthCheck) { + return { complete: false, reason: 'fewArgs' }; + } + const hasCorrectTypes = fnDefinition.signatures.some((def) => { + return arg.args.every((a, index) => { + if (def.infiniteParams) { + return true; + } + return def.params[index].type === extractFinalTypeFromArg(a, references); + }); + }); + if (!hasCorrectTypes) { + return { complete: false, reason: 'wrongTypes' }; + } + return { complete: true }; +} + async function getExpressionSuggestionsByType( innerText: string, commands: ESQLCommand[], @@ -421,162 +392,402 @@ async function getExpressionSuggestionsByType( getSources: GetSourceFn, getFieldsByType: GetFieldsByTypeFn, getFieldsMap: GetFieldsMapFn, - getPolicies: GetPoliciesFn + getPolicies: GetPoliciesFn, + getPolicyMetadata: GetPolicyMetadataFn ) { const commandDef = getCommandDefinition(command.name); // get the argument position let argIndex = command.args.length; - const lastArg = command.args[Math.max(argIndex - 1, 0)]; + const prevIndex = Math.max(argIndex - 1, 0); + const lastArg = removeMarkerArgFromArgsList(command)!.args[prevIndex]; if (isIncompleteItem(lastArg)) { - argIndex = Math.max(argIndex - 1, 0); + argIndex = prevIndex; } - const isNewExpression = getLastCharFromTrimmed(innerText) === ',' || argIndex === 0; + + // if a node is not specified use the lastArg + // mind to give priority to node as lastArg might be a function root + // => "a > b and c == d" gets translated into and( gt(a, b) , eq(c, d) ) => hence "and" is lastArg + const nodeArg = node || lastArg; + // A new expression is considered either + // * just after a command name => i.e. ... | STATS + // * or after a comma => i.e. STATS fieldA, + const isNewExpression = isRestartingExpression(innerText) || argIndex === 0; + + // Are options already declared? This is useful to suggest only new ones const optionsAlreadyDeclared = ( command.args.filter((arg) => isOptionItem(arg)) as ESQLCommandOption[] ).map(({ name }) => ({ name, index: commandDef.options.findIndex(({ name: defName }) => defName === name), })); - const optionsAvailable = commandDef.options.filter(({ name }, index) => { const optArg = optionsAlreadyDeclared.find(({ name: optionName }) => optionName === name); return (!optArg && !optionsAlreadyDeclared.length) || (optArg && index > optArg.index); }); + // get the next definition for the given command let argDef = commandDef.signature.params[argIndex]; - if (!argDef && isNewExpression && commandDef.signature.multipleParams) { - argDef = commandDef.signature.params[0]; + // tune it for the variadic case + if (!argDef) { + // this is the case of a comma argument + if (commandDef.signature.multipleParams) { + if (isNewExpression || (isAssignment(lastArg) && !isAssignmentComplete(lastArg))) { + // i.e. ... | a, + // i.e. ... | a = ..., b = + argDef = commandDef.signature.params[0]; + } + } + + // this is the case where there's an argument, but it's of the wrong type + // i.e. ... | WHERE numberField (WHERE wants a boolean expression!) + // i.e. ... | STATS numberfield (STATS wants a function expression!) + if (!isNewExpression && nodeArg && !Array.isArray(nodeArg)) { + const prevArg = commandDef.signature.params[prevIndex]; + // in some cases we do not want to go back as the command only accepts a literal + // i.e. LIMIT 5 -> that's it, so no argDef should be assigned + + // make an exception for STATS (STATS is the only command who accept a function type as arg) + if ( + prevArg && + (prevArg.type === 'function' || (!Array.isArray(nodeArg) && prevArg.type !== nodeArg.type)) + ) { + if (!isLiteralItem(nodeArg) || !prevArg.literalOnly) { + argDef = prevArg; + } + } + } } - const lastValidArgDef = commandDef.signature.params[commandDef.signature.params.length - 1]; - const suggestions: AutocompleteCommandDefinition[] = []; - const fieldsMap: Map = await (argDef && - !isIncompleteItem(lastArg) && - isColumnItem(lastArg) - ? getFieldsMap() - : new Map()); + // collect all fields + variables to suggest + const fieldsMap: Map = await (argDef ? getFieldsMap() : new Map()); const anyVariables = collectVariables(commands, fieldsMap); + // enrich with assignment has some special rules who are handled somewhere else - const canHaveAssignments = ['eval', 'stats', 'where', 'row'].includes(command.name); + const canHaveAssignments = ['eval', 'stats', 'row'].includes(command.name); - if (canHaveAssignments && isNewExpression) { - suggestions.push(buildNewVarDefinition(findNewVariable(anyVariables))); - } - if (canHaveAssignments && !isNewExpression && lastArg && !isIncompleteItem(lastArg)) { - if (!argDef || lastValidArgDef.type !== 'function') { - if (isColumnItem(lastArg) || isLiteralItem(lastArg)) { - let argType = 'number'; - if (isLiteralItem(lastArg)) { - argType = lastArg.literalType; - } - if (isColumnItem(lastArg)) { - const hit = getColumnHit(lastArg.name, { fields: fieldsMap, variables: anyVariables }); - if (hit) { - argType = hit.type; - } - } - suggestions.push(...getBuiltinCompatibleFunctionDefinition(command.name, argType)); + const references = { fields: fieldsMap, variables: anyVariables }; + + const suggestions: AutocompleteCommandDefinition[] = []; + + // in this flow there's a clear plan here from argument definitions so try to follow it + if (argDef) { + if (argDef.type === 'column' || argDef.type === 'any' || argDef.type === 'function') { + if (isNewExpression && canHaveAssignments) { + // i.e. + // ... | ROW + // ... | STATS + // ... | STATS ..., + // ... | EVAL + // ... | EVAL ..., + suggestions.push(buildNewVarDefinition(findNewVariable(anyVariables))); } } - if (canHaveAssignments && lastValidArgDef?.type === 'function' && isColumnItem(lastArg)) { - suggestions.push(getAssignmentDefinitionCompletitionItem()); - } - } else if (argDef) { + // Suggest fields or variables if (argDef.type === 'column' || argDef.type === 'any') { - suggestions.push( - ...(await getAllSuggestionsByType( - [argDef.innerType || 'any'], - command.name, - getFieldsByType, - { - functions: canHaveAssignments, - fields: true, - newVariables: false, + // ... | + if (!nodeArg) { + suggestions.push( + ...(await getFieldsOrFunctionsSuggestions( + [argDef.innerType || 'any'], + command.name, + getFieldsByType, + { + functions: canHaveAssignments, + fields: true, + variables: anyVariables, + } + )) + ); + } + } + if (argDef.type === 'function' || argDef.type === 'any') { + if (isColumnItem(nodeArg)) { + // ... | STATS a + // ... | EVAL a + const nodeArgType = extractFinalTypeFromArg(nodeArg, references); + if (nodeArgType) { + suggestions.push(...getBuiltinCompatibleFunctionDefinition(command.name, nodeArgType)); + } else { + suggestions.push(getAssignmentDefinitionCompletitionItem()); + } + } + if (isNewExpression || (isAssignment(nodeArg) && !isAssignmentComplete(nodeArg))) { + // ... | STATS a = + // ... | EVAL a = + // ... | STATS a = ..., + // ... | EVAL a = ..., + // ... | STATS a = ..., b = + // ... | EVAL a = ..., b = + suggestions.push( + ...(await getFieldsOrFunctionsSuggestions(['any'], command.name, getFieldsByType, { + functions: true, + fields: false, + variables: nodeArg ? undefined : anyVariables, + })) + ); + } + } + + if (argDef.type === 'any') { + // ... | EVAL var = field + // ... | EVAL var = fn(field) + // make sure we're still in the same assignment context and there's no comma (newExpression ensures that) + if (!isNewExpression) { + if (isAssignment(nodeArg) && isAssignmentComplete(nodeArg)) { + const [rightArg] = nodeArg.args[1] as [ESQLSingleAstItem]; + const nodeArgType = extractFinalTypeFromArg(rightArg, references); + suggestions.push( + ...getBuiltinCompatibleFunctionDefinition(command.name, nodeArgType || 'any') + ); + if (nodeArgType === 'number' && isLiteralItem(rightArg)) { + // ... EVAL var = 1 + suggestions.push(...getCompatibleLiterals(command.name, ['time_literal_unit'])); } - )) - ); + if (isFunctionItem(rightArg)) { + if (rightArg.args.some(isTimeIntervalItem)) { + const lastFnArg = rightArg.args[rightArg.args.length - 1]; + const lastFnArgType = extractFinalTypeFromArg(lastFnArg, references); + if (lastFnArgType === 'number' && isLiteralItem(lastFnArg)) + // ... EVAL var = 1 year + 2 + suggestions.push(...getCompatibleLiterals(command.name, ['time_literal_unit'])); + } + } + } else { + if (isFunctionItem(nodeArg)) { + const nodeArgType = extractFinalTypeFromArg(nodeArg, references); + suggestions.push( + ...(await getBuiltinFunctionNextArgument( + command, + argDef, + nodeArg, + nodeArgType || 'any', + references, + getFieldsByType + )) + ); + if (nodeArg.args.some(isTimeIntervalItem)) { + const lastFnArg = nodeArg.args[nodeArg.args.length - 1]; + const lastFnArgType = extractFinalTypeFromArg(lastFnArg, references); + if (lastFnArgType === 'number' && isLiteralItem(lastFnArg)) + // ... EVAL var = 1 year + 2 + suggestions.push(...getCompatibleLiterals(command.name, ['time_literal_unit'])); + } + } + } + } } + + // if the definition includes a list of constants, suggest them if (argDef.values) { + // ... | ... suggestions.push(...buildConstantsDefinitions(argDef.values)); } - // @TODO: better handle the where command here - if (argDef.type === 'boolean' && command.name === 'where') { - suggestions.push( - ...(await getAllSuggestionsByType(['any'], command.name, getFieldsByType, { - functions: true, - fields: true, - newVariables: false, - })) - ); + // If the type is specified try to dig deeper in the definition to suggest the best candidate + if (['string', 'number', 'boolean'].includes(argDef.type) && !argDef.values) { + // it can be just literal values (i.e. "string") + if (argDef.literalOnly) { + // ... | ... + suggestions.push(...getCompatibleLiterals(command.name, [argDef.type], [argDef.name])); + } else { + // or it can be anything else as long as it is of the right type and the end (i.e. column or function) + if (!nodeArg) { + // ... | + // In this case start suggesting something not strictly based on type + suggestions.push( + ...(await getFieldsOrFunctionsSuggestions(['any'], command.name, getFieldsByType, { + functions: true, + fields: true, + variables: anyVariables, + })) + ); + } else { + // if something is already present, leverage its type to suggest something in context + const nodeArgType = extractFinalTypeFromArg(nodeArg, references); + // These cases can happen here, so need to identify each and provide the right suggestion + // i.e. ... | field + // i.e. ... | field + + // i.e. ... | field >= + // i.e. ... | field > 0 + // i.e. ... | field + otherN + + if (nodeArgType) { + if (isFunctionItem(nodeArg)) { + suggestions.push( + ...(await getBuiltinFunctionNextArgument( + command, + argDef, + nodeArg, + nodeArgType, + references, + getFieldsByType + )) + ); + } else { + // i.e. ... | field + suggestions.push( + ...getBuiltinCompatibleFunctionDefinition(command.name, nodeArgType) + ); + } + } + } + } } if (argDef.type === 'source') { if (argDef.innerType === 'policy') { + // ... | ENRICH const policies = await getPolicies(); suggestions.push(...(policies.length ? policies : [buildNoPoliciesAvailableDefinition()])); } else { + // FROM // @TODO: filter down the suggestions here based on other existing sources defined suggestions.push(...(await getSources())); } } - if (['string', 'number', 'boolean'].includes(argDef.type) && !argDef.values) { - suggestions.push(...getCompatibleLiterals(command.name, [argDef.type], [argDef.name])); - } } const nonOptionArgs = command.args.filter( (arg) => !isOptionItem(arg) && !Array.isArray(arg) && !arg.incomplete ); + // Perform some checks on mandatory arguments const mandatoryArgsAlreadyPresent = (commandDef.signature.multipleParams && nonOptionArgs.length > 1) || nonOptionArgs.length >= commandDef.signature.params.filter(({ optional }) => !optional).length || - (!argDef && lastValidArgDef?.type === 'function'); + argDef?.type === 'function'; + + // check if declared args are fully valid for the given command + const currentArgsAreValidForCommand = areCurrentArgsValid(command, nodeArg, references); - if (!isNewExpression && mandatoryArgsAlreadyPresent) { + // latest suggestions: options and final ones + if ( + (!isNewExpression && mandatoryArgsAlreadyPresent && currentArgsAreValidForCommand) || + optionsAlreadyDeclared.length + ) { + // suggest some command options if (optionsAvailable.length) { + suggestions.push(...optionsAvailable.map(buildOptionDefinition)); + } + + if (!optionsAvailable.length || optionsAvailable.every(({ optional }) => optional)) { + // now suggest pipe or comma suggestions.push( - ...optionsAvailable.map((option) => { - const completeItem: AutocompleteCommandDefinition = { - label: option.name, - insertText: option.name, - kind: 21, - detail: option.description, - sortText: 'D', - }; - if (option.wrapped) { - completeItem.insertText = `${option.wrapped[0]}${option.name} $0 ${option.wrapped[1]}`; - completeItem.insertTextRules = 4; // monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; - } - return completeItem; + ...getFinalSuggestions({ + comma: + commandDef.signature.multipleParams && + optionsAvailable.length === commandDef.options.length, }) ); } - suggestions.push( - ...getFinalSuggestions({ - comma: - commandDef.signature.multipleParams && - optionsAvailable.length === commandDef.options.length, - }) - ); + } + // Due to some logic overlapping functions can be repeated + // so dedupe here based on insertText string (it can differ from name) + return uniqBy(suggestions, (suggestion) => suggestion.insertText); +} + +async function getBuiltinFunctionNextArgument( + command: ESQLCommand, + argDef: { type: string }, + nodeArg: ESQLFunction, + nodeArgType: string, + references: Pick, + getFieldsByType: GetFieldsByTypeFn +) { + const suggestions = []; + const isFnComplete = isFunctionArgComplete(nodeArg, references); + if (isFnComplete.complete) { + // i.e. ... | field > 0 + // i.e. ... | field + otherN + suggestions.push(...getBuiltinCompatibleFunctionDefinition(command.name, nodeArgType || 'any')); + } else { + // i.e. ... | field >= + // i.e. ... | field + + // i.e. ... | field and + + // Because it's an incomplete function, need to extract the type of the current argument + // and suggest the next argument based on types + + // pick the last arg and check its type to verify whether is incomplete for the given function + const cleanedArgs = removeMarkerArgFromArgsList(nodeArg)!.args; + const nestedType = extractFinalTypeFromArg(nodeArg.args[cleanedArgs.length - 1], references); + + if (isFnComplete.reason === 'fewArgs') { + suggestions.push( + ...(await getFieldsOrFunctionsSuggestions( + [nestedType || nodeArgType || 'any'], + command.name, + getFieldsByType, + { + functions: true, + fields: true, + variables: references.variables, + } + )) + ); + } + if (isFnComplete.reason === 'wrongTypes') { + if (nestedType) { + // suggest something to complete the builtin function + if (nestedType !== argDef.type) { + suggestions.push( + ...getBuiltinCompatibleFunctionDefinition(command.name, nestedType, [argDef.type]) + ); + } + } + } } return suggestions; } -async function getAllSuggestionsByType( +async function getFieldsOrFunctionsSuggestions( types: string[], commandName: string, getFieldsByType: GetFieldsByTypeFn, { functions, fields, - newVariables, - }: { functions: boolean; newVariables: boolean; fields: boolean } + variables, + }: { + functions: boolean; + fields: boolean; + variables?: Map; + }, + { + ignoreFn = [], + ignoreFields = [], + }: { + ignoreFn?: string[]; + ignoreFields?: string[]; + } = {} ): Promise { const filteredFieldsByType = (await (fields - ? getFieldsByType(types) + ? getFieldsByType(types, ignoreFields) : [])) as AutocompleteCommandDefinition[]; + const filteredVariablesByType: string[] = []; + if (variables) { + for (const variable of variables.values()) { + if (types.includes('any') || types.includes(variable[0].type)) { + 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_ + if ( + filteredVariablesByType.length && + filteredVariablesByType.some((v) => /[^a-zA-Z\d]/.test(v)) + ) { + for (const variable of filteredVariablesByType) { + const underscoredName = variable.replace(/[^a-zA-Z\d]/g, '_'); + const index = filteredFieldsByType.findIndex(({ label }) => underscoredName === label); + if (index >= 0) { + filteredFieldsByType.splice(index); + } + } + } + } + const suggestions = filteredFieldsByType.concat( - functions ? getCompatibleFunctionDefinition(commandName, types) : [], + functions ? getCompatibleFunctionDefinition(commandName, types, ignoreFn) : [], + variables ? buildVariablesDefinitions(filteredVariablesByType) : [], getCompatibleLiterals(commandName, types) // literals are handled internally ); @@ -592,28 +803,109 @@ async function getAllSuggestionsByType( async function getFunctionArgsSuggestions( innerText: string, commands: ESQLCommand[], - fn: ESQLFunction, - command: ESQLCommand, + { + command, + node, + }: { + command: ESQLCommand; + node: ESQLFunction; + }, getFieldsByType: GetFieldsByTypeFn, getFieldsMap: GetFieldsMapFn, getPolicyMetadata: GetPolicyMetadataFn ): Promise { - const fnDefinition = getFunctionDefinition(fn.name); + const fnDefinition = getFunctionDefinition(node.name); if (fnDefinition) { - const argIndex = Math.max(fn.args.length - 1, 0); - const types = fnDefinition.signatures.flatMap((signature) => signature.params[argIndex].type); - const suggestions = await getAllSuggestionsByType(types, command.name, getFieldsByType, { - functions: command.name !== 'stats', - fields: true, - newVariables: false, + const fieldsMap: Map = await getFieldsMap(); + const variablesExcludingCurrentCommandOnes = excludeVariablesFromCurrentCommand( + commands, + command, + fieldsMap + ); + // pick the type of the next arg + const shouldGetNextArgument = node.text.includes(EDITOR_MARKER); + let argIndex = Math.max(node.args.length, 0); + if (!shouldGetNextArgument && argIndex) { + argIndex -= 1; + } + const types = fnDefinition.signatures.flatMap((signature) => { + if (signature.params.length > argIndex) { + return signature.params[argIndex].type; + } + if (signature.infiniteParams) { + return signature.params[0].type; + } + return []; }); + const arg = node.args[argIndex]; + const hasMoreMandatoryArgs = - fnDefinition.signatures[0].params.filter(({ optional }) => !optional).length > argIndex + 1; + fnDefinition.signatures[0].params.filter( + ({ optional }, index) => !optional && index > argIndex + ).length > argIndex; + + const suggestions = []; + if (!arg) { + // ... | EVAL fn( ) + // ... | EVAL fn( field, ) + suggestions.push( + ...(await getFieldsOrFunctionsSuggestions( + types, + command.name, + getFieldsByType, + { + functions: command.name !== 'stats', + fields: true, + variables: variablesExcludingCurrentCommandOnes, + }, + // do not repropose the same function as arg + // i.e. avoid cases like abs(abs(abs(...))) with suggestions + { ignoreFn: [node.name] } + )) + ); + } + + // for eval and row commands try also to complete numeric literals with time intervals where possible + if (arg) { + if (command.name !== 'stats') { + if (isLiteralItem(arg) && arg.literalType === 'number') { + // ... | EVAL fn(2 ) + suggestions.push( + ...(await getFieldsOrFunctionsSuggestions( + ['time_literal_unit'], + command.name, + getFieldsByType, + { + functions: false, + fields: false, + variables: variablesExcludingCurrentCommandOnes, + } + )) + ); + } + } + if (hasMoreMandatoryArgs) { + // suggest a comma if there's another argument for the function + suggestions.push(commaCompleteItem); + } + // if there are other arguments in the function, inject automatically a comma after each suggestion + return suggestions.map((suggestion) => + suggestion !== commaCompleteItem + ? { + ...suggestion, + insertText: + hasMoreMandatoryArgs && !fnDefinition.builtin + ? `${suggestion.insertText},` + : suggestion.insertText, + } + : suggestion + ); + } return suggestions.map(({ insertText, ...rest }) => ({ ...rest, - insertText: hasMoreMandatoryArgs ? `${insertText},` : insertText, + insertText: hasMoreMandatoryArgs && !fnDefinition.builtin ? `${insertText},` : insertText, })); } return mathCommandDefinition; @@ -622,37 +914,57 @@ async function getFunctionArgsSuggestions( async function getOptionArgsSuggestions( innerText: string, commands: ESQLCommand[], - option: ESQLCommandOption, - command: ESQLCommand, + { + command, + option, + node, + }: { + command: ESQLCommand; + option: ESQLCommandOption; + node: ESQLSingleAstItem | undefined; + }, getFieldsByType: GetFieldsByTypeFn, getFieldsMaps: GetFieldsMapFn, getPolicyMetadata: GetPolicyMetadataFn ) { const optionDef = getCommandOption(option.name); const suggestions = []; + const isNewExpression = isRestartingExpression(innerText) || option.args.length === 0; if (command.name === 'enrich') { if (option.name === 'on') { - const policyName = isSourceItem(command.args[0]) ? command.args[0].name : undefined; - if (policyName) { - const [policyMetadata, fieldsMap] = await Promise.all([ - getPolicyMetadata(policyName), - getFieldsMaps(), - ]); - if (policyMetadata) { - suggestions.push( - ...buildMatchingFieldsDefinition( - policyMetadata.matchField, - Array.from(fieldsMap.keys()) - ) - ); + // if it's a new expression, suggest fields to match on + if (isNewExpression || (option && isAssignment(option.args[0]) && !option.args[1])) { + const policyName = isSourceItem(command.args[0]) ? command.args[0].name : undefined; + if (policyName) { + const [policyMetadata, fieldsMap] = await Promise.all([ + getPolicyMetadata(policyName), + getFieldsMaps(), + ]); + if (policyMetadata) { + suggestions.push( + ...buildMatchingFieldsDefinition( + policyMetadata.matchField, + Array.from(fieldsMap.keys()) + ) + ); + } } + } else { + // propose the with option + suggestions.push( + buildOptionDefinition(getCommandOption('with')!), + ...getFinalSuggestions({ + comma: true, + }) + ); } } if (option.name === 'with') { let argIndex = option.args.length; - const lastArg = option.args[Math.max(argIndex - 1, 0)]; + let lastArg = option.args[Math.max(argIndex - 1, 0)]; if (isIncompleteItem(lastArg)) { argIndex = Math.max(argIndex - 1, 0); + lastArg = option.args[argIndex]; } const policyName = isSourceItem(command.args[0]) ? command.args[0].name : undefined; if (policyName) { @@ -660,7 +972,6 @@ async function getOptionArgsSuggestions( getPolicyMetadata(policyName), getFieldsMaps(), ]); - const isNewExpression = getLastCharFromTrimmed(innerText) === ',' || argIndex === 0; const anyVariables = collectVariables( commands, appendEnrichFields(fieldsMap, policyMetadata) @@ -669,24 +980,34 @@ async function getOptionArgsSuggestions( if (isNewExpression) { suggestions.push(buildNewVarDefinition(findNewVariable(anyVariables))); } - if ( - policyMetadata && - ((isAssignment(option.args[0]) && !hasSameArgBothSides(option.args[0])) || - isNewExpression) - ) { - suggestions.push(...buildFieldsDefinitions(policyMetadata.enrichFields)); + + // make sure to remove the marker arg from the assign fn + const assignFn = isAssignment(lastArg) + ? (removeMarkerArgFromArgsList(lastArg) as ESQLFunction) + : undefined; + + if (policyMetadata) { + if (isNewExpression || (assignFn && !isAssignmentComplete(assignFn))) { + // ... | ENRICH ... WITH a = + suggestions.push(...buildFieldsDefinitions(policyMetadata.enrichFields)); + } } if ( - isAssignment(option.args[0]) && - hasSameArgBothSides(option.args[0]) && + assignFn && + hasSameArgBothSides(assignFn) && !isNewExpression && - lastArg && - !isIncompleteItem(lastArg) + !isIncompleteItem(assignFn) ) { + // ... | ENRICH ... WITH a + // effectively only assign will apper suggestions.push(...getBuiltinCompatibleFunctionDefinition(command.name, 'any')); } - if (isAssignment(option.args[0]) && hasSameArgBothSides(option.args[0])) { + if ( + assignFn && + (isAssignmentComplete(assignFn) || hasSameArgBothSides(assignFn)) && + !isNewExpression + ) { suggestions.push( ...getFinalSuggestions({ comma: true, @@ -696,17 +1017,37 @@ async function getOptionArgsSuggestions( } } } + if (command.name === 'rename') { + if (option.args.length < 2) { + const fieldsMap = await getFieldsMaps(); + const anyVariables = collectVariables(commands, fieldsMap); + suggestions.push(...buildVariablesDefinitions([findNewVariable(anyVariables)])); + } + } + if (optionDef) { if (!suggestions.length) { const argIndex = Math.max(option.args.length - 1, 0); const types = [optionDef.signature.params[argIndex].type].filter(nonNullable); - suggestions.push( - ...(await getAllSuggestionsByType(types, command.name, getFieldsByType, { - functions: false, - fields: true, - newVariables: false, - })) - ); + if (option.args.length && !isRestartingExpression(innerText)) { + suggestions.push( + ...getFinalSuggestions({ + comma: true, + }) + ); + } else if (!option.args.length || isRestartingExpression(innerText)) { + suggestions.push( + ...(await getFieldsOrFunctionsSuggestions( + types[0] === 'column' ? ['any'] : types, + command.name, + getFieldsByType, + { + functions: false, + fields: true, + } + )) + ); + } } } return suggestions; diff --git a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/complete_items.ts b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/complete_items.ts index eb368baa764d7..32b0e1bdc314d 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/complete_items.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/complete_items.ts @@ -32,14 +32,25 @@ export function getAssignmentDefinitionCompletitionItem() { export const getBuiltinCompatibleFunctionDefinition = ( command: string, - argType: string + argType: string, + returnTypes?: string[] ): AutocompleteCommandDefinition[] => { - return builtinFunctions - .filter( - ({ name, supportedCommands, signatures }) => - !/not_/.test(name) && - supportedCommands.includes(command) && - signatures.some(({ params }) => params.some((pArg) => pArg.type === argType)) + const compatibleFunctions = builtinFunctions.filter( + ({ name, supportedCommands, signatures, ignoreAsSuggestion }) => + !ignoreAsSuggestion && + !/not_/.test(name) && + supportedCommands.includes(command) && + signatures.some(({ params }) => params.some((pArg) => pArg.type === argType)) + ); + + if (!returnTypes) { + return compatibleFunctions.map(getAutocompleteBuiltinDefinition); + } + return compatibleFunctions + .filter((mathDefinition) => + mathDefinition.signatures.some( + (signature) => returnTypes[0] === 'any' || returnTypes.includes(signature.returnType) + ) ) .map(getAutocompleteBuiltinDefinition); }; @@ -57,3 +68,13 @@ export const pipeCompleteItem: AutocompleteCommandDefinition = { }), sortText: 'B', }; + +export const commaCompleteItem: AutocompleteCommandDefinition = { + label: ',', + insertText: ',', + kind: 1, + detail: i18n.translate('monaco.esql.autocomplete.commaDoc', { + defaultMessage: 'Comma (,)', + }), + sortText: 'B', +}; diff --git a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/factories.ts b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/factories.ts index d80dbf086d995..fc136a87b23da 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/autocomplete/factories.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/autocomplete/factories.ts @@ -12,12 +12,21 @@ import { statsAggregationFunctionDefinitions } from '../definitions/aggs'; import { evalFunctionsDefinitions } from '../definitions/functions'; import { getFunctionSignatures, getCommandSignature } from '../definitions/helpers'; import { chronoLiterals, timeLiterals } from '../definitions/literals'; -import { FunctionDefinition, CommandDefinition } from '../definitions/types'; +import { + FunctionDefinition, + CommandDefinition, + CommandOptionsDefinition, +} from '../definitions/types'; import { getCommandDefinition } from '../shared/helpers'; import { buildDocumentation, buildFunctionDocumentation } from './documentation_util'; const allFunctions = statsAggregationFunctionDefinitions.concat(evalFunctionsDefinitions); +export const TRIGGER_SUGGESTION_COMMAND = { + title: 'Trigger Suggestion Dialog', + id: 'editor.action.triggerSuggest', +}; + export function getAutocompleteFunctionDefinition(fn: FunctionDefinition) { const fullSignatures = getFunctionSignatures(fn); return { @@ -44,15 +53,24 @@ export function getAutocompleteBuiltinDefinition(fn: FunctionDefinition) { value: '', }, sortText: 'D', + command: TRIGGER_SUGGESTION_COMMAND, }; } +export const isCompatibleFunctionName = (fnName: string, command: string) => { + const fnSupportedByCommand = allFunctions.filter(({ supportedCommands }) => + supportedCommands.includes(command) + ); + return fnSupportedByCommand.some(({ name }) => name === fnName); +}; + export const getCompatibleFunctionDefinition = ( command: string, - returnTypes?: string[] + returnTypes?: string[], + ignored: string[] = [] ): AutocompleteCommandDefinition[] => { - const fnSupportedByCommand = allFunctions.filter(({ supportedCommands }) => - supportedCommands.includes(command) + const fnSupportedByCommand = allFunctions.filter( + ({ name, supportedCommands }) => supportedCommands.includes(command) && !ignored.includes(name) ); if (!returnTypes) { return fnSupportedByCommand.map(getAutocompleteFunctionDefinition); @@ -94,6 +112,17 @@ export const buildFieldsDefinitions = (fields: string[]): AutocompleteCommandDef sortText: 'D', })); +export const buildVariablesDefinitions = (variables: string[]): AutocompleteCommandDefinition[] => + variables.map((label) => ({ + label, + insertText: /[^a-zA-Z\d]/.test(label) ? `\`${label}\`` : label, + kind: 4, + detail: i18n.translate('monaco.esql.autocomplete.variableDefinition', { + defaultMessage: `Variable specified by the user within the ES|QL query`, + }), + sortText: 'D', + })); + export const buildSourcesDefinitions = (sources: string[]): AutocompleteCommandDefinition[] => sources.map((label) => ({ label, @@ -124,12 +153,12 @@ export const buildConstantsDefinitions = ( export const buildNewVarDefinition = (label: string): AutocompleteCommandDefinition => { return { label, - insertText: label, + insertText: `${label} =`, kind: 21, detail: i18n.translate('monaco.esql.autocomplete.newVarDoc', { defaultMessage: 'Define a new variable', }), - sortText: 'A', + sortText: '1', }; }; @@ -167,6 +196,21 @@ export const buildMatchingFieldsDefinition = ( sortText: 'D', })); +export const buildOptionDefinition = (option: CommandOptionsDefinition) => { + const completeItem: AutocompleteCommandDefinition = { + label: option.name, + insertText: option.name, + kind: 21, + detail: option.description, + sortText: 'D', + }; + if (option.wrapped) { + completeItem.insertText = `${option.wrapped[0]}${option.name} $0 ${option.wrapped[1]}`; + completeItem.insertTextRules = 4; // monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; + } + return completeItem; +}; + export const buildNoPoliciesAvailableDefinition = (): AutocompleteCommandDefinition => ({ label: i18n.translate('monaco.esql.autocomplete.noPoliciesLabel', { defaultMessage: 'No available policy', @@ -190,7 +234,7 @@ function getUnitDuration(unit: number = 1) { const result = /s$/.test(name); return unit > 1 ? result : !result; }); - return filteredTimeLiteral.map(({ name }) => name); + return filteredTimeLiteral.map(({ name }) => `${unit} ${name}`); } export function getCompatibleLiterals(commandName: string, types: string[], names?: string[]) { @@ -201,11 +245,14 @@ export function getCompatibleLiterals(commandName: string, types: string[], name } if (types.includes('time_literal')) { // filter plural for now and suggest only unit + singular - suggestions.push(...buildConstantsDefinitions(getUnitDuration(1))); // i.e. 1 year } + // this is a special type built from the suggestion system, not inherited from the AST + if (types.includes('time_literal_unit')) { + suggestions.push(...buildConstantsDefinitions(timeLiterals.map(({ name }) => name))); // i.e. year, month, ... + } if (types.includes('chrono_literal')) { - suggestions.push(...buildConstantsDefinitions(chronoLiterals.map(({ name }) => name))); // i.e. EPOC_DAY + suggestions.push(...buildConstantsDefinitions(chronoLiterals.map(({ name }) => name))); // i.e. EPOC_DAY, ... } if (types.includes('string')) { if (names) { diff --git a/packages/kbn-monaco/src/esql/lib/ast/definitions/builtin.ts b/packages/kbn-monaco/src/esql/lib/ast/definitions/builtin.ts index e64303b64ce03..eed2fcc5d65b4 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/builtin.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/builtin.ts @@ -16,6 +16,7 @@ function createMathDefinition( warning?: FunctionDefinition['warning'] ) { return { + builtin: true, name, description, supportedCommands: ['eval', 'where', 'row'], @@ -52,6 +53,7 @@ function createComparisonDefinition( warning?: FunctionDefinition['warning'] ) { return { + builtin: true, name, description, supportedCommands: ['eval', 'where', 'row'], @@ -197,6 +199,8 @@ export const builtinFunctions: FunctionDefinition[] = [ }, { name: 'not_rlike', description: '' }, ].map(({ name, description }) => ({ + builtin: true, + ignoreAsSuggestion: /not/.test(name), name, description, supportedCommands: ['eval', 'where', 'row'], @@ -220,6 +224,8 @@ export const builtinFunctions: FunctionDefinition[] = [ }, { name: 'not_in', description: '' }, ].map(({ name, description }) => ({ + builtin: true, + ignoreAsSuggestion: /not/.test(name), name, description, supportedCommands: ['eval', 'where', 'row'], @@ -268,6 +274,7 @@ export const builtinFunctions: FunctionDefinition[] = [ }), }, ].map(({ name, description }) => ({ + builtin: true, name, description, supportedCommands: ['eval', 'where', 'row'], @@ -282,6 +289,7 @@ export const builtinFunctions: FunctionDefinition[] = [ ], })), { + builtin: true, name: 'not', description: i18n.translate('monaco.esql.definition.notDoc', { defaultMessage: 'Not', @@ -295,6 +303,7 @@ export const builtinFunctions: FunctionDefinition[] = [ ], }, { + builtin: true, name: '=', description: i18n.translate('monaco.esql.definition.assignDoc', { defaultMessage: 'Assign (=)', diff --git a/packages/kbn-monaco/src/esql/lib/ast/definitions/commands.ts b/packages/kbn-monaco/src/esql/lib/ast/definitions/commands.ts index 1316a00e53c0a..3f11962e6a7d7 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/commands.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/commands.ts @@ -40,7 +40,7 @@ export const commandDefinitions: CommandDefinition[] = [ defaultMessage: 'Retrieves data from one or more datasets. A dataset is a collection of data that you want to search. The only supported dataset is an index. In a query or subquery, you must use the from command first and it does not need a leading pipe. For example, to retrieve data from an index:', }), - examples: ['from logs', 'from logs-*', 'from logs_*, events-*', 'from from remote*:logs*'], + examples: ['from logs', 'from logs-*', 'from logs_*, events-*', 'from remote*:logs*'], options: [metadataOption], signature: { multipleParams: true, @@ -97,8 +97,8 @@ export const commandDefinitions: CommandDefinition[] = [ }), examples: ['… | rename old as new', '… | rename old as new, a as b'], signature: { - multipleParams: false, - params: [{ name: 'renameClause', type: 'any' }], + multipleParams: true, + params: [{ name: 'renameClause', type: 'column' }], }, options: [asOption], }, @@ -111,7 +111,7 @@ export const commandDefinitions: CommandDefinition[] = [ examples: ['… | limit 100', '… | limit 0'], signature: { multipleParams: false, - params: [{ name: 'size', type: 'number' }], + params: [{ name: 'size', type: 'number', literalOnly: true }], }, options: [], }, @@ -213,7 +213,7 @@ export const commandDefinitions: CommandDefinition[] = [ multipleParams: false, params: [ { name: 'column', type: 'column', innerType: 'string' }, - { name: 'pattern', type: 'string' }, + { name: 'pattern', type: 'string', literalOnly: true }, ], }, }, @@ -229,7 +229,7 @@ export const commandDefinitions: CommandDefinition[] = [ multipleParams: false, params: [ { name: 'column', type: 'column', innerType: 'string' }, - { name: 'pattern', type: 'string' }, + { name: 'pattern', type: 'string', literalOnly: true }, ], }, }, diff --git a/packages/kbn-monaco/src/esql/lib/ast/definitions/literals.ts b/packages/kbn-monaco/src/esql/lib/ast/definitions/literals.ts index 071b94b2fe834..3cc61683372a7 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/literals.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/literals.ts @@ -10,18 +10,18 @@ import { i18n } from '@kbn/i18n'; import type { Literals } from './types'; export const timeLiterals: Literals[] = [ - { - name: 'years', - description: i18n.translate('monaco.esql.definitions.dateDurationDefinition.years', { - defaultMessage: 'Years (Plural)', - }), - }, { name: 'year', description: i18n.translate('monaco.esql.definitions.dateDurationDefinition.year', { defaultMessage: 'Year', }), }, + { + name: 'years', + description: i18n.translate('monaco.esql.definitions.dateDurationDefinition.years', { + defaultMessage: 'Years (Plural)', + }), + }, { name: 'month', description: i18n.translate('monaco.esql.definitions.dateDurationDefinition.month', { diff --git a/packages/kbn-monaco/src/esql/lib/ast/definitions/options.ts b/packages/kbn-monaco/src/esql/lib/ast/definitions/options.ts index 8916a850653ff..465d6d25dc32e 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/options.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/options.ts @@ -56,7 +56,7 @@ export const onOption: CommandOptionsDefinition = { multipleParams: false, params: [{ name: 'matchingColumn', type: 'column' }], }, - optional: false, + optional: true, }; export const withOption: CommandOptionsDefinition = { diff --git a/packages/kbn-monaco/src/esql/lib/ast/definitions/types.ts b/packages/kbn-monaco/src/esql/lib/ast/definitions/types.ts index c1a0f047d4e32..f6241e63d3d51 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/definitions/types.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/definitions/types.ts @@ -9,6 +9,8 @@ import type { ESQLCommand, ESQLCommandOption, ESQLMessage, ESQLSingleAstItem } from '../types'; export interface FunctionDefinition { + builtin?: boolean; + ignoreAsSuggestion?: boolean; name: string; alias?: string[]; description: string; @@ -42,6 +44,7 @@ export interface CommandBaseDefinition { optional?: boolean; innerType?: string; values?: string[]; + literalOnly?: boolean; wildcards?: boolean; }>; }; diff --git a/packages/kbn-monaco/src/esql/lib/ast/hover/index.ts b/packages/kbn-monaco/src/esql/lib/ast/hover/index.ts new file mode 100644 index 0000000000000..cd91be8c66a3c --- /dev/null +++ b/packages/kbn-monaco/src/esql/lib/ast/hover/index.ts @@ -0,0 +1,43 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { monaco } from '../../../../monaco_imports'; +import { getFunctionSignatures } from '../definitions/helpers'; +import { getAstContext } from '../shared/context'; +import { monacoPositionToOffset, getFunctionDefinition } from '../shared/helpers'; +import type { AstProviderFn } from '../types'; + +export async function getHoverItem( + model: monaco.editor.ITextModel, + position: monaco.Position, + token: monaco.CancellationToken, + astProvider: AstProviderFn +) { + const innerText = model.getValue(); + const offset = monacoPositionToOffset(innerText, position); + + const { ast } = await astProvider(innerText); + const astContext = getAstContext(innerText, ast, offset); + + if (astContext.type !== 'function') { + return { contents: [] }; + } + + const fnDefinition = getFunctionDefinition(astContext.node.name); + + if (!fnDefinition) { + return { contents: [] }; + } + + return { + contents: [ + { value: getFunctionSignatures(fnDefinition)[0].declaration }, + { value: fnDefinition.description }, + ], + }; +} diff --git a/packages/kbn-monaco/src/esql/lib/ast/shared/constants.ts b/packages/kbn-monaco/src/esql/lib/ast/shared/constants.ts new file mode 100644 index 0000000000000..618928f36bcfb --- /dev/null +++ b/packages/kbn-monaco/src/esql/lib/ast/shared/constants.ts @@ -0,0 +1,9 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export const EDITOR_MARKER = 'marker_esql_editor'; diff --git a/packages/kbn-monaco/src/esql/lib/ast/shared/context.ts b/packages/kbn-monaco/src/esql/lib/ast/shared/context.ts new file mode 100644 index 0000000000000..3a51038ec4e92 --- /dev/null +++ b/packages/kbn-monaco/src/esql/lib/ast/shared/context.ts @@ -0,0 +1,158 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { + ESQLAstItem, + ESQLSingleAstItem, + ESQLAst, + ESQLFunction, + ESQLCommand, + ESQLCommandOption, +} from '../types'; +import { EDITOR_MARKER } from './constants'; +import { + isOptionItem, + isColumnItem, + getLastCharFromTrimmed, + getFunctionDefinition, +} from './helpers'; + +function findNode(nodes: ESQLAstItem[], offset: number): ESQLSingleAstItem | undefined { + for (const node of nodes) { + if (Array.isArray(node)) { + const ret = findNode(node, offset); + if (ret) { + return ret; + } + } else { + if (node.location.min <= offset && node.location.max >= offset) { + if ('args' in node) { + const ret = findNode(node.args, offset); + // if the found node is the marker, then return its parent + if (ret?.text === EDITOR_MARKER) { + return node; + } + if (ret) { + return ret; + } + } + return node; + } + } + } +} + +function findCommand(ast: ESQLAst, offset: number) { + const commandIndex = ast.findIndex( + ({ location }) => location.min <= offset && location.max >= offset + ); + return ast[commandIndex] || ast[ast.length - 1]; +} + +function findOption(nodes: ESQLAstItem[], offset: number): ESQLCommandOption | undefined { + // this is a similar logic to the findNode, but it check if the command is in root or option scope + for (const node of nodes) { + if (!Array.isArray(node) && isOptionItem(node)) { + if ( + (node.location.min <= offset && node.location.max >= offset) || + (nodes[nodes.length - 1] === node && node.location.max < offset) + ) { + return node; + } + } + } +} + +function isMarkerNode(node: ESQLSingleAstItem | undefined): boolean { + return Boolean(node && isColumnItem(node) && node.name === EDITOR_MARKER); +} + +function cleanMarkerNode(node: ESQLSingleAstItem | undefined): ESQLSingleAstItem | undefined { + return isMarkerNode(node) ? undefined : node; +} + +function isNotMarkerNodeOrArray(arg: ESQLAstItem) { + return Array.isArray(arg) || !isMarkerNode(arg); +} + +function mapToNonMarkerNode(arg: ESQLAstItem): ESQLAstItem { + return Array.isArray(arg) ? arg.filter(isNotMarkerNodeOrArray).map(mapToNonMarkerNode) : arg; +} + +export function removeMarkerArgFromArgsList( + node: T | undefined +) { + if (!node) { + return; + } + if (node.type === 'command' || node.type === 'option' || node.type === 'function') { + return { + ...node, + args: node.args.filter(isNotMarkerNodeOrArray).map(mapToNonMarkerNode), + }; + } + return node; +} + +function findAstPosition(ast: ESQLAst, offset: number) { + const command = findCommand(ast, offset); + if (!command) { + return { command: undefined, node: undefined, option: undefined }; + } + return { + command: removeMarkerArgFromArgsList(command)!, + option: removeMarkerArgFromArgsList(findOption(command.args, offset)), + node: removeMarkerArgFromArgsList(cleanMarkerNode(findNode(command.args, offset))), + }; +} + +function isNotEnrichClauseAssigment(node: ESQLFunction, command: ESQLCommand) { + return node.name !== '=' && command.name !== 'enrich'; +} +function isBuiltinFunction(node: ESQLFunction) { + return Boolean(getFunctionDefinition(node.name)?.builtin); +} + +export function getAstContext(innerText: string, ast: ESQLAst, offset: number) { + const { command, option, node } = findAstPosition(ast, offset); + if (node) { + if (node.type === 'function') { + if (['in', 'not_in'].includes(node.name)) { + // command ... a in ( ) + return { type: 'list' as const, command, node, option }; + } + if (isNotEnrichClauseAssigment(node, command) && !isBuiltinFunction(node)) { + // command ... fn( ) + return { type: 'function' as const, command, node, option }; + } + } + if (node.type === 'option' || option) { + // command ... by + return { type: 'option' as const, command, node, option }; + } + } + + if (!command || (innerText.length <= offset && getLastCharFromTrimmed(innerText) === '|')) { + // // ... | + return { type: 'newCommand' as const, command: undefined, node, option }; + } + + if (command && command.args.length) { + if (option) { + return { type: 'option' as const, command, node, option }; + } + } + + // command a ... OR command a = ... + return { + type: 'expression' as const, + command, + option, + node, + }; +} diff --git a/packages/kbn-monaco/src/esql/lib/ast/shared/helpers.ts b/packages/kbn-monaco/src/esql/lib/ast/shared/helpers.ts index d799d934f4dfc..337dc0ce8023f 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/shared/helpers.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/shared/helpers.ts @@ -31,6 +31,7 @@ import { ESQLTimeInterval, } from '../types'; import { ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types'; +import { removeMarkerArgFromArgsList } from './context'; export function isFunctionItem(arg: ESQLAstItem): arg is ESQLFunction { return arg && !Array.isArray(arg) && arg.type === 'function'; @@ -49,17 +50,22 @@ export function isColumnItem(arg: ESQLAstItem): arg is ESQLColumn { } export function isLiteralItem(arg: ESQLAstItem): arg is ESQLLiteral { - return !Array.isArray(arg) && arg.type === 'literal'; + return arg && !Array.isArray(arg) && arg.type === 'literal'; } export function isTimeIntervalItem(arg: ESQLAstItem): arg is ESQLTimeInterval { - return !Array.isArray(arg) && arg.type === 'timeInterval'; + return arg && !Array.isArray(arg) && arg.type === 'timeInterval'; } export function isAssignment(arg: ESQLAstItem): arg is ESQLFunction { return isFunctionItem(arg) && arg.name === '='; } +export function isAssignmentComplete(node: ESQLFunction | undefined) { + const assignExpression = removeMarkerArgFromArgsList(node)?.args?.[1]; + return Boolean(assignExpression && Array.isArray(assignExpression) && assignExpression.length); +} + export function isExpression(arg: ESQLAstItem): arg is ESQLFunction { return isFunctionItem(arg) && arg.name !== '='; } @@ -406,13 +412,24 @@ export function hasWildcard(name: string) { } export function columnExists( - column: string, + column: ESQLColumn, { fields, variables }: Pick ) { - if (fields.has(column) || variables.has(column)) { - return true; + if (fields.has(column.name) || variables.has(column.name)) { + return { hit: true, nameHit: column.name }; + } + if (column.quoted) { + const trimmedName = column.name.replace(/\s/g, ''); + if (variables.has(trimmedName)) { + return { hit: true, nameHit: trimmedName }; + } } - return Boolean(fuzzySearch(column, fields.keys()) || fuzzySearch(column, variables.keys())); + if ( + Boolean(fuzzySearch(column.name, fields.keys()) || fuzzySearch(column.name, variables.keys())) + ) { + return { hit: true, nameHit: column.name }; + } + return { hit: false }; } export function sourceExists(index: string, sources: Set) { @@ -421,3 +438,11 @@ export function sourceExists(index: string, sources: Set) { } return Boolean(fuzzySearch(index, sources.keys())); } + +export function getLastCharFromTrimmed(text: string) { + return text[text.trimEnd().length - 1]; +} + +export function isRestartingExpression(text: string) { + return getLastCharFromTrimmed(text) === ','; +} diff --git a/packages/kbn-monaco/src/esql/lib/ast/shared/variables.ts b/packages/kbn-monaco/src/esql/lib/ast/shared/variables.ts index 46aecd745baff..80269458e1c2d 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/shared/variables.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/shared/variables.ts @@ -8,6 +8,7 @@ import type { ESQLColumn, ESQLAstItem, ESQLCommand, ESQLCommandOption } from '../types'; import type { ESQLVariable, ESQLRealField } from '../validation/types'; +import { EDITOR_MARKER } from './constants'; import { isColumnItem, isAssignment, @@ -95,6 +96,22 @@ function getAssignRightHandSideType(item: ESQLAstItem, fields: Map +) { + const anyVariables = collectVariables(commands, fieldsMap); + const currentCommandVariables = collectVariables([currentCommand], fieldsMap); + const resultVariables = new Map(); + anyVariables.forEach((value, key) => { + if (!currentCommandVariables.has(key)) { + resultVariables.set(key, value); + } + }); + return resultVariables; +} + export function collectVariables( commands: ESQLCommand[], fields: Map @@ -115,13 +132,15 @@ export function collectVariables( } const expressionOperations = command.args.filter(isExpression); for (const expressionOperation of expressionOperations) { - // just save the entire expression as variable string - const expressionType = 'number'; - addToVariableOccurrencies(variables, { - name: expressionOperation.text, - type: expressionType, - location: expressionOperation.location, - }); + if (!expressionOperation.text.includes(EDITOR_MARKER)) { + // just save the entire expression as variable string + const expressionType = 'number'; + addToVariableOccurrencies(variables, { + name: expressionOperation.text, + type: expressionType, + location: expressionOperation.location, + }); + } } } if (command.name === 'enrich') { diff --git a/packages/kbn-monaco/src/esql/lib/ast/signature/index.ts b/packages/kbn-monaco/src/esql/lib/ast/signature/index.ts new file mode 100644 index 0000000000000..a3202591a9c7a --- /dev/null +++ b/packages/kbn-monaco/src/esql/lib/ast/signature/index.ts @@ -0,0 +1,22 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { monaco } from '../../../../monaco_imports'; +import type { AstProviderFn } from '../types'; + +export function getSignatureHelp( + model: monaco.editor.ITextModel, + position: monaco.Position, + context: monaco.languages.SignatureHelpContext, + astProvider: AstProviderFn +): monaco.languages.SignatureHelpResult { + return { + value: { signatures: [], activeParameter: 0, activeSignature: 0 }, + dispose: () => {}, + }; +} diff --git a/packages/kbn-monaco/src/esql/lib/ast/types.ts b/packages/kbn-monaco/src/esql/lib/ast/types.ts index cdb5c73fec2ab..d2d6b601fbb0f 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/types.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/types.ts @@ -78,3 +78,5 @@ export interface ESQLMessage { text: string; location: ESQLLocation; } + +export type AstProviderFn = (text: string | undefined) => Promise<{ ast: ESQLAst }>; diff --git a/packages/kbn-monaco/src/esql/lib/ast/validation/validation.test.ts b/packages/kbn-monaco/src/esql/lib/ast/validation/validation.test.ts index 1554b7d2cb288..38535c1455769 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/validation/validation.test.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/validation/validation.test.ts @@ -163,24 +163,53 @@ describe('validation logic', () => { return { ...parseListener.getAst(), syntaxErrors: errorListener.getErrors() }; }; - function testErrorsAndWarnings( + function testErrorsAndWarningsFn( statement: string, expectedErrors: string[] = [], - expectedWarnings: string[] = [] + expectedWarnings: string[] = [], + { only, skip }: { only?: boolean; skip?: boolean } = {} ) { - it(`${statement} => ${expectedErrors.length} errors, ${expectedWarnings.length} warnings`, async () => { - const { ast, syntaxErrors } = getAstAndErrors(statement); - const callbackMocks = getCallbackMocks(); - const { warnings, errors } = await validateAst(ast, callbackMocks); - const finalErrors = errors.concat( - // squash syntax errors - syntaxErrors.map(({ message }) => ({ text: message })) as ESQLMessage[] - ); - expect(finalErrors.map((e) => e.text)).toEqual(expectedErrors); - expect(warnings.map((w) => w.text)).toEqual(expectedWarnings); - }); + const testFn = only ? it.only : skip ? it.skip : it; + testFn( + `${statement} => ${expectedErrors.length} errors, ${expectedWarnings.length} warnings`, + async () => { + const { ast, syntaxErrors } = getAstAndErrors(statement); + const callbackMocks = getCallbackMocks(); + const { warnings, errors } = await validateAst(ast, callbackMocks); + const finalErrors = errors.concat( + // squash syntax errors + syntaxErrors.map(({ message }) => ({ text: message })) as ESQLMessage[] + ); + expect(finalErrors.map((e) => e.text)).toEqual(expectedErrors); + expect(warnings.map((w) => w.text)).toEqual(expectedWarnings); + } + ); } + type TestArgs = [string, string[], string[]?]; + + // Make only and skip work with our custom wrapper + const testErrorsAndWarnings = Object.assign(testErrorsAndWarningsFn, { + skip: (...args: TestArgs) => { + const warningArgs = [[]].slice(args.length - 2); + return testErrorsAndWarningsFn( + ...((args.length > 1 ? [...args, ...warningArgs] : args) as TestArgs), + { + skip: true, + } + ); + }, + only: (...args: TestArgs) => { + const warningArgs = [[]].slice(args.length - 2); + return testErrorsAndWarningsFn( + ...((args.length > 1 ? [...args, ...warningArgs] : args) as TestArgs), + { + only: true, + } + ); + }, + }); + describe('ESQL query should start with a source command', () => { ['eval', 'stats', 'rename', 'limit', 'keep', 'drop', 'mv_expand', 'dissect', 'grok'].map( (command) => @@ -550,7 +579,13 @@ describe('validation logic', () => { testErrorsAndWarnings('from a | rename', [ "SyntaxError: missing {SRC_UNQUOTED_IDENTIFIER, SRC_QUOTED_IDENTIFIER} at ''", ]); - testErrorsAndWarnings('from a | rename a', ['SyntaxError: expected {AS} but found ""']); + testErrorsAndWarnings('from a | rename stringField', [ + 'SyntaxError: expected {AS} but found ""', + ]); + testErrorsAndWarnings('from a | rename a', [ + 'Unknown column [a]', + 'SyntaxError: expected {AS} but found ""', + ]); testErrorsAndWarnings('from a | rename stringField as', [ "SyntaxError: missing {SRC_UNQUOTED_IDENTIFIER, SRC_QUOTED_IDENTIFIER} at ''", ]); @@ -567,6 +602,10 @@ describe('validation logic', () => { 'Unknown column [a]', ]); testErrorsAndWarnings('from a | eval numberField + 1 | rename `numberField + 1` as a', []); + testErrorsAndWarnings( + 'from a | stats avg(numberField) | rename `avg(numberField)` as avg0', + [] + ); testErrorsAndWarnings('from a | eval numberField + 1 | rename `numberField + 1` as ', [ "SyntaxError: missing {SRC_UNQUOTED_IDENTIFIER, SRC_QUOTED_IDENTIFIER} at ''", ]); @@ -950,6 +989,7 @@ describe('validation logic', () => { ]); testErrorsAndWarnings('from a | eval avg(numberField)', ['Eval does not support function avg']); + testErrorsAndWarnings('from a | stats avg(numberField) | eval `avg(numberField)` + 1', []); describe('date math', () => { testErrorsAndWarnings('from a | eval 1 anno', [ @@ -1037,6 +1077,11 @@ describe('validation logic', () => { [] ); + testErrorsAndWarnings( + 'from a | stats avg(numberField), percentile(numberField, 50) BY ipField', + [] + ); + testErrorsAndWarnings('from a | stats numberField + 1', ['Stats does not support function +']); testErrorsAndWarnings('from a | stats numberField + 1 by ipField', [ diff --git a/packages/kbn-monaco/src/esql/lib/ast/validation/validation.ts b/packages/kbn-monaco/src/esql/lib/ast/validation/validation.ts index 67b1722cabf43..0a7e7a10bbf8a 100644 --- a/packages/kbn-monaco/src/esql/lib/ast/validation/validation.ts +++ b/packages/kbn-monaco/src/esql/lib/ast/validation/validation.ts @@ -164,8 +164,8 @@ function validateFunctionColumnArg( ) { const messages: ESQLMessage[] = []; if (isColumnItem(actualArg) && actualArg.name) { - const columnHit = getColumnHit(actualArg.name, references); - if (!columnHit) { + const { hit: columnCheck, nameHit } = columnExists(actualArg, references); + if (!columnCheck) { messages.push( getMessageFromId({ messageId: 'unknownColumn', @@ -176,8 +176,10 @@ function validateFunctionColumnArg( }) ); } else { + // guaranteed by the check above + const columnHit = getColumnHit(nameHit!, references); // check the type of the column hit - const typeHit = columnHit.type; + const typeHit = columnHit!.type; if (!isEqualType(actualArg, argDef, references, parentCommand)) { messages.push( getMessageFromId({ @@ -521,8 +523,8 @@ function validateColumnForCommand( ); } } else { - const columnCheck = columnExists(column.name, references); - if (columnCheck) { + const { hit: columnCheck, nameHit } = columnExists(column, references); + if (columnCheck && nameHit) { const commandDef = getCommandDefinition(commandName); const columnParamsWithInnerTypes = commandDef.signature.params.filter( ({ type, innerType }) => type === 'column' && innerType @@ -530,7 +532,7 @@ function validateColumnForCommand( if (columnParamsWithInnerTypes.length) { // this should be guaranteed by the columnCheck above - const columnRef = getColumnHit(column.name, references)!; + const columnRef = getColumnHit(nameHit, references)!; if ( columnParamsWithInnerTypes.every(({ innerType }) => { return innerType !== columnRef.type; @@ -546,7 +548,7 @@ function validateColumnForCommand( type: supportedTypes.join(', '), typeCount: supportedTypes.length, givenType: columnRef.type, - column: column.name, + column: nameHit, }, locations: column.location, }) @@ -554,7 +556,7 @@ function validateColumnForCommand( } } if ( - hasWildcard(column.name) && + hasWildcard(nameHit) && !commandDef.signature.params.some(({ type, wildcards }) => type === 'column' && wildcards) ) { messages.push( @@ -562,7 +564,7 @@ function validateColumnForCommand( messageId: 'wildcardNotSupportedForCommand', values: { command: commandName, - value: column.name, + value: nameHit, }, locations: column.location, }) diff --git a/packages/kbn-monaco/src/esql/lib/monaco/esql_ast_provider.ts b/packages/kbn-monaco/src/esql/lib/monaco/esql_ast_provider.ts index 378f0ceee3a54..7ac5857fc8a33 100644 --- a/packages/kbn-monaco/src/esql/lib/monaco/esql_ast_provider.ts +++ b/packages/kbn-monaco/src/esql/lib/monaco/esql_ast_provider.ts @@ -9,7 +9,9 @@ import type { ESQLCallbacks } from '../../../..'; import type { monaco } from '../../../monaco_imports'; import type { ESQLWorker } from '../../worker/esql_worker'; -import { getHoverItem, getSignatureHelp, suggest } from '../ast/autocomplete/autocomplete'; +import { suggest } from '../ast/autocomplete/autocomplete'; +import { getHoverItem } from '../ast/hover'; +import { getSignatureHelp } from '../ast/signature'; import { validateAst } from '../ast/validation/validation'; export class ESQLAstAdapter { diff --git a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx index 0616bcf35e571..48642d0a584a1 100644 --- a/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx +++ b/packages/kbn-text-based-editor/src/text_based_languages_editor.tsx @@ -309,7 +309,13 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ return await getIndicesForAutocomplete(dataViews); }, getFieldsFor: async (options: { sourcesOnly?: boolean } | { customQuery?: string } = {}) => { - const pipes = editorModel.current?.getValue().split('|'); + // we're caching here with useMemo + // and when the editor becomes multi-line it cann be disposed and the ref we had throws + // with this method we can get always the fresh model to use + const model = monaco.editor + .getModels() + .find((m) => !m.isDisposed() && m.isAttachedToEditor()); + const pipes = model?.getValue().split('|'); pipes?.pop(); let validContent = pipes?.join('|'); if ('customQuery' in options && options.customQuery) { @@ -346,7 +352,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({ const queryValidation = useCallback( async ({ active }: { active: boolean }) => { - if (!editorModel.current || language !== 'esql') return; + if (!editorModel.current || language !== 'esql' || editorModel.current.isDisposed()) return; monaco.editor.setModelMarkers(editorModel.current, 'Unified search', []); const { warnings: parserWarnings, errors: parserErrors } = await ESQLLang.validate( editorModel.current, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 9c0f8b699540a..4db1d3c456ff1 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -4953,129 +4953,6 @@ "management.settings.offLabel": "Désactivé", "management.settings.onLabel": "Activé", "management.settings.resetToDefaultLinkText": "Réinitialiser à la valeur par défaut", - "monaco.esql.autocomplete.matchingFieldDefinition": "Utiliser pour correspondance avec {matchingField} de la politique", - "monaco.esql.autocomplete.policyDefinition": "Politique définie selon {count, plural, one {index} many {index système non migrés} other {des index}} : {indices}", - "monaco.esql.autocomplete.absDoc": "Renvoie la valeur absolue.", - "monaco.esql.autocomplete.acosDoc": "Fonction trigonométrique cosinus inverse", - "monaco.esql.autocomplete.addDoc": "Ajouter (+)", - "monaco.esql.autocomplete.andDoc": "et", - "monaco.esql.autocomplete.ascDoc": "Ordre croissant", - "monaco.esql.autocomplete.asDoc": "En tant que", - "monaco.esql.autocomplete.asinDoc": "Fonction trigonométrique sinus inverse", - "monaco.esql.autocomplete.assignDoc": "Affecter (=)", - "monaco.esql.autocomplete.atan2Doc": "L'angle entre l'axe positif des x et le rayon allant de l'origine au point (x , y) dans le plan cartésien", - "monaco.esql.autocomplete.atanDoc": "Fonction trigonométrique tangente inverse", - "monaco.esql.autocomplete.autoBucketDoc": "Groupement automatique des dates en fonction d'une plage et d'un compartiment cible donnés.", - "monaco.esql.autocomplete.avgDoc": "Renvoie la moyenne des valeurs dans un champ", - "monaco.esql.autocomplete.byDoc": "Par", - "monaco.esql.autocomplete.caseDoc": "Accepte les paires de conditions et de valeurs. La fonction renvoie la valeur correspondant à la première condition évaluée à \"True\" (Vraie). Si le nombre d'arguments est impair, le dernier argument est la valeur par défaut qui est renvoyée si aucune condition ne correspond.", - "monaco.esql.autocomplete.cidrMatchDoc": "La fonction utilise un premier paramètre de type adresse IP, puis un ou plusieurs paramètres évalués en fonction d'une spécification CIDR.", - "monaco.esql.autocomplete.closeBracketDoc": "Parenthèse fermante )", - "monaco.esql.autocomplete.coalesceDoc": "Renvoie la première valeur non nulle.", - "monaco.esql.autocomplete.concatDoc": "Concatène deux ou plusieurs chaînes.", - "monaco.esql.autocomplete.constantDefinition": "Variable définie par l'utilisateur", - "monaco.esql.autocomplete.cosDoc": "Fonction trigonométrique cosinus", - "monaco.esql.autocomplete.coshDoc": "Fonction hyperbolique cosinus", - "monaco.esql.autocomplete.countDistinctDoc": "Renvoie le décompte des valeurs distinctes dans un champ.", - "monaco.esql.autocomplete.countDoc": "Renvoie le décompte des valeurs dans un champ.", - "monaco.esql.autocomplete.createNewPolicy": "Cliquez pour créer", - "monaco.esql.autocomplete.dateDurationDefinition.day": "Jour", - "monaco.esql.autocomplete.dateDurationDefinition.days": "Jours (pluriel)", - "monaco.esql.autocomplete.dateDurationDefinition.hour": "Heure", - "monaco.esql.autocomplete.dateDurationDefinition.hours": "Heures (pluriel)", - "monaco.esql.autocomplete.dateDurationDefinition.millisecond": "Milliseconde", - "monaco.esql.autocomplete.dateDurationDefinition.milliseconds": "Millisecondes (pluriel)", - "monaco.esql.autocomplete.dateDurationDefinition.minute": "Minute", - "monaco.esql.autocomplete.dateDurationDefinition.minutes": "Minutes (pluriel)", - "monaco.esql.autocomplete.dateDurationDefinition.month": "Mois", - "monaco.esql.autocomplete.dateDurationDefinition.months": "Mois (pluriel)", - "monaco.esql.autocomplete.dateDurationDefinition.second": "Seconde", - "monaco.esql.autocomplete.dateDurationDefinition.seconds": "Secondes (pluriel)", - "monaco.esql.autocomplete.dateDurationDefinition.week": "Semaine", - "monaco.esql.autocomplete.dateDurationDefinition.weeks": "Semaines (pluriel)", - "monaco.esql.autocomplete.dateDurationDefinition.year": "An", - "monaco.esql.autocomplete.dateDurationDefinition.years": "Ans (pluriel)", - "monaco.esql.autocomplete.dateExtractDoc": "Extrait des parties d'une date, telles que l'année, le mois, le jour, l'heure. Les types de champs pris en charge sont ceux fournis par la fonction \"java.time.temporal.ChronoField\"", - "monaco.esql.autocomplete.dateFormatDoc": "Renvoie une représentation sous forme de chaîne d'une date dans le format fourni. Si aucun format n'est indiqué, le format \"yyyy-MM-dd'T'HH:mm:ss.SSSZ\" est utilisé.", - "monaco.esql.autocomplete.dateParseDoc": "Analyser les dates à partir de chaînes.", - "monaco.esql.autocomplete.dateTruncDoc": "Arrondit une date à l'intervalle le plus proche. Les intervalles peuvent être exprimés à l'aide de la syntaxe littérale timespan.", - "monaco.esql.autocomplete.declarationLabel": "Déclaration :", - "monaco.esql.autocomplete.descDoc": "Ordre décroissant", - "monaco.esql.autocomplete.dissectDoc": "Extrait de multiples valeurs de chaîne à partir d'une entrée de chaîne unique, suivant un modèle", - "monaco.esql.autocomplete.divideDoc": "Diviser (/)", - "monaco.esql.autocomplete.dropDoc": "Supprime les colonnes", - "monaco.esql.autocomplete.enrichDoc": "Enrichir le tableau à l'aide d'un autre tableau", - "monaco.esql.autocomplete.equalToDoc": "Égal à", - "monaco.esql.autocomplete.evalDoc": "Calcule une expression et place la valeur résultante dans un champ de résultats de recherche.", - "monaco.esql.autocomplete.examplesLabel": "Exemples :", - "monaco.esql.autocomplete.fieldDefinition": "Champ spécifié par le tableau d'entrée", - "monaco.esql.autocomplete.floorDoc": "Arrondir un nombre à l'entier inférieur.", - "monaco.esql.autocomplete.fromDoc": "Récupère les données d'un ou de plusieurs ensembles de données. Un ensemble de données est une collection de données dans laquelle vous souhaitez effectuer une recherche. Le seul ensemble de données pris en charge est un index. Dans une requête ou une sous-requête, vous devez utiliser d'abord la commande from, et cette dernière ne nécessite pas de barre verticale au début. Par exemple, pour récupérer des données d'un index :", - "monaco.esql.autocomplete.greaterThanDoc": "Supérieur à", - "monaco.esql.autocomplete.greaterThanOrEqualToDoc": "Supérieur ou égal à", - "monaco.esql.autocomplete.greatestDoc": "Renvoie la valeur maximale de plusieurs colonnes.", - "monaco.esql.autocomplete.grokDoc": "Extrait de multiples valeurs de chaîne à partir d'une entrée de chaîne unique, suivant un modèle", - "monaco.esql.autocomplete.inDoc": "Teste si la valeur d'une expression est contenue dans une liste d'autres expressions", - "monaco.esql.autocomplete.isFiniteDoc": "Renvoie un booléen qui indique si son entrée est un nombre fini.", - "monaco.esql.autocomplete.isInfiniteDoc": "Renvoie un booléen qui indique si son entrée est infinie.", - "monaco.esql.autocomplete.keepDoc": "Réarrange les champs dans le tableau d'entrée en appliquant les clauses \"KEEP\" dans les champs", - "monaco.esql.autocomplete.leftDoc": "Renvoyer la sous-chaîne qui extrait la longueur des caractères de la chaîne en partant de la gauche.", - "monaco.esql.autocomplete.lengthDoc": "Renvoie la longueur des caractères d'une chaîne.", - "monaco.esql.autocomplete.lessThanDoc": "Inférieur à", - "monaco.esql.autocomplete.lessThanOrEqualToDoc": "Inférieur ou égal à", - "monaco.esql.autocomplete.likeDoc": "Filtrer les données en fonction des modèles de chaînes", - "monaco.esql.autocomplete.limitDoc": "Renvoie les premiers résultats de recherche, dans l'ordre de recherche, en fonction de la \"limite\" spécifiée.", - "monaco.esql.autocomplete.log10Doc": "Renvoie le log de base 10.", - "monaco.esql.autocomplete.ltrimDoc": "Supprime les espaces blancs au début des chaînes.", - "monaco.esql.autocomplete.maxDoc": "Renvoie la valeur maximale dans un champ.", - "monaco.esql.autocomplete.medianDeviationDoc": "Renvoie la médiane de chaque écart de point de données par rapport à la médiane de l'ensemble de l'échantillon.", - "monaco.esql.autocomplete.medianDoc": "Renvoie le 50centile.", - "monaco.esql.autocomplete.minDoc": "Renvoie la valeur minimale dans un champ.", - "monaco.esql.autocomplete.multiplyDoc": "Multiplier (*)", - "monaco.esql.autocomplete.mvExpandDoc": "Développe des champs comportant des valeurs multiples en indiquant une valeur par ligne et en dupliquant les autres champs", - "monaco.esql.autocomplete.newVarDoc": "Définir une nouvelle variable", - "monaco.esql.autocomplete.noPoliciesLabel": "Pas de stratégie disponible", - "monaco.esql.autocomplete.noPoliciesLabelsFound": "Cliquez pour créer", - "monaco.esql.autocomplete.notEqualToDoc": "Différent de", - "monaco.esql.autocomplete.nowDoc": "Renvoie la date et l'heure actuelles.", - "monaco.esql.autocomplete.onDoc": "Activé", - "monaco.esql.autocomplete.openBracketDoc": "Parenthèse ouvrante (", - "monaco.esql.autocomplete.orDoc": "ou", - "monaco.esql.autocomplete.percentiletDoc": "Renvoie le n-ième centile d'un champ.", - "monaco.esql.autocomplete.pipeDoc": "Barre verticale (|)", - "monaco.esql.autocomplete.powDoc": "Renvoie la valeur d'une base (premier argument) élevée à une puissance (deuxième argument).", - "monaco.esql.autocomplete.renameDoc": "Attribue un nouveau nom à une ancienne colonne", - "monaco.esql.autocomplete.rightDoc": "Renvoyer la sous-chaîne qui extrait la longueur des caractères de la chaîne en partant de la droite.", - "monaco.esql.autocomplete.rlikeDoc": "Filtrer les données en fonction des expressions régulières des chaînes", - "monaco.esql.autocomplete.roundDoc": "Renvoie un nombre arrondi à la décimale, spécifié par la valeur entière la plus proche. La valeur par défaut est arrondie à un entier.", - "monaco.esql.autocomplete.rtrimDoc": "Supprime les espaces blancs à la fin des chaînes.", - "monaco.esql.autocomplete.sinDoc": "Fonction trigonométrique sinus.", - "monaco.esql.autocomplete.sinhDoc": "Fonction hyperbolique sinus.", - "monaco.esql.autocomplete.sortDoc": "Trie tous les résultats en fonction des champs spécifiés. Lorsqu'ils sont en ordre décroissant, les résultats pour lesquels un champ est manquant sont considérés comme la plus petite valeur possible du champ, ou la plus grande valeur possible du champ lorsqu'ils sont en ordre croissant.", - "monaco.esql.autocomplete.sourceDefinition": "Tableau d'entrée", - "monaco.esql.autocomplete.splitDoc": "Divise une chaîne de valeur unique en plusieurs chaînes.", - "monaco.esql.autocomplete.sqrtDoc": "Renvoie la racine carrée d'un nombre. ", - "monaco.esql.autocomplete.startsWithDoc": "Renvoie un booléen qui indique si une chaîne de mot-clés débute par une autre chaîne.", - "monaco.esql.autocomplete.statsDoc": "Calcule les statistiques agrégées, telles que la moyenne, le décompte et la somme, sur l'ensemble des résultats de recherche entrants. Comme pour l'agrégation SQL, si la commande stats est utilisée sans clause BY, une seule ligne est renvoyée, qui est l'agrégation de tout l'ensemble des résultats de recherche entrants. Lorsque vous utilisez une clause BY, une ligne est renvoyée pour chaque valeur distincte dans le champ spécifié dans la clause BY. La commande stats renvoie uniquement les champs dans l'agrégation, et vous pouvez utiliser un large éventail de fonctions statistiques avec la commande stats. Lorsque vous effectuez plusieurs agrégations, séparez chacune d'entre elle par une virgule.", - "monaco.esql.autocomplete.substringDoc": "Renvoie la sous-chaîne d'une chaîne, délimitée en fonction d'une position de départ et d'une longueur optionnelle. Cet exemple renvoie les trois premières lettres de chaque nom de famille.", - "monaco.esql.autocomplete.subtractDoc": "Subtract (-)", - "monaco.esql.autocomplete.sumDoc": "Renvoie la somme des valeurs dans un champ.", - "monaco.esql.autocomplete.tanDoc": "Fonction trigonométrique tangente.", - "monaco.esql.autocomplete.tanhDoc": "Fonction hyperbolique tangente.", - "monaco.esql.autocomplete.toBooleanDoc": "Convertit en booléen.", - "monaco.esql.autocomplete.toDateTimeDoc": "Convertit en date.", - "monaco.esql.autocomplete.toDegreesDoc": "Convertit en degrés", - "monaco.esql.autocomplete.toDoubleDoc": "Convertit en double.", - "monaco.esql.autocomplete.toIntegerDoc": "Convertit en nombre entier.", - "monaco.esql.autocomplete.toIpDoc": "Convertit en ip.", - "monaco.esql.autocomplete.toLongDoc": "Convertit en long.", - "monaco.esql.autocomplete.toRadiansDoc": "Convertit en radians", - "monaco.esql.autocomplete.toStringDoc": "Convertit en chaîne.", - "monaco.esql.autocomplete.toUnsignedLongDoc": "Convertit en long non signé.", - "monaco.esql.autocomplete.toVersionDoc": "Convertit en version.", - "monaco.esql.autocomplete.trimDoc": "Supprime les espaces de début et de fin d'une chaîne.", - "monaco.esql.autocomplete.whereDoc": "Utilise \"predicate-expressions\" pour filtrer les résultats de recherche. Une expression predicate, lorsqu'elle est évaluée, renvoie TRUE ou FALSE. La commande where renvoie uniquement les résultats qui donnent la valeur TRUE. Par exemple, pour filtrer les résultats pour une valeur de champ spécifique", - "monaco.esql.autocomplete.withDoc": "Avec", "monaco.painlessLanguage.autocomplete.docKeywordDescription": "Accéder à une valeur de champ dans un script au moyen de la syntaxe doc['field_name']", "monaco.painlessLanguage.autocomplete.emitKeywordDescription": "Émettre une valeur sans rien renvoyer", "monaco.painlessLanguage.autocomplete.fieldValueDescription": "Récupérer la valeur du champ \"{fieldName}\"", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 564e643b52a7d..daf919ffab1c1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4968,129 +4968,6 @@ "management.settings.offLabel": "オフ", "management.settings.onLabel": "オン", "management.settings.resetToDefaultLinkText": "デフォルトにリセット", - "monaco.esql.autocomplete.matchingFieldDefinition": "ポリシーで{matchingField}で照合するために使用", - "monaco.esql.autocomplete.policyDefinition": "{count, plural, other {インデックス}}で定義されたポリシー:{indices}", - "monaco.esql.autocomplete.absDoc": "絶対値を返します。", - "monaco.esql.autocomplete.acosDoc": "逆余弦三角関数", - "monaco.esql.autocomplete.addDoc": "加算(+)", - "monaco.esql.autocomplete.andDoc": "AND", - "monaco.esql.autocomplete.ascDoc": "昇順", - "monaco.esql.autocomplete.asDoc": "として", - "monaco.esql.autocomplete.asinDoc": "逆正弦三角関数", - "monaco.esql.autocomplete.assignDoc": "割り当て(=)", - "monaco.esql.autocomplete.atan2Doc": "直交平面上の原点から点(x , y)に向かう光線と正のx軸のなす角", - "monaco.esql.autocomplete.atanDoc": "逆正接三角関数", - "monaco.esql.autocomplete.autoBucketDoc": "指定された範囲とバケット目標に基づいて、日付を自動的にバケット化します。", - "monaco.esql.autocomplete.avgDoc": "フィールドの値の平均を返します", - "monaco.esql.autocomplete.byDoc": "グループ基準", - "monaco.esql.autocomplete.caseDoc": "条件と値のペアを指定できます。この関数は、最初にtrueと評価された条件に属する値を返します。引数の数が奇数の場合、最後の引数は条件に一致しない場合に返されるデフォルト値になります。", - "monaco.esql.autocomplete.cidrMatchDoc": "この関数は、IP型の最初のパラメーターを取り、その後にCIDR指定に対して評価された1つ以上のパラメーターを取ります。", - "monaco.esql.autocomplete.closeBracketDoc": "閉じ括弧 )", - "monaco.esql.autocomplete.coalesceDoc": "最初のNULL以外の値を返します。", - "monaco.esql.autocomplete.concatDoc": "2つ以上の文字列を連結します。", - "monaco.esql.autocomplete.constantDefinition": "ユーザー定義変数", - "monaco.esql.autocomplete.cosDoc": "余弦三角関数", - "monaco.esql.autocomplete.coshDoc": "余弦双曲線関数", - "monaco.esql.autocomplete.countDistinctDoc": "フィールド内の異なる値の数を返します。", - "monaco.esql.autocomplete.countDoc": "フィールドの値の数を返します。", - "monaco.esql.autocomplete.createNewPolicy": "クリックして作成", - "monaco.esql.autocomplete.dateDurationDefinition.day": "日", - "monaco.esql.autocomplete.dateDurationDefinition.days": "日(複数)", - "monaco.esql.autocomplete.dateDurationDefinition.hour": "時間", - "monaco.esql.autocomplete.dateDurationDefinition.hours": "時間(複数)", - "monaco.esql.autocomplete.dateDurationDefinition.millisecond": "ミリ秒", - "monaco.esql.autocomplete.dateDurationDefinition.milliseconds": "ミリ秒(複数)", - "monaco.esql.autocomplete.dateDurationDefinition.minute": "分", - "monaco.esql.autocomplete.dateDurationDefinition.minutes": "分(複数)", - "monaco.esql.autocomplete.dateDurationDefinition.month": "月", - "monaco.esql.autocomplete.dateDurationDefinition.months": "月(複数)", - "monaco.esql.autocomplete.dateDurationDefinition.second": "秒", - "monaco.esql.autocomplete.dateDurationDefinition.seconds": "秒(複数)", - "monaco.esql.autocomplete.dateDurationDefinition.week": "週", - "monaco.esql.autocomplete.dateDurationDefinition.weeks": "週(複数)", - "monaco.esql.autocomplete.dateDurationDefinition.year": "年", - "monaco.esql.autocomplete.dateDurationDefinition.years": "年(複数)", - "monaco.esql.autocomplete.dateExtractDoc": "年、月、日、時間など、日付の一部を抽出します。サポートされているフィールド型はjava.time.temporal.ChronoFieldで提供されている型です。", - "monaco.esql.autocomplete.dateFormatDoc": "指定した書式の日付の文字列表現を返します。書式が指定されていない場合は、yyyy-MM-dd'T'HH:mm:ss.SSSZの書式が使用されます。", - "monaco.esql.autocomplete.dateParseDoc": "文字列から日付を解析します。", - "monaco.esql.autocomplete.dateTruncDoc": "最も近い区間まで日付を切り捨てます。区間はtimespanリテラル構文を使って表現できます。", - "monaco.esql.autocomplete.declarationLabel": "宣言:", - "monaco.esql.autocomplete.descDoc": "降順", - "monaco.esql.autocomplete.dissectDoc": "単一の文字列入力から、パターンに基づいて複数の文字列値を抽出", - "monaco.esql.autocomplete.divideDoc": "除算(/)", - "monaco.esql.autocomplete.dropDoc": "列を削除", - "monaco.esql.autocomplete.enrichDoc": "別のテーブルでテーブルをエンリッチ", - "monaco.esql.autocomplete.equalToDoc": "等しい", - "monaco.esql.autocomplete.evalDoc": "式を計算し、結果の値を検索結果フィールドに入力します。", - "monaco.esql.autocomplete.examplesLabel": "例:", - "monaco.esql.autocomplete.fieldDefinition": "入力テーブルで指定されたフィールド", - "monaco.esql.autocomplete.floorDoc": "最も近い整数に数値を切り捨てます。", - "monaco.esql.autocomplete.fromDoc": "1つ以上のデータセットからデータを取得します。データセットは検索するデータの集合です。唯一のサポートされているデータセットはインデックスです。クエリまたはサブクエリでは、最初にコマンドから使用する必要があります。先頭のパイプは不要です。たとえば、インデックスからデータを取得します。", - "monaco.esql.autocomplete.greaterThanDoc": "より大きい", - "monaco.esql.autocomplete.greaterThanOrEqualToDoc": "よりも大きいまたは等しい", - "monaco.esql.autocomplete.greatestDoc": "多数の列から最大値を返します。", - "monaco.esql.autocomplete.grokDoc": "単一の文字列入力から、パターンに基づいて複数の文字列値を抽出", - "monaco.esql.autocomplete.inDoc": "ある式が取る値が、他の式のリストに含まれているかどうかをテストします", - "monaco.esql.autocomplete.isFiniteDoc": "入力が有限数であるかどうかを示すブール値を返します。", - "monaco.esql.autocomplete.isInfiniteDoc": "入力が無限数であるかどうかを示すブール値を返します。", - "monaco.esql.autocomplete.keepDoc": "フィールドでkeep句を適用して、入力テーブルのフィールドを並べ替えます", - "monaco.esql.autocomplete.leftDoc": "stringから左から順にlength文字を抜き出したサブ文字列を返します。", - "monaco.esql.autocomplete.lengthDoc": "文字列の文字数を返します。", - "monaco.esql.autocomplete.lessThanDoc": "より小さい", - "monaco.esql.autocomplete.lessThanOrEqualToDoc": "以下", - "monaco.esql.autocomplete.likeDoc": "文字列パターンに基づいてデータをフィルター", - "monaco.esql.autocomplete.limitDoc": "指定された「制限」に基づき、検索順序で、最初の検索結果を返します。", - "monaco.esql.autocomplete.log10Doc": "底が10の対数を返します。", - "monaco.esql.autocomplete.ltrimDoc": "文字列から先頭の空白を取り除きます。", - "monaco.esql.autocomplete.maxDoc": "フィールドの最大値を返します。", - "monaco.esql.autocomplete.medianDeviationDoc": "サンプル全体の中央値からの各データポイントの偏差の中央値を返します。", - "monaco.esql.autocomplete.medianDoc": "50%パーセンタイルを返します。", - "monaco.esql.autocomplete.minDoc": "フィールドの最小値を返します。", - "monaco.esql.autocomplete.multiplyDoc": "乗算(*)", - "monaco.esql.autocomplete.mvExpandDoc": "複数値フィールドを値ごとに1行に展開し、他のフィールドを複製します", - "monaco.esql.autocomplete.newVarDoc": "新しい変数を定義", - "monaco.esql.autocomplete.noPoliciesLabel": "ポリシーがありません", - "monaco.esql.autocomplete.noPoliciesLabelsFound": "クリックして作成", - "monaco.esql.autocomplete.notEqualToDoc": "Not equal to", - "monaco.esql.autocomplete.nowDoc": "現在の日付と時刻を返します。", - "monaco.esql.autocomplete.onDoc": "オン", - "monaco.esql.autocomplete.openBracketDoc": "開き括弧 (", - "monaco.esql.autocomplete.orDoc": "または", - "monaco.esql.autocomplete.percentiletDoc": "フィールドのnパーセンタイルを返します。", - "monaco.esql.autocomplete.pipeDoc": "パイプ(|)", - "monaco.esql.autocomplete.powDoc": "底(第1引数)を累乗(第2引数)した値を返します。", - "monaco.esql.autocomplete.renameDoc": "古い列の名前を新しい列に変更", - "monaco.esql.autocomplete.rightDoc": "stringのうち右から数えてlength文字までのサブ文字列を返します。", - "monaco.esql.autocomplete.rlikeDoc": "文字列の正規表現に基づいてデータをフィルター", - "monaco.esql.autocomplete.roundDoc": "最も近い整数値で指定された数字まで端数処理された数値を返します。デフォルトは整数になるように四捨五入されます。", - "monaco.esql.autocomplete.rtrimDoc": "文字列から末尾の空白を取り除きます。", - "monaco.esql.autocomplete.sinDoc": "正弦三角関数。", - "monaco.esql.autocomplete.sinhDoc": "正弦双曲線関数。", - "monaco.esql.autocomplete.sortDoc": "すべての結果を指定されたフィールドで並べ替えます。降順では、フィールドが見つからない結果は、フィールドの最も小さい可能な値と見なされます。昇順では、フィールドの最も大きい可能な値と見なされます。", - "monaco.esql.autocomplete.sourceDefinition": "入力テーブル", - "monaco.esql.autocomplete.splitDoc": "単一の値の文字列を複数の文字列に分割します。", - "monaco.esql.autocomplete.sqrtDoc": "数値の平方根を返します。", - "monaco.esql.autocomplete.startsWithDoc": "キーワード文字列が他の文字列で始まるかどうかを示すブール値を返します。", - "monaco.esql.autocomplete.statsDoc": "受信検索結果セットで、平均、カウント、合計などの集約統計情報を計算します。SQL集約と同様に、statsコマンドをBY句なしで使用した場合は、1行のみが返されます。これは、受信検索結果セット全体に対する集約です。BY句を使用すると、BY句で指定したフィールドの1つの値ごとに1行が返されます。statsコマンドは集約のフィールドのみを返します。statsコマンドではさまざまな統計関数を使用できます。複数の集約を実行するときには、各集約をカンマで区切ります。", - "monaco.esql.autocomplete.substringDoc": "文字列のサブ文字列を、開始位置とオプションの長さで指定して返します。この例では、それぞれの姓の最初の3文字を返します。", - "monaco.esql.autocomplete.subtractDoc": "減算(-)", - "monaco.esql.autocomplete.sumDoc": "フィールドの値の合計を返します。", - "monaco.esql.autocomplete.tanDoc": "正接三角関数。", - "monaco.esql.autocomplete.tanhDoc": "正接双曲線関数。", - "monaco.esql.autocomplete.toBooleanDoc": "ブール値に変換します。", - "monaco.esql.autocomplete.toDateTimeDoc": "日付に変換します。", - "monaco.esql.autocomplete.toDegreesDoc": "度に変換します", - "monaco.esql.autocomplete.toDoubleDoc": "doubleに変換します。", - "monaco.esql.autocomplete.toIntegerDoc": "整数に変換します。", - "monaco.esql.autocomplete.toIpDoc": "IPに変換します。", - "monaco.esql.autocomplete.toLongDoc": "longに変換します。", - "monaco.esql.autocomplete.toRadiansDoc": "ラジアンに変換します", - "monaco.esql.autocomplete.toStringDoc": "文字列に変換します。", - "monaco.esql.autocomplete.toUnsignedLongDoc": "符号なしlongに変換します。", - "monaco.esql.autocomplete.toVersionDoc": "バージョンに変換します。", - "monaco.esql.autocomplete.trimDoc": "文字列から先頭と末尾の空白を削除します。", - "monaco.esql.autocomplete.whereDoc": "「predicate-expressions」を使用して、検索結果をフィルターします。予測式は評価時にTRUEまたはFALSEを返します。whereコマンドはTRUEに評価される結果のみを返します。たとえば、特定のフィールド値の結果をフィルターします", - "monaco.esql.autocomplete.withDoc": "を使用して", "monaco.painlessLanguage.autocomplete.docKeywordDescription": "doc['field_name'] 構文を使用して、スクリプトからフィールド値にアクセスします", "monaco.painlessLanguage.autocomplete.emitKeywordDescription": "戻らずに値を発行します。", "monaco.painlessLanguage.autocomplete.fieldValueDescription": "フィールド「{fieldName}」の値を取得します", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d3e27d0fcf2c1..6ca5dfb423b66 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4967,129 +4967,6 @@ "management.settings.offLabel": "关闭", "management.settings.onLabel": "开启", "management.settings.resetToDefaultLinkText": "重置为默认值", - "monaco.esql.autocomplete.matchingFieldDefinition": "用于匹配策略上的{matchingField}", - "monaco.esql.autocomplete.policyDefinition": "策略在{count, plural, other {索引}}上定义:{indices}", - "monaco.esql.autocomplete.absDoc": "返回绝对值。", - "monaco.esql.autocomplete.acosDoc": "反余弦三角函数", - "monaco.esql.autocomplete.addDoc": "添加 (+)", - "monaco.esql.autocomplete.andDoc": "且", - "monaco.esql.autocomplete.ascDoc": "升序", - "monaco.esql.autocomplete.asDoc": "作为", - "monaco.esql.autocomplete.asinDoc": "反正弦三角函数", - "monaco.esql.autocomplete.assignDoc": "分配 (=)", - "monaco.esql.autocomplete.atan2Doc": "笛卡儿平面中正 x 轴与从原点到点 (x , y) 构成的射线之间的角度", - "monaco.esql.autocomplete.atanDoc": "反正切三角函数", - "monaco.esql.autocomplete.autoBucketDoc": "根据给定范围和存储桶目标自动收集存储桶日期。", - "monaco.esql.autocomplete.avgDoc": "返回字段中的值的平均值", - "monaco.esql.autocomplete.byDoc": "依据", - "monaco.esql.autocomplete.caseDoc": "接受成对的条件和值。此函数返回属于第一个评估为 `true` 的条件的值。如果参数数量为奇数,则最后一个参数为在无条件匹配时返回的默认值。", - "monaco.esql.autocomplete.cidrMatchDoc": "此函数接受的第一个参数应为 IP 类型,后接一个或多个评估为 CIDR 规范的参数。", - "monaco.esql.autocomplete.closeBracketDoc": "右括号 )", - "monaco.esql.autocomplete.coalesceDoc": "返回第一个非 null 值。", - "monaco.esql.autocomplete.concatDoc": "串联两个或多个字符串。", - "monaco.esql.autocomplete.constantDefinition": "用户定义的变量", - "monaco.esql.autocomplete.cosDoc": "余弦三角函数", - "monaco.esql.autocomplete.coshDoc": "余弦双曲函数", - "monaco.esql.autocomplete.countDistinctDoc": "返回字段中不同值的计数。", - "monaco.esql.autocomplete.countDoc": "返回字段中的值的计数。", - "monaco.esql.autocomplete.createNewPolicy": "单击以创建", - "monaco.esql.autocomplete.dateDurationDefinition.day": "天", - "monaco.esql.autocomplete.dateDurationDefinition.days": "天(复数)", - "monaco.esql.autocomplete.dateDurationDefinition.hour": "小时", - "monaco.esql.autocomplete.dateDurationDefinition.hours": "小时(复数)", - "monaco.esql.autocomplete.dateDurationDefinition.millisecond": "毫秒", - "monaco.esql.autocomplete.dateDurationDefinition.milliseconds": "毫秒(复数)", - "monaco.esql.autocomplete.dateDurationDefinition.minute": "分钟", - "monaco.esql.autocomplete.dateDurationDefinition.minutes": "分钟(复数)", - "monaco.esql.autocomplete.dateDurationDefinition.month": "月", - "monaco.esql.autocomplete.dateDurationDefinition.months": "月(复数)", - "monaco.esql.autocomplete.dateDurationDefinition.second": "秒", - "monaco.esql.autocomplete.dateDurationDefinition.seconds": "秒(复数)", - "monaco.esql.autocomplete.dateDurationDefinition.week": "周", - "monaco.esql.autocomplete.dateDurationDefinition.weeks": "周(复数)", - "monaco.esql.autocomplete.dateDurationDefinition.year": "年", - "monaco.esql.autocomplete.dateDurationDefinition.years": "年(复数)", - "monaco.esql.autocomplete.dateExtractDoc": "提取日期的某些部分,如年、月、日、小时。支持的字段类型为 java.time.temporal.ChronoField 提供的那些类型", - "monaco.esql.autocomplete.dateFormatDoc": "以提供的格式返回日期的字符串表示形式。如果未指定格式,则使用“yyyy-MM-dd'T'HH:mm:ss.SSSZ”格式。", - "monaco.esql.autocomplete.dateParseDoc": "解析字符串中的日期。", - "monaco.esql.autocomplete.dateTruncDoc": "将日期向下舍入到最近的时间间隔。时间间隔可以用时间跨度文本语法表示。", - "monaco.esql.autocomplete.declarationLabel": "声明:", - "monaco.esql.autocomplete.descDoc": "降序", - "monaco.esql.autocomplete.dissectDoc": "根据模式从单个字符串输入中提取多个字符串值", - "monaco.esql.autocomplete.divideDoc": "除 (/)", - "monaco.esql.autocomplete.dropDoc": "丢弃列", - "monaco.esql.autocomplete.enrichDoc": "用其他表来扩充表", - "monaco.esql.autocomplete.equalToDoc": "等于", - "monaco.esql.autocomplete.evalDoc": "计算表达式并将生成的值置入搜索结果字段。", - "monaco.esql.autocomplete.examplesLabel": "示例:", - "monaco.esql.autocomplete.fieldDefinition": "由输入表指定的字段", - "monaco.esql.autocomplete.floorDoc": "将数字向下舍入到最近的整数。", - "monaco.esql.autocomplete.fromDoc": "从一个或多个数据集中检索数据。数据集是您希望搜索的数据的集合。索引是唯一受支持的数据集。在查询或子查询中,必须先使用 from 命令,并且它不需要前导管道符。例如,要从索引中检索数据:", - "monaco.esql.autocomplete.greaterThanDoc": "大于", - "monaco.esql.autocomplete.greaterThanOrEqualToDoc": "大于或等于", - "monaco.esql.autocomplete.greatestDoc": "返回许多列中的最大值。", - "monaco.esql.autocomplete.grokDoc": "根据模式从单个字符串输入中提取多个字符串值", - "monaco.esql.autocomplete.inDoc": "测试某表达式接受的值是否包含在其他表达式列表中", - "monaco.esql.autocomplete.isFiniteDoc": "返回指示其输入是否为有限数字的布尔值。", - "monaco.esql.autocomplete.isInfiniteDoc": "返回指示其输入是否无限的布尔值。", - "monaco.esql.autocomplete.keepDoc": "通过在字段中应用 keep 子句重新安排输入表中的字段", - "monaco.esql.autocomplete.leftDoc": "返回从字符串中提取长度字符的子字符串,从左侧开始。", - "monaco.esql.autocomplete.lengthDoc": "返回字符串的字符长度。", - "monaco.esql.autocomplete.lessThanDoc": "小于", - "monaco.esql.autocomplete.lessThanOrEqualToDoc": "小于或等于", - "monaco.esql.autocomplete.likeDoc": "根据字符串模式筛选数据", - "monaco.esql.autocomplete.limitDoc": "根据指定的“限制”按搜索顺序返回第一个搜索结果。", - "monaco.esql.autocomplete.log10Doc": "返回对数底数 10。", - "monaco.esql.autocomplete.ltrimDoc": "从字符串中移除前导空格。", - "monaco.esql.autocomplete.maxDoc": "返回字段中的最大值。", - "monaco.esql.autocomplete.medianDeviationDoc": "返回每个数据点的中位数与整个样例的中位数的偏差。", - "monaco.esql.autocomplete.medianDoc": "返回 50% 百分位数。", - "monaco.esql.autocomplete.minDoc": "返回字段中的最小值。", - "monaco.esql.autocomplete.multiplyDoc": "乘 (*)", - "monaco.esql.autocomplete.mvExpandDoc": "将多值字段扩展成每个值一行,从而复制其他字段", - "monaco.esql.autocomplete.newVarDoc": "定义新变量", - "monaco.esql.autocomplete.noPoliciesLabel": "没有可用策略", - "monaco.esql.autocomplete.noPoliciesLabelsFound": "单击以创建", - "monaco.esql.autocomplete.notEqualToDoc": "不等于", - "monaco.esql.autocomplete.nowDoc": "返回当前日期和时间。", - "monaco.esql.autocomplete.onDoc": "开启", - "monaco.esql.autocomplete.openBracketDoc": "左括号 (", - "monaco.esql.autocomplete.orDoc": "或", - "monaco.esql.autocomplete.percentiletDoc": "返回字段的第 n 个百分位。", - "monaco.esql.autocomplete.pipeDoc": "管道符 (|)", - "monaco.esql.autocomplete.powDoc": "返回提升为幂(第二个参数)的底数(第一个参数)的值。", - "monaco.esql.autocomplete.renameDoc": "将旧列重命名为新列", - "monaco.esql.autocomplete.rightDoc": "返回从字符串中提取长度字符的子字符串,从右侧开始。", - "monaco.esql.autocomplete.rlikeDoc": "根据字符串正则表达式筛选数据", - "monaco.esql.autocomplete.roundDoc": "返回四舍五入到小数(由最近的整数值指定)的数字。默认做法是四舍五入到整数。", - "monaco.esql.autocomplete.rtrimDoc": "从字符串中移除尾随空格。", - "monaco.esql.autocomplete.sinDoc": "正弦三角函数。", - "monaco.esql.autocomplete.sinhDoc": "正弦双曲函数。", - "monaco.esql.autocomplete.sortDoc": "按指定字段对所有结果排序。采用降序时,会将缺少字段的结果视为字段的最小可能值,或者,在采用升序时,会将其视为字段的最大可能值。", - "monaco.esql.autocomplete.sourceDefinition": "输入表", - "monaco.esql.autocomplete.splitDoc": "将单值字符串拆分成多个字符串。", - "monaco.esql.autocomplete.sqrtDoc": "返回数字的平方根。", - "monaco.esql.autocomplete.startsWithDoc": "返回指示关键字字符串是否以另一个字符串开头的布尔值。", - "monaco.esql.autocomplete.statsDoc": "对传入的搜索结果集计算汇总统计信息,如平均值、计数和总和。与 SQL 聚合类似,如果使用不含 BY 子句的 stats 命令,则只返回一行内容,即聚合传入的整个搜索结果集。使用 BY 子句时,将为在 BY 子句中指定的字段中的每个不同值返回一行内容。stats 命令仅返回聚合中的字段,并且您可以将一系列统计函数与 stats 命令搭配在一起使用。执行多个聚合时,请用逗号分隔每个聚合。", - "monaco.esql.autocomplete.substringDoc": "返回字符串的子字符串,用起始位置和可选长度指定。此示例返回每个姓氏的前三个字符。", - "monaco.esql.autocomplete.subtractDoc": "减 (-)", - "monaco.esql.autocomplete.sumDoc": "返回字段中的值的总和。", - "monaco.esql.autocomplete.tanDoc": "正切三角函数。", - "monaco.esql.autocomplete.tanhDoc": "正切双曲函数。", - "monaco.esql.autocomplete.toBooleanDoc": "转换为布尔值。", - "monaco.esql.autocomplete.toDateTimeDoc": "转换为日期。", - "monaco.esql.autocomplete.toDegreesDoc": "转换为度", - "monaco.esql.autocomplete.toDoubleDoc": "转换为双精度值。", - "monaco.esql.autocomplete.toIntegerDoc": "转换为整数。", - "monaco.esql.autocomplete.toIpDoc": "转换为 IP。", - "monaco.esql.autocomplete.toLongDoc": "转换为长整型。", - "monaco.esql.autocomplete.toRadiansDoc": "转换为弧度", - "monaco.esql.autocomplete.toStringDoc": "转换为字符串。", - "monaco.esql.autocomplete.toUnsignedLongDoc": "转换为无符号长整型。", - "monaco.esql.autocomplete.toVersionDoc": "转换为版本。", - "monaco.esql.autocomplete.trimDoc": "从字符串中移除前导和尾随空格。", - "monaco.esql.autocomplete.whereDoc": "使用“predicate-expressions”可筛选搜索结果。进行计算时,谓词表达式将返回 TRUE 或 FALSE。where 命令仅返回计算结果为 TRUE 的结果。例如,筛选特定字段值的结果", - "monaco.esql.autocomplete.withDoc": "具有", "monaco.painlessLanguage.autocomplete.docKeywordDescription": "使用 doc['field_name'] 语法,从脚本中访问字段值", "monaco.painlessLanguage.autocomplete.emitKeywordDescription": "发出值,而不返回值。", "monaco.painlessLanguage.autocomplete.fieldValueDescription": "检索字段“{fieldName}”的值",