import * as React from "react"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; require("../scss/monaco.scss"); interface IMonacoComponentProps { language: "JS" | "TS"; } export class MonacoComponent extends React.Component { private _hostReference: React.RefObject; private _editor: monaco.editor.IStandaloneCodeEditor; private _definitionWorker: Worker; private _deprecatedCandidates: string[]; // private _templates: string[]; public constructor(props: IMonacoComponentProps) { super(props); this._hostReference = React.createRef(); } async setupMonaco() { let hostElement = this._hostReference.current!; var editorOptions: monaco.editor.IEditorConstructionOptions = { value: "", language: this.props.language == "JS" ? "javascript" : "typescript", lineNumbers: "on", roundedSelection: true, automaticLayout: true, scrollBeyondLastLine: false, readOnly: false, theme: "vs", contextmenu: false, folding: true, showFoldingControls: "always", renderIndentGuides: true, minimap: { enabled: true } }; this._editor = monaco.editor.create( hostElement, editorOptions ); let response = await fetch("https://preview.babylonjs.com/babylon.d.ts"); if (!response.ok) { return; } let libContent = await response.text(); response = await fetch("https://preview.babylonjs.com/gui/babylon.gui.d.ts"); if (!response.ok) { return; } libContent += await response.text(); this.setupDefinitionWorker(libContent); // Load code templates response = await fetch("/templates.json"); if (response.ok) { // this._templates = await response.json(); } // Setup the Monaco compilation pipeline, so we can reuse it directly for our scrpting needs this.setupMonacoCompilationPipeline(libContent); // This is used for a vscode-like color preview for ColorX types this.setupMonacoColorProvider(); } // Provide an adornment for BABYLON.ColorX types: color preview setupMonacoColorProvider() { monaco.languages.registerColorProvider(this.props.language == "JS" ? "javascript" : "typescript", { provideColorPresentations: (model, colorInfo) => { const color = colorInfo.color; const precision = 100.0; const converter = (n: number) => Math.round(n * precision) / precision; let label; if (color.alpha === undefined || color.alpha === 1.0) { label = `(${converter(color.red)}, ${converter(color.green)}, ${converter(color.blue)})`; } else { label = `(${converter(color.red)}, ${converter(color.green)}, ${converter(color.blue)}, ${converter(color.alpha)})`; } return [{ label: label }]; }, provideDocumentColors: (model) => { const digitGroup = "\\s*(\\d*(?:\\.\\d+)?)\\s*"; // we add \n{0} to workaround a Monaco bug, when setting regex options on their side const regex = `BABYLON\\.Color(?:3|4)\\s*\\(${digitGroup},${digitGroup},${digitGroup}(?:,${digitGroup})?\\)\\n{0}`; const matches = model.findMatches(regex, false, true, true, null, true); const converter = (g: string) => g === undefined ? undefined : Number(g); return matches.map(match => ({ color: { red: converter(match.matches![1])!, green: converter(match.matches![2])!, blue: converter(match.matches![3])!, alpha: converter(match.matches![4])! }, range: { startLineNumber: match.range.startLineNumber, startColumn: match.range.startColumn + match.matches![0].indexOf("("), endLineNumber: match.range.startLineNumber, endColumn: match.range.endColumn } })); } }); } // Setup both JS and TS compilation pipelines to work with our scripts. setupMonacoCompilationPipeline(libContent: string) { const typescript = monaco.languages.typescript; if (this.props.language == "JS") { typescript.javascriptDefaults.setCompilerOptions({ noLib: false, allowNonTsExtensions: true // required to prevent Uncaught Error: Could not find file: 'inmemory://model/1'. }); typescript.javascriptDefaults.addExtraLib(libContent, 'babylon.d.ts'); } else { typescript.typescriptDefaults.setCompilerOptions({ module: typescript.ModuleKind.AMD, target: typescript.ScriptTarget.ESNext, noLib: false, strict: false, alwaysStrict: false, strictFunctionTypes: false, suppressExcessPropertyErrors: false, suppressImplicitAnyIndexErrors: true, noResolve: true, suppressOutputPathCheck: true, allowNonTsExtensions: true // required to prevent Uncaught Error: Could not find file: 'inmemory://model/1'. }); typescript.typescriptDefaults.addExtraLib(libContent, 'babylon.d.ts'); } } setupDefinitionWorker(libContent: string) { this._definitionWorker = new Worker('workers/definitionWorker.js'); this._definitionWorker.addEventListener('message', ({ data }) => { this._deprecatedCandidates = data.result; this.analyzeCode(); }); this._definitionWorker.postMessage({ code: libContent }); } // This will make sure that all members marked with a deprecated jsdoc attribute will be marked as such in Monaco UI // We use a prefiltered list of deprecated candidates, because the effective call to getCompletionEntryDetails is slow. // @see setupDefinitionWorker async analyzeCode() { // if the definition worker is very fast, this can be called out of context. @see setupDefinitionWorker if (!this._editor) return; const model = this._editor.getModel(); if (!model) return; const uri = model.uri; let worker = null; if (this.props.language == "JS") worker = await monaco.languages.typescript.getJavaScriptWorker(); else worker = await monaco.languages.typescript.getTypeScriptWorker(); const languageService = await worker(uri); const source = '[deprecated members]'; monaco.editor.setModelMarkers(model, source, []); const markers: { startLineNumber: number, endLineNumber: number, startColumn: number, endColumn: number, message: string, severity: monaco.MarkerSeverity.Warning, source: string, }[] = []; for (const candidate of this._deprecatedCandidates) { const matches = model.findMatches(candidate, false, false, true, null, false); for (const match of matches) { const position = { lineNumber: match.range.startLineNumber, column: match.range.startColumn }; const wordInfo = model.getWordAtPosition(position); const offset = model.getOffsetAt(position); if (!wordInfo) { continue; } // continue if we already found an issue here if (markers.find(m => m.startLineNumber == position.lineNumber && m.startColumn == position.column)) continue; // the following is time consuming on all suggestions, that's why we precompute deprecated candidate names in the definition worker to filter calls // @see setupDefinitionWorker const details = await languageService.getCompletionEntryDetails(uri.toString(), offset, wordInfo.word); if (this.isDeprecatedEntry(details)) { const deprecatedInfo = details.tags.find(this.isDeprecatedTag); markers.push({ startLineNumber: match.range.startLineNumber, endLineNumber: match.range.endLineNumber, startColumn: wordInfo.startColumn, endColumn: wordInfo.endColumn, message: deprecatedInfo.text, severity: monaco.MarkerSeverity.Warning, source: source, }); } } } monaco.editor.setModelMarkers(model, source, markers); } isDeprecatedEntry(details: any) { return details && details.tags && details.tags.find(this.isDeprecatedTag); } isDeprecatedTag(tag: any) { return tag && tag.name == "deprecated"; } componentDidMount() { this.setupMonaco(); } public render() { return (
) } }