mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ES|QL] open the suggestion menu automatically in more places (#189585)
## Summary Closes https://github.com/elastic/kibana/issues/189662 ### Before https://github.com/user-attachments/assets/9f4e0fc6-1399-4b17-a151-753178e5ec7f ### After https://github.com/user-attachments/assets/45d59b63-3f18-457e-b4b9-1d98fa0ad8b1 | | Before | After | |------------------------------|------------------------------------------|-----------------------| | Source command | ✅ | ✅ | | Pipe command | ✅ | ✅ | | Function argument (when the minimum args aren't met) | ❌ | ✅ | | Pipe `\|` | ❌ | ✅ | | Assignment `var0 =` | ❌ | ✅ | | FROM source | ❌ | ✅ | | FROM source METADATA | ✅ | ✅ | | FROM source METADATA field | ❌ | ❌ | | FROM source METADATA field, | ❌ | ❌ | | EVAL argument | ❌ | ❌ | | DISSECT field | ❌ | ❌ | | DISSECT field pattern | ❌ | ❌ | | DISSECT field pattern APPEND_SEPARATOR | ✅ | ✅ | | DISSECT field pattern APPEND_SEPARATOR = separator | ❌ | ❌ | | DROP field | ❌ | ❌ | | DROP field1, field2 | ❌ | ❌ | | ENRICH policy | ❌ | ❌ | | ENRICH policy ON | ✅ | ✅ | | ENRICH policy ON field | ❌ | ❌ | | ENRICH policy WITH | ✅ | ✅ | | ENRICH policy WITH field | ❌ | ❌ | | GROK field | ❌ | ❌ | | GROK field pattern | ❌ | ❌ | | KEEP field | ❌ | ❌ | | KEEP field1, field2 | ❌ | ❌ | | LIMIT number | ❌ | ✅ | | MV_EXPAND field | ❌ | ❌ | | RENAME field | ❌ | ❌ | | RENAME field AS | ✅ |✅ | | RENAME field AS var0 | ❌ | ❌ | | SORT field | ❌ | ✅ | | SORT field order | ❌ | ✅ | | SORT field order nulls-order | ❌ | ✅ | | STATS argument | ✅ | ✅ | | STATS argument BY | ✅ | ✅ | | STATS argument BY expression | ❌ | ✅ | | WHERE argument | ❌ | ✅ | | WHERE argument comparison | ✅ | ✅ | | WHERE argument comparison argument | ❌ | ❌ | Also made a couple of improvements - Added support for Invoke completion triggers for subsequent function arguments (e.g. `date_diff("day", <cursor here>)`) - When you select a date in the date picker, your cursor is now advanced past the inserted date and the editor is refocused. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
parent
358e104ebc
commit
44c5f8ca80
10 changed files with 654 additions and 269 deletions
|
@ -14,6 +14,9 @@ const visibleIndices = indexes
|
|||
.map(({ name, suggestedAs }) => suggestedAs || name)
|
||||
.sort();
|
||||
|
||||
const addTrailingSpace = (strings: string[], predicate: (s: string) => boolean = (_s) => true) =>
|
||||
strings.map((string) => (predicate(string) ? `${string} ` : string));
|
||||
|
||||
const metadataFields = [...METADATA_FIELDS].sort();
|
||||
|
||||
describe('autocomplete.suggest', () => {
|
||||
|
@ -33,17 +36,17 @@ describe('autocomplete.suggest', () => {
|
|||
test('suggests visible indices on space', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
|
||||
await assertSuggestions('from /', visibleIndices);
|
||||
await assertSuggestions('FROM /', visibleIndices);
|
||||
await assertSuggestions('from /index', visibleIndices);
|
||||
await assertSuggestions('from /', addTrailingSpace(visibleIndices));
|
||||
await assertSuggestions('FROM /', addTrailingSpace(visibleIndices));
|
||||
await assertSuggestions('from /index', addTrailingSpace(visibleIndices));
|
||||
});
|
||||
|
||||
test('suggests visible indices on comma', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
|
||||
await assertSuggestions('FROM a,/', visibleIndices);
|
||||
await assertSuggestions('FROM a, /', visibleIndices);
|
||||
await assertSuggestions('from *,/', visibleIndices);
|
||||
await assertSuggestions('FROM a,/', addTrailingSpace(visibleIndices));
|
||||
await assertSuggestions('FROM a, /', addTrailingSpace(visibleIndices));
|
||||
await assertSuggestions('from *,/', addTrailingSpace(visibleIndices));
|
||||
});
|
||||
|
||||
test('can suggest integration data sources', async () => {
|
||||
|
@ -52,17 +55,21 @@ describe('autocomplete.suggest', () => {
|
|||
.filter(({ hidden }) => !hidden)
|
||||
.map(({ name, suggestedAs }) => suggestedAs || name)
|
||||
.sort();
|
||||
const expectedSuggestions = addTrailingSpace(
|
||||
visibleDataSources,
|
||||
(s) => !integrations.find(({ name }) => name === s)
|
||||
);
|
||||
const { assertSuggestions, callbacks } = await setup();
|
||||
const cb = {
|
||||
...callbacks,
|
||||
getSources: jest.fn().mockResolvedValue(dataSources),
|
||||
};
|
||||
|
||||
assertSuggestions('from /', visibleDataSources, { callbacks: cb });
|
||||
assertSuggestions('FROM /', visibleDataSources, { callbacks: cb });
|
||||
assertSuggestions('FROM a,/', visibleDataSources, { callbacks: cb });
|
||||
assertSuggestions('from a, /', visibleDataSources, { callbacks: cb });
|
||||
assertSuggestions('from *,/', visibleDataSources, { callbacks: cb });
|
||||
await assertSuggestions('from /', expectedSuggestions, { callbacks: cb });
|
||||
await assertSuggestions('FROM /', expectedSuggestions, { callbacks: cb });
|
||||
await assertSuggestions('FROM a,/', expectedSuggestions, { callbacks: cb });
|
||||
await assertSuggestions('from a, /', expectedSuggestions, { callbacks: cb });
|
||||
await assertSuggestions('from *,/', expectedSuggestions, { callbacks: cb });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -71,7 +78,7 @@ describe('autocomplete.suggest', () => {
|
|||
|
||||
test('on <kbd>SPACE</kbd> without comma ",", suggests adding metadata', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
const expected = ['METADATA $0', ',', '|'].sort();
|
||||
const expected = ['METADATA $0', ',', '| '].sort();
|
||||
|
||||
await assertSuggestions('from a, b /', expected);
|
||||
});
|
||||
|
@ -86,10 +93,10 @@ describe('autocomplete.suggest', () => {
|
|||
test('on <kbd>SPACE</kbd> after "METADATA" column suggests command and pipe operators', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
|
||||
await assertSuggestions('from a, b [metadata _index /]', [',', '|']);
|
||||
await assertSuggestions('from a, b metadata _index /', [',', '|']);
|
||||
await assertSuggestions('from a, b metadata _index, _source /', [',', '|']);
|
||||
await assertSuggestions(`from a, b metadata ${METADATA_FIELDS.join(', ')} /`, ['|']);
|
||||
await assertSuggestions('from a, b [metadata _index /]', [',', '| ']);
|
||||
await assertSuggestions('from a, b metadata _index /', [',', '| ']);
|
||||
await assertSuggestions('from a, b metadata _index, _source /', [',', '| ']);
|
||||
await assertSuggestions(`from a, b metadata ${METADATA_FIELDS.join(', ')} /`, ['| ']);
|
||||
});
|
||||
|
||||
test('filters out already used metadata fields', async () => {
|
||||
|
|
|
@ -45,7 +45,7 @@ describe('autocomplete.suggest', () => {
|
|||
describe('... <aggregates> ...', () => {
|
||||
test('lists possible aggregations on space after command', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
const expected = ['var0 =', ...allAggFunctions, ...allEvaFunctions];
|
||||
const expected = ['var0 = ', ...allAggFunctions, ...allEvaFunctions];
|
||||
|
||||
await assertSuggestions('from a | stats /', expected);
|
||||
await assertSuggestions('FROM a | STATS /', expected);
|
||||
|
@ -60,14 +60,14 @@ describe('autocomplete.suggest', () => {
|
|||
test('on space after aggregate field', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
|
||||
await assertSuggestions('from a | stats a=min(b) /', ['BY $0', ',', '|']);
|
||||
await assertSuggestions('from a | stats a=min(b) /', ['BY $0', ',', '| ']);
|
||||
});
|
||||
|
||||
test('on space after aggregate field with comma', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
|
||||
await assertSuggestions('from a | stats a=max(b), /', [
|
||||
'var0 =',
|
||||
'var0 = ',
|
||||
...allAggFunctions,
|
||||
...allEvaFunctions,
|
||||
]);
|
||||
|
@ -78,7 +78,7 @@ describe('autocomplete.suggest', () => {
|
|||
|
||||
await assertSuggestions('from a | stats by bucket(/', [
|
||||
...getFieldNamesByType([...ESQL_COMMON_NUMERIC_TYPES, 'date']).map(
|
||||
(field) => `${field},`
|
||||
(field) => `${field}, `
|
||||
),
|
||||
...getFunctionSignaturesByReturnType('eval', ['date', ...ESQL_COMMON_NUMERIC_TYPES], {
|
||||
scalar: true,
|
||||
|
@ -172,21 +172,21 @@ describe('autocomplete.suggest', () => {
|
|||
test('when typing right paren', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
|
||||
await assertSuggestions('from a | stats a = min(b)/ | sort b', ['BY $0', ',', '|']);
|
||||
await assertSuggestions('from a | stats a = min(b)/ | sort b', ['BY $0', ',', '| ']);
|
||||
});
|
||||
|
||||
test('increments suggested variable name counter', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
|
||||
await assertSuggestions('from a | eval var0=round(b), var1=round(c) | stats /', [
|
||||
'var2 =',
|
||||
'var2 = ',
|
||||
...allAggFunctions,
|
||||
'var0',
|
||||
'var1',
|
||||
...allEvaFunctions,
|
||||
]);
|
||||
await assertSuggestions('from a | stats var0=min(b),var1=c,/', [
|
||||
'var2 =',
|
||||
'var2 = ',
|
||||
...allAggFunctions,
|
||||
...allEvaFunctions,
|
||||
]);
|
||||
|
@ -197,8 +197,8 @@ describe('autocomplete.suggest', () => {
|
|||
test('on space after "BY" keyword', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
const expected = [
|
||||
'var0 =',
|
||||
...getFieldNamesByType('any'),
|
||||
'var0 = ',
|
||||
...getFieldNamesByType('any').map((field) => `${field} `),
|
||||
...allEvaFunctions,
|
||||
...allGroupingFunctions,
|
||||
];
|
||||
|
@ -211,26 +211,27 @@ describe('autocomplete.suggest', () => {
|
|||
test('on space after grouping field', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
|
||||
await assertSuggestions('from a | stats a=c by d /', [',', '|']);
|
||||
await assertSuggestions('from a | stats a=c by d /', [',', '| ']);
|
||||
});
|
||||
|
||||
test('after comma "," in grouping fields', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
|
||||
const fields = getFieldNamesByType('any').map((field) => `${field} `);
|
||||
await assertSuggestions('from a | stats a=c by d, /', [
|
||||
'var0 =',
|
||||
...getFieldNamesByType('any'),
|
||||
'var0 = ',
|
||||
...fields,
|
||||
...allEvaFunctions,
|
||||
...allGroupingFunctions,
|
||||
]);
|
||||
await assertSuggestions('from a | stats a=min(b),/', [
|
||||
'var0 =',
|
||||
'var0 = ',
|
||||
...allAggFunctions,
|
||||
...allEvaFunctions,
|
||||
]);
|
||||
await assertSuggestions('from a | stats avg(b) by c, /', [
|
||||
'var0 =',
|
||||
...getFieldNamesByType('any'),
|
||||
'var0 = ',
|
||||
...fields,
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
|
||||
...allGroupingFunctions,
|
||||
]);
|
||||
|
@ -251,12 +252,12 @@ describe('autocomplete.suggest', () => {
|
|||
...allGroupingFunctions,
|
||||
]);
|
||||
await assertSuggestions('from a | stats avg(b) by var0 = /', [
|
||||
...getFieldNamesByType('any'),
|
||||
...getFieldNamesByType('any').map((field) => `${field} `),
|
||||
...allEvaFunctions,
|
||||
...allGroupingFunctions,
|
||||
]);
|
||||
await assertSuggestions('from a | stats avg(b) by c, var0 = /', [
|
||||
...getFieldNamesByType('any'),
|
||||
...getFieldNamesByType('any').map((field) => `${field} `),
|
||||
...allEvaFunctions,
|
||||
...allGroupingFunctions,
|
||||
]);
|
||||
|
@ -265,11 +266,11 @@ describe('autocomplete.suggest', () => {
|
|||
test('on space after expression right hand side operand', async () => {
|
||||
const { assertSuggestions } = await setup();
|
||||
|
||||
await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [',', '|']);
|
||||
await assertSuggestions('from a | stats avg(b) by doubleField % 2 /', [',', '| ']);
|
||||
|
||||
await assertSuggestions(
|
||||
'from a | stats var0 = AVG(doubleField) BY var1 = BUCKET(dateField, 1 day)/',
|
||||
[',', '|', '+ $0', '- $0']
|
||||
[',', '| ', '+ $0', '- $0']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,6 +41,7 @@ export const triggerCharacters = [',', '(', '=', ' '];
|
|||
export const fields: Array<{ name: string; type: string; suggestedAs?: string }> = [
|
||||
...[
|
||||
'string',
|
||||
'keyword',
|
||||
'double',
|
||||
'date',
|
||||
'boolean',
|
||||
|
|
|
@ -10,7 +10,12 @@ import { suggest } from './autocomplete';
|
|||
import { evalFunctionDefinitions } from '../definitions/functions';
|
||||
import { timeUnitsToSuggest } from '../definitions/literals';
|
||||
import { commandDefinitions } from '../definitions/commands';
|
||||
import { getSafeInsertText, getUnitDuration, TRIGGER_SUGGESTION_COMMAND } from './factories';
|
||||
import {
|
||||
getSafeInsertText,
|
||||
getUnitDuration,
|
||||
TIME_SYSTEM_PARAMS,
|
||||
TRIGGER_SUGGESTION_COMMAND,
|
||||
} from './factories';
|
||||
import { camelCase, partition } from 'lodash';
|
||||
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
|
||||
import { FunctionParameter, FunctionReturnType } from '../definitions/types';
|
||||
|
@ -26,6 +31,7 @@ import {
|
|||
createCompletionContext,
|
||||
getPolicyFields,
|
||||
PartialSuggestionWithText,
|
||||
TIME_PICKER_SUGGESTION,
|
||||
} from './__tests__/helpers';
|
||||
import { METADATA_FIELDS } from '../shared/constants';
|
||||
import {
|
||||
|
@ -143,7 +149,7 @@ describe('autocomplete', () => {
|
|||
describe('show', () => {
|
||||
testSuggestions('show ', ['INFO']);
|
||||
for (const fn of ['info']) {
|
||||
testSuggestions(`show ${fn} `, ['|']);
|
||||
testSuggestions(`show ${fn} `, ['| ']);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -151,9 +157,12 @@ describe('autocomplete', () => {
|
|||
const allEvalFns = getFunctionSignaturesByReturnType('where', 'any', {
|
||||
scalar: true,
|
||||
});
|
||||
testSuggestions('from a | where ', [...getFieldNamesByType('any'), ...allEvalFns]);
|
||||
testSuggestions('from a | where ', [
|
||||
...getFieldNamesByType('any').map((field) => `${field} `),
|
||||
...allEvalFns,
|
||||
]);
|
||||
testSuggestions('from a | eval var0 = 1 | where ', [
|
||||
...getFieldNamesByType('any'),
|
||||
...getFieldNamesByType('any').map((name) => `${name} `),
|
||||
'var0',
|
||||
...allEvalFns,
|
||||
]);
|
||||
|
@ -172,6 +181,14 @@ describe('autocomplete', () => {
|
|||
...getFieldNamesByType('any'),
|
||||
...getFunctionSignaturesByReturnType('where', ['any'], { scalar: true }),
|
||||
]);
|
||||
|
||||
testSuggestions('from a | where dateField >= ', [
|
||||
TIME_PICKER_SUGGESTION,
|
||||
...TIME_SYSTEM_PARAMS,
|
||||
...getFieldNamesByType('date'),
|
||||
...getFunctionSignaturesByReturnType('where', ['date'], { scalar: true }),
|
||||
]);
|
||||
|
||||
// Skip these tests until the insensitive case equality gets restored back
|
||||
testSuggestions.skip('from a | where stringField =~ ', [
|
||||
...getFieldNamesByType('string'),
|
||||
|
@ -182,7 +199,7 @@ describe('autocomplete', () => {
|
|||
...getFunctionSignaturesByReturnType('where', 'any', { scalar: true }),
|
||||
]);
|
||||
testSuggestions.skip('from a | where stringField =~ stringField ', [
|
||||
'|',
|
||||
'| ',
|
||||
...getFunctionSignaturesByReturnType(
|
||||
'where',
|
||||
'boolean',
|
||||
|
@ -307,7 +324,7 @@ describe('autocomplete', () => {
|
|||
for (const subExpression of subExpressions) {
|
||||
testSuggestions(`from a | ${subExpression} grok `, getFieldNamesByType('string'));
|
||||
testSuggestions(`from a | ${subExpression} grok stringField `, [constantPattern], ' ');
|
||||
testSuggestions(`from a | ${subExpression} grok stringField ${constantPattern} `, ['|']);
|
||||
testSuggestions(`from a | ${subExpression} grok stringField ${constantPattern} `, ['| ']);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -324,7 +341,7 @@ describe('autocomplete', () => {
|
|||
testSuggestions(`from a | ${subExpression} dissect stringField `, [constantPattern], ' ');
|
||||
testSuggestions(
|
||||
`from a | ${subExpression} dissect stringField ${constantPattern} `,
|
||||
['APPEND_SEPARATOR = $0', '|'],
|
||||
['APPEND_SEPARATOR = $0', '| '],
|
||||
' '
|
||||
);
|
||||
testSuggestions(
|
||||
|
@ -333,30 +350,30 @@ describe('autocomplete', () => {
|
|||
);
|
||||
testSuggestions(
|
||||
`from a | ${subExpression} dissect stringField ${constantPattern} append_separator = ":" `,
|
||||
['|']
|
||||
['| ']
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
describe('sort', () => {
|
||||
testSuggestions('from a | sort ', [
|
||||
...getFieldNamesByType('any'),
|
||||
...getFieldNamesByType('any').map((name) => `${name} `),
|
||||
...getFunctionSignaturesByReturnType('sort', 'any', { scalar: true }),
|
||||
]);
|
||||
testSuggestions('from a | sort stringField ', ['ASC', 'DESC', ',', '|']);
|
||||
testSuggestions('from a | sort stringField desc ', ['NULLS FIRST', 'NULLS LAST', ',', '|']);
|
||||
testSuggestions('from a | sort stringField ', ['ASC ', 'DESC ', ',', '| ']);
|
||||
testSuggestions('from a | sort stringField desc ', ['NULLS FIRST ', 'NULLS LAST ', ',', '| ']);
|
||||
// @TODO: improve here
|
||||
// testSuggestions('from a | sort stringField desc ', ['first', 'last']);
|
||||
});
|
||||
|
||||
describe('limit', () => {
|
||||
testSuggestions('from a | limit ', ['10', '100', '1000']);
|
||||
testSuggestions('from a | limit 4 ', ['|']);
|
||||
testSuggestions('from a | limit ', ['10 ', '100 ', '1000 ']);
|
||||
testSuggestions('from a | limit 4 ', ['| ']);
|
||||
});
|
||||
|
||||
describe('mv_expand', () => {
|
||||
testSuggestions('from a | mv_expand ', getFieldNamesByType('any'));
|
||||
testSuggestions('from a | mv_expand a ', ['|']);
|
||||
testSuggestions('from a | mv_expand a ', ['| ']);
|
||||
});
|
||||
|
||||
describe('rename', () => {
|
||||
|
@ -413,8 +430,9 @@ describe('autocomplete', () => {
|
|||
testSuggestions(`from a ${prevCommand}| enrich _${mode.toUpperCase()}:`, policyNames, ':');
|
||||
testSuggestions(`from a ${prevCommand}| enrich _${camelCase(mode)}:`, policyNames, ':');
|
||||
}
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy `, ['ON $0', 'WITH $0', '|']);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy `, ['ON $0', 'WITH $0', '| ']);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy on `, [
|
||||
'keywordField',
|
||||
'stringField',
|
||||
'doubleField',
|
||||
'dateField',
|
||||
|
@ -427,28 +445,28 @@ describe('autocomplete', () => {
|
|||
'`any#Char$Field`',
|
||||
'kubernetes.something.something',
|
||||
]);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy on b `, ['WITH $0', ',', '|']);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy on b `, ['WITH $0', ',', '| ']);
|
||||
testSuggestions(
|
||||
`from a ${prevCommand}| enrich policy on b with `,
|
||||
['var0 =', ...getPolicyFields('policy')],
|
||||
['var0 = ', ...getPolicyFields('policy')],
|
||||
' '
|
||||
);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 `, ['= $0', ',', '|']);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 `, ['= $0', ',', '| ']);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = `, [
|
||||
...getPolicyFields('policy'),
|
||||
]);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = stringField `, [
|
||||
',',
|
||||
'|',
|
||||
'| ',
|
||||
]);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = stringField, `, [
|
||||
'var1 =',
|
||||
'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 = `,
|
||||
|
@ -456,16 +474,20 @@ describe('autocomplete', () => {
|
|||
);
|
||||
testSuggestions(
|
||||
`from a ${prevCommand}| enrich policy with `,
|
||||
['var0 =', ...getPolicyFields('policy')],
|
||||
['var0 = ', ...getPolicyFields('policy')],
|
||||
' '
|
||||
);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy with stringField `, ['= $0', ',', '|']);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy with stringField `, [
|
||||
'= $0',
|
||||
',',
|
||||
'| ',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
describe('eval', () => {
|
||||
testSuggestions('from a | eval ', [
|
||||
'var0 =',
|
||||
'var0 = ',
|
||||
...getFieldNamesByType('any'),
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
|
||||
]);
|
||||
|
@ -474,7 +496,7 @@ describe('autocomplete', () => {
|
|||
'double',
|
||||
]),
|
||||
',',
|
||||
'|',
|
||||
'| ',
|
||||
]);
|
||||
testSuggestions('from index | EVAL stringField not ', ['LIKE $0', 'RLIKE $0', 'IN $0']);
|
||||
testSuggestions('from index | EVAL stringField NOT ', ['LIKE $0', 'RLIKE $0', 'IN $0']);
|
||||
|
@ -499,7 +521,7 @@ describe('autocomplete', () => {
|
|||
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
|
||||
]);
|
||||
testSuggestions('from a | eval a=doubleField, ', [
|
||||
'var0 =',
|
||||
'var0 = ',
|
||||
...getFieldNamesByType('any'),
|
||||
'a',
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
|
||||
|
@ -548,7 +570,7 @@ describe('autocomplete', () => {
|
|||
);
|
||||
testSuggestions('from a | eval a=round(doubleField) ', [
|
||||
',',
|
||||
'|',
|
||||
'| ',
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [
|
||||
'double',
|
||||
]),
|
||||
|
@ -573,7 +595,7 @@ describe('autocomplete', () => {
|
|||
' '
|
||||
);
|
||||
testSuggestions('from a | eval a=round(doubleField),', [
|
||||
'var0 =',
|
||||
'var0 = ',
|
||||
...getFieldNamesByType('any'),
|
||||
'a',
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
|
||||
|
@ -597,7 +619,7 @@ describe('autocomplete', () => {
|
|||
testSuggestions(
|
||||
'from a | stats avg(doubleField) by stringField | eval ',
|
||||
[
|
||||
'var0 =',
|
||||
'var0 = ',
|
||||
'`avg(doubleField)`',
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
|
||||
],
|
||||
|
@ -609,7 +631,7 @@ describe('autocomplete', () => {
|
|||
testSuggestions(
|
||||
'from a | eval abs(doubleField) + 1 | eval ',
|
||||
[
|
||||
'var0 =',
|
||||
'var0 = ',
|
||||
...getFieldNamesByType('any'),
|
||||
'`abs(doubleField) + 1`',
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
|
||||
|
@ -619,7 +641,7 @@ describe('autocomplete', () => {
|
|||
testSuggestions(
|
||||
'from a | stats avg(doubleField) by stringField | eval ',
|
||||
[
|
||||
'var0 =',
|
||||
'var0 = ',
|
||||
'`avg(doubleField)`',
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
|
||||
],
|
||||
|
@ -631,7 +653,7 @@ describe('autocomplete', () => {
|
|||
testSuggestions(
|
||||
'from a | stats avg(doubleField), avg(kubernetes.something.something) by stringField | eval ',
|
||||
[
|
||||
'var0 =',
|
||||
'var0 = ',
|
||||
'`avg(doubleField)`',
|
||||
'`avg(kubernetes.something.something)`',
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
|
||||
|
@ -664,7 +686,7 @@ describe('autocomplete', () => {
|
|||
);
|
||||
// test that comma is correctly added to the suggestions if minParams is not reached yet
|
||||
testSuggestions('from a | eval a=concat( ', [
|
||||
...getFieldNamesByType(['text', 'keyword']).map((v) => `${v},`),
|
||||
...getFieldNamesByType(['text', 'keyword']).map((v) => `${v}, `),
|
||||
...getFunctionSignaturesByReturnType(
|
||||
'eval',
|
||||
['text', 'keyword'],
|
||||
|
@ -691,7 +713,7 @@ describe('autocomplete', () => {
|
|||
testSuggestions(
|
||||
'from a | eval a=cidr_match(ipField, textField, ',
|
||||
[
|
||||
...getFieldNamesByType('text'),
|
||||
...getFieldNamesByType('keyword'),
|
||||
...getFunctionSignaturesByReturnType(
|
||||
'eval',
|
||||
['text', 'keyword'],
|
||||
|
@ -704,7 +726,7 @@ describe('autocomplete', () => {
|
|||
);
|
||||
// test that comma is correctly added to the suggestions if minParams is not reached yet
|
||||
testSuggestions('from a | eval a=cidr_match( ', [
|
||||
...getFieldNamesByType('ip').map((v) => `${v},`),
|
||||
...getFieldNamesByType('ip').map((v) => `${v}, `),
|
||||
...getFunctionSignaturesByReturnType('eval', 'ip', { scalar: true }, undefined, [
|
||||
'cidr_match',
|
||||
]).map((v) => ({ ...v, text: `${v.text},` })),
|
||||
|
@ -749,7 +771,7 @@ describe('autocomplete', () => {
|
|||
'from a | eval var0 = abs(doubleField) | eval abs(var0)',
|
||||
[
|
||||
',',
|
||||
'|',
|
||||
'| ',
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [
|
||||
'double',
|
||||
]),
|
||||
|
@ -807,13 +829,13 @@ describe('autocomplete', () => {
|
|||
if (!requiresMoreArgs || s === '' || (typeof s === 'object' && s.text === '')) {
|
||||
return s;
|
||||
}
|
||||
return typeof s === 'string' ? `${s},` : { ...s, text: `${s.text},` };
|
||||
return typeof s === 'string' ? `${s}, ` : { ...s, text: `${s.text},` };
|
||||
};
|
||||
|
||||
testSuggestions(
|
||||
`from a | eval ${fn.name}(${Array(i).fill('field').join(', ')}${i ? ',' : ''} )`,
|
||||
suggestedConstants?.length
|
||||
? suggestedConstants.map((option) => `"${option}"${requiresMoreArgs ? ',' : ''}`)
|
||||
? suggestedConstants.map((option) => `"${option}"${requiresMoreArgs ? ', ' : ''}`)
|
||||
: [
|
||||
...getDateLiteralsByFieldType(getTypesFromParamDefs(acceptsFieldParamDefs)),
|
||||
...getFieldNamesByType(getTypesFromParamDefs(acceptsFieldParamDefs)),
|
||||
|
@ -833,7 +855,7 @@ describe('autocomplete', () => {
|
|||
i ? ',' : ''
|
||||
} )`,
|
||||
suggestedConstants?.length
|
||||
? suggestedConstants.map((option) => `"${option}"${requiresMoreArgs ? ',' : ''}`)
|
||||
? suggestedConstants.map((option) => `"${option}"${requiresMoreArgs ? ', ' : ''}`)
|
||||
: [
|
||||
...getDateLiteralsByFieldType(getTypesFromParamDefs(acceptsFieldParamDefs)),
|
||||
...getFieldNamesByType(getTypesFromParamDefs(acceptsFieldParamDefs)),
|
||||
|
@ -865,7 +887,7 @@ describe('autocomplete', () => {
|
|||
testSuggestions(
|
||||
`from a | eval ${fn.name}(`,
|
||||
suggestedConstants?.length
|
||||
? [...suggestedConstants.map((option) => `"${option}"${requiresMoreArgs ? ',' : ''}`)]
|
||||
? [...suggestedConstants.map((option) => `"${option}"${requiresMoreArgs ? ', ' : ''}`)]
|
||||
: []
|
||||
);
|
||||
}
|
||||
|
@ -881,7 +903,7 @@ describe('autocomplete', () => {
|
|||
[
|
||||
...dateSuggestions,
|
||||
',',
|
||||
'|',
|
||||
'| ',
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [
|
||||
'integer',
|
||||
]),
|
||||
|
@ -890,12 +912,12 @@ describe('autocomplete', () => {
|
|||
);
|
||||
testSuggestions('from a | eval a = 1 year ', [
|
||||
',',
|
||||
'|',
|
||||
'| ',
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { builtin: true, skipAssign: true }, [
|
||||
'time_interval',
|
||||
]),
|
||||
]);
|
||||
testSuggestions('from a | eval a = 1 day + 2 ', [',', '|']);
|
||||
testSuggestions('from a | eval a = 1 day + 2 ', [',', '| ']);
|
||||
testSuggestions(
|
||||
'from a | eval 1 day + 2 ',
|
||||
[
|
||||
|
@ -908,19 +930,19 @@ describe('autocomplete', () => {
|
|||
);
|
||||
testSuggestions(
|
||||
'from a | eval var0=date_trunc()',
|
||||
[...getLiteralsByType('time_literal').map((t) => `${t},`)],
|
||||
getLiteralsByType('time_literal').map((t) => `${t}, `),
|
||||
'('
|
||||
);
|
||||
testSuggestions(
|
||||
'from a | eval var0=date_trunc(2 )',
|
||||
[...dateSuggestions.map((t) => `${t},`), ','],
|
||||
[...dateSuggestions.map((t) => `${t}, `), ','],
|
||||
' '
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('values suggestions', () => {
|
||||
testSuggestions('FROM "a"', ['a', 'b'], undefined, 7, [
|
||||
testSuggestions('FROM "a"', ['a ', 'b '], undefined, 7, [
|
||||
,
|
||||
[
|
||||
{ name: 'a', hidden: false },
|
||||
|
@ -975,50 +997,6 @@ describe('autocomplete', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('auto triggers', () => {
|
||||
function getSuggestionsFor(statement: string) {
|
||||
const callbackMocks = createCustomCallbackMocks(undefined, undefined, undefined);
|
||||
const triggerOffset = statement.lastIndexOf(' ') + 1; // drop <here>
|
||||
const context = createCompletionContext(statement[triggerOffset]);
|
||||
return suggest(
|
||||
statement,
|
||||
triggerOffset + 1,
|
||||
context,
|
||||
async (text) => (text ? getAstAndSyntaxErrors(text) : { ast: [], errors: [] }),
|
||||
callbackMocks
|
||||
);
|
||||
}
|
||||
it('should trigger further suggestions for functions', async () => {
|
||||
const suggestions = await getSuggestionsFor('from a | eval ');
|
||||
// test that all functions will retrigger suggestions
|
||||
expect(
|
||||
suggestions
|
||||
.filter(({ kind }) => kind === 'Function')
|
||||
.every(({ command }) => command === TRIGGER_SUGGESTION_COMMAND)
|
||||
).toBeTruthy();
|
||||
// now test that non-function won't retrigger
|
||||
expect(
|
||||
suggestions
|
||||
.filter(({ kind }) => kind !== 'Function')
|
||||
.every(({ command }) => command == null)
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('should trigger further suggestions for commands', async () => {
|
||||
const suggestions = await getSuggestionsFor('from a | ');
|
||||
// test that all commands will retrigger suggestions
|
||||
expect(
|
||||
suggestions.every(({ command }) => command === TRIGGER_SUGGESTION_COMMAND)
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('should trigger further suggestions after enrich mode', async () => {
|
||||
const suggestions = await getSuggestionsFor('from a | enrich _any:');
|
||||
// test that all commands will retrigger suggestions
|
||||
expect(
|
||||
suggestions.every(({ command }) => command === TRIGGER_SUGGESTION_COMMAND)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Monaco asks for suggestions in at least two different scenarios.
|
||||
* 1. When the user types a non-whitespace character (e.g. 'FROM k') - this is the Invoke trigger kind
|
||||
|
@ -1049,24 +1027,47 @@ describe('autocomplete', () => {
|
|||
10
|
||||
);
|
||||
|
||||
// function argument
|
||||
testSuggestions(
|
||||
'FROM kibana_sample_data_logs | EVAL TRIM(e)',
|
||||
[
|
||||
...getFunctionSignaturesByReturnType(
|
||||
'eval',
|
||||
['text', 'keyword'],
|
||||
{ scalar: true },
|
||||
undefined,
|
||||
['trim']
|
||||
),
|
||||
],
|
||||
undefined,
|
||||
42
|
||||
);
|
||||
describe('function arguments', () => {
|
||||
// function argument
|
||||
testSuggestions(
|
||||
'FROM kibana_sample_data_logs | EVAL TRIM(e)',
|
||||
[
|
||||
...getFieldNamesByType(['text', 'keyword']),
|
||||
...getFunctionSignaturesByReturnType(
|
||||
'eval',
|
||||
['text', 'keyword'],
|
||||
{ scalar: true },
|
||||
undefined,
|
||||
['trim']
|
||||
),
|
||||
],
|
||||
undefined,
|
||||
42
|
||||
);
|
||||
|
||||
// subsequent function argument
|
||||
const expectedDateDiff2ndArgSuggestions = [
|
||||
TIME_PICKER_SUGGESTION,
|
||||
...TIME_SYSTEM_PARAMS.map((t) => `${t}, `),
|
||||
...getFieldNamesByType('date').map((name) => `${name}, `),
|
||||
...getFunctionSignaturesByReturnType('eval', 'date', { scalar: true }).map((s) => ({
|
||||
...s,
|
||||
text: `${s.text},`,
|
||||
})),
|
||||
];
|
||||
testSuggestions(
|
||||
'FROM a | EVAL DATE_DIFF("day", )',
|
||||
expectedDateDiff2ndArgSuggestions,
|
||||
undefined,
|
||||
31
|
||||
);
|
||||
|
||||
// trigger character case for comparison
|
||||
testSuggestions('FROM a | EVAL DATE_DIFF("day", )', expectedDateDiff2ndArgSuggestions, ' ');
|
||||
});
|
||||
|
||||
// FROM source
|
||||
testSuggestions('FROM k', ['index1', 'index2'], undefined, 6, [
|
||||
testSuggestions('FROM k', ['index1 ', 'index2 '], undefined, 6, [
|
||||
,
|
||||
[
|
||||
{ name: 'index1', hidden: false },
|
||||
|
@ -1075,7 +1076,7 @@ describe('autocomplete', () => {
|
|||
]);
|
||||
|
||||
// FROM source METADATA
|
||||
testSuggestions('FROM index1 M', [',', 'METADATA $0', '|'], undefined, 13);
|
||||
testSuggestions('FROM index1 M', [',', 'METADATA $0', '| '], undefined, 13);
|
||||
|
||||
// FROM source METADATA field
|
||||
testSuggestions('FROM index1 METADATA _', METADATA_FIELDS, undefined, 22);
|
||||
|
@ -1084,7 +1085,7 @@ describe('autocomplete', () => {
|
|||
testSuggestions(
|
||||
'FROM index1 | EVAL b',
|
||||
[
|
||||
'var0 =',
|
||||
'var0 = ',
|
||||
...getFieldNamesByType('any'),
|
||||
...getFunctionSignaturesByReturnType('eval', 'any', { scalar: true }),
|
||||
],
|
||||
|
@ -1117,7 +1118,7 @@ describe('autocomplete', () => {
|
|||
);
|
||||
|
||||
// ENRICH policy ON
|
||||
testSuggestions('FROM index1 | ENRICH policy O', ['ON $0', 'WITH $0', '|'], undefined, 29);
|
||||
testSuggestions('FROM index1 | ENRICH policy O', ['ON $0', 'WITH $0', '| '], undefined, 29);
|
||||
|
||||
// ENRICH policy ON field
|
||||
testSuggestions('FROM index1 | ENRICH policy ON f', getFieldNamesByType('any'), undefined, 32);
|
||||
|
@ -1125,14 +1126,14 @@ describe('autocomplete', () => {
|
|||
// ENRICH policy WITH policyfield
|
||||
testSuggestions(
|
||||
'FROM index1 | ENRICH policy WITH v',
|
||||
['var0 =', ...getPolicyFields('policy')],
|
||||
['var0 = ', ...getPolicyFields('policy')],
|
||||
undefined,
|
||||
34
|
||||
);
|
||||
|
||||
testSuggestions(
|
||||
'FROM index1 | ENRICH policy WITH \tv',
|
||||
['var0 =', ...getPolicyFields('policy')],
|
||||
['var0 = ', ...getPolicyFields('policy')],
|
||||
undefined,
|
||||
34
|
||||
);
|
||||
|
@ -1154,7 +1155,7 @@ describe('autocomplete', () => {
|
|||
// LIMIT argument
|
||||
// Here we actually test that the invoke trigger kind does not work
|
||||
// because it isn't very useful to see literal suggestions when typing a number
|
||||
testSuggestions('FROM a | LIMIT 1', ['|'], undefined, 16);
|
||||
testSuggestions('FROM a | LIMIT 1', ['| '], undefined, 16);
|
||||
|
||||
// MV_EXPAND field
|
||||
testSuggestions('FROM index1 | MV_EXPAND f', getFieldNamesByType('any'), undefined, 25);
|
||||
|
@ -1173,19 +1174,24 @@ describe('autocomplete', () => {
|
|||
'FROM index1 | SORT f',
|
||||
[
|
||||
...getFunctionSignaturesByReturnType('sort', 'any', { scalar: true }),
|
||||
...getFieldNamesByType('any'),
|
||||
...getFieldNamesByType('any').map((field) => `${field} `),
|
||||
],
|
||||
undefined,
|
||||
20
|
||||
);
|
||||
|
||||
// SORT field order
|
||||
testSuggestions('FROM index1 | SORT stringField a', ['ASC', 'DESC', ',', '|'], undefined, 32);
|
||||
testSuggestions(
|
||||
'FROM index1 | SORT stringField a',
|
||||
['ASC ', 'DESC ', ',', '| '],
|
||||
undefined,
|
||||
32
|
||||
);
|
||||
|
||||
// SORT field order nulls
|
||||
testSuggestions(
|
||||
'FROM index1 | SORT stringField ASC n',
|
||||
['NULLS FIRST', 'NULLS LAST', ',', '|'],
|
||||
['NULLS FIRST ', 'NULLS LAST ', ',', '| '],
|
||||
undefined,
|
||||
36
|
||||
);
|
||||
|
@ -1193,21 +1199,24 @@ describe('autocomplete', () => {
|
|||
// STATS argument
|
||||
testSuggestions(
|
||||
'FROM index1 | STATS f',
|
||||
['var0 =', ...getFunctionSignaturesByReturnType('stats', 'any', { scalar: true, agg: true })],
|
||||
[
|
||||
'var0 = ',
|
||||
...getFunctionSignaturesByReturnType('stats', 'any', { scalar: true, agg: true }),
|
||||
],
|
||||
undefined,
|
||||
21
|
||||
);
|
||||
|
||||
// STATS argument BY
|
||||
testSuggestions('FROM index1 | STATS AVG(booleanField) B', ['BY $0', ',', '|'], undefined, 39);
|
||||
testSuggestions('FROM index1 | STATS AVG(booleanField) B', ['BY $0', ',', '| '], undefined, 39);
|
||||
|
||||
// STATS argument BY expression
|
||||
testSuggestions(
|
||||
'FROM index1 | STATS field BY f',
|
||||
[
|
||||
'var0 =',
|
||||
'var0 = ',
|
||||
...getFunctionSignaturesByReturnType('stats', 'any', { grouping: true, scalar: true }),
|
||||
...getFieldNamesByType('any'),
|
||||
...getFieldNamesByType('any').map((field) => `${field} `),
|
||||
],
|
||||
undefined,
|
||||
30
|
||||
|
@ -1217,7 +1226,7 @@ describe('autocomplete', () => {
|
|||
testSuggestions(
|
||||
'FROM index1 | WHERE f',
|
||||
[
|
||||
...getFieldNamesByType('any'),
|
||||
...getFieldNamesByType('any').map((field) => `${field} `),
|
||||
...getFunctionSignaturesByReturnType('where', 'any', { scalar: true }),
|
||||
],
|
||||
undefined,
|
||||
|
@ -1239,4 +1248,264 @@ describe('autocomplete', () => {
|
|||
33
|
||||
);
|
||||
});
|
||||
|
||||
describe('advancing the cursor and opening the suggestion menu automatically ✨', () => {
|
||||
const attachTriggerCommand = (
|
||||
s: string | PartialSuggestionWithText
|
||||
): PartialSuggestionWithText =>
|
||||
typeof s === 'string'
|
||||
? {
|
||||
text: s,
|
||||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
}
|
||||
: { ...s, command: TRIGGER_SUGGESTION_COMMAND };
|
||||
|
||||
const attachAsSnippet = (s: PartialSuggestionWithText): PartialSuggestionWithText => ({
|
||||
...s,
|
||||
asSnippet: true,
|
||||
});
|
||||
|
||||
// Source command
|
||||
testSuggestions(
|
||||
'F',
|
||||
['FROM $0', 'ROW $0', 'SHOW $0'].map(attachTriggerCommand).map(attachAsSnippet),
|
||||
undefined,
|
||||
1
|
||||
);
|
||||
|
||||
// Pipe command
|
||||
testSuggestions(
|
||||
'FROM a | E',
|
||||
commandDefinitions
|
||||
.filter(({ name }) => !sourceCommands.includes(name))
|
||||
.map(({ name }) => attachTriggerCommand(name.toUpperCase() + ' $0'))
|
||||
.map(attachAsSnippet), // TODO consider making this check more fundamental
|
||||
undefined,
|
||||
10
|
||||
);
|
||||
|
||||
describe('function arguments', () => {
|
||||
// literalSuggestions parameter
|
||||
const dateDiffFirstParamSuggestions =
|
||||
evalFunctionDefinitions.find(({ name }) => name === 'date_diff')?.signatures[0].params?.[0]
|
||||
.literalSuggestions ?? [];
|
||||
testSuggestions(
|
||||
'FROM a | EVAL DATE_DIFF()',
|
||||
dateDiffFirstParamSuggestions.map((s) => `"${s}", `).map(attachTriggerCommand),
|
||||
undefined,
|
||||
24
|
||||
);
|
||||
|
||||
// field parameter
|
||||
|
||||
const expectedStringSuggestionsWhenMoreArgsAreNeeded = [
|
||||
...getFieldNamesByType('keyword')
|
||||
.map((field) => `${field}, `)
|
||||
.map(attachTriggerCommand),
|
||||
...getFunctionSignaturesByReturnType('eval', 'keyword', { scalar: true }, undefined, [
|
||||
'replace',
|
||||
]).map((s) => ({
|
||||
...s,
|
||||
text: `${s.text},`,
|
||||
})),
|
||||
];
|
||||
|
||||
testSuggestions(
|
||||
'FROM a | EVAL REPLACE()',
|
||||
expectedStringSuggestionsWhenMoreArgsAreNeeded,
|
||||
undefined,
|
||||
22
|
||||
);
|
||||
|
||||
// subsequent parameter
|
||||
testSuggestions(
|
||||
'FROM a | EVAL REPLACE(stringField, )',
|
||||
expectedStringSuggestionsWhenMoreArgsAreNeeded,
|
||||
undefined,
|
||||
35
|
||||
);
|
||||
|
||||
// final parameter — should not advance!
|
||||
testSuggestions(
|
||||
'FROM a | EVAL REPLACE(stringField, stringField, )',
|
||||
[
|
||||
...getFieldNamesByType('keyword').map((field) => ({ text: field, command: undefined })),
|
||||
...getFunctionSignaturesByReturnType('eval', 'keyword', { scalar: true }, undefined, [
|
||||
'replace',
|
||||
]),
|
||||
],
|
||||
undefined,
|
||||
48
|
||||
);
|
||||
|
||||
// Trigger character because this is how it will actually be... the user will press
|
||||
// space-bar... this may change if we fix the tokenization of timespan literals
|
||||
// such that "2 days" is a single monaco token
|
||||
testSuggestions(
|
||||
'FROM a | EVAL DATE_TRUNC(2 )',
|
||||
[...timeUnitsToSuggest.map((s) => `${s.name}, `).map(attachTriggerCommand), ','],
|
||||
' '
|
||||
);
|
||||
});
|
||||
|
||||
// PIPE (|)
|
||||
testSuggestions(
|
||||
'FROM a ',
|
||||
[attachTriggerCommand('| '), ',', attachAsSnippet(attachTriggerCommand('METADATA $0'))],
|
||||
undefined,
|
||||
7
|
||||
);
|
||||
|
||||
// Assignment
|
||||
testSuggestions(`FROM a | ENRICH policy on b with `, [
|
||||
attachTriggerCommand('var0 = '),
|
||||
...getPolicyFields('policy'),
|
||||
]);
|
||||
|
||||
// FROM source
|
||||
//
|
||||
// Using an Invoke trigger kind here because that's what Monaco uses when the show suggestions
|
||||
// action is triggered (e.g. accepting the "FROM" suggestion)
|
||||
testSuggestions(
|
||||
'FROM ',
|
||||
[
|
||||
{ text: 'index1 ', command: TRIGGER_SUGGESTION_COMMAND },
|
||||
{ text: 'index2 ', command: TRIGGER_SUGGESTION_COMMAND },
|
||||
],
|
||||
undefined,
|
||||
5,
|
||||
[
|
||||
,
|
||||
[
|
||||
{ name: 'index1', hidden: false },
|
||||
{ name: 'index2', hidden: false },
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// FROM source METADATA
|
||||
testSuggestions(
|
||||
'FROM index1 M',
|
||||
[',', attachAsSnippet(attachTriggerCommand('METADATA $0')), '| '],
|
||||
undefined,
|
||||
13
|
||||
);
|
||||
|
||||
// LIMIT number
|
||||
testSuggestions('FROM a | LIMIT ', ['10 ', '100 ', '1000 '].map(attachTriggerCommand));
|
||||
|
||||
// SORT field
|
||||
testSuggestions(
|
||||
'FROM a | SORT ',
|
||||
[
|
||||
...getFieldNamesByType('any').map((field) => `${field} `),
|
||||
...getFunctionSignaturesByReturnType('sort', 'any', { scalar: true }),
|
||||
].map(attachTriggerCommand),
|
||||
undefined,
|
||||
14
|
||||
);
|
||||
|
||||
// SORT field order
|
||||
testSuggestions(
|
||||
'FROM a | SORT field ',
|
||||
[',', ...['ASC ', 'DESC ', '| '].map(attachTriggerCommand)],
|
||||
undefined,
|
||||
20
|
||||
);
|
||||
|
||||
// SORT field order nulls
|
||||
testSuggestions(
|
||||
'FROM a | SORT field ASC ',
|
||||
[',', ...['NULLS FIRST ', 'NULLS LAST ', '| '].map(attachTriggerCommand)],
|
||||
undefined,
|
||||
24
|
||||
);
|
||||
|
||||
// STATS argument
|
||||
testSuggestions(
|
||||
'FROM a | STATS ',
|
||||
[
|
||||
'var0 = ',
|
||||
...getFunctionSignaturesByReturnType('stats', 'any', { scalar: true, agg: true }).map(
|
||||
attachAsSnippet
|
||||
),
|
||||
].map(attachTriggerCommand),
|
||||
undefined,
|
||||
15
|
||||
);
|
||||
|
||||
// STATS argument BY
|
||||
testSuggestions(
|
||||
'FROM a | STATS AVG(numberField) ',
|
||||
[',', attachAsSnippet(attachTriggerCommand('BY $0')), attachTriggerCommand('| ')],
|
||||
undefined,
|
||||
32
|
||||
);
|
||||
|
||||
// STATS argument BY field
|
||||
const allByCompatibleFunctions = getFunctionSignaturesByReturnType(
|
||||
'stats',
|
||||
'any',
|
||||
{
|
||||
scalar: true,
|
||||
grouping: true,
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
'by'
|
||||
);
|
||||
testSuggestions(
|
||||
'FROM a | STATS AVG(numberField) BY ',
|
||||
[
|
||||
attachTriggerCommand('var0 = '),
|
||||
...getFieldNamesByType('any')
|
||||
.map((field) => `${field} `)
|
||||
.map(attachTriggerCommand),
|
||||
...allByCompatibleFunctions,
|
||||
],
|
||||
undefined,
|
||||
35
|
||||
);
|
||||
|
||||
// STATS argument BY assignment (checking field suggestions)
|
||||
testSuggestions(
|
||||
'FROM a | STATS AVG(numberField) BY var0 = ',
|
||||
[
|
||||
...getFieldNamesByType('any')
|
||||
.map((field) => `${field} `)
|
||||
.map(attachTriggerCommand),
|
||||
...allByCompatibleFunctions,
|
||||
],
|
||||
undefined,
|
||||
41
|
||||
);
|
||||
|
||||
// WHERE argument (field suggestions)
|
||||
testSuggestions(
|
||||
'FROM a | WHERE ',
|
||||
[
|
||||
...getFieldNamesByType('any')
|
||||
.map((field) => `${field} `)
|
||||
.map(attachTriggerCommand),
|
||||
...getFunctionSignaturesByReturnType('where', 'any', { scalar: true }).map(attachAsSnippet),
|
||||
],
|
||||
undefined,
|
||||
15
|
||||
);
|
||||
|
||||
// WHERE argument comparison
|
||||
testSuggestions(
|
||||
'FROM a | WHERE stringField ',
|
||||
getFunctionSignaturesByReturnType(
|
||||
'where',
|
||||
'boolean',
|
||||
{
|
||||
builtin: true,
|
||||
},
|
||||
['string']
|
||||
).map((s) => (s.text.toLowerCase().includes('null') ? s : attachTriggerCommand(s))),
|
||||
undefined,
|
||||
27
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,10 +13,11 @@ import type {
|
|||
ESQLCommand,
|
||||
ESQLCommandOption,
|
||||
ESQLFunction,
|
||||
ESQLLiteral,
|
||||
ESQLSingleAstItem,
|
||||
} from '@kbn/esql-ast';
|
||||
import { partition } from 'lodash';
|
||||
import { ESQL_NUMBER_TYPES, isNumericType } from '../shared/esql_types';
|
||||
import { ESQL_NUMBER_TYPES, compareTypesWithLiterals, isNumericType } from '../shared/esql_types';
|
||||
import type { EditorContext, SuggestionRawDefinition } from './types';
|
||||
import {
|
||||
lookupColumn,
|
||||
|
@ -44,6 +45,7 @@ import {
|
|||
nonNullable,
|
||||
getColumnExists,
|
||||
findPreviousWord,
|
||||
noCaseCompare,
|
||||
} from '../shared/helpers';
|
||||
import { collectVariables, excludeVariablesFromCurrentCommand } from '../shared/variables';
|
||||
import type { ESQLPolicy, ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types';
|
||||
|
@ -93,7 +95,7 @@ import {
|
|||
isAggFunctionUsedAlready,
|
||||
removeQuoteForSuggestedSources,
|
||||
} from './helper';
|
||||
import { FunctionParameter } from '../definitions/types';
|
||||
import { FunctionParameter, FunctionReturnType, SupportedFieldType } from '../definitions/types';
|
||||
|
||||
type GetSourceFn = () => Promise<SuggestionRawDefinition[]>;
|
||||
type GetDataStreamsForIntegrationFn = (
|
||||
|
@ -101,7 +103,8 @@ type GetDataStreamsForIntegrationFn = (
|
|||
) => Promise<Array<{ name: string; title?: string }> | undefined>;
|
||||
type GetFieldsByTypeFn = (
|
||||
type: string | string[],
|
||||
ignored?: string[]
|
||||
ignored?: string[],
|
||||
options?: { advanceCursorAndOpenSuggestions?: boolean; addComma?: boolean }
|
||||
) => Promise<SuggestionRawDefinition[]>;
|
||||
type GetFieldsMapFn = () => Promise<Map<string, ESQLRealField>>;
|
||||
type GetPoliciesFn = () => Promise<SuggestionRawDefinition[]>;
|
||||
|
@ -185,8 +188,8 @@ function correctQuerySyntax(_query: string, context: EditorContext) {
|
|||
(context.triggerCharacter && charThatNeedMarkers.includes(context.triggerCharacter)) ||
|
||||
// monaco.editor.CompletionTriggerKind['Invoke'] === 0
|
||||
(context.triggerKind === 0 && unclosedRoundBrackets === 0) ||
|
||||
(context.triggerCharacter === ' ' &&
|
||||
(isMathFunction(query, query.length) || isComma(query.trimEnd()[query.trimEnd().length - 1])))
|
||||
(context.triggerCharacter === ' ' && isMathFunction(query, query.length)) ||
|
||||
isComma(query.trimEnd()[query.trimEnd().length - 1])
|
||||
) {
|
||||
query += EDITOR_MARKER;
|
||||
}
|
||||
|
@ -308,12 +311,19 @@ export async function suggest(
|
|||
return [];
|
||||
}
|
||||
|
||||
function getFieldsByTypeRetriever(queryString: string, resourceRetriever?: ESQLCallbacks) {
|
||||
function getFieldsByTypeRetriever(
|
||||
queryString: string,
|
||||
resourceRetriever?: ESQLCallbacks
|
||||
): { getFieldsByType: GetFieldsByTypeFn; getFieldsMap: GetFieldsMapFn } {
|
||||
const helpers = getFieldsByTypeHelper(queryString, resourceRetriever);
|
||||
return {
|
||||
getFieldsByType: async (expectedType: string | string[] = 'any', ignored: string[] = []) => {
|
||||
getFieldsByType: async (
|
||||
expectedType: string | string[] = 'any',
|
||||
ignored: string[] = [],
|
||||
options
|
||||
) => {
|
||||
const fields = await helpers.getFieldsByType(expectedType, ignored);
|
||||
return buildFieldsDefinitionsWithMetadata(fields);
|
||||
return buildFieldsDefinitionsWithMetadata(fields, options);
|
||||
},
|
||||
getFieldsMap: helpers.getFieldsMap,
|
||||
};
|
||||
|
@ -429,7 +439,13 @@ function areCurrentArgsValid(
|
|||
function extractFinalTypeFromArg(
|
||||
arg: ESQLAstItem,
|
||||
references: Pick<ReferenceMaps, 'fields' | 'variables'>
|
||||
): string | undefined {
|
||||
):
|
||||
| ESQLLiteral['literalType']
|
||||
| SupportedFieldType
|
||||
| FunctionReturnType
|
||||
| 'timeInterval'
|
||||
| string // @TODO remove this
|
||||
| undefined {
|
||||
if (Array.isArray(arg)) {
|
||||
return extractFinalTypeFromArg(arg[0], references);
|
||||
}
|
||||
|
@ -792,7 +808,11 @@ async function getExpressionSuggestionsByType(
|
|||
// if the definition includes a list of constants, suggest them
|
||||
if (argDef.values) {
|
||||
// ... | <COMMAND> ... <suggest enums>
|
||||
suggestions.push(...buildConstantsDefinitions(argDef.values));
|
||||
suggestions.push(
|
||||
...buildConstantsDefinitions(argDef.values, undefined, undefined, {
|
||||
advanceCursorAndOpenSuggestions: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
// If the type is specified try to dig deeper in the definition to suggest the best candidate
|
||||
if (
|
||||
|
@ -815,6 +835,7 @@ async function getExpressionSuggestionsByType(
|
|||
// ... | <COMMAND> <suggest>
|
||||
// In this case start suggesting something not strictly based on type
|
||||
suggestions.push(
|
||||
...(await getFieldsByType('any', [], { advanceCursorAndOpenSuggestions: true })),
|
||||
...(await getFieldsOrFunctionsSuggestions(
|
||||
['any'],
|
||||
command.name,
|
||||
|
@ -822,7 +843,7 @@ async function getExpressionSuggestionsByType(
|
|||
getFieldsByType,
|
||||
{
|
||||
functions: true,
|
||||
fields: true,
|
||||
fields: false,
|
||||
variables: anyVariables,
|
||||
}
|
||||
))
|
||||
|
@ -1087,7 +1108,11 @@ async function getFieldsOrFunctionsSuggestions(
|
|||
} = {}
|
||||
): Promise<SuggestionRawDefinition[]> {
|
||||
const filteredFieldsByType = pushItUpInTheList(
|
||||
(await (fields ? getFieldsByType(types, ignoreFields) : [])) as SuggestionRawDefinition[],
|
||||
(await (fields
|
||||
? getFieldsByType(types, ignoreFields, {
|
||||
advanceCursorAndOpenSuggestions: commandName === 'sort',
|
||||
})
|
||||
: [])) as SuggestionRawDefinition[],
|
||||
functions
|
||||
);
|
||||
|
||||
|
@ -1187,6 +1212,8 @@ async function getFunctionArgsSuggestions(
|
|||
? refSignature.minParams - 1 > argIndex
|
||||
: false);
|
||||
|
||||
const shouldAddComma = hasMoreMandatoryArgs && fnDefinition.type !== 'builtin';
|
||||
|
||||
const suggestedConstants = Array.from(
|
||||
new Set(
|
||||
fnDefinition.signatures.reduce<string[]>((acc, signature) => {
|
||||
|
@ -1207,13 +1234,13 @@ async function getFunctionArgsSuggestions(
|
|||
);
|
||||
|
||||
if (suggestedConstants.length) {
|
||||
return buildValueDefinitions(suggestedConstants).map((suggestion) => ({
|
||||
...suggestion,
|
||||
text: addCommaIf(hasMoreMandatoryArgs && fnDefinition.type !== 'builtin', suggestion.text),
|
||||
}));
|
||||
return buildValueDefinitions(suggestedConstants, {
|
||||
addComma: hasMoreMandatoryArgs,
|
||||
advanceCursorAndOpenSuggestions: hasMoreMandatoryArgs,
|
||||
});
|
||||
}
|
||||
|
||||
const suggestions = [];
|
||||
const suggestions: SuggestionRawDefinition[] = [];
|
||||
const noArgDefined = !arg;
|
||||
const isUnknownColumn =
|
||||
arg &&
|
||||
|
@ -1267,7 +1294,9 @@ async function getFunctionArgsSuggestions(
|
|||
// if existing arguments are preset already, use them to filter out incompatible signatures
|
||||
.filter((signature) => {
|
||||
if (existingTypes.length) {
|
||||
return existingTypes.every((type, index) => signature.params[index].type === type);
|
||||
return existingTypes.every((type, index) =>
|
||||
compareTypesWithLiterals(signature.params[index].type, type)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
@ -1299,28 +1328,51 @@ async function getFunctionArgsSuggestions(
|
|||
return Array.from(new Set(paramDefs.map(({ type }) => type)));
|
||||
};
|
||||
|
||||
// Literals
|
||||
suggestions.push(
|
||||
...getCompatibleLiterals(command.name, getTypesFromParamDefs(constantOnlyParamDefs))
|
||||
...getCompatibleLiterals(
|
||||
command.name,
|
||||
getTypesFromParamDefs(constantOnlyParamDefs),
|
||||
undefined,
|
||||
{ addComma: shouldAddComma, advanceCursorAndOpenSuggestions: hasMoreMandatoryArgs }
|
||||
)
|
||||
);
|
||||
|
||||
// Fields
|
||||
suggestions.push(
|
||||
...(await getFieldsOrFunctionsSuggestions(
|
||||
getTypesFromParamDefs(paramDefsWhichSupportFields),
|
||||
...pushItUpInTheList(
|
||||
await getFieldsByType(getTypesFromParamDefs(paramDefsWhichSupportFields), [], {
|
||||
addComma: shouldAddComma,
|
||||
advanceCursorAndOpenSuggestions: hasMoreMandatoryArgs,
|
||||
}),
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
// Functions
|
||||
suggestions.push(
|
||||
...getCompatibleFunctionDefinition(
|
||||
command.name,
|
||||
option?.name,
|
||||
getFieldsByType,
|
||||
{
|
||||
functions: true,
|
||||
fields: true,
|
||||
variables: variablesExcludingCurrentCommandOnes,
|
||||
},
|
||||
// do not repropose the same function as arg
|
||||
// i.e. avoid cases like abs(abs(abs(...))) with suggestions
|
||||
{
|
||||
ignoreFn: fnToIgnore,
|
||||
}
|
||||
))
|
||||
getTypesFromParamDefs(paramDefsWhichSupportFields),
|
||||
fnToIgnore
|
||||
).map((suggestion) => ({
|
||||
...suggestion,
|
||||
text: addCommaIf(shouldAddComma, suggestion.text),
|
||||
}))
|
||||
);
|
||||
|
||||
// could also be in stats (bucket) but our autocomplete is not great yet
|
||||
if (
|
||||
getTypesFromParamDefs(paramDefsWhichSupportFields).includes('date') &&
|
||||
['where', 'eval'].includes(command.name)
|
||||
)
|
||||
suggestions.push(
|
||||
...getDateLiterals({
|
||||
addComma: shouldAddComma,
|
||||
advanceCursorAndOpenSuggestions: hasMoreMandatoryArgs,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// for eval and row commands try also to complete numeric literals with time intervals where possible
|
||||
|
@ -1329,18 +1381,10 @@ async function getFunctionArgsSuggestions(
|
|||
if (isLiteralItem(arg) && isNumericType(arg.literalType)) {
|
||||
// ... | EVAL fn(2 <suggest>)
|
||||
suggestions.push(
|
||||
...(await getFieldsOrFunctionsSuggestions(
|
||||
['time_literal_unit'],
|
||||
command.name,
|
||||
option?.name,
|
||||
getFieldsByType,
|
||||
{
|
||||
functions: false,
|
||||
fields: false,
|
||||
variables: variablesExcludingCurrentCommandOnes,
|
||||
literals: true,
|
||||
}
|
||||
))
|
||||
...getCompatibleLiterals(command.name, ['time_literal_unit'], undefined, {
|
||||
addComma: shouldAddComma,
|
||||
advanceCursorAndOpenSuggestions: hasMoreMandatoryArgs,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1349,24 +1393,9 @@ async function getFunctionArgsSuggestions(
|
|||
// 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,
|
||||
text:
|
||||
hasMoreMandatoryArgs && fnDefinition.type !== 'builtin'
|
||||
? `${suggestion.text},`
|
||||
: suggestion.text,
|
||||
}
|
||||
: suggestion
|
||||
);
|
||||
}
|
||||
|
||||
return suggestions.map(({ text, ...rest }) => ({
|
||||
...rest,
|
||||
text: addCommaIf(hasMoreMandatoryArgs && fnDefinition.type !== 'builtin' && text !== '', text),
|
||||
}));
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
async function getListArgsSuggestions(
|
||||
|
@ -1521,7 +1550,7 @@ async function getOptionArgsSuggestions(
|
|||
innerText
|
||||
);
|
||||
|
||||
if (isNewExpression || findPreviousWord(innerText) === 'WITH') {
|
||||
if (isNewExpression || noCaseCompare(findPreviousWord(innerText), 'WITH')) {
|
||||
suggestions.push(buildNewVarDefinition(findNewVariable(anyEnhancedVariables)));
|
||||
}
|
||||
|
||||
|
@ -1595,19 +1624,6 @@ async function getOptionArgsSuggestions(
|
|||
}
|
||||
|
||||
if (command.name === 'stats') {
|
||||
suggestions.push(
|
||||
...(await getFieldsOrFunctionsSuggestions(
|
||||
['column'],
|
||||
command.name,
|
||||
option.name,
|
||||
getFieldsByType,
|
||||
{
|
||||
functions: false,
|
||||
fields: true,
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
const argDef = optionDef?.signature.params[argIndex];
|
||||
|
||||
const nodeArgType = extractFinalTypeFromArg(nodeArg, references);
|
||||
|
@ -1667,20 +1683,27 @@ async function getOptionArgsSuggestions(
|
|||
})
|
||||
);
|
||||
} else if (isNewExpression || (isAssignment(nodeArg) && !isAssignmentComplete(nodeArg))) {
|
||||
// Otherwise try to complete the expression suggesting some columns
|
||||
suggestions.push(
|
||||
...(await getFieldsOrFunctionsSuggestions(
|
||||
types[0] === 'column' ? ['any'] : types,
|
||||
command.name,
|
||||
option.name,
|
||||
getFieldsByType,
|
||||
{
|
||||
functions: option.name === 'by',
|
||||
fields: true,
|
||||
}
|
||||
))
|
||||
...(await getFieldsByType(types[0] === 'column' ? ['any'] : types, [], {
|
||||
advanceCursorAndOpenSuggestions: true,
|
||||
}))
|
||||
);
|
||||
|
||||
if (option.name === 'by') {
|
||||
suggestions.push(
|
||||
...(await getFieldsOrFunctionsSuggestions(
|
||||
types[0] === 'column' ? ['any'] : types,
|
||||
command.name,
|
||||
option.name,
|
||||
getFieldsByType,
|
||||
{
|
||||
functions: true,
|
||||
fields: false,
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
if (command.name === 'stats' && isNewExpression) {
|
||||
suggestions.push(buildNewVarDefinition(findNewVariable(anyVariables)));
|
||||
}
|
||||
|
|
|
@ -98,13 +98,16 @@ function buildCharCompleteItem(
|
|||
sortText,
|
||||
};
|
||||
}
|
||||
export const pipeCompleteItem = buildCharCompleteItem(
|
||||
'|',
|
||||
i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.pipeDoc', {
|
||||
export const pipeCompleteItem: SuggestionRawDefinition = {
|
||||
label: '|',
|
||||
text: '| ',
|
||||
kind: 'Keyword',
|
||||
detail: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.pipeDoc', {
|
||||
defaultMessage: 'Pipe (|)',
|
||||
}),
|
||||
{ sortText: 'C', quoted: false }
|
||||
);
|
||||
sortText: 'C',
|
||||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
};
|
||||
|
||||
export const commaCompleteItem = buildCharCompleteItem(
|
||||
',',
|
||||
|
|
|
@ -130,7 +130,8 @@ export function getSuggestionCommandDefinition(
|
|||
}
|
||||
|
||||
export const buildFieldsDefinitionsWithMetadata = (
|
||||
fields: ESQLRealField[]
|
||||
fields: ESQLRealField[],
|
||||
options?: { advanceCursorAndOpenSuggestions?: boolean; addComma?: boolean }
|
||||
): SuggestionRawDefinition[] => {
|
||||
return fields.map((field) => {
|
||||
const description = field.metadata?.description;
|
||||
|
@ -138,7 +139,10 @@ export const buildFieldsDefinitionsWithMetadata = (
|
|||
const titleCaseType = field.type.charAt(0).toUpperCase() + field.type.slice(1);
|
||||
return {
|
||||
label: field.name,
|
||||
text: getSafeInsertText(field.name),
|
||||
text:
|
||||
getSafeInsertText(field.name) +
|
||||
(options?.addComma ? ',' : '') +
|
||||
(options?.advanceCursorAndOpenSuggestions ? ' ' : ''),
|
||||
kind: 'Variable',
|
||||
detail: titleCaseType,
|
||||
documentation: description
|
||||
|
@ -151,6 +155,7 @@ ${description}`,
|
|||
: undefined,
|
||||
// If there is a description, it is a field from ECS, so it should be sorted to the top
|
||||
sortText: description ? '1D' : 'D',
|
||||
command: options?.advanceCursorAndOpenSuggestions ? TRIGGER_SUGGESTION_COMMAND : undefined,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -185,9 +190,8 @@ export const buildSourcesDefinitions = (
|
|||
): SuggestionRawDefinition[] =>
|
||||
sources.map(({ name, isIntegration, title }) => ({
|
||||
label: title ?? name,
|
||||
text: getSafeInsertSourceText(name),
|
||||
text: getSafeInsertSourceText(name) + (!isIntegration ? ' ' : ''),
|
||||
isSnippet: isIntegration,
|
||||
...(isIntegration && { command: TRIGGER_SUGGESTION_COMMAND }),
|
||||
kind: isIntegration ? 'Class' : 'Issue',
|
||||
detail: isIntegration
|
||||
? i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.integrationDefinition', {
|
||||
|
@ -197,16 +201,24 @@ export const buildSourcesDefinitions = (
|
|||
defaultMessage: `Index`,
|
||||
}),
|
||||
sortText: 'A',
|
||||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
}));
|
||||
|
||||
export const buildConstantsDefinitions = (
|
||||
userConstants: string[],
|
||||
detail?: string,
|
||||
sortText?: string
|
||||
sortText?: string,
|
||||
/**
|
||||
* Whether or not to advance the cursor and open the suggestions dialog after inserting the constant.
|
||||
*/
|
||||
options?: { advanceCursorAndOpenSuggestions?: boolean; addComma?: boolean }
|
||||
): SuggestionRawDefinition[] =>
|
||||
userConstants.map((label) => ({
|
||||
label,
|
||||
text: label,
|
||||
text:
|
||||
label +
|
||||
(options?.addComma ? ',' : '') +
|
||||
(options?.advanceCursorAndOpenSuggestions ? ' ' : ''),
|
||||
kind: 'Constant',
|
||||
detail:
|
||||
detail ??
|
||||
|
@ -214,32 +226,35 @@ export const buildConstantsDefinitions = (
|
|||
defaultMessage: `Constant`,
|
||||
}),
|
||||
sortText: sortText ?? 'A',
|
||||
command: options?.advanceCursorAndOpenSuggestions ? TRIGGER_SUGGESTION_COMMAND : undefined,
|
||||
}));
|
||||
|
||||
export const buildValueDefinitions = (
|
||||
values: string[],
|
||||
detail?: string
|
||||
options?: { advanceCursorAndOpenSuggestions?: boolean; addComma?: boolean }
|
||||
): SuggestionRawDefinition[] =>
|
||||
values.map((value) => ({
|
||||
label: `"${value}"`,
|
||||
text: `"${value}"`,
|
||||
detail:
|
||||
detail ??
|
||||
i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.valueDefinition', {
|
||||
defaultMessage: 'Literal value',
|
||||
}),
|
||||
text: `"${value}"${options?.addComma ? ',' : ''}${
|
||||
options?.advanceCursorAndOpenSuggestions ? ' ' : ''
|
||||
}`,
|
||||
detail: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.valueDefinition', {
|
||||
defaultMessage: 'Literal value',
|
||||
}),
|
||||
kind: 'Value',
|
||||
command: options?.advanceCursorAndOpenSuggestions ? TRIGGER_SUGGESTION_COMMAND : undefined,
|
||||
}));
|
||||
|
||||
export const buildNewVarDefinition = (label: string): SuggestionRawDefinition => {
|
||||
return {
|
||||
label,
|
||||
text: `${label} =`,
|
||||
text: `${label} = `,
|
||||
kind: 'Variable',
|
||||
detail: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.newVarDoc', {
|
||||
defaultMessage: 'Define a new variable',
|
||||
}),
|
||||
sortText: '1',
|
||||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -358,21 +373,39 @@ export function getUnitDuration(unit: number = 1) {
|
|||
* "magical" logic. Maybe this is really the same thing as the literalOptions parameter
|
||||
* definition property...
|
||||
*/
|
||||
export function getCompatibleLiterals(commandName: string, types: string[], names?: string[]) {
|
||||
export function getCompatibleLiterals(
|
||||
commandName: string,
|
||||
types: string[],
|
||||
names?: string[],
|
||||
options?: { advanceCursorAndOpenSuggestions?: boolean; addComma?: boolean }
|
||||
) {
|
||||
const suggestions: SuggestionRawDefinition[] = [];
|
||||
if (types.some(isNumericType)) {
|
||||
if (commandName === 'limit') {
|
||||
// suggest 10/100/1000 for limit
|
||||
suggestions.push(...buildConstantsDefinitions(['10', '100', '1000'], ''));
|
||||
suggestions.push(
|
||||
...buildConstantsDefinitions(['10', '100', '1000'], '', undefined, {
|
||||
advanceCursorAndOpenSuggestions: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
if (types.includes('time_literal')) {
|
||||
// filter plural for now and suggest only unit + singular
|
||||
suggestions.push(...buildConstantsDefinitions(getUnitDuration(1))); // i.e. 1 year
|
||||
suggestions.push(
|
||||
...buildConstantsDefinitions(getUnitDuration(1), undefined, undefined, options)
|
||||
); // 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(timeUnitsToSuggest.map(({ name }) => name))); // i.e. year, month, ...
|
||||
suggestions.push(
|
||||
...buildConstantsDefinitions(
|
||||
timeUnitsToSuggest.map(({ name }) => name),
|
||||
undefined,
|
||||
undefined,
|
||||
options
|
||||
)
|
||||
); // i.e. year, month, ...
|
||||
}
|
||||
if (types.includes('string')) {
|
||||
if (names) {
|
||||
|
@ -383,25 +416,31 @@ export function getCompatibleLiterals(commandName: string, types: string[], name
|
|||
[commandName === 'grok' ? '"%{WORD:firstWord}"' : '"%{firstWord}"'],
|
||||
i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.aPatternString', {
|
||||
defaultMessage: 'A pattern string',
|
||||
})
|
||||
}),
|
||||
undefined,
|
||||
options
|
||||
)
|
||||
);
|
||||
} else {
|
||||
suggestions.push(...buildConstantsDefinitions(['string'], ''));
|
||||
suggestions.push(...buildConstantsDefinitions(['string'], '', undefined, options));
|
||||
}
|
||||
}
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
export function getDateLiterals() {
|
||||
export function getDateLiterals(options?: {
|
||||
advanceCursorAndOpenSuggestions?: boolean;
|
||||
addComma?: boolean;
|
||||
}) {
|
||||
return [
|
||||
...buildConstantsDefinitions(
|
||||
TIME_SYSTEM_PARAMS,
|
||||
i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.namedParamDefinition', {
|
||||
defaultMessage: 'Named parameter',
|
||||
}),
|
||||
'1A'
|
||||
'1A',
|
||||
options
|
||||
),
|
||||
{
|
||||
label: i18n.translate(
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ESQLDecimalLiteral, ESQLNumericLiteralType } from '@kbn/esql-ast/src/types';
|
||||
import { ESQLDecimalLiteral, ESQLLiteral, ESQLNumericLiteralType } from '@kbn/esql-ast/src/types';
|
||||
import { FunctionParameterType } from '../definitions/types';
|
||||
|
||||
export const ESQL_COMMON_NUMERIC_TYPES = ['double', 'long', 'integer'] as const;
|
||||
export const ESQL_NUMERIC_DECIMAL_TYPES = [
|
||||
|
@ -47,3 +48,29 @@ export function isNumericDecimalType(type: unknown): type is ESQLDecimalLiteral
|
|||
ESQL_NUMERIC_DECIMAL_TYPES.includes(type as (typeof ESQL_NUMERIC_DECIMAL_TYPES)[number])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two types, taking into account literal types
|
||||
* @TODO strengthen typing here (remove `string`)
|
||||
*/
|
||||
export const compareTypesWithLiterals = (
|
||||
a: ESQLLiteral['literalType'] | FunctionParameterType | string,
|
||||
b: ESQLLiteral['literalType'] | FunctionParameterType | string
|
||||
) => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (a === 'decimal') {
|
||||
return isNumericDecimalType(b);
|
||||
}
|
||||
if (b === 'decimal') {
|
||||
return isNumericDecimalType(a);
|
||||
}
|
||||
if (a === 'string') {
|
||||
return isStringType(b);
|
||||
}
|
||||
if (b === 'string') {
|
||||
return isStringType(a);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -645,3 +645,8 @@ export const isParam = (x: unknown): x is ESQLParamLiteral =>
|
|||
typeof x === 'object' &&
|
||||
(x as ESQLParamLiteral).type === 'literal' &&
|
||||
(x as ESQLParamLiteral).literalType === 'param';
|
||||
|
||||
/**
|
||||
* Compares two strings in a case-insensitive manner
|
||||
*/
|
||||
export const noCaseCompare = (a: string, b: string) => a.toLowerCase() === b.toLowerCase();
|
||||
|
|
|
@ -763,8 +763,18 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
|
|||
forceMoveMarkers: true,
|
||||
},
|
||||
]);
|
||||
|
||||
setPopoverPosition({});
|
||||
|
||||
datePickerOpenStatusRef.current = false;
|
||||
|
||||
// move the cursor past the date we just inserted
|
||||
editor1.current?.setPosition({
|
||||
lineNumber: currentCursorPosition?.lineNumber ?? 0,
|
||||
column: (currentCursorPosition?.column ?? 0) + addition.length - 1,
|
||||
});
|
||||
// restore focus to the editor
|
||||
editor1.current?.focus();
|
||||
}
|
||||
}}
|
||||
inline
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue