mirror of
				https://github.com/livebook-dev/livebook.git
				synced 2025-11-01 00:06:04 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			643 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			643 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import renderMathInElement from "katex/contrib/auto-render";
 | |
| 
 | |
| import monaco from "./live_editor/monaco";
 | |
| import EditorClient from "./live_editor/editor_client";
 | |
| import MonacoEditorAdapter from "./live_editor/monaco_editor_adapter";
 | |
| import HookServerAdapter from "./live_editor/hook_server_adapter";
 | |
| import RemoteUser from "./live_editor/remote_user";
 | |
| import { replacedSuffixLength } from "../../lib/text_utils";
 | |
| import { settingsStore } from "../../lib/settings";
 | |
| 
 | |
| /**
 | |
|  * Mounts cell source editor with real-time collaboration mechanism.
 | |
|  */
 | |
| class LiveEditor {
 | |
|   constructor(
 | |
|     hook,
 | |
|     container,
 | |
|     cellId,
 | |
|     tag,
 | |
|     source,
 | |
|     revision,
 | |
|     language,
 | |
|     intellisense,
 | |
|     readOnly
 | |
|   ) {
 | |
|     this.hook = hook;
 | |
|     this.container = container;
 | |
|     this.cellId = cellId;
 | |
|     this.source = source;
 | |
|     this.language = language;
 | |
|     this.intellisense = intellisense;
 | |
|     this.readOnly = readOnly;
 | |
|     this._onMount = [];
 | |
|     this._onChange = [];
 | |
|     this._onBlur = [];
 | |
|     this._onCursorSelectionChange = [];
 | |
|     this._remoteUserByClientId = {};
 | |
| 
 | |
|     const serverAdapter = new HookServerAdapter(hook, cellId, tag);
 | |
|     this.editorClient = new EditorClient(serverAdapter, revision);
 | |
| 
 | |
|     this.editorClient.onDelta((delta) => {
 | |
|       this.source = delta.applyToString(this.source);
 | |
|       this._onChange.forEach((callback) => callback(this.source));
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Checks if an editor instance has been mounted in the DOM.
 | |
|    */
 | |
|   isMounted() {
 | |
|     return !!this.editor;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Mounts and configures an editor instance in the DOM.
 | |
|    */
 | |
|   mount() {
 | |
|     if (this.isMounted()) {
 | |
|       throw new Error("The editor is already mounted");
 | |
|     }
 | |
| 
 | |
|     this._mountEditor();
 | |
| 
 | |
|     if (this.intellisense) {
 | |
|       this._setupIntellisense();
 | |
|     }
 | |
| 
 | |
|     this.editorClient.setEditorAdapter(new MonacoEditorAdapter(this.editor));
 | |
| 
 | |
|     this.editor.onDidFocusEditorWidget(() => {
 | |
|       this.editor.updateOptions({ matchBrackets: "always" });
 | |
|     });
 | |
| 
 | |
|     this.editor.onDidBlurEditorWidget(() => {
 | |
|       this.editor.updateOptions({ matchBrackets: "never" });
 | |
|       this._onBlur.forEach((callback) => callback());
 | |
|     });
 | |
| 
 | |
|     this.editor.onDidChangeCursorSelection((event) => {
 | |
|       this._onCursorSelectionChange.forEach((callback) =>
 | |
|         callback(event.selection)
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     this._onMount.forEach((callback) => callback());
 | |
|   }
 | |
| 
 | |
|   _ensureMounted() {
 | |
|     if (!this.isMounted()) {
 | |
|       this.mount();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Returns current editor content.
 | |
|    */
 | |
|   getSource() {
 | |
|     return this.source;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Registers a callback called with the editor is mounted in DOM.
 | |
|    */
 | |
|   onMount(callback) {
 | |
|     this._onMount.push(callback);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Registers a callback called with a new cell content whenever it changes.
 | |
|    */
 | |
|   onChange(callback) {
 | |
|     this._onChange.push(callback);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Registers a callback called with a new cursor selection whenever it changes.
 | |
|    */
 | |
|   onCursorSelectionChange(callback) {
 | |
|     this._onCursorSelectionChange.push(callback);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Registers a callback called whenever the editor loses focus.
 | |
|    */
 | |
|   onBlur(callback) {
 | |
|     this._onBlur.push(callback);
 | |
|   }
 | |
| 
 | |
|   focus() {
 | |
|     this._ensureMounted();
 | |
| 
 | |
|     this.editor.focus();
 | |
|   }
 | |
| 
 | |
|   blur() {
 | |
|     this._ensureMounted();
 | |
| 
 | |
|     if (this.editor.hasTextFocus()) {
 | |
|       document.activeElement.blur();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   insert(text) {
 | |
|     this._ensureMounted();
 | |
| 
 | |
|     const range = this.editor.getSelection();
 | |
|     this.editor
 | |
|       .getModel()
 | |
|       .pushEditOperations([], [{ forceMoveMarkers: true, range, text }]);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Performs necessary cleanup actions.
 | |
|    */
 | |
|   dispose() {
 | |
|     if (this.isMounted()) {
 | |
|       // Explicitly destroy the editor instance and its text model.
 | |
|       this.editor.dispose();
 | |
| 
 | |
|       const model = this.editor.getModel();
 | |
| 
 | |
|       if (model) {
 | |
|         model.dispose();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Either adds or moves remote user cursor to the new position.
 | |
|    */
 | |
|   updateUserSelection(client, selection) {
 | |
|     this._ensureMounted();
 | |
| 
 | |
|     if (this._remoteUserByClientId[client.id]) {
 | |
|       this._remoteUserByClientId[client.id].update(selection);
 | |
|     } else {
 | |
|       this._remoteUserByClientId[client.id] = new RemoteUser(
 | |
|         this.editor,
 | |
|         selection,
 | |
|         client.hex_color,
 | |
|         client.name
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Removes remote user cursor.
 | |
|    */
 | |
|   removeUserSelection(client) {
 | |
|     this._ensureMounted();
 | |
| 
 | |
|     if (this._remoteUserByClientId[client.id]) {
 | |
|       this._remoteUserByClientId[client.id].dispose();
 | |
|       delete this._remoteUserByClientId[client.id];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Adds an underline marker for the given syntax error.
 | |
|    *
 | |
|    * To clear an existing marker `null` error is also supported.
 | |
|    */
 | |
|   setCodeErrorMarker(error) {
 | |
|     this._ensureMounted();
 | |
| 
 | |
|     const owner = "livebook.error.syntax";
 | |
| 
 | |
|     if (error) {
 | |
|       const line = this.editor.getModel().getLineContent(error.line);
 | |
|       const [, leadingWhitespace, trailingWhitespace] =
 | |
|         line.match(/^(\s*).*?(\s*)$/);
 | |
| 
 | |
|       monaco.editor.setModelMarkers(this.editor.getModel(), owner, [
 | |
|         {
 | |
|           startLineNumber: error.line,
 | |
|           startColumn: leadingWhitespace.length + 1,
 | |
|           endLineNumber: error.line,
 | |
|           endColumn: line.length + 1 - trailingWhitespace.length,
 | |
|           message: error.description,
 | |
|           severity: monaco.MarkerSeverity.Error,
 | |
|         },
 | |
|       ]);
 | |
|     } else {
 | |
|       monaco.editor.setModelMarkers(this.editor.getModel(), owner, []);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _mountEditor() {
 | |
|     const settings = settingsStore.get();
 | |
| 
 | |
|     this.editor = monaco.editor.create(this.container, {
 | |
|       language: this.language,
 | |
|       value: this.source,
 | |
|       readOnly: this.readOnly,
 | |
|       scrollbar: {
 | |
|         vertical: "hidden",
 | |
|         alwaysConsumeMouseWheel: false,
 | |
|       },
 | |
|       minimap: {
 | |
|         enabled: false,
 | |
|       },
 | |
|       overviewRulerLanes: 0,
 | |
|       scrollBeyondLastLine: false,
 | |
|       guides: {
 | |
|         indentation: false,
 | |
|       },
 | |
|       occurrencesHighlight: false,
 | |
|       renderLineHighlight: "none",
 | |
|       theme: settings.editor_theme,
 | |
|       fontFamily: "JetBrains Mono, Droid Sans Mono, monospace",
 | |
|       fontSize: settings.editor_font_size,
 | |
|       tabIndex: -1,
 | |
|       tabSize: 2,
 | |
|       autoIndent: true,
 | |
|       formatOnType: true,
 | |
|       formatOnPaste: true,
 | |
|       quickSuggestions: this.intellisense && settings.editor_auto_completion,
 | |
|       tabCompletion: "on",
 | |
|       suggestSelection: "first",
 | |
|       // For Elixir word suggestions are confusing at times.
 | |
|       // For example given `defmodule<CURSOR> Foo do`, if the
 | |
|       // user opens completion list and then jumps to the end
 | |
|       // of the line we would get "defmodule" as a word completion.
 | |
|       wordBasedSuggestions: !this.intellisense,
 | |
|       parameterHints: this.intellisense && settings.editor_auto_signature,
 | |
|       wordWrap:
 | |
|         this.language === "markdown" && settings.editor_markdown_word_wrap
 | |
|           ? "on"
 | |
|           : "off",
 | |
|     });
 | |
| 
 | |
|     this.editor.addAction({
 | |
|       contextMenuGroupId: "word-wrapping",
 | |
|       id: "enable-word-wrapping",
 | |
|       label: "Enable word wrapping",
 | |
|       precondition: "config.editor.wordWrap == off",
 | |
|       keybindings: [monaco.KeyMod.Alt | monaco.KeyCode.KeyZ],
 | |
|       run: (editor) => editor.updateOptions({ wordWrap: "on" }),
 | |
|     });
 | |
| 
 | |
|     this.editor.addAction({
 | |
|       contextMenuGroupId: "word-wrapping",
 | |
|       id: "disable-word-wrapping",
 | |
|       label: "Disable word wrapping",
 | |
|       precondition: "config.editor.wordWrap == on",
 | |
|       keybindings: [monaco.KeyMod.Alt | monaco.KeyCode.KeyZ],
 | |
|       run: (editor) => editor.updateOptions({ wordWrap: "off" }),
 | |
|     });
 | |
| 
 | |
|     // Automatically adjust the editor size to fit the container.
 | |
|     const resizeObserver = new ResizeObserver((entries) => {
 | |
|       entries.forEach((entry) => {
 | |
|         // Ignore hidden container.
 | |
|         if (this.container.offsetHeight > 0) {
 | |
|           this.editor.layout();
 | |
|         }
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     resizeObserver.observe(this.container);
 | |
| 
 | |
|     // Whenever editor content size changes (new line is added/removed)
 | |
|     // update the container height. Thanks to the above observer
 | |
|     // the editor is resized to fill the container.
 | |
|     // Related: https://github.com/microsoft/monaco-editor/issues/794#issuecomment-688959283
 | |
|     this.editor.onDidContentSizeChange(() => {
 | |
|       const contentHeight = this.editor.getContentHeight();
 | |
|       this.container.style.height = `${contentHeight}px`;
 | |
|     });
 | |
| 
 | |
|     /* Overrides */
 | |
| 
 | |
|     // Move the command palette widget to overflowing widgets container,
 | |
|     // so that it's visible on small editors.
 | |
|     // See: https://github.com/microsoft/monaco-editor/issues/70
 | |
|     const commandPaletteNode = this.editor.getContribution(
 | |
|       "editor.controller.quickInput"
 | |
|     ).widget.domNode;
 | |
|     commandPaletteNode.remove();
 | |
|     this.editor._modelData.view._contentWidgets.overflowingContentWidgetsDomNode.domNode.appendChild(
 | |
|       commandPaletteNode
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Defines cell-specific providers for various editor features.
 | |
|    */
 | |
|   _setupIntellisense() {
 | |
|     const settings = settingsStore.get();
 | |
| 
 | |
|     this.handlerByRef = {};
 | |
| 
 | |
|     /**
 | |
|      * Intellisense requests such as completion or formatting are
 | |
|      * handled asynchronously by the runtime.
 | |
|      *
 | |
|      * As an example, let's go through the steps for completion:
 | |
|      *
 | |
|      *   * the user opens the completion list, which triggers the global
 | |
|      *     completion provider registered in `live_editor/monaco.js`
 | |
|      *
 | |
|      *   * the global provider delegates to the cell-specific `__getCompletionItems__`
 | |
|      *     defined below. That's a little bit hacky, but this way we make
 | |
|      *     completion cell-specific
 | |
|      *
 | |
|      *   * then `__getCompletionItems__` sends a completion request to the LV process
 | |
|      *     and gets a unique reference, under which it keeps completion callback
 | |
|      *
 | |
|      *   * finally the hook receives the "intellisense_response" event with completion
 | |
|      *     response, it looks up completion callback for the received reference and calls
 | |
|      *     it with the response, which finally returns the completion items to the editor
 | |
|      */
 | |
| 
 | |
|     this.editor.getModel().__getCompletionItems__ = (model, position) => {
 | |
|       const line = model.getLineContent(position.lineNumber);
 | |
|       const lineUntilCursor = line.slice(0, position.column - 1);
 | |
| 
 | |
|       return this._asyncIntellisenseRequest("completion", {
 | |
|         hint: lineUntilCursor,
 | |
|         editor_auto_completion: settings.editor_auto_completion,
 | |
|       })
 | |
|         .then((response) => {
 | |
|           const suggestions = completionItemsToSuggestions(
 | |
|             response.items,
 | |
|             settings
 | |
|           ).map((suggestion) => {
 | |
|             const replaceLength = replacedSuffixLength(
 | |
|               lineUntilCursor,
 | |
|               suggestion.insertText
 | |
|             );
 | |
| 
 | |
|             const range = new monaco.Range(
 | |
|               position.lineNumber,
 | |
|               position.column - replaceLength,
 | |
|               position.lineNumber,
 | |
|               position.column
 | |
|             );
 | |
| 
 | |
|             return { ...suggestion, range };
 | |
|           });
 | |
| 
 | |
|           return { suggestions };
 | |
|         })
 | |
|         .catch(() => null);
 | |
|     };
 | |
| 
 | |
|     this.editor.getModel().__getHover__ = (model, position) => {
 | |
|       // On the first hover, we setup a listener to postprocess hover
 | |
|       // content with KaTeX. Prior to that, the hover element is not
 | |
|       // in the DOM
 | |
| 
 | |
|       this.hoverContentProcessed = false;
 | |
| 
 | |
|       if (!this.hoverContentEl) {
 | |
|         this.hoverContentEl = this.container.querySelector(
 | |
|           ".monaco-hover-content"
 | |
|         );
 | |
| 
 | |
|         if (this.hoverContentEl) {
 | |
|           new MutationObserver((event) => {
 | |
|             // We mutate the DOM, so we use a flag to ignore events
 | |
|             // that we triggered ourselves
 | |
|             if (!this.hoverContentProcessed) {
 | |
|               renderMathInElement(this.hoverContentEl, {
 | |
|                 delimiters: [
 | |
|                   { left: "$$", right: "$$", display: true },
 | |
|                   { left: "$", right: "$", display: false },
 | |
|                 ],
 | |
|                 throwOnError: false,
 | |
|               });
 | |
|               this.hoverContentProcessed = true;
 | |
|             }
 | |
|           }).observe(this.hoverContentEl, { childList: true });
 | |
|         } else {
 | |
|           console.warn(
 | |
|             "Could not find an element matching .monaco-hover-content"
 | |
|           );
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       const line = model.getLineContent(position.lineNumber);
 | |
|       const column = position.column;
 | |
| 
 | |
|       return this._asyncIntellisenseRequest("details", { line, column })
 | |
|         .then((response) => {
 | |
|           const contents = response.contents.map((content) => ({
 | |
|             value: content,
 | |
|             isTrusted: true,
 | |
|           }));
 | |
| 
 | |
|           const range = new monaco.Range(
 | |
|             position.lineNumber,
 | |
|             response.range.from,
 | |
|             position.lineNumber,
 | |
|             response.range.to
 | |
|           );
 | |
| 
 | |
|           return { contents, range };
 | |
|         })
 | |
|         .catch(() => null);
 | |
|     };
 | |
| 
 | |
|     const signatureCache = {
 | |
|       codeUntilLastStop: null,
 | |
|       response: null,
 | |
|     };
 | |
| 
 | |
|     this.editor.getModel().__getSignatureHelp__ = (model, position) => {
 | |
|       const lines = model.getLinesContent();
 | |
|       const lineIdx = position.lineNumber - 1;
 | |
|       const prevLines = lines.slice(0, lineIdx);
 | |
|       const lineUntilCursor = lines[lineIdx].slice(0, position.column - 1);
 | |
|       const codeUntilCursor = [...prevLines, lineUntilCursor].join("\n");
 | |
| 
 | |
|       const codeUntilLastStop = codeUntilCursor
 | |
|         // Remove trailing characters that don't affect the signature
 | |
|         .replace(/[^(),\s]*?$/, "")
 | |
|         // Remove whitespace before delimiter
 | |
|         .replace(/([(),])\s*$/, "$1");
 | |
| 
 | |
|       // Cache subsequent requests for the same prefix, so that we don't
 | |
|       // make unnecessary requests
 | |
|       if (codeUntilLastStop === signatureCache.codeUntilLastStop) {
 | |
|         return {
 | |
|           value: signatureResponseToSignatureHelp(signatureCache.response),
 | |
|           dispose: () => {},
 | |
|         };
 | |
|       }
 | |
| 
 | |
|       return this._asyncIntellisenseRequest("signature", {
 | |
|         hint: codeUntilCursor,
 | |
|       })
 | |
|         .then((response) => {
 | |
|           signatureCache.response = response;
 | |
|           signatureCache.codeUntilLastStop = codeUntilLastStop;
 | |
| 
 | |
|           return {
 | |
|             value: signatureResponseToSignatureHelp(response),
 | |
|             dispose: () => {},
 | |
|           };
 | |
|         })
 | |
|         .catch(() => null);
 | |
|     };
 | |
| 
 | |
|     this.editor.getModel().__getDocumentFormattingEdits__ = (model) => {
 | |
|       const content = model.getValue();
 | |
| 
 | |
|       return this._asyncIntellisenseRequest("format", { code: content })
 | |
|         .then((response) => {
 | |
|           this.setCodeErrorMarker(response.code_error);
 | |
| 
 | |
|           if (response.code) {
 | |
|             /**
 | |
|              * We use a single edit replacing the whole editor content,
 | |
|              * but the editor itself optimises this into a list of edits
 | |
|              * that produce minimal diff using the Myers string difference.
 | |
|              *
 | |
|              * References:
 | |
|              *   * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/contrib/format/format.ts#L324
 | |
|              *   * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/common/services/editorSimpleWorker.ts#L489
 | |
|              *   * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/base/common/diff/diff.ts#L227-L231
 | |
|              *
 | |
|              * Eventually the editor will received the optimised list of edits,
 | |
|              * which we then convert to Delta and send to the server.
 | |
|              * Consequently, the Delta carries only the minimal formatting diff.
 | |
|              *
 | |
|              * Also, if edits are applied to the editor, either by typing
 | |
|              * or receiving remote changes, the formatting is cancelled.
 | |
|              * In other words the formatting changes are actually applied
 | |
|              * only if the editor stays intact.
 | |
|              *
 | |
|              * References:
 | |
|              *   * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/contrib/format/format.ts#L313
 | |
|              *   * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/browser/core/editorState.ts#L137
 | |
|              *   * https://github.com/microsoft/vscode/blob/628b4d46357f2420f1dbfcea499f8ff59ee2c251/src/vs/editor/contrib/format/format.ts#L326
 | |
|              */
 | |
| 
 | |
|             const replaceEdit = {
 | |
|               range: model.getFullModelRange(),
 | |
|               text: response.code,
 | |
|             };
 | |
| 
 | |
|             return [replaceEdit];
 | |
|           } else {
 | |
|             return [];
 | |
|           }
 | |
|         })
 | |
|         .catch(() => null);
 | |
|     };
 | |
| 
 | |
|     this.hook.handleEvent("intellisense_response", ({ ref, response }) => {
 | |
|       const handler = this.handlerByRef[ref];
 | |
| 
 | |
|       if (handler) {
 | |
|         handler(response);
 | |
|         delete this.handlerByRef[ref];
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Pushes an intellisense request.
 | |
|    *
 | |
|    * The returned promise is either resolved with a valid
 | |
|    * response or rejected with null.
 | |
|    */
 | |
|   _asyncIntellisenseRequest(type, props) {
 | |
|     return new Promise((resolve, reject) => {
 | |
|       this.hook.pushEvent(
 | |
|         "intellisense_request",
 | |
|         { cell_id: this.cellId, type, ...props },
 | |
|         ({ ref }) => {
 | |
|           if (ref) {
 | |
|             this.handlerByRef[ref] = (response) => {
 | |
|               if (response) {
 | |
|                 resolve(response);
 | |
|               } else {
 | |
|                 reject(null);
 | |
|               }
 | |
|             };
 | |
|           } else {
 | |
|             reject(null);
 | |
|           }
 | |
|         }
 | |
|       );
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| function completionItemsToSuggestions(items, settings) {
 | |
|   return items
 | |
|     .map((item) => parseItem(item, settings))
 | |
|     .map((suggestion, index) => ({
 | |
|       ...suggestion,
 | |
|       sortText: numberToSortableString(index, items.length),
 | |
|     }));
 | |
| }
 | |
| 
 | |
| // See `Livebook.Runtime` for completion item definition
 | |
| function parseItem(item, settings) {
 | |
|   return {
 | |
|     label: item.label,
 | |
|     kind: parseItemKind(item.kind),
 | |
|     detail: item.detail,
 | |
|     documentation: item.documentation && {
 | |
|       value: item.documentation,
 | |
|       isTrusted: true,
 | |
|     },
 | |
|     insertText: item.insert_text,
 | |
|     insertTextRules:
 | |
|       monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
 | |
|     command: settings.editor_auto_signature
 | |
|       ? {
 | |
|           title: "Trigger Parameter Hint",
 | |
|           id: "editor.action.triggerParameterHints",
 | |
|         }
 | |
|       : null,
 | |
|   };
 | |
| }
 | |
| 
 | |
| function parseItemKind(kind) {
 | |
|   switch (kind) {
 | |
|     case "function":
 | |
|       return monaco.languages.CompletionItemKind.Function;
 | |
|     case "module":
 | |
|       return monaco.languages.CompletionItemKind.Module;
 | |
|     case "struct":
 | |
|       return monaco.languages.CompletionItemKind.Struct;
 | |
|     case "interface":
 | |
|       return monaco.languages.CompletionItemKind.Interface;
 | |
|     case "type":
 | |
|       return monaco.languages.CompletionItemKind.Class;
 | |
|     case "variable":
 | |
|       return monaco.languages.CompletionItemKind.Variable;
 | |
|     case "field":
 | |
|       return monaco.languages.CompletionItemKind.Field;
 | |
|     case "keyword":
 | |
|       return monaco.languages.CompletionItemKind.Keyword;
 | |
|     default:
 | |
|       return null;
 | |
|   }
 | |
| }
 | |
| 
 | |
| function numberToSortableString(number, maxNumber) {
 | |
|   return String(number).padStart(maxNumber, "0");
 | |
| }
 | |
| 
 | |
| function signatureResponseToSignatureHelp(response) {
 | |
|   return {
 | |
|     activeSignature: 0,
 | |
|     activeParameter: response.active_argument,
 | |
|     signatures: response.signature_items.map((signature_item) => ({
 | |
|       label: signature_item.signature,
 | |
|       parameters: signature_item.arguments.map((argument) => ({
 | |
|         label: argument,
 | |
|       })),
 | |
|       documentation: null,
 | |
|     })),
 | |
|   };
 | |
| }
 | |
| 
 | |
| export default LiveEditor;
 |