Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Console] Add theme and more lexer rules #178757

Merged
merged 14 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering whether we could use the already defined euiTheme colors for consistency, like it is done for the code editor theme (which I think is used in Painless lab). For example, the method color could be euiTheme.euiColorAccentText. But this decision depends on whether we want to keep the theme as close to the ace editor theme as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, the hard coded values are not great, but we can't currently map them one-to-one without changing the colors. I think there are 2 ways to improve this code: either re-use the already existing json code editor theme, but that changes the look of console. Or alternatively, we try to keep the original look but re-use the built-in colors from EUI as much as possible.
I added a section to #176926 with follow up work once the initial language definition is implemented.

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