monacoComponent.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import * as React from "react";
  2. import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
  3. require("../scss/monaco.scss");
  4. interface IMonacoComponentProps {
  5. language: "JS" | "TS";
  6. }
  7. export class MonacoComponent extends React.Component<IMonacoComponentProps> {
  8. private _hostReference: React.RefObject<HTMLDivElement>;
  9. private _editor: monaco.editor.IStandaloneCodeEditor;
  10. private _definitionWorker: Worker;
  11. private _deprecatedCandidates: string[];
  12. // private _templates: string[];
  13. public constructor(props: IMonacoComponentProps) {
  14. super(props);
  15. this._hostReference = React.createRef();
  16. }
  17. async setupMonaco() {
  18. let hostElement = this._hostReference.current!;
  19. var editorOptions: monaco.editor.IEditorConstructionOptions = {
  20. value: "",
  21. language: this.props.language == "JS" ? "javascript" : "typescript",
  22. lineNumbers: "on",
  23. roundedSelection: true,
  24. automaticLayout: true,
  25. scrollBeyondLastLine: false,
  26. readOnly: false,
  27. theme: "vs",
  28. contextmenu: false,
  29. folding: true,
  30. showFoldingControls: "always",
  31. renderIndentGuides: true,
  32. minimap: {
  33. enabled: true
  34. }
  35. };
  36. this._editor = monaco.editor.create(
  37. hostElement,
  38. editorOptions
  39. );
  40. let response = await fetch("https://preview.babylonjs.com/babylon.d.ts");
  41. if (!response.ok) {
  42. return;
  43. }
  44. let libContent = await response.text();
  45. response = await fetch("https://preview.babylonjs.com/gui/babylon.gui.d.ts");
  46. if (!response.ok) {
  47. return;
  48. }
  49. libContent += await response.text();
  50. this.setupDefinitionWorker(libContent);
  51. // Load code templates
  52. response = await fetch("/templates.json");
  53. if (response.ok) {
  54. // this._templates = await response.json();
  55. }
  56. // Setup the Monaco compilation pipeline, so we can reuse it directly for our scrpting needs
  57. this.setupMonacoCompilationPipeline(libContent);
  58. // This is used for a vscode-like color preview for ColorX types
  59. this.setupMonacoColorProvider();
  60. }
  61. // Provide an adornment for BABYLON.ColorX types: color preview
  62. setupMonacoColorProvider() {
  63. monaco.languages.registerColorProvider(this.props.language == "JS" ? "javascript" : "typescript", {
  64. provideColorPresentations: (model, colorInfo) => {
  65. const color = colorInfo.color;
  66. const precision = 100.0;
  67. const converter = (n: number) => Math.round(n * precision) / precision;
  68. let label;
  69. if (color.alpha === undefined || color.alpha === 1.0) {
  70. label = `(${converter(color.red)}, ${converter(color.green)}, ${converter(color.blue)})`;
  71. } else {
  72. label = `(${converter(color.red)}, ${converter(color.green)}, ${converter(color.blue)}, ${converter(color.alpha)})`;
  73. }
  74. return [{
  75. label: label
  76. }];
  77. },
  78. provideDocumentColors: (model) => {
  79. const digitGroup = "\\s*(\\d*(?:\\.\\d+)?)\\s*";
  80. // we add \n{0} to workaround a Monaco bug, when setting regex options on their side
  81. const regex = `BABYLON\\.Color(?:3|4)\\s*\\(${digitGroup},${digitGroup},${digitGroup}(?:,${digitGroup})?\\)\\n{0}`;
  82. const matches = model.findMatches(regex, false, true, true, null, true);
  83. const converter = (g: string) => g === undefined ? undefined : Number(g);
  84. return matches.map(match => ({
  85. color: {
  86. red: converter(match.matches![1])!,
  87. green: converter(match.matches![2])!,
  88. blue: converter(match.matches![3])!,
  89. alpha: converter(match.matches![4])!
  90. },
  91. range: {
  92. startLineNumber: match.range.startLineNumber,
  93. startColumn: match.range.startColumn + match.matches![0].indexOf("("),
  94. endLineNumber: match.range.startLineNumber,
  95. endColumn: match.range.endColumn
  96. }
  97. }));
  98. }
  99. });
  100. }
  101. // Setup both JS and TS compilation pipelines to work with our scripts.
  102. setupMonacoCompilationPipeline(libContent: string) {
  103. const typescript = monaco.languages.typescript;
  104. if (this.props.language == "JS") {
  105. typescript.javascriptDefaults.setCompilerOptions({
  106. noLib: false,
  107. allowNonTsExtensions: true // required to prevent Uncaught Error: Could not find file: 'inmemory://model/1'.
  108. });
  109. typescript.javascriptDefaults.addExtraLib(libContent, 'babylon.d.ts');
  110. } else {
  111. typescript.typescriptDefaults.setCompilerOptions({
  112. module: typescript.ModuleKind.AMD,
  113. target: typescript.ScriptTarget.ESNext,
  114. noLib: false,
  115. strict: false,
  116. alwaysStrict: false,
  117. strictFunctionTypes: false,
  118. suppressExcessPropertyErrors: false,
  119. suppressImplicitAnyIndexErrors: true,
  120. noResolve: true,
  121. suppressOutputPathCheck: true,
  122. allowNonTsExtensions: true // required to prevent Uncaught Error: Could not find file: 'inmemory://model/1'.
  123. });
  124. typescript.typescriptDefaults.addExtraLib(libContent, 'babylon.d.ts');
  125. }
  126. }
  127. setupDefinitionWorker(libContent: string) {
  128. this._definitionWorker = new Worker('workers/definitionWorker.js');
  129. this._definitionWorker.addEventListener('message', ({
  130. data
  131. }) => {
  132. this._deprecatedCandidates = data.result;
  133. this.analyzeCode();
  134. });
  135. this._definitionWorker.postMessage({
  136. code: libContent
  137. });
  138. }
  139. // This will make sure that all members marked with a deprecated jsdoc attribute will be marked as such in Monaco UI
  140. // We use a prefiltered list of deprecated candidates, because the effective call to getCompletionEntryDetails is slow.
  141. // @see setupDefinitionWorker
  142. async analyzeCode() {
  143. // if the definition worker is very fast, this can be called out of context. @see setupDefinitionWorker
  144. if (!this._editor)
  145. return;
  146. const model = this._editor.getModel();
  147. if (!model)
  148. return;
  149. const uri = model.uri;
  150. let worker = null;
  151. if (this.props.language == "JS")
  152. worker = await monaco.languages.typescript.getJavaScriptWorker();
  153. else
  154. worker = await monaco.languages.typescript.getTypeScriptWorker();
  155. const languageService = await worker(uri);
  156. const source = '[deprecated members]';
  157. monaco.editor.setModelMarkers(model, source, []);
  158. const markers: {
  159. startLineNumber: number,
  160. endLineNumber: number,
  161. startColumn: number,
  162. endColumn: number,
  163. message: string,
  164. severity: monaco.MarkerSeverity.Warning,
  165. source: string,
  166. }[] = [];
  167. for (const candidate of this._deprecatedCandidates) {
  168. const matches = model.findMatches(candidate, false, false, true, null, false);
  169. for (const match of matches) {
  170. const position = {
  171. lineNumber: match.range.startLineNumber,
  172. column: match.range.startColumn
  173. };
  174. const wordInfo = model.getWordAtPosition(position);
  175. const offset = model.getOffsetAt(position);
  176. if (!wordInfo) {
  177. continue;
  178. }
  179. // continue if we already found an issue here
  180. if (markers.find(m => m.startLineNumber == position.lineNumber && m.startColumn == position.column))
  181. continue;
  182. // the following is time consuming on all suggestions, that's why we precompute deprecated candidate names in the definition worker to filter calls
  183. // @see setupDefinitionWorker
  184. const details = await languageService.getCompletionEntryDetails(uri.toString(), offset, wordInfo.word);
  185. if (this.isDeprecatedEntry(details)) {
  186. const deprecatedInfo = details.tags.find(this.isDeprecatedTag);
  187. markers.push({
  188. startLineNumber: match.range.startLineNumber,
  189. endLineNumber: match.range.endLineNumber,
  190. startColumn: wordInfo.startColumn,
  191. endColumn: wordInfo.endColumn,
  192. message: deprecatedInfo.text,
  193. severity: monaco.MarkerSeverity.Warning,
  194. source: source,
  195. });
  196. }
  197. }
  198. }
  199. monaco.editor.setModelMarkers(model, source, markers);
  200. }
  201. isDeprecatedEntry(details: any) {
  202. return details &&
  203. details.tags &&
  204. details.tags.find(this.isDeprecatedTag);
  205. }
  206. isDeprecatedTag(tag: any) {
  207. return tag &&
  208. tag.name == "deprecated";
  209. }
  210. componentDidMount() {
  211. this.setupMonaco();
  212. }
  213. public render() {
  214. return (
  215. <div id="monacoHost" ref={this._hostReference}>
  216. </div>
  217. )
  218. }
  219. }