mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[ES|QL] Separate RENAME
autocomplete routine (#213641)
## Summary Part of https://github.com/elastic/kibana/issues/195418 Gives `RENAME` autocomplete logic its own home 🏡 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### Identify risks - [ ] As with any refactor, there's a possibility this will introduce a regression in the behavior of commands. However, all automated tests are passing and I have tested the behavior manually and can detect no regression.
This commit is contained in:
parent
60ccd5805f
commit
63d3364817
6 changed files with 108 additions and 116 deletions
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { getFieldNamesByType, setup } from './helpers';
|
||||
|
||||
describe('autocomplete.suggest', () => {
|
||||
describe('RENAME', () => {
|
||||
it('suggests fields', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
await assertSuggestions(
|
||||
'from a | rename /',
|
||||
getFieldNamesByType('any').map((field) => field + ' ')
|
||||
);
|
||||
await assertSuggestions(
|
||||
'from a | rename fie/',
|
||||
getFieldNamesByType('any').map((field) => field + ' ')
|
||||
);
|
||||
await assertSuggestions(
|
||||
'from a | rename field AS foo, /',
|
||||
getFieldNamesByType('any').map((field) => field + ' ')
|
||||
);
|
||||
await assertSuggestions(
|
||||
'from a | rename field AS foo, fie/',
|
||||
getFieldNamesByType('any').map((field) => field + ' ')
|
||||
);
|
||||
});
|
||||
|
||||
it('suggests AS after field', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
await assertSuggestions('from a | rename field /', ['AS ']);
|
||||
await assertSuggestions('from a | rename field A/', ['AS ']);
|
||||
await assertSuggestions('from a | rename field AS foo, field2 /', ['AS ']);
|
||||
await assertSuggestions('from a | rename field as foo , field2 /', ['AS ']);
|
||||
await assertSuggestions('from a | rename field AS foo, field2 A/', ['AS ']);
|
||||
});
|
||||
|
||||
it('suggests nothing after AS', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
await assertSuggestions('from a | rename field AS /', []);
|
||||
});
|
||||
|
||||
it('suggests pipe and comma after complete expression', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
await assertSuggestions('from a | rename field AS foo /', ['| ', ', ']);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -112,12 +112,6 @@ describe('autocomplete', () => {
|
|||
testSuggestions('from a metadata _id | eval var0 = a | /', commands);
|
||||
});
|
||||
|
||||
describe('rename', () => {
|
||||
testSuggestions('from a | rename /', getFieldNamesByType('any'));
|
||||
testSuggestions('from a | rename keywordField /', ['AS $0'], ' ');
|
||||
testSuggestions('from a | rename keywordField as /', ['var0']);
|
||||
});
|
||||
|
||||
for (const command of ['keep', 'drop']) {
|
||||
describe(command, () => {
|
||||
testSuggestions(`from a | ${command} /`, getFieldNamesByType('any'));
|
||||
|
@ -405,13 +399,13 @@ describe('autocomplete', () => {
|
|||
);
|
||||
|
||||
// RENAME field
|
||||
testSuggestions('FROM index1 | RENAME f/', getFieldNamesByType('any'));
|
||||
testSuggestions(
|
||||
'FROM index1 | RENAME f/',
|
||||
getFieldNamesByType('any').map((name) => `${name} `)
|
||||
);
|
||||
|
||||
// RENAME field AS
|
||||
testSuggestions('FROM index1 | RENAME field A/', ['AS $0']);
|
||||
|
||||
// RENAME field AS var0
|
||||
testSuggestions('FROM index1 | RENAME field AS v/', ['var0']);
|
||||
testSuggestions('FROM index1 | RENAME field A/', ['AS ']);
|
||||
|
||||
// STATS argument
|
||||
testSuggestions('FROM index1 | STATS f/', [
|
||||
|
|
|
@ -22,7 +22,6 @@ import type { EditorContext, ItemKind, SuggestionRawDefinition, GetColumnsByType
|
|||
import {
|
||||
getColumnForASTNode,
|
||||
getCommandDefinition,
|
||||
getCommandOption,
|
||||
getFunctionDefinition,
|
||||
isAssignment,
|
||||
isAssignmentComplete,
|
||||
|
@ -36,7 +35,6 @@ import {
|
|||
isTimeIntervalItem,
|
||||
getAllFunctions,
|
||||
isSingleItem,
|
||||
nonNullable,
|
||||
getColumnExists,
|
||||
findPreviousWord,
|
||||
correctQuerySyntax,
|
||||
|
@ -59,7 +57,6 @@ import {
|
|||
getFunctionSuggestions,
|
||||
getCompatibleLiterals,
|
||||
buildConstantsDefinitions,
|
||||
buildVariablesDefinitions,
|
||||
buildOptionDefinition,
|
||||
buildValueDefinitions,
|
||||
getDateLiterals,
|
||||
|
@ -172,13 +169,7 @@ export async function suggest(
|
|||
return suggestions.filter((def) => !isSourceCommand(def));
|
||||
}
|
||||
|
||||
if (
|
||||
astContext.type === 'expression' ||
|
||||
(astContext.type === 'option' && astContext.command?.name === 'join') ||
|
||||
(astContext.type === 'option' && astContext.command?.name === 'dissect') ||
|
||||
(astContext.type === 'option' && astContext.command?.name === 'from') ||
|
||||
(astContext.type === 'option' && astContext.command?.name === 'enrich')
|
||||
) {
|
||||
if (astContext.type === 'expression') {
|
||||
return getSuggestionsWithinCommandExpression(
|
||||
innerText,
|
||||
ast,
|
||||
|
@ -194,20 +185,6 @@ export async function suggest(
|
|||
supportsControls
|
||||
);
|
||||
}
|
||||
if (astContext.type === 'option') {
|
||||
// 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') {
|
||||
return getFunctionArgsSuggestions(
|
||||
innerText,
|
||||
|
@ -1190,71 +1167,3 @@ async function getListArgsSuggestions(
|
|||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated — this will disappear when https://github.com/elastic/kibana/issues/195418 is complete
|
||||
* because "options" will be handled in imperative command-specific routines instead of being independent.
|
||||
*/
|
||||
async function getOptionArgsSuggestions(
|
||||
innerText: string,
|
||||
commands: ESQLCommand[],
|
||||
{
|
||||
command,
|
||||
option,
|
||||
node,
|
||||
}: {
|
||||
command: ESQLCommand;
|
||||
option: ESQLCommandOption;
|
||||
node: ESQLSingleAstItem | undefined;
|
||||
},
|
||||
getFieldsByType: GetColumnsByTypeFn,
|
||||
getFieldsMaps: GetFieldsMapFn,
|
||||
getPolicyMetadata: GetPolicyMetadataFn
|
||||
) {
|
||||
const optionDef = getCommandOption(option.name);
|
||||
if (!optionDef || !optionDef.signature) {
|
||||
return [];
|
||||
}
|
||||
const { nodeArg, lastArg } = extractArgMeta(option, node);
|
||||
const suggestions = [];
|
||||
const isNewExpression = isRestartingExpression(innerText) || option.args.length === 0;
|
||||
|
||||
const fieldsMap = await getFieldsMaps();
|
||||
const anyVariables = collectVariables(commands, fieldsMap, innerText);
|
||||
|
||||
if (command.name === 'rename') {
|
||||
if (option.args.length < 2) {
|
||||
suggestions.push(...buildVariablesDefinitions([findNewVariable(anyVariables)]));
|
||||
}
|
||||
}
|
||||
|
||||
if (optionDef) {
|
||||
if (!suggestions.length) {
|
||||
const argDefIndex = optionDef.signature.multipleParams
|
||||
? 0
|
||||
: Math.max(option.args.length - 1, 0);
|
||||
const types = [optionDef.signature.params[argDefIndex].type].filter(nonNullable);
|
||||
// If it's a complete expression then proposed some final suggestions
|
||||
// A complete expression is either a function or a column: <COMMAND> <OPTION> field <here>
|
||||
// Or an assignment complete: <COMMAND> <OPTION> field = ... <here>
|
||||
if (
|
||||
(option.args.length && !isNewExpression && !isAssignment(lastArg)) ||
|
||||
(isAssignment(lastArg) && isAssignmentComplete(lastArg))
|
||||
) {
|
||||
suggestions.push(
|
||||
...getFinalSuggestions({
|
||||
comma: optionDef.signature.multipleParams,
|
||||
})
|
||||
);
|
||||
} else if (isNewExpression || (isAssignment(nodeArg) && !isAssignmentComplete(nodeArg))) {
|
||||
suggestions.push(
|
||||
...(await getFieldsByType(types[0] === 'column' ? ['any'] : types, [], {
|
||||
advanceCursor: true,
|
||||
openSuggestions: true,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
|
|
@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CommandSuggestParams } from '../../../definitions/types';
|
||||
|
||||
import type { SuggestionRawDefinition } from '../../types';
|
||||
import { commaCompleteItem, pipeCompleteItem } from '../../complete_items';
|
||||
|
||||
export async function suggest({
|
||||
getColumnsByType,
|
||||
innerText,
|
||||
}: CommandSuggestParams<'rename'>): Promise<SuggestionRawDefinition[]> {
|
||||
if (/(?:rename|,)\s+\S+\s+a?$/i.test(innerText)) {
|
||||
return [asCompletionItem];
|
||||
}
|
||||
|
||||
if (/rename(?:\s+\S+\s+as\s+\S+\s*,)*\s+\S+\s+as\s+[^\s,]+\s+$/i.test(innerText)) {
|
||||
return [pipeCompleteItem, { ...commaCompleteItem, text: ', ' }];
|
||||
}
|
||||
|
||||
if (/as\s+$/i.test(innerText)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getColumnsByType('any', [], { advanceCursor: true, openSuggestions: true });
|
||||
}
|
||||
|
||||
const asCompletionItem: SuggestionRawDefinition = {
|
||||
detail: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.asDoc', {
|
||||
defaultMessage: 'As',
|
||||
}),
|
||||
kind: 'Reference',
|
||||
label: 'AS',
|
||||
sortText: '1',
|
||||
text: 'AS ',
|
||||
};
|
|
@ -42,6 +42,7 @@ import { suggest as suggestForShow } from '../autocomplete/commands/show';
|
|||
import { suggest as suggestForGrok } from '../autocomplete/commands/grok';
|
||||
import { suggest as suggestForDissect } from '../autocomplete/commands/dissect';
|
||||
import { suggest as suggestForEnrich } from '../autocomplete/commands/enrich';
|
||||
import { suggest as suggestForRename } from '../autocomplete/commands/rename';
|
||||
import { suggest as suggestForLimit } from '../autocomplete/commands/limit';
|
||||
import { suggest as suggestForMvExpand } from '../autocomplete/commands/mv_expand';
|
||||
|
||||
|
@ -275,6 +276,7 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
|
|||
},
|
||||
options: [asOption],
|
||||
modes: [],
|
||||
suggest: suggestForRename,
|
||||
},
|
||||
{
|
||||
name: 'limit',
|
||||
|
|
|
@ -13,19 +13,19 @@ import {
|
|||
type ESQLAst,
|
||||
type ESQLFunction,
|
||||
type ESQLCommand,
|
||||
type ESQLCommandOption,
|
||||
type ESQLCommandMode,
|
||||
Walker,
|
||||
isIdentifier,
|
||||
ESQLCommandOption,
|
||||
ESQLCommandMode,
|
||||
} from '@kbn/esql-ast';
|
||||
import { FunctionDefinitionTypes } from '../definitions/types';
|
||||
import { EDITOR_MARKER } from './constants';
|
||||
import {
|
||||
isOptionItem,
|
||||
isColumnItem,
|
||||
isSourceItem,
|
||||
pipePrecedesCurrentWord,
|
||||
getFunctionDefinition,
|
||||
isOptionItem,
|
||||
} from './helpers';
|
||||
|
||||
function findNode(nodes: ESQLAstItem[], offset: number): ESQLSingleAstItem | undefined {
|
||||
|
@ -119,7 +119,7 @@ export function removeMarkerArgFromArgsList<T extends ESQLSingleAstItem | ESQLCo
|
|||
function findAstPosition(ast: ESQLAst, offset: number) {
|
||||
const command = findCommand(ast, offset);
|
||||
if (!command) {
|
||||
return { command: undefined, node: undefined, option: undefined, setting: undefined };
|
||||
return { command: undefined, node: undefined };
|
||||
}
|
||||
return {
|
||||
command: removeMarkerArgFromArgsList(command)!,
|
||||
|
@ -144,8 +144,6 @@ function isOperator(node: ESQLFunction) {
|
|||
* Type details:
|
||||
* * "list": the cursor is inside a "in" list of values (i.e. `a in (1, 2, <here>)`)
|
||||
* * "function": the cursor is inside a function call (i.e. `fn(<here>)`)
|
||||
* * "option": the cursor is inside a command option (i.e. `command ... by <here>`)
|
||||
* * "setting": the cursor is inside a setting (i.e. `command _<here>`)
|
||||
* * "expression": the cursor is inside a command expression (i.e. `command ... <here>` or `command a = ... <here>`)
|
||||
* * "newCommand": the cursor is at the beginning of a new command (i.e. `command1 | command2 | <here>`)
|
||||
*/
|
||||
|
@ -193,13 +191,6 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number)
|
|||
return { type: 'newCommand' as const, command: undefined, node, option };
|
||||
}
|
||||
|
||||
// TODO — remove this option branch once https://github.com/elastic/kibana/issues/195418 is complete
|
||||
if (command && isOptionItem(command.args[command.args.length - 1]) && command.name !== 'stats') {
|
||||
if (option) {
|
||||
return { type: 'option' as const, command, node, option };
|
||||
}
|
||||
}
|
||||
|
||||
// command a ... <here> OR command a = ... <here>
|
||||
return {
|
||||
type: 'expression' as const,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue