[Monaco] Add JSON syntax support to the Monaco editor (#143739)

* Add JSON syntax support to the Monaco editor
* Bump `monaco-editor` version
* Fix the `monaco` package and usages to initialize lazily
* Add a story demonstrating JSON schema usage
This commit is contained in:
Michael Dokolin 2022-11-01 08:54:33 +01:00 committed by GitHub
parent f5acf76351
commit 9456303c97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 151 additions and 88 deletions

View file

@ -558,7 +558,7 @@
"moment": "^2.29.4",
"moment-duration-format": "^2.3.2",
"moment-timezone": "^0.5.34",
"monaco-editor": "^0.22.3",
"monaco-editor": "^0.24.0",
"mustache": "^2.3.2",
"node-fetch": "^2.6.7",
"node-forge": "^1.3.1",

View file

@ -12,10 +12,12 @@ function registerLanguage(language: LangModuleType) {
const { ID, lexerRules, languageConfiguration } = language;
monaco.languages.register({ id: ID });
monaco.languages.setMonarchTokensProvider(ID, lexerRules);
if (languageConfiguration) {
monaco.languages.setLanguageConfiguration(ID, languageConfiguration);
}
monaco.languages.onLanguage(ID, () => {
monaco.languages.setMonarchTokensProvider(ID, lexerRules);
if (languageConfiguration) {
monaco.languages.setLanguageConfiguration(ID, languageConfiguration);
}
});
}
export { registerLanguage };

View file

@ -23,6 +23,7 @@ import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js'; // Needed for hover
import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js'; // Needed for signature
import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching.js'; // Needed for brackets matching highlight
import 'monaco-editor/esm/vs/language/json/monaco.contribution.js';
import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js'; // Needed for basic javascript support
import 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution.js'; // Needed for basic xml support

View file

@ -12,11 +12,9 @@ import { EsqlLang } from './esql';
import { monaco } from './monaco_imports';
import { registerLanguage } from './helpers';
// @ts-ignore
import jsonWorkerSrc from '!!raw-loader!../../target_workers/json.editor.worker.js';
import xJsonWorkerSrc from '!!raw-loader!../../target_workers/xjson.editor.worker.js';
// @ts-ignore
import defaultWorkerSrc from '!!raw-loader!../../target_workers/default.editor.worker.js';
// @ts-ignore
import painlessWorkerSrc from '!!raw-loader!../../target_workers/painless.editor.worker.js';
/**
@ -32,6 +30,7 @@ registerLanguage(EsqlLang);
const mapLanguageIdToWorker: { [key: string]: any } = {
[XJsonLang.ID]: xJsonWorkerSrc,
[PainlessLang.ID]: painlessWorkerSrc,
[monaco.languages.json.jsonDefaults.languageId]: jsonWorkerSrc,
};
// @ts-ignore

14
packages/kbn-monaco/src/worker.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
/*
* 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.
*/
declare module '!!raw-loader!*.editor.worker.js' {
const contents: string;
// eslint-disable-next-line import/no-default-export
export default contents;
}

View file

@ -12,16 +12,12 @@ import { monaco } from '../monaco_imports';
import { WorkerProxyService } from './worker_proxy_service';
import { ID } from './constants';
const wps = new WorkerProxyService();
monaco.languages.onLanguage(ID, async () => {
return wps.setup();
});
const OWNER = 'XJSON_GRAMMAR_CHECKER';
export const registerGrammarChecker = () => {
const allDisposables: monaco.IDisposable[] = [];
monaco.languages.onLanguage(ID, async () => {
const wps = new WorkerProxyService();
wps.setup();
const updateAnnotations = async (model: monaco.editor.IModel): Promise<void> => {
if (model.isDisposed()) {
@ -50,21 +46,20 @@ export const registerGrammarChecker = () => {
};
const onModelAdd = (model: monaco.editor.IModel) => {
if (model.getModeId() === ID) {
allDisposables.push(
model.onDidChangeContent(async () => {
updateAnnotations(model);
})
);
updateAnnotations(model);
if (model.getModeId() !== ID) {
return;
}
};
allDisposables.push(monaco.editor.onDidCreateModel(onModelAdd));
return () => {
wps.stop();
allDisposables.forEach((d) => d.dispose());
};
};
registerGrammarChecker();
const { dispose } = model.onDidChangeContent(async () => {
updateAnnotations(model);
});
model.onWillDispose(() => {
dispose();
});
updateAnnotations(model);
};
monaco.editor.onDidCreateModel(onModelAdd);
});

View file

@ -8,43 +8,43 @@
const path = require('path');
const createLangWorkerConfig = (lang) => {
const entry =
lang === 'default'
? 'monaco-editor/esm/vs/editor/editor.worker.js'
: path.resolve(__dirname, 'src', lang, 'worker', `${lang}.worker.ts`);
return {
mode: 'production',
entry,
output: {
path: path.resolve(__dirname, 'target_workers'),
filename: `${lang}.editor.worker.js`,
},
resolve: {
extensions: ['.js', '.ts', '.tsx'],
},
stats: 'errors-only',
module: {
rules: [
{
test: /\.(js|ts)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
babelrc: false,
presets: [require.resolve('@kbn/babel-preset/webpack_preset')],
},
},
},
],
},
};
const getWorkerEntry = (language) => {
switch (language) {
case 'default':
return 'monaco-editor/esm/vs/editor/editor.worker.js';
case 'json':
return 'monaco-editor/esm/vs/language/json/json.worker.js';
default:
return path.resolve(__dirname, 'src', language, 'worker', `${language}.worker.ts`);
}
};
module.exports = [
createLangWorkerConfig('xjson'),
createLangWorkerConfig('painless'),
createLangWorkerConfig('default'),
];
const getWorkerConfig = (language) => ({
mode: 'production',
entry: getWorkerEntry(language),
output: {
path: path.resolve(__dirname, 'target_workers'),
filename: `${language}.editor.worker.js`,
},
resolve: {
extensions: ['.js', '.ts', '.tsx'],
},
stats: 'errors-only',
module: {
rules: [
{
test: /\.(js|ts)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
babelrc: false,
presets: [require.resolve('@kbn/babel-preset/webpack_preset')],
},
},
},
],
},
});
module.exports = ['default', 'json', 'painless', 'xjson'].map(getWorkerConfig);

