mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[ES|QL] Use Monaco Monarch ES|QL grammar for highlighting (#219394)
## Summary Closes https://github.com/elastic/kibana/issues/217784 Closes https://github.com/elastic/kibana/issues/197250 - Adds Monarch grammar from `@elastic/monaco-esql` for ES|QL highlighting in the Kibana Monaco editor. - Tested to work in dark mode. - Function list is injected from Kibana, as we expect it to change frequently. Sample query: ``` FROM kibana_sample_data_ecommerce, "kibana_sample_data_ecommerce", """kibana_sample_data_ecommerce""" /* this is a comment */ | WHERE order_date >= ?_tstart AND @timestamp <= ?_tend | LIMIT 10 /* this is another comment */ | FORK // This is a comment (WHERE currency == "EUR" AND day_of_week_i == AVG(123, "asdf", {"this": "is", "map": 123})) (WHERE customer_gender == ?asdf) (WHERE category.??param == [TRUE, FALSE]) (WHERE category.`keyword` == NULL) (WHERE category.keyword == (1.5)::INTEGER) (LIMIT 123) ``` Dark: <img width="887" alt="image" src="https://github.com/user-attachments/assets/5c9b3850-f3d4-4fb8-893d-362e85567ea2" /> Light: <img width="897" alt="image" src="https://github.com/user-attachments/assets/22d8793c-dcb4-4d97-9d95-00eacb2bf9c1" /> --------- Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
362a25a1e7
commit
4416bc8bf5
15 changed files with 379 additions and 517 deletions
|
@ -128,6 +128,7 @@
|
|||
"@elastic/eui": "102.0.0",
|
||||
"@elastic/eui-theme-borealis": "1.0.0",
|
||||
"@elastic/filesaver": "1.1.2",
|
||||
"@elastic/monaco-esql": "^3.1.0",
|
||||
"@elastic/node-crypto": "^1.2.3",
|
||||
"@elastic/numeral": "^2.5.1",
|
||||
"@elastic/react-search-ui": "^1.20.2",
|
||||
|
|
|
@ -4416,6 +4416,24 @@
|
|||
"minimumReleaseAge": "7 days",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"groupName": "Kibana ES|QL Highlighting",
|
||||
"matchDepNames": [
|
||||
"@elastic/monaco-esql"
|
||||
],
|
||||
"reviewers": [
|
||||
"team:kibana-esql"
|
||||
],
|
||||
"matchBaseBranches": [
|
||||
"main"
|
||||
],
|
||||
"labels": [
|
||||
"Team:ESQL",
|
||||
"release_note:skip",
|
||||
"backport:version"
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"groupName": "re2js",
|
||||
"matchDepNames": [
|
||||
|
|
|
@ -62,3 +62,5 @@ export {
|
|||
} from './src/shared/resources_helpers';
|
||||
|
||||
export { getRecommendedQueries } from './src/autocomplete/recommended_queries/templates';
|
||||
|
||||
export { esqlFunctionNames } from './src/definitions/generated/function_names';
|
||||
|
|
|
@ -951,6 +951,32 @@ ${
|
|||
|
||||
const allFunctionDefinitions = ESFunctionDefinitions.concat(ESFOperatorDefinitions);
|
||||
|
||||
const functionNames = allFunctionDefinitions.map((def) => def.name.toUpperCase());
|
||||
await writeFile(
|
||||
join(__dirname, '../src/definitions/generated/function_names.ts'),
|
||||
`/**
|
||||
* __AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.__
|
||||
*
|
||||
* @note This file is generated by the \`generate_function_definitions.ts\`
|
||||
* script. Do not edit it manually.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
export const esqlFunctionNames = ${JSON.stringify(functionNames, null, 2)};
|
||||
`
|
||||
);
|
||||
|
||||
const scalarFunctionDefinitions: FunctionDefinition[] = [];
|
||||
const aggFunctionDefinitions: FunctionDefinition[] = [];
|
||||
const operatorDefinitions: FunctionDefinition[] = [];
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* 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".
|
||||
*/
|
||||
|
||||
/**
|
||||
* __AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.__
|
||||
*
|
||||
* @note This file is generated by the `generate_function_definitions.ts`
|
||||
* script. Do not edit it manually.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
export const esqlFunctionNames = [
|
||||
'ABS',
|
||||
'ACOS',
|
||||
'ASIN',
|
||||
'ATAN',
|
||||
'ATAN2',
|
||||
'AVG',
|
||||
'BIT_LENGTH',
|
||||
'BUCKET',
|
||||
'BYTE_LENGTH',
|
||||
'CASE',
|
||||
'CATEGORIZE',
|
||||
'CBRT',
|
||||
'CEIL',
|
||||
'CIDR_MATCH',
|
||||
'COALESCE',
|
||||
'CONCAT',
|
||||
'COS',
|
||||
'COSH',
|
||||
'COUNT',
|
||||
'COUNT_DISTINCT',
|
||||
'DATE_DIFF',
|
||||
'DATE_EXTRACT',
|
||||
'DATE_FORMAT',
|
||||
'DATE_PARSE',
|
||||
'DATE_TRUNC',
|
||||
'E',
|
||||
'ENDS_WITH',
|
||||
'EXP',
|
||||
'FLOOR',
|
||||
'FROM_BASE64',
|
||||
'GREATEST',
|
||||
'HASH',
|
||||
'HYPOT',
|
||||
'IP_PREFIX',
|
||||
'KQL',
|
||||
'LEAST',
|
||||
'LEFT',
|
||||
'LENGTH',
|
||||
'LOCATE',
|
||||
'LOG',
|
||||
'LOG10',
|
||||
'LTRIM',
|
||||
'MATCH',
|
||||
'MAX',
|
||||
'MD5',
|
||||
'MEDIAN',
|
||||
'MEDIAN_ABSOLUTE_DEVIATION',
|
||||
'MIN',
|
||||
'MULTI_MATCH',
|
||||
'MV_APPEND',
|
||||
'MV_AVG',
|
||||
'MV_CONCAT',
|
||||
'MV_COUNT',
|
||||
'MV_DEDUPE',
|
||||
'MV_FIRST',
|
||||
'MV_LAST',
|
||||
'MV_MAX',
|
||||
'MV_MEDIAN',
|
||||
'MV_MEDIAN_ABSOLUTE_DEVIATION',
|
||||
'MV_MIN',
|
||||
'MV_PERCENTILE',
|
||||
'MV_PSERIES_WEIGHTED_SUM',
|
||||
'MV_SLICE',
|
||||
'MV_SORT',
|
||||
'MV_SUM',
|
||||
'MV_ZIP',
|
||||
'NOW',
|
||||
'PERCENTILE',
|
||||
'PI',
|
||||
'POW',
|
||||
'QSTR',
|
||||
'REPEAT',
|
||||
'REPLACE',
|
||||
'REVERSE',
|
||||
'RIGHT',
|
||||
'ROUND',
|
||||
'RTRIM',
|
||||
'SHA1',
|
||||
'SHA256',
|
||||
'SIGNUM',
|
||||
'SIN',
|
||||
'SINH',
|
||||
'SPACE',
|
||||
'SPLIT',
|
||||
'SQRT',
|
||||
'ST_CENTROID_AGG',
|
||||
'ST_CONTAINS',
|
||||
'ST_DISJOINT',
|
||||
'ST_DISTANCE',
|
||||
'ST_ENVELOPE',
|
||||
'ST_EXTENT_AGG',
|
||||
'ST_INTERSECTS',
|
||||
'ST_WITHIN',
|
||||
'ST_X',
|
||||
'ST_XMAX',
|
||||
'ST_XMIN',
|
||||
'ST_Y',
|
||||
'ST_YMAX',
|
||||
'ST_YMIN',
|
||||
'STARTS_WITH',
|
||||
'STD_DEV',
|
||||
'SUBSTRING',
|
||||
'SUM',
|
||||
'TAN',
|
||||
'TANH',
|
||||
'TAU',
|
||||
'TERM',
|
||||
'TO_AGGREGATE_METRIC_DOUBLE',
|
||||
'TO_BASE64',
|
||||
'TO_BOOLEAN',
|
||||
'TO_CARTESIANPOINT',
|
||||
'TO_CARTESIANSHAPE',
|
||||
'TO_DATE_NANOS',
|
||||
'TO_DATEPERIOD',
|
||||
'TO_DATETIME',
|
||||
'TO_DEGREES',
|
||||
'TO_DOUBLE',
|
||||
'TO_GEOPOINT',
|
||||
'TO_GEOSHAPE',
|
||||
'TO_INTEGER',
|
||||
'TO_IP',
|
||||
'TO_LONG',
|
||||
'TO_LOWER',
|
||||
'TO_RADIANS',
|
||||
'TO_STRING',
|
||||
'TO_TIMEDURATION',
|
||||
'TO_UNSIGNED_LONG',
|
||||
'TO_UPPER',
|
||||
'TO_VERSION',
|
||||
'TOP',
|
||||
'TRIM',
|
||||
'VALUES',
|
||||
'WEIGHTED_AVG',
|
||||
'ADD',
|
||||
'CAST',
|
||||
'DIV',
|
||||
'EQUALS',
|
||||
'GREATER_THAN',
|
||||
'GREATER_THAN_OR_EQUAL',
|
||||
'IN',
|
||||
'IS_NOT_NULL',
|
||||
'IS_NULL',
|
||||
'LESS_THAN',
|
||||
'LESS_THAN_OR_EQUAL',
|
||||
'LIKE',
|
||||
'MATCH_OPERATOR',
|
||||
'MOD',
|
||||
'MUL',
|
||||
'NEG',
|
||||
'NOT_IN',
|
||||
'NOT_LIKE',
|
||||
'NOT_RLIKE',
|
||||
'NOT_EQUALS',
|
||||
'PREDICATES',
|
||||
'RLIKE',
|
||||
'SUB',
|
||||
];
|
|
@ -32,6 +32,7 @@ SHARED_DEPS = [
|
|||
"@npm//antlr4",
|
||||
"@npm//monaco-editor",
|
||||
"@npm//monaco-yaml",
|
||||
"@npm//@elastic/monaco-esql",
|
||||
]
|
||||
|
||||
webpack_cli(
|
||||
|
|
|
@ -9,4 +9,3 @@
|
|||
|
||||
export { ESQL_LANG_ID, ESQL_DARK_THEME_ID, ESQL_LIGHT_THEME_ID } from './lib/constants';
|
||||
export { ESQLLang } from './language';
|
||||
export { buildESQLTheme } from './lib/esql_theme';
|
||||
|
|
|
@ -7,18 +7,22 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { validateQuery, type ESQLCallbacks, suggest } from '@kbn/esql-validation-autocomplete';
|
||||
import {
|
||||
validateQuery,
|
||||
type ESQLCallbacks,
|
||||
suggest,
|
||||
esqlFunctionNames,
|
||||
} from '@kbn/esql-validation-autocomplete';
|
||||
import { monarch } from '@elastic/monaco-esql';
|
||||
import * as monarchDefinitions from '@elastic/monaco-esql/lib/definitions';
|
||||
import { monaco } from '../../monaco_imports';
|
||||
|
||||
import { ESQL_LANG_ID } from './lib/constants';
|
||||
|
||||
import type { CustomLangModuleType } from '../../types';
|
||||
|
||||
import { buildESQLTheme } from './lib/esql_theme';
|
||||
import { buildEsqlTheme } from './lib/theme';
|
||||
import { wrapAsMonacoSuggestions } from './lib/converters/suggestions';
|
||||
import { wrapAsMonacoMessages } from './lib/converters/positions';
|
||||
import { getHoverItem } from './lib/hover/hover';
|
||||
import { monacoPositionToOffset } from './lib/shared/utils';
|
||||
import type { CustomLangModuleType } from '../../types';
|
||||
|
||||
const removeKeywordSuffix = (name: string) => {
|
||||
return name.endsWith('.keyword') ? name.slice(0, -8) : name;
|
||||
|
@ -27,11 +31,14 @@ const removeKeywordSuffix = (name: string) => {
|
|||
export const ESQLLang: CustomLangModuleType<ESQLCallbacks> = {
|
||||
ID: ESQL_LANG_ID,
|
||||
async onLanguage() {
|
||||
const { ESQLTokensProvider } = await import('./lib');
|
||||
const language = monarch.create({
|
||||
...monarchDefinitions,
|
||||
functions: esqlFunctionNames,
|
||||
});
|
||||
|
||||
monaco.languages.setTokensProvider(ESQL_LANG_ID, new ESQLTokensProvider());
|
||||
monaco.languages.setMonarchTokensProvider(ESQL_LANG_ID, language);
|
||||
},
|
||||
languageThemeResolver: buildESQLTheme,
|
||||
languageThemeResolver: buildEsqlTheme,
|
||||
languageConfiguration: {
|
||||
brackets: [
|
||||
['(', ')'],
|
||||
|
|
|
@ -1,141 +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 { ESQLErrorListener, getLexer as _getLexer } from '@kbn/esql-ast';
|
||||
import type { UseEuiTheme } from '@elastic/eui';
|
||||
import { ESQL_TOKEN_POSTFIX } from './constants';
|
||||
import { buildESQLTheme } from './esql_theme';
|
||||
import { CharStreams } from 'antlr4';
|
||||
|
||||
const mockTheme: UseEuiTheme = {
|
||||
colorMode: 'DARK',
|
||||
euiTheme: { colors: {} } as unknown as UseEuiTheme['euiTheme'],
|
||||
modifications: {},
|
||||
highContrastMode: false,
|
||||
};
|
||||
|
||||
describe('ESQL Theme', () => {
|
||||
it('should not have multiple rules for a single token', () => {
|
||||
const theme = buildESQLTheme(mockTheme);
|
||||
|
||||
const seen = new Set<string>();
|
||||
const duplicates: string[] = [];
|
||||
for (const rule of theme.rules) {
|
||||
if (seen.has(rule.token)) {
|
||||
duplicates.push(rule.token);
|
||||
}
|
||||
seen.add(rule.token);
|
||||
}
|
||||
|
||||
expect(duplicates).toEqual([]);
|
||||
});
|
||||
|
||||
const getLexer = () => {
|
||||
const errorListener = new ESQLErrorListener();
|
||||
const inputStream = CharStreams.fromString('FROM foo');
|
||||
return _getLexer(inputStream, errorListener);
|
||||
};
|
||||
|
||||
const lexer = getLexer();
|
||||
const lexicalNames = lexer.symbolicNames
|
||||
.filter((name) => typeof name === 'string')
|
||||
.map((name) => name!.toLowerCase());
|
||||
|
||||
it('every rule should apply to a valid lexical name', () => {
|
||||
const theme = buildESQLTheme(mockTheme);
|
||||
|
||||
// These names aren't from the lexer... they are added on our side
|
||||
// see src/platform/packages/shared/kbn-monaco/src/esql/lib/esql_token_helpers.ts
|
||||
const syntheticNames = ['functions', 'nulls_order', 'timespan_literal'];
|
||||
|
||||
const rulesWithNoName: string[] = [];
|
||||
for (const rule of theme.rules) {
|
||||
const token = rule.token.replace(ESQL_TOKEN_POSTFIX, '');
|
||||
if (![...lexicalNames, ...syntheticNames].includes(token)) {
|
||||
rulesWithNoName.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
if (rulesWithNoName.length) {
|
||||
throw new Error(
|
||||
`These rules have no corresponding lexical name: ${rulesWithNoName.join(', ')}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('every valid lexical name should have a corresponding rule', () => {
|
||||
const theme = buildESQLTheme(mockTheme);
|
||||
const tokenIDs = theme.rules.map((rule) => rule.token.replace(ESQL_TOKEN_POSTFIX, ''));
|
||||
|
||||
const validExceptions = [
|
||||
'unquoted_source',
|
||||
'false', // @TODO consider if this should get styling
|
||||
'true', // @TODO consider if this should get styling
|
||||
'info', // @TODO consider if this should get styling
|
||||
'colon', // @TODO consider if this should get styling
|
||||
|
||||
'nulls', // nulls is a part of nulls_order so it doesn't need its own rule
|
||||
'first', // first is a part of nulls_order so it doesn't need its own rule
|
||||
'last', // last is a part of nulls_order so it doesn't need its own rule
|
||||
|
||||
'id_pattern', // "KEEP <id_pattern>, <id_pattern>"... no styling needed
|
||||
'enrich_policy_name', // "ENRICH <enrich_policy_name>"
|
||||
'expr_ws', // whitespace, so no reason to style it
|
||||
'unknown_cmd', // unknown command, so no reason to style it
|
||||
|
||||
'explain_ws',
|
||||
'project_ws',
|
||||
'rename_ws',
|
||||
'from_ws',
|
||||
'enrich_ws',
|
||||
'mvexpand_ws',
|
||||
'enrich_field_ws',
|
||||
'lookup_ws',
|
||||
'lookup_field_ws',
|
||||
'show_ws',
|
||||
'setting',
|
||||
'setting_ws',
|
||||
'join_ws',
|
||||
'change_point_ws',
|
||||
'fork_ws',
|
||||
];
|
||||
|
||||
// First, check that every valid exception is actually valid
|
||||
const invalidExceptions: string[] = [];
|
||||
for (const name of validExceptions) {
|
||||
if (!lexicalNames.includes(name)) {
|
||||
invalidExceptions.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidExceptions.length) {
|
||||
throw new Error(
|
||||
`These rule requirement exceptions are not valid lexical names: ${invalidExceptions.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const namesToCheck = lexicalNames.filter((name) => !validExceptions.includes(name));
|
||||
|
||||
// Now, check that every lexical name has a corresponding rule
|
||||
const missingRules: string[] = [];
|
||||
for (const name of namesToCheck) {
|
||||
if (!tokenIDs.includes(name)) {
|
||||
missingRules.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingRules.length) {
|
||||
throw new Error(
|
||||
`These lexical names are missing corresponding rules: ${missingRules.join(', ')}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -1,199 +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 { UseEuiTheme } from '@elastic/eui';
|
||||
import { themeRuleGroupBuilderFactory } from '../../../common/theme';
|
||||
import { ESQL_TOKEN_POSTFIX } from './constants';
|
||||
import { monaco } from '../../../monaco_imports';
|
||||
|
||||
const buildRuleGroup = themeRuleGroupBuilderFactory(ESQL_TOKEN_POSTFIX);
|
||||
|
||||
export const buildESQLTheme = ({
|
||||
euiTheme,
|
||||
colorMode,
|
||||
}: UseEuiTheme): monaco.editor.IStandaloneThemeData => {
|
||||
return {
|
||||
base: colorMode === 'DARK' ? 'vs-dark' : 'vs',
|
||||
inherit: true,
|
||||
rules: [
|
||||
// base
|
||||
...buildRuleGroup(
|
||||
[
|
||||
'explain',
|
||||
'ws',
|
||||
'assign',
|
||||
'comma',
|
||||
'dot',
|
||||
'opening_bracket',
|
||||
'closing_bracket',
|
||||
'left_braces',
|
||||
'right_braces',
|
||||
'quoted_identifier',
|
||||
'unquoted_identifier',
|
||||
'pipe',
|
||||
],
|
||||
euiTheme.colors.textParagraph
|
||||
),
|
||||
|
||||
// source commands
|
||||
...buildRuleGroup(
|
||||
['from', 'row', 'show'],
|
||||
euiTheme.colors.primary,
|
||||
true // isBold
|
||||
),
|
||||
|
||||
// commands
|
||||
...buildRuleGroup(
|
||||
[
|
||||
'dev_time_series',
|
||||
'dev_rerank',
|
||||
'dev_fork',
|
||||
'dev_sample',
|
||||
'metadata',
|
||||
'mv_expand',
|
||||
'stats',
|
||||
'dev_inlinestats',
|
||||
'dissect',
|
||||
'grok',
|
||||
'keep',
|
||||
'rename',
|
||||
'drop',
|
||||
'eval',
|
||||
'sort',
|
||||
'by',
|
||||
'where',
|
||||
'not',
|
||||
'is',
|
||||
'like',
|
||||
'rlike',
|
||||
'in',
|
||||
'as',
|
||||
'limit',
|
||||
'dev_lookup',
|
||||
'dev_join_full',
|
||||
'dev_join_left',
|
||||
'dev_join_right',
|
||||
'null',
|
||||
'enrich',
|
||||
'on',
|
||||
'using',
|
||||
'with',
|
||||
'asc',
|
||||
'desc',
|
||||
'nulls_order',
|
||||
'join_lookup',
|
||||
'join',
|
||||
'change_point',
|
||||
'dev_insist',
|
||||
'dev_rrf',
|
||||
'dev_completion',
|
||||
],
|
||||
euiTheme.colors.accent,
|
||||
true // isBold
|
||||
),
|
||||
|
||||
// functions
|
||||
...buildRuleGroup(['functions'], euiTheme.colors.primary),
|
||||
|
||||
// operators
|
||||
...buildRuleGroup(
|
||||
[
|
||||
'or',
|
||||
'and',
|
||||
'rp', // ')'
|
||||
'lp', // '('
|
||||
'eq', // '=='
|
||||
'cieq', // '=~'
|
||||
'neq', // '!='
|
||||
'lt', // '<'
|
||||
'lte', // '<='
|
||||
'gt', // '>'
|
||||
'gte', // '>='
|
||||
'plus', // '+'
|
||||
'minus', // '-'
|
||||
'asterisk', // '*'
|
||||
'slash', // '/'
|
||||
'percent', // '%'
|
||||
'cast_op', // '::'
|
||||
],
|
||||
euiTheme.colors.primary
|
||||
),
|
||||
|
||||
// comments
|
||||
...buildRuleGroup(
|
||||
[
|
||||
'line_comment',
|
||||
'multiline_comment',
|
||||
'expr_line_comment',
|
||||
'expr_multiline_comment',
|
||||
'explain_line_comment',
|
||||
'explain_multiline_comment',
|
||||
'project_line_comment',
|
||||
'project_multiline_comment',
|
||||
'rename_line_comment',
|
||||
'rename_multiline_comment',
|
||||
'from_line_comment',
|
||||
'from_multiline_comment',
|
||||
'enrich_line_comment',
|
||||
'enrich_multiline_comment',
|
||||
'mvexpand_line_comment',
|
||||
'mvexpand_multiline_comment',
|
||||
'enrich_field_line_comment',
|
||||
'enrich_field_multiline_comment',
|
||||
'lookup_line_comment',
|
||||
'lookup_multiline_comment',
|
||||
'lookup_field_line_comment',
|
||||
'lookup_field_multiline_comment',
|
||||
'join_line_comment',
|
||||
'join_multiline_comment',
|
||||
'show_line_comment',
|
||||
'show_multiline_comment',
|
||||
'setting',
|
||||
'setting_line_comment',
|
||||
'settting_multiline_comment',
|
||||
'change_point_line_comment',
|
||||
'change_point_multiline_comment',
|
||||
'fork_line_comment',
|
||||
'fork_multiline_comment',
|
||||
],
|
||||
euiTheme.colors.textSubdued
|
||||
),
|
||||
|
||||
// values
|
||||
...buildRuleGroup(
|
||||
[
|
||||
'quoted_string',
|
||||
'integer_literal',
|
||||
'decimal_literal',
|
||||
'named_or_positional_param',
|
||||
'named_or_positional_double_params',
|
||||
'double_params',
|
||||
'param',
|
||||
'timespan_literal',
|
||||
],
|
||||
euiTheme.colors.textSuccess
|
||||
),
|
||||
],
|
||||
colors: {
|
||||
'editor.foreground': euiTheme.colors.textParagraph,
|
||||
'editor.background': euiTheme.colors.backgroundBasePlain,
|
||||
'editor.lineHighlightBackground': euiTheme.colors.lightestShade,
|
||||
'editor.lineHighlightBorder': euiTheme.colors.lightestShade,
|
||||
'editor.selectionHighlightBackground': euiTheme.colors.lightestShade,
|
||||
'editor.selectionHighlightBorder': euiTheme.colors.lightShade,
|
||||
'editorSuggestWidget.background': euiTheme.colors.emptyShade,
|
||||
'editorSuggestWidget.border': euiTheme.colors.emptyShade,
|
||||
'editorSuggestWidget.focusHighlightForeground': euiTheme.colors.emptyShade,
|
||||
'editorSuggestWidget.foreground': euiTheme.colors.textParagraph,
|
||||
'editorSuggestWidget.highlightForeground': euiTheme.colors.primary,
|
||||
'editorSuggestWidget.selectedBackground': euiTheme.colors.primary,
|
||||
'editorSuggestWidget.selectedForeground': euiTheme.colors.emptyShade,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,71 +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 { ESQLState } from './esql_state';
|
||||
import { ESQLToken } from './esql_token';
|
||||
import { ESQLTokensProvider } from './esql_tokens_provider';
|
||||
|
||||
describe('ES|QL Tokens Provider', () => {
|
||||
it('should tokenize a line', () => {
|
||||
const line = 'SELECT * FROM my_index';
|
||||
const prevState = new ESQLState();
|
||||
const provider = new ESQLTokensProvider();
|
||||
const { tokens } = provider.tokenize(line, prevState);
|
||||
expect(tokens.map((t) => t.scopes)).toEqual([
|
||||
'unknown_cmd.esql',
|
||||
'expr_ws.esql',
|
||||
'asterisk.esql',
|
||||
'expr_ws.esql',
|
||||
'unquoted_identifier.esql',
|
||||
'expr_ws.esql',
|
||||
'unquoted_identifier.esql',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should properly tokenize functions', () => {
|
||||
const line = 'FROM my_index | EVAL date_diff("day", NOW()) | STATS abs(field1), avg(field1)';
|
||||
const provider = new ESQLTokensProvider();
|
||||
const { tokens } = provider.tokenize(line, new ESQLState());
|
||||
const functionTokens = tokens.filter((t) => t.scopes === 'functions.esql');
|
||||
expect(functionTokens).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should properly tokenize SORT... NULLS clauses', () => {
|
||||
const line = 'SELECT * FROM my_index | SORT BY field1 ASC NULLS FIRST, field2 DESC NULLS LAST';
|
||||
const provider = new ESQLTokensProvider();
|
||||
const { tokens } = provider.tokenize(line, new ESQLState());
|
||||
// Make sure the tokens got merged properly
|
||||
const nullsOrderTokens = tokens.filter((t) => t.scopes === 'nulls_order.esql');
|
||||
expect(nullsOrderTokens).toHaveLength(2);
|
||||
expect(nullsOrderTokens).toEqual<ESQLToken[]>([
|
||||
{
|
||||
scopes: 'nulls_order.esql',
|
||||
startIndex: 44,
|
||||
stopIndex: 54,
|
||||
},
|
||||
{
|
||||
scopes: 'nulls_order.esql',
|
||||
startIndex: 69,
|
||||
stopIndex: 78,
|
||||
},
|
||||
]);
|
||||
// Ensure that the NULLS FIRST and NULLS LAST tokens are not present
|
||||
expect(tokens.map((t) => t.scopes)).not.toContain('nulls.esql');
|
||||
expect(tokens.map((t) => t.scopes)).not.toContain('first.esql');
|
||||
expect(tokens.map((t) => t.scopes)).not.toContain('last.esql');
|
||||
});
|
||||
|
||||
it('should properly tokenize timespan literals', () => {
|
||||
const line = 'SELECT * FROM my_index | WHERE date_field > 1 day AND other_field < 2 hours';
|
||||
const provider = new ESQLTokensProvider();
|
||||
const { tokens } = provider.tokenize(line, new ESQLState());
|
||||
const timespanTokens = tokens.filter((t) => t.scopes === 'timespan_literal.esql');
|
||||
expect(timespanTokens).toHaveLength(2);
|
||||
});
|
||||
});
|
|
@ -1,86 +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 { CharStreams, type Token } from 'antlr4';
|
||||
import { getLexer, ESQLErrorListener } from '@kbn/esql-ast';
|
||||
import { monaco } from '../../../monaco_imports';
|
||||
|
||||
import { ESQLToken } from './esql_token';
|
||||
import { ESQLLineTokens } from './esql_line_tokens';
|
||||
import { ESQLState } from './esql_state';
|
||||
|
||||
import { ESQL_TOKEN_POSTFIX } from './constants';
|
||||
import { addFunctionTokens, mergeTokens } from './esql_token_helpers';
|
||||
|
||||
const EOF = -1;
|
||||
|
||||
export class ESQLTokensProvider implements monaco.languages.TokensProvider {
|
||||
getInitialState(): monaco.languages.IState {
|
||||
return new ESQLState();
|
||||
}
|
||||
|
||||
tokenize(line: string, prevState: ESQLState): monaco.languages.ILineTokens {
|
||||
const errorStartingPoints: number[] = [];
|
||||
const errorListener = new ESQLErrorListener();
|
||||
// This has the drawback of not styling any ESQL wrong query as
|
||||
// | from ...
|
||||
const cleanedLine =
|
||||
prevState.getLineNumber() && line.trimStart()[0] === '|'
|
||||
? line.trimStart().substring(1)
|
||||
: line;
|
||||
const inputStream = CharStreams.fromString(cleanedLine);
|
||||
const lexer = getLexer(inputStream, errorListener);
|
||||
|
||||
let done = false;
|
||||
const myTokens: ESQLToken[] = [];
|
||||
|
||||
do {
|
||||
let token: Token | null;
|
||||
try {
|
||||
token = lexer.nextToken();
|
||||
} catch (e) {
|
||||
token = null;
|
||||
}
|
||||
|
||||
if (token == null) {
|
||||
done = true;
|
||||
} else {
|
||||
if (token.type === EOF) {
|
||||
done = true;
|
||||
} else {
|
||||
const tokenTypeName = lexer.symbolicNames[token.type];
|
||||
|
||||
if (tokenTypeName) {
|
||||
const indexOffset = cleanedLine === line ? 0 : line.length - cleanedLine.length;
|
||||
const myToken = new ESQLToken(
|
||||
tokenTypeName,
|
||||
token.start + indexOffset,
|
||||
token.stop + indexOffset
|
||||
);
|
||||
myTokens.push(myToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (!done);
|
||||
|
||||
for (const e of errorStartingPoints) {
|
||||
myTokens.push(new ESQLToken('error' + ESQL_TOKEN_POSTFIX, e));
|
||||
}
|
||||
|
||||
myTokens.sort((a, b) => a.startIndex - b.startIndex);
|
||||
|
||||
// special treatment for functions
|
||||
// the previous custom Kibana grammar baked functions directly as tokens, so highlight was easier
|
||||
// The ES grammar doesn't have the token concept of "function"
|
||||
const tokensWithFunctions = addFunctionTokens(myTokens);
|
||||
mergeTokens(tokensWithFunctions);
|
||||
|
||||
return new ESQLLineTokens(tokensWithFunctions, prevState.getLineNumber() + 1);
|
||||
}
|
||||
}
|
|
@ -1,10 +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".
|
||||
*/
|
||||
|
||||
export { ESQLTokensProvider } from './esql_tokens_provider';
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 { UseEuiTheme } from '@elastic/eui';
|
||||
import { monaco } from '../../../monaco_imports';
|
||||
|
||||
export const buildEsqlTheme = ({
|
||||
euiTheme,
|
||||
colorMode,
|
||||
}: UseEuiTheme): monaco.editor.IStandaloneThemeData => {
|
||||
const { colors } = euiTheme;
|
||||
|
||||
const rules: monaco.editor.IStandaloneThemeData['rules'] = [
|
||||
{ token: 'keyword', foreground: colors.primary },
|
||||
|
||||
// -------------------------------------------------------------- plain text
|
||||
|
||||
{ token: 'identifier', foreground: colors.textParagraph },
|
||||
{ token: 'delimiter', foreground: colors.textParagraph },
|
||||
{ token: 'source', foreground: colors.textParagraph },
|
||||
|
||||
// --------------------------------------------------- strings & string-like
|
||||
|
||||
{ token: 'string', foreground: colors.accent },
|
||||
|
||||
// ----------------------------------------------------------------- numbers
|
||||
|
||||
// Numbers, decimals, and time intervals
|
||||
{ token: 'number', foreground: colors.textSuccess },
|
||||
|
||||
// Constants, such as "true", "false", "null"
|
||||
{ token: 'keyword.literal', foreground: colors.accentSecondary },
|
||||
|
||||
// ------------------------------------------------------------------ params
|
||||
|
||||
// All params
|
||||
{ token: 'variable', foreground: colors.textSuccess },
|
||||
|
||||
// Override for unnamed param "?"
|
||||
{
|
||||
token: 'variable.name.unnamed',
|
||||
foreground: colors.textSuccess,
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
|
||||
// Override for named param "?name"
|
||||
{ token: 'variable.name.named', foreground: colors.textSuccess },
|
||||
|
||||
// Override for positional param "?123"
|
||||
{ token: 'variable.name.positional', foreground: colors.textSuccess },
|
||||
|
||||
// --------------------------------------------------------------- functions
|
||||
|
||||
{ token: 'identifier.function', foreground: colors.primary },
|
||||
|
||||
// --------------------------------------------------------- named operators
|
||||
|
||||
// Named operators such as "AND", "OR", "NOT" etc.
|
||||
{ token: 'keyword.operator', foreground: colors.primary },
|
||||
|
||||
// Type cast "::" operator
|
||||
{ token: 'type', foreground: colors.primary },
|
||||
|
||||
// ---------------------------------------------------------------- comments
|
||||
|
||||
// All comments, single line "// asdf" and multi line "/* asdf */"
|
||||
{ token: 'comment', foreground: colors.textSubdued },
|
||||
|
||||
// ---------------------------------------------------------------- commands
|
||||
|
||||
// All commands. (Below is an override for *source* commands).
|
||||
{
|
||||
token: 'keyword.command',
|
||||
foreground: colors.accent,
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
|
||||
// Source commands.
|
||||
{
|
||||
token: `keyword.command.source`,
|
||||
foreground: colors.primary,
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
|
||||
// Command option, such as "METADATA", "BY", etc..
|
||||
{
|
||||
token: 'keyword.option',
|
||||
foreground: colors.primary,
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
];
|
||||
|
||||
// `lightestShade` and `emptyShade` are deprecated, so we backfill them with
|
||||
// equivalent colors from the theme.
|
||||
const borderColor = colors.lightestShade || colors.borderBasePlain;
|
||||
const bgColor = colors.emptyShade || colors.backgroundBasePlain;
|
||||
|
||||
return {
|
||||
base: colorMode === 'DARK' ? 'vs-dark' : 'vs',
|
||||
inherit: true,
|
||||
rules,
|
||||
colors: {
|
||||
'editor.foreground': colors.textParagraph,
|
||||
'editor.background': colors.backgroundBasePlain,
|
||||
'editor.lineHighlightBackground': borderColor,
|
||||
'editor.lineHighlightBorder': borderColor,
|
||||
'editor.selectionHighlightBackground': borderColor,
|
||||
'editor.selectionHighlightBorder': borderColor,
|
||||
'editorSuggestWidget.background': bgColor,
|
||||
'editorSuggestWidget.border': bgColor,
|
||||
'editorSuggestWidget.focusHighlightForeground': bgColor,
|
||||
'editorSuggestWidget.foreground': colors.textParagraph,
|
||||
'editorSuggestWidget.highlightForeground': colors.primary,
|
||||
'editorSuggestWidget.selectedBackground': colors.primary,
|
||||
'editorSuggestWidget.selectedForeground': bgColor,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -2240,6 +2240,11 @@
|
|||
progress "^1.1.8"
|
||||
through2 "^2.0.0"
|
||||
|
||||
"@elastic/monaco-esql@^3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@elastic/monaco-esql/-/monaco-esql-3.1.0.tgz#4ebc59facffeea5fd371b36aa9ad6a0b79223c74"
|
||||
integrity sha512-vJ1fBwm/DvUgAaHg2I1lCrUVOH+mRxb7l7GVR7knDq7LXV9GVjfsDN5XhzFKKbruHhLfkdH1ld1rtSYFDdcnLA==
|
||||
|
||||
"@elastic/node-crypto@^1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@elastic/node-crypto/-/node-crypto-1.2.3.tgz#7ebd71964ea09cf085c713c1a6edfc2dfac08b29"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue