monaco-yaml/examples/demo/src/index.ts
2021-11-10 22:28:06 +01:00

225 lines
6.1 KiB
TypeScript

import './index.css';
import { JSONSchemaForSchemaStoreOrgCatalogFiles } from '@schemastore/schema-catalog';
import { CancellationToken } from 'monaco-editor/esm/vs/base/common/cancellation';
import { getDocumentSymbols } from 'monaco-editor/esm/vs/editor/contrib/documentSymbols/documentSymbols';
import {
editor,
Environment,
languages,
Position,
Range,
Uri,
} from 'monaco-editor/esm/vs/editor/editor.api';
import { SchemasSettings, setDiagnosticsOptions } from 'monaco-yaml';
// NOTE: This will give you all editor featues. If you would prefer to limit to only the editor
// features you want to use, import them each individually. See this example: (https://github.com/microsoft/monaco-editor-samples/blob/main/browser-esm-webpack-small/index.js#L1-L91)
import 'monaco-editor';
import defaultSchemaUri from './schema.json';
declare global {
interface Window {
MonacoEnvironment: Environment;
}
}
window.MonacoEnvironment = {
getWorker(moduleId, label) {
switch (label) {
case 'editorWorkerService':
return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker', import.meta.url));
case 'yaml':
return new Worker(new URL('monaco-yaml/lib/esm/yaml.worker', import.meta.url));
default:
throw new Error(`Unknown label ${label}`);
}
},
};
const defaultSchema: SchemasSettings = {
uri: defaultSchemaUri,
fileMatch: ['monaco-yaml.yaml'],
};
setDiagnosticsOptions({
schemas: [defaultSchema],
});
const value = `
# Property descriptions are displayed when hovering over properties using your cursor
property: This property has a JSON schema description
# Titles work too!
titledProperty: Titles work too!
# Even markdown descriptions work
markdown: hover me to get a markdown based description 😮
# Enums can be autocompleted by placing the cursor after the colon and pressing Ctrl+Space
enum:
# Of course numbers are supported!
number: 12
# As well as booleans!
boolean: true
# And strings
string: I am a string
# This property is using the JSON schema recursively
reference:
boolean: Not a boolean
# Also works in arrays
array:
- string: 12
enum: Mewtwo
reference:
reference:
boolean: true
# JSON referenses can be clicked for navigation
pointer:
$ref: '#/array'
# This anchor can be referenced
anchorRef: &anchor can be clicked as well
# Press control while hovering over the anchor
anchorPointer: *anchor
formatting: Formatting is supported too! Under the hood this is powered by Prettier. Just press Ctrl+Shift+I or right click and press Format to format this document.
`.replace(/:$/m, ': ');
const ed = editor.create(document.getElementById('editor'), {
automaticLayout: true,
model: editor.createModel(value, 'yaml', Uri.parse('monaco-yaml.yaml')),
theme: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'vs-dark' : 'vs-light',
});
const select = document.getElementById('schema-selection') as HTMLSelectElement;
fetch('https://www.schemastore.org/api/json/catalog.json').then(async (response) => {
if (!response.ok) {
return;
}
const catalog = (await response.json()) as JSONSchemaForSchemaStoreOrgCatalogFiles;
const schemas = [defaultSchema];
catalog.schemas.sort((a, b) => a.name.localeCompare(b.name));
for (const { fileMatch, name, url } of catalog.schemas) {
const match =
typeof name === 'string' && fileMatch?.find((filename) => /\.ya?ml$/i.test(filename));
if (!match) {
continue;
}
const option = document.createElement('option');
option.value = match;
option.textContent = name;
select.append(option);
schemas.push({
fileMatch: [match],
uri: url,
});
}
setDiagnosticsOptions({
validate: true,
enableSchemaRequest: true,
format: true,
hover: true,
completion: true,
schemas,
});
});
select.addEventListener('change', () => {
const oldModel = ed.getModel();
const newModel = editor.createModel(oldModel.getValue(), 'yaml', Uri.parse(select.value));
ed.setModel(newModel);
oldModel.dispose();
});
function* iterateSymbols(
symbols: languages.DocumentSymbol[],
position: Position,
): Iterable<languages.DocumentSymbol> {
for (const symbol of symbols) {
if (Range.containsPosition(symbol.range, position)) {
yield symbol;
yield* iterateSymbols(symbol.children, position);
}
}
}
ed.onDidChangeCursorPosition(async (event) => {
const breadcrumbs = document.getElementById('breadcrumbs');
const symbols = await getDocumentSymbols(ed.getModel(), false, CancellationToken.None);
while (breadcrumbs.lastChild) {
breadcrumbs.lastChild.remove();
}
for (const symbol of iterateSymbols(symbols, event.position)) {
const breadcrumb = document.createElement('span');
breadcrumb.setAttribute('role', 'button');
breadcrumb.classList.add('breadcrumb');
breadcrumb.textContent = symbol.name;
if (symbol.kind === languages.SymbolKind.Array) {
breadcrumb.classList.add('array');
} else if (symbol.kind === languages.SymbolKind.Module) {
breadcrumb.classList.add('object');
}
breadcrumb.addEventListener('click', () => {
ed.setPosition({
lineNumber: symbol.range.startLineNumber,
column: symbol.range.startColumn,
});
ed.focus();
});
breadcrumbs.append(breadcrumb);
}
});
editor.onDidChangeMarkers(([resource]) => {
const problems = document.getElementById('problems');
const markers = editor.getModelMarkers({ resource });
while (problems.lastChild) {
problems.lastChild.remove();
}
for (const marker of markers) {
const wrapper = document.createElement('div');
wrapper.setAttribute('role', 'button');
const codicon = document.createElement('div');
const text = document.createElement('div');
wrapper.classList.add('problem');
codicon.classList.add('codicon', 'codicon-warning');
text.classList.add('problem-text');
text.textContent = marker.message;
wrapper.append(codicon, text);
wrapper.addEventListener('click', () => {
ed.setPosition({ lineNumber: marker.startLineNumber, column: marker.startColumn });
ed.focus();
});
problems.append(wrapper);
}
});