View file

@ -238,4 +238,39 @@ storiesOf('CodeEditor', module)
text: 'Hover dialog example can be triggered by hovering over a word',
},
}
)
.add(
'json support',
() => (
<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>
),
{
info: { text: 'JSON language support' },
}
);

View file

@ -34,23 +34,36 @@ const simpleLogLang: monaco.languages.IMonarchLanguage = {
},
};
monaco.languages.register({ id: 'loglang' });
monaco.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
`;
class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
describe('<CodeEditor />', () => {
window.ResizeObserver = ResizeObserver;
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(

View file

@ -115,6 +115,8 @@ const expressionsLanguage: ExpressionsLanguage = {
export function registerExpressionsLanguage(functions: ExpressionFunction[]) {
expressionsLanguage.keywords = functions.map((fn) => fn.name);
expressionsLanguage.deprecated = functions.filter((fn) => fn.deprecated).map((fn) => fn.name);
monaco.languages.register({ id: EXPRESSIONS_LANGUAGE_ID });
monaco.languages.setMonarchTokensProvider(EXPRESSIONS_LANGUAGE_ID, expressionsLanguage);
monaco.languages.onLanguage(EXPRESSIONS_LANGUAGE_ID, () => {
monaco.languages.register({ id: EXPRESSIONS_LANGUAGE_ID });
monaco.languages.setMonarchTokensProvider(EXPRESSIONS_LANGUAGE_ID, expressionsLanguage);
});
}

View file

@ -62,5 +62,7 @@ export const lexerRules = {
},
} as monaco.languages.IMonarchLanguage;
monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, lexerRules);
monaco.languages.setLanguageConfiguration(LANGUAGE_ID, languageConfiguration);
monaco.languages.onLanguage(LANGUAGE_ID, () => {
monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, lexerRules);
monaco.languages.setLanguageConfiguration(LANGUAGE_ID, languageConfiguration);
});

View file

@ -19366,10 +19366,10 @@ moment-timezone@*, moment-timezone@^0.5.34:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
monaco-editor@*, monaco-editor@^0.22.3:
version "0.22.3"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.22.3.tgz#69b42451d3116c6c08d9b8e052007ff891fd85d7"
integrity sha512-RM559z2CJbczZ3k2b+ouacMINkAYWwRit4/vs0g2X/lkYefDiu0k2GmgWjAuiIpQi+AqASPOKvXNmYc8KUSvVQ==
monaco-editor@*, monaco-editor@^0.24.0:
version "0.24.0"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.24.0.tgz#990b55096bcc95d08d8d28e55264c6eb17707269"
integrity sha512-o1f0Lz6ABFNTtnEqqqvlY9qzNx24rQZx1RgYNQ8SkWkE+Ka63keHH/RqxQ4QhN4fs/UYOnvAtEUZsPrzccH++A==
monitor-event-loop-delay@^1.0.0:
version "1.0.0"