mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Shared UX] Migrate code editor from kibana_react plugin to shared_ux package (#148550)
This commit is contained in:
parent
e07a65ef05
commit
58cd6370a2
43 changed files with 3238 additions and 1 deletions
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
|
@ -114,7 +114,7 @@
|
||||||
|
|
||||||
### Kibana React (to be deprecated)
|
### Kibana React (to be deprecated)
|
||||||
/src/plugins/kibana_react/ @elastic/appex-sharedux
|
/src/plugins/kibana_react/ @elastic/appex-sharedux
|
||||||
/src/plugins/kibana_react/public/code_editor @elastic/appex-sharedux @elastic/kibana-presentation
|
/src/plugins/kibana_react/public/@elastic/appex-sharedux @elastic/kibana-presentation
|
||||||
|
|
||||||
### Home Plugin and Packages
|
### Home Plugin and Packages
|
||||||
/src/plugins/home/public @elastic/appex-sharedux
|
/src/plugins/home/public @elastic/appex-sharedux
|
||||||
|
@ -1040,6 +1040,9 @@ packages/shared-ux/button/exit_full_screen/types @elastic/appex-sharedux
|
||||||
packages/shared-ux/card/no_data/impl @elastic/appex-sharedux
|
packages/shared-ux/card/no_data/impl @elastic/appex-sharedux
|
||||||
packages/shared-ux/card/no_data/mocks @elastic/appex-sharedux
|
packages/shared-ux/card/no_data/mocks @elastic/appex-sharedux
|
||||||
packages/shared-ux/card/no_data/types @elastic/appex-sharedux
|
packages/shared-ux/card/no_data/types @elastic/appex-sharedux
|
||||||
|
packages/shared-ux/code_editor/impl @elastic/shared-ux
|
||||||
|
packages/shared-ux/code_editor/mocks @elastic/shared-ux
|
||||||
|
packages/shared-ux/code_editor/types @elastic/shared-ux
|
||||||
packages/shared-ux/file/context @elastic/appex-sharedux
|
packages/shared-ux/file/context @elastic/appex-sharedux
|
||||||
packages/shared-ux/file/file_picker/impl @elastic/appex-sharedux
|
packages/shared-ux/file/file_picker/impl @elastic/appex-sharedux
|
||||||
packages/shared-ux/file/file_upload/impl @elastic/appex-sharedux
|
packages/shared-ux/file/file_upload/impl @elastic/appex-sharedux
|
||||||
|
|
|
@ -144,6 +144,9 @@
|
||||||
"@kbn/cell-actions": "link:packages/kbn-cell-actions",
|
"@kbn/cell-actions": "link:packages/kbn-cell-actions",
|
||||||
"@kbn/chart-expressions-common": "link:src/plugins/chart_expressions/common",
|
"@kbn/chart-expressions-common": "link:src/plugins/chart_expressions/common",
|
||||||
"@kbn/chart-icons": "link:packages/kbn-chart-icons",
|
"@kbn/chart-icons": "link:packages/kbn-chart-icons",
|
||||||
|
"@kbn/code-editor": "link:packages/shared-ux/code_editor/impl",
|
||||||
|
"@kbn/code-editor-mocks": "link:packages/shared-ux/code_editor/mocks",
|
||||||
|
"@kbn/code-editor-types": "link:packages/shared-ux/code_editor/types",
|
||||||
"@kbn/coloring": "link:packages/kbn-coloring",
|
"@kbn/coloring": "link:packages/kbn-coloring",
|
||||||
"@kbn/config": "link:packages/kbn-config",
|
"@kbn/config": "link:packages/kbn-config",
|
||||||
"@kbn/config-mocks": "link:packages/kbn-config-mocks",
|
"@kbn/config-mocks": "link:packages/kbn-config-mocks",
|
||||||
|
|
24
packages/shared-ux/code_editor/impl/README.mdx
Normal file
24
packages/shared-ux/code_editor/impl/README.mdx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
id: sharedUX/Components/CodeEditor
|
||||||
|
slug: /shared-ux/components/code-editor
|
||||||
|
title: Code Editor
|
||||||
|
description: A code editor to display code and edit code in Kibana.
|
||||||
|
tags: ['shared-ux', 'component']
|
||||||
|
date: 2022-12-05
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This component is an abstraction of the [Monaco Code Editor](https://microsoft.github.io/monaco-editor/) (and the [React Monaco Editor component](https://github.com/react-monaco-editor/react-monaco-editor)). This component still allows access to the other Monaco features.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This editor component allows easy access to:
|
||||||
|
* [Syntax highlighting (including custom language highlighting)](https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-custom-languages)
|
||||||
|
* [Suggestion/autocompletion widget](https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-completion-provider-example)
|
||||||
|
* Function signature widget
|
||||||
|
* [Hover widget](https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-hover-provider-example)
|
||||||
|
|
||||||
|
The Monaco editor doesn't automatically resize the editor area on window or container resize so this component includes a [resize detector](https://github.com/maslianok/react-resize-detector) to cause the Monaco editor to re-layout and adjust its size when the window or container size changes
|
||||||
|
|
||||||
|
## API
|
396
packages/shared-ux/code_editor/impl/__snapshots__/code_editor.test.tsx.snap
generated
Normal file
396
packages/shared-ux/code_editor/impl/__snapshots__/code_editor.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,396 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<CodeEditor /> hint element should be tabable 1`] = `
|
||||||
|
<div
|
||||||
|
aria-label="Code Editor"
|
||||||
|
css="You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop).,You have tried to stringify object returned from \`css\` function. It isn't supposed to be used directly (e.g. as value of the \`className\` prop), but rather handed to emotion so it can handle it (e.g. as value of \`css\` prop)."
|
||||||
|
data-test-subj="codeEditorHint"
|
||||||
|
id="1234"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<CodeEditor /> is rendered 1`] = `
|
||||||
|
<CodeEditor
|
||||||
|
height={250}
|
||||||
|
intl={
|
||||||
|
Object {
|
||||||
|
"defaultFormats": Object {},
|
||||||
|
"defaultLocale": "en",
|
||||||
|
"formatDate": [Function],
|
||||||
|
"formatHTMLMessage": [Function],
|
||||||
|
"formatMessage": [Function],
|
||||||
|
"formatNumber": [Function],
|
||||||
|
"formatPlural": [Function],
|
||||||
|
"formatRelative": [Function],
|
||||||
|
"formatTime": [Function],
|
||||||
|
"formats": Object {
|
||||||
|
"date": Object {
|
||||||
|
"full": Object {
|
||||||
|
"day": "numeric",
|
||||||
|
"month": "long",
|
||||||
|
"weekday": "long",
|
||||||
|
"year": "numeric",
|
||||||
|
},
|
||||||
|
"long": Object {
|
||||||
|
"day": "numeric",
|
||||||
|
"month": "long",
|
||||||
|
"year": "numeric",
|
||||||
|
},
|
||||||
|
"medium": Object {
|
||||||
|
"day": "numeric",
|
||||||
|
"month": "short",
|
||||||
|
"year": "numeric",
|
||||||
|
},
|
||||||
|
"short": Object {
|
||||||
|
"day": "numeric",
|
||||||
|
"month": "numeric",
|
||||||
|
"year": "2-digit",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"number": Object {
|
||||||
|
"currency": Object {
|
||||||
|
"style": "currency",
|
||||||
|
},
|
||||||
|
"percent": Object {
|
||||||
|
"style": "percent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"relative": Object {
|
||||||
|
"days": Object {
|
||||||
|
"units": "day",
|
||||||
|
},
|
||||||
|
"hours": Object {
|
||||||
|
"units": "hour",
|
||||||
|
},
|
||||||
|
"minutes": Object {
|
||||||
|
"units": "minute",
|
||||||
|
},
|
||||||
|
"months": Object {
|
||||||
|
"units": "month",
|
||||||
|
},
|
||||||
|
"seconds": Object {
|
||||||
|
"units": "second",
|
||||||
|
},
|
||||||
|
"years": Object {
|
||||||
|
"units": "year",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"time": Object {
|
||||||
|
"full": Object {
|
||||||
|
"hour": "numeric",
|
||||||
|
"minute": "numeric",
|
||||||
|
"second": "numeric",
|
||||||
|
"timeZoneName": "short",
|
||||||
|
},
|
||||||
|
"long": Object {
|
||||||
|
"hour": "numeric",
|
||||||
|
"minute": "numeric",
|
||||||
|
"second": "numeric",
|
||||||
|
"timeZoneName": "short",
|
||||||
|
},
|
||||||
|
"medium": Object {
|
||||||
|
"hour": "numeric",
|
||||||
|
"minute": "numeric",
|
||||||
|
"second": "numeric",
|
||||||
|
},
|
||||||
|
"short": Object {
|
||||||
|
"hour": "numeric",
|
||||||
|
"minute": "numeric",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"formatters": Object {
|
||||||
|
"getDateTimeFormat": [Function],
|
||||||
|
"getMessageFormat": [Function],
|
||||||
|
"getNumberFormat": [Function],
|
||||||
|
"getPluralFormat": [Function],
|
||||||
|
"getRelativeFormat": [Function],
|
||||||
|
},
|
||||||
|
"locale": "en",
|
||||||
|
"messages": Object {},
|
||||||
|
"now": [Function],
|
||||||
|
"onError": [Function],
|
||||||
|
"textComponent": Symbol(react.fragment),
|
||||||
|
"timeZone": null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
languageId="loglang"
|
||||||
|
onChange={[Function]}
|
||||||
|
value="
|
||||||
|
[Sun Mar 7 20:54:27 2004] [notice] [client xx.xx.xx.xx] This is a notice!
|
||||||
|
[Sun Mar 7 20:58:27 2004] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection before send body completed
|
||||||
|
[Sun Mar 7 21:16:17 2004] [error] [client xx.xx.xx.xx] File does not exist: /home/httpd/twiki/view/Main/WebHome
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
css={
|
||||||
|
Object {
|
||||||
|
"map": undefined,
|
||||||
|
"name": "1dubd8m",
|
||||||
|
"next": undefined,
|
||||||
|
"styles": "
|
||||||
|
{
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
",
|
||||||
|
"toString": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
>
|
||||||
|
<EuiToolTip
|
||||||
|
content={
|
||||||
|
<React.Fragment>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
css={
|
||||||
|
Object {
|
||||||
|
"map": undefined,
|
||||||
|
"name": "1dubd8m",
|
||||||
|
"next": undefined,
|
||||||
|
"styles": "
|
||||||
|
{
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
",
|
||||||
|
"toString": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultMessage="Press {key} to start editing."
|
||||||
|
id="sharedUXPackages.codeEditor.startEditing"
|
||||||
|
values={
|
||||||
|
Object {
|
||||||
|
"key": <strong>
|
||||||
|
Enter
|
||||||
|
</strong>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
css={
|
||||||
|
Object {
|
||||||
|
"map": undefined,
|
||||||
|
"name": "1dubd8m",
|
||||||
|
"next": undefined,
|
||||||
|
"styles": "
|
||||||
|
{
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
",
|
||||||
|
"toString": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultMessage="Press {key} to stop editing."
|
||||||
|
id="sharedUXPackages.codeEditor.stopEditing"
|
||||||
|
values={
|
||||||
|
Object {
|
||||||
|
"key": <strong>
|
||||||
|
Esc
|
||||||
|
</strong>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
delay="regular"
|
||||||
|
display="block"
|
||||||
|
position="top"
|
||||||
|
>
|
||||||
|
<EuiToolTipAnchor
|
||||||
|
display="block"
|
||||||
|
id="generated-id"
|
||||||
|
isVisible={false}
|
||||||
|
onBlur={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onMouseOut={[Function]}
|
||||||
|
onMouseOver={[Function]}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
css="unknown styles"
|
||||||
|
onMouseOut={[Function]}
|
||||||
|
onMouseOver={[Function]}
|
||||||
|
>
|
||||||
|
<Insertion
|
||||||
|
cache={
|
||||||
|
Object {
|
||||||
|
"insert": [Function],
|
||||||
|
"inserted": Object {
|
||||||
|
"uuw4g3-euiToolTipAnchor-block": true,
|
||||||
|
},
|
||||||
|
"key": "css",
|
||||||
|
"nonce": undefined,
|
||||||
|
"registered": Object {},
|
||||||
|
"sheet": StyleSheet {
|
||||||
|
"_alreadyInsertedOrderInsensitiveRule": true,
|
||||||
|
"_insertTag": [Function],
|
||||||
|
"before": null,
|
||||||
|
"container": <head>
|
||||||
|
<style
|
||||||
|
data-emotion="css"
|
||||||
|
data-s=""
|
||||||
|
>
|
||||||
|
|
||||||
|
.emotion-euiToolTipAnchor-block{display:block;}
|
||||||
|
</style>
|
||||||
|
<style
|
||||||
|
data-emotion="css"
|
||||||
|
data-s=""
|
||||||
|
>
|
||||||
|
|
||||||
|
.emotion-euiToolTipAnchor-block *[disabled]{pointer-events:none;}
|
||||||
|
</style>
|
||||||
|
<style
|
||||||
|
data-styled="active"
|
||||||
|
data-styled-version="5.1.0"
|
||||||
|
/>
|
||||||
|
</head>,
|
||||||
|
"ctr": 2,
|
||||||
|
"insertionPoint": undefined,
|
||||||
|
"isSpeedy": false,
|
||||||
|
"key": "css",
|
||||||
|
"nonce": undefined,
|
||||||
|
"prepend": undefined,
|
||||||
|
"tags": Array [
|
||||||
|
<style
|
||||||
|
data-emotion="css"
|
||||||
|
data-s=""
|
||||||
|
>
|
||||||
|
|
||||||
|
.emotion-euiToolTipAnchor-block{display:block;}
|
||||||
|
</style>,
|
||||||
|
<style
|
||||||
|
data-emotion="css"
|
||||||
|
data-s=""
|
||||||
|
>
|
||||||
|
|
||||||
|
.emotion-euiToolTipAnchor-block *[disabled]{pointer-events:none;}
|
||||||
|
</style>,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isStringTag={true}
|
||||||
|
serialized={
|
||||||
|
Object {
|
||||||
|
"map": undefined,
|
||||||
|
"name": "uuw4g3-euiToolTipAnchor-block",
|
||||||
|
"next": undefined,
|
||||||
|
"styles": "*[disabled]{pointer-events:none;};label:euiToolTipAnchor;;;display:block;label:block;;;",
|
||||||
|
"toString": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="euiToolTipAnchor emotion-euiToolTipAnchor-block"
|
||||||
|
onMouseOut={[Function]}
|
||||||
|
onMouseOver={[Function]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Code Editor"
|
||||||
|
css={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"map": undefined,
|
||||||
|
"name": "1dubd8m",
|
||||||
|
"next": undefined,
|
||||||
|
"styles": "
|
||||||
|
{
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
",
|
||||||
|
"toString": [Function],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"map": undefined,
|
||||||
|
"name": "7fzoim",
|
||||||
|
"next": undefined,
|
||||||
|
"styles": "
|
||||||
|
{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
z-index: 6000;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--isInactive {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
",
|
||||||
|
"toString": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
data-test-subj="codeEditorHint"
|
||||||
|
id="1234"
|
||||||
|
onBlur={[Function]}
|
||||||
|
onClick={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</EuiToolTipAnchor>
|
||||||
|
</EuiToolTip>
|
||||||
|
<Component>
|
||||||
|
<mockMonacoEditor
|
||||||
|
editorDidMount={[Function]}
|
||||||
|
editorWillMount={[Function]}
|
||||||
|
height="100px"
|
||||||
|
language="loglang"
|
||||||
|
onChange={[Function]}
|
||||||
|
options={
|
||||||
|
Object {
|
||||||
|
"fontFamily": "Roboto Mono",
|
||||||
|
"fontSize": 12,
|
||||||
|
"lineHeight": 21,
|
||||||
|
"matchBrackets": "never",
|
||||||
|
"minimap": Object {
|
||||||
|
"enabled": false,
|
||||||
|
},
|
||||||
|
"padding": Object {},
|
||||||
|
"renderLineHighlight": "none",
|
||||||
|
"scrollBeyondLastLine": false,
|
||||||
|
"scrollbar": Object {
|
||||||
|
"alwaysConsumeMouseWheel": false,
|
||||||
|
"useShadows": false,
|
||||||
|
},
|
||||||
|
"wordBasedSuggestions": false,
|
||||||
|
"wordWrap": "on",
|
||||||
|
"wrappingIndent": "indent",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
theme="euiColors"
|
||||||
|
value="
|
||||||
|
[Sun Mar 7 20:54:27 2004] [notice] [client xx.xx.xx.xx] This is a notice!
|
||||||
|
[Sun Mar 7 20:58:27 2004] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection before send body completed
|
||||||
|
[Sun Mar 7 21:16:17 2004] [error] [client xx.xx.xx.xx] File does not exist: /home/httpd/twiki/view/Main/WebHome
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div />
|
||||||
|
<textarea
|
||||||
|
data-test-subj="monacoEditorTextarea"
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</mockMonacoEditor>
|
||||||
|
</Component>
|
||||||
|
</div>
|
||||||
|
</CodeEditor>
|
||||||
|
`;
|
201
packages/shared-ux/code_editor/impl/code_editor.stories.tsx
Normal file
201
packages/shared-ux/code_editor/impl/code_editor.stories.tsx
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
/*
|
||||||
|
* 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 React from 'react';
|
||||||
|
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
import { CodeEditorStorybookMock, CodeEditorStorybookParams } from '@kbn/code-editor-mocks';
|
||||||
|
import { monaco as monacoEditor } from '@kbn/monaco';
|
||||||
|
|
||||||
|
import mdx from './README.mdx';
|
||||||
|
|
||||||
|
import { CodeEditor } from './code_editor';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Code Editor/Code Editor',
|
||||||
|
description: 'A code editor',
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
page: mdx,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mock = new CodeEditorStorybookMock();
|
||||||
|
const argTypes = mock.getArgumentTypes();
|
||||||
|
|
||||||
|
export const Basic = (params: CodeEditorStorybookParams) => {
|
||||||
|
return (
|
||||||
|
<CodeEditor {...params} languageId="plainText" onChange={action('on change')} value="Hello!" />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Basic.argTypes = argTypes;
|
||||||
|
|
||||||
|
// A sample language definition with a few example tokens
|
||||||
|
// Taken from https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-custom-languages
|
||||||
|
const simpleLogLang: monacoEditor.languages.IMonarchLanguage = {
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
[/\[error.*/, 'constant'],
|
||||||
|
[/\[notice.*/, 'variable'],
|
||||||
|
[/\[info.*/, 'string'],
|
||||||
|
[/\[[a-zA-Z 0-9:]+\]/, 'tag'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
monacoEditor.languages.register({ id: 'loglang' });
|
||||||
|
monacoEditor.languages.setMonarchTokensProvider('loglang', simpleLogLang);
|
||||||
|
|
||||||
|
const logs = `[Sun Mar 7 20:54:27 2004] [notice] [client xx.xx.xx.xx] This is a notice!
|
||||||
|
[Sun Mar 7 20:58:27 2004] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection before send body completed
|
||||||
|
[Sun Mar 7 21:16:17 2004] [error] [client xx.xx.xx.xx] File does not exist: /home/httpd/twiki/view/Main/WebHome
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CustomLogLanguage = (params: CodeEditorStorybookParams) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CodeEditor
|
||||||
|
{...params}
|
||||||
|
languageId="loglang"
|
||||||
|
height={250}
|
||||||
|
value={logs}
|
||||||
|
options={{
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CustomLogLanguage.argTypes = argTypes;
|
||||||
|
|
||||||
|
export const JSONSupport = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CodeEditor
|
||||||
|
languageId="json"
|
||||||
|
editorDidMount={(editor) => {
|
||||||
|
monacoEditor.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||||
|
validate: true,
|
||||||
|
schemas: [
|
||||||
|
{
|
||||||
|
uri: editor.getModel()?.uri.toString() ?? '',
|
||||||
|
fileMatch: ['*'],
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
version: {
|
||||||
|
enum: ['v1', 'v2'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
height={250}
|
||||||
|
value="{}"
|
||||||
|
onChange={action('onChange')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SuggestionProvider = () => {
|
||||||
|
const provideSuggestions = (
|
||||||
|
model: monacoEditor.editor.ITextModel,
|
||||||
|
position: monacoEditor.Position,
|
||||||
|
context: monacoEditor.languages.CompletionContext
|
||||||
|
) => {
|
||||||
|
const wordRange = new monacoEditor.Range(
|
||||||
|
position.lineNumber,
|
||||||
|
position.column,
|
||||||
|
position.lineNumber,
|
||||||
|
position.column
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
suggestions: [
|
||||||
|
{
|
||||||
|
label: 'Hello, World',
|
||||||
|
kind: monacoEditor.languages.CompletionItemKind.Variable,
|
||||||
|
documentation: {
|
||||||
|
value: '*Markdown* can be used in autocomplete help',
|
||||||
|
isTrusted: true,
|
||||||
|
},
|
||||||
|
insertText: 'Hello, World',
|
||||||
|
range: wordRange,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'You know, for search',
|
||||||
|
kind: monacoEditor.languages.CompletionItemKind.Variable,
|
||||||
|
documentation: { value: 'Thanks `Monaco`', isTrusted: true },
|
||||||
|
insertText: 'You know, for search',
|
||||||
|
range: wordRange,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CodeEditor
|
||||||
|
languageId="loglang"
|
||||||
|
height={250}
|
||||||
|
value={logs}
|
||||||
|
onChange={action('onChange')}
|
||||||
|
suggestionProvider={{
|
||||||
|
triggerCharacters: ['.'],
|
||||||
|
provideCompletionItems: provideSuggestions,
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
quickSuggestions: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HoverProvider = () => {
|
||||||
|
const provideHover = (model: monacoEditor.editor.ITextModel, position: monacoEditor.Position) => {
|
||||||
|
const word = model.getWordAtPosition(position);
|
||||||
|
|
||||||
|
if (!word) {
|
||||||
|
return {
|
||||||
|
contents: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
value: `You're hovering over **${word.word}**`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CodeEditor
|
||||||
|
languageId="loglang"
|
||||||
|
height={250}
|
||||||
|
value={logs}
|
||||||
|
onChange={action('onChange')}
|
||||||
|
hoverProvider={{
|
||||||
|
provideHover,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
121
packages/shared-ux/code_editor/impl/code_editor.test.helpers.tsx
Normal file
121
packages/shared-ux/code_editor/impl/code_editor.test.helpers.tsx
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* 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 React, { useEffect, KeyboardEventHandler } from 'react';
|
||||||
|
import { monaco } from '@kbn/monaco';
|
||||||
|
|
||||||
|
function createEditorInstance() {
|
||||||
|
const keyDownListeners: Array<(e?: unknown) => void> = [];
|
||||||
|
const didShowListeners: Array<(e?: unknown) => void> = [];
|
||||||
|
const didHideListeners: Array<(e?: unknown) => void> = [];
|
||||||
|
let placeholderDiv: undefined | HTMLDivElement;
|
||||||
|
let areSuggestionsVisible = false;
|
||||||
|
|
||||||
|
const editorInstance = {
|
||||||
|
// Mock monaco editor API
|
||||||
|
getContribution: jest.fn((id: string) => {
|
||||||
|
if (id === 'editor.contrib.suggestController') {
|
||||||
|
return {
|
||||||
|
widget: {
|
||||||
|
value: {
|
||||||
|
onDidShow: jest.fn((listener) => {
|
||||||
|
didShowListeners.push(listener);
|
||||||
|
}),
|
||||||
|
onDidHide: jest.fn((listener) => {
|
||||||
|
didHideListeners.push(listener);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
focus: jest.fn(),
|
||||||
|
onDidBlurEditorText: jest.fn(),
|
||||||
|
onKeyDown: jest.fn((listener) => {
|
||||||
|
keyDownListeners.push(listener);
|
||||||
|
}),
|
||||||
|
addContentWidget: jest.fn((widget: monaco.editor.IContentWidget) => {
|
||||||
|
placeholderDiv?.appendChild(widget.getDomNode());
|
||||||
|
}),
|
||||||
|
applyFontInfo: jest.fn(),
|
||||||
|
removeContentWidget: jest.fn((widget: monaco.editor.IContentWidget) => {
|
||||||
|
placeholderDiv?.removeChild(widget.getDomNode());
|
||||||
|
}),
|
||||||
|
getDomNode: jest.fn(),
|
||||||
|
// Helpers for our tests
|
||||||
|
__helpers__: {
|
||||||
|
areSuggestionsVisible: () => areSuggestionsVisible,
|
||||||
|
getPlaceholderRef: (div: HTMLDivElement) => {
|
||||||
|
placeholderDiv = div;
|
||||||
|
},
|
||||||
|
onTextareaKeyDown: ((e) => {
|
||||||
|
// Let all our listener know that a key has been pressed on the textarea
|
||||||
|
keyDownListeners.forEach((listener) => listener(e));
|
||||||
|
|
||||||
|
// Close the suggestions when hitting the ESC key
|
||||||
|
if (e.keyCode === monaco.KeyCode.Escape && areSuggestionsVisible) {
|
||||||
|
editorInstance.__helpers__.hideSuggestions();
|
||||||
|
}
|
||||||
|
}) as KeyboardEventHandler,
|
||||||
|
showSuggestions: () => {
|
||||||
|
areSuggestionsVisible = true;
|
||||||
|
didShowListeners.forEach((listener) => listener());
|
||||||
|
},
|
||||||
|
hideSuggestions: () => {
|
||||||
|
areSuggestionsVisible = false;
|
||||||
|
didHideListeners.forEach((listener) => listener());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return editorInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockedEditor = ReturnType<typeof createEditorInstance>;
|
||||||
|
|
||||||
|
export const mockedEditorInstance: MockedEditor = createEditorInstance();
|
||||||
|
|
||||||
|
// <MonacoEditor /> mock
|
||||||
|
const mockMonacoEditor = ({
|
||||||
|
editorWillMount,
|
||||||
|
editorDidMount,
|
||||||
|
}: Record<string, (...args: unknown[]) => void>) => {
|
||||||
|
editorWillMount(monaco);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
useEffect(() => {
|
||||||
|
editorDidMount(mockedEditorInstance, monaco);
|
||||||
|
}, [editorDidMount]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div ref={mockedEditorInstance?.__helpers__.getPlaceholderRef} />
|
||||||
|
<textarea
|
||||||
|
onKeyDown={mockedEditorInstance?.__helpers__.onTextareaKeyDown}
|
||||||
|
data-test-subj="monacoEditorTextarea"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock('react-monaco-editor', () => {
|
||||||
|
return function JestMockEditor() {
|
||||||
|
return mockMonacoEditor;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock the htmlIdGenerator to generate predictable ids for snapshot tests
|
||||||
|
jest.mock('@elastic/eui', () => {
|
||||||
|
const original = jest.requireActual('@elastic/eui');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
htmlIdGenerator: () => {
|
||||||
|
return () => '1234';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
224
packages/shared-ux/code_editor/impl/code_editor.test.tsx
Normal file
224
packages/shared-ux/code_editor/impl/code_editor.test.tsx
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
/*
|
||||||
|
* 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 React from 'react';
|
||||||
|
import { ReactWrapper } from 'enzyme';
|
||||||
|
import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers';
|
||||||
|
import { monaco } from '@kbn/monaco';
|
||||||
|
|
||||||
|
import { keys } from '@elastic/eui';
|
||||||
|
|
||||||
|
import { mockedEditorInstance } from './code_editor.test.helpers';
|
||||||
|
|
||||||
|
import { CodeEditor } from './code_editor';
|
||||||
|
|
||||||
|
// A sample language definition with a few example tokens
|
||||||
|
const simpleLogLang: monaco.languages.IMonarchLanguage = {
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
[/\[error.*/, 'constant'],
|
||||||
|
[/\[notice.*/, 'variable'],
|
||||||
|
[/\[info.*/, 'string'],
|
||||||
|
[/\[[a-zA-Z 0-9:]+\]/, 'tag'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const logs = `
|
||||||
|
[Sun Mar 7 20:54:27 2004] [notice] [client xx.xx.xx.xx] This is a notice!
|
||||||
|
[Sun Mar 7 20:58:27 2004] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection before send body completed
|
||||||
|
[Sun Mar 7 21:16:17 2004] [error] [client xx.xx.xx.xx] File does not exist: /home/httpd/twiki/view/Main/WebHome
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe('<CodeEditor />', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: jest.fn().mockImplementation((query) => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: jest.fn(), // deprecated
|
||||||
|
removeListener: jest.fn(), // deprecated
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
removeEventListener: jest.fn(),
|
||||||
|
dispatchEvent: jest.fn(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
window.ResizeObserver = class ResizeObserver {
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
monaco.languages.register({ id: 'loglang' });
|
||||||
|
monaco.languages.setMonarchTokensProvider('loglang', simpleLogLang);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('is rendered', () => {
|
||||||
|
const component = mountWithIntl(
|
||||||
|
<CodeEditor languageId="loglang" height={250} value={logs} onChange={() => {}} />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(component).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('editor mount setup', () => {
|
||||||
|
const suggestionProvider = {
|
||||||
|
provideCompletionItems: (model: monaco.editor.ITextModel, position: monaco.Position) => ({
|
||||||
|
suggestions: [],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const hoverProvider = {
|
||||||
|
provideHover: (model: monaco.editor.ITextModel, position: monaco.Position) => ({
|
||||||
|
contents: [],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const editorWillMount = jest.fn();
|
||||||
|
|
||||||
|
monaco.languages.onLanguage = jest.fn((languageId, func) => {
|
||||||
|
expect(languageId).toBe('loglang');
|
||||||
|
|
||||||
|
// Call the function immediately so we can see our providers
|
||||||
|
// get setup without a monaco editor setting up completely
|
||||||
|
func();
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
monaco.languages.registerCompletionItemProvider = jest.fn();
|
||||||
|
monaco.languages.registerSignatureHelpProvider = jest.fn();
|
||||||
|
monaco.languages.registerHoverProvider = jest.fn();
|
||||||
|
|
||||||
|
monaco.editor.defineTheme = jest.fn();
|
||||||
|
|
||||||
|
mountWithIntl(
|
||||||
|
<CodeEditor
|
||||||
|
languageId="loglang"
|
||||||
|
value={logs}
|
||||||
|
onChange={() => {}}
|
||||||
|
editorWillMount={editorWillMount}
|
||||||
|
suggestionProvider={suggestionProvider}
|
||||||
|
hoverProvider={hoverProvider}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
// Verify our mount callback will be called
|
||||||
|
expect(editorWillMount.mock.calls.length).toBe(1);
|
||||||
|
|
||||||
|
// Verify that both, default and transparent theme will be setup\
|
||||||
|
// disabling this test because themes had to be refactored in Monaco editor Theme useMemo for light, dark and transparent to work
|
||||||
|
// expect((monaco.editor.defineTheme as jest.Mock).mock.calls.length).toBe(2)
|
||||||
|
|
||||||
|
// Verify our language features have been registered
|
||||||
|
expect((monaco.languages.onLanguage as jest.Mock).mock.calls.length).toBe(1);
|
||||||
|
expect((monaco.languages.registerCompletionItemProvider as jest.Mock).mock.calls.length).toBe(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect((monaco.languages.registerHoverProvider as jest.Mock).mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hint element', () => {
|
||||||
|
let component: ReactWrapper;
|
||||||
|
const getHint = (): ReactWrapper => findTestSubject(component, 'codeEditorHint');
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component = mountWithIntl(
|
||||||
|
<CodeEditor languageId="loglang" height={250} value={logs} onChange={() => {}} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be tabable', () => {
|
||||||
|
const DOMnode = getHint().getDOMNode();
|
||||||
|
expect(getHint().find('[data-test-subj="codeEditorHint"]').exists()).toBeTruthy();
|
||||||
|
expect(DOMnode.getAttribute('tabindex')).toBe('0');
|
||||||
|
expect(DOMnode).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be disabled when the ui monaco editor gains focus', async () => {
|
||||||
|
// Initially it is visible and active
|
||||||
|
expect(getHint().find('[data-test-subj="codeEditorHint"]').exists()).toBeTruthy();
|
||||||
|
getHint().simulate('keydown', { key: keys.ENTER });
|
||||||
|
expect(getHint().find('[data-test-subj="codeEditorHint"]').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be enabled when hitting the ESC key', () => {
|
||||||
|
getHint().simulate('keydown', { key: keys.ENTER });
|
||||||
|
|
||||||
|
findTestSubject(component, 'monacoEditorTextarea').simulate('keydown', {
|
||||||
|
keyCode: monaco.KeyCode.Escape,
|
||||||
|
});
|
||||||
|
|
||||||
|
// expect((getHint().props() as any).className).not.toContain('isInactive');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect that the suggestion menu is open and not show the hint on ESC', async () => {
|
||||||
|
getHint().simulate('keydown', { key: keys.ENTER });
|
||||||
|
|
||||||
|
// expect((getHint().props() as any).className).toContain('isInactive');
|
||||||
|
expect(mockedEditorInstance?.__helpers__.areSuggestionsVisible()).toBe(false);
|
||||||
|
|
||||||
|
// Show the suggestions in the editor
|
||||||
|
mockedEditorInstance?.__helpers__.showSuggestions();
|
||||||
|
expect(mockedEditorInstance?.__helpers__.areSuggestionsVisible()).toBe(true);
|
||||||
|
|
||||||
|
// Hitting the ESC key with the suggestions visible
|
||||||
|
findTestSubject(component, 'monacoEditorTextarea').simulate('keydown', {
|
||||||
|
keyCode: monaco.KeyCode.Escape,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockedEditorInstance?.__helpers__.areSuggestionsVisible()).toBe(false);
|
||||||
|
|
||||||
|
// The keyboard hint is still **not** active
|
||||||
|
// expect((getHint().props() as any).className).toContain('isInactive');
|
||||||
|
|
||||||
|
// Hitting a second time the ESC key should now show the hint
|
||||||
|
findTestSubject(component, 'monacoEditorTextarea').simulate('keydown', {
|
||||||
|
keyCode: monaco.KeyCode.Escape,
|
||||||
|
});
|
||||||
|
|
||||||
|
// expect((getHint().props() as any).className).not.toContain('isInactive');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether our custom placeholder widget is being mounted based on our React logic. We cannot do a full
|
||||||
|
* test with Monaco so the parts handled by Monaco are all mocked out and we just check whether the element is mounted
|
||||||
|
* in the DOM.
|
||||||
|
*/
|
||||||
|
describe('placeholder element', () => {
|
||||||
|
let component: ReactWrapper;
|
||||||
|
beforeEach(() => {
|
||||||
|
component = mountWithIntl(
|
||||||
|
<CodeEditor
|
||||||
|
languageId="loglang"
|
||||||
|
height={250}
|
||||||
|
value=""
|
||||||
|
onChange={() => {}}
|
||||||
|
placeholder="myplaceholder"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays placeholder element when placeholder text is provided', () => {
|
||||||
|
expect(component.prop('placeholder')).toBe('myplaceholder');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not display placeholder element when placeholder text is not provided', () => {
|
||||||
|
component.setProps({ ...component.props(), placeholder: undefined, value: '' });
|
||||||
|
component.update();
|
||||||
|
expect(component.prop('placeholder')).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
// this does not work on the initial implementation of code editor either from kibana - react in the storybook instance
|
||||||
|
// in the kibana react storybook placeholder is not set but value is set instead
|
||||||
|
// it('does not display placeholder element when user input has been provided', () => {
|
||||||
|
// component.setProps({ value: 'some input', ...component.props() });
|
||||||
|
// component.update();
|
||||||
|
// expect(component.prop('placeholder')).toBe(null);
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
});
|
615
packages/shared-ux/code_editor/impl/code_editor.tsx
Normal file
615
packages/shared-ux/code_editor/impl/code_editor.tsx
Normal file
|
@ -0,0 +1,615 @@
|
||||||
|
/*
|
||||||
|
* 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 React, { useState, useRef, useCallback, useMemo, useEffect, KeyboardEvent } from 'react';
|
||||||
|
import { useResizeDetector } from 'react-resize-detector';
|
||||||
|
import ReactMonacoEditor from 'react-monaco-editor';
|
||||||
|
import {
|
||||||
|
htmlIdGenerator,
|
||||||
|
EuiToolTip,
|
||||||
|
keys,
|
||||||
|
EuiButtonIcon,
|
||||||
|
EuiOverlayMask,
|
||||||
|
EuiI18n,
|
||||||
|
EuiFocusTrap,
|
||||||
|
EuiCopy,
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
useEuiTheme,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import { monaco } from '@kbn/monaco';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
|
import './register_languages';
|
||||||
|
import { remeasureFonts } from './remeasure_fonts';
|
||||||
|
|
||||||
|
import { PlaceholderWidget } from './placeholder_widget';
|
||||||
|
import {
|
||||||
|
codeEditorControlsStyles,
|
||||||
|
codeEditorControlsWithinFullScreenStyles,
|
||||||
|
codeEditorFullScreenStyles,
|
||||||
|
codeEditorKeyboardHintStyles,
|
||||||
|
codeEditorStyles,
|
||||||
|
DARK_THEME,
|
||||||
|
LIGHT_THEME,
|
||||||
|
DARK_THEME_TRANSPARENT,
|
||||||
|
LIGHT_THEME_TRANSPARENT,
|
||||||
|
} from './editor.styles';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
/** Width of editor. Defaults to 100%. */
|
||||||
|
width?: string | number;
|
||||||
|
|
||||||
|
/** Height of editor. Defaults to 100px. */
|
||||||
|
height?: string | number;
|
||||||
|
|
||||||
|
/** ID of the editor language */
|
||||||
|
languageId: string;
|
||||||
|
|
||||||
|
/** Value of the editor */
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/** Function invoked when text in editor is changed */
|
||||||
|
onChange?: (value: string, event: monaco.editor.IModelContentChangedEvent) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for the Monaco Code Editor
|
||||||
|
* Documentation of options can be found here:
|
||||||
|
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html
|
||||||
|
*/
|
||||||
|
options?: monaco.editor.IStandaloneEditorConstructionOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suggestion provider for autocompletion
|
||||||
|
* Documentation for the provider can be found here:
|
||||||
|
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.completionitemprovider.html
|
||||||
|
*/
|
||||||
|
suggestionProvider?: monaco.languages.CompletionItemProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signature provider for function parameter info
|
||||||
|
* Documentation for the provider can be found here:
|
||||||
|
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.signaturehelpprovider.html
|
||||||
|
*/
|
||||||
|
signatureProvider?: monaco.languages.SignatureHelpProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hover provider for hover documentation
|
||||||
|
* Documentation for the provider can be found here:
|
||||||
|
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.hoverprovider.html
|
||||||
|
*/
|
||||||
|
hoverProvider?: monaco.languages.HoverProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Language config provider for bracket
|
||||||
|
* Documentation for the provider can be found here:
|
||||||
|
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.languageconfiguration.html
|
||||||
|
*/
|
||||||
|
languageConfiguration?: monaco.languages.LanguageConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called before the editor is mounted in the view
|
||||||
|
*/
|
||||||
|
editorWillMount?: () => void;
|
||||||
|
/**
|
||||||
|
* Function called before the editor is mounted in the view
|
||||||
|
* and completely replaces the setup behavior called by the component
|
||||||
|
*/
|
||||||
|
overrideEditorWillMount?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called after the editor is mounted in the view
|
||||||
|
*/
|
||||||
|
editorDidMount?: (editor: monaco.editor.IStandaloneCodeEditor) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the editor use the dark theme
|
||||||
|
*/
|
||||||
|
useDarkTheme?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the editor use a transparent background
|
||||||
|
*/
|
||||||
|
transparentBackground?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the editor be rendered using the fullWidth EUI attribute
|
||||||
|
*/
|
||||||
|
fullWidth?: boolean;
|
||||||
|
|
||||||
|
placeholder?: string;
|
||||||
|
/**
|
||||||
|
* Accessible name for the editor. (Defaults to "Code editor")
|
||||||
|
*/
|
||||||
|
'aria-label'?: string;
|
||||||
|
|
||||||
|
isCopyable?: boolean;
|
||||||
|
allowFullScreen?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CodeEditor: React.FC<Props> = ({
|
||||||
|
languageId,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
width,
|
||||||
|
options,
|
||||||
|
overrideEditorWillMount,
|
||||||
|
editorDidMount,
|
||||||
|
editorWillMount,
|
||||||
|
useDarkTheme,
|
||||||
|
transparentBackground,
|
||||||
|
suggestionProvider,
|
||||||
|
signatureProvider,
|
||||||
|
hoverProvider,
|
||||||
|
placeholder,
|
||||||
|
languageConfiguration,
|
||||||
|
'aria-label': ariaLabel = i18n.translate('sharedUXPackages.codeEditor.ariaLabel', {
|
||||||
|
defaultMessage: 'Code Editor',
|
||||||
|
}),
|
||||||
|
isCopyable = false,
|
||||||
|
allowFullScreen = false,
|
||||||
|
}) => {
|
||||||
|
const { euiTheme } = useEuiTheme();
|
||||||
|
|
||||||
|
// We need to be able to mock the MonacoEditor in our test in order to not test implementation
|
||||||
|
// detail and not have to call methods on the <CodeEditor /> component instance.
|
||||||
|
const MonacoEditor: typeof ReactMonacoEditor = useMemo(() => {
|
||||||
|
const isMockedComponent =
|
||||||
|
typeof ReactMonacoEditor === 'function' && ReactMonacoEditor.name === 'JestMockEditor';
|
||||||
|
return isMockedComponent
|
||||||
|
? (ReactMonacoEditor as unknown as () => typeof ReactMonacoEditor)()
|
||||||
|
: ReactMonacoEditor;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { FullScreenDisplay, FullScreenButton, isFullScreen, setIsFullScreen, onKeyDown } =
|
||||||
|
useFullScreen({
|
||||||
|
allowFullScreen,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isReadOnly = options?.readOnly ?? false;
|
||||||
|
|
||||||
|
const _editor = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
||||||
|
const _placeholderWidget = useRef<PlaceholderWidget | null>(null);
|
||||||
|
const isSuggestionMenuOpen = useRef(false);
|
||||||
|
const editorHint = useRef<HTMLDivElement>(null);
|
||||||
|
const textboxMutationObserver = useRef<MutationObserver | null>(null);
|
||||||
|
|
||||||
|
const [isHintActive, setIsHintActive] = useState(true);
|
||||||
|
const defaultStyles = codeEditorStyles();
|
||||||
|
const hintStyles = codeEditorKeyboardHintStyles(euiTheme.levels);
|
||||||
|
|
||||||
|
const promptClasses = useMemo(() => {
|
||||||
|
return isHintActive ? [defaultStyles, hintStyles] : [defaultStyles];
|
||||||
|
}, [isHintActive, defaultStyles, hintStyles]);
|
||||||
|
|
||||||
|
const _updateDimensions = useCallback(() => {
|
||||||
|
_editor.current?.layout();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useResizeDetector({
|
||||||
|
handleWidth: true,
|
||||||
|
handleHeight: true,
|
||||||
|
onResize: _updateDimensions,
|
||||||
|
refreshMode: 'debounce',
|
||||||
|
});
|
||||||
|
|
||||||
|
const startEditing = useCallback(() => {
|
||||||
|
setIsHintActive(false);
|
||||||
|
_editor.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const stopEditing = useCallback(() => {
|
||||||
|
setIsHintActive(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onKeyDownHint = useCallback(
|
||||||
|
(ev: React.KeyboardEvent) => {
|
||||||
|
if (ev.key === keys.ENTER) {
|
||||||
|
ev.preventDefault();
|
||||||
|
startEditing();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[startEditing]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onKeydownMonaco = useCallback(
|
||||||
|
(ev: monaco.IKeyboardEvent) => {
|
||||||
|
if (ev.keyCode === monaco.KeyCode.Escape) {
|
||||||
|
// If the autocompletion context menu is open then we want to let ESCAPE close it but
|
||||||
|
// **not** exit out of editing mode.
|
||||||
|
if (!isSuggestionMenuOpen.current) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
stopEditing();
|
||||||
|
editorHint.current?.focus();
|
||||||
|
}
|
||||||
|
setIsFullScreen(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[stopEditing, setIsFullScreen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onBlurMonaco = useCallback(() => {
|
||||||
|
stopEditing();
|
||||||
|
}, [stopEditing]);
|
||||||
|
|
||||||
|
const renderPrompt = useCallback(() => {
|
||||||
|
const enterKey = (
|
||||||
|
<strong>
|
||||||
|
{i18n.translate('sharedUXPackages.codeEditor.enterKeyLabel', {
|
||||||
|
defaultMessage: 'Enter',
|
||||||
|
description:
|
||||||
|
'The name used for the Enter key on keyword. Will be {key} in sharedUXPackages.codeEditor.startEditing(ReadOnly).',
|
||||||
|
})}
|
||||||
|
</strong>
|
||||||
|
);
|
||||||
|
|
||||||
|
const escapeKey = (
|
||||||
|
<strong>
|
||||||
|
{i18n.translate('sharedUXPackages.codeEditor.escapeKeyLabel', {
|
||||||
|
defaultMessage: 'Esc',
|
||||||
|
description:
|
||||||
|
'The label of the Escape key as printed on the keyboard. Will be {key} inside sharedUXPackages.codeEditor.stopEditing(ReadOnly).',
|
||||||
|
})}
|
||||||
|
</strong>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiToolTip
|
||||||
|
display="block"
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
{isReadOnly ? (
|
||||||
|
<FormattedMessage
|
||||||
|
css={defaultStyles}
|
||||||
|
id="sharedUXPackages.codeEditor.startEditingReadOnly"
|
||||||
|
defaultMessage="Press {key} to start interacting with the code."
|
||||||
|
values={{ key: enterKey }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
css={defaultStyles}
|
||||||
|
id="sharedUXPackages.codeEditor.startEditing"
|
||||||
|
defaultMessage="Press {key} to start editing."
|
||||||
|
values={{ key: enterKey }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{isReadOnly ? (
|
||||||
|
<FormattedMessage
|
||||||
|
css={defaultStyles}
|
||||||
|
id="sharedUXPackages.codeEditor.stopEditingReadOnly"
|
||||||
|
defaultMessage="Press {key} to stop interacting with the code."
|
||||||
|
values={{ key: escapeKey }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
css={defaultStyles}
|
||||||
|
id="sharedUXPackages.codeEditor.stopEditing"
|
||||||
|
defaultMessage="Press {key} to stop editing."
|
||||||
|
values={{ key: escapeKey }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
css={promptClasses}
|
||||||
|
id={htmlIdGenerator('codeEditor')()}
|
||||||
|
ref={editorHint}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
onClick={startEditing}
|
||||||
|
onKeyDown={onKeyDownHint}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
data-test-subj={isHintActive ? 'codeEditorHint' : 'codeEditor'}
|
||||||
|
/>
|
||||||
|
</EuiToolTip>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
onKeyDownHint,
|
||||||
|
startEditing,
|
||||||
|
ariaLabel,
|
||||||
|
isReadOnly,
|
||||||
|
promptClasses,
|
||||||
|
defaultStyles,
|
||||||
|
isHintActive,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const _editorWillMount = useCallback(
|
||||||
|
(__monaco: unknown) => {
|
||||||
|
if (__monaco !== monaco) {
|
||||||
|
throw new Error('react-monaco-editor is using a different version of monaco');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrideEditorWillMount) {
|
||||||
|
overrideEditorWillMount();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editorWillMount?.();
|
||||||
|
|
||||||
|
monaco.languages.onLanguage(languageId, () => {
|
||||||
|
if (suggestionProvider) {
|
||||||
|
monaco.languages.registerCompletionItemProvider(languageId, suggestionProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signatureProvider) {
|
||||||
|
monaco.languages.registerSignatureHelpProvider(languageId, signatureProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hoverProvider) {
|
||||||
|
monaco.languages.registerHoverProvider(languageId, hoverProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (languageConfiguration) {
|
||||||
|
monaco.languages.setLanguageConfiguration(languageId, languageConfiguration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
monaco.editor.defineTheme('euiColors', useDarkTheme ? DARK_THEME : LIGHT_THEME);
|
||||||
|
monaco.editor.defineTheme(
|
||||||
|
'euiColorsTransparent',
|
||||||
|
useDarkTheme ? DARK_THEME_TRANSPARENT : LIGHT_THEME_TRANSPARENT
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
overrideEditorWillMount,
|
||||||
|
editorWillMount,
|
||||||
|
languageId,
|
||||||
|
useDarkTheme,
|
||||||
|
suggestionProvider,
|
||||||
|
signatureProvider,
|
||||||
|
hoverProvider,
|
||||||
|
languageConfiguration,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const _editorDidMount = useCallback(
|
||||||
|
(editor: monaco.editor.IStandaloneCodeEditor, __monaco: unknown) => {
|
||||||
|
if (__monaco !== monaco) {
|
||||||
|
throw new Error('react-monaco-editor is using a different version of monaco');
|
||||||
|
}
|
||||||
|
|
||||||
|
remeasureFonts();
|
||||||
|
|
||||||
|
_editor.current = editor;
|
||||||
|
|
||||||
|
const textbox = editor.getDomNode()?.getElementsByTagName('textarea')[0];
|
||||||
|
if (textbox) {
|
||||||
|
// Make sure the textarea is not directly accesible with TAB
|
||||||
|
textbox.tabIndex = -1;
|
||||||
|
|
||||||
|
// The Monaco editor seems to override the tabindex and set it back to "0"
|
||||||
|
// so we make sure that whenever the attributes change the tabindex stays at -1
|
||||||
|
textboxMutationObserver.current = new MutationObserver(function onTextboxAttributeChange() {
|
||||||
|
if (textbox.tabIndex >= 0) {
|
||||||
|
textbox.tabIndex = -1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
textboxMutationObserver.current.observe(textbox, { attributes: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.onKeyDown(onKeydownMonaco);
|
||||||
|
editor.onDidBlurEditorText(onBlurMonaco);
|
||||||
|
|
||||||
|
// "widget" is not part of the TS interface but does exist
|
||||||
|
// @ts-expect-errors
|
||||||
|
const suggestionWidget = editor.getContribution('editor.contrib.suggestController')?.widget
|
||||||
|
?.value;
|
||||||
|
|
||||||
|
// As I haven't found official documentation for "onDidShow" and "onDidHide"
|
||||||
|
// we guard from possible changes in the underlying lib
|
||||||
|
if (suggestionWidget && suggestionWidget.onDidShow && suggestionWidget.onDidHide) {
|
||||||
|
suggestionWidget.onDidShow(() => {
|
||||||
|
isSuggestionMenuOpen.current = true;
|
||||||
|
});
|
||||||
|
suggestionWidget.onDidHide(() => {
|
||||||
|
isSuggestionMenuOpen.current = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
editorDidMount?.(editor);
|
||||||
|
},
|
||||||
|
[editorDidMount, onBlurMonaco, onKeydownMonaco]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
textboxMutationObserver.current?.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (placeholder && !value && _editor.current) {
|
||||||
|
// Mounts editor inside constructor
|
||||||
|
_placeholderWidget.current = new PlaceholderWidget(placeholder, _editor.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
_placeholderWidget.current?.dispose();
|
||||||
|
_placeholderWidget.current = null;
|
||||||
|
};
|
||||||
|
}, [placeholder, value]);
|
||||||
|
|
||||||
|
const { CopyButton } = useCopy({ isCopyable, value });
|
||||||
|
|
||||||
|
const controlStyles = useMemo(() => {
|
||||||
|
const copyableStyles = [defaultStyles, codeEditorControlsStyles(euiTheme.size, euiTheme.base)];
|
||||||
|
return allowFullScreen || isCopyable ? copyableStyles && defaultStyles : defaultStyles;
|
||||||
|
}, [allowFullScreen, isCopyable, defaultStyles, euiTheme]);
|
||||||
|
|
||||||
|
const theme = useMemo(() => {
|
||||||
|
// register theme for dark or light
|
||||||
|
monaco.editor.defineTheme('euiColors', useDarkTheme ? DARK_THEME : LIGHT_THEME);
|
||||||
|
return options?.theme ?? (transparentBackground ? 'euiColorsTransparent' : 'euiColors');
|
||||||
|
}, [useDarkTheme, transparentBackground, options]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div css={codeEditorStyles()} onKeyDown={onKeyDown}>
|
||||||
|
{renderPrompt()}
|
||||||
|
|
||||||
|
<FullScreenDisplay>
|
||||||
|
{allowFullScreen || isCopyable ? (
|
||||||
|
<div css={controlStyles}>
|
||||||
|
<EuiFlexGroup gutterSize="xs">
|
||||||
|
<EuiFlexItem>
|
||||||
|
<CopyButton />
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem>
|
||||||
|
<FullScreenButton />
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<MonacoEditor
|
||||||
|
theme={theme}
|
||||||
|
language={languageId}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
width={isFullScreen ? '100vw' : width}
|
||||||
|
// previously defaulted to height which defaulted to 100% but this makes it unviewable
|
||||||
|
height={isFullScreen ? '100vh' : '100px'}
|
||||||
|
editorWillMount={_editorWillMount}
|
||||||
|
editorDidMount={_editorDidMount}
|
||||||
|
options={{
|
||||||
|
padding: allowFullScreen || isCopyable ? { top: 24 } : {},
|
||||||
|
renderLineHighlight: 'none',
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
scrollbar: {
|
||||||
|
useShadows: false,
|
||||||
|
// Scroll events are handled only when there is scrollable content. When there is scrollable content, the
|
||||||
|
// editor should scroll to the bottom then break out of that scroll context and continue scrolling on any
|
||||||
|
// outer scrollbars.
|
||||||
|
alwaysConsumeMouseWheel: false,
|
||||||
|
},
|
||||||
|
wordBasedSuggestions: false,
|
||||||
|
wordWrap: 'on',
|
||||||
|
wrappingIndent: 'indent',
|
||||||
|
matchBrackets: 'never',
|
||||||
|
fontFamily: 'Roboto Mono',
|
||||||
|
fontSize: isFullScreen ? 16 : 12,
|
||||||
|
lineHeight: isFullScreen ? 24 : 21,
|
||||||
|
...options,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FullScreenDisplay>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fullscreen logic
|
||||||
|
*/
|
||||||
|
|
||||||
|
const useFullScreen = ({ allowFullScreen }: { allowFullScreen?: boolean }) => {
|
||||||
|
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||||
|
|
||||||
|
const toggleFullScreen = () => {
|
||||||
|
setIsFullScreen(!isFullScreen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = useCallback((event: KeyboardEvent<HTMLElement>) => {
|
||||||
|
if (event.key === keys.ESCAPE) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setIsFullScreen(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const FullScreenButton: React.FC = () => {
|
||||||
|
if (!allowFullScreen) return null;
|
||||||
|
return (
|
||||||
|
<EuiI18n
|
||||||
|
tokens={['euiCodeBlock.fullscreenCollapse', 'euiCodeBlock.fullscreenExpand']}
|
||||||
|
defaults={['Collapse', 'Expand']}
|
||||||
|
>
|
||||||
|
{([fullscreenCollapse, fullscreenExpand]: string[]) => (
|
||||||
|
<EuiButtonIcon
|
||||||
|
css={[codeEditorStyles(), codeEditorFullScreenStyles]}
|
||||||
|
onClick={toggleFullScreen}
|
||||||
|
iconType={isFullScreen ? 'fullScreenExit' : 'fullScreen'}
|
||||||
|
color="text"
|
||||||
|
aria-label={isFullScreen ? fullscreenCollapse : fullscreenExpand}
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</EuiI18n>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { euiTheme } = useEuiTheme();
|
||||||
|
|
||||||
|
const FullScreenDisplay = useMemo(
|
||||||
|
() =>
|
||||||
|
({ children }: { children: Array<JSX.Element | null> | JSX.Element }) => {
|
||||||
|
if (!isFullScreen) return <>{children}</>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiOverlayMask>
|
||||||
|
<EuiFocusTrap clickOutsideDisables={true}>
|
||||||
|
<div
|
||||||
|
css={[
|
||||||
|
codeEditorStyles(),
|
||||||
|
codeEditorFullScreenStyles(),
|
||||||
|
codeEditorControlsWithinFullScreenStyles(euiTheme.size.l),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</EuiFocusTrap>
|
||||||
|
</EuiOverlayMask>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[isFullScreen, euiTheme]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
FullScreenButton,
|
||||||
|
FullScreenDisplay,
|
||||||
|
onKeyDown,
|
||||||
|
isFullScreen,
|
||||||
|
setIsFullScreen,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useCopy = ({ isCopyable, value }: { isCopyable: boolean; value: string }) => {
|
||||||
|
const showCopyButton = isCopyable && value;
|
||||||
|
|
||||||
|
const CopyButton = () => {
|
||||||
|
if (!showCopyButton) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div css={codeEditorStyles()} className="euiCodeBlock__copyButton">
|
||||||
|
<EuiI18n token="euiCodeBlock.copyButton" default="Copy">
|
||||||
|
{(copyButton: string) => (
|
||||||
|
<EuiCopy textToCopy={value}>
|
||||||
|
{(copy) => (
|
||||||
|
<EuiButtonIcon
|
||||||
|
onClick={copy}
|
||||||
|
iconType="copyClipboard"
|
||||||
|
color="text"
|
||||||
|
aria-label={copyButton}
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</EuiCopy>
|
||||||
|
)}
|
||||||
|
</EuiI18n>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { showCopyButton, CopyButton };
|
||||||
|
};
|
211
packages/shared-ux/code_editor/impl/editor.styles.ts
Normal file
211
packages/shared-ux/code_editor/impl/editor.styles.ts
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
/*
|
||||||
|
* 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 { ComponentSelector, css, CSSObject, SerializedStyles } from '@emotion/react';
|
||||||
|
import { ArrayCSSInterpolation } from '@emotion/serialize';
|
||||||
|
import { Property } from 'csstype';
|
||||||
|
|
||||||
|
import { monaco } from '@kbn/monaco';
|
||||||
|
import { euiLightVars as lightTheme, euiDarkVars as darkTheme } from '@kbn/ui-theme';
|
||||||
|
|
||||||
|
export const codeEditorMonacoStyles = () => css`
|
||||||
|
{
|
||||||
|
animation: none !important; // Removes textarea EUI blue underline animation from EUI
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const codeEditorStyles = () => css`
|
||||||
|
{
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const codeEditorPlaceholderContainerStyles = (subduedText: string) => css`
|
||||||
|
{
|
||||||
|
color: ${subduedText};
|
||||||
|
width: max-content;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const codeEditorKeyboardHintStyles = (levels: {
|
||||||
|
content: Property.ZIndex;
|
||||||
|
mask: Property.ZIndex;
|
||||||
|
toast: Property.ZIndex;
|
||||||
|
modal: Property.ZIndex;
|
||||||
|
navigation: Property.ZIndex;
|
||||||
|
menu: Property.ZIndex;
|
||||||
|
header: Property.ZIndex;
|
||||||
|
flyout: Property.ZIndex;
|
||||||
|
maskBelowHeader: Property.ZIndex;
|
||||||
|
}) =>
|
||||||
|
css`
|
||||||
|
{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
z-index: ${levels.mask};
|
||||||
|
}
|
||||||
|
|
||||||
|
&--isInactive {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const codeEditorControlsStyles = (
|
||||||
|
size: {
|
||||||
|
base: string;
|
||||||
|
xxs: string;
|
||||||
|
xs: string;
|
||||||
|
s: string;
|
||||||
|
m: string;
|
||||||
|
l: string;
|
||||||
|
xl: string;
|
||||||
|
xxl: string;
|
||||||
|
xxxl: string;
|
||||||
|
xxxxl: string;
|
||||||
|
},
|
||||||
|
base:
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| ComponentSelector
|
||||||
|
| SerializedStyles
|
||||||
|
| CSSObject
|
||||||
|
| ArrayCSSInterpolation
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
) => css`
|
||||||
|
{
|
||||||
|
top: ${size.xs};
|
||||||
|
right: ${base};
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const codeEditorFullScreenStyles = () => css`
|
||||||
|
{
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const codeEditorControlsWithinFullScreenStyles = (size: string) => css`
|
||||||
|
top: ${size};
|
||||||
|
right: ${size};
|
||||||
|
}`;
|
||||||
|
|
||||||
|
// NOTE: For talk around where this theme information will ultimately live,
|
||||||
|
// please see this discuss issue: https://github.com/elastic/kibana/issues/43814
|
||||||
|
|
||||||
|
export function createTheme(
|
||||||
|
euiTheme: typeof darkTheme | typeof lightTheme,
|
||||||
|
selectionBackgroundColor: string,
|
||||||
|
backgroundColor?: string
|
||||||
|
): monaco.editor.IStandaloneThemeData {
|
||||||
|
return {
|
||||||
|
base: 'vs',
|
||||||
|
inherit: true,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
token: '',
|
||||||
|
foreground: euiTheme.euiColorDarkestShade,
|
||||||
|
background: euiTheme.euiFormBackgroundColor,
|
||||||
|
},
|
||||||
|
{ token: 'invalid', foreground: euiTheme.euiColorAccent },
|
||||||
|
{ token: 'emphasis', fontStyle: 'italic' },
|
||||||
|
{ token: 'strong', fontStyle: 'bold' },
|
||||||
|
|
||||||
|
{ token: 'variable', foreground: euiTheme.euiColorPrimary },
|
||||||
|
{ token: 'variable.predefined', foreground: euiTheme.euiColorSuccess },
|
||||||
|
{ token: 'constant', foreground: euiTheme.euiColorAccent },
|
||||||
|
{ token: 'comment', foreground: euiTheme.euiColorMediumShade },
|
||||||
|
{ token: 'number', foreground: euiTheme.euiColorAccent },
|
||||||
|
{ token: 'number.hex', foreground: euiTheme.euiColorAccent },
|
||||||
|
{ token: 'regexp', foreground: euiTheme.euiColorDanger },
|
||||||
|
{ token: 'annotation', foreground: euiTheme.euiColorMediumShade },
|
||||||
|
{ token: 'type', foreground: euiTheme.euiColorVis0 },
|
||||||
|
|
||||||
|
{ token: 'delimiter', foreground: euiTheme.euiTextSubduedColor },
|
||||||
|
{ token: 'delimiter.html', foreground: euiTheme.euiColorDarkShade },
|
||||||
|
{ token: 'delimiter.xml', foreground: euiTheme.euiColorPrimary },
|
||||||
|
|
||||||
|
{ token: 'tag', foreground: euiTheme.euiColorDanger },
|
||||||
|
{ token: 'tag.id.jade', foreground: euiTheme.euiColorPrimary },
|
||||||
|
{ token: 'tag.class.jade', foreground: euiTheme.euiColorPrimary },
|
||||||
|
{ token: 'meta.scss', foreground: euiTheme.euiColorAccent },
|
||||||
|
{ token: 'metatag', foreground: euiTheme.euiColorSuccess },
|
||||||
|
{ token: 'metatag.content.html', foreground: euiTheme.euiColorDanger },
|
||||||
|
{ token: 'metatag.html', foreground: euiTheme.euiColorMediumShade },
|
||||||
|
{ token: 'metatag.xml', foreground: euiTheme.euiColorMediumShade },
|
||||||
|
{ token: 'metatag.php', fontStyle: 'bold' },
|
||||||
|
|
||||||
|
{ token: 'key', foreground: euiTheme.euiColorWarning },
|
||||||
|
{ token: 'string.key.json', foreground: euiTheme.euiColorDanger },
|
||||||
|
{ token: 'string.value.json', foreground: euiTheme.euiColorPrimary },
|
||||||
|
|
||||||
|
{ token: 'attribute.name', foreground: euiTheme.euiColorDanger },
|
||||||
|
{ token: 'attribute.name.css', foreground: euiTheme.euiColorSuccess },
|
||||||
|
{ token: 'attribute.value', foreground: euiTheme.euiColorPrimary },
|
||||||
|
{ token: 'attribute.value.number', foreground: euiTheme.euiColorWarning },
|
||||||
|
{ token: 'attribute.value.unit', foreground: euiTheme.euiColorWarning },
|
||||||
|
{ token: 'attribute.value.html', foreground: euiTheme.euiColorPrimary },
|
||||||
|
{ token: 'attribute.value.xml', foreground: euiTheme.euiColorPrimary },
|
||||||
|
|
||||||
|
{ token: 'string', foreground: euiTheme.euiColorDanger },
|
||||||
|
{ token: 'string.html', foreground: euiTheme.euiColorPrimary },
|
||||||
|
{ token: 'string.sql', foreground: euiTheme.euiColorDanger },
|
||||||
|
{ token: 'string.yaml', foreground: euiTheme.euiColorPrimary },
|
||||||
|
|
||||||
|
{ token: 'keyword', foreground: euiTheme.euiColorPrimary },
|
||||||
|
{ token: 'keyword.json', foreground: euiTheme.euiColorPrimary },
|
||||||
|
{ token: 'keyword.flow', foreground: euiTheme.euiColorWarning },
|
||||||
|
{ token: 'keyword.flow.scss', foreground: euiTheme.euiColorPrimary },
|
||||||
|
// Monaco editor supports strikethrough font style only starting from 0.32.0.
|
||||||
|
{ token: 'keyword.deprecated', foreground: euiTheme.euiColorAccent },
|
||||||
|
|
||||||
|
{ token: 'operator.scss', foreground: euiTheme.euiColorDarkShade },
|
||||||
|
{ token: 'operator.sql', foreground: euiTheme.euiColorMediumShade },
|
||||||
|
{ token: 'operator.swift', foreground: euiTheme.euiColorMediumShade },
|
||||||
|
{ token: 'predefined.sql', foreground: euiTheme.euiColorMediumShade },
|
||||||
|
|
||||||
|
{ token: 'text', foreground: euiTheme.euiTitleColor },
|
||||||
|
{ token: 'label', foreground: euiTheme.euiColorVis9 },
|
||||||
|
],
|
||||||
|
colors: {
|
||||||
|
'editor.foreground': euiTheme.euiColorDarkestShade,
|
||||||
|
'editor.background': backgroundColor ?? euiTheme.euiFormBackgroundColor,
|
||||||
|
'editorLineNumber.foreground': euiTheme.euiColorDarkShade,
|
||||||
|
'editorLineNumber.activeForeground': euiTheme.euiColorDarkShade,
|
||||||
|
'editorIndentGuide.background': euiTheme.euiColorLightShade,
|
||||||
|
'editor.selectionBackground': selectionBackgroundColor,
|
||||||
|
'editorWidget.border': euiTheme.euiColorLightShade,
|
||||||
|
'editorWidget.background': euiTheme.euiColorLightestShade,
|
||||||
|
'editorCursor.foreground': euiTheme.euiColorDarkestShade,
|
||||||
|
'editorSuggestWidget.selectedBackground': euiTheme.euiColorLightShade,
|
||||||
|
'list.hoverBackground': euiTheme.euiColorLightShade,
|
||||||
|
'list.highlightForeground': euiTheme.euiColorPrimary,
|
||||||
|
'editor.lineHighlightBorder': euiTheme.euiColorLightestShade,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const DARK_THEME = createTheme(darkTheme, '#343551');
|
||||||
|
const LIGHT_THEME = createTheme(lightTheme, '#E3E4ED');
|
||||||
|
const DARK_THEME_TRANSPARENT = createTheme(darkTheme, '#343551', '#00000000');
|
||||||
|
const LIGHT_THEME_TRANSPARENT = createTheme(lightTheme, '#E3E4ED', '#00000000');
|
||||||
|
|
||||||
|
export { DARK_THEME, LIGHT_THEME, DARK_THEME_TRANSPARENT, LIGHT_THEME_TRANSPARENT };
|
9
packages/shared-ux/code_editor/impl/index.ts
Normal file
9
packages/shared-ux/code_editor/impl/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* 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 { CodeEditor } from './code_editor';
|
13
packages/shared-ux/code_editor/impl/jest.config.js
Normal file
13
packages/shared-ux/code_editor/impl/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
preset: '@kbn/test',
|
||||||
|
rootDir: '../../../..',
|
||||||
|
roots: ['<rootDir>/packages/shared-ux/code_editor'],
|
||||||
|
};
|
5
packages/shared-ux/code_editor/impl/kibana.jsonc
Normal file
5
packages/shared-ux/code_editor/impl/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"type": "shared-common",
|
||||||
|
"id": "@kbn/code-editor",
|
||||||
|
"owner": "@elastic/shared-ux"
|
||||||
|
}
|
11
packages/shared-ux/code_editor/impl/languages/css/index.ts
Normal file
11
packages/shared-ux/code_editor/impl/languages/css/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
* 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 { LangModuleType } from '@kbn/monaco';
|
||||||
|
import { lexerRules, languageConfiguration } from './language';
|
||||||
|
|
||||||
|
export const Lang: LangModuleType = { ID: 'css', lexerRules, languageConfiguration };
|
184
packages/shared-ux/code_editor/impl/languages/css/language.ts
Normal file
184
packages/shared-ux/code_editor/impl/languages/css/language.ts
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
/*
|
||||||
|
* 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 { monaco } from '@kbn/monaco';
|
||||||
|
|
||||||
|
export const languageConfiguration: monaco.languages.LanguageConfiguration = {
|
||||||
|
wordPattern: /(#?-?\d*\.\d\w*%?)|((::|[@#.!:])?[\w-?]+%?)|::|[@#.!:]/g,
|
||||||
|
comments: {
|
||||||
|
blockComment: ['/*', '*/'],
|
||||||
|
},
|
||||||
|
brackets: [
|
||||||
|
['{', '}'],
|
||||||
|
['[', ']'],
|
||||||
|
['(', ')'],
|
||||||
|
],
|
||||||
|
autoClosingPairs: [
|
||||||
|
{ open: '{', close: '}', notIn: ['string', 'comment'] },
|
||||||
|
{ open: '[', close: ']', notIn: ['string', 'comment'] },
|
||||||
|
{ open: '(', close: ')', notIn: ['string', 'comment'] },
|
||||||
|
{ open: '"', close: '"', notIn: ['string', 'comment'] },
|
||||||
|
{ open: "'", close: "'", notIn: ['string', 'comment'] },
|
||||||
|
],
|
||||||
|
surroundingPairs: [
|
||||||
|
{ open: '{', close: '}' },
|
||||||
|
{ open: '[', close: ']' },
|
||||||
|
{ open: '(', close: ')' },
|
||||||
|
{ open: '"', close: '"' },
|
||||||
|
{ open: "'", close: "'" },
|
||||||
|
],
|
||||||
|
folding: {
|
||||||
|
markers: {
|
||||||
|
start: new RegExp('^\\s*\\/\\*\\s*#region\\b\\s*(.*?)\\s*\\*\\/'),
|
||||||
|
end: new RegExp('^\\s*\\/\\*\\s*#endregion\\b.*\\*\\/'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lexerRules: monaco.languages.IMonarchLanguage = {
|
||||||
|
defaultToken: '',
|
||||||
|
tokenPostfix: '.css',
|
||||||
|
ws: '[ \t\n\r\f]*',
|
||||||
|
identifier:
|
||||||
|
'-?-?([a-zA-Z]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))([\\w\\-]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))*',
|
||||||
|
brackets: [
|
||||||
|
{ open: '{', close: '}', token: 'delimiter.bracket' },
|
||||||
|
{ open: '[', close: ']', token: 'delimiter.bracket' },
|
||||||
|
{ open: '(', close: ')', token: 'delimiter.parenthesis' },
|
||||||
|
{ open: '<', close: '>', token: 'delimiter.angle' },
|
||||||
|
],
|
||||||
|
tokenizer: {
|
||||||
|
root: [{ include: '@selector' }],
|
||||||
|
selector: [
|
||||||
|
{ include: '@comments' },
|
||||||
|
{ include: '@import' },
|
||||||
|
{ include: '@strings' },
|
||||||
|
[
|
||||||
|
'[@](keyframes|-webkit-keyframes|-moz-keyframes|-o-keyframes)',
|
||||||
|
{ token: 'keyword', next: '@keyframedeclaration' },
|
||||||
|
],
|
||||||
|
['[@](page|content|font-face|-moz-document)', { token: 'keyword' }],
|
||||||
|
['[@](charset|namespace)', { token: 'keyword', next: '@declarationbody' }],
|
||||||
|
[
|
||||||
|
'(url-prefix)(\\()',
|
||||||
|
['attribute.value', { token: 'delimiter.parenthesis', next: '@urldeclaration' }],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'(url)(\\()',
|
||||||
|
['attribute.value', { token: 'delimiter.parenthesis', next: '@urldeclaration' }],
|
||||||
|
],
|
||||||
|
{ include: '@selectorname' },
|
||||||
|
['[\\*]', 'tag'],
|
||||||
|
['[>\\+,]', 'delimiter'],
|
||||||
|
['\\[', { token: 'delimiter.bracket', next: '@selectorattribute' }],
|
||||||
|
['{', { token: 'delimiter.bracket', next: '@selectorbody' }],
|
||||||
|
],
|
||||||
|
selectorbody: [
|
||||||
|
{ include: '@comments' },
|
||||||
|
['[*_]?@identifier@ws:(?=(\\s|\\d|[^{;}]*[;}]))', 'attribute.name', '@rulevalue'],
|
||||||
|
['}', { token: 'delimiter.bracket', next: '@pop' }],
|
||||||
|
],
|
||||||
|
selectorname: [['(\\.|#(?=[^{])|%|(@identifier)|:)+', 'tag']],
|
||||||
|
selectorattribute: [{ include: '@term' }, [']', { token: 'delimiter.bracket', next: '@pop' }]],
|
||||||
|
term: [
|
||||||
|
{ include: '@comments' },
|
||||||
|
[
|
||||||
|
'(url-prefix)(\\()',
|
||||||
|
['attribute.value', { token: 'delimiter.parenthesis', next: '@urldeclaration' }],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'(url)(\\()',
|
||||||
|
['attribute.value', { token: 'delimiter.parenthesis', next: '@urldeclaration' }],
|
||||||
|
],
|
||||||
|
{ include: '@functioninvocation' },
|
||||||
|
{ include: '@numbers' },
|
||||||
|
{ include: '@name' },
|
||||||
|
['([<>=\\+\\-\\*\\/\\^\\|\\~,])', 'delimiter'],
|
||||||
|
[',', 'delimiter'],
|
||||||
|
],
|
||||||
|
rulevalue: [
|
||||||
|
{ include: '@comments' },
|
||||||
|
{ include: '@strings' },
|
||||||
|
{ include: '@term' },
|
||||||
|
['!important', 'keyword'],
|
||||||
|
[';', 'delimiter', '@pop'],
|
||||||
|
['(?=})', { token: '', next: '@pop' }], // missing semicolon
|
||||||
|
],
|
||||||
|
warndebug: [['[@](warn|debug)', { token: 'keyword', next: '@declarationbody' }]],
|
||||||
|
import: [['[@](import)', { token: 'keyword', next: '@declarationbody' }]],
|
||||||
|
urldeclaration: [
|
||||||
|
{ include: '@strings' },
|
||||||
|
['[^)\r\n]+', 'string'],
|
||||||
|
['\\)', { token: 'delimiter.parenthesis', next: '@pop' }],
|
||||||
|
],
|
||||||
|
parenthizedterm: [
|
||||||
|
{ include: '@term' },
|
||||||
|
['\\)', { token: 'delimiter.parenthesis', next: '@pop' }],
|
||||||
|
],
|
||||||
|
declarationbody: [
|
||||||
|
{ include: '@term' },
|
||||||
|
[';', 'delimiter', '@pop'],
|
||||||
|
['(?=})', { token: '', next: '@pop' }], // missing semicolon
|
||||||
|
],
|
||||||
|
comments: [
|
||||||
|
['\\/\\*', 'comment', '@comment'],
|
||||||
|
['\\/\\/+.*', 'comment'],
|
||||||
|
],
|
||||||
|
comment: [
|
||||||
|
['\\*\\/', 'comment', '@pop'],
|
||||||
|
[/[^*/]+/, 'comment'],
|
||||||
|
[/./, 'comment'],
|
||||||
|
],
|
||||||
|
name: [['@identifier', 'attribute.value']],
|
||||||
|
numbers: [
|
||||||
|
['-?(\\d*\\.)?\\d+([eE][\\-+]?\\d+)?', { token: 'attribute.value.number', next: '@units' }],
|
||||||
|
['#[0-9a-fA-F_]+(?!\\w)', 'attribute.value.hex'],
|
||||||
|
],
|
||||||
|
units: [
|
||||||
|
[
|
||||||
|
'(em|ex|ch|rem|vmin|vmax|vw|vh|vm|cm|mm|in|px|pt|pc|deg|grad|rad|turn|s|ms|Hz|kHz|%)?',
|
||||||
|
'attribute.value.unit',
|
||||||
|
'@pop',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
keyframedeclaration: [
|
||||||
|
['@identifier', 'attribute.value'],
|
||||||
|
['{', { token: 'delimiter.bracket', switchTo: '@keyframebody' }],
|
||||||
|
],
|
||||||
|
keyframebody: [
|
||||||
|
{ include: '@term' },
|
||||||
|
['{', { token: 'delimiter.bracket', next: '@selectorbody' }],
|
||||||
|
['}', { token: 'delimiter.bracket', next: '@pop' }],
|
||||||
|
],
|
||||||
|
functioninvocation: [
|
||||||
|
['@identifier\\(', { token: 'attribute.value', next: '@functionarguments' }],
|
||||||
|
],
|
||||||
|
functionarguments: [
|
||||||
|
['\\$@identifier@ws:', 'attribute.name'],
|
||||||
|
['[,]', 'delimiter'],
|
||||||
|
{ include: '@term' },
|
||||||
|
['\\)', { token: 'attribute.value', next: '@pop' }],
|
||||||
|
],
|
||||||
|
strings: [
|
||||||
|
['~?"', { token: 'string', next: '@stringenddoublequote' }],
|
||||||
|
["~?'", { token: 'string', next: '@stringendquote' }],
|
||||||
|
],
|
||||||
|
stringenddoublequote: [
|
||||||
|
['\\\\.', 'string'],
|
||||||
|
['"', { token: 'string', next: '@pop' }],
|
||||||
|
[/[^\\"]+/, 'string'],
|
||||||
|
['.', 'string'],
|
||||||
|
],
|
||||||
|
stringendquote: [
|
||||||
|
['\\\\.', 'string'],
|
||||||
|
["'", { token: 'string', next: '@pop' }],
|
||||||
|
[/[^\\']+/, 'string'],
|
||||||
|
['.', 'string'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as monaco.languages.IMonarchLanguage;
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* 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 { LangModuleType } from '@kbn/monaco';
|
||||||
|
import { languageConfiguration, lexerRules } from './language';
|
||||||
|
|
||||||
|
export const Lang: LangModuleType = { ID: 'handlebars', languageConfiguration, lexerRules };
|
|
@ -0,0 +1,198 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is adapted from: https://github.com/microsoft/monaco-languages/blob/master/src/handlebars/handlebars.ts
|
||||||
|
* License: https://github.com/microsoft/monaco-languages/blob/master/LICENSE.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { monaco } from '@kbn/monaco';
|
||||||
|
|
||||||
|
export const languageConfiguration: monaco.languages.LanguageConfiguration = {
|
||||||
|
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
|
||||||
|
|
||||||
|
comments: {
|
||||||
|
blockComment: ['{{!--', '--}}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
brackets: [
|
||||||
|
['<', '>'],
|
||||||
|
['{{', '}}'],
|
||||||
|
['{', '}'],
|
||||||
|
['(', ')'],
|
||||||
|
],
|
||||||
|
|
||||||
|
autoClosingPairs: [
|
||||||
|
{ open: '{', close: '}' },
|
||||||
|
{ open: '[', close: ']' },
|
||||||
|
{ open: '(', close: ')' },
|
||||||
|
{ open: '"', close: '"' },
|
||||||
|
{ open: "'", close: "'" },
|
||||||
|
],
|
||||||
|
|
||||||
|
surroundingPairs: [
|
||||||
|
{ open: '<', close: '>' },
|
||||||
|
{ open: '"', close: '"' },
|
||||||
|
{ open: "'", close: "'" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lexerRules: monaco.languages.IMonarchLanguage = {
|
||||||
|
// Set defaultToken to invalid to see what you do not tokenize yet.
|
||||||
|
defaultToken: 'invalid',
|
||||||
|
tokenPostfix: '',
|
||||||
|
brackets: [
|
||||||
|
{
|
||||||
|
token: 'constant.delimiter.double',
|
||||||
|
open: '{{',
|
||||||
|
close: '}}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: 'constant.delimiter.triple',
|
||||||
|
open: '{{{',
|
||||||
|
close: '}}}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
{ include: '@maybeHandlebars' },
|
||||||
|
{ include: '@whitespace' },
|
||||||
|
{ include: '@urlScheme' },
|
||||||
|
{ include: '@urlAuthority' },
|
||||||
|
{ include: '@urlSlash' },
|
||||||
|
{ include: '@urlParamKey' },
|
||||||
|
{ include: '@urlParamValue' },
|
||||||
|
{ include: '@text' },
|
||||||
|
],
|
||||||
|
|
||||||
|
maybeHandlebars: [
|
||||||
|
[
|
||||||
|
/\{\{/,
|
||||||
|
{
|
||||||
|
token: '@rematch',
|
||||||
|
switchTo: '@handlebars.root',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
whitespace: [[/[ \t\r\n]+/, '']],
|
||||||
|
|
||||||
|
text: [
|
||||||
|
[
|
||||||
|
/[^<{\?\&\/]+/,
|
||||||
|
{
|
||||||
|
token: 'text',
|
||||||
|
next: '@popall',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
rematchAsRoot: [
|
||||||
|
[
|
||||||
|
/.+/,
|
||||||
|
{
|
||||||
|
token: '@rematch',
|
||||||
|
switchTo: '@root',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
urlScheme: [
|
||||||
|
[
|
||||||
|
/([a-zA-Z0-9\+\.\-]{1,10})(:)/,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
token: 'text.keyword.scheme.url',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: 'delimiter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
urlAuthority: [
|
||||||
|
[
|
||||||
|
/(\/\/)([a-zA-Z0-9\.\-_]+)/,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
token: 'delimiter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: 'metatag.keyword.authority.url',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
urlSlash: [
|
||||||
|
[
|
||||||
|
/\/+/,
|
||||||
|
{
|
||||||
|
token: 'delimiter',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
urlParamKey: [
|
||||||
|
[
|
||||||
|
/([\?\&\#])([a-zA-Z0-9_\-]+)/,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
token: 'delimiter.key.query.url',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: 'label.label.key.query.url',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
urlParamValue: [
|
||||||
|
[
|
||||||
|
/(\=)([^\?\&\{}]+)/,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
token: 'text.separator.value.query.url',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: 'text.value.query.url',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
handlebars: [
|
||||||
|
[
|
||||||
|
/\{\{\{?/,
|
||||||
|
{
|
||||||
|
token: '@brackets',
|
||||||
|
bracket: '@open',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
/\}\}\}?/,
|
||||||
|
{
|
||||||
|
token: '@brackets',
|
||||||
|
bracket: '@close',
|
||||||
|
switchTo: '@$S2.$S3',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ include: 'handlebarsExpression' },
|
||||||
|
],
|
||||||
|
|
||||||
|
handlebarsExpression: [
|
||||||
|
[/"[^"]*"/, 'string.handlebars'],
|
||||||
|
[/[#/][^\s}]+/, 'keyword.helper.handlebars'],
|
||||||
|
[/else\b/, 'keyword.helper.handlebars'],
|
||||||
|
[/[\s]+/],
|
||||||
|
[/[^}]/, 'variable.parameter.handlebars'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as monaco.languages.IMonarchLanguage;
|
12
packages/shared-ux/code_editor/impl/languages/hjson/index.ts
Normal file
12
packages/shared-ux/code_editor/impl/languages/hjson/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
* 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 { LangModuleType } from '@kbn/monaco';
|
||||||
|
import { languageConfiguration, lexerRules } from './language';
|
||||||
|
|
||||||
|
export const Lang: LangModuleType = { ID: 'hjson', languageConfiguration, lexerRules };
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* 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 { monaco } from '@kbn/monaco';
|
||||||
|
|
||||||
|
export const languageConfiguration: monaco.languages.LanguageConfiguration = {
|
||||||
|
brackets: [
|
||||||
|
['{', '}'],
|
||||||
|
['[', ']'],
|
||||||
|
],
|
||||||
|
autoClosingPairs: [
|
||||||
|
{ open: '{', close: '}' },
|
||||||
|
{ open: '[', close: ']' },
|
||||||
|
{ open: '"', close: '"', notIn: ['string'] },
|
||||||
|
],
|
||||||
|
comments: {
|
||||||
|
lineComment: '//',
|
||||||
|
blockComment: ['/*', '*/'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lexerRules: monaco.languages.IMonarchLanguage = {
|
||||||
|
defaultToken: '',
|
||||||
|
tokenPostfix: '',
|
||||||
|
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
|
||||||
|
digits: /-?(?:0|[1-9]\d*)(?:(?:\.\d+)?(?:[eE][+-]?\d+)?)?/,
|
||||||
|
symbols: /[,:]+/,
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
[/(@digits)n?/, 'number'],
|
||||||
|
[/(@symbols)n?/, 'delimiter'],
|
||||||
|
|
||||||
|
{ include: '@keyword' },
|
||||||
|
{ include: '@url' },
|
||||||
|
{ include: '@whitespace' },
|
||||||
|
{ include: '@brackets' },
|
||||||
|
{ include: '@keyName' },
|
||||||
|
{ include: '@string' },
|
||||||
|
],
|
||||||
|
|
||||||
|
keyword: [[/(?:true|false|null)\b/, 'keyword']],
|
||||||
|
|
||||||
|
url: [
|
||||||
|
[
|
||||||
|
/(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/,
|
||||||
|
'string',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
keyName: [[/(?:[^,\{\[\}\]\s]+|"(?:[^"\\]|\\.)*")\s*(?=:)/, 'variable']],
|
||||||
|
|
||||||
|
brackets: [[/{/, '@push'], [/}/, '@pop'], [/[[(]/], [/[\])]/]],
|
||||||
|
|
||||||
|
whitespace: [
|
||||||
|
[/[ \t\r\n]+/, ''],
|
||||||
|
[/\/\*/, 'comment', '@comment'],
|
||||||
|
[/\/\/.*$/, 'comment'],
|
||||||
|
],
|
||||||
|
|
||||||
|
comment: [
|
||||||
|
[/[^\/*]+/, 'comment'],
|
||||||
|
[/\*\//, 'comment', '@pop'],
|
||||||
|
[/[\/*]/, 'comment'],
|
||||||
|
],
|
||||||
|
|
||||||
|
string: [
|
||||||
|
[/(?:[^,\{\[\}\]\s]+|"(?:[^"\\]|\\.)*")\s*/, 'string'],
|
||||||
|
[/"""/, 'string', '@stringLiteral'],
|
||||||
|
[/"/, 'string', '@stringDouble'],
|
||||||
|
],
|
||||||
|
|
||||||
|
stringDouble: [
|
||||||
|
[/[^\\"]+/, 'string'],
|
||||||
|
[/@escapes/, 'string.escape'],
|
||||||
|
[/\\./, 'string.escape.invalid'],
|
||||||
|
[/"/, 'string', '@pop'],
|
||||||
|
],
|
||||||
|
|
||||||
|
stringLiteral: [
|
||||||
|
[/"""/, 'string', '@pop'],
|
||||||
|
[/\\""""/, 'string', '@pop'],
|
||||||
|
[/./, 'string'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as monaco.languages.IMonarchLanguage;
|
15
packages/shared-ux/code_editor/impl/languages/index.ts
Normal file
15
packages/shared-ux/code_editor/impl/languages/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* 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 { Lang as CssLang } from './css';
|
||||||
|
import { Lang as HandlebarsLang } from './handlebars';
|
||||||
|
import { Lang as MarkdownLang } from './markdown';
|
||||||
|
import { Lang as YamlLang } from './yaml';
|
||||||
|
import { Lang as HJson } from './hjson';
|
||||||
|
|
||||||
|
export { CssLang, HandlebarsLang, MarkdownLang, YamlLang, HJson };
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
* 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 { LangModuleType } from '@kbn/monaco';
|
||||||
|
import { languageConfiguration, lexerRules } from './language';
|
||||||
|
|
||||||
|
export const Lang: LangModuleType = { ID: 'markdown', languageConfiguration, lexerRules };
|
|
@ -0,0 +1,223 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is adapted from: https://code-room.io/libs/monaco-editor/esm/vs/basic-languages/markdown/markdown.js
|
||||||
|
* License: https://github.com/microsoft/monaco-languages/blob/master/LICENSE.md
|
||||||
|
*/
|
||||||
|
import { monaco } from '@kbn/monaco';
|
||||||
|
|
||||||
|
export const languageConfiguration: monaco.languages.LanguageConfiguration = {
|
||||||
|
comments: {
|
||||||
|
blockComment: ['<!--', '-->'],
|
||||||
|
},
|
||||||
|
brackets: [
|
||||||
|
['{', '}'],
|
||||||
|
['[', ']'],
|
||||||
|
['(', ')'],
|
||||||
|
],
|
||||||
|
autoClosingPairs: [
|
||||||
|
{ open: '{', close: '}' },
|
||||||
|
{ open: '[', close: ']' },
|
||||||
|
{ open: '(', close: ')' },
|
||||||
|
{ open: '<', close: '>', notIn: ['string'] },
|
||||||
|
],
|
||||||
|
surroundingPairs: [
|
||||||
|
{ open: '(', close: ')' },
|
||||||
|
{ open: '[', close: ']' },
|
||||||
|
{ open: '`', close: '`' },
|
||||||
|
],
|
||||||
|
folding: {
|
||||||
|
markers: {
|
||||||
|
start: new RegExp('^\\s*<!--\\s*#?region\\b.*-->'),
|
||||||
|
end: new RegExp('^\\s*<!--\\s*#?endregion\\b.*-->'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lexerRules: monaco.languages.IMonarchLanguage = {
|
||||||
|
defaultToken: '',
|
||||||
|
tokenPostfix: '.md',
|
||||||
|
// escape codes
|
||||||
|
control: /[\\`*_\[\]{}()#+\-\.!]/,
|
||||||
|
noncontrol: /[^\\`*_\[\]{}()#+\-\.!]/,
|
||||||
|
escapes: /\\(?:@control)/,
|
||||||
|
// escape codes for javascript/CSS strings
|
||||||
|
jsescapes: /\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,
|
||||||
|
// non matched elements
|
||||||
|
empty: [
|
||||||
|
'area',
|
||||||
|
'base',
|
||||||
|
'basefont',
|
||||||
|
'br',
|
||||||
|
'col',
|
||||||
|
'frame',
|
||||||
|
'hr',
|
||||||
|
'img',
|
||||||
|
'input',
|
||||||
|
'isindex',
|
||||||
|
'link',
|
||||||
|
'meta',
|
||||||
|
'param',
|
||||||
|
],
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
// markdown tables
|
||||||
|
[/^\s*\|/, '@rematch', '@table_header'],
|
||||||
|
// headers (with #)
|
||||||
|
[/^(\s{0,3})(#+)((?:[^\\#]|@escapes)+)((?:#+)?)/, ['white', 'keyword', 'keyword', 'keyword']],
|
||||||
|
// headers (with =)
|
||||||
|
[/^\s*(=+|\-+)\s*$/, 'keyword'],
|
||||||
|
// headers (with ***)
|
||||||
|
[/^\s*((\*[ ]?)+)\s*$/, 'meta.separator'],
|
||||||
|
// quote
|
||||||
|
[/^\s*>+/, 'comment'],
|
||||||
|
// list (starting with * or number)
|
||||||
|
[/^\s*([\*\-+:]|\d+\.)\s/, 'keyword'],
|
||||||
|
// code block (4 spaces indent)
|
||||||
|
[/^(\t|[ ]{4})[^ ].*$/, 'string'],
|
||||||
|
// code block (3 tilde)
|
||||||
|
[/^\s*~~~\s*((?:\w|[\/\-#])+)?\s*$/, { token: 'string', next: '@codeblock' }],
|
||||||
|
// github style code blocks (with backticks and language)
|
||||||
|
[
|
||||||
|
/^\s*```\s*((?:\w|[\/\-#])+).*$/,
|
||||||
|
{ token: 'string', next: '@codeblockgh', nextEmbedded: '$1' },
|
||||||
|
],
|
||||||
|
// github style code blocks (with backticks but no language)
|
||||||
|
[/^\s*```\s*$/, { token: 'string', next: '@codeblock' }],
|
||||||
|
// markup within lines
|
||||||
|
{ include: '@linecontent' },
|
||||||
|
],
|
||||||
|
table_header: [{ include: '@table_common' }, [/[^\|]+/, 'keyword.table.header']],
|
||||||
|
table_body: [{ include: '@table_common' }, { include: '@linecontent' }],
|
||||||
|
table_common: [
|
||||||
|
[/\s*[\-:]+\s*/, { token: 'keyword', switchTo: 'table_body' }],
|
||||||
|
[/^\s*\|/, 'keyword.table.left'],
|
||||||
|
[/^\s*[^\|]/, '@rematch', '@pop'],
|
||||||
|
[/^\s*$/, '@rematch', '@pop'],
|
||||||
|
[
|
||||||
|
/\|/,
|
||||||
|
{
|
||||||
|
cases: {
|
||||||
|
'@eos': 'keyword.table.right',
|
||||||
|
'@default': 'keyword.table.middle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
codeblock: [
|
||||||
|
[/^\s*~~~\s*$/, { token: 'string', next: '@pop' }],
|
||||||
|
[/^\s*```\s*$/, { token: 'string', next: '@pop' }],
|
||||||
|
[/.*$/, 'variable.source'],
|
||||||
|
],
|
||||||
|
// github style code blocks
|
||||||
|
codeblockgh: [
|
||||||
|
[/```\s*$/, { token: 'variable.source', next: '@pop', nextEmbedded: '@pop' }],
|
||||||
|
[/[^`]+/, 'variable.source'],
|
||||||
|
],
|
||||||
|
linecontent: [
|
||||||
|
// escapes
|
||||||
|
[/&\w+;/, 'string.escape'],
|
||||||
|
[/@escapes/, 'escape'],
|
||||||
|
// various markup
|
||||||
|
[/\b__([^\\_]|@escapes|_(?!_))+__\b/, 'strong'],
|
||||||
|
[/\*\*([^\\*]|@escapes|\*(?!\*))+\*\*/, 'strong'],
|
||||||
|
[/\b_[^_]+_\b/, 'emphasis'],
|
||||||
|
[/\*([^\\*]|@escapes)+\*/, 'emphasis'],
|
||||||
|
[/`([^\\`]|@escapes)+`/, 'variable'],
|
||||||
|
// links
|
||||||
|
[/\{+[^}]+\}+/, 'string.target'],
|
||||||
|
[/(!?\[)((?:[^\]\\]|@escapes)*)(\]\([^\)]+\))/, ['string.link', '', 'string.link']],
|
||||||
|
[/(!?\[)((?:[^\]\\]|@escapes)*)(\])/, 'string.link'],
|
||||||
|
// or html
|
||||||
|
{ include: 'html' },
|
||||||
|
],
|
||||||
|
// Note: it is tempting to rather switch to the real HTML mode instead of building our own here
|
||||||
|
// but currently there is a limitation in Monarch that prevents us from doing it: The opening
|
||||||
|
// '<' would start the HTML mode, however there is no way to jump 1 character back to let the
|
||||||
|
// HTML mode also tokenize the opening angle bracket. Thus, even though we could jump to HTML,
|
||||||
|
// we cannot correctly tokenize it in that mode yet.
|
||||||
|
html: [
|
||||||
|
// html tags
|
||||||
|
[/<(\w+)\/>/, 'tag'],
|
||||||
|
[
|
||||||
|
/<(\w+)/,
|
||||||
|
{
|
||||||
|
cases: {
|
||||||
|
'@empty': { token: 'tag', next: '@tag.$1' },
|
||||||
|
'@default': { token: 'tag', next: '@tag.$1' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[/<\/(\w+)\s*>/, { token: 'tag' }],
|
||||||
|
[/<!--/, 'comment', '@comment'],
|
||||||
|
],
|
||||||
|
comment: [
|
||||||
|
[/[^<\-]+/, 'comment.content'],
|
||||||
|
[/-->/, 'comment', '@pop'],
|
||||||
|
[/<!--/, 'comment.content.invalid'],
|
||||||
|
[/[<\-]/, 'comment.content'],
|
||||||
|
],
|
||||||
|
// Almost full HTML tag matching, complete with embedded scripts & styles
|
||||||
|
tag: [
|
||||||
|
[/[ \t\r\n]+/, 'white'],
|
||||||
|
[
|
||||||
|
/(type)(\s*=\s*)(")([^"]+)(")/,
|
||||||
|
[
|
||||||
|
'attribute.name.html',
|
||||||
|
'delimiter.html',
|
||||||
|
'string.html',
|
||||||
|
{ token: 'string.html', switchTo: '@tag.$S2.$4' },
|
||||||
|
'string.html',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
/(type)(\s*=\s*)(')([^']+)(')/,
|
||||||
|
[
|
||||||
|
'attribute.name.html',
|
||||||
|
'delimiter.html',
|
||||||
|
'string.html',
|
||||||
|
{ token: 'string.html', switchTo: '@tag.$S2.$4' },
|
||||||
|
'string.html',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[/(\w+)(\s*=\s*)("[^"]*"|'[^']*')/, ['attribute.name.html', 'delimiter.html', 'string.html']],
|
||||||
|
[/\w+/, 'attribute.name.html'],
|
||||||
|
[/\/>/, 'tag', '@pop'],
|
||||||
|
[
|
||||||
|
/>/,
|
||||||
|
{
|
||||||
|
cases: {
|
||||||
|
'$S2==style': { token: 'tag', switchTo: 'embeddedStyle', nextEmbedded: 'text/css' },
|
||||||
|
'$S2==script': {
|
||||||
|
cases: {
|
||||||
|
$S3: { token: 'tag', switchTo: 'embeddedScript', nextEmbedded: '$S3' },
|
||||||
|
'@default': {
|
||||||
|
token: 'tag',
|
||||||
|
switchTo: 'embeddedScript',
|
||||||
|
nextEmbedded: 'text/javascript',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'@default': { token: 'tag', next: '@pop' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
embeddedStyle: [
|
||||||
|
[/[^<]+/, ''],
|
||||||
|
[/<\/style\s*>/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }],
|
||||||
|
[/</, ''],
|
||||||
|
],
|
||||||
|
embeddedScript: [
|
||||||
|
[/[^<]+/, ''],
|
||||||
|
[/<\/script\s*>/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }],
|
||||||
|
[/</, ''],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as monaco.languages.IMonarchLanguage;
|
11
packages/shared-ux/code_editor/impl/languages/yaml/index.ts
Normal file
11
packages/shared-ux/code_editor/impl/languages/yaml/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
* 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 { LangModuleType } from '@kbn/monaco';
|
||||||
|
import { languageConfiguration, lexerRules } from './language';
|
||||||
|
|
||||||
|
export const Lang: LangModuleType = { ID: 'yaml', languageConfiguration, lexerRules };
|
198
packages/shared-ux/code_editor/impl/languages/yaml/language.ts
Normal file
198
packages/shared-ux/code_editor/impl/languages/yaml/language.ts
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
/*
|
||||||
|
* 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 { monaco } from '@kbn/monaco';
|
||||||
|
|
||||||
|
export const languageConfiguration: monaco.languages.LanguageConfiguration = {
|
||||||
|
comments: {
|
||||||
|
lineComment: '#',
|
||||||
|
},
|
||||||
|
brackets: [
|
||||||
|
['{', '}'],
|
||||||
|
['[', ']'],
|
||||||
|
['(', ')'],
|
||||||
|
],
|
||||||
|
autoClosingPairs: [
|
||||||
|
{ open: '{', close: '}' },
|
||||||
|
{ open: '[', close: ']' },
|
||||||
|
{ open: '(', close: ')' },
|
||||||
|
{ open: '"', close: '"' },
|
||||||
|
{ open: "'", close: "'" },
|
||||||
|
],
|
||||||
|
surroundingPairs: [
|
||||||
|
{ open: '{', close: '}' },
|
||||||
|
{ open: '[', close: ']' },
|
||||||
|
{ open: '(', close: ')' },
|
||||||
|
{ open: '"', close: '"' },
|
||||||
|
{ open: "'", close: "'" },
|
||||||
|
],
|
||||||
|
folding: {
|
||||||
|
offSide: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lexerRules: monaco.languages.IMonarchLanguage = {
|
||||||
|
tokenPostfix: '.yaml',
|
||||||
|
brackets: [
|
||||||
|
{ token: 'delimiter.bracket', open: '{', close: '}' },
|
||||||
|
{ token: 'delimiter.square', open: '[', close: ']' },
|
||||||
|
],
|
||||||
|
keywords: ['true', 'True', 'TRUE', 'false', 'False', 'FALSE', 'null', 'Null', 'Null', '~'],
|
||||||
|
numberInteger: /(?:0|[+-]?[0-9]+)/,
|
||||||
|
numberFloat: /(?:0|[+-]?[0-9]+)(?:\.[0-9]+)?(?:e[-+][1-9][0-9]*)?/,
|
||||||
|
numberOctal: /0o[0-7]+/,
|
||||||
|
numberHex: /0x[0-9a-fA-F]+/,
|
||||||
|
numberInfinity: /[+-]?\.(?:inf|Inf|INF)/,
|
||||||
|
numberNaN: /\.(?:nan|Nan|NAN)/,
|
||||||
|
numberDate: /\d{4}-\d\d-\d\d([Tt ]\d\d:\d\d:\d\d(\.\d+)?(( ?[+-]\d\d?(:\d\d)?)|Z)?)?/,
|
||||||
|
escapes: /\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
{ include: '@whitespace' },
|
||||||
|
{ include: '@comment' },
|
||||||
|
// Directive
|
||||||
|
[/%[^ ]+.*$/, 'meta.directive'],
|
||||||
|
// Document Markers
|
||||||
|
[/---/, 'operators.directivesEnd'],
|
||||||
|
[/\.{3}/, 'operators.documentEnd'],
|
||||||
|
// Block Structure Indicators
|
||||||
|
[/[-?:](?= )/, 'operators'],
|
||||||
|
{ include: '@anchor' },
|
||||||
|
{ include: '@tagHandle' },
|
||||||
|
{ include: '@flowCollections' },
|
||||||
|
{ include: '@blockStyle' },
|
||||||
|
// Numbers
|
||||||
|
[/@numberInteger(?![ \t]*\S+)/, 'number'],
|
||||||
|
[/@numberFloat(?![ \t]*\S+)/, 'number.float'],
|
||||||
|
[/@numberOctal(?![ \t]*\S+)/, 'number.octal'],
|
||||||
|
[/@numberHex(?![ \t]*\S+)/, 'number.hex'],
|
||||||
|
[/@numberInfinity(?![ \t]*\S+)/, 'number.infinity'],
|
||||||
|
[/@numberNaN(?![ \t]*\S+)/, 'number.nan'],
|
||||||
|
[/@numberDate(?![ \t]*\S+)/, 'number.date'],
|
||||||
|
// Key:Value pair
|
||||||
|
[/(".*?"|'.*?'|.*?)([ \t]*)(:)( |$)/, ['type', 'white', 'operators', 'white']],
|
||||||
|
{ include: '@flowScalars' },
|
||||||
|
// String nodes
|
||||||
|
[
|
||||||
|
/.+$/,
|
||||||
|
{
|
||||||
|
cases: {
|
||||||
|
'@keywords': 'keyword',
|
||||||
|
'@default': 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
// Flow Collection: Flow Mapping
|
||||||
|
object: [
|
||||||
|
{ include: '@whitespace' },
|
||||||
|
{ include: '@comment' },
|
||||||
|
// Flow Mapping termination
|
||||||
|
[/\}/, '@brackets', '@pop'],
|
||||||
|
// Flow Mapping delimiter
|
||||||
|
[/,/, 'delimiter.comma'],
|
||||||
|
// Flow Mapping Key:Value delimiter
|
||||||
|
[/:(?= )/, 'operators'],
|
||||||
|
// Flow Mapping Key:Value key
|
||||||
|
[/(?:".*?"|'.*?'|[^,\{\[]+?)(?=: )/, 'type'],
|
||||||
|
// Start Flow Style
|
||||||
|
{ include: '@flowCollections' },
|
||||||
|
{ include: '@flowScalars' },
|
||||||
|
// Scalar Data types
|
||||||
|
{ include: '@tagHandle' },
|
||||||
|
{ include: '@anchor' },
|
||||||
|
{ include: '@flowNumber' },
|
||||||
|
// Other value (keyword or string)
|
||||||
|
[
|
||||||
|
/[^\},]+/,
|
||||||
|
{
|
||||||
|
cases: {
|
||||||
|
'@keywords': 'keyword',
|
||||||
|
'@default': 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
// Flow Collection: Flow Sequence
|
||||||
|
array: [
|
||||||
|
{ include: '@whitespace' },
|
||||||
|
{ include: '@comment' },
|
||||||
|
// Flow Sequence termination
|
||||||
|
[/\]/, '@brackets', '@pop'],
|
||||||
|
// Flow Sequence delimiter
|
||||||
|
[/,/, 'delimiter.comma'],
|
||||||
|
// Start Flow Style
|
||||||
|
{ include: '@flowCollections' },
|
||||||
|
{ include: '@flowScalars' },
|
||||||
|
// Scalar Data types
|
||||||
|
{ include: '@tagHandle' },
|
||||||
|
{ include: '@anchor' },
|
||||||
|
{ include: '@flowNumber' },
|
||||||
|
// Other value (keyword or string)
|
||||||
|
[
|
||||||
|
/[^\],]+/,
|
||||||
|
{
|
||||||
|
cases: {
|
||||||
|
'@keywords': 'keyword',
|
||||||
|
'@default': 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
// First line of a Block Style
|
||||||
|
multiString: [[/^( +).+$/, 'string', '@multiStringContinued.$1']],
|
||||||
|
// Further lines of a Block Style
|
||||||
|
// Workaround for indentation detection
|
||||||
|
multiStringContinued: [
|
||||||
|
[
|
||||||
|
/^( *).+$/,
|
||||||
|
{
|
||||||
|
cases: {
|
||||||
|
'$1==$S2': 'string',
|
||||||
|
'@default': { token: '@rematch', next: '@popall' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
whitespace: [[/[ \t\r\n]+/, 'white']],
|
||||||
|
// Only line comments
|
||||||
|
comment: [[/#.*$/, 'comment']],
|
||||||
|
// Start Flow Collections
|
||||||
|
flowCollections: [
|
||||||
|
[/\[/, '@brackets', '@array'],
|
||||||
|
[/\{/, '@brackets', '@object'],
|
||||||
|
],
|
||||||
|
// Start Flow Scalars (quoted strings)
|
||||||
|
flowScalars: [
|
||||||
|
[/"([^"\\]|\\.)*$/, 'string.invalid'],
|
||||||
|
[/'([^'\\]|\\.)*$/, 'string.invalid'],
|
||||||
|
[/'[^']*'/, 'string'],
|
||||||
|
[/"/, 'string', '@doubleQuotedString'],
|
||||||
|
],
|
||||||
|
doubleQuotedString: [
|
||||||
|
[/[^\\"]+/, 'string'],
|
||||||
|
[/@escapes/, 'string.escape'],
|
||||||
|
[/\\./, 'string.escape.invalid'],
|
||||||
|
[/"/, 'string', '@pop'],
|
||||||
|
],
|
||||||
|
// Start Block Scalar
|
||||||
|
blockStyle: [[/[>|][0-9]*[+-]?$/, 'operators', '@multiString']],
|
||||||
|
// Numbers in Flow Collections (terminate with ,]})
|
||||||
|
flowNumber: [
|
||||||
|
[/@numberInteger(?=[ \t]*[,\]\}])/, 'number'],
|
||||||
|
[/@numberFloat(?=[ \t]*[,\]\}])/, 'number.float'],
|
||||||
|
[/@numberOctal(?=[ \t]*[,\]\}])/, 'number.octal'],
|
||||||
|
[/@numberHex(?=[ \t]*[,\]\}])/, 'number.hex'],
|
||||||
|
[/@numberInfinity(?=[ \t]*[,\]\}])/, 'number.infinity'],
|
||||||
|
[/@numberNaN(?=[ \t]*[,\]\}])/, 'number.nan'],
|
||||||
|
[/@numberDate(?=[ \t]*[,\]\}])/, 'number.date'],
|
||||||
|
],
|
||||||
|
tagHandle: [[/\![^ ]*/, 'tag']],
|
||||||
|
anchor: [[/[&*][^ ]+/, 'namespace']],
|
||||||
|
},
|
||||||
|
} as monaco.languages.IMonarchLanguage;
|
6
packages/shared-ux/code_editor/impl/package.json
Normal file
6
packages/shared-ux/code_editor/impl/package.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "@kbn/code-editor",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||||
|
}
|
49
packages/shared-ux/code_editor/impl/placeholder_widget.ts
Normal file
49
packages/shared-ux/code_editor/impl/placeholder_widget.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* 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 { monaco } from '@kbn/monaco';
|
||||||
|
|
||||||
|
export class PlaceholderWidget implements monaco.editor.IContentWidget {
|
||||||
|
constructor(
|
||||||
|
private readonly placeholderText: string,
|
||||||
|
private readonly editor: monaco.editor.ICodeEditor
|
||||||
|
) {
|
||||||
|
editor.addContentWidget(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private domNode: undefined | HTMLElement;
|
||||||
|
|
||||||
|
public getId(): string {
|
||||||
|
return 'KBN_CODE_EDITOR_PLACEHOLDER_WIDGET_ID';
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDomNode(): HTMLElement {
|
||||||
|
if (!this.domNode) {
|
||||||
|
const domNode = document.createElement('div');
|
||||||
|
domNode.innerText = this.placeholderText;
|
||||||
|
domNode.className = 'kibanaCodeEditor__placeholderContainer';
|
||||||
|
this.editor.applyFontInfo(domNode);
|
||||||
|
this.domNode = domNode;
|
||||||
|
}
|
||||||
|
return this.domNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPosition(): monaco.editor.IContentWidgetPosition | null {
|
||||||
|
return {
|
||||||
|
position: {
|
||||||
|
column: 1,
|
||||||
|
lineNumber: 1,
|
||||||
|
},
|
||||||
|
preference: [monaco.editor.ContentWidgetPositionPreference.EXACT],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this.editor.removeContentWidget(this);
|
||||||
|
}
|
||||||
|
}
|
15
packages/shared-ux/code_editor/impl/register_languages.ts
Normal file
15
packages/shared-ux/code_editor/impl/register_languages.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* 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 { registerLanguage } from '@kbn/monaco';
|
||||||
|
import { CssLang, HandlebarsLang, MarkdownLang, YamlLang, HJson } from './languages';
|
||||||
|
|
||||||
|
registerLanguage(CssLang);
|
||||||
|
registerLanguage(HandlebarsLang);
|
||||||
|
registerLanguage(MarkdownLang);
|
||||||
|
registerLanguage(YamlLang);
|
||||||
|
registerLanguage(HJson);
|
28
packages/shared-ux/code_editor/impl/remeasure_fonts.ts
Normal file
28
packages/shared-ux/code_editor/impl/remeasure_fonts.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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 { monaco } from '@kbn/monaco';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When using custom fonts with monaco need to call `monaco.editor.remeasureFonts()` when custom fonts finished loading
|
||||||
|
* Otherwise initial measurements on fallback font are used which causes visual glitches in the editor
|
||||||
|
*/
|
||||||
|
export const remeasureFonts = () => {
|
||||||
|
if ('fonts' in window.document && 'ready' in window.document.fonts) {
|
||||||
|
window.document.fonts.ready
|
||||||
|
.then(() => {
|
||||||
|
monaco.editor.remeasureFonts();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('Failed to remeasureFonts in <CodeEditor/>');
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
29
packages/shared-ux/code_editor/impl/tsconfig.json
Normal file
29
packages/shared-ux/code_editor/impl/tsconfig.json
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"extends": "../../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "target/types",
|
||||||
|
"types": [
|
||||||
|
"jest",
|
||||||
|
"node",
|
||||||
|
"react",
|
||||||
|
"@emotion/react/types/css-prop",
|
||||||
|
"@kbn/ambient-ui-types",
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"target/**/*"
|
||||||
|
],
|
||||||
|
"kbn_references": [
|
||||||
|
"@kbn/monaco",
|
||||||
|
"@kbn/i18n",
|
||||||
|
"@kbn/i18n-react",
|
||||||
|
"@kbn/code-editor-mocks",
|
||||||
|
"@kbn/ui-theme",
|
||||||
|
"@kbn/test-jest-helpers",
|
||||||
|
]
|
||||||
|
}
|
3
packages/shared-ux/code_editor/mocks/README.md
Normal file
3
packages/shared-ux/code_editor/mocks/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# @kbn/code-editor-mocks
|
||||||
|
|
||||||
|
Empty package generated by @kbn/generate
|
9
packages/shared-ux/code_editor/mocks/index.ts
Normal file
9
packages/shared-ux/code_editor/mocks/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* 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 { CodeEditorStorybookMock } from './storybook';
|
||||||
|
export type { Params as CodeEditorStorybookParams } from './storybook';
|
13
packages/shared-ux/code_editor/mocks/jest.config.js
Normal file
13
packages/shared-ux/code_editor/mocks/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
preset: '@kbn/test/jest_node',
|
||||||
|
rootDir: '../../../..',
|
||||||
|
roots: ['<rootDir>/packages/shared-ux/code_editor/mocks'],
|
||||||
|
};
|
5
packages/shared-ux/code_editor/mocks/kibana.jsonc
Normal file
5
packages/shared-ux/code_editor/mocks/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"type": "shared-common",
|
||||||
|
"id": "@kbn/code-editor-mocks",
|
||||||
|
"owner": "@elastic/shared-ux"
|
||||||
|
}
|
6
packages/shared-ux/code_editor/mocks/package.json
Normal file
6
packages/shared-ux/code_editor/mocks/package.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "@kbn/code-editor-mocks",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||||
|
}
|
99
packages/shared-ux/code_editor/mocks/storybook.ts
Normal file
99
packages/shared-ux/code_editor/mocks/storybook.ts
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* 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 { AbstractStorybookMock } from '@kbn/shared-ux-storybook-mock';
|
||||||
|
|
||||||
|
import type { Props as CodeEditorProps } from '@kbn/code-editor-types';
|
||||||
|
|
||||||
|
type PropArguments = Pick<
|
||||||
|
CodeEditorProps,
|
||||||
|
| 'languageId'
|
||||||
|
| 'value'
|
||||||
|
| 'aria-label'
|
||||||
|
| 'allowFullScreen'
|
||||||
|
| 'useDarkTheme'
|
||||||
|
| 'transparentBackground'
|
||||||
|
| 'placeholder'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type Params = Record<keyof PropArguments, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storybook mock for the `CodeEditor` component
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class CodeEditorStorybookMock extends AbstractStorybookMock<
|
||||||
|
CodeEditorProps,
|
||||||
|
{},
|
||||||
|
PropArguments,
|
||||||
|
{}
|
||||||
|
> {
|
||||||
|
propArguments = {
|
||||||
|
languageId: {
|
||||||
|
control: {
|
||||||
|
type: 'radio',
|
||||||
|
},
|
||||||
|
options: ['json', 'loglang', 'plaintext'],
|
||||||
|
defaultValue: 'json',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
control: {
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
defaultValue: '',
|
||||||
|
},
|
||||||
|
'aria-label': {
|
||||||
|
control: {
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
defaultValue: 'code editor',
|
||||||
|
},
|
||||||
|
allowFullScreen: {
|
||||||
|
control: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
useDarkTheme: {
|
||||||
|
control: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
transparentBackground: {
|
||||||
|
control: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
control: {
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
defaultValue: 'myplaceholder',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceArguments = {};
|
||||||
|
dependencies = [];
|
||||||
|
|
||||||
|
getProps(params?: Params): CodeEditorProps {
|
||||||
|
return {
|
||||||
|
languageId: this.getArgumentValue('languageId', params),
|
||||||
|
value: this.getArgumentValue('value', params),
|
||||||
|
'aria-label': this.getArgumentValue('aria-label', params),
|
||||||
|
allowFullScreen: this.getArgumentValue('allowFullScreen', params),
|
||||||
|
useDarkTheme: this.getArgumentValue('useDarkTheme', params),
|
||||||
|
transparentBackground: this.getArgumentValue('transparentBackground', params),
|
||||||
|
placeholder: this.getArgumentValue('placeholder', params),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getServices() {
|
||||||
|
return { ...this.getProps() };
|
||||||
|
}
|
||||||
|
}
|
20
packages/shared-ux/code_editor/mocks/tsconfig.json
Normal file
20
packages/shared-ux/code_editor/mocks/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"extends": "../../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "target/types",
|
||||||
|
"types": [
|
||||||
|
"jest",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"target/**/*"
|
||||||
|
],
|
||||||
|
"kbn_references": [
|
||||||
|
"@kbn/shared-ux-storybook-mock",
|
||||||
|
"@kbn/code-editor-types",
|
||||||
|
]
|
||||||
|
}
|
3
packages/shared-ux/code_editor/types/README.md
Normal file
3
packages/shared-ux/code_editor/types/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# @kbn/code-editor-types
|
||||||
|
|
||||||
|
Empty package generated by @kbn/generate
|
101
packages/shared-ux/code_editor/types/index.d.ts
vendored
Normal file
101
packages/shared-ux/code_editor/types/index.d.ts
vendored
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* 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 { monaco } from '@kbn/monaco';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
/** Width of editor. Defaults to 100%. */
|
||||||
|
width?: string | number;
|
||||||
|
|
||||||
|
/** Height of editor. Defaults to 100%. */
|
||||||
|
height?: string | number;
|
||||||
|
|
||||||
|
/** ID of the editor language */
|
||||||
|
languageId: enum;
|
||||||
|
|
||||||
|
/** Value of the editor */
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/** Function invoked when text in editor is changed */
|
||||||
|
onChange?: (value: string, event: monaco.editor.IModelContentChangedEvent) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for the Monaco Code Editor
|
||||||
|
* Documentation of options can be found here:
|
||||||
|
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html
|
||||||
|
*/
|
||||||
|
options?: monaco.editor.IStandaloneEditorConstructionOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suggestion provider for autocompletion
|
||||||
|
* Documentation for the provider can be found here:
|
||||||
|
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.completionitemprovider.html
|
||||||
|
*/
|
||||||
|
suggestionProvider?: monaco.languages.CompletionItemProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signature provider for function parameter info
|
||||||
|
* Documentation for the provider can be found here:
|
||||||
|
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.signaturehelpprovider.html
|
||||||
|
*/
|
||||||
|
signatureProvider?: monaco.languages.SignatureHelpProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hover provider for hover documentation
|
||||||
|
* Documentation for the provider can be found here:
|
||||||
|
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.hoverprovider.html
|
||||||
|
*/
|
||||||
|
hoverProvider?: monaco.languages.HoverProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Language config provider for bracket
|
||||||
|
* Documentation for the provider can be found here:
|
||||||
|
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.languageconfiguration.html
|
||||||
|
*/
|
||||||
|
languageConfiguration?: monaco.languages.LanguageConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called before the editor is mounted in the view
|
||||||
|
*/
|
||||||
|
editorWillMount?: () => void;
|
||||||
|
/**
|
||||||
|
* Function called before the editor is mounted in the view
|
||||||
|
* and completely replaces the setup behavior called by the component
|
||||||
|
*/
|
||||||
|
overrideEditorWillMount?: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called after the editor is mounted in the view
|
||||||
|
*/
|
||||||
|
editorDidMount?: (editor: monaco.editor.IStandaloneCodeEditor) => void;
|
||||||
|
/**
|
||||||
|
* Should the editor use the dark theme
|
||||||
|
*/
|
||||||
|
useDarkTheme?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the editor use a transparent background
|
||||||
|
*/
|
||||||
|
transparentBackground?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should the editor be rendered using the fullWidth EUI attribute
|
||||||
|
*/
|
||||||
|
fullWidth?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place holder text for the code editor
|
||||||
|
*/
|
||||||
|
placeholder?: string;
|
||||||
|
/**
|
||||||
|
* Accessible name for the editor. (Defaults to "Code editor")
|
||||||
|
*/
|
||||||
|
'aria-label'?: string;
|
||||||
|
|
||||||
|
isCopyable?: boolean;
|
||||||
|
allowFullScreen?: boolean;
|
||||||
|
}
|
13
packages/shared-ux/code_editor/types/jest.config.js
Normal file
13
packages/shared-ux/code_editor/types/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
preset: '@kbn/test/jest_node',
|
||||||
|
rootDir: '../../../..',
|
||||||
|
roots: ['<rootDir>/packages/shared-ux/code_editor/types'],
|
||||||
|
};
|
5
packages/shared-ux/code_editor/types/kibana.jsonc
Normal file
5
packages/shared-ux/code_editor/types/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"type": "shared-common",
|
||||||
|
"id": "@kbn/code-editor-types",
|
||||||
|
"owner": "@elastic/shared-ux"
|
||||||
|
}
|
6
packages/shared-ux/code_editor/types/package.json
Normal file
6
packages/shared-ux/code_editor/types/package.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "@kbn/code-editor-types",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||||
|
}
|
19
packages/shared-ux/code_editor/types/tsconfig.json
Normal file
19
packages/shared-ux/code_editor/types/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"extends": "../../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "target/types",
|
||||||
|
"types": [
|
||||||
|
"jest",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.d.ts",
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"target/**/*"
|
||||||
|
],
|
||||||
|
"kbn_references": [
|
||||||
|
"@kbn/monaco",
|
||||||
|
]
|
||||||
|
}
|
|
@ -128,6 +128,12 @@
|
||||||
"@kbn/cloud-plugin/*": ["x-pack/plugins/cloud/*"],
|
"@kbn/cloud-plugin/*": ["x-pack/plugins/cloud/*"],
|
||||||
"@kbn/cloud-security-posture-plugin": ["x-pack/plugins/cloud_security_posture"],
|
"@kbn/cloud-security-posture-plugin": ["x-pack/plugins/cloud_security_posture"],
|
||||||
"@kbn/cloud-security-posture-plugin/*": ["x-pack/plugins/cloud_security_posture/*"],
|
"@kbn/cloud-security-posture-plugin/*": ["x-pack/plugins/cloud_security_posture/*"],
|
||||||
|
"@kbn/code-editor": ["packages/shared-ux/code_editor/impl"],
|
||||||
|
"@kbn/code-editor/*": ["packages/shared-ux/code_editor/impl/*"],
|
||||||
|
"@kbn/code-editor-mocks": ["packages/shared-ux/code_editor/mocks"],
|
||||||
|
"@kbn/code-editor-mocks/*": ["packages/shared-ux/code_editor/mocks/*"],
|
||||||
|
"@kbn/code-editor-types": ["packages/shared-ux/code_editor/types"],
|
||||||
|
"@kbn/code-editor-types/*": ["packages/shared-ux/code_editor/types/*"],
|
||||||
"@kbn/coloring": ["packages/kbn-coloring"],
|
"@kbn/coloring": ["packages/kbn-coloring"],
|
||||||
"@kbn/coloring/*": ["packages/kbn-coloring/*"],
|
"@kbn/coloring/*": ["packages/kbn-coloring/*"],
|
||||||
"@kbn/config": ["packages/kbn-config"],
|
"@kbn/config": ["packages/kbn-config"],
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -2857,6 +2857,18 @@
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
uid ""
|
uid ""
|
||||||
|
|
||||||
|
"@kbn/code-editor-mocks@link:packages/shared-ux/code_editor/mocks":
|
||||||
|
version "0.0.0"
|
||||||
|
uid ""
|
||||||
|
|
||||||
|
"@kbn/code-editor-types@link:packages/shared-ux/code_editor/types":
|
||||||
|
version "0.0.0"
|
||||||
|
uid ""
|
||||||
|
|
||||||
|
"@kbn/code-editor@link:packages/shared-ux/code_editor/impl":
|
||||||
|
version "0.0.0"
|
||||||
|
uid ""
|
||||||
|
|
||||||
"@kbn/coloring@link:packages/kbn-coloring":
|
"@kbn/coloring@link:packages/kbn-coloring":
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
uid ""
|
uid ""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue