[ES|QL] remove worker (#218006)

## Summary

Fix https://github.com/elastic/kibana/issues/217923

Investigations in https://github.com/elastic/kibana/issues/217368 showed
that there was basically no performance impact to passing the AST across
a thread boundary. But we also didn't detect a pressing reason to remove
the worker.

Since then, however, we noticed another cost associated with the worker:
it's a hefty Javascript file, even in production builds. In addition, we
are doing parsing on the main thread _and_ the worker, so the
`kbn-esql-ast` package is actually being loaded and parsed twice by the
browser, once for the main thread and once for the worker.

This PR removes our worker. Our parsing associated with validation and
autocomplete will still be done asynchronously, but on the main thread.

I do not see any regression in perceived performance.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
Drew Tate 2025-04-15 10:18:07 -06:00 committed by GitHub
parent f660e0140e
commit 9b4403b7dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 119 additions and 407 deletions

View file

@ -25,7 +25,6 @@ import {
import type { CoreStart } from '@kbn/core/public';
import { ESQLCallbacks, ESQLRealField, validateQuery } from '@kbn/esql-validation-autocomplete';
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import type { StartDependencies } from './plugin';
import { CodeSnippet } from './code_snippet';
@ -77,16 +76,13 @@ export const App = (props: { core: CoreStart; plugins: StartDependencies }) => {
if (currentQuery === '') {
return;
}
validateQuery(
currentQuery,
getAstAndSyntaxErrors,
{ ignoreOnMissingCallbacks: ignoreErrors },
callbacks
).then(({ errors: validationErrors, warnings: validationWarnings }) => {
// syntax errors come with a slight different format than other validation errors
setErrors(validationErrors.map((e) => ('severity' in e ? e.message : e.text)));
setWarnings(validationWarnings.map((e) => e.text));
});
validateQuery(currentQuery, { ignoreOnMissingCallbacks: ignoreErrors }, callbacks).then(
({ errors: validationErrors, warnings: validationWarnings }) => {
// syntax errors come with a slight different format than other validation errors
setErrors(validationErrors.map((e) => ('severity' in e ? e.message : e.text)));
setWarnings(validationWarnings.map((e) => e.text));
}
);
}, [currentQuery, ignoreErrors, callbacks]);
const checkboxes = [
@ -106,7 +102,7 @@ export const App = (props: { core: CoreStart; plugins: StartDependencies }) => {
return (
<EuiPage>
<EuiPageBody style={{ maxWidth: 800, margin: '0 auto' }}>
<EuiPageBody css={{ maxWidth: 800, margin: '0 auto' }}>
<EuiPageHeader paddingSize="s" bottomBorder={true} pageTitle="ES|QL validation example" />
<EuiPageSection paddingSize="s">
<p>This app shows how to use the ES|QL validation API with all its options</p>

View file

@ -56,7 +56,7 @@ export function CodeSnippet({ currentQuery, callbacks, ignoreErrors }: CodeSnipp
<EuiCodeBlock language="typescript" isCopyable>
{`
import { ESQLCallbacks, validateQuery } from '@kbn/esql-validation-autocomplete';
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { parse } from '@kbn/esql-ast';
const currentQuery = "${currentQuery}";
@ -64,7 +64,6 @@ const callbacks: ESQLCallbacks = () => ${getCallbacksCode(callbacks)};
const {errors, warnings} = validateQuery(
currentQuery,
getAstAndSyntaxErrors,
{ ignoreOnMissingCallbacks: ${Boolean(ignoreErrors)} },
callbacks
);

View file

@ -18,7 +18,6 @@
"@kbn/data-plugin",
"@kbn/data-views-plugin",
"@kbn/developer-examples-plugin",
"@kbn/esql-ast",
"@kbn/esql-validation-autocomplete",
]
}

View file

@ -26,7 +26,6 @@ export type {
ESQLColumn,
ESQLLiteral,
ESQLParamLiteral,
AstProviderFn,
EditorError,
ESQLAstNode,
ESQLInlineCast,
@ -57,7 +56,6 @@ export {
parseErrors,
type ParseOptions,
type ParseResult,
getAstAndSyntaxErrors,
ESQLErrorListener,
} from './src/parser';

View file

@ -15,9 +15,6 @@ export {
parseErrors,
type ParseOptions,
type ParseResult,
/** @deprecated Use `parse` instead. */
parse as getAstAndSyntaxErrors,
} from './parser';
export { ESQLErrorListener } from './esql_error_listener';

View file

@ -489,13 +489,6 @@ export interface ESQLMessage {
code: string;
}
export type AstProviderFn = (text: string | undefined) =>
| Promise<{
ast: ESQLAst;
errors: EditorError[];
}>
| { ast: ESQLAst; errors: EditorError[] };
export interface EditorError {
startLineNumber: number;
endLineNumber: number;

View file

@ -7,7 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { parse, mutate, BasicPrettyPrinter } from '@kbn/esql-ast';
import { sanitazeESQLInput } from './sanitaze_input';
@ -61,7 +60,7 @@ export function appendWhereClauseToESQLQuery(
filterValue = '';
}
const { ast } = getAstAndSyntaxErrors(baseESQLQuery);
const { ast } = parse(baseESQLQuery);
const lastCommandIsWhere = ast[ast.length - 1].name === 'where';
// if where command already exists in the end of the query:

View file

@ -7,10 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { parse } from '@kbn/esql-ast';
export function getESQLWithSafeLimit(esql: string, limit: number): string {
const { ast } = getAstAndSyntaxErrors(esql);
const { ast } = parse(esql);
const sourceCommand = ast.find(({ name }) => ['from', 'metrics'].includes(name));
if (!sourceCommand) {
return esql;

View file

@ -25,7 +25,7 @@ For instance, not passing the `getSources` callback will report all index mentio
##### Usage
```js
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { parse } from '@kbn/esql-ast';
import { validateQuery } from '@kbn/esql-validation-autocomplete';
// define all callbacks
@ -35,13 +35,13 @@ const myCallbacks = {
};
// Full validation performed
const { errors, warnings } = await validateQuery("from index | stats 1 + avg(myColumn)", getAstAndSyntaxErrors, undefined, myCallbacks);
const { errors, warnings } = await validateQuery("from index | stats 1 + avg(myColumn)", parse, undefined, myCallbacks);
```
If not all callbacks are available it is possible to gracefully degrade the validation experience with the `ignoreOnMissingCallbacks` option:
```js
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { parse } from '@kbn/esql-ast';
import { validateQuery } from '@kbn/esql-validation-autocomplete';
// define only the getSources callback
@ -52,7 +52,7 @@ const myCallbacks = {
// ignore errors that might be triggered by the lack of some callbacks (i.e. "Unknown columns", etc...)
const { errors, warnings } = await validateQuery(
'from index | stats 1 + avg(myColumn)',
getAstAndSyntaxErrors,
parse,
{ ignoreOnMissingCallbacks: true },
myCallbacks
);
@ -63,7 +63,7 @@ const { errors, warnings } = await validateQuery(
This is the complete logic for the ES|QL autocomplete language, it is completely independent from the actual editor (i.e. Monaco) and the suggestions reported need to be wrapped against the specific editor shape.
```js
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { parse } from '@kbn/esql-ast';
import { suggest } from '@kbn/esql-validation-autocomplete';
const queryString = "from index | stats 1 + avg(myColumn) ";
@ -76,7 +76,7 @@ const suggestions = await suggest(
queryString,
queryString.length - 1, // the cursor position in a single line context
{ triggerCharacter: " "; triggerKind: 1 }, // kind = 0 is a programmatic trigger, while other values are ignored
getAstAndSyntaxErrors,
parse,
myCallbacks
);
@ -102,7 +102,7 @@ This feature provides a list of suggestions to propose as fixes for a subset of
The feature works in combination with the validation service.
```js
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { parse } from '@kbn/esql-ast';
import { validateQuery, getActions } from '@kbn/esql-validation-autocomplete';
const queryString = "from index2 | stats 1 + avg(myColumn)"
@ -111,12 +111,12 @@ const myCallbacks = {
getSources: async () => [{name: 'index', hidden: false}],
...
};
const { errors, warnings } = await validateQuery(queryString, getAstAndSyntaxErrors, undefined, myCallbacks);
const { errors, warnings } = await validateQuery(queryString, parse, undefined, myCallbacks);
const {title, edits} = await getActions(
queryString,
errors,
getAstAndSyntaxErrors,
parse,
undefined,
myCallbacks
);
@ -129,7 +129,7 @@ console.log({ title, edits });
Like with validation also `getActions` can 'relax' its internal checks when no callbacks, either all or specific ones, are passed.
```js
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { parse } from '@kbn/esql-ast';
import { validateQuery, getActions } from '@kbn/esql-validation-autocomplete';
const queryString = "from index2 | keep unquoted-field"
@ -138,12 +138,12 @@ const myCallbacks = {
getSources: async () => [{name: 'index', hidden: false}],
...
};
const { errors, warnings } = await validateQuery(queryString, getAstAndSyntaxErrors, undefined, myCallbacks);
const { errors, warnings } = await validateQuery(queryString, parse, undefined, myCallbacks);
const {title, edits} = await getActions(
queryString,
errors,
getAstAndSyntaxErrors,
parse,
{ relaxOnMissingCallbacks: true },
myCallbacks
);
@ -159,13 +159,13 @@ This is an important function in order to build more features on top of the exis
For instance to show contextual information on Hover the `getAstContext` function can be leveraged to get the correct context for the cursor position:
```js
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { parse } from '@kbn/esql-ast';
import { getAstContext } from '@kbn/esql-validation-autocomplete';
const queryString = 'from index2 | stats 1 + avg(myColumn)';
const offset = queryString.indexOf('avg');
const astContext = getAstContext(queryString, getAstAndSyntaxErrors(queryString), offset);
const astContext = getAstContext(queryString, parse(queryString), offset);
if (astContext.type === 'function') {
const fnNode = astContext.node;

View file

@ -7,7 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { ESQLCallbacks } from '../../shared/types';
import * as autocomplete from '../autocomplete';
import { getCallbackMocks } from '../../__tests__/helpers';
@ -26,7 +25,7 @@ const setup = async (caret = '?') => {
const pos = query.indexOf(caret);
if (pos < 0) throw new Error(`User cursor/caret "${caret}" not found in query: ${query}`);
const querySansCaret = query.slice(0, pos) + query.slice(pos + 1);
return await autocomplete.suggest(querySansCaret, pos, ctx, getAstAndSyntaxErrors, cb);
return await autocomplete.suggest(querySansCaret, pos, ctx, cb);
};
return {

View file

@ -8,7 +8,6 @@
*/
import { camelCase } from 'lodash';
import { parse } from '@kbn/esql-ast';
import { scalarFunctionDefinitions } from '../../definitions/generated/scalar_functions';
import { operatorsDefinitions } from '../../definitions/all_operators';
import { NOT_SUGGESTED_TYPES } from '../../shared/resources_helpers';
@ -353,13 +352,7 @@ export const setup = async (caret = '/') => {
? { triggerKind: 1, triggerCharacter: opts.triggerCharacter }
: { triggerKind: 0 };
return await autocomplete.suggest(
querySansCaret,
pos,
ctx,
(_query: string | undefined) => parse(_query, { withFormatting: true }),
opts.callbacks ?? callbacks
);
return await autocomplete.suggest(querySansCaret, pos, ctx, opts.callbacks ?? callbacks);
};
const assertSuggestions: AssertSuggestionsFn = async (query, expected, opts) => {

View file

@ -7,31 +7,30 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { suggest } from './autocomplete';
import { commandDefinitions as unmodifiedCommandDefinitions } from '../definitions/commands';
import { scalarFunctionDefinitions } from '../definitions/generated/scalar_functions';
import { timeUnitsToSuggest } from '../definitions/literals';
import { commandDefinitions as unmodifiedCommandDefinitions } from '../definitions/commands';
import { getSafeInsertText, TIME_SYSTEM_PARAMS, TRIGGER_SUGGESTION_COMMAND } from './factories';
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import {
policies,
getFunctionSignaturesByReturnType,
getFieldNamesByType,
createCustomCallbackMocks,
createCompletionContext,
getPolicyFields,
PartialSuggestionWithText,
TIME_PICKER_SUGGESTION,
setup,
attachTriggerCommand,
SuggestOptions,
fields,
} from './__tests__/helpers';
import { Location } from '../definitions/types';
import { METADATA_FIELDS } from '../shared/constants';
import { ESQL_STRING_TYPES } from '../shared/esql_types';
import { getRecommendedQueries } from './recommended_queries/templates';
import {
attachTriggerCommand,
createCompletionContext,
createCustomCallbackMocks,
fields,
getFieldNamesByType,
getFunctionSignaturesByReturnType,
getPolicyFields,
PartialSuggestionWithText,
policies,
setup,
SuggestOptions,
TIME_PICKER_SUGGESTION,
} from './__tests__/helpers';
import { suggest } from './autocomplete';
import { getDateHistogramCompletionItem } from './commands/stats/util';
import { Location } from '../definitions/types';
import { getSafeInsertText, TIME_SYSTEM_PARAMS, TRIGGER_SUGGESTION_COMMAND } from './factories';
import { getRecommendedQueries } from './recommended_queries/templates';
const commandDefinitions = unmodifiedCommandDefinitions.filter(({ hidden }) => !hidden);
@ -213,13 +212,7 @@ describe('autocomplete', () => {
const statement = 'from a | drop keywordField | eval var0 = abs(doubleField) ';
const triggerOffset = statement.lastIndexOf(' ');
const context = createCompletionContext(statement[triggerOffset]);
await suggest(
statement,
triggerOffset + 1,
context,
async (text) => (text ? getAstAndSyntaxErrors(text) : { ast: [], errors: [] }),
callbackMocks
);
await suggest(statement, triggerOffset + 1, context, callbackMocks);
expect(callbackMocks.getColumnsFor).toHaveBeenCalledWith({
query: 'from a | drop keywordField',
});
@ -229,13 +222,7 @@ describe('autocomplete', () => {
const statement = 'from a | drop | eval var0 = abs(doubleField) ';
const triggerOffset = statement.lastIndexOf('p') + 1; // drop <here>
const context = createCompletionContext(statement[triggerOffset]);
await suggest(
statement,
triggerOffset + 1,
context,
async (text) => (text ? getAstAndSyntaxErrors(text) : { ast: [], errors: [] }),
callbackMocks
);
await suggest(statement, triggerOffset + 1, context, callbackMocks);
expect(callbackMocks.getColumnsFor).toHaveBeenCalledWith({ query: 'from a' });
});
});

View file

@ -9,7 +9,7 @@
import { uniq } from 'lodash';
import {
type AstProviderFn,
parse,
type ESQLAstItem,
type ESQLCommand,
type ESQLCommandOption,
@ -87,13 +87,12 @@ export async function suggest(
fullText: string,
offset: number,
context: EditorContext,
astProvider: AstProviderFn,
resourceRetriever?: ESQLCallbacks
): Promise<SuggestionRawDefinition[]> {
// Partition out to inner ast / ast context for the latest command
const innerText = fullText.substring(0, offset);
const correctedQuery = correctQuerySyntax(innerText, context);
const { ast } = await astProvider(correctedQuery);
const { ast } = parse(correctedQuery, { withFormatting: true });
const astContext = getAstContext(innerText, ast, offset);
if (astContext.type === 'comment') {

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EditorError, ESQLMessage, getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { EditorError, ESQLMessage } from '@kbn/esql-ast';
import { ESQLCallbacks } from '../../shared/types';
import { getCallbackMocks } from '../../__tests__/helpers';
import { ValidationOptions } from '../types';
@ -29,7 +29,7 @@ export const setup = async () => {
opts: ValidationOptions = {},
cb: ESQLCallbacks = callbacks
) => {
return await validateQuery(query, getAstAndSyntaxErrors, opts, cb);
return await validateQuery(query, opts, cb);
};
const assertErrors = (errors: unknown[], expectedErrors: string[], query?: string) => {
@ -66,7 +66,7 @@ export const setup = async () => {
opts: ValidationOptions = {},
cb: ESQLCallbacks = callbacks
) => {
const { errors, warnings } = await validateQuery(query, getAstAndSyntaxErrors, opts, cb);
const { errors, warnings } = await validateQuery(query, opts, cb);
assertErrors(errors, expectedErrors, query);
if (expectedWarnings) {
assertErrors(warnings, expectedWarnings, query);

View file

@ -7,34 +7,33 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { join } from 'path';
import { writeFile, readFile } from 'fs/promises';
import { ignoreErrorsMap, validateQuery } from './validation';
import { scalarFunctionDefinitions } from '../definitions/generated/scalar_functions';
import { getFunctionSignatures } from '../definitions/helpers';
import {
FieldType,
FunctionDefinition,
SupportedDataType,
dataTypes,
fieldTypes as _fieldTypes,
} from '../definitions/types';
import { timeUnits, timeUnitsToSuggest } from '../definitions/literals';
import { aggFunctionDefinitions } from '../definitions/generated/aggregation_functions';
import capitalize from 'lodash/capitalize';
import { readFile, writeFile } from 'fs/promises';
import { camelCase } from 'lodash';
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { nonNullable } from '../shared/helpers';
import capitalize from 'lodash/capitalize';
import { join } from 'path';
import {
fields,
enrichFields,
fields,
getCallbackMocks,
indexes,
policies,
unsupported_field,
} from '../__tests__/helpers';
import { validationFromCommandTestSuite as runFromTestSuite } from './__tests__/test_suites/validation.command.from';
import { aggFunctionDefinitions } from '../definitions/generated/aggregation_functions';
import { scalarFunctionDefinitions } from '../definitions/generated/scalar_functions';
import { getFunctionSignatures } from '../definitions/helpers';
import { timeUnits, timeUnitsToSuggest } from '../definitions/literals';
import {
FieldType,
FunctionDefinition,
SupportedDataType,
fieldTypes as _fieldTypes,
dataTypes,
} from '../definitions/types';
import { nonNullable } from '../shared/helpers';
import { Setup, setup } from './__tests__/helpers';
import { validationFromCommandTestSuite as runFromTestSuite } from './__tests__/test_suites/validation.command.from';
import { ignoreErrorsMap, validateQuery } from './validation';
const fieldTypes = _fieldTypes.filter((type) => type !== 'unsupported');
@ -226,7 +225,7 @@ describe('validation logic', () => {
const callbackMocks = getCallbackMocks();
const { warnings, errors } = await validateQuery(
statement,
getAstAndSyntaxErrors,
undefined,
callbackMocks
);
@ -1547,20 +1546,20 @@ describe('validation logic', () => {
describe('callbacks', () => {
it(`should not fetch source and fields list when a row command is set`, async () => {
const callbackMocks = getCallbackMocks();
await validateQuery(`row a = 1 | eval a`, getAstAndSyntaxErrors, undefined, callbackMocks);
await validateQuery(`row a = 1 | eval a`, undefined, callbackMocks);
expect(callbackMocks.getColumnsFor).not.toHaveBeenCalled();
expect(callbackMocks.getSources).not.toHaveBeenCalled();
});
it(`should not fetch policies if no enrich command is found`, async () => {
const callbackMocks = getCallbackMocks();
await validateQuery(`row a = 1 | eval a`, getAstAndSyntaxErrors, undefined, callbackMocks);
await validateQuery(`row a = 1 | eval a`, undefined, callbackMocks);
expect(callbackMocks.getPolicies).not.toHaveBeenCalled();
});
it(`should not fetch source and fields for empty command`, async () => {
const callbackMocks = getCallbackMocks();
await validateQuery(` `, getAstAndSyntaxErrors, undefined, callbackMocks);
await validateQuery(` `, undefined, callbackMocks);
expect(callbackMocks.getColumnsFor).not.toHaveBeenCalled();
expect(callbackMocks.getSources).not.toHaveBeenCalled();
});
@ -1569,7 +1568,7 @@ describe('validation logic', () => {
const callbackMocks = getCallbackMocks();
await validateQuery(
`row a = 1 | eval b = a | enrich policy`,
getAstAndSyntaxErrors,
undefined,
callbackMocks
);
@ -1583,12 +1582,7 @@ describe('validation logic', () => {
it('should call fields callbacks also for show command', async () => {
const callbackMocks = getCallbackMocks();
await validateQuery(
`show info | keep name`,
getAstAndSyntaxErrors,
undefined,
callbackMocks
);
await validateQuery(`show info | keep name`, undefined, callbackMocks);
expect(callbackMocks.getSources).not.toHaveBeenCalled();
expect(callbackMocks.getPolicies).not.toHaveBeenCalled();
expect(callbackMocks.getColumnsFor).toHaveBeenCalledTimes(1);
@ -1601,7 +1595,7 @@ describe('validation logic', () => {
const callbackMocks = getCallbackMocks();
await validateQuery(
`from a_index | eval b = a | enrich policy`,
getAstAndSyntaxErrors,
undefined,
callbackMocks
);
@ -1617,7 +1611,7 @@ describe('validation logic', () => {
try {
await validateQuery(
`from a_index | eval b = a | enrich policy | dissect textField "%{firstWord}"`,
getAstAndSyntaxErrors,
undefined,
{
getColumnsFor: undefined,
@ -1633,8 +1627,7 @@ describe('validation logic', () => {
it(`should not crash if no callbacks are passed`, async () => {
try {
await validateQuery(
`from a_index | eval b = a | enrich policy | dissect textField "%{firstWord}"`,
getAstAndSyntaxErrors
`from a_index | eval b = a | enrich policy | dissect textField "%{firstWord}"`
);
} catch {
fail('Should not throw');
@ -1773,7 +1766,7 @@ describe('validation logic', () => {
.map(({ query }) =>
validateQuery(
query,
getAstAndSyntaxErrors,
{ ignoreOnMissingCallbacks: true },
getCallbackMocks()
)
@ -1801,7 +1794,6 @@ describe('validation logic', () => {
filteredTestCases.map(({ query }) =>
validateQuery(
query,
getAstAndSyntaxErrors,
{ ignoreOnMissingCallbacks: true },
getPartialCallbackMocks(excludedCallback)
)
@ -1826,7 +1818,7 @@ describe('validation logic', () => {
excludeErrorsByContent(excludedCallbacks).every((regexp) => regexp?.test(message))
)
)) {
const { errors } = await validateQuery(testCase.query, getAstAndSyntaxErrors, {
const { errors } = await validateQuery(testCase.query, {
ignoreOnMissingCallbacks: true,
});
expect(

View file

@ -8,7 +8,6 @@
*/
import {
AstProviderFn,
ESQLAst,
ESQLAstTimeseriesCommand,
ESQLColumn,
@ -17,6 +16,7 @@ import {
ESQLMessage,
ESQLSource,
isIdentifier,
parse,
walk,
} from '@kbn/esql-ast';
import type { ESQLAstJoinCommand, ESQLIdentifier } from '@kbn/esql-ast/src/types';
@ -65,11 +65,10 @@ import { validate as validateTimeseriesCommand } from './commands/metrics';
*/
export async function validateQuery(
queryString: string,
astProvider: AstProviderFn,
options: ValidationOptions = {},
callbacks?: ESQLCallbacks
): Promise<ValidationResult> {
const result = await validateAst(queryString, astProvider, callbacks);
const result = await validateAst(queryString, callbacks);
// early return if we do not want to ignore errors
if (!options.ignoreOnMissingCallbacks) {
return result;
@ -128,12 +127,11 @@ export const ignoreErrorsMap: Record<keyof ESQLCallbacks, ErrorTypes[]> = {
*/
async function validateAst(
queryString: string,
astProvider: AstProviderFn,
callbacks?: ESQLCallbacks
): Promise<ValidationResult> {
const messages: ESQLMessage[] = [];
const parsingResult = await astProvider(queryString);
const parsingResult = parse(queryString);
const { ast } = parsingResult;

View file

@ -7,20 +7,19 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { ESQLCallbacks } from '@kbn/esql-validation-autocomplete';
import { validateQuery, type ESQLCallbacks, suggest } from '@kbn/esql-validation-autocomplete';
import { monaco } from '../../monaco_imports';
import { ESQL_LANG_ID } from './lib/constants';
import type { CustomLangModuleType } from '../../types';
import type { ESQLWorker } from './worker/esql_worker';
import { WorkerProxyService } from '../../common/worker_proxy';
import { buildESQLTheme } from './lib/esql_theme';
import { ESQLAstAdapter } from './lib/esql_ast_provider';
import { wrapAsMonacoSuggestions } from './lib/converters/suggestions';
import { wrapAsMonacoMessages } from './lib/converters/positions';
import { getHoverItem } from './lib/hover/hover';
import { monacoPositionToOffset } from './lib/shared/utils';
const workerProxyService = new WorkerProxyService<ESQLWorker>();
const removeKeywordSuffix = (name: string) => {
return name.endsWith('.keyword') ? name.slice(0, -8) : name;
};
@ -30,8 +29,6 @@ export const ESQLLang: CustomLangModuleType<ESQLCallbacks> = {
async onLanguage() {
const { ESQLTokensProvider } = await import('./lib');
workerProxyService.setup(ESQL_LANG_ID);
monaco.languages.setTokensProvider(ESQL_LANG_ID, new ESQLTokensProvider());
},
languageThemeResolver: buildESQLTheme,
@ -55,28 +52,11 @@ export const ESQLLang: CustomLangModuleType<ESQLCallbacks> = {
],
},
validate: async (model: monaco.editor.ITextModel, code: string, callbacks?: ESQLCallbacks) => {
const astAdapter = new ESQLAstAdapter(
(...uris) => workerProxyService.getWorker(uris),
callbacks
);
return await astAdapter.validate(model, code);
},
getSignatureProvider: (callbacks?: ESQLCallbacks): monaco.languages.SignatureHelpProvider => {
return {
signatureHelpTriggerCharacters: [' ', '('],
async provideSignatureHelp(
model: monaco.editor.ITextModel,
position: monaco.Position,
_token: monaco.CancellationToken,
context: monaco.languages.SignatureHelpContext
) {
const astAdapter = new ESQLAstAdapter(
(...uris) => workerProxyService.getWorker(uris),
callbacks
);
return astAdapter.suggestSignature(model, position, context);
},
};
const text = code ?? model.getValue();
const { errors, warnings } = await validateQuery(text, undefined, callbacks);
const monacoErrors = wrapAsMonacoMessages(text, errors);
const monacoWarnings = wrapAsMonacoMessages(text, warnings);
return { errors: monacoErrors, warnings: monacoWarnings };
},
getHoverProvider: (callbacks?: ESQLCallbacks): monaco.languages.HoverProvider => {
return {
@ -85,11 +65,7 @@ export const ESQLLang: CustomLangModuleType<ESQLCallbacks> = {
position: monaco.Position,
token: monaco.CancellationToken
) {
const astAdapter = new ESQLAstAdapter(
(...uris) => workerProxyService.getWorker(uris),
callbacks
);
return astAdapter.getHover(model, position, token);
return getHoverItem(model, position, token, callbacks);
},
};
},
@ -101,14 +77,12 @@ export const ESQLLang: CustomLangModuleType<ESQLCallbacks> = {
position: monaco.Position,
context: monaco.languages.CompletionContext
): Promise<monaco.languages.CompletionList> {
const astAdapter = new ESQLAstAdapter(
(...uris) => workerProxyService.getWorker(uris),
callbacks
);
const suggestions = await astAdapter.autocomplete(model, position, context);
const fullText = model.getValue();
const offset = monacoPositionToOffset(fullText, position);
const suggestions = await suggest(fullText, offset, context, callbacks);
return {
// @ts-expect-error because of range typing: https://github.com/microsoft/monaco-editor/issues/4638
suggestions: wrapAsMonacoSuggestions(suggestions),
suggestions: wrapAsMonacoSuggestions(suggestions, fullText),
};
},
async resolveCompletionItem(item, token): Promise<monaco.languages.CompletionItem> {

View file

@ -7,14 +7,14 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { SuggestionRawDefinition } from '@kbn/esql-validation-autocomplete';
import { monaco } from '../../../../monaco_imports';
import {
MonacoAutocompleteCommandDefinition,
SuggestionRawDefinitionWithMonacoRange,
} from '../types';
import { MonacoAutocompleteCommandDefinition } from '../types';
import { offsetRangeToMonacoRange } from '../shared/utils';
export function wrapAsMonacoSuggestions(
suggestions: SuggestionRawDefinitionWithMonacoRange[]
suggestions: SuggestionRawDefinition[],
fullText: string
): MonacoAutocompleteCommandDefinition[] {
return suggestions.map<MonacoAutocompleteCommandDefinition>(
({
@ -27,7 +27,7 @@ export function wrapAsMonacoSuggestions(
sortText,
filterText,
command,
range,
rangeToReplace,
}) => {
const monacoSuggestion: MonacoAutocompleteCommandDefinition = {
label,
@ -44,7 +44,7 @@ export function wrapAsMonacoSuggestions(
insertTextRules: asSnippet
? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
: undefined,
range,
range: rangeToReplace ? offsetRangeToMonacoRange(fullText, rangeToReplace) : undefined,
};
return monacoSuggestion;
}

View file

@ -1,78 +0,0 @@
/*
* 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 { type ESQLCallbacks, suggest, validateQuery } from '@kbn/esql-validation-autocomplete';
import { monaco } from '../../../monaco_imports';
import type { ESQLWorker } from '../worker/esql_worker';
import { wrapAsMonacoMessages } from './converters/positions';
import { getHoverItem } from './hover/hover';
import { monacoPositionToOffset, offsetRangeToMonacoRange } from './shared/utils';
import { getSignatureHelp } from './signature';
import { SuggestionRawDefinitionWithMonacoRange } from './types';
export class ESQLAstAdapter {
constructor(
private worker: (...uris: monaco.Uri[]) => Promise<ESQLWorker>,
private callbacks?: ESQLCallbacks
) {}
private async getAstWorker(model: monaco.editor.ITextModel) {
const worker = await this.worker(model.uri);
return worker.getAst;
}
async getAst(model: monaco.editor.ITextModel, code?: string) {
const getAstFn = await this.getAstWorker(model);
return getAstFn(code ?? model.getValue());
}
async validate(model: monaco.editor.ITextModel, code: string) {
const getAstFn = await this.getAstWorker(model);
const text = code ?? model.getValue();
const { errors, warnings } = await validateQuery(text, getAstFn, undefined, this.callbacks);
const monacoErrors = wrapAsMonacoMessages(text, errors);
const monacoWarnings = wrapAsMonacoMessages(text, warnings);
return { errors: monacoErrors, warnings: monacoWarnings };
}
async suggestSignature(
model: monaco.editor.ITextModel,
position: monaco.Position,
context: monaco.languages.SignatureHelpContext
) {
const getAstFn = await this.getAstWorker(model);
return getSignatureHelp(model, position, context, getAstFn);
}
async getHover(
model: monaco.editor.ITextModel,
position: monaco.Position,
token: monaco.CancellationToken
) {
const getAstFn = await this.getAstWorker(model);
return getHoverItem(model, position, token, getAstFn, this.callbacks);
}
async autocomplete(
model: monaco.editor.ITextModel,
position: monaco.Position,
context: monaco.languages.CompletionContext
): Promise<SuggestionRawDefinitionWithMonacoRange[]> {
const getAstFn = await this.getAstWorker(model);
const fullText = model.getValue();
const offset = monacoPositionToOffset(fullText, position);
const suggestions = await suggest(fullText, offset, context, getAstFn, this.callbacks);
for (const s of suggestions) {
(s as SuggestionRawDefinitionWithMonacoRange).range = s.rangeToReplace
? offsetRangeToMonacoRange(fullText, s.rangeToReplace)
: undefined;
}
return suggestions;
}
}

View file

@ -9,7 +9,6 @@
import { monaco } from '../../../../monaco_imports';
import { getHoverItem } from './hover';
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import {
ESQLRealField,
getFunctionDefinition,
@ -129,13 +128,7 @@ describe('hover', () => {
})=> ["${expected.join('","')}"]`,
async () => {
const callbackMocks = createCustomCallbackMocks(...customCallbacksArgs);
const { contents } = await getHoverItem(
model,
position,
token,
async (text) => (text ? getAstAndSyntaxErrors(text) : { ast: [], errors: [] }),
callbackMocks
);
const { contents } = await getHoverItem(model, position, token, callbackMocks);
expect(contents.map(({ value }) => value)).toEqual(expected);
}
);

View file

@ -8,7 +8,7 @@
*/
import { i18n } from '@kbn/i18n';
import type { AstProviderFn, ESQLAstItem } from '@kbn/esql-ast';
import { parse, type ESQLAstItem } from '@kbn/esql-ast';
import {
getAstContext,
getFunctionDefinition,
@ -45,8 +45,6 @@ const ACCEPTABLE_TYPES_HOVER = i18n.translate('monaco.esql.hover.acceptableTypes
async function getHoverItemForFunction(
model: monaco.editor.ITextModel,
position: monaco.Position,
token: monaco.CancellationToken,
astProvider: AstProviderFn,
resourceRetriever?: ESQLCallbacks
) {
const context: EditorContext = {
@ -59,7 +57,7 @@ async function getHoverItemForFunction(
const innerText = fullText.substring(0, offset);
const correctedQuery = correctQuerySyntax(innerText, context);
const { ast } = await astProvider(correctedQuery);
const { ast } = parse(correctedQuery);
const astContext = getAstContext(innerText, ast, offset);
const { node } = astContext;
@ -140,14 +138,13 @@ async function getHoverItemForFunction(
export async function getHoverItem(
model: monaco.editor.ITextModel,
position: monaco.Position,
token: monaco.CancellationToken,
astProvider: AstProviderFn,
_token: monaco.CancellationToken,
resourceRetriever?: ESQLCallbacks
) {
const fullText = model.getValue();
const offset = monacoPositionToOffset(fullText, position);
const { ast } = await astProvider(fullText);
const { ast } = parse(fullText);
const astContext = getAstContext(fullText, ast, offset);
const { getPolicyMetadata } = getPolicyHelper(resourceRetriever);
@ -162,13 +159,7 @@ export async function getHoverItem(
hoverContent.contents.push(...variablesContent);
}
const hoverItemsForFunction = await getHoverItemForFunction(
model,
position,
token,
astProvider,
resourceRetriever
);
const hoverItemsForFunction = await getHoverItemForFunction(model, position, resourceRetriever);
if (hoverItemsForFunction) {
hoverContent.contents.push(...hoverItemsForFunction.contents);
hoverContent.range = hoverItemsForFunction.range;

View file

@ -1,23 +0,0 @@
/*
* 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 type { AstProviderFn } from '@kbn/esql-ast';
import type { monaco } from '../../../../monaco_imports';
export function getSignatureHelp(
model: monaco.editor.ITextModel,
position: monaco.Position,
context: monaco.languages.SignatureHelpContext,
astProvider: AstProviderFn
): monaco.languages.SignatureHelpResult {
return {
value: { signatures: [], activeParameter: 0, activeSignature: 0 },
dispose: () => {},
};
}

View file

@ -7,7 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { SuggestionRawDefinition } from '@kbn/esql-validation-autocomplete';
import { monaco } from '../../../monaco_imports';
export type MonacoAutocompleteCommandDefinition = Pick<
@ -24,10 +23,3 @@ export type MonacoAutocompleteCommandDefinition = Pick<
> & { range?: monaco.IRange };
export type MonacoCodeAction = monaco.languages.CodeAction;
export type SuggestionRawDefinitionWithMonacoRange = Omit<
SuggestionRawDefinition,
'rangeToReplace'
> & {
range?: monaco.IRange;
};

View file

@ -1,25 +0,0 @@
/*
* 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".
*/
// This module is intended to be run inside of a webworker
/* eslint-disable @kbn/eslint/module_migration */
import '@babel/runtime/regenerator';
// @ts-ignore
import * as worker from 'monaco-editor/esm/vs/editor/editor.worker';
import { monaco } from '../../../monaco_imports';
import { ESQLWorker } from './esql_worker';
self.onmessage = () => {
worker.initialize((ctx: monaco.worker.IWorkerContext, createData: any) => {
return new ESQLWorker(ctx);
});
};

View file

@ -1,52 +0,0 @@
/*
* 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 { parse, parseErrors, type EditorError } from '@kbn/esql-ast';
import type { monaco } from '../../../monaco_imports';
import type { BaseWorkerDefinition } from '../../../types';
/**
* While this function looks similar to the wrapAsMonacoMessages one, it prevents from
* loading the whole monaco stuff within the WebWorker.
* Given that we're dealing only with EditorError objects here, and not other types, it is
* possible to use this simpler inline function to work.
*/
function inlineToMonacoErrors({ severity, ...error }: EditorError) {
return {
...error,
severity: severity === 'error' ? 8 : 4, // monaco.MarkerSeverity.Error : monaco.MarkerSeverity.Warning
};
}
export class ESQLWorker implements BaseWorkerDefinition {
private readonly _ctx: monaco.worker.IWorkerContext;
constructor(ctx: monaco.worker.IWorkerContext) {
this._ctx = ctx;
}
public async getSyntaxErrors(modelUri: string) {
const model = this._ctx.getMirrorModels().find((m) => m.uri.toString() === modelUri);
const text = model?.getValue();
if (!text) return [];
const errors = parseErrors(text);
return errors.map(inlineToMonacoErrors);
}
getAst(text: string | undefined) {
const rawAst = parse(text, { withFormatting: true });
return {
ast: rawAst.root.commands,
errors: rawAst.errors.map(inlineToMonacoErrors),
};
}
}

View file

@ -7,13 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import {
XJSON_LANG_ID,
PAINLESS_LANG_ID,
ESQL_LANG_ID,
CONSOLE_LANG_ID,
YAML_LANG_ID,
} from './languages';
import { XJSON_LANG_ID, PAINLESS_LANG_ID, CONSOLE_LANG_ID, YAML_LANG_ID } from './languages';
import { monaco } from './monaco_imports';
export const DEFAULT_WORKER_ID = 'default' as const;
@ -22,7 +16,6 @@ const langSpecificWorkerIds = [
monaco.languages.json.jsonDefaults.languageId,
XJSON_LANG_ID,
PAINLESS_LANG_ID,
ESQL_LANG_ID,
YAML_LANG_ID,
CONSOLE_LANG_ID,
] as const;

View file

@ -104,4 +104,4 @@ const workerConfig = (languages) => ({
},
});
module.exports = workerConfig(['default', 'json', 'xjson', 'painless', 'esql', 'yaml', 'console']);
module.exports = workerConfig(['default', 'json', 'xjson', 'painless', 'yaml', 'console']);

View file

@ -6,7 +6,7 @@
*/
import { run } from '@kbn/dev-cli-runner';
import { ESQLMessage, EditorError, getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { ESQLMessage, EditorError } from '@kbn/esql-ast';
import { validateQuery } from '@kbn/esql-validation-autocomplete';
import Fs from 'fs/promises';
import Path from 'path';
@ -156,7 +156,7 @@ const findEsqlSyntaxError = async (doc: FileToWrite): Promise<SyntaxError[]> =>
return Array.from(doc.content.matchAll(INLINE_ESQL_QUERY_REGEX)).reduce(
async (listP, [match, query]) => {
const list = await listP;
const { errors, warnings } = await validateQuery(query, getAstAndSyntaxErrors, {
const { errors, warnings } = await validateQuery(query, {
// setting this to true, we don't want to validate the index / fields existence
ignoreOnMissingCallbacks: true,
});

View file

@ -6,7 +6,6 @@
*/
import { validateQuery } from '@kbn/esql-validation-autocomplete';
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import type { ElasticsearchClient } from '@kbn/core/server';
import { ESQLSearchResponse, ESQLRow } from '@kbn/es-types';
import { esFieldTypeToKibanaFieldType } from '@kbn/field-types';
@ -25,7 +24,7 @@ export async function runAndValidateEsqlQuery({
error?: Error;
errorMessages?: string[];
}> {
const { errors } = await validateQuery(query, getAstAndSyntaxErrors, {
const { errors } = await validateQuery(query, {
// setting this to true, we don't want to validate the index / fields existence
ignoreOnMissingCallbacks: true,
});

View file

@ -6,7 +6,6 @@
*/
import { validateQuery } from '@kbn/esql-validation-autocomplete';
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
import type { ElasticsearchClient } from '@kbn/core/server';
import { ESQLSearchResponse, ESQLRow } from '@kbn/es-types';
import { esFieldTypeToKibanaFieldType } from '@kbn/field-types';
@ -29,7 +28,7 @@ export async function runAndValidateEsqlQuery({
}> {
const queryWithoutLineBreaks = query.replaceAll(/\n/g, '');
const { errors } = await validateQuery(queryWithoutLineBreaks, getAstAndSyntaxErrors, {
const { errors } = await validateQuery(queryWithoutLineBreaks, {
// setting this to true, we don't want to validate the index / fields existence
ignoreOnMissingCallbacks: true,
});