Skip to content

Commit 63d3364

Browse files
authored
[ES|QL] Separate RENAME autocomplete routine (elastic#213641)
## Summary Part of elastic#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.
1 parent 60ccd58 commit 63d3364

File tree

6 files changed

+108
-116
lines changed

6 files changed

+108
-116
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { getFieldNamesByType, setup } from './helpers';
11+
12+
describe('autocomplete.suggest', () => {
13+
describe('RENAME', () => {
14+
it('suggests fields', async () => {
15+
const { assertSuggestions } = await setup();
16+
await assertSuggestions(
17+
'from a | rename /',
18+
getFieldNamesByType('any').map((field) => field + ' ')
19+
);
20+
await assertSuggestions(
21+
'from a | rename fie/',
22+
getFieldNamesByType('any').map((field) => field + ' ')
23+
);
24+
await assertSuggestions(
25+
'from a | rename field AS foo, /',
26+
getFieldNamesByType('any').map((field) => field + ' ')
27+
);
28+
await assertSuggestions(
29+
'from a | rename field AS foo, fie/',
30+
getFieldNamesByType('any').map((field) => field + ' ')
31+
);
32+
});
33+
34+
it('suggests AS after field', async () => {
35+
const { assertSuggestions } = await setup();
36+
await assertSuggestions('from a | rename field /', ['AS ']);
37+
await assertSuggestions('from a | rename field A/', ['AS ']);
38+
await assertSuggestions('from a | rename field AS foo, field2 /', ['AS ']);
39+
await assertSuggestions('from a | rename field as foo , field2 /', ['AS ']);
40+
await assertSuggestions('from a | rename field AS foo, field2 A/', ['AS ']);
41+
});
42+
43+
it('suggests nothing after AS', async () => {
44+
const { assertSuggestions } = await setup();
45+
await assertSuggestions('from a | rename field AS /', []);
46+
});
47+
48+
it('suggests pipe and comma after complete expression', async () => {
49+
const { assertSuggestions } = await setup();
50+
await assertSuggestions('from a | rename field AS foo /', ['| ', ', ']);
51+
});
52+
});
53+
});

src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts

+5-11
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,6 @@ describe('autocomplete', () => {
112112
testSuggestions('from a metadata _id | eval var0 = a | /', commands);
113113
});
114114

115-
describe('rename', () => {
116-
testSuggestions('from a | rename /', getFieldNamesByType('any'));
117-
testSuggestions('from a | rename keywordField /', ['AS $0'], ' ');
118-
testSuggestions('from a | rename keywordField as /', ['var0']);
119-
});
120-
121115
for (const command of ['keep', 'drop']) {
122116
describe(command, () => {
123117
testSuggestions(`from a | ${command} /`, getFieldNamesByType('any'));
@@ -405,13 +399,13 @@ describe('autocomplete', () => {
405399
);
406400

407401
// RENAME field
408-
testSuggestions('FROM index1 | RENAME f/', getFieldNamesByType('any'));
402+
testSuggestions(
403+
'FROM index1 | RENAME f/',
404+
getFieldNamesByType('any').map((name) => `${name} `)
405+
);
409406

410407
// RENAME field AS
411-
testSuggestions('FROM index1 | RENAME field A/', ['AS $0']);
412-
413-
// RENAME field AS var0
414-
testSuggestions('FROM index1 | RENAME field AS v/', ['var0']);
408+
testSuggestions('FROM index1 | RENAME field A/', ['AS ']);
415409

416410
// STATS argument
417411
testSuggestions('FROM index1 | STATS f/', [

src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.ts

+1-92
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import type { EditorContext, ItemKind, SuggestionRawDefinition, GetColumnsByType
2222
import {
2323
getColumnForASTNode,
2424
getCommandDefinition,
25-
getCommandOption,
2625
getFunctionDefinition,
2726
isAssignment,
2827
isAssignmentComplete,
@@ -36,7 +35,6 @@ import {
3635
isTimeIntervalItem,
3736
getAllFunctions,
3837
isSingleItem,
39-
nonNullable,
4038
getColumnExists,
4139
findPreviousWord,
4240
correctQuerySyntax,
@@ -59,7 +57,6 @@ import {
5957
getFunctionSuggestions,
6058
getCompatibleLiterals,
6159
buildConstantsDefinitions,
62-
buildVariablesDefinitions,
6360
buildOptionDefinition,
6461
buildValueDefinitions,
6562
getDateLiterals,
@@ -172,13 +169,7 @@ export async function suggest(
172169
return suggestions.filter((def) => !isSourceCommand(def));
173170
}
174171

175-
if (
176-
astContext.type === 'expression' ||
177-
(astContext.type === 'option' && astContext.command?.name === 'join') ||
178-
(astContext.type === 'option' && astContext.command?.name === 'dissect') ||
179-
(astContext.type === 'option' && astContext.command?.name === 'from') ||
180-
(astContext.type === 'option' && astContext.command?.name === 'enrich')
181-
) {
172+
if (astContext.type === 'expression') {
182173
return getSuggestionsWithinCommandExpression(
183174
innerText,
184175
ast,
@@ -194,20 +185,6 @@ export async function suggest(
194185
supportsControls
195186
);
196187
}
197-
if (astContext.type === 'option') {
198-
// need this wrap/unwrap thing to make TS happy
199-
const { option, ...rest } = astContext;
200-
if (option && isOptionItem(option)) {
201-
return getOptionArgsSuggestions(
202-
innerText,
203-
ast,
204-
{ option, ...rest },
205-
getFieldsByType,
206-
getFieldsMap,
207-
getPolicyMetadata
208-
);
209-
}
210-
}
211188
if (astContext.type === 'function') {
212189
return getFunctionArgsSuggestions(
213190
innerText,
@@ -1190,71 +1167,3 @@ async function getListArgsSuggestions(
11901167
}
11911168
return suggestions;
11921169
}
1193-
1194-
/**
1195-
* @deprecated — this will disappear when https://github.com/elastic/kibana/issues/195418 is complete
1196-
* because "options" will be handled in imperative command-specific routines instead of being independent.
1197-
*/
1198-
async function getOptionArgsSuggestions(
1199-
innerText: string,
1200-
commands: ESQLCommand[],
1201-
{
1202-
command,
1203-
option,
1204-
node,
1205-
}: {
1206-
command: ESQLCommand;
1207-
option: ESQLCommandOption;
1208-
node: ESQLSingleAstItem | undefined;
1209-
},
1210-
getFieldsByType: GetColumnsByTypeFn,
1211-
getFieldsMaps: GetFieldsMapFn,
1212-
getPolicyMetadata: GetPolicyMetadataFn
1213-
) {
1214-
const optionDef = getCommandOption(option.name);
1215-
if (!optionDef || !optionDef.signature) {
1216-
return [];
1217-
}
1218-
const { nodeArg, lastArg } = extractArgMeta(option, node);
1219-
const suggestions = [];
1220-
const isNewExpression = isRestartingExpression(innerText) || option.args.length === 0;
1221-
1222-
const fieldsMap = await getFieldsMaps();
1223-
const anyVariables = collectVariables(commands, fieldsMap, innerText);
1224-
1225-
if (command.name === 'rename') {
1226-
if (option.args.length < 2) {
1227-
suggestions.push(...buildVariablesDefinitions([findNewVariable(anyVariables)]));
1228-
}
1229-
}
1230-
1231-
if (optionDef) {
1232-
if (!suggestions.length) {
1233-
const argDefIndex = optionDef.signature.multipleParams
1234-
? 0
1235-
: Math.max(option.args.length - 1, 0);
1236-
const types = [optionDef.signature.params[argDefIndex].type].filter(nonNullable);
1237-
// If it's a complete expression then proposed some final suggestions
1238-
// A complete expression is either a function or a column: <COMMAND> <OPTION> field <here>
1239-
// Or an assignment complete: <COMMAND> <OPTION> field = ... <here>
1240-
if (
1241-
(option.args.length && !isNewExpression && !isAssignment(lastArg)) ||
1242-
(isAssignment(lastArg) && isAssignmentComplete(lastArg))
1243-
) {
1244-
suggestions.push(
1245-
...getFinalSuggestions({
1246-
comma: optionDef.signature.multipleParams,
1247-
})
1248-
);
1249-
} else if (isNewExpression || (isAssignment(nodeArg) && !isAssignmentComplete(nodeArg))) {
1250-
suggestions.push(
1251-
...(await getFieldsByType(types[0] === 'column' ? ['any'] : types, [], {
1252-
advanceCursor: true,
1253-
openSuggestions: true,
1254-
}))
1255-
);
1256-
}
1257-
}
1258-
}
1259-
return suggestions;
1260-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { i18n } from '@kbn/i18n';
11+
import { CommandSuggestParams } from '../../../definitions/types';
12+
13+
import type { SuggestionRawDefinition } from '../../types';
14+
import { commaCompleteItem, pipeCompleteItem } from '../../complete_items';
15+
16+
export async function suggest({
17+
getColumnsByType,
18+
innerText,
19+
}: CommandSuggestParams<'rename'>): Promise<SuggestionRawDefinition[]> {
20+
if (/(?:rename|,)\s+\S+\s+a?$/i.test(innerText)) {
21+
return [asCompletionItem];
22+
}
23+
24+
if (/rename(?:\s+\S+\s+as\s+\S+\s*,)*\s+\S+\s+as\s+[^\s,]+\s+$/i.test(innerText)) {
25+
return [pipeCompleteItem, { ...commaCompleteItem, text: ', ' }];
26+
}
27+
28+
if (/as\s+$/i.test(innerText)) {
29+
return [];
30+
}
31+
32+
return getColumnsByType('any', [], { advanceCursor: true, openSuggestions: true });
33+
}
34+
35+
const asCompletionItem: SuggestionRawDefinition = {
36+
detail: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.asDoc', {
37+
defaultMessage: 'As',
38+
}),
39+
kind: 'Reference',
40+
label: 'AS',
41+
sortText: '1',
42+
text: 'AS ',
43+
};

src/platform/packages/shared/kbn-esql-validation-autocomplete/src/definitions/commands.ts

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { suggest as suggestForShow } from '../autocomplete/commands/show';
4242
import { suggest as suggestForGrok } from '../autocomplete/commands/grok';
4343
import { suggest as suggestForDissect } from '../autocomplete/commands/dissect';
4444
import { suggest as suggestForEnrich } from '../autocomplete/commands/enrich';
45+
import { suggest as suggestForRename } from '../autocomplete/commands/rename';
4546
import { suggest as suggestForLimit } from '../autocomplete/commands/limit';
4647
import { suggest as suggestForMvExpand } from '../autocomplete/commands/mv_expand';
4748

@@ -275,6 +276,7 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
275276
},
276277
options: [asOption],
277278
modes: [],
279+
suggest: suggestForRename,
278280
},
279281
{
280282
name: 'limit',

src/platform/packages/shared/kbn-esql-validation-autocomplete/src/shared/context.ts

+4-13
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@ import {
1313
type ESQLAst,
1414
type ESQLFunction,
1515
type ESQLCommand,
16-
type ESQLCommandOption,
17-
type ESQLCommandMode,
1816
Walker,
1917
isIdentifier,
18+
ESQLCommandOption,
19+
ESQLCommandMode,
2020
} from '@kbn/esql-ast';
2121
import { FunctionDefinitionTypes } from '../definitions/types';
2222
import { EDITOR_MARKER } from './constants';
2323
import {
24-
isOptionItem,
2524
isColumnItem,
2625
isSourceItem,
2726
pipePrecedesCurrentWord,
2827
getFunctionDefinition,
28+
isOptionItem,
2929
} from './helpers';
3030

3131
function findNode(nodes: ESQLAstItem[], offset: number): ESQLSingleAstItem | undefined {
@@ -119,7 +119,7 @@ export function removeMarkerArgFromArgsList<T extends ESQLSingleAstItem | ESQLCo
119119
function findAstPosition(ast: ESQLAst, offset: number) {
120120
const command = findCommand(ast, offset);
121121
if (!command) {
122-
return { command: undefined, node: undefined, option: undefined, setting: undefined };
122+
return { command: undefined, node: undefined };
123123
}
124124
return {
125125
command: removeMarkerArgFromArgsList(command)!,
@@ -144,8 +144,6 @@ function isOperator(node: ESQLFunction) {
144144
* Type details:
145145
* * "list": the cursor is inside a "in" list of values (i.e. `a in (1, 2, <here>)`)
146146
* * "function": the cursor is inside a function call (i.e. `fn(<here>)`)
147-
* * "option": the cursor is inside a command option (i.e. `command ... by <here>`)
148-
* * "setting": the cursor is inside a setting (i.e. `command _<here>`)
149147
* * "expression": the cursor is inside a command expression (i.e. `command ... <here>` or `command a = ... <here>`)
150148
* * "newCommand": the cursor is at the beginning of a new command (i.e. `command1 | command2 | <here>`)
151149
*/
@@ -193,13 +191,6 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number)
193191
return { type: 'newCommand' as const, command: undefined, node, option };
194192
}
195193

196-
// TODO — remove this option branch once https://github.com/elastic/kibana/issues/195418 is complete
197-
if (command && isOptionItem(command.args[command.args.length - 1]) && command.name !== 'stats') {
198-
if (option) {
199-
return { type: 'option' as const, command, node, option };
200-
}
201-
}
202-
203194
// command a ... <here> OR command a = ... <here>
204195
return {
205196
type: 'expression' as const,

0 commit comments

Comments
 (0)