[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:
Vadim Kibana 2025-05-01 11:08:18 +02:00 committed by GitHub
parent 362a25a1e7
commit 4416bc8bf5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 379 additions and 517 deletions

View file

@ -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",

View file

@ -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": [

View file

@ -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';

View file

@ -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[] = [];

View file

@ -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',
];

View file

@ -32,6 +32,7 @@ SHARED_DEPS = [
"@npm//antlr4",
"@npm//monaco-editor",
"@npm//monaco-yaml",
"@npm//@elastic/monaco-esql",
]
webpack_cli(

View file

@ -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';

View file

@ -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: [
['(', ')'],

View file

@ -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(', ')}`
);
}
});
});

View file

@ -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,
},
};
};

View file

@ -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);
});
});

View file

@ -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);
}
}

View file

@ -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';

View file

@ -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,
},
};
};

View file

@ -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"