Skip to content

Commit

Permalink
[Console] Add theme and more lexer rules (#178757)
Browse files Browse the repository at this point in the history
## Summary

This PR adds a theme for the Console language in Monaco editor and adds
more lexer rules to bring the highlighting of the input closed to the
original in Ace editor.

### Screenshots
Monaco editor 
<img width="682" alt="Screenshot 2024-03-19 at 12 38 07"
src="https://github.com/elastic/kibana/assets/6585477/98a1acc7-3a8a-4ad9-a79e-5236091c4c39">

Ace editor
<img width="651" alt="Screenshot 2024-03-19 at 12 37 52"
src="https://github.com/elastic/kibana/assets/6585477/37935a68-923b-493c-ac56-ef4982f27fdf">

### How to test
1. Add `console.dev.enableMonaco: true` to `kibana.dev.yml``
2. Type different requests into Console and check that the highlighting
works the same as in Ace. For example, use the following requests

```
GET ${pathVariable}/_search
{
 "query": {
   "match": {
     "${bodyNameVariable}": "${bodyValueVariable}",
     "number_property": 1234,
     "array_property": ["test1", 1234, false], 
     "boolean_property": true,
     "text_property": "text_value",
     "triple_quote": """
     inside triple quote
     """
     // line comment
     /* 
      block comment
    */
   }
 }
}

// line comment
/* 
block comment
*/

GET _sql
{
  "query": """
  SELECT "field" FROM "index-*" WHERE "column" = "value"
  """
}
```
3. To check that `xjson` highlighting still works
 a. Navigate to Ingest pipelines and click the "create from csv" button
b. Load a valid csv file, for example this
[one](https://github.com/kgeller/ecs-mapper/blob/master/example/mapping.csv)

#### Known issues that will be addressed in follow up PRs
- SQL highlighting needs to be re-implemented (added to the follow up
list in #176926)
- Strings inside triple quotes are not using italics (added to the
follow up list in #176926)
- Font size needs to be set via settings and the default value provided
(fixed via #178982)
- Font family: do we want to use the same font as for other Monaco
languages are use the one for Ace? (added to the follow up list in
#176926)
- In the future, we might want to use the same theme for `xjson` and
Console (added to the follow up list in
#176926)

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
yuliacech and kibanamachine authored Mar 27, 2024
1 parent f320d56 commit c59016e
Show file tree
Hide file tree
Showing 15 changed files with 305 additions and 51 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-monaco/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ import { registerLanguage } from './src/helpers';
export { BarePluginApi, registerLanguage };
export * from './src/types';

export { CONSOLE_LANG_ID } from './src/console';
export { CONSOLE_LANG_ID, CONSOLE_THEME_ID } from './src/console';
16 changes: 16 additions & 0 deletions packages/kbn-monaco/src/common/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/

export const themeRuleGroupBuilderFactory =
(postfix: string = '') =>
(tokens: string[], color: string, isBold: boolean = false) =>
tokens.map((i) => ({
token: i + postfix,
foreground: color,
fontStyle: isBold ? 'bold' : '',
}));
2 changes: 2 additions & 0 deletions packages/kbn-monaco/src/console/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@
*/

export const CONSOLE_LANG_ID = 'console';
export const CONSOLE_THEME_ID = 'consoleTheme';
export const CONSOLE_POSTFIX = '.console';
4 changes: 3 additions & 1 deletion packages/kbn-monaco/src/console/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import type { LangModuleType } from '../types';
import { CONSOLE_LANG_ID } from './constants';
import { lexerRules, languageConfiguration } from './lexer_rules';

export { CONSOLE_LANG_ID } from './constants';
export { CONSOLE_LANG_ID, CONSOLE_THEME_ID } from './constants';

export { buildConsoleTheme } from './theme';

export const ConsoleLang: LangModuleType = {
ID: CONSOLE_LANG_ID,
Expand Down
205 changes: 175 additions & 30 deletions packages/kbn-monaco/src/console/lexer_rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,191 @@
*/

import { monaco } from '../../monaco_imports';
import { globals } from '../../common/lexer_rules';
import { buildXjsonRules } from '../../xjson/lexer_rules/xjson';

export const languageConfiguration: monaco.languages.LanguageConfiguration = {};
export const languageConfiguration: monaco.languages.LanguageConfiguration = {
brackets: [
['{', '}'],
['[', ']'],
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '"', close: '"' },
],
};

/*
util function to build the action object
*/
const addNextStateToAction = (tokens: string[], nextState?: string) => {
return tokens.map((token, index) => {
// only last action needs to specify the next state
if (index === tokens.length - 1 && nextState) {
return { token, next: nextState };
}
return token;
});
};

/*
if regex is matched, tokenize as "token" and move to the state "nextState" if defined
*/
const matchToken = (token: string, regex: string | RegExp, nextState?: string) => {
if (nextState) {
return { regex, action: { token, next: nextState } };
}
return { regex, action: { token } };
};

/*
if regex is matched, tokenize as "tokens" consecutively and move to the state "nextState"
regex needs to have the same number of capturing group as the number of tokens
*/
const matchTokens = (tokens: string[], regex: string | RegExp, nextState?: string) => {
const action = addNextStateToAction(tokens, nextState);
return {
regex,
action,
};
};

const matchTokensWithEOL = (
tokens: string | string[],
regex: string | RegExp,
nextIfEOL: string,
normalNext?: string
) => {
if (Array.isArray(tokens)) {
const endOfLineAction = addNextStateToAction(tokens, nextIfEOL);
const action = addNextStateToAction(tokens, normalNext);
return {
regex,
action: {
cases: {
'@eos': endOfLineAction,
'@default': action,
},
},
};
}
return {
regex,
action: {
cases: {
'@eos': { token: tokens, next: nextIfEOL },
'@default': { token: tokens, next: normalNext },
},
},
};
};

const xjsonRules = { ...buildXjsonRules('json_root') };
// @ts-expect-error include comments into json
xjsonRules.json_root = [{ include: '@comments' }, ...xjsonRules.json_root];
xjsonRules.json_root = [
// @ts-expect-error include variables into json
matchToken('variable.template', /("\${\w+}")/),
...xjsonRules.json_root,
];

export const lexerRules: monaco.languages.IMonarchLanguage = {
...(globals as any),

defaultToken: 'invalid',
regex_method: /get|post|put|patch|delete/,
regex_url: /.*$/,
// C# style strings
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
ignoreCase: true,
tokenizer: {
root: [
// whitespace
{ include: '@rule_whitespace' },
// start a multi-line comment
{ include: '@rule_start_multi_comment' },
// a one-line comment
[/\/\/.*$/, 'comment'],
// warning comment
matchToken('warning', '#!.*$'),
// comments
{ include: '@comments' },
// start of json
matchToken('paren.lparen', '{', 'json_root'),
// method
[/@regex_method/, 'keyword'],
// url
[/@regex_url/, 'identifier'],
matchTokensWithEOL('method', /([a-zA-Z]+)/, 'root', 'method_sep'),
// whitespace
matchToken('whitespace', '\\s+'),
// text
matchToken('text', '.+?'),
],
rule_whitespace: [[/[ \t\r\n]+/, 'WHITESPACE']],
rule_start_multi_comment: [[/\/\*/, 'comment', '@rule_multi_comment']],
rule_multi_comment: [
method_sep: [
// protocol host with slash
matchTokensWithEOL(
['whitespace', 'url.protocol_host', 'url.slash'],
/(\s+)(https?:\/\/[^?\/,]+)(\/)/,
'root',
'url'
),
// variable template
matchTokensWithEOL(['whitespace', 'variable.template'], /(\s+)(\${\w+})/, 'root', 'url'),
// protocol host
matchTokensWithEOL(
['whitespace', 'url.protocol_host'],
/(\s+)(https?:\/\/[^?\/,]+)/,
'root',
'url'
),
// slash
matchTokensWithEOL(['whitespace', 'url.slash'], /(\s+)(\/)/, 'root', 'url'),
// whitespace
matchTokensWithEOL('whitespace', /(\s+)/, 'root', 'url'),
],
url: [
// variable template
matchTokensWithEOL('variable.template', /(\${\w+})/, 'root'),
// pathname
matchTokensWithEOL('url.part', /([^?\/,\s]+)\s*/, 'root'),
// comma
matchTokensWithEOL('url.comma', /(,)/, 'root'),
// slash
matchTokensWithEOL('url.slash', /(\/)/, 'root'),
// question mark
matchTokensWithEOL('url.questionmark', /(\?)/, 'root', 'urlParams'),
// comment
matchTokensWithEOL(
['whitespace', 'comment.punctuation', 'comment.line'],
/(\s+)(\/\/)(.*$)/,
'root'
),
],
urlParams: [
// param with variable template
matchTokensWithEOL(
['url.param', 'url.equal', 'variable.template'],
/([^&=]+)(=)(\${\w+})/,
'root'
),
// param with value
matchTokensWithEOL(['url.param', 'url.equal', 'url.value'], /([^&=]+)(=)([^&]*)/, 'root'),
// param
matchTokensWithEOL('url.param', /([^&=]+)/, 'root'),
// ampersand
matchTokensWithEOL('url.amp', /(&)/, 'root'),
// comment
matchTokensWithEOL(
['whitespace', 'comment.punctuation', 'comment.line'],
/(\s+)(\/\/)(.*$)/,
'root'
),
],
comments: [
// line comment indicated by #
matchTokens(['comment.punctuation', 'comment.line'], /(#)(.*$)/),
// start a block comment indicated by /*
matchToken('comment.punctuation', /\/\*/, 'block_comment'),
// line comment indicated by //
matchTokens(['comment.punctuation', 'comment.line'], /(\/\/)(.*$)/),
],
block_comment: [
// match everything on a single line inside the comment except for chars / and *
[/[^\/*]+/, 'comment'],
// start a nested comment by going 1 level down
[/\/\*/, 'comment', '@push'],
// match the closing of the comment and return 1 level up
['\\*/', 'comment', '@pop'],
matchToken('comment', /[^\/*]+/),
// end block comment
matchToken('comment.punctuation', /\*\//, '@pop'),
// match individual chars inside a multi-line comment
[/[\/*]/, 'comment'],
],
string: [
[/[^\\"]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }],
matchToken('comment', /[\/*]/),
],
// include json rules
...xjsonRules,
},
};
54 changes: 54 additions & 0 deletions packages/kbn-monaco/src/console/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/

import { makeHighContrastColor } from '@elastic/eui';
import { darkMode, euiThemeVars } from '@kbn/ui-theme';

import { themeRuleGroupBuilderFactory } from '../common/theme';
import { monaco } from '../monaco_imports';

const buildRuleGroup = themeRuleGroupBuilderFactory();

const background = euiThemeVars.euiColorLightestShade;
const methodTextColor = '#DD0A73';
const urlTextColor = '#00A69B';
const stringTextColor = '#009926';
const commentTextColor = '#4C886B';
const variableTextColor = '#0079A5';
const booleanTextColor = '#585CF6';
const numericTextColor = variableTextColor;
export const buildConsoleTheme = (): monaco.editor.IStandaloneThemeData => {
return {
base: darkMode ? 'vs-dark' : 'vs',
inherit: true,
rules: [
...buildRuleGroup(['method'], makeHighContrastColor(methodTextColor)(background)),
...buildRuleGroup(['url'], makeHighContrastColor(urlTextColor)(background)),
...buildRuleGroup(
['string', 'string-literal', 'multi-string', 'punctuation.end-triple-quote'],
makeHighContrastColor(stringTextColor)(background)
),
...buildRuleGroup(['comment'], makeHighContrastColor(commentTextColor)(background)),
...buildRuleGroup(['variable'], makeHighContrastColor(variableTextColor)(background)),
...buildRuleGroup(
['constant.language.boolean'],
makeHighContrastColor(booleanTextColor)(background)
),
...buildRuleGroup(['constant.numeric'], makeHighContrastColor(numericTextColor)(background)),
],
colors: {
'editor.background': background,
// color of the line numbers
'editorLineNumber.foreground': euiThemeVars.euiColorDarkShade,
// color of the active line number
'editorLineNumber.activeForeground': euiThemeVars.euiColorDarkShade,
// background of the line numbers side panel
'editorGutter.background': euiThemeVars.euiColorEmptyShade,
},
};
};
8 changes: 2 additions & 6 deletions packages/kbn-monaco/src/esql/lib/monaco/esql_theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,11 @@
*/

import { euiThemeVars, darkMode } from '@kbn/ui-theme';
import { themeRuleGroupBuilderFactory } from '../../../common/theme';
import { ESQL_TOKEN_POSTFIX } from '../constants';
import { monaco } from '../../../monaco_imports';

const buildRuleGroup = (tokens: string[], color: string, isBold: boolean = false) =>
tokens.map((i) => ({
token: i + ESQL_TOKEN_POSTFIX,
foreground: color,
fontStyle: isBold ? 'bold' : '',
}));
const buildRuleGroup = themeRuleGroupBuilderFactory(ESQL_TOKEN_POSTFIX);

export const buildESQlTheme = (): monaco.editor.IStandaloneThemeData => ({
base: darkMode ? 'vs-dark' : 'vs',
Expand Down
3 changes: 2 additions & 1 deletion packages/kbn-monaco/src/register_globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { monaco } from './monaco_imports';
import { ESQL_THEME_ID, ESQLLang, buildESQlTheme } from './esql';
import { YAML_LANG_ID } from './yaml';
import { registerLanguage, registerTheme } from './helpers';
import { ConsoleLang } from './console';
import { ConsoleLang, CONSOLE_THEME_ID, buildConsoleTheme } from './console';

export const DEFAULT_WORKER_ID = 'default';
const langSpecificWorkerIds = [
Expand All @@ -38,6 +38,7 @@ registerLanguage(ConsoleLang);
* Register custom themes
*/
registerTheme(ESQL_THEME_ID, buildESQlTheme());
registerTheme(CONSOLE_THEME_ID, buildConsoleTheme());

const monacoBundleDir = (window as any).__kbnPublicPath__?.['kbn-monaco'];

Expand Down
23 changes: 13 additions & 10 deletions packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,11 @@

import { monaco } from '../../monaco_imports';

import { globals } from './shared';
import { globals } from '../../common/lexer_rules';

export const lexerRules: monaco.languages.IMonarchLanguage = {
...(globals as any),

defaultToken: 'invalid',
tokenPostfix: '',

tokenizer: {
root: [
export const buildXjsonRules = (root: string = 'root') => {
return {
[root]: [
[
/("(?:[^"]*_)?script"|"inline"|"source")(\s*?)(:)(\s*?)(""")/,
[
Expand Down Expand Up @@ -106,7 +101,15 @@ export const lexerRules: monaco.languages.IMonarchLanguage = {
[/\\""""/, { token: 'punctuation.end_triple_quote', next: '@pop' }],
[/./, { token: 'multi_string' }],
],
},
};
};
export const lexerRules: monaco.languages.IMonarchLanguage = {
...(globals as any),

defaultToken: 'invalid',
tokenPostfix: '',

tokenizer: { ...buildXjsonRules() },
};

export const languageConfiguration: monaco.languages.LanguageConfiguration = {
Expand Down
Loading

0 comments on commit c59016e

Please sign in to comment